substitutions: # Phases of the Voice Assistant # The voice assistant is ready to be triggered by a wake word voice_assist_idle_phase_id: '1' # The voice assistant is waiting for a voice command (after being triggered by the wake word) voice_assist_waiting_for_command_phase_id: '2' # The voice assistant is listening for a voice command voice_assist_listening_for_command_phase_id: '3' # The voice assistant is currently processing the command voice_assist_thinking_phase_id: '4' # The voice assistant is replying to the command voice_assist_replying_phase_id: '5' # The voice assistant is not ready voice_assist_not_ready_phase_id: '10' # The voice assistant encountered an error voice_assist_error_phase_id: '11' # Change this to true in case you ahve a hidden SSID at home. hidden_ssid: "false" # Substitutions for audio files mute_switch_on_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/mute_switch_on.flac mute_switch_off_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/mute_switch_off.flac timer_finished_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac wake_word_triggered_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/wake_word_triggered.flac center_button_press_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_press.flac center_button_double_press_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_double_press.flac center_button_triple_press_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_triple_press.flac center_button_long_press_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/center_button_long_press.flac factory_reset_initiated_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_initiated.mp3 factory_reset_cancelled_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_cancelled.mp3 factory_reset_confirmed_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/factory_reset_confirmed.mp3 error_cloud_expired_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/error_cloud_expired.mp3 esphome: name: respeaker-xvf3800-assistant friendly_name: reSpeaker XVF3800 Assistant project: name: formatbce.Respeaker XVF3800 Satellite version: 2026.2.1 min_version: 2026.2.0 on_boot: - priority: 375 then: - sensor.template.publish: id: next_timer state: -1 # Run the script to refresh the LED status - script.execute: control_leds # If after 10 minutes, the device is still initializing (It did not yet connect to Home Assistant), turn off the init_in_progress variable and run the script to refresh the LED status - delay: 10min - if: condition: lambda: return id(init_in_progress); then: - lambda: id(init_in_progress) = false; - script.execute: control_leds - priority: -100 then: - lambda: |- id(alarm_time).publish_state(id(saved_alarm_time)); - lambda: |- auto call = id(alarm_action).make_call(); call.set_option(id(saved_alarm_action)); call.perform(); - lambda: |- setenv("TZ", id(saved_time_zone).c_str(), 1); tzset(); esp32: board: esp32-s3-devkitc-1 cpu_frequency: 240MHz variant: esp32s3 flash_size: 8MB framework: type: esp-idf version: recommended sdkconfig_options: CONFIG_ESP32S3_DATA_CACHE_64KB: "y" CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y" # Moves instructions and read only data from flash into PSRAM on boot. # Both enabled allows instructions to execute while a flash operation is in progress without needing to be placed in IRAM. # Considerably speeds up mWW at the cost of using more PSRAM. CONFIG_SPIRAM_RODATA: "y" CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y" CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y" CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y" CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC: "y" CONFIG_MBEDTLS_SSL_PROTO_TLS1_3: "y" # TLS1.3 support isn't enabled by default in IDF 5.1.5 wifi: id: wifi_id fast_connect: ${hidden_ssid} ssid: !secret wifi_ssid password: !secret wifi_password # Add power management and connection optimizations power_save_mode: none # Disable power saving for better performance reboot_timeout: 10min # Prevent unnecessary reboots on_connect: - lambda: id(improv_ble_in_progress) = false; - script.execute: control_leds on_disconnect: - script.execute: control_leds network: enable_ipv6: true logger: level: INFO initial_level: INFO logs: sensor: WARN voice_assistant: INFO micro_wake_word: INFO i2s_audio: WARN respeaker_xvf3800: INFO api: id: api_id actions: - action: set_led_color variables: red: float green: float blue: float then: - lambda: |- id(user_led_ring_color_r) = std::min(255.0f, std::max(0.0f, red)); id(user_led_ring_color_g) = std::min(255.0f, std::max(0.0f, green)); id(user_led_ring_color_b) = std::min(255.0f, std::max(0.0f, blue)); - select.set: id: user_led_color_preset option: "Custom" - action: start_va then: - voice_assistant.start - action: stop_va then: - voice_assistant.stop - action: set_alarm_time variables: alarm_time_hh_mm: string then: - lambda: |- if (alarm_time_hh_mm.length() == 5 && isdigit(alarm_time_hh_mm[0]) && isdigit(alarm_time_hh_mm[1]) && isdigit(alarm_time_hh_mm[3]) && isdigit(alarm_time_hh_mm[4])) { id(alarm_time).publish_state(alarm_time_hh_mm); id(saved_alarm_time) = alarm_time_hh_mm; } - action: set_time_zone variables: posix_time_zone: string then: - lambda: |- setenv("TZ", posix_time_zone.c_str(), 1); tzset(); id(saved_time_zone) = posix_time_zone; id(publish_current_time).execute(); on_client_connected: - script.execute: control_leds on_client_disconnected: - script.execute: control_leds http_request: # Uncomment this, if you have problems with text-to-speech because of Home Assistant HTTPS internal URL # verify_ssl: false ota: - platform: esphome id: ota_esphome password: !secret ota_password i2c: - id: internal_i2c sda: GPIO5 scl: GPIO6 scan: true frequency: 100kHz psram: mode: octal speed: 80MHz ignore_not_found: false # The VPE has PSRAM, so this is safe. Allows configuring WiFi driver to use more resources (done automatically by the speaker media player) globals: - id: user_led_ring_color_r type: float restore_value: yes initial_value: '255.0' - id: user_led_ring_color_g type: float restore_value: yes initial_value: '0.0' - id: user_led_ring_color_b type: float restore_value: yes initial_value: '255.0' - id: effect_color_r type: float restore_value: no initial_value: '255.0' - id: effect_color_g type: float restore_value: no initial_value: '0.0' - id: effect_color_b type: float restore_value: no initial_value: '255.0' - id: effect_brightness type: float restore_value: no initial_value: '1.0' - id: effect_speed type: float restore_value: yes initial_value: "0.05" # effects cycle per second (adjust as needed) - id: init_in_progress type: bool restore_value: no initial_value: 'true' # Global variable storing the state of ImprovBLE. Used to draw different LED animations - id: improv_ble_in_progress type: bool restore_value: no initial_value: 'false' # Global variable tracking the phase of the voice assistant (defined above). Initialized to not_ready - id: voice_assistant_phase type: int restore_value: no initial_value: ${voice_assist_not_ready_phase_id} - id: saved_alarm_time type: std::string restore_value: yes initial_value: '"Unknown"' - id: saved_time_zone type: std::string restore_value: yes initial_value: '"UTC0"' - id: saved_alarm_action type: std::string restore_value: yes initial_value: '"Play sound"' # Global variable storing the first active timer - id: first_active_timer type: voice_assistant::Timer restore_value: no # Global variable storing if a timer is active - id: is_timer_active type: bool restore_value: no # Global variable storing if a factory reset was requested. If it is set to true, the device will factory reset once the center button is released - id: factory_reset_requested type: bool restore_value: no initial_value: 'false' - id: current_led_effect type: std::string restore_value: no initial_value: '"off"' - id: volume_display_active type: bool restore_value: no initial_value: 'false' - id: last_led_update_time type: uint32_t restore_value: no initial_value: '0' # Global variable to store the current animated position for the beam effect - id: animated_beam_position type: float restore_value: no initial_value: '0.0' # Time sync from Home Assistant time: - platform: homeassistant id: homeassistant_time on_time: # Every 1 minute - seconds: 0 minutes: /1 then: - script.execute: check_alarm on_time_sync: - script.execute: publish_current_time switch: # Mute Sound Switch. - platform: template id: mute_sound name: Mute-unmute sound icon: "mdi:bullhorn" entity_category: config optimistic: true restore_mode: RESTORE_DEFAULT_ON # Wake Word Sound Switch. - platform: template id: wake_sound name: Wake sound icon: "mdi:bullhorn" entity_category: config optimistic: true restore_mode: RESTORE_DEFAULT_ON # Internal switch to track when a timer is ringing on the device. - platform: template id: timer_ringing optimistic: true internal: true restore_mode: ALWAYS_OFF on_turn_off: # Disable stop wake word - micro_wake_word.disable_model: stop - script.execute: disable_repeat # Stop any current announcement (ie: stop the timer ring mid playback) - if: condition: media_player.is_announcing: id: external_media_player then: media_player.stop: announcement: true id: external_media_player # Set back ducking ratio to zero - mixer_speaker.apply_ducking: id: media_mixing_input decibel_reduction: 0 duration: 1.0s # Refresh the LED ring - script.execute: control_leds on_turn_on: # Duck audio - mixer_speaker.apply_ducking: id: media_mixing_input decibel_reduction: 20 duration: 0.0s # Enable stop wake word - micro_wake_word.enable_model: stop # Ring timer - script.execute: ring_timer # Refresh LED - script.execute: control_leds # If 15 minutes have passed and the timer is still ringing, stop it. - delay: 15min - switch.turn_off: timer_ringing # Defines if alarm is active - platform: template optimistic: true restore_mode: RESTORE_DEFAULT_OFF id: alarm_on icon: mdi:bell-badge name: "Alarm on" binary_sensor: number: - platform: template id: user_led_ring_brightness name: "LED Ring Brightness" icon: mdi:brightness-6 entity_category: config optimistic: true restore_value: true min_value: 0.4 max_value: 1.0 step: 0.05 initial_value: 0.8 mode: slider sensor: - platform: template id: next_timer name: "Next timer" update_interval: never disabled_by_default: true device_class: duration unit_of_measurement: s icon: "mdi:timer" accuracy_decimals: 0 text_sensor: - platform: template id: next_timer_name name: "Next timer name" icon: "mdi:timer" disabled_by_default: true - platform: template name: "Alarm time" id: alarm_time icon: mdi:bell-ring - platform: template name: "Current device time" id: current_time icon: mdi:clock interval: - interval: 50ms id: led_animation_interval then: - lambda: |- if (id(volume_display_active)) { id(update_volume_display_effect).execute(); return; } std::string effect = id(current_led_effect); if (effect == "off") { return; } else if (effect == "breathe") { id(update_breathe_effect).execute(); } else if (effect == "rainbow") { id(update_rainbow_effect).execute(); } else if (effect == "comet_cw") { id(update_comet_cw_effect).execute(); } else if (effect == "comet_ccw") { id(update_comet_ccw_effect).execute(); } else if (effect == "twinkle") { id(update_twinkle_effect).execute(); } else if (effect == "timer_tick") { id(update_timer_tick_effect).execute(); } else if (effect == "led_beam") { id(update_led_beam_effect).execute(); } script: # ========================================================================= # == Centralized script to control all LED effects == # ========================================================================= - id: led_set_effect mode: restart parameters: effect: std::string r: float g: float b: float speed: float brightness: float then: - lambda: |- // Update global variables with the new parameters id(effect_color_r) = r; id(effect_color_g) = g; id(effect_color_b) = b; id(effect_speed) = speed; id(effect_brightness) = brightness; id(current_led_effect) = effect; // Handle the two types of effects: Off and Animated if (effect == "off") { uint32_t colors[12] = {0}; id(respeaker).set_led_ring(colors); } else { id(last_led_update_time) = millis(); // Reset timer for smooth animation start } # Individual update scripts for each animated effect - id: update_breathe_effect then: - lambda: |- static float phase = 0.0f; uint32_t now = millis(); float dt = (now - id(last_led_update_time)) / 1000.0f; id(last_led_update_time) = now; phase += dt * id(effect_speed); while (phase >= 1.0f) phase -= 1.0f; float master_brightness = id(user_led_ring_brightness).state * id(effect_brightness); float breath_brightness = 0.5f * (1.0f + sinf(phase * 2.0f * M_PI)) * master_brightness; uint8_t r = (uint8_t)(id(effect_color_r) * breath_brightness); uint8_t g = (uint8_t)(id(effect_color_g) * breath_brightness); uint8_t b = (uint8_t)(id(effect_color_b) * breath_brightness); uint32_t current_color = (r << 16) | (g << 8) | b; uint32_t colors[12]; for (int i = 0; i < 12; i++) colors[i] = current_color; id(respeaker).set_led_ring(colors); - id: update_rainbow_effect then: - lambda: |- static float hue_offset = 0.0f; uint32_t now = millis(); float dt = (now - id(last_led_update_time)) / 1000.0f; id(last_led_update_time) = now; hue_offset += dt * id(effect_speed); if (hue_offset >= 1.0f) hue_offset -= 1.0f; constexpr int NUM_LEDS = 12; constexpr float HUE_STEP = 1.0f / NUM_LEDS; uint32_t colors[NUM_LEDS]; float brightness = id(user_led_ring_brightness).state * id(effect_brightness); float current_hue = hue_offset; for (int i = 0; i < NUM_LEDS; i++) { float r, g, b; if (current_hue >= 1.0f) current_hue -= 1.0f; hsv_to_rgb((int)(current_hue * 360.0f), 1.0f, brightness, r, g, b); colors[i] = ((uint8_t)(r * 255.0f) << 16) | ((uint8_t)(g * 255.0f) << 8) | ((uint8_t)(b * 255.0f)); current_hue += HUE_STEP; } id(respeaker).set_led_ring(colors); - id: update_comet_cw_effect then: - lambda: |- static float comet_pos = 0.0f; uint32_t now = millis(); float dt = (now - id(last_led_update_time)) / 1000.0f; id(last_led_update_time) = now; constexpr int NUM_LEDS = 12; constexpr int BASE_TAIL = 3; float leds_per_sec = id(effect_speed) * NUM_LEDS; comet_pos += dt * leds_per_sec; while (comet_pos >= NUM_LEDS) comet_pos -= NUM_LEDS; int head_index = (int)comet_pos; int tail_length = BASE_TAIL + (int)(id(effect_speed)); if (tail_length > NUM_LEDS - 1) tail_length = NUM_LEDS - 1; uint32_t colors[NUM_LEDS] = {0}; float brightness = id(user_led_ring_brightness).state * id(effect_brightness); uint8_t head_r = (uint8_t)(id(effect_color_r) * brightness); uint8_t head_g = (uint8_t)(id(effect_color_g) * brightness); uint8_t head_b = (uint8_t)(id(effect_color_b) * brightness); colors[head_index % NUM_LEDS] = (head_r << 16) | (head_g << 8) | head_b; for (int i = 1; i <= tail_length; i++) { float tail_factor = (float)i / (tail_length + 1); float tail_brightness = (1.0f - tail_factor) * brightness; uint8_t r = (uint8_t)(id(effect_color_r) * tail_brightness); uint8_t g = (uint8_t)(id(effect_color_g) * tail_brightness); uint8_t b = (uint8_t)(id(effect_color_b) * tail_brightness); int tail_index = (head_index - i + NUM_LEDS) % NUM_LEDS; colors[tail_index] = (r << 16) | (g << 8) | b; } id(respeaker).set_led_ring(colors); - id: update_comet_ccw_effect then: - lambda: |- static float comet_pos = 0.0f; uint32_t now = millis(); float dt = (now - id(last_led_update_time)) / 1000.0f; id(last_led_update_time) = now; constexpr int NUM_LEDS = 12; constexpr int BASE_TAIL = 3; float leds_per_sec = id(effect_speed) * NUM_LEDS; comet_pos -= dt * leds_per_sec; while (comet_pos < 0.0f) comet_pos += NUM_LEDS; int head_index = (int)comet_pos; int tail_length = BASE_TAIL + (int)(id(effect_speed)); if (tail_length > NUM_LEDS - 1) tail_length = NUM_LEDS - 1; uint32_t colors[NUM_LEDS] = {0}; float brightness = id(user_led_ring_brightness).state * id(effect_brightness); uint8_t head_r = (uint8_t)(id(effect_color_r) * brightness); uint8_t head_g = (uint8_t)(id(effect_color_g) * brightness); uint8_t head_b = (uint8_t)(id(effect_color_b) * brightness); colors[head_index % NUM_LEDS] = (head_r << 16) | (head_g << 8) | head_b; for (int i = 1; i <= tail_length; i++) { float tail_factor = (float)i / (tail_length + 1); float tail_brightness = (1.0f - tail_factor) * brightness; uint8_t r = (uint8_t)(id(effect_color_r) * tail_brightness); uint8_t g = (uint8_t)(id(effect_color_g) * tail_brightness); uint8_t b = (uint8_t)(id(effect_color_b) * tail_brightness); int tail_index = (head_index + i) % NUM_LEDS; colors[tail_index] = (r << 16) | (g << 8) | b; } id(respeaker).set_led_ring(colors); - id: update_twinkle_effect then: - lambda: |- constexpr int NUM_LEDS = 12; static float led_brightness[NUM_LEDS] = {0.0f}; static float led_fade_speed[NUM_LEDS] = {0.0f}; uint32_t now = millis(); float dt = (now - id(last_led_update_time)) / 1000.0f; id(last_led_update_time) = now; // Update existing twinkles for (int i = 0; i < NUM_LEDS; i++) { if (led_fade_speed[i] != 0.0f) { led_brightness[i] += led_fade_speed[i] * dt; if (led_fade_speed[i] > 0.0f && led_brightness[i] >= 1.0f) { led_brightness[i] = 1.0f; led_fade_speed[i] *= -1.0f; } else if (led_fade_speed[i] < 0.0f && led_brightness[i] <= 0.0f) { led_brightness[i] = 0.0f; led_fade_speed[i] = 0.0f; } } } // Start new twinkles float twinkle_chance = dt * id(effect_speed); if (random_float() < twinkle_chance) { int led_to_start = (int)(random_float() * NUM_LEDS); if (led_fade_speed[led_to_start] == 0.0f) { led_brightness[led_to_start] = 0.0f; float min_speed = 1.5f, max_speed = 3.0f; led_fade_speed[led_to_start] = min_speed + (random_float() * (max_speed - min_speed)); } } // Render colors uint32_t colors[NUM_LEDS]; float master_brightness = id(user_led_ring_brightness).state * id(effect_brightness); for (int i = 0; i < NUM_LEDS; i++) { float current_led_brightness = led_brightness[i] * master_brightness; uint8_t r = (uint8_t)(id(effect_color_r) * current_led_brightness); uint8_t g = (uint8_t)(id(effect_color_g) * current_led_brightness); uint8_t b = (uint8_t)(id(effect_color_b) * current_led_brightness); colors[i] = (r << 16) | (g << 8) | b; } id(respeaker).set_led_ring(colors); - id: update_timer_tick_effect then: - lambda: |- constexpr int NUM_LEDS = 12; static int tick_index = 0; uint32_t now = millis(); // Only update tick position every 100ms to reduce I2C traffic static uint32_t last_tick_update = 0; if (now - last_tick_update >= 100) { tick_index = (tick_index - 1 + NUM_LEDS) % NUM_LEDS; last_tick_update = now; } uint32_t colors[NUM_LEDS] = {0}; uint32_t seconds_left = id(first_active_timer).seconds_left; uint32_t total_seconds = id(first_active_timer).total_seconds; float timer_ratio = (float)NUM_LEDS * seconds_left / std::max(total_seconds, (uint32_t)1); float master_brightness = id(user_led_ring_brightness).state * id(effect_brightness); for (int i = 0; i < NUM_LEDS; i++) { float bar_brightness = clamp(timer_ratio - i, 0.0f, 1.0f); if (bar_brightness > 0.0f) { float tick_dip = (i == tick_index) ? 0.9f : 1.0f; float final_brightness = bar_brightness * tick_dip * master_brightness; uint8_t r = (uint8_t)(id(effect_color_r) * final_brightness); uint8_t g = (uint8_t)(id(effect_color_g) * final_brightness); uint8_t b = (uint8_t)(id(effect_color_b) * final_brightness); colors[i] = (r << 16) | (g << 8) | b; } } id(respeaker).set_led_ring(colors); - id: update_volume_display_effect then: - lambda: |- constexpr int NUM_LEDS = 12; uint32_t colors[NUM_LEDS] = {0}; if (id(external_media_player).is_ready()) { bool is_muted = id(external_media_player).is_muted(); float volume = id(external_media_player).volume; if (is_muted || volume == 0.0f) { uint32_t mute_color = (255 << 16); // Red colors[0] = mute_color; colors[6] = mute_color; } else { float num_leds_on = volume * NUM_LEDS; float master_brightness = id(user_led_ring_brightness).state; for (int i = 0; i < NUM_LEDS; i++) { float brightness = clamp(num_leds_on - i, 0.0f, 1.0f); if (brightness > 0.0f) { uint8_t r = (uint8_t)(id(user_led_ring_color_r) * brightness * master_brightness); uint8_t g = (uint8_t)(id(user_led_ring_color_g) * brightness * master_brightness); uint8_t b = (uint8_t)(id(user_led_ring_color_b) * brightness * master_brightness); colors[i] = (r << 16) | (g << 8) | b; } } } } id(respeaker).set_led_ring(colors); - id: update_led_beam_effect then: - lambda: |- constexpr int NUM_LEDS = 12; constexpr int FADE_LEDS = 3; constexpr float TRANSITION_DURATION = 0.5f; // Duration of the smooth transition uint32_t colors[NUM_LEDS] = {0}; uint32_t now = millis(); float dt = (now - id(last_led_update_time)) / 1000.0f; id(last_led_update_time) = now; if (id(beam_direction).has_state()) { // CORRECTING THE OFFSET: add 5 from the sensor reading float target_pos = ((int)id(beam_direction).state + 5) % NUM_LEDS; float current_pos = id(animated_beam_position); // Calculate the shortest path around the circle float diff = target_pos - current_pos; if (diff > NUM_LEDS / 2.0f) { diff -= NUM_LEDS; } else if (diff < -NUM_LEDS / 2.0f) { diff += NUM_LEDS; } // Move current position towards target if (abs(diff) > 0.01f) { float move_speed = diff / TRANSITION_DURATION; current_pos += move_speed * dt; } else { current_pos = target_pos; } // Handle wrap-around for the animated position if (current_pos >= NUM_LEDS) current_pos -= NUM_LEDS; if (current_pos < 0.0f) current_pos += NUM_LEDS; id(animated_beam_position) = current_pos; // Render the smoothed beam float master_brightness = id(user_led_ring_brightness).state * id(effect_brightness); for (int i = 0; i < NUM_LEDS; i++) { // Calculate circular distance from current LED to the animated position float dist = abs(i - current_pos); if (dist > NUM_LEDS / 2.0f) { dist = NUM_LEDS - dist; } // Calculate brightness based on distance (linear falloff) float brightness_factor = 1.0f - (dist / (FADE_LEDS + 1.0f)); brightness_factor = std::max(0.0f, brightness_factor); if (brightness_factor > 0.0f) { float final_brightness = brightness_factor * master_brightness; uint8_t r = (uint8_t)(id(effect_color_r) * final_brightness); uint8_t g = (uint8_t)(id(effect_color_g) * final_brightness); uint8_t b = (uint8_t)(id(effect_color_b) * final_brightness); colors[i] = (r << 16) | (g << 8) | b; } } } id(respeaker).set_led_ring(colors); # Master script controlling the LEDs, based on different conditions : initialization in progress, wifi and api connected and voice assistant phase. # For the sake of simplicity and re-usability, the script calls child scripts defined below. # This script will be called every time one of these conditions is changing. - id: control_leds mode: single # Prevent multiple simultaneous executions then: - lambda: | // Cache expensive component checks static bool last_respeaker_failed = false; static bool last_wifi_connected = false; static bool last_api_connected = false; static int last_voice_phase = -1; static bool last_timer_ringing = false; static bool last_timer_active = false; static bool last_improv_ble = false; static bool last_init_progress = false; bool respeaker_failed = id(respeaker).is_failed(); bool wifi_connected = id(wifi_id).is_connected(); bool api_connected = id(api_id).is_connected(); int voice_phase = id(voice_assistant_phase); bool new_timer_ringing = id(timer_ringing).state; bool improv_ble = id(improv_ble_in_progress); bool init_progress = id(init_in_progress); // Only update if something actually changed bool needs_update = ( respeaker_failed != last_respeaker_failed || wifi_connected != last_wifi_connected || api_connected != last_api_connected || voice_phase != last_voice_phase || new_timer_ringing != last_timer_ringing || improv_ble != last_improv_ble || init_progress != last_init_progress ); if (!needs_update) return; // Update cache last_respeaker_failed = respeaker_failed; last_wifi_connected = wifi_connected; last_api_connected = api_connected; last_voice_phase = voice_phase; last_timer_ringing = new_timer_ringing; last_improv_ble = improv_ble; last_init_progress = init_progress; if (respeaker_failed) { id(control_leds_respeaker_startup_failed).execute(); return; } // Only check timers if we need to id(check_if_timers_active).execute(); if (id(is_timer_active)){ id(fetch_first_active_timer).execute(); } // Continue with existing logic... if (improv_ble) { id(control_leds_improv_ble_state).execute(); } else if (init_progress) { id(control_leds_init_state).execute(); } else if (!wifi_connected || !api_connected){ id(control_leds_no_ha_connection_state).execute(); } else if (new_timer_ringing) { id(control_leds_timer_ringing).execute(); } else if (voice_phase == ${voice_assist_waiting_for_command_phase_id}) { id(control_leds_voice_assistant_waiting_for_command_phase).execute(); } else if (voice_phase == ${voice_assist_listening_for_command_phase_id}) { id(control_leds_voice_assistant_listening_for_command_phase).execute(); } else if (voice_phase == ${voice_assist_thinking_phase_id}) { id(control_leds_voice_assistant_thinking_phase).execute(); } else if (voice_phase == ${voice_assist_replying_phase_id}) { id(control_leds_voice_assistant_replying_phase).execute(); } else if (voice_phase == ${voice_assist_error_phase_id}) { id(control_leds_voice_assistant_error_phase).execute(); } else if (voice_phase == ${voice_assist_not_ready_phase_id}) { id(control_leds_voice_assistant_not_ready_phase).execute(); } else if (id(is_timer_active)) { id(control_leds_timer_ticking).execute(); } else if (voice_phase == ${voice_assist_idle_phase_id}) { id(control_leds_voice_assistant_idle_phase).execute(); } # Script executed if respeaker startup failed - id: control_leds_respeaker_startup_failed then: - script.execute: id: led_set_effect effect: "breathe" r: 200.0 g: 0.0 b: 0.0 speed: 0.5 brightness: 0.6 # Script executed during Improv BLE - id: control_leds_improv_ble_state then: - script.execute: id: led_set_effect effect: "twinkle" r: 255.0 g: 200.0 b: 160.0 speed: 10.0 brightness: 0.8 # Script executed during initialization - id: control_leds_init_state then: - if: condition: wifi.connected: then: - script.execute: id: led_set_effect effect: "twinkle" r: 20.0 g: 200.0 b: 250.0 speed: 20.0 brightness: 1.0 else: - script.execute: id: led_set_effect effect: "twinkle" r: 20.0 g: 200.0 b: 250.0 speed: 4.0 brightness: 0.6 # Script executed when the device has no connection to Home Assistant - id: control_leds_no_ha_connection_state then: - script.execute: id: led_set_effect effect: "twinkle" r: 255.0 g: 0.0 b: 0.0 speed: 10.0 brightness: 0.4 # Script executed when the voice assistant is waiting for a command (After the wake word) - id: control_leds_voice_assistant_waiting_for_command_phase then: - lambda: | id(animated_beam_position) = id(beam_direction).state; - script.execute: id: led_set_effect effect: "led_beam" r: !lambda return id(user_led_ring_color_r); g: !lambda return id(user_led_ring_color_g); b: !lambda return id(user_led_ring_color_b); speed: 0.0 brightness: 0.8 # Script executed when the voice assistant is listening to a command - id: control_leds_voice_assistant_listening_for_command_phase then: - script.execute: id: led_set_effect effect: "led_beam" r: !lambda return id(user_led_ring_color_r); g: !lambda return id(user_led_ring_color_g); b: !lambda return id(user_led_ring_color_b); speed: 0.0 brightness: 1.0 # Script executed when the voice assistant is thinking to a command - id: control_leds_voice_assistant_thinking_phase then: - script.execute: id: led_set_effect effect: "breathe" r: !lambda return id(user_led_ring_color_r); g: !lambda return id(user_led_ring_color_g); b: !lambda return id(user_led_ring_color_b); speed: 1.0 brightness: 0.6 # Script executed when the voice assistant is replying to a command - id: control_leds_voice_assistant_replying_phase then: - script.execute: id: led_set_effect effect: "comet_ccw" r: !lambda return id(user_led_ring_color_r); g: !lambda return id(user_led_ring_color_g); b: !lambda return id(user_led_ring_color_b); speed: 1.0 brightness: 0.8 # Script executed when the voice assistant is in error - id: control_leds_voice_assistant_error_phase then: - script.execute: id: led_set_effect effect: "breathe" r: 255.0 g: 0.0 b: 0.0 speed: 3.0 brightness: 0.8 # Script executed when the voice assistant is not ready - id: control_leds_voice_assistant_not_ready_phase then: - script.execute: id: led_set_effect effect: "twinkle" r: 255.0 g: 0.0 b: 0.0 speed: 5.0 brightness: 0.8 # Script executed when the volume is changed - id: control_leds_volume_changed mode: restart then: - lambda: |- id(volume_display_active) = true; - delay: 2s - lambda: |- id(volume_display_active) = false; // Turn off LEDs - any active effect will take over on next interval tick uint32_t colors[12] = {0}; id(respeaker).set_led_ring(colors); # Script executed when the timer is ringing, to control the LEDs - id: control_leds_timer_ringing then: - script.execute: id: led_set_effect effect: "breathe" r: !lambda return id(user_led_ring_color_r); g: !lambda return id(user_led_ring_color_g); b: !lambda return id(user_led_ring_color_b); speed: 5.0 brightness: 1.0 # Script executed when the timer is ticking, to control the LEDs - id: control_leds_timer_ticking then: - script.execute: id: led_set_effect effect: "timer_tick" r: !lambda return id(user_led_ring_color_r); g: !lambda return id(user_led_ring_color_g); b: !lambda return id(user_led_ring_color_b); speed: 1.0 brightness: 0.7 # Script executed when the voice assistant is idle (waiting for a wake word) - id: control_leds_voice_assistant_idle_phase then: - script.execute: id: led_set_effect effect: "off" r: 0.0 g: 0.0 b: 0.0 speed: 0.0 brightness: 0.0 # Script executed when the timer is ringing, to playback sounds. - id: ring_timer then: - script.execute: enable_repeat_one - script.execute: id: play_sound priority: true sound_file: "timer_finished_sound" # Script executed when the timer is ringing, to repeat the timer finished sound. - id: enable_repeat_one then: - media_player.repeat_one: id: external_media_player announcement: true # Turn on the repeat mode and pause for 500 ms between playlist items/repeats - lambda: |- id(external_media_player)->set_playlist_delay_ms(1, 500); # Script execute when the timer is done ringing, to disable repeat mode. - id: disable_repeat then: # Turn off the repeat mode and pause for 0 ms between playlist items/repeats - media_player.repeat_off: id: external_media_player announcement: true - lambda: |- id(external_media_player)->set_playlist_delay_ms(1, 0); # Script executed when we want to play sounds on the device. - id: play_sound parameters: priority: bool # sound_file: "audio::AudioFile*" sound_file: string then: - if: condition: lambda: return priority; then: - media_player.stop: id: external_media_player announcement: true - lambda: |- if ( (id(external_media_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING ) || priority) { id(external_media_player) ->make_call() .set_media_url("file://" + sound_file) .set_announcement(true) .perform(); } # Script used to fetch the first active timer (Stored in global first_active_timer) - id: fetch_first_active_timer then: - lambda: | const auto &timers = id(va).get_timers(); auto output_timer = *timers.begin(); for (const auto &timer : timers) { if (timer.is_active && timer.seconds_left <= output_timer.seconds_left) { output_timer = timer; } } id(first_active_timer) = output_timer; # Script used to check if a timer is active (Stored in global is_timer_active) - id: check_if_timers_active then: - lambda: | const auto &timers = id(va).get_timers(); bool output = false; for (const auto &timer : timers) { if (timer.is_active) { output = true; } } id(is_timer_active) = output; # Script used activate the stop word if the TTS step is long. # Why is this wrapped on a script? # Becasue we want to stop the sequence if the TTS step is faster than that. # This allows us to prevent having the deactivation of the stop word before its own activation. - id: activate_stop_word_once then: - wait_until: condition: media_player.is_announcing: id: external_media_player - delay: 1s # Enable stop wake word - if: condition: switch.is_off: timer_ringing then: - micro_wake_word.enable_model: stop - wait_until: not: media_player.is_announcing: id: external_media_player - if: condition: switch.is_off: timer_ringing then: - micro_wake_word.disable_model: stop - id: check_alarm then: - lambda: |- id(publish_current_time).execute(); // Check alarm if (id(alarm_on).state && id(alarm_time).has_state()) { // Get the stored alarm time from the sensor auto set_alarm_time = id(alarm_time).state; if (set_alarm_time.length() == 5 && isdigit(set_alarm_time[0]) && isdigit(set_alarm_time[1]) && isdigit(set_alarm_time[3]) && isdigit(set_alarm_time[4])) { auto alarm_hour = std::stoi(set_alarm_time.substr(0, 2)); auto alarm_minute = std::stoi(set_alarm_time.substr(3, 2)); // Trigger action if current time matches alarm time auto time_now = id(homeassistant_time).now(); if (time_now.hour == alarm_hour && time_now.minute == alarm_minute) { auto action = id(alarm_action).current_option(); if (action == "Play sound") { id(timer_ringing).turn_on(); } else if (action == "Send event") { id(send_alarm_event).execute(); } else if (action == "Sound and event") { id(timer_ringing).turn_on(); id(send_alarm_event).execute(); } } } else { ESP_LOGW("alarm", "Incorrect alarm time setting"); } } - id: send_wake_word_event parameters: wake_word: string then: - homeassistant.event: event: esphome.wake_word_detected data: wake_word: !lambda return wake_word; - id: send_alarm_event then: - homeassistant.event: event: esphome.alarm_ringing - id: send_tts_uri_event parameters: tts_uri: string then: - homeassistant.event: event: esphome.tts_uri data: uri: !lambda return tts_uri; - id: send_stt_text_event parameters: stt_text: string then: - homeassistant.event: event: esphome.stt_text data: text: !lambda return stt_text; - id: publish_current_time mode: single then: - lambda: |- static std::string last_time_string = ""; auto time_now = id(homeassistant_time).now(); std::string current_time_string = time_now.strftime("%H:%M"); // Only publish if time actually changed if (current_time_string != last_time_string) { id(current_time).publish_state(current_time_string); last_time_string = current_time_string; } i2s_audio: - id: i2s_output i2s_lrclk_pin: number: GPIO7 allow_other_uses: true i2s_bclk_pin: number: GPIO8 allow_other_uses: true # i2s_mclk_pin: # number: GPIO9 # allow_other_uses: true - id: i2s_input i2s_lrclk_pin: number: GPIO7 allow_other_uses: true i2s_bclk_pin: number: GPIO8 allow_other_uses: true # i2s_mclk_pin: # number: GPIO9 # allow_other_uses: true microphone: - platform: i2s_audio id: i2s_mics i2s_din_pin: GPIO43 adc_type: external pdm: false sample_rate: 48000 bits_per_sample: 32bit i2s_mode: secondary i2s_audio_id: i2s_input channel: stereo speaker: # Hardware speaker output - platform: i2s_audio id: i2s_audio_speaker sample_rate: 48000 i2s_mode: secondary i2s_dout_pin: GPIO44 bits_per_sample: 32bit i2s_audio_id: i2s_output dac_type: external channel: stereo timeout: never buffer_duration: 100ms audio_dac: aic3104_dac # Virtual speakers to combine the announcement and media streams together into one output - platform: mixer id: mixing_speaker output_speaker: i2s_audio_speaker num_channels: 2 task_stack_in_psram: true source_speakers: - id: announcement_mixing_input timeout: never - id: media_mixing_input timeout: never # Virtual speakers to resample each pipelines' audio, if necessary, as the mixer speaker requires the same sample rate - platform: resampler id: announcement_resampling_speaker output_speaker: announcement_mixing_input sample_rate: 48000 bits_per_sample: 16 - platform: resampler id: media_resampling_speaker output_speaker: media_mixing_input sample_rate: 48000 bits_per_sample: 16 sendspin: id: sendspin_hub task_stack_in_psram: true kalman_process_error: 0.01 media_source: - platform: sendspin id: sendspin_source - platform: http_request id: http_source buffer_size: 500000 - platform: file id: file_source files: - id: mute_switch_on_sound file: ${mute_switch_on_sound_file} - id: mute_switch_off_sound file: ${mute_switch_off_sound_file} - id: timer_finished_sound file: ${timer_finished_sound_file} - id: wake_word_triggered_sound file: ${wake_word_triggered_sound_file} - id: error_cloud_expired file: ${error_cloud_expired_sound_file} media_player: - platform: sendspin id: sendspin_group_media_player - platform: speaker_source id: external_media_player name: Media Player announcement_speaker: announcement_resampling_speaker media_speaker: media_resampling_speaker announcement_pipeline: format: FLAC # FLAC is the least processor intensive codec num_channels: 1 # Stereo audio is unnecessary for announcements sample_rate: 48000 media_pipeline: format: FLAC # FLAC is the least processor intensive codec num_channels: 2 sample_rate: 48000 volume_increment: 0.05 volume_min: 0.0 volume_max: 1.0 sources: - file_source - http_source - sendspin_source on_mute: - delay: 100ms # Debounce - script.execute: control_leds_volume_changed on_unmute: - delay: 100ms # Debounce - script.execute: control_leds_volume_changed on_volume: if: condition: - lambda: return !id(init_in_progress); then: - delay: 100ms # Debounce - script.execute: control_leds_volume_changed on_announcement: - mixer_speaker.apply_ducking: id: media_mixing_input decibel_reduction: 20 duration: 0.0s on_state: if: condition: and: - switch.is_off: timer_ringing - not: voice_assistant.is_running: - not: media_player.is_announcing: external_media_player then: - mixer_speaker.apply_ducking: id: media_mixing_input decibel_reduction: 0 duration: 1.0s external_components: - source: type: git url: https://github.com/formatBCE/esphome ref: respeaker_microphone components: - i2s_audio refresh: 0s - source: type: git url: https://github.com/formatBCE/Respeaker-XVF3800-ESPHome-integration ref: main components: - respeaker_xvf3800 - aic3104 refresh: 0s - source: # https://github.com/esphome/esphome/pull/12256 type: git url: https://github.com/esphome/esphome ref: a2d98e1d5e020200db8f3caf27a74a939a661dc4 components: [audio] - source: # https://github.com/esphome/esphome/pull/12258 type: git url: https://github.com/esphome/esphome ref: b4b7c5b25ebe0f2ab988f700219fa3c57b2377b7 components: [media_player] - source: # https://github.com/esphome/esphome/pull/12284 type: git url: https://github.com/esphome/esphome ref: d48058e140c98f5c2d902661d851a6b712d62434 components: [sendspin] - source: # https://github.com/esphome/esphome/pull/14013 type: git url: https://github.com/esphome/esphome ref: 51dcce3d1f22865ebb458a5447bbc877ac946b5a components: [mdns] - source: # https://github.com/esphome/esphome/pull/12429 type: git url: https://github.com/esphome/esphome ref: b49b09b6ae56502aa3ce51be86f90d732d019b2c refresh: 0s components: [file, http_request, media_source, speaker_source] respeaker_xvf3800: id: respeaker address: 0x2C mute_switch: id: mic_mute_switch name: "Microphone Mute" update_interval: 1s on_turn_on: - if: condition: and: - lambda: return !id(init_in_progress); - switch.is_on: mute_sound then: - script.execute: id: play_sound priority: false sound_file: "mute_switch_on_sound" on_turn_off: - if: condition: and: - lambda: return !id(init_in_progress); - switch.is_on: mute_sound then: - script.execute: id: play_sound priority: false sound_file: "mute_switch_off_sound" dfu_version: name: "Firmware Version" update_interval: 120s led_beam_sensor: name: "Voice Beam Direction" id: beam_direction internal: true firmware: url: https://github.com/formatBCE/Respeaker-XVF3800-ESPHome-integration/raw/refs/heads/main/application_xvf3800_inthost-lr48-sqr-i2c-v1.0.7-release.bin version: "1.0.7" md5: 043a848f544ff2c7265ac19685daf5de audio_dac: - platform: aic3104 id: aic3104_dac i2c_id: internal_i2c micro_wake_word: id: mww microphone: microphone: i2s_mics channels: 1 # gain_factor: 4 stop_after_detection: false models: - model: https://github.com/kahrendt/microWakeWord/releases/download/okay_nabu_20241226.3/okay_nabu.json # probability_cutoff: 0.8 id: okay_nabu - model: https://raw.githubusercontent.com/formatBCE/Respeaker-Lite-ESPHome-integration/refs/heads/main/microwakeword/models/v2/kenobi.json id: kenobi - model: hey_jarvis id: hey_jarvis - model: hey_mycroft id: hey_mycroft - model: https://github.com/kahrendt/microWakeWord/releases/download/stop/stop.json id: stop internal: true vad: probability_cutoff: 0.05 on_wake_word_detected: # If the wake word is detected when the device is muted (Possible with the software mute switch): Do nothing - if: condition: switch.is_off: mic_mute_switch then: - script.execute: id: send_wake_word_event wake_word: !lambda return wake_word; # If a timer is ringing: Stop it, do not start the voice assistant (We can stop timer from voice!) - if: condition: switch.is_on: timer_ringing then: - switch.turn_off: timer_ringing # Stop voice assistant if running else: - if: condition: voice_assistant.is_running: then: voice_assistant.stop: # Stop any other media player announcement else: - if: condition: media_player.is_announcing: id: external_media_player then: - media_player.stop: announcement: true id: external_media_player # Start the voice assistant and play the wake sound, if enabled else: - if: condition: switch.is_on: wake_sound then: - script.execute: id: play_sound priority: true sound_file: "wake_word_triggered_sound" - delay: 300ms - voice_assistant.start: wake_word: !lambda return wake_word; select: - platform: template name: "Wake word sensitivity" optimistic: true initial_option: Slightly sensitive restore_value: true entity_category: config options: - Slightly sensitive - Moderately sensitive - Very sensitive on_value: # Sets specific wake word probabilities computed for each particular model # Note probability cutoffs are set as a quantized uint8 value, each comment has the corresponding floating point cutoff # False Accepts per Hour values are tested against all units and channels from the Dinner Party Corpus. # These cutoffs apply only to the specific models included in the firmware: okay_nabu@20241226.3, hey_jarvis@v2, hey_mycroft@v2 lambda: |- if (x == "Slightly sensitive") { id(okay_nabu).set_probability_cutoff(217); // 0.85 -> 0.000 FAPH on DipCo (Manifest's default) id(hey_jarvis).set_probability_cutoff(247); // 0.97 -> 0.563 FAPH on DipCo (Manifest's default) id(hey_mycroft).set_probability_cutoff(253); // 0.99 -> 0.567 FAPH on DipCo } else if (x == "Moderately sensitive") { id(okay_nabu).set_probability_cutoff(176); // 0.69 -> 0.376 FAPH on DipCo id(hey_jarvis).set_probability_cutoff(235); // 0.92 -> 0.939 FAPH on DipCo id(hey_mycroft).set_probability_cutoff(242); // 0.95 -> 1.502 FAPH on DipCo (Manifest's default) } else if (x == "Very sensitive") { id(okay_nabu).set_probability_cutoff(143); // 0.56 -> 0.751 FAPH on DipCo 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: logger id: logger_select name: Logger Level disabled_by_default: true - platform: template optimistic: true name: "Alarm action" id: alarm_action icon: mdi:bell-plus options: - "Play sound" - "Send event" - "Sound and event" initial_option: "Play sound" on_value: then: - lambda: |- id(saved_alarm_action) = x; - platform: template name: "LED Ring Color Preset" id: user_led_color_preset icon: "mdi:palette" entity_category: config optimistic: true restore_value: true initial_option: "Custom" options: - "Purple" - "Blue" - "Green" - "Yellow" - "Cyan" - "White" - "Orange" - "Pink" - "Custom" on_value: - lambda: |- if (x == "Purple") { id(user_led_ring_color_r) = 255.0f; id(user_led_ring_color_g) = 0.0f; id(user_led_ring_color_b) = 255.0f; } else if (x == "Blue") { id(user_led_ring_color_r) = 0.0f; id(user_led_ring_color_g) = 0.0f; id(user_led_ring_color_b) = 255.0f; } else if (x == "Green") { id(user_led_ring_color_r) = 0.0f; id(user_led_ring_color_g) = 255.0f; id(user_led_ring_color_b) = 0.0f; } else if (x == "Yellow") { id(user_led_ring_color_r) = 255.0f; id(user_led_ring_color_g) = 255.0f; id(user_led_ring_color_b) = 0.0f; } else if (x == "Cyan") { id(user_led_ring_color_r) = 0.0f; id(user_led_ring_color_g) = 255.0f; id(user_led_ring_color_b) = 255.0f; } else if (x == "White") { id(user_led_ring_color_r) = 255.0f; id(user_led_ring_color_g) = 255.0f; id(user_led_ring_color_b) = 255.0f; } else if (x == "Orange") { id(user_led_ring_color_r) = 255.0f; id(user_led_ring_color_g) = 128.0f; id(user_led_ring_color_b) = 0.0f; } else if (x == "Pink") { id(user_led_ring_color_r) = 255.0f; id(user_led_ring_color_g) = 50.0f; id(user_led_ring_color_b) = 200.0f; } voice_assistant: id: va microphone: microphone: i2s_mics channels: 0 media_player: external_media_player micro_wake_word: mww use_wake_word: false noise_suppression_level: 0 auto_gain: 0 dbfs volume_multiplier: 1 on_client_connected: - if: condition: - lambda: return id(init_in_progress); - switch.is_on: mic_mute_switch then: - switch.turn_off: mic_mute_switch - lambda: id(init_in_progress) = false; - micro_wake_word.start: - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id}; - script.execute: control_leds on_client_disconnected: - voice_assistant.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: - lambda: return !id(init_in_progress); - lambda: return code != "duplicate_wake_up_detected"; - lambda: return code != "stt-no-text-recognized"; 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"; then: - script.execute: id: play_sound priority: true sound_file: "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) 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 on_tts_end: - script.execute: id: send_tts_uri_event tts_uri: !lambda 'return x;' on_stt_end: - script.execute: id: send_stt_text_event stt_text: !lambda 'return x;' # When the voice assistant ends ... 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 - lambda: | id(next_timer).publish_state(-1); id(next_timer_name).publish_state("-"); on_timer_started: - lambda: | id(check_if_timers_active).execute(); if (id(is_timer_active)) { id(fetch_first_active_timer).execute(); id(next_timer).publish_state(id(first_active_timer).seconds_left); id(next_timer_name).publish_state(id(first_active_timer).name); } - script.execute: control_leds on_timer_cancelled: - lambda: | id(check_if_timers_active).execute(); if (id(is_timer_active)) { id(fetch_first_active_timer).execute(); id(next_timer).publish_state(id(first_active_timer).seconds_left); id(next_timer_name).publish_state(id(first_active_timer).name); } else { id(next_timer).publish_state(-1); id(next_timer_name).publish_state("-"); } - script.execute: control_leds on_timer_updated: - lambda: | id(check_if_timers_active).execute(); if (id(is_timer_active)) { id(fetch_first_active_timer).execute(); id(next_timer).publish_state(id(first_active_timer).seconds_left); id(next_timer_name).publish_state(id(first_active_timer).name); } - script.execute: control_leds on_timer_tick: # Reduce LED updates - only every 5 seconds instead of every second - lambda: | id(fetch_first_active_timer).execute(); int seconds_left = id(first_active_timer).seconds_left; if (seconds_left % 5 == 0) { id(next_timer).publish_state(seconds_left); } if (id(current_led_effect) == "timer_tick") { id(control_leds_timer_ticking).execute(); } button: - platform: factory_reset id: factory_reset_button name: "Factory Reset" entity_category: diagnostic internal: true - platform: restart id: restart_button name: "Restart" entity_category: config disabled_by_default: true icon: "mdi:restart" debug: update_interval: 5s