From 1df21869c52979acf0010e982edc4cd5e5d3685d Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 30 Jul 2026 15:40:05 -0500 Subject: [PATCH] Prepare for HACS custom wake words --- .../components/esphome/assist_satellite.py | 25 +++++++++++++------ .../esphome/test_assist_satellite.py | 4 +-- .../choo_choo_homie.json | 0 .../choo_choo_homie.tflite | 0 4 files changed, 20 insertions(+), 9 deletions(-) rename tests/testing_config/custom_wake_words/{ => choo_choo_homie}/choo_choo_homie.json (100%) rename tests/testing_config/custom_wake_words/{ => choo_choo_homie}/choo_choo_homie.tflite (100%) diff --git a/homeassistant/components/esphome/assist_satellite.py b/homeassistant/components/esphome/assist_satellite.py index 0182cacf9cfb..f766e272b296 100644 --- a/homeassistant/components/esphome/assist_satellite.py +++ b/homeassistant/components/esphome/assist_satellite.py @@ -904,13 +904,13 @@ def _get_custom_wake_words( wake_words: dict[str, VoiceAssistantExternalWakeWord] = {} # Look for config/model files - for config_path in wake_words_dir.glob("*.json"): - wake_word_id = config_path.stem - model_path = config_path.with_suffix(".tflite") - if not model_path.exists(): - # Missing model file - continue + for config_path in wake_words_dir.rglob("*.json"): + # Use relative path to deconflict ids: + # custom_wake_words/my_wake_word.json -> my_wake_word + # custom_wake_words/sub_dir/my_wake_word.json -> sub_dir/my_wake_word + wake_word_id = str(config_path.relative_to(wake_words_dir).with_suffix("")) + # Inspect config file and load model with open(config_path, encoding="utf-8") as config_file: config_dict = json.load(config_file) try: @@ -924,6 +924,16 @@ def _get_custom_wake_words( ) continue + model_path = config_path.parent / config["model"] + if not model_path.exists(): + # Missing model file + _LOGGER.debug( + "Missing custom wake word model file: %s (config=%s)", + model_path, + config_path, + ) + continue + with open(model_path, "rb") as model_file: model_hash = hashlib.sha256(model_file.read()).hexdigest() @@ -933,10 +943,11 @@ def _get_custom_wake_words( # Only intended for the internal network base_url = get_url(hass, prefer_external=False, allow_cloud=False) + wake_word = config["wake_word"] wake_words[wake_word_id] = VoiceAssistantExternalWakeWord.from_dict( { "id": wake_word_id, - "wake_word": config["wake_word"], + "wake_word": wake_word, "trained_languages": config_dict.get("trained_languages", []), "model_type": config["type"], "model_size": model_size, diff --git a/tests/components/esphome/test_assist_satellite.py b/tests/components/esphome/test_assist_satellite.py index c83fc9d55b3b..c57c5df87596 100644 --- a/tests/components/esphome/test_assist_satellite.py +++ b/tests/components/esphome/test_assist_satellite.py @@ -2229,7 +2229,7 @@ async def test_custom_wake_words( Expects 2 models in testing_config/custom_wake_words: - hey_home_assistant - - choo_choo_homie + - choo_choo_homie (in choo_choo_homie sub-directory) """ http_client = await hass_client() expected_config = AssistSatelliteConfiguration( @@ -2261,7 +2261,7 @@ async def test_custom_wake_words( assert {external_wake_words[0].id, external_wake_words[1].id} == { "hey_home_assistant", - "choo_choo_homie", + "choo_choo_homie/choo_choo_homie", } # Verify details diff --git a/tests/testing_config/custom_wake_words/choo_choo_homie.json b/tests/testing_config/custom_wake_words/choo_choo_homie/choo_choo_homie.json similarity index 100% rename from tests/testing_config/custom_wake_words/choo_choo_homie.json rename to tests/testing_config/custom_wake_words/choo_choo_homie/choo_choo_homie.json diff --git a/tests/testing_config/custom_wake_words/choo_choo_homie.tflite b/tests/testing_config/custom_wake_words/choo_choo_homie/choo_choo_homie.tflite similarity index 100% rename from tests/testing_config/custom_wake_words/choo_choo_homie.tflite rename to tests/testing_config/custom_wake_words/choo_choo_homie/choo_choo_homie.tflite