From c7fdc0411c6eff46aea310019723a3fccf1c8e70 Mon Sep 17 00:00:00 2001 From: brandon Date: Fri, 31 Oct 2025 23:46:36 -0400 Subject: [PATCH 1/6] adding support for openwakeword as a wake word engine. Adds a toggle in the home assistant integration section to switch between micro and openwakeword --- home-assistant-voice.yaml | 134 +++++++++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 24 deletions(-) diff --git a/home-assistant-voice.yaml b/home-assistant-voice.yaml index ad20e497..b581b082 100644 --- a/home-assistant-voice.yaml +++ b/home-assistant-voice.yaml @@ -1,4 +1,7 @@ substitutions: + name: home-assistant-voice-0a38ec + wifi_ssid: !secret wifi_ssid + wifi_password: !secret wifi_password # Phases of the Voice Assistant # The voice assistant is ready to be triggered by a wake word voice_assist_idle_phase_id: '1' @@ -76,12 +79,29 @@ esp32: wifi: id: wifi_id - fast_connect: ${hidden_ssid} + fast_connect: false on_connect: - - lambda: id(improv_ble_in_progress) = false; - - script.execute: control_leds + then: + - lambda: |- + id(improv_ble_in_progress) = false; + - script.execute: + id: control_leds on_disconnect: - - script.execute: control_leds + then: + - script.execute: + id: control_leds + domain: .local + reboot_timeout: 15min + power_save_mode: LIGHT + enable_btm: false + enable_rrm: false + passive_scan: false + enable_on_boot: true + networks: + - ssid: ${wifi_ssid} + password: ${wifi_password} + priority: 0.0 + use_address: ${name}.local network: enable_ipv6: true @@ -97,7 +117,6 @@ api: - script.execute: control_leds on_client_disconnected: - script.execute: control_leds - encryption: # Uses key set by Home Assistant ota: - platform: esphome @@ -167,6 +186,10 @@ globals: type: bool restore_value: no initial_value: 'false' + - id: wake_word_engine + type: std::string + restore_value: true + initial_value: '"micro_wake_word"' switch: # This is the master mute switch. It is exposed to Home Assistant. The user can only turn it on and off if the hardware switch is off. (The hardware switch overrides the software one) @@ -324,7 +347,15 @@ binary_sensor: priority: true sound_file: !lambda return id(center_button_press_sound); - delay: 300ms - - voice_assistant.start: + # Start based on selected engine + - if: + condition: + lambda: return id(wake_word_engine) == "open_wake_word"; + then: + - voice_assistant.start: + else: + - voice_assistant.start: + wake_word: "button_press" # Double Click # . Exposed as an event entity. To be used in automations inside Home Assistant - timing: @@ -1671,6 +1702,40 @@ select: id(hey_jarvis).set_probability_cutoff(212); // 0.83 -> 1.502 FAPH on DipCo id(hey_mycroft).set_probability_cutoff(237); // 0.93 -> 1.878 FAPH on DipCo } + - platform: template + name: "Wake Word Engine" + id: wake_word_engine_select + options: + - "micro_wake_word" + - "open_wake_word" + restore_value: true + initial_option: "micro_wake_word" + entity_category: config + optimistic: true + on_value: + then: + - lambda: |- + id(wake_word_engine) = x; + - logger.log: + format: "Wake word engine changed to: %s" + args: [x.c_str()] + level: INFO + # Stop both engines + - if: + condition: + lambda: return id(mww).is_running(); + then: + - micro_wake_word.stop: + - voice_assistant.stop: + - delay: 500ms + # Start the selected engine + - if: + condition: + lambda: return x == "micro_wake_word"; + then: + - micro_wake_word.start: + else: + - voice_assistant.start_continuous: voice_assistant: id: va @@ -1679,22 +1744,30 @@ voice_assistant: channels: 0 media_player: external_media_player micro_wake_word: mww - use_wake_word: false + use_wake_word: true noise_suppression_level: 0 auto_gain: 0 dbfs volume_multiplier: 1 + on_client_connected: - lambda: id(init_in_progress) = false; - - micro_wake_word.start: - - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - script.execute: control_leds + - if: + condition: + lambda: return id(wake_word_engine) == "micro_wake_word"; + then: + - micro_wake_word.start: + else: + - voice_assistant.start_continuous: + - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; + on_client_disconnected: - voice_assistant.stop: + - micro_wake_word.stop: - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; - script.execute: control_leds + on_error: - # Only set the error phase if the error code is different than duplicate_wake_up_detected or stt-no-text-recognized - # These two are ignored for a better user experience - if: condition: and: @@ -1704,7 +1777,6 @@ voice_assistant: then: - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; - script.execute: control_leds - # If the error code is cloud-auth-failed, serve a local audio file guiding the user. - if: condition: - lambda: return code == "cloud-auth-failed"; @@ -1713,60 +1785,64 @@ voice_assistant: id: play_sound priority: true sound_file: !lambda return id(error_cloud_expired); - # When the voice assistant starts: Play a wake up sound, duck audio. + on_start: - mixer_speaker.apply_ducking: id: media_mixing_input - decibel_reduction: 20 # Number of dB quieter; higher implies more quiet, 0 implies full volume - duration: 0.0s # The duration of the transition (default is no transition) + decibel_reduction: 20 + duration: 0.0s + on_listening: - lambda: id(voice_assistant_phase) = ${voice_assist_waiting_for_command_phase_id}; - script.execute: control_leds + on_stt_vad_start: - lambda: id(voice_assistant_phase) = ${voice_assist_listening_for_command_phase_id}; - script.execute: control_leds + on_stt_vad_end: - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; - script.execute: control_leds + on_intent_progress: - if: condition: - # A nonempty x variable means a streaming TTS url was sent to the media player lambda: 'return !x.empty();' then: - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; - script.execute: control_leds - # Start a script that would potentially enable the stop word if the response is longer than a second - script.execute: activate_stop_word_once + on_tts_start: - if: condition: - # The intent_progress trigger didn't start the TTS Reponse lambda: 'return id(voice_assistant_phase) != ${voice_assist_replying_phase_id};' then: - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; - script.execute: control_leds - # Start a script that would potentially enable the stop word if the response is longer than a second - script.execute: activate_stop_word_once - # When the voice assistant ends ... + + # ✅ New section: reset LEDs after TTS playback ends + on_tts_end: + - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; + - script.execute: control_leds + on_end: - wait_until: not: voice_assistant.is_running: - # Stop ducking audio. - mixer_speaker.apply_ducking: id: media_mixing_input decibel_reduction: 0 duration: 1.0s - # If the end happened because of an error, let the error phase on for a second - if: condition: lambda: return id(voice_assistant_phase) == ${voice_assist_error_phase_id}; then: - delay: 1s - # Reset the voice assistant phase id and reset the LED animations. - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - script.execute: control_leds + on_timer_finished: - switch.turn_on: timer_ringing on_timer_started: @@ -1777,6 +1853,16 @@ voice_assistant: - script.execute: control_leds on_timer_tick: - script.execute: control_leds + + on_wake_word_detected: + - if: + condition: + switch.is_on: wake_sound + then: + - script.execute: + id: play_sound + priority: false + sound_file: !lambda return id(wake_word_triggered_sound); button: - platform: factory_reset @@ -1792,4 +1878,4 @@ button: icon: "mdi:restart" debug: - update_interval: 5s + update_interval: 5s \ No newline at end of file From 58df37997992735be05312f977117e036f9601ac Mon Sep 17 00:00:00 2001 From: bmcwilliams96 <35578248+bmcwilliams96@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:54:32 -0400 Subject: [PATCH 2/6] Refactor WiFi settings in home-assistant-voice.yaml --- home-assistant-voice.yaml | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/home-assistant-voice.yaml b/home-assistant-voice.yaml index b581b082..da0e2c7d 100644 --- a/home-assistant-voice.yaml +++ b/home-assistant-voice.yaml @@ -1,7 +1,4 @@ substitutions: - name: home-assistant-voice-0a38ec - wifi_ssid: !secret wifi_ssid - wifi_password: !secret wifi_password # Phases of the Voice Assistant # The voice assistant is ready to be triggered by a wake word voice_assist_idle_phase_id: '1' @@ -79,29 +76,12 @@ esp32: wifi: id: wifi_id - fast_connect: false + fast_connect: ${hidden_ssid} on_connect: - then: - - lambda: |- - id(improv_ble_in_progress) = false; - - script.execute: - id: control_leds + - lambda: id(improv_ble_in_progress) = false; + - script.execute: control_leds on_disconnect: - then: - - script.execute: - id: control_leds - domain: .local - reboot_timeout: 15min - power_save_mode: LIGHT - enable_btm: false - enable_rrm: false - passive_scan: false - enable_on_boot: true - networks: - - ssid: ${wifi_ssid} - password: ${wifi_password} - priority: 0.0 - use_address: ${name}.local + - script.execute: control_leds network: enable_ipv6: true @@ -117,6 +97,7 @@ api: - script.execute: control_leds on_client_disconnected: - script.execute: control_leds + encryption: # Uses key set by Home Assistant ota: - platform: esphome @@ -1878,4 +1859,4 @@ button: icon: "mdi:restart" debug: - update_interval: 5s \ No newline at end of file + update_interval: 5s From cc9507a1529b5c680cabb2ff7b9b3714a37bc2f3 Mon Sep 17 00:00:00 2001 From: bmcwilliams96 <35578248+bmcwilliams96@users.noreply.github.com> Date: Sat, 1 Nov 2025 00:04:03 -0400 Subject: [PATCH 3/6] whitespace --- home-assistant-voice.yaml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/home-assistant-voice.yaml b/home-assistant-voice.yaml index da0e2c7d..3bf39a41 100644 --- a/home-assistant-voice.yaml +++ b/home-assistant-voice.yaml @@ -1741,13 +1741,11 @@ voice_assistant: else: - voice_assistant.start_continuous: - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - on_client_disconnected: - voice_assistant.stop: - micro_wake_word.stop: - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; - script.execute: control_leds - on_error: - if: condition: @@ -1766,25 +1764,20 @@ voice_assistant: id: play_sound priority: true sound_file: !lambda return id(error_cloud_expired); - on_start: - mixer_speaker.apply_ducking: id: media_mixing_input decibel_reduction: 20 duration: 0.0s - on_listening: - lambda: id(voice_assistant_phase) = ${voice_assist_waiting_for_command_phase_id}; - script.execute: control_leds - on_stt_vad_start: - lambda: id(voice_assistant_phase) = ${voice_assist_listening_for_command_phase_id}; - script.execute: control_leds - on_stt_vad_end: - lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id}; - script.execute: control_leds - on_intent_progress: - if: condition: @@ -1793,7 +1786,6 @@ voice_assistant: - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; - script.execute: control_leds - script.execute: activate_stop_word_once - on_tts_start: - if: condition: @@ -1802,8 +1794,6 @@ voice_assistant: - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; - script.execute: control_leds - script.execute: activate_stop_word_once - - # ✅ New section: reset LEDs after TTS playback ends on_tts_end: - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - script.execute: control_leds @@ -1834,7 +1824,6 @@ voice_assistant: - script.execute: control_leds on_timer_tick: - script.execute: control_leds - on_wake_word_detected: - if: condition: @@ -1844,7 +1833,6 @@ voice_assistant: id: play_sound priority: false sound_file: !lambda return id(wake_word_triggered_sound); - button: - platform: factory_reset id: factory_reset_button From b14a11d0998fc850d8af2826c10e6495c4750e92 Mon Sep 17 00:00:00 2001 From: bmcwilliams96 <35578248+bmcwilliams96@users.noreply.github.com> Date: Sat, 1 Nov 2025 00:18:45 -0400 Subject: [PATCH 4/6] adding back comments and taking out more whitespace --- home-assistant-voice.yaml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/home-assistant-voice.yaml b/home-assistant-voice.yaml index 3bf39a41..7f2eeb10 100644 --- a/home-assistant-voice.yaml +++ b/home-assistant-voice.yaml @@ -1729,7 +1729,6 @@ voice_assistant: noise_suppression_level: 0 auto_gain: 0 dbfs volume_multiplier: 1 - on_client_connected: - lambda: id(init_in_progress) = false; - script.execute: control_leds @@ -1747,6 +1746,8 @@ voice_assistant: - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id}; - script.execute: control_leds on_error: + # Only set the error phase if the error code is different than duplicate_wake_up_detected or stt-no-text-recognized + # These two are ignored for a better user experience - if: condition: and: @@ -1756,6 +1757,7 @@ voice_assistant: then: - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; - script.execute: control_leds + # If the error code is cloud-auth-failed, serve a local audio file guiding the user. - if: condition: - lambda: return code == "cloud-auth-failed"; @@ -1764,11 +1766,12 @@ voice_assistant: id: play_sound priority: true sound_file: !lambda return id(error_cloud_expired); + # When the voice assistant starts: Play a wake up sound, duck audio. on_start: - mixer_speaker.apply_ducking: id: media_mixing_input - decibel_reduction: 20 - duration: 0.0s + decibel_reduction: 20 # Number of dB quieter; higher implies more quiet, 0 implies full volume + duration: 0.0s # The duration of the transition (default is no transition) on_listening: - lambda: id(voice_assistant_phase) = ${voice_assist_waiting_for_command_phase_id}; - script.execute: control_leds @@ -1781,10 +1784,12 @@ voice_assistant: on_intent_progress: - if: condition: + # A nonempty x variable means a streaming TTS url was sent to the media player lambda: 'return !x.empty();' then: - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; - script.execute: control_leds + # Start a script that would potentially enable the stop word if the response is longer than a second - script.execute: activate_stop_word_once on_tts_start: - if: @@ -1797,23 +1802,24 @@ voice_assistant: on_tts_end: - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - script.execute: control_leds - on_end: - wait_until: not: voice_assistant.is_running: + # Stop ducking audio. - mixer_speaker.apply_ducking: id: media_mixing_input decibel_reduction: 0 duration: 1.0s + # If the end happened because of an error, let the error phase on for a second - if: condition: lambda: return id(voice_assistant_phase) == ${voice_assist_error_phase_id}; then: - delay: 1s + # Reset the voice assistant phase id and reset the LED animations. - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - script.execute: control_leds - on_timer_finished: - switch.turn_on: timer_ringing on_timer_started: From 3f453ac31839eb78e3d24d971c587d54e234470b Mon Sep 17 00:00:00 2001 From: bmcwilliams96 <35578248+bmcwilliams96@users.noreply.github.com> Date: Sat, 1 Nov 2025 00:25:57 -0400 Subject: [PATCH 5/6] whitespace and comments --- home-assistant-voice.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/home-assistant-voice.yaml b/home-assistant-voice.yaml index 7f2eeb10..7cdcf58f 100644 --- a/home-assistant-voice.yaml +++ b/home-assistant-voice.yaml @@ -1770,8 +1770,8 @@ voice_assistant: on_start: - mixer_speaker.apply_ducking: id: media_mixing_input - decibel_reduction: 20 # Number of dB quieter; higher implies more quiet, 0 implies full volume - duration: 0.0s # The duration of the transition (default is no transition) + decibel_reduction: 20 # Number of dB quieter; higher implies more quiet, 0 implies full volume + duration: 0.0s # The duration of the transition (default is no transition) on_listening: - lambda: id(voice_assistant_phase) = ${voice_assist_waiting_for_command_phase_id}; - script.execute: control_leds @@ -1794,11 +1794,14 @@ voice_assistant: on_tts_start: - if: condition: + # The intent_progress trigger didn't start the TTS Reponse lambda: 'return id(voice_assistant_phase) != ${voice_assist_replying_phase_id};' then: - lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id}; - script.execute: control_leds + # Start a script that would potentially enable the stop word if the response is longer than a second - script.execute: activate_stop_word_once + # When the voice assistant ends ... on_tts_end: - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - script.execute: control_leds From b2b9890ed2b4b8de508b648dd9309ea716bde49a Mon Sep 17 00:00:00 2001 From: bmcwilliams96 <35578248+bmcwilliams96@users.noreply.github.com> Date: Sat, 1 Nov 2025 00:28:05 -0400 Subject: [PATCH 6/6] we love yaml --- home-assistant-voice.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/home-assistant-voice.yaml b/home-assistant-voice.yaml index 7cdcf58f..a639ee12 100644 --- a/home-assistant-voice.yaml +++ b/home-assistant-voice.yaml @@ -1757,7 +1757,7 @@ voice_assistant: then: - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id}; - script.execute: control_leds - # If the error code is cloud-auth-failed, serve a local audio file guiding the user. + # If the error code is cloud-auth-failed, serve a local audio file guiding the user. - if: condition: - lambda: return code == "cloud-auth-failed";