Custom ESP32-S3 Voice Assistant (INMP441 + MAX98357A): I2S Bus Ownership and Playback Collisions

I think a new ESP32 Voice Assistant discussion is worth revisiting now that the ESPHome Speaker Mixer has become the standard approach.

Following tutorials from @tpage, @lmatter and others, I built my own ESP32-S3-based Voice Assistant using the Speaker Mixer, a microphone, wake word detection, and Voice Assistant.


I repurposed a HAS-useless Xiaomi Gateway :smiley:

The issue I'm facing is that the ESP32-S3 SuperMini only has a single I2S controller available for my setup. The microphone is always active by default, continuously capturing audio for wake word detection. Whenever audio playback starts (WAV files, announcements, TTS responses, or media playback), the microphone should stop and release the I2S bus so the speaker can take ownership of it.

The problem is that I haven't found a reliable way to receive a "pre-playback" event. Triggers such as on_play, on_turn_on, or on_state seem to occur after the speaker pipeline has already started allocating resources, which is too late. At that point, the I2S bus is still owned by the microphone and I get allocation/collision errors.

My ideal flow would be:

  1. Stop wake word detection.
  2. Stop Voice Assistant if running.
  3. Stop microphone capture.
  4. Wait for the I2S RX channel to be released.
  5. Start speaker playback.
  6. When playback finishes, stop the speaker.
  7. Re-enable microphone capture.
  8. Restart wake word detection.

Has anyone successfully implemented this with a shared I2S bus between microphone and speaker on an ESP32-S3 Supermini or similar?

Is there an existing pattern, component hook, or recommended architecture for handling I2S ownership transitions before audio playback begins?

Hardware

Component Model
MCU ESP32-S3 SuperMini
Microphone INMP441 I2S MEMS Microphone
Amplifier MAX98357A I2S DAC/Amplifier
Speaker 4Ξ© / 3W Speaker
RGB LED RGB LED Strip
Button GPIO Push Button

Pinout

Function GPIO
I2S LRCLK / WS GPIO8
I2S BCLK / SCK GPIO9
INMP441 SD (Data Out) GPIO7
MAX98357A DIN (Data In) GPIO10
Push Button GPIO6
RGB LED Red GPIO11
RGB LED Green GPIO12
RGB LED Blue GPIO13

Audio Topology

              ESP32-S3 SuperMini

            Shared I2S Control Bus
              GPIO8 (LRCLK / WS)
              GPIO9 (BCLK  / SCK)

                       β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚                             β”‚
        β–Ό                             β–Ό

   INMP441 Microphone           MAX98357A Amplifier
       GPIO7 (SD)               GPIO10 (DIN)

       I2S RX Data               I2S TX Data

Software Stack

  • ESPHome 2026.5

  • Home Assistant Voice Assistant

  • Speaker Mixer

  • micro_wake_word

  • Shared I2S bus for microphone and speaker

  • Wake word always active while idle

  • Speaker used for:

    • TTS responses
    • Announcement pipeline
    • WAV playback
    • Media playback

I'm including my ESPHome configuration below in case it helps others reproduce the issue.

substitutions:
  device_name: hall-speaker
  friendly_name: Hall Speaker


esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}
  on_boot:
    priority: 600.0
    then:
      - speaker.stop: va_speaker_hw
      - microphone.stop_capture: va_mic

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf
    sdkconfig_options:
      CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
      CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
      CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: ""

  on_client_connected:
    then:
      - light.turn_on:
          id: light1
          brightness: 25%
          red: 0%
          green: 50.9%
          blue: 98.8%
          effect: "Soft Breath"
      - delay: 800ms
      - light.turn_off: light1

ota:
  - platform: esphome
    password: ""

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  power_save_mode: none
  manual_ip:
    static_ip: 192.168.1.51
    gateway: 192.168.1.1
    subnet: 255.255.255.0
    dns1: 1.1.1.1
    dns2: 8.8.8.8

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "${friendly_name} Hotspot"
    password: ""

network:
  enable_high_performance: false   # Avoid aggressive high-performance networking to save RAM

captive_portal:

web_server:

binary_sensor:
  - platform: gpio
    name: "Button 1"
    id: button_1
    pin:
      number: GPIO6
      mode:
        input: true
        pullup: true
      inverted: true
    on_press:
      - if:
          condition:
            switch.is_on: mute_mic_sw
          then:
            - switch.turn_off: mute_mic_sw
          else:
            - switch.turn_on: mute_mic_sw

output:
  - platform: ledc
    id: output_led_red
    pin: GPIO11
    frequency: 1220 Hz

  - platform: ledc
    id: output_led_green
    pin: GPIO12
    frequency: 1220 Hz

  - platform: ledc
    id: output_led_blue
    pin: GPIO13
    frequency: 1220 Hz

light:
  - platform: rgb
    id: light1
    name: "LED bar"
    red: output_led_red
    green: output_led_green
    blue: output_led_blue
    default_transition_length: 500ms
    gamma_correct: 0
    restore_mode: ALWAYS_OFF
    effects:
      - pulse:
          name: "Soft Breath"
          min_brightness: 0%
          max_brightness: 25%
          transition_length:
            on_length: 1s
            off_length: 1500ms
          update_interval: 2s

button:
  - platform: restart
    name: "Reinicio ESP32 Media Player"
    
  - platform: template
    name: "Test Speaker WAV"
    on_press:
      - script.execute: audio_enter_speaker_mode
      - script.wait: audio_enter_speaker_mode
      - media_player.play_media:
          id: va_mediaplayer
          media_url: "http://192.168.1.101:8123/local/beep.wav"
          announcement: true

switch:
  - platform: template
    id: mute_mic_sw
    name: "Mute microphone"
    optimistic: true
    on_turn_on:
      - script.execute: mute_mic

    on_turn_off:
      - script.execute: unmute_mic

script:
  - id: audio_enter_speaker_mode
    mode: single
    then:
      - logger.log: "[AUDIO] Entering speaker mode: stopping wake word, VA and microphone"
      - micro_wake_word.stop:
      - voice_assistant.stop:
      - delay: 300ms
      - microphone.stop_capture: va_mic
      - delay: 500ms # Give the I2S RX side time to release before the speaker starts using TX.

  - id: audio_leave_speaker_mode
    mode: single
    then:
      - logger.log: "[AUDIO] Leaving speaker mode: stopping hardware speaker and restoring microphone if allowed"
      - speaker.stop: va_speaker_hw # Stop the real I2S hardware speaker, not the mixer.
      - delay: 700ms # Give the I2S TX side time to release before restoring RX/microphone.

      - if:
          condition:
            and:
              - switch.is_off: mute_mic_sw
              - not:
                  voice_assistant.is_running:
              - not:
                  media_player.is_playing: va_mediaplayer
          then:
            - logger.log: "[AUDIO] Restoring microphone capture + wake word"
            - microphone.capture: va_mic
            - delay: 250ms
            - micro_wake_word.start:
          else:
            - logger.log: "[AUDIO] Microphone restore skipped: muted, VA running or media still playing"

  - id: mute_mic
    mode: single
    then:
      - logger.log: "[MIC] Muting microphone"
      - micro_wake_word.stop:
      - voice_assistant.stop:
      - delay: 300ms
      - microphone.stop_capture: va_mic
      - light.turn_on:
          id: light1
          red: 100%
          green: 30%
          blue: 18%
          brightness: 20%
          transition_length: 800ms

  - id: unmute_mic
    mode: single
    then:
      - logger.log: "[MIC] Unmuting microphone"
      - light.turn_off:
          id: light1
          transition_length: 300ms

      - script.execute: audio_leave_speaker_mode

i2s_audio:
  - id: i2s
    i2s_lrclk_pin: GPIO8
    i2s_bclk_pin: GPIO9

microphone:
  - platform: i2s_audio
    id: va_mic
    i2s_audio_id: i2s
    adc_type: external
    i2s_din_pin: GPIO7
    channel: left
    pdm: false

speaker:
  - platform: i2s_audio
    id: va_speaker_hw
    i2s_audio_id: i2s
    dac_type: external
    i2s_dout_pin: GPIO10
    channel: mono
    bits_per_sample: 16bit
    sample_rate: 16000

  - platform: mixer
    id: va_speaker
    output_speaker: va_speaker_hw
    source_speakers:
      - id: va_speaker_announcement
      - id: va_speaker_media

media_player:
  - platform: speaker
    id: va_mediaplayer
    name: "Corridor Speaker"
    buffer_size: 50000
    announcement_pipeline:
        speaker: va_speaker_announcement
        format: WAV # Ask Home Assistant to transcode to a low‑cost WAV stream
        #format: FLAC 
        num_channels: 1
        sample_rate: 16000
    media_pipeline:
      speaker: va_speaker_media
      format: WAV
      num_channels: 1
      sample_rate: 16000
    on_turn_on:
      then:
        - logger.log: "[MEDIA] Media Player turned on"
        - script.execute: audio_enter_speaker_mode
        - script.wait: audio_enter_speaker_mode
    on_play:
      then:
        - logger.log: "Media playback started."

    on_idle:
      then:
        - logger.log: "[MEDIA] Playback finished"
        - delay: 5s
        - if:
            condition:
              media_player.is_idle: va_mediaplayer
            then:
              - media_player.turn_off: va_mediaplayer
            else:
              - logger.log: "[MEDIA] Media Player not idle, not turning off"

    on_turn_off:
      then:
        - logger.log: "[MEDIA] Media Player turned off"
        - script.execute: audio_leave_speaker_mode

micro_wake_word:
  id: my_micro_wake_word
  vad:
    model: github://esphome/micro-wake-word-models/models/v2/vad.json
  models:
    - model: github://esphome/micro-wake-word-models/models/v2/hey_jarvis.json
  on_wake_word_detected:
    - micro_wake_word.stop:
    - voice_assistant.start:
        wake_word: !lambda return wake_word;
    - light.turn_on:
        id: light1
        red: 0%
        green: 0%
        blue: 100%
        brightness: 30%

voice_assistant:
  id: va
  microphone: va_mic
  media_player: va_mediaplayer
  noise_suppression_level: 2
  volume_multiplier: 3.0
  auto_gain: 31dBFS
  micro_wake_word: my_micro_wake_word
  use_wake_word: true
  on_start:
    - logger.log: "[VA] Starting"
    - micro_wake_word.stop:
    - light.turn_on:
        id: light1
        red: 0%
        green: 100%
        blue: 0%
        brightness: 50%
  on_listening:
    - logger.log: "[VA] Listening"
    - light.turn_on:
        id: light1
        red: 0%
        green: 0%
        blue: 100%
        brightness: 100%
        effect: "Soft Breath"
  on_stt_end:
    then:
      - logger.log: "[VA] STT END"
      - light.turn_off: light1
      - script.execute: audio_enter_speaker_mode
  on_error:
    - logger.log: "[VA] ERROR"
    - delay: 1s
    - script.execute: audio_leave_speaker_mode
  on_end:
    then:
      - logger.log: "[VA] END"
      - light.turn_off: light1
      - wait_until:
          not:
            voice_assistant.is_running:
      - delay: 500ms
      - script.execute: audio_leave_speaker_mode

I have used both ESP32C6 and ESP32C61WROOM1 successfully for voice satellite with microwakeword with the same mic and amp components. Both of these ESPs have only one I2S bus.
I'd expect to be successful using similar YAML with an S3 which has 1 I2S bus.

You can see the YAML I use at
https://github.com/modernman1/Home-Assistant-Voice-Assist-ESP32-C6/blob/c0f5247c0c484ba739196ac30678b4dc8e24c504/May4-2026%20add%20int-ext%20antenna%20.yaml
and
Github-C61

Having skimmed over the YAML you are using I see you are stopping micro_wake_word within the mww block on wake word detected. My YAML also has no script: block at all (which is where you are trying to do mode changes) - it's not needed.

(The C6 has no Psram, the C61 does, so the C6 YAML has no media player entity, the C61 does.)

Your S3 mini has NO PSRAM - I do not believe it will support media player due to the limited SRAM - and suggest you start just doing voice assist and mww in manner similar to the yaml i'm using for the C6.

Thank you for sharing,

I will try to remove the block to switch between Mic and Audio playback in the following days.

You are right, my S3 mini has no PSRAM, what limits the support of media player. But it works good enough to play streamed content (such as radio or TTS messages)

I will try to take a look at your yaml and come back to you with the result of the test.

Again, thank you for taking the time to check on this issue.

Hi,
I have just built pretty much exactly that solution, except with dual MAX98357A amps for stereo output

The below works for me, but it is not as good as a Seeed studio respeaker-lite.
(which is where I cobbled most of the code from...)

substitutions: 
  INMP441_WS: GPIO13 # Green > Green 
  INMP441_SCK: GPIO2 # Blue > Orange
  INMP441_SD: GPIO12 # White > Yellow > SD
  i2s_bclk: GPIO9  # BCLK Pin of the Max98357 = WHITE / Yellow
  i2s_lrclk: GPIO10 # LRC Pin of the Max98357 = GREEN / Green
  spk: GPIO11 # DIN Pin of the Max98357 = BLUE / Orange
  POT_PIN: GPIO3 # connection to the Potentiometer
  # 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

# https://www.espboards.dev/esp32/esp32-s3-super-mini/
esphome:
  name: esphome-web-f7f288
  friendly_name: ESPHome Web f7f288
  project:
    name: linkedupbits.repurposed_bluetooth_s3
    version: "1.0"  
  min_version: 2026.4.0
  name_add_mac_suffix: false
  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: |-
            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();
  on_shutdown:
    then: {}
      # Prevent loud noise on software restart
      # - lambda: id(respeaker).mute_speaker();

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  flash_size: 4MB
  framework:
    type: esp-idf
    version: recommended

wifi:
  id: wifi_id
  fast_connect: False
  output_power: 8.5dB
  on_connect:
    - script.execute: control_leds
  on_disconnect:
    - script.execute: control_leds
  ssid: !secret wifi_ssid
  password: !secret wifi_password
 
# captive_portal:

# web_server:
#   port: 80

# Enable logging
logger:

# Enable Home Assistant API
api:
  id: api_id
  actions:
    - action: start_va
      then:
        - voice_assistant.start
    - action: stop_va
      then:
        - voice_assistant.stop
    - 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
    # - wait_until:
    #     condition:
    #       api.connected:
    #         state_subscription_only: true
    # - lvgl.widget.show: lbl_hastatus
  on_client_disconnected:
    - script.execute: control_leds
    # - if:
    #     condition:
    #       lambda: 'return (0 == client_info.find("Home Assistant "));'
    #     then:
    #       - lvgl.widget.hide: lbl_hastatus

# Allow Over-The-Air updates
ota:
  - platform: esphome
    id: ota_esphome
    on_begin:
      then:
        - light.turn_on:
            id: led_respeaker_onboard
            effect: "Slow Blink Blue"
        # - logger.log: "OTA start a"
        # - lvgl.resume:
        # - lvgl.widget.redraw:        
        # - lambda: "id(lvgl_comp).loop();"
        # #- lvgl.widget.hide: root
        # - lvgl.widget.show: popup_obj
        # - lambda: "id(lvgl_comp).loop();"
        # - logger.log: "OTA start b"
    on_progress: 
      then:
        # - lvgl.bar.update:
        #     id: popup_pb_percentage
        #     value: !lambda "return (int)x;"
        # - lvgl.label.update:
        #     id: popup_lbl_percentage
        #     text:
        #       format: "OTA progress %0.1f%%"
        #       args: ["x"]
        # - lambda: "id(lvgl_comp).loop();"  
        - light.turn_on:
            id: led_respeaker_onboard
            effect: "Slow Blink Blue"
        - lambda: "id(led_respeaker_onboard).loop();"           
    on_end:
      then:
        - logger.log: "OTA end"
        - light.turn_on:
            id: led_respeaker_onboard
            effect: "None"
        - delay: 500 ms
        # - light.turn_on:
        #     id: led_respeaker_onboard
        #     effect: "None"
        # - lambda: "id(led_respeaker_onboard).loop();"

psram:
  mode: quad
  speed: 80MHz

globals:
  - 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_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: pseudo_mute
    type: bool
    restore_value: no
    initial_value: 'false'

# Get the time from Home Assistant to sync the onboard real-time clock. 
time:
  - platform: homeassistant
    id: homeassistant_time
    on_time:
      # Every 1 minute
      - seconds: 0
        then:
          - script.execute: check_alarm
    on_time_sync:
      - script.execute: publish_current_time

datetime:
  # schedule the alarm time from Home Assistant
  - platform: template
    icon: mdi:bell-ring
    name: "Alarm time"
    id: alarm_time
    type: time
    initial_value: "00:00:00"
    restore_value: true
    optimistic: true
    internal: false
    set_action:
      then:
        - switch.turn_on: alarm_on

switch:
  # Hardware speaker mute
  - platform: template
    id: speaker_mute_switch
    name: Speaker mute
    icon: mdi:volume-mute
    internal: true
    optimistic: true
    turn_on_action:
      - lambda: id(pseudo_mute) = true;
    turn_off_action:
      - lambda: id(pseudo_mute) = false;
  # stateless momentary mic mute switch
  - platform: template
    internal: true
    #pin: 
    #  number: GPIO4 # D3
    #  inverted: true
    id: mute_toggle
    on_turn_on:
      - delay: 300ms
      - switch.turn_off: mute_toggle
    optimistic: true
    lambda: |-
      return false;
  # stateful user facing mic mute switch
  - platform: template
    id: mic_mute_switch
    name: Mic mute
    icon: mdi:microphone-off
    optimistic: false
    lambda: |-
      if (id(mute_state).state) {
        return true;
      } else {
        return false;
      }
    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"
    turn_on_action:
      - if:
          condition:
            lambda: return !id(mute_state).state;
          then:
            - switch.turn_on: mute_toggle
    turn_off_action:
      - if:
          condition:
            lambda: return id(mute_state).state;
          then:
            - switch.turn_on: mute_toggle
  # Button click Sounds Switch.
  - platform: template
    id: button_sound
    name: Button click sounds
    icon: "mdi:bullhorn"
    entity_category: config
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON
  # 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"
    on_turn_on:
      - script.execute: control_leds
    on_turn_off:
      - script.execute: control_leds
 

binary_sensor:
  # User Button. Used for many things (See on_multi_click)
  - platform: template
    id: user_button
    lambda: |- 
      return false;
  - platform: template
    id: mute_state
    name: "Mute state"
    icon: mdi:microphone-off
    lambda: |- 
      return false;
  - platform: gpio
    pin: 
      number: GPIO00
      inverted: true
      mode:
        input: true
        pullup: true
 
    name: Boot Switch
    filters:
      - delayed_on: 10ms
      - delayed_off: 10ms    
    internal: False
  

light:
  - platform: esp32_rmt_led_strip
    id: led_internal
    internal: true
    rgb_order: GRB
    pin: GPIO48
    num_leds: 1
    rmt_symbols: 192
    chipset: ws2812
    #restore_mode: ALWAYS_ON
    default_transition_length: 0s
    effects:
      - addressable_lambda:
          name: "Fast Pulse"
          update_interval: 10ms
          lambda: |-
            static float fraction = 0.0;
            static float step = 0.05;
            static bool increasing = true;

            auto values = id(led_internal)->current_values;
            Color color(values.get_red() * 255, values.get_green() * 255, values.get_blue() * 255);
            it[0].set_rgb(color.red * fraction, 
                             color.green * fraction, 
                             color.blue * fraction);
            
            fraction += (step * (increasing ? 1 : -1));
            if (fraction > 1.0) {
              fraction = 1.0;
              increasing = !increasing;
            } else if (fraction < 0.0) {
              fraction = 0.0;
              increasing = !increasing;
            }

      - addressable_lambda:
          name: "Slow Pulse"
          update_interval: 30ms
          lambda: |-
            static float fraction = 0.0;
            static float step = 0.05;
            static bool increasing = true;

            auto values = id(led_internal)->current_values;
            Color color(values.get_red() * 255, values.get_green() * 255, values.get_blue() * 255);
            it[0].set_rgb(color.red * fraction, 
                             color.green * fraction, 
                             color.blue * fraction);
            
            fraction += (step * (increasing ? 1 : -1));
            if (fraction > 1.0) {
              fraction = 1.0;
              increasing = !increasing;
            } else if (fraction < 0.0) {
              fraction = 0.0;
              increasing = !increasing;
            }

      - addressable_lambda:
          name: "Factory Reset Coming Up"
          update_interval: 200ms
          lambda: |-
            static bool on = false;
            on = !on;
            it[0].set_rgb(on ? 255 : 0, 0, 0);
  # User facing LED.
  # Exposed to be used by the user.
  - platform: partition
    id: led_respeaker_onboard
    name: LED Respeaker onboard
    entity_category: config
    icon: "mdi:circle-outline"
    default_transition_length: 0ms
    restore_mode: RESTORE_DEFAULT_OFF
    on_turn_off:
      - script.execute: control_leds
    initial_state:
      color_mode: rgb
      brightness: 100%
      red: 9.4%
      green: 73.3%
      blue: 94.9%
    segments:
      - id: led_internal
        from: 0
        to: 0
    effects:
      - addressable_lambda:
          name: "Fast Pulse"
          update_interval: 10ms
          lambda: |-
            static float fraction = 0.0;
            static float step = 0.05;
            static bool increasing = true;

            auto values = id(led_respeaker_onboard)->current_values;
            Color color(values.get_red() * 255, values.get_green() * 255, values.get_blue() * 255);
            it[0].set_rgb(color.red * fraction, 
                             color.green * fraction, 
                             color.blue * fraction);
            
            fraction += (step * (increasing ? 1 : -1));
            if (fraction > 1.0) {
              fraction = 1.0;
              increasing = !increasing;
            } else if (fraction < 0.0) {
              fraction = 0.0;
              increasing = !increasing;
            }

      - addressable_lambda:
          name: "Slow Pulse"
          update_interval: 30ms
          lambda: |-
            static float fraction = 0.0;
            static float step = 0.05;
            static bool increasing = true;

            auto values = id(led_respeaker_onboard)->current_values;
            Color color(values.get_red() * 255, values.get_green() * 255, values.get_blue() * 255);
            it[0].set_rgb(color.red * fraction, 
                             color.green * fraction, 
                             color.blue * fraction);
            
            fraction += (step * (increasing ? 1 : -1));
            if (fraction > 1.0) {
              fraction = 1.0;
              increasing = !increasing;
            } else if (fraction < 0.0) {
              fraction = 0.0;
              increasing = !increasing;
            }
      - strobe:
          name: "Wifi connected"
          colors:
            - state: true
              duration: 250ms
              red: 0%
              blue: 12%
              green: 12%
              brightness: 12%
            - state: true
              duration: 500ms
              red: 0%
              blue: 25%
              green: 25%
              brightness: 25%
            - state: true
              duration: 250ms
              red: 0%
              blue: 12%
              green: 12%
              brightness: 12%
            - state: false
              duration: 500ms
      - strobe:
          name: "Slow Blink"
          colors:
            - state: true
              duration: 500ms
              red: 25%
              blue: 25%
              green: 0%              
            - state: false
              duration: 500ms
      - strobe:
          name: "Slow Blink Blue"
          colors:
            - state: true
              duration: 500ms
              red: 0%
              blue: 25%
              green: 0%
            - state: false
              duration: 500ms
      - strobe:
          name: "Fast Blink Blue"
          colors:
            - state: true
              duration: 100ms
              red: 0%
              blue: 25%
              green: 0%
            - state: false
              duration: 100ms
      - strobe:
          name: "Fast Blink"
          colors:
            - state: true
              duration: 100ms
            - state: false
              duration: 100ms            


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"
  - platform: adc
    id: volume_control
    pin: ${POT_PIN}
    name: "Volume Control"
    update_interval: 200ms      
    attenuation: auto
    internal: False
    # on_value: 
    #   then:
    #     - media_player.volume_set: !lambda "return (roundf(id(volume_control).state * 10.0f) / 10.0f) / 3.3;"
        #- media_player.set_volume:
        #  id: external_media_player
        #  volume: | lambda
        #    return id(volume_control).value / 3.3;
        # lambda: |-
        #   float raw = id(volume_control).state;
        #   id(external_media_player).volume = raw / 3.3;
    filters:
      - sliding_window_moving_average:
          window_size: 10
          send_every: 1
  - platform: sound_level
    microphone: i2s_mics
    passive: True
    peak:
      name: "Peak Loudness"
    rms:
      name: "Average Loudness"
  - platform: template
    id: volume_stepped_percent
    name: Volume Control Percent
    update_interval: 200ms
    internal: False
    lambda: |-
      return roundf((id(volume_control).state/3.3) * 10.0)/10.0;
    on_value: 
      then:
        - media_player.volume_set: !lambda "return id(volume_stepped_percent).state;"
text_sensor:
  - platform: template
    id: next_timer_name
    name: "Next timer name"
    icon: "mdi:timer"
    disabled_by_default: true
  - platform: template
    name: "Current device time"
    id: current_time
    icon: mdi:clock
  - platform: debug
    device:
      name: "Device Info"
  - platform: wifi_info
    ip_address:
      name: "ESP IP Address"
      id: "wifi_ip_address"
    ssid:
      name: "ESP Connected SSID"
      id: "wifi_ssid"
      internal: true
    bssid:
      name: "ESP Connected BSSID"
      internal: true
  - id: text_request
    platform: template
    name: "Text Request"
    on_value:
      lambda: |-
        if(id(text_request).state.length()>30) {
          std::string name = id(text_request).state.c_str();
          std::string truncated = esphome::str_truncate(name.c_str(),31);
          id(text_request).state = (truncated+"...").c_str();
        }

  - id: text_response
    platform: template
    name: "Text Response"
    on_value:
      lambda: |-
        if(id(text_response).state.length()>30) {
          std::string name = id(text_response).state.c_str();
          std::string truncated = esphome::str_truncate(name.c_str(),31);
          id(text_response).state = (truncated+"...").c_str();
        }    
event:
  # Event entity exposed to the user to automate on complex center button presses.
  # The simple press is not exposed as it is used to control the device itself.
  - platform: template
    id: button_press_event
    name: "Button press"
    icon: mdi:button-pointer
    device_class: button
    event_types:
      - double_press
      - triple_press
      - long_press

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(improv_ble_in_progress)) {
            id(control_leds_improv_ble_state).execute();
          } else 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(user_button).state) {
            id(control_leds_center_button_touched).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(alarm_on).state && !id(led_respeaker_onboard).remote_values.is_on()) {
            id(control_leds_alarm_active).execute();
          } else if (id(voice_assistant_phase) == ${voice_assist_idle_phase_id}) {
            id(control_leds_voice_assistant_idle_phase).execute();
          }


TBC in the next post...

  # Warm White slow pulse
  - id: control_leds_improv_ble_state
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f );
          red: 100%
          green: 89%
          blue: 71%
          id: led_internal
          effect: "Slow Pulse"

  # Script executed during initialization
  # Fast Blue pulse if Wifi is connected, Else slow blue pulse
  - id: control_leds_init_state
    then:
      - if:
          condition:
            wifi.connected:
          then:
            - light.turn_on:
                brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f );
                red: 9%
                green: 73%
                blue: 95%
                id: led_internal
                effect: "Fast Pulse"
            - delay: 2s
            - light.turn_on:
                id: led_internal
                effect: none
          else:
            - light.turn_on:
                brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f );
                red: 9%
                green: 73%
                blue: 95%
                id: led_internal
                effect: "Slow Pulse"
  # Script executed when the device has no connection to Home Assistant
  # Red slow pulse (This will be visible during HA updates for example)
  - id: control_leds_no_ha_connection_state
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f );
          red: 1
          green: 0
          blue: 0
          id: led_internal
          effect: "Slow Pulse"

  # Script executed when the voice assistant is idle (waiting for a wake word)
  # Nothing
  - id: control_leds_voice_assistant_idle_phase
    then:
      - light.turn_off: led_internal
      - if:
          condition:
            light.is_on: led_respeaker_onboard
          then:
            light.turn_on: led_respeaker_onboard

  # Script executed when the voice assistant is waiting for a command (After the wake word)
  # Slow purple pulse
  - id: control_leds_voice_assistant_waiting_for_command_phase
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f );
          red: 1
          green: 0.2
          blue: 1
          id: led_internal
          effect: "Slow Pulse"

  # Script executed when the voice assistant is listening to a command
  # Slow purple pulse
  - id: control_leds_voice_assistant_listening_for_command_phase
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f );
          red: 1
          green: 0.2
          blue: 1
          id: led_internal
          effect: "Slow Pulse"

  # Script executed when the voice assistant is thinking to a command
  # Fast purple pulse
  - id: control_leds_voice_assistant_thinking_phase
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f );
          red: 1
          green: 0.2
          blue: 1
          id: led_internal
          effect: "Fast Pulse"

  # Script executed when the voice assistant is replying to a command
  # Slow cyan pulse
  - id: control_leds_voice_assistant_replying_phase
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f );
          red: 0.2
          green: 1
          blue: 1
          id: led_internal
          effect: "Slow Pulse"

  # 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 max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f );
          red: 1
          green: 0
          blue: 0
          id: led_internal
          effect: "Fast Pulse"

  # Script executed when the voice assistant is not ready
  - id: control_leds_voice_assistant_not_ready_phase
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f );
          red: 1
          green: 0
          blue: 0
          id: led_internal
          effect: "Slow Pulse"

  # Script executed when the center button is touched
  # The LED turns on blue
  - id: control_leds_center_button_touched
    then:
      - light.turn_on:
          brightness: !lambda return min ( max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f );
          red: 0
          green: 0
          blue: 1
          id: led_internal
          effect: "None"

  # Script executed when the timer is ringing, to control the LEDs
  # The LED blinks green.
  - id: control_leds_timer_ringing
    then:
      - light.turn_on:
          brightness: !lambda return min ( max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f ) + 0.1f , 1.0f );
          red: 0
          green: 1
          blue: 0
          id: led_internal
          effect: "Fast Pulse"

  # Script executed when the timer is ticking, to control the LEDs
  # Slow dim while pulse.
  - id: control_leds_timer_ticking
    then:
      - light.turn_on:
          brightness: !lambda return max( id(led_respeaker_onboard).current_values.get_brightness() , 0.2f );
          red: 0.3
          green: 0.3
          blue: 0.3
          id: led_internal
          effect: "Slow Pulse"
  
  # Script executed when the alarm is active
  # The LED turns on dim green
  - id: control_leds_alarm_active
    then:
      - light.turn_on:
          brightness: !lambda return 0.3f;
          red: 0
          green: 1
          blue: 0
          id: led_internal
          effect: "None"


  # 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:
      # Turn on the repeat mode and pause for 500 ms between playlist items/repeats
      - media_player.repeat_one:
          id: external_media_player
          announcement: true
      - speaker_source.set_playlist_delay:
          id: external_media_player
          pipeline: announcement
          delay: 500ms

  # 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
      - speaker_source.set_playlist_delay:
          id: external_media_player
          pipeline: announcement
          delay: 0ms

  # 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("audio-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) {
            auto alarm_dt = id(alarm_time).state_as_esptime();
            auto time_now = id(homeassistant_time).now();

            if (time_now.hour == alarm_dt.hour && time_now.minute == alarm_dt.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();
              }
            }
          }

  - 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
    then:
      - lambda: |-
          // Publish current time
          auto time_now = id(homeassistant_time).now();
          id(current_time).publish_state(time_now.strftime("%H:%M"));

i2s_audio:
  - id: i2s_output # pair of MAX98357A
    i2s_lrclk_pin: 
      number: ${i2s_lrclk} # LRC Pin of the Max98357 = GREEN
    i2s_bclk_pin: 
      number: ${i2s_bclk} # BCLK Pin of the Max98357 = WHITE
  - id: i2s_input # inmp441
    i2s_lrclk_pin: # (WS)
      number: ${INMP441_WS} # (WS) Green > Green (l/r should be ground?)
    i2s_bclk_pin: # (SCK)
      number: ${INMP441_SCK} # (SCK) > Orange > Blue 

microphone:
  - platform: i2s_audio
    id: i2s_mics
    i2s_din_pin: # (SD)
      number: ${INMP441_SD} # Green (sd)
      # allow_other_uses: True
    adc_type: external
    pdm: false
    # sample_rate: 48000
    #mclk_multiple: 256
    bits_per_sample: 16bit
    num_channels: 1
    # i2s_mode: secondary
    i2s_audio_id: i2s_input
    channel: right
speaker:
  # Hardware speaker output
  - platform: i2s_audio
    id: i2s_audio_speaker
    sample_rate: 48000
    #i2s_mode: secondary
    i2s_dout_pin: ${spk}   #  DIN Pin of the MAX98357A Audio Amplifier = GREEN
    #bits_per_sample: 32bit
    i2s_audio_id: i2s_output
    dac_type: external
    channel: stereo
    #timeout: never
    #buffer_duration: 100ms

  # 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: false
    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: false

audio_file:
  # - id: center_button_press_sound
  #   file: ${center_button_press_sound_file}
  # - id: center_button_double_press_sound
  #   file: ${center_button_double_press_sound_file}
  # - id: center_button_triple_press_sound
  #   file: ${center_button_triple_press_sound_file}
  # - id: center_button_long_press_sound
  #   file: ${center_button_long_press_sound_file}
  # - id: factory_reset_initiated_sound
  #   file: ${factory_reset_initiated_sound_file}
  # - id: factory_reset_cancelled_sound
  #   file: ${factory_reset_cancelled_sound_file}
  # - id: factory_reset_confirmed_sound
  #   file: ${factory_reset_confirmed_sound_file}
  - 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_source:
  - platform: audio_file
    id: audio_file_announcement_source
  - platform: audio_http
    id: http_announcement_source
    buffer_size: 250000
  - platform: audio_http
    id: http_media_source
    buffer_size: 500000
#  - platform: sendspin
#    id: sendspin_media_source
#    fixed_delay: 480 microseconds  # The AIC3204 DAC used, as configured, on the VPE delays audio by 480 microseconds

media_player:
  # - platform: sendspin
  #    id: sendspin_group_media_player
  # - platform: speaker
  #   name: "Speaker Media Player"
  #   id: speaker_media_player_id
  #   media_pipeline:
  #       speaker: media_spk_resampling_input
  #       num_channels: 2
  #   announcement_pipeline:
  #       speaker: media_resampling_speaker
  #       num_channels: 2
  #   files:
  #     - id: alarm_sound
  #       # https://github.com/sfiera/flac-test-files/blob/9f9caf867883aa7066b74f64c82185364fd070ef/stereo.flac
  #       file: stereo.flac # Placed in the yaml directory. Should be encoded with a 48000 Hz sample rate, mono or stereo audio, and 16 bits per sample.
  - platform: speaker_source
    id: external_media_player
    name: Media Player
    announcement_pipeline:
      format: FLAC     # FLAC is the least processor intensive codec
      num_channels: 1  # Stereo audio is unnecessary for announcements
      sample_rate: 48000
      speaker: announcement_resampling_speaker
      sources:
        - audio_file_announcement_source
        - http_announcement_source
    media_pipeline:
      format: FLAC     # FLAC is the least processor intensive codec
      num_channels: 2
      sample_rate: 48000
      speaker: media_resampling_speaker
      sources:
        - http_media_source
#        - sendspin_media_source
    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
micro_wake_word:
  id: mww
  microphone:
    microphone: i2s_mics
  #  channels: 1
    gain_factor: 4
  stop_after_detection: false
  models:
    - model: okay_nabu
      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 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"
    id: 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: [email protected], 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
    entity_category: config
    name: Wake word engine location
    id: wake_word_engine_location
    optimistic: true
    restore_value: true
    options:
      - In Home Assistant
      - On device
    initial_option: On device
    on_value:
      - if:
          condition:
            lambda: return x == "In Home Assistant";
          then:
            - micro_wake_word.stop:
            - delay: 500ms
            - lambda: id(va).set_use_wake_word(true);
            - voice_assistant.start_continuous:
      - if:
          condition:
            lambda: return x == "On device";
          then:
            - lambda: id(va).set_use_wake_word(false);
            - voice_assistant.stop:
            - delay: 500ms
            - micro_wake_word.start:

voice_assistant:
  id: va
  microphone:
    microphone: i2s_mics
    channels: 0
    gain_factor: 4
  media_player: external_media_player
  micro_wake_word: mww
  noise_suppression_level: 2
  auto_gain: 31dBFS
  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:
    - script.execute: control_leds
    - lambda: |
        id(next_timer).publish_state(id(first_active_timer).seconds_left);
        id(next_timer_name).publish_state(id(first_active_timer).name);
  on_timer_cancelled:
    - script.execute: control_leds
    - lambda: |
        id(next_timer).publish_state(id(first_active_timer).seconds_left);
        id(next_timer_name).publish_state(id(first_active_timer).name);
  on_timer_updated:
    - script.execute: control_leds
    - lambda: |
        id(next_timer).publish_state(id(first_active_timer).seconds_left);
        id(next_timer_name).publish_state(id(first_active_timer).name);
  on_timer_tick:
    - script.execute: control_leds
    - lambda: | 
        int seconds_left = id(first_active_timer).seconds_left;
        if (std::abs(seconds_left) % 5 == 0) {
          id(next_timer).publish_state(seconds_left); 
        }

debug:
  update_interval: 5s