Link M5 Atom Echo to external speaker

in the end I found my final configuration… the 8ohm 3W speaker gives a great change in the volume of the device making it decidedly very acceptable and functional for a room without adding other cables or complicated connections.

You just have to solder the two cables and the result is great (the speaker is the 8Ohm version posted above).

if you remove the Reset button of the case you can also unplug the wires and put the cover back on. (alternatively you can also connect two 4Ohm speakers in parallel to bring the resistance to 8Ohm for a more diffused effect)

3 Likes

Hi,
I’m very interested in your version, but could you please look at 25.6.1 version in which we have fully functional countinued conversation mode? Could you incorporate your changes to this yaml version retaining continued conversation mode?

Small followup: I’ve managed to modify the newest yaml (25.6.1) incorporating your changes and everything is working like a charm since 2 days :slight_smile: Here is the modified yaml:

substitutions:
  name: atom-echo-sypialnia
  friendly_name: Atom Echo sypialnia

esphome:
  name: ${name}
  name_add_mac_suffix: false
  friendly_name: ${friendly_name}
  min_version: 2025.5.0

esp32:
  board: m5stack-atom
  cpu_frequency: 240MHz
  framework:
    type: esp-idf

logger:
api:
  encryption:
    key: "your key"

ota:
  - platform: esphome
    password: "your password"
    id: ota_esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "Atom-Echo-Sypialnia"
    password: "your password"

captive_portal:

button:
  - platform: factory_reset
    id: factory_reset_btn
    name: Factory reset

i2s_audio:
  - id: i2s_audio_bus
    i2s_lrclk_pin: GPIO33
    i2s_bclk_pin: GPIO19

microphone:
  - platform: i2s_audio
    id: echo_microphone
    i2s_din_pin: GPIO23
    adc_type: external
    pdm: true
    sample_rate: 16000
    correct_dc_offset: true

speaker:
  - platform: i2s_audio
    id: echo_speaker
    i2s_dout_pin: GPIO22
    dac_type: external
    bits_per_sample: 16bit
    sample_rate: 16000
    channel: stereo  # The Echo has poor playback audio quality when using mon audio
    buffer_duration: 60ms

media_player:
  - platform: speaker
    name: None
    id: echo_media_player
    announcement_pipeline:
      speaker: echo_speaker
      format: WAV
    codec_support_enabled: false
    buffer_size: 6000
    files:
      - id: timer_finished_wave_file
        file: https://github.com/esphome/wake-word-voice-assistants/raw/main/sounds/timer_finished.wav
    on_announcement:
      - if:
          condition:
            - microphone.is_capturing:
          then:
            - script.execute: stop_wake_word
      - light.turn_on:
          id: led
          blue: 100%
          red: 0%
          green: 0%
          brightness: 100%
          effect: none
    on_idle:
      - script.execute: start_wake_word
      - script.execute: reset_led

voice_assistant:
  id: va
  micro_wake_word:
  microphone:
    microphone: echo_microphone
    channels: 0
    gain_factor: 4
  media_player: echo_media_player
  noise_suppression_level: 2
  auto_gain: 31dBFS
  on_listening:
    - light.turn_on:
        id: led
        blue: 100%
        red: 0%
        green: 0%
        effect: "Slow Pulse"
  on_stt_vad_end:
    - light.turn_on:
        id: led
        blue: 100%
        red: 0%
        green: 0%
        effect: "Fast Pulse"
  on_tts_start:
    - if:
        condition:
          switch.is_on: call_script_message
        then:
          - logger.log: "calling script with message (instead of playing directly to media player)"
          - homeassistant.service:
              service: !lambda 'return id(script_id).state.c_str();'
              data:
                message: !lambda 'return x;'
        else:
          - if:
              condition:
                switch.is_on: use_remote_media_player
              then:
                - logger.log: "using remote media player"
                - speaker.volume_set: 10%
                - homeassistant.service:
                    service: tts.cloud_say
                    data:
                      entity_id: !lambda 'return id(media_player_entity_id).state.c_str();'
                      message: !lambda 'return x;'
              else:
                - logger.log: "using internal speaker"
                - speaker.volume_set: 100%
    - light.turn_on:
        id: led
        blue: 100%
        red: 0%
        green: 0%
        brightness: 100%
        effect: none
  on_end:
    # Handle the "nevermind" case where there is no announcement
    - wait_until:
        condition:
          - media_player.is_announcing:
        timeout: 0.5s
    # Restart only mWW if enabled; streaming wake words automatically restart
    - if:
        condition:
          - lambda: return id(wake_word_engine_location).state == "On device";
        then:
          - wait_until:
              - and:
                  - not:
                      voice_assistant.is_running:
                  - not:
                      speaker.is_playing:
          - lambda: id(va).set_use_wake_word(false);
          - micro_wake_word.start:
    - script.execute: reset_led
  on_error:
    - light.turn_on:
        id: led
        red: 100%
        green: 0%
        blue: 0%
        brightness: 100%
        effect: none
    - delay: 2s
    - script.execute: reset_led
  on_client_connected:
    - delay: 2s  # Give the api server time to settle
    - script.execute: start_wake_word
  on_client_disconnected:
    - script.execute: stop_wake_word
  on_timer_finished:
    - script.execute: stop_wake_word
    - wait_until:
        not:
          microphone.is_capturing:
    - speaker.volume_set: 100%
    - switch.turn_on: timer_ringing
    - light.turn_on:
        id: led
        red: 0%
        green: 100%
        blue: 0%
        brightness: 100%
        effect: "Fast Pulse"
    - if:
        condition:
          switch.is_on: call_alarm_script
        then:
          - logger.log: "calling alarm script"
          - homeassistant.service:
              service: !lambda 'return id(alarm_script_id).state.c_str();'
    - wait_until:
        - switch.is_off: timer_ringing
    - light.turn_off: led
    - switch.turn_off: timer_ringing
    - speaker.volume_set: 10%

binary_sensor:
  # button does the following:
  # short click - stop a timer
  # if no timer then restart either microwakeword or voice assistant continuous
  - platform: gpio
    pin:
      number: GPIO39
      inverted: true
    name: Button
    disabled_by_default: true
    entity_category: diagnostic
    id: echo_button
    on_multi_click:
      - timing:
          - ON for at least 50ms
          - OFF for at least 50ms
        then:
          - logger.log: "single button short click"
          - if:
              condition:
                switch.is_on: timer_ringing
              then:
                - switch.turn_off: timer_ringing
              else:
                - script.execute: start_wake_word
      - timing:
          - ON for at least 10s
        then:
          - logger.log: "single button long click"
          - button.press: factory_reset_btn

light:
  - platform: esp32_rmt_led_strip
    id: led
    name: None
    disabled_by_default: true
    entity_category: config
    pin: GPIO27
    default_transition_length: 0s
    chipset: SK6812
    num_leds: 1
    rgb_order: grb
    effects:
      - pulse:
          name: "Slow Pulse"
          transition_length: 250ms
          update_interval: 250ms
          min_brightness: 50%
          max_brightness: 100%
      - pulse:
          name: "Fast Pulse"
          transition_length: 100ms
          update_interval: 100ms
          min_brightness: 50%
          max_brightness: 100%

script:
  - id: reset_led
    then:
      - if:
          condition:
            - lambda: return id(wake_word_engine_location).state == "On device";
            - switch.is_on: use_listen_light
          then:
            - light.turn_on:
                id: led
                red: 100%
                green: 89%
                blue: 71%
                brightness: 60%
                effect: none
          else:
            - if:
                condition:
                  - lambda: return id(wake_word_engine_location).state != "On device";
                  - switch.is_on: use_listen_light
                then:
                  - light.turn_on:
                      id: led
                      red: 0%
                      green: 100%
                      blue: 100%
                      brightness: 60%
                      effect: none
                else:
                  - light.turn_off: led
  - id: start_wake_word
    then:
      - if:
          condition:
            and:
              - not:
                  - voice_assistant.is_running:
              - lambda: return id(wake_word_engine_location).state == "On device";
          then:
            - lambda: id(va).set_use_wake_word(false);
            - micro_wake_word.start:
      - if:
          condition:
            and:
              - not:
                  - voice_assistant.is_running:
              - lambda: return id(wake_word_engine_location).state == "In Home Assistant";
          then:
            - lambda: id(va).set_use_wake_word(true);
            - voice_assistant.start_continuous:
  - id: stop_wake_word
    then:
      - if:
          condition:
            lambda: return id(wake_word_engine_location).state == "In Home Assistant";
          then:
            - lambda: id(va).set_use_wake_word(false);
            - voice_assistant.stop:
      - if:
          condition:
            lambda: return id(wake_word_engine_location).state == "On device";
          then:
            - micro_wake_word.stop:

switch:
  - platform: template
    name: Use listen light
    id: use_listen_light
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON
    entity_category: config
    on_turn_on:
      - script.execute: reset_led
    on_turn_off:
      - script.execute: reset_led
  - platform: template
    name: Use remote media player
    id: use_remote_media_player
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON
    entity_category: config
    on_turn_on:
      - logger.log: "use remote media player turned on"
      - logger.log:
          format: "current media player entity: %s"
          args: ["id(media_player_entity_id).state.c_str()"]
    on_turn_off:
      - logger.log: "use remote media player turned off"
      - logger.log:
          format: "current media player entity: %s"
          args: ["id(media_player_entity_id).state.c_str()"]
  - platform: template
    name: Call script with message
    id: call_script_message
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON
    entity_category: config
    on_turn_on:
      - logger.log: "call script with message turned on"
    on_turn_off:
      - logger.log: "call script with message turned off"
  - platform: template
    name: Call alarm script
    id: call_alarm_script
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON
    entity_category: config
    on_turn_on:
      - logger.log: "call alarm script turned on"
    on_turn_off:
      - logger.log: "call alarm script turned off"
  - platform: template
    id: timer_ringing
    optimistic: true
    restore_mode: ALWAYS_OFF
    on_turn_off:
      # Turn off the repeat mode and disable the pause between playlist items
      - lambda: |-
              id(echo_media_player)
                ->make_call()
                .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF)
                .set_announcement(true)
                .perform();
              id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0);
      # Stop playing the alarm
      - media_player.stop:
          announcement: true
    on_turn_on:
      # Turn on the repeat mode and pause for 1000 ms between playlist items/repeats
      - lambda: |-
            id(echo_media_player)
              ->make_call()
              .set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE)
              .set_announcement(true)
              .perform();
            id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000);
      - media_player.speaker.play_on_device_media_file:
          media_file: timer_finished_wave_file
          announcement: true
      - delay: 15min
      - switch.turn_off: timer_ringing

text:
  - platform: template
    name: Media Player Entity ID
    id: media_player_entity_id
    mode: text
    optimistic: true
    restore_value: true
    entity_category: config
    on_value:
      then:
        - logger.log:
            format: "Set new media player entity: %s"
            args: ["x.c_str()"]
  - platform: template
    name: Script ID
    id: script_id
    mode: text
    optimistic: true
    restore_value: true
    entity_category: config
    on_value:
      then:
        - logger.log:
            format: "Set new script: %s"
            args: ["x.c_str()"]
  - platform: template
    name: Script on Alarm ID
    id: alarm_script_id
    mode: text
    optimistic: true
    restore_value: true
    entity_category: config
    on_value:
      then:
        - logger.log:
            format: "Set new alarm script: %s"
            args: ["x.c_str()"]

select:
  - 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:

micro_wake_word:
  on_wake_word_detected:
    - voice_assistant.start:
        wake_word: !lambda return wake_word;
  vad:
  models:
    - model: okay_nabu
    - model: hey_mycroft
    - model: hey_jarvis

Now, thanks to you, I’m totally happy with my two echos. One of them is constantly switched to an external speaker via VLC and another is linked to my Windows computer speakers via HASS.Agent and switched back to internal echo speaker by an automation when the HASS.Agent player becomes unavalilable (after Windows shutting down). Both are working with continued conversation with zero problems :slight_smile: Thank you for your yaml :slight_smile: