Skip to content

Instantly share code, notes, and snippets.

@EverythingSmartHome
Last active March 19, 2025 14:39
Show Gist options
  • Save EverythingSmartHome/055fbdde31a607ef9d695d5cac780e94 to your computer and use it in GitHub Desktop.
Save EverythingSmartHome/055fbdde31a607ef9d695d5cac780e94 to your computer and use it in GitHub Desktop.
ESP32 & ESPHome Voice Assistant
esphome:
name: esp32-mic-speaker
friendly_name: esp32-mic-speaker
on_boot:
- priority: -100
then:
- wait_until: api.connected
- delay: 1s
- if:
condition:
switch.is_on: use_wake_word
then:
- voice_assistant.start_continuous:
esp32:
board: esp32dev
framework:
type: esp-idf
version: recommended
# Enable logging
logger:
# Enable Home Assistant API
api:
ota:
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Esp32-Mic-Speaker"
password: "9vYvAFzzPjuc"
i2s_audio:
i2s_lrclk_pin: GPIO27
i2s_bclk_pin: GPIO26
microphone:
- platform: i2s_audio
id: mic
adc_type: external
i2s_din_pin: GPIO13
pdm: false
speaker:
- platform: i2s_audio
id: big_speaker
dac_type: external
i2s_dout_pin: GPIO25
mode: mono
voice_assistant:
microphone: mic
use_wake_word: false
noise_suppression_level: 2
auto_gain: 31dBFS
volume_multiplier: 2.0
speaker: big_speaker
id: assist
switch:
- platform: template
name: Use wake word
id: use_wake_word
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
entity_category: config
on_turn_on:
- lambda: id(assist).set_use_wake_word(true);
- if:
condition:
not:
- voice_assistant.is_running
then:
- voice_assistant.start_continuous
on_turn_off:
- voice_assistant.stop
- lambda: id(assist).set_use_wake_word(false);
@cosminhriscu
Copy link

Hello, here is my working code for a configuration with ESP32-S3, two microphones inmp441 and one max98357 amp. Also, I used a led strip and a speaker. This code is an adaptation from the original code, excluded the mute button and the button on top. Unfortunately, I wanted to modify the code to turn on lights if a media file is played, but I found that something is changed in the external components: Could not find init.py file for component media_player. Please check the component is defined by this source (search path: /data/external_components/989fb39b/esphome/components/media_player/init.py). and I don't know how to resolve this. Also, it seems that they changed the original code, and for the media player component they are using now speaker platform instead nabu.

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'

  device_name: "home-assistant-voice"
  friendly_name: "home-assistant-voice"
  device_description: "home-assistant-voice"
  din: "GPIO8"   # MAX98357A - DIN
  lrclk_mic: "GPIO3" # MAX98357A & ICS43434 - LRC/LRCL
  lrclk_amp: "GPIO6" # MAX98357A & ICS43434 - LRC/LRCL
  bclk_mic: "GPIO2"  # MAX98357A & ICS43434 - BCLK
  bclk_amp: "GPIO7"  # MAX98357A & ICS43434 - BCLK
  dout: "GPIO4"  # ICS43434 - DOUT
  # ICS43434 - SEL low = left, high = right
  duck: "30" # db reduction for audio ducking
  vol: "90%" # volume level at boot
  
external_components:
  - source:
      type: git
      url: https://github.com/esphome/voice-kit
      ref: dev
    components:
      - media_player
      - micro_wake_word
      - microphone
      - nabu
      - voice_assistant
    refresh: 0s
  - source:
      type: git
      url: https://github.com/formatBCE/home-assistant-voice-pe
      ref: 48kHz_mic_support
    components:
      - nabu_microphone
    refresh: 0s

esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}
  name_add_mac_suffix: true
  min_version: 2024.12.2
  platformio_options:
    board_build.flash_mode: dio
  on_boot:
    priority: 375
    then:
      - script.execute: control_leds
      - media_player.volume_set: ${vol}
      - delay: 10min
      - if:
          condition:
            lambda: return id(init_in_progress);
          then:
            - lambda: id(init_in_progress) = false;
            - script.execute: control_leds

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  flash_size: 16MB
  framework:
    type: esp-idf
    version: recommended
    sdkconfig_options:
      CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
      CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
      CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
      CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB: "y"
      CONFIG_ESP32_S3_BOX_BOARD: "y"
      CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY: "y"

      CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP: "y"

      # Settings based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702
      CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM: "16"
      CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM: "512"
      CONFIG_ESP32_WIFI_STATIC_TX_BUFFER: "y"
      CONFIG_ESP32_WIFI_TX_BUFFER_TYPE: "0"
      CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM: "8"
      CONFIG_ESP32_WIFI_CACHE_TX_BUFFER_NUM: "32"
      CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED: "y"
      CONFIG_ESP32_WIFI_TX_BA_WIN: "16"
      CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED: "y"
      CONFIG_ESP32_WIFI_RX_BA_WIN: "32"
      CONFIG_LWIP_MAX_ACTIVE_TCP: "16"
      CONFIG_LWIP_MAX_LISTENING_TCP: "16"
      CONFIG_TCP_MAXRTX: "12"
      CONFIG_TCP_SYNMAXRTX: "6"
      CONFIG_TCP_MSS: "1436"
      CONFIG_TCP_MSL: "60000"
      CONFIG_TCP_SND_BUF_DEFAULT: "65535"
      CONFIG_TCP_WND_DEFAULT: "65535"  # Adjusted from linked settings to avoid compilation error
      CONFIG_TCP_RECVMBOX_SIZE: "512"
      CONFIG_TCP_QUEUE_OOSEQ: "y"
      CONFIG_TCP_OVERSIZE_MSS: "y"
      CONFIG_LWIP_WND_SCALE: "y"
      CONFIG_TCP_RCV_SCALE: "3"
      CONFIG_LWIP_TCPIP_RECVMBOX_SIZE: "512"

      CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y"
      CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y"
  
psram:
  mode: octal # quad for N8R2 and octal for N16R8
  speed: 80MHz

globals:
  # Global index for our LEDs. So that switching between different animation does not lead to unwanted effects.
  - id: global_led_animation_index
    type: int
    restore_value: no
    initial_value: '0'
  # Global variable tracking if the LED color was recently changed.
  - id: color_changed
    type: bool
    restore_value: no
    initial_value: 'false'
  # Global initialization variable. Initialized to true and set to false once everything is connected. Only used to have a smooth "plugging" experience
  - id: init_in_progress
    type: bool
    restore_value: no
    initial_value: 'true'
  # 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}
  # Global variable storing the first active timer
  - id: first_active_timer
    type: voice_assistant::Timer
    restore_value: false
  # Global variable storing the timer finished TTS
  - id: timer_tts_str
    type: std::string
    restore_value: false
  # Global variable storing if a timer is active
  - id: is_timer_active
    type: bool
    restore_value: false


logger:
  level: DEBUG
  logs:
    sensor: WARN  # avoids logging debug sensor updates

ota:
  - platform: esphome
    id: ota_esphome

debug:
  update_interval: 5s   

captive_portal:

web_server:
  port: 80

api:
  id: api_id
  encryption:
    key: !secret test-va_api
  on_client_connected:
    - script.execute: control_leds
  on_client_disconnected:
    - script.execute: control_leds



wifi:
  id: wifi_id
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  ap:
    ssid: "Esp32-Mic-Speaker"
    password: "9vYvAFzzPjuc"

i2s_audio:
  - id: i2s_in
    i2s_lrclk_pin: ${lrclk_mic}
    i2s_bclk_pin: ${bclk_mic}
  - id: i2s_out
    i2s_lrclk_pin: ${lrclk_amp}
    i2s_bclk_pin: ${bclk_amp}

microphone:
 - platform: nabu_microphone
   i2s_din_pin: ${dout}
   adc_type: external
   pdm: false
   sample_rate: 48000
   bits_per_sample: 32bit
   i2s_audio_id: i2s_in
   channel_0:
     id: mic0
   channel_1:
     id: mic1

speaker:
  - platform: i2s_audio
    id: spk
    sample_rate: 48000
    i2s_dout_pin: ${din}
    bits_per_sample: 32bit
    i2s_audio_id: i2s_out
    dac_type: external
    channel: mono
    timeout: never
    buffer_duration: 100ms

media_player:
  - platform: nabu
    id: nabu_media_player
    name: Media Player
    internal: false
    speaker:
    sample_rate: 48000
    volume_increment: 0.05
    volume_min: 0.4
    volume_max: 1
    on_announcement:
      - nabu.set_ducking:
          decibel_reduction: ${duck}
          duration: 0.0s
    on_state:
      if:
        condition:
          and:
            - switch.is_off: timer_ringing
            - not:
                voice_assistant.is_running:
            - not:
                lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING;
        then:
          - nabu.set_ducking:
              decibel_reduction: 0
              duration: 1.0s

    files:
      
      - id: timer_finished_sound
        file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac
      - id: wake_word_triggered_sound
        file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/wake_word_triggered.flac
      - id: jack_connected_sound
        file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/jack_connected.flac
      - id: jack_disconnected_sound
        file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/jack_disconnected.flac

micro_wake_word:
  id: mww
  microphone: mic0
  models:
    - model: https://github.com/kahrendt/microWakeWord/releases/download/okay_nabu_20241226.3/okay_nabu.json
      id: okay_nabu
    - 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:
  on_wake_word_detected:
    # 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
        # Start voice assistant, stop current announcement.
        else:
          - if:
              condition:
                lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING;
              then:
                lambda: |-
                  id(nabu_media_player)
                    ->make_call()
                    .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP)
                    .set_announcement(true)
                    .perform();
              else:
                - script.execute:
                    id: play_sound
                    priority: true
                    sound_file: !lambda return id(wake_word_triggered_sound);
                - delay: 500ms
                - voice_assistant.start:

voice_assistant:
  id: va
  microphone: mic1
  media_player: nabu_media_player
  micro_wake_word: mww
  noise_suppression_level: 1
  auto_gain: 31dBFS
  volume_multiplier: 4.0
  on_client_connected:
    - lambda: id(init_in_progress) = false;
    - script.execute:
        id: play_sound
        priority: true
        sound_file: !lambda return id(jack_connected_sound);
    - micro_wake_word.start:
    - lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
    - script.execute: control_leds
  on_client_disconnected:
    - voice_assistant.stop:
    - script.execute:
        id: play_sound
        priority: true
        sound_file: !lambda return id(jack_disconnected_sound);
    - lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id};
    - script.execute: control_leds
  on_error:
    - if:
        condition:
          and:
            - lambda: return !id(init_in_progress);
            - lambda: return code != "duplicate_wake_up_detected";
        then:
          - lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id};
          - script.execute: control_leds
  # When the voice assistant starts: Play a wake up sound, duck audio.
  on_start:
    - nabu.set_ducking:
        decibel_reduction: ${duck}   # Number of dB quieter; higher implies more quiet, 0 implies full volume
        duration: 0.0s          # The duration of the transition (default is 0)
  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_tts_start:
    - 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_if_tts_step_is_long
  # When the voice assistant ends ...
  on_end:
    - wait_until:
        not:
          voice_assistant.is_running:
    # Stop ducking audio.
    - nabu.set_ducking:
        decibel_reduction: 0   # 0 dB means no reduction
        duration: 1.0s
    # Stop the script that would potentially enable the stop word if the response is longer than a second
    - script.stop: activate_stop_word_if_tts_step_is_long
    # Disable the stop word (If the timer is not ringing)
    - if:
        condition:
          switch.is_off: timer_ringing
        then:
          - lambda: id(stop).disable();
    # 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:
    - script.execute: control_leds
  on_timer_cancelled:
    - script.execute: control_leds
  on_timer_updated:
    - script.execute: control_leds
  on_timer_tick:
    - script.execute: control_leds
  
button:
  - platform: restart
    id: restart_btn
    name: "${friendly_name} REBOOT"



switch:
  # 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
      - lambda: id(stop).disable();
      # Stop any current annoucement (ie: stop the timer ring mid playback)
      - if:
          condition:
            lambda: return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING;
          then:
            lambda: |-
              id(nabu_media_player)
                ->make_call()
                .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP)
                .set_announcement(true)
                .perform();
      # Set back ducking ratio to zero
      - nabu.set_ducking:
          decibel_reduction: 0
          duration: 1.0s
    on_turn_on:
      # Duck audio
      - nabu.set_ducking:
          decibel_reduction: ${duck}
          duration: 0.0s
      # Enable stop wake word
      - lambda: id(stop).enable();
      # Ring timer
      - script.execute: ring_timer
      # If 15 minutes have passed and the timer is still ringing, stop it.
      - delay: 15min
      - switch.turn_off: timer_ringing
     

script:

  # 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
    then:
      - lambda: |
          id(check_if_timers_active).execute();
          if (id(is_timer_active)){
            id(fetch_first_active_timer).execute();
          }
          if (id(init_in_progress)) {
            id(control_leds_init_state).execute();
          } else if (!id(wifi_id).is_connected() || !id(api_id).is_connected()){
            id(control_leds_no_ha_connection_state).execute();
          } else if (id(timer_ringing).state) {
            id(control_leds_timer_ringing).execute();
          } else if (id(voice_assistant_phase) == ${voice_assist_waiting_for_command_phase_id}) {
            id(control_leds_voice_assistant_waiting_for_command_phase).execute();
          } else if (id(voice_assistant_phase) == ${voice_assist_listening_for_command_phase_id}) {
            id(control_leds_voice_assistant_listening_for_command_phase).execute();
          } else if (id(voice_assistant_phase) == ${voice_assist_thinking_phase_id}) {
            id(control_leds_voice_assistant_thinking_phase).execute();
          } else if (id(voice_assistant_phase) == ${voice_assist_replying_phase_id}) {
            id(control_leds_voice_assistant_replying_phase).execute();
          } else if (id(voice_assistant_phase) == ${voice_assist_error_phase_id}) {
            id(control_leds_voice_assistant_error_phase).execute();
          } else if (id(voice_assistant_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 (id(voice_assistant_phase) == ${voice_assist_idle_phase_id}) {
            id(control_leds_voice_assistant_idle_phase).execute();
          }


  # Script executed when the device has no connection to Home Assistant
  # Red Twinkle (This will be visible during HA updates for example)
  - id: control_leds_no_ha_connection_state
    then:
      - light.turn_on:
          brightness: 66%
          red: 1
          green: 0
          blue: 0
          id: voice_assistant_leds
          effect: "Twinkle"

  # Script executed during initialization
  # Blue Twinkle if Wifi is connected, Else solid warm white
  - id: control_leds_init_state
    then:
      - if:
          condition:
            wifi.connected:
          then:
            - light.turn_on:
                brightness: 66%
                red: 9.4%
                green: 73.3%
                blue: 94.9%
                id: voice_assistant_leds
                effect: "Twinkle"
          else:
            - light.turn_on:
                brightness: 66%
                red: 100%
                green: 89%
                blue: 71%
                id: voice_assistant_leds
                effect: "none"
  # Script executed when the voice assistant is idle (waiting for a wake word)
  # Nothing (Either LED ring off or LED ring on if the user decided to turn the user facing LED ring on)
  - id: control_leds_voice_assistant_idle_phase
    then:
      - light.turn_off: voice_assistant_leds
      - if:
          condition:
            light.is_on: led_ring
          then:
            light.turn_on: led_ring

  # Script executed when the voice assistant is waiting for a command (After the wake word)
  # Slow clockwise spin of the LED ring.
  - id: control_leds_voice_assistant_waiting_for_command_phase
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f );
          id: voice_assistant_leds
          effect: "Waiting for Command"

  # Script executed when the voice assistant is listening to a command
  # Fast clockwise spin of the LED ring.
  - id: control_leds_voice_assistant_listening_for_command_phase
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f );
          id: voice_assistant_leds
          effect: "Listening For Command"

  # Script executed when the voice assistant is thinking to a command
  # The spin stops and the 2 LEDs that are currently on and blinking indicating the commend is being processed.
  - id: control_leds_voice_assistant_thinking_phase
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f );
          id: voice_assistant_leds
          effect: "Thinking"

  # Script executed when the voice assistant is thinking to a command
  # Fast anticlockwise spin of the LED ring.
  - id: control_leds_voice_assistant_replying_phase
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f );
          id: voice_assistant_leds
          effect: "Replying"

  # Script executed when the voice assistant is in error
  # Fast Red Pulse
  - id: control_leds_voice_assistant_error_phase
    then:
      - light.turn_on:
          brightness: !lambda return min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f );
          red: 1
          green: 0
          blue: 0
          id: voice_assistant_leds
          effect: "Error"
  # Script executed when the voice assistant is not ready
  - id: control_leds_voice_assistant_not_ready_phase
    then:
      - light.turn_on:
          brightness: 66%
          red: 1
          green: 0
          blue: 0
          id: voice_assistant_leds
          effect: "Twinkle"         
  # Script executed when the timer is ringing, to control the LEDs
  # The LED ring blinks.
  - id: control_leds_timer_ringing
    then:
      - light.turn_on:
          brightness: !lambda return min ( max( id(led_ring).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f );
          id: voice_assistant_leds
          effect: "Timer Ring"

  # Script executed when the timer is ticking, to control the LEDs
  # The LEDs shows the remaining time as a fraction of the full ring.
  - id: control_leds_timer_ticking
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_ring).current_values.get_brightness() , 0.2f );
          id: voice_assistant_leds
          effect: "Timer tick"
 

  # Script executed when the timer is ringing, to playback sounds.
  - id: ring_timer
    then:
      - script.execute:
          id: timer_tts
      - while:
          condition:
            switch.is_on: timer_ringing
          then: # this is required to play the output on a media player
            - script.execute:
                id: play_sound
                priority: true
                sound_file: !lambda return id(timer_finished_sound);
            - delay: 4s
            - homeassistant.service:
                service: assist_satellite.announce
                data:
                  entity_id: assist_satellite.assist_media_player_kitchen_assist_satellite
                  message: !lambda return id(timer_tts_str);
            - wait_until:
                lambda: |-
                  return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING;
            - wait_until:
                not:
                  lambda: |-
                    return id(nabu_media_player)->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING;

  # Script executed when we want to play sounds on the device.
  - id: play_sound
    parameters:
      priority: bool
      sound_file: "media_player::MediaFile*"
    then:
      - lambda: |-
          if (priority) {
            id(nabu_media_player)
              ->make_call()
              .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP)
              .set_announcement(true)
              .perform();
          }
          if ( (id(nabu_media_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING ) || priority) {
            id(nabu_media_player)
              ->make_call()
              .set_announcement(true)
              .set_local_media_file(sound_file)
              .perform();
          }

  # 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;
          if (timers.size() > 0) {
            for (auto &iterable_timer : timers) {
              if(iterable_timer.second.is_active) {
                output = true;
              }
            }
          }
          id(is_timer_active) = output;

  # 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()->second;
          for (auto &iterable_timer : timers) {
            if (iterable_timer.second.is_active && iterable_timer.second.seconds_left <= output_timer.seconds_left) {
              output_timer = iterable_timer.second;
            }
          }
          id(first_active_timer) = output_timer;
    
  # Script used to create TTS string for timer
  - id: timer_tts
    then:
      - script.execute:
          id: fetch_first_active_timer
      - lambda: |-
          std::string finished_timer_name = id(first_active_timer).name;
          int total_seconds = id(first_active_timer).total_seconds;
          // Variables for hours, minutes, and seconds
          int hours = total_seconds / 3600;
          int minutes = (total_seconds % 3600) / 60;
          int seconds = total_seconds % 60;
          std::string finished_timer_duration;
          std::string result;
          // If more than 1 hour
          if (hours > 0) {
            finished_timer_duration = std::to_string(hours) + " hour" + (hours > 1 ? "s" : "");
            if (minutes > 0) {
              finished_timer_duration += " " + std::to_string(minutes) + " minute" + (minutes > 1 ? "s" : "");
            }
            if (seconds > 0) {
              finished_timer_duration += " " + std::to_string(seconds) + " second" + (seconds > 1 ? "s" : "");
            }
          }
          // If less than 1 hour but more than 1 minute
          else if (minutes > 0) {
            finished_timer_duration = std::to_string(minutes) + " minute" + (minutes > 1 ? "s" : "");
            if (seconds > 0) {
              finished_timer_duration += " " + std::to_string(seconds) + " second" + (seconds > 1 ? "s" : "");
            }
          }
          // If less than 1 minute
          else {
            finished_timer_duration = std::to_string(seconds) + " second" + (seconds > 1 ? "s" : "");
          }
          // Construct the final message
          if (finished_timer_name.empty()) {
            result = finished_timer_duration + "timer finished";
          }
          else {
            result = finished_timer_name + "timer finished";
          }
          id(timer_tts_str) = result;

  # 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_if_tts_step_is_long
    then:
      - delay: 1s
      # Enable stop wake word
      - lambda: id(stop).enable();
   
light:
  # Hardware LED ring. Not used because remapping needed
  - platform: esp32_rmt_led_strip
    id: leds_internal
    pin: GPIO9
    rmt_channel: 1
    num_leds: 19
    rgb_order: GRB
    chipset: WS2812
    default_transition_length: 0ms
    power_supply: led_power

  # Voice Assistant LED ring. Remapping of the internal LED.
  # This light is not exposed. The device controls it
  - platform: partition
    id: voice_assistant_leds
    internal: true
    default_transition_length: 0ms
    segments:
      - id: leds_internal
        from: 10
        to: 18
      - id: leds_internal
        from: 0
        to: 9
    effects:
      - addressable_lambda:
          name: "Waiting for Command"
          update_interval: 100ms
          lambda: |-
            auto light_color = id(led_ring).current_values;
            Color color(light_color.get_red() * 255, light_color.get_green() * 255,
                  light_color.get_blue() * 255);
            for (int i = 0; i < 12; i++) {
              if (i == id(global_led_animation_index) % 12) {
                it[i] = color;
              } else if (i == (id(global_led_animation_index) + 11) % 12) {
                it[i] = color * 192;
              } else if (i == (id(global_led_animation_index) + 10) % 12) {
                it[i] = color * 128;
              } else if (i == (id(global_led_animation_index) + 6) % 12) {
                it[i] = color;
              } else if (i == (id(global_led_animation_index) + 5) % 12) {
                it[i] = color * 192;
              } else if (i == (id(global_led_animation_index) + 4) % 12) {
                it[i] = color * 128;
              } else {
                it[i] = Color::BLACK;
              }
            }
            id(global_led_animation_index) = (id(global_led_animation_index) + 1) % 12;
      - addressable_lambda:
          name: "Listening For Command"
          update_interval: 50ms
          lambda: |-
            auto light_color = id(led_ring).current_values;
            Color color(light_color.get_red() * 255, light_color.get_green() * 255,
                  light_color.get_blue() * 255);
            for (int i = 0; i < 12; i++) {
              if (i == id(global_led_animation_index) % 12) {
                it[i] = color;
              } else if (i == (id(global_led_animation_index) + 11) % 12) {
                it[i] = color * 192;
              } else if (i == (id(global_led_animation_index) + 10) % 12) {
                it[i] = color * 128;
              } else if (i == (id(global_led_animation_index) + 6) % 12) {
                it[i] = color;
              } else if (i == (id(global_led_animation_index) + 5) % 12) {
                it[i] = color * 192;
              } else if (i == (id(global_led_animation_index) + 4) % 12) {
                it[i] = color * 128;
              } else {
                it[i] = Color::BLACK;
              }
            }
            id(global_led_animation_index) = (id(global_led_animation_index) + 1) % 12;
      - addressable_lambda:
          name: "Thinking"
          update_interval: 10ms
          lambda: |-
            static uint8_t brightness_step = 0;
            static bool brightness_decreasing = true;
            static uint8_t brightness_step_number = 10;
            if (initial_run) {
              brightness_step = 0;
              brightness_decreasing = true;
            }
            auto light_color = id(led_ring).current_values;
            Color color(light_color.get_red() * 255, light_color.get_green() * 255,
                  light_color.get_blue() * 255);
            for (int i = 0; i < 12; i++) {
              if (i == id(global_led_animation_index) % 12) {
                it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step));
              } else if (i == (id(global_led_animation_index) + 6) % 12) {
                it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step));
              } else {
                it[i] = Color::BLACK;
              }
            }
            if (brightness_decreasing) {
              brightness_step++;
            } else {
              brightness_step--;
            }
            if (brightness_step == 0 || brightness_step == brightness_step_number) {
              brightness_decreasing = !brightness_decreasing;
            }
      - addressable_lambda:
          name: "Replying"
          update_interval: 50ms
          lambda: |-
            id(global_led_animation_index) = (12 + id(global_led_animation_index) - 1) % 12;
            auto light_color = id(led_ring).current_values;
            Color color(light_color.get_red() * 255, light_color.get_green() * 255,
                  light_color.get_blue() * 255);
            for (int i = 0; i < 12; i++) {
              if (i == (id(global_led_animation_index)) % 12) {
                it[i] = color;
              } else if (i == ( id(global_led_animation_index) + 1) % 12) {
                it[i] = color * 192;
              } else if (i == ( id(global_led_animation_index) + 2) % 12) {
                it[i] = color * 128;
              } else if (i == ( id(global_led_animation_index) + 6) % 12) {
                it[i] = color;
              } else if (i == ( id(global_led_animation_index) + 7) % 12) {
                it[i] = color * 192;
              } else if (i == ( id(global_led_animation_index) + 8) % 12) {
                it[i] = color * 128;
              } else {
                it[i] = Color::BLACK;
              }
            }
      
      - addressable_lambda:
          name: "Volume Display"
          update_interval: 50ms
          lambda: |-
            auto light_color = id(led_ring).current_values;
            Color color(light_color.get_red() * 255, light_color.get_green() * 255,
                  light_color.get_blue() * 255);
            Color silenced_color(255, 0, 0);
            auto volume_ratio = 12.0f * id(nabu_media_player).volume;
            for (int i = 0; i < 12; i++) {
              if (i <= volume_ratio) {
                it[(6+i)%12] = color * min( 255.0f * (volume_ratio - i) , 255.0f ) ;
              } else {
                it[(6+i)%12] = Color::BLACK;
              }
            }
            if (id(nabu_media_player).volume == 0.0f) {
              it[6] = silenced_color;
            }

      - addressable_twinkle:
          name: "Twinkle"
          twinkle_probability: 50%
      - addressable_lambda:
          name: "Error"
          update_interval: 10ms
          lambda: |-
            static uint8_t brightness_step = 0;
            static bool brightness_decreasing = true;
            static uint8_t brightness_step_number = 10;
            if (initial_run) {
              brightness_step = 0;
              brightness_decreasing = true;
            }
            Color error_color(255, 0, 0);
            for (int i = 0; i < 12; i++) {
              it[i] = error_color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step));
            }
            if (brightness_decreasing) {
              brightness_step++;
            } else {
              brightness_step--;
            }
            if (brightness_step == 0 || brightness_step == brightness_step_number) {
              brightness_decreasing = !brightness_decreasing;
            }
      - addressable_lambda:
          name: "Timer Ring"
          update_interval: 10ms
          lambda: |-
            static uint8_t brightness_step = 0;
            static bool brightness_decreasing = true;
            static uint8_t brightness_step_number = 10;
            if (initial_run) {
              brightness_step = 0;
              brightness_decreasing = true;
            }
            auto light_color = id(led_ring).current_values;
            Color color(light_color.get_red() * 255, light_color.get_green() * 255,
                  light_color.get_blue() * 255);
            Color muted_color(255, 0, 0);
            for (int i = 0; i < 12; i++) {
              it[i] = color * uint8_t(255/brightness_step_number*(brightness_step_number-brightness_step));
            }
            if (brightness_decreasing) {
              brightness_step++;
            } else {
              brightness_step--;
            }
            if (brightness_step == 0 || brightness_step == brightness_step_number) {
              brightness_decreasing = !brightness_decreasing;
            }
      - addressable_lambda:
          name: "Timer Tick"
          update_interval: 100ms
          lambda: |-
            auto light_color = id(led_ring).current_values;
            Color color(light_color.get_red() * 255, light_color.get_green() * 255,
                  light_color.get_blue() * 255);
            Color muted_color(255, 0, 0);
            auto timer_ratio = 12.0f * id(first_active_timer).seconds_left / max(id(first_active_timer).total_seconds , static_cast<uint32_t>(1));
            uint8_t last_led_on = static_cast<uint8_t>(ceil(timer_ratio)) - 1;
            for (int i = 0; i < 12; i++) {
              float brightness_dip = ( i == id(global_led_animation_index) % 12 && i != last_led_on ) ? 0.9f : 1.0f ;
              if (i <= timer_ratio) {
                it[i] = color * min(255.0f * brightness_dip * (timer_ratio - i) , 255.0f * brightness_dip) ;
              } else {
                it[i] = Color::BLACK;
              }
            }
            id(global_led_animation_index) = (12 + id(global_led_animation_index) - 1) % 12;
      - addressable_rainbow:
          name: "Rainbow"
          width: 12
      - addressable_lambda:
          name: "Tick"
          update_interval: 333ms
          lambda: |-
            static uint8_t index = 0;
            Color color(255, 0, 0);
            if (initial_run) {
              index = 0;
            }
            for (int i = 0; i < 12; i++) {
              if (i <= index ) {
                it[i] = Color::BLACK;
              } else {
                it[i] = color;
              }
            }
            index = (index + 1) % 12;



 

  # User facing LED ring. Remapping of the internal LEDs.
  # Exposed to be used by the user.
  - platform: partition
    id: led_ring
    name: LED Ring
    entity_category: config
    icon: "mdi:circle-outline"
    default_transition_length: 0ms
    restore_mode: RESTORE_DEFAULT_OFF
    initial_state:
      color_mode: rgb
      brightness: 66%
      red: 9.4%
      green: 73.3%
      blue: 94.9%
    segments:
      - id: leds_internal
        from: 10
        to: 18
      - id: leds_internal
        from: 0
        to: 9
        
power_supply:
  - id: led_power
    pin: GPIO45


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment