From 757f7aee4f94538ae798f9cce86b4a20302d46cc Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 10 Aug 2026 07:49:08 -0400 Subject: [PATCH] [micro_wake_word] Own the enabled-state preference key voice_assistant derived the same fnv1_hash(id) key independently to restore downloaded models, with nothing tying the two together. Expose the derivation as WakeWordModel::enabled_preference_key so the component that owns the persistence format owns the key too. --- esphome/components/micro_wake_word/streaming_model.cpp | 4 ++-- esphome/components/micro_wake_word/streaming_model.h | 7 +++++++ esphome/components/voice_assistant/voice_assistant.cpp | 6 +++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 72984f04fb..faf5a6fe9a 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -270,7 +270,7 @@ WakeWordModel::WakeWordModel(const std::string &id, const uint8_t *model_start, this->current_stride_step_ = 0; this->internal_only_ = internal_only; - this->pref_ = global_preferences->make_preference(fnv1_hash(id)); + this->pref_ = global_preferences->make_preference(WakeWordModel::enabled_preference_key(id)); bool enabled; if (this->pref_.load(&enabled)) { // Use the enabled state loaded from flash @@ -305,7 +305,7 @@ WakeWordModel::WakeWordModel(const std::string &id, std::shared_ptr m this->current_stride_step_ = 0; this->internal_only_ = false; // Runtime models are always exposed to Home Assistant - this->pref_ = global_preferences->make_preference(fnv1_hash(id)); + this->pref_ = global_preferences->make_preference(WakeWordModel::enabled_preference_key(id)); bool enabled; if (this->pref_.load(&enabled)) { // Use the enabled state loaded from flash diff --git a/esphome/components/micro_wake_word/streaming_model.h b/esphome/components/micro_wake_word/streaming_model.h index 1cb9d6eba5..de918f0994 100644 --- a/esphome/components/micro_wake_word/streaming_model.h +++ b/esphome/components/micro_wake_word/streaming_model.h @@ -157,6 +157,13 @@ class WakeWordModel final : public StreamingModel { bool get_internal_only() { return this->internal_only_; } + /// @brief Derives the preference key holding a model's enabled state. This component owns the persistence + /// format, so anything reading or writing that state (voice_assistant restores downloaded models from it) + /// must derive the key through here rather than repeating the hash. + /// @param id (std::string) identifier for the model + /// @return The preference key for the model's enabled state + static uint32_t enabled_preference_key(const std::string &id) { return fnv1_hash(id); } + protected: // Kept for runtime-downloaded models so the model buffer stays alive for the model's lifetime. // Null for compiled-in models (their data lives in flash). diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 6052d64190..3541e0697e 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -1309,8 +1309,8 @@ void VoiceAssistant::restore_runtime_models_() { // Only re-download models the user had enabled before the reboot. Read the key directly: make_preference // allocates a backend that is never freed, and this runs for every advertised model on every request. bool enabled = false; - if (global_preferences->load_from_key(fnv1_hash(cached_ww.id), reinterpret_cast(&enabled), - sizeof(enabled)) && + if (global_preferences->load_from_key(micro_wake_word::WakeWordModel::enabled_preference_key(cached_ww.id), + reinterpret_cast(&enabled), sizeof(enabled)) && enabled) { ESP_LOGD(TAG, "Restoring runtime model %s", cached_ww.id.c_str()); this->model_download_queue_.push_back(cached_ww); @@ -1345,7 +1345,7 @@ void VoiceAssistant::erase_pending_wake_word_(const std::string &id) { void VoiceAssistant::mark_model_load_failed_(const std::string &id) { // Persist disabled so a broken model isn't retried on every boot, and drop the optimistic active entry so // HA sees the real (inactive) state. - auto pref = global_preferences->make_preference(fnv1_hash(id)); + auto pref = global_preferences->make_preference(micro_wake_word::WakeWordModel::enabled_preference_key(id)); bool enabled = false; pref.save(&enabled); this->erase_pending_wake_word_(id);