Below code was working fine until I added the bluetooth_proxy and esp32_ble_tracker lines.
BTW, below code was generated by Claude AI by adapting official ESPHome code for the M5Stack Atom Echo to allow to send output sound to another external player.
Any idea why adding these two lines breaks everything?
(And by that I really mean that code compiles, is flashed correctly and then breaks MWW because of those two lines)
substitutions:
name: m5stack-atom-echo
friendly_name: M5Stack Atom Echo
esphome:
name: ${name}
name_add_mac_suffix: true
friendly_name: ${friendly_name}
min_version: 2025.5.0
esp32:
board: m5stack-atom
cpu_frequency: 240MHz
framework:
type: esp-idf
logger:
api:
encryption:
key: ${api_key}
ota:
- platform: esphome
id: ota_esphome
password: ${ota_password}
wifi:
ssid: !secret wifi_IoT_ssid
password: !secret wifi_IoT_password
min_auth_mode: WPA2
esp32_ble_tracker:
scan_parameters:
active: true
bluetooth_proxy:
active: true
# True when the current TTS response must be routed to the external speaker.
globals:
- id: tts_to_external
type: bool
restore_value: false
initial_value: "false"
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
buffer_duration: 60ms
media_player:
- platform: speaker
name: None
id: echo_media_player
announcement_pipeline:
speaker: echo_speaker
format: WAV
buffer_size: 6000
volume_min: 0.4
files:
- id: timer_finished_wave_file
file: https://github.com/esphome/wake-word-voice-assistants/raw/main/sounds/timer_finished.wav
on_announcement:
# Original behaviour: stop the wake word if the microphone is capturing
- 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
# -----------------------------------------------------------------------
# EXTERNAL SPEAKER ROUTING
#
# on_announcement fires exactly when echo_media_player enters the
# ANNOUNCING state โ before the I2S DMA has had time to emit any
# significant audio (a few ms depending on buffer size).
#
# We immediately stop the internal announcement and set media_player to
# null so the voice_assistant stops feeding the pipeline.
# HA sees the ANNOUNCING โ IDLE transition and releases the
# assist_satellite from the "responding" state.
# The TTS is played via tts.speak on the external speaker (launched in
# on_tts_start).
#
# on_tts_end restores the media_player pointer so the voice_assistant
# can finish its pipeline normally (on_end + wake word restart).
# -----------------------------------------------------------------------
- if:
condition:
lambda: return id(tts_to_external);
then:
# Detach first to block further TTS data, then stop the pipeline
- lambda: |-
id(tts_to_external) = false;
id(va)->set_media_player(nullptr);
- media_player.stop:
id: echo_media_player
announcement: true
on_idle:
- script.execute: start_wake_word
- script.execute: reset_led
voice_assistant:
id: va
microphone:
microphone: echo_microphone
channels: 0
gain_factor: 4
micro_wake_word:
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:
- light.turn_on:
id: led
blue: 100%
red: 0%
green: 0%
brightness: 100%
effect: none
- if:
condition:
lambda: return !id(external_speaker_entity).state.empty();
then:
- lambda: id(tts_to_external) = true;
- homeassistant.service:
service: tts.speak
data_template:
entity_id: "{{ engine }}"
message: "{{ message }}"
media_player_entity_id: "{{ player }}"
variables:
engine: !lambda return id(tts_engine_entity).state;
message: !lambda return x;
player: !lambda return id(external_speaker_entity).state;
else:
- lambda: id(tts_to_external) = false;
on_tts_end:
# Restore the media_player before C++ processes TTS_END, so that
# signal_end_() is called normally and on_end fires as expected.
- lambda: id(va)->set_media_player(id(echo_media_player));
on_end:
# Original logic unchanged.
- wait_until:
condition:
- media_player.is_announcing:
timeout: 0.5s
- if:
condition:
- lambda: |-
return id(wake_word_engine_location).current_option() == "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
- lambda: |-
id(tts_to_external) = false;
id(va)->set_media_player(id(echo_media_player));
on_client_connected:
- delay: 2s
- script.execute: start_wake_word
on_client_disconnected:
- script.execute: stop_wake_word
- lambda: |-
id(tts_to_external) = false;
id(va)->set_media_player(id(echo_media_player));
on_timer_finished:
- script.execute: stop_wake_word
- wait_until:
not:
microphone.is_capturing:
- switch.turn_on: timer_ringing
- light.turn_on:
id: led
red: 0%
green: 100%
blue: 0%
brightness: 100%
effect: "Fast Pulse"
- wait_until:
- switch.is_off: timer_ringing
- light.turn_off: led
- switch.turn_off: timer_ringing
binary_sensor:
- 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:
- 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:
- 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%
text:
# HA entity ID of the media_player to use for voice assistant responses.
# Example: media_player.lenovo_smart_clock
# Leave empty to use the M5Stack Atom Echo's internal speaker.
- platform: template
name: "External Speaker Entity ID"
id: external_speaker_entity
optimistic: true
restore_value: true
initial_value: ""
mode: text
entity_category: config
icon: mdi:speaker-wireless
# Entity ID of the TTS engine used for the external speaker.
# Common examples: tts.home_assistant (Piper), tts.cloud (Nabu Casa)
# To find yours: HA โ Settings โ Devices & services โ Entities,
# filter by domain "tts".
- platform: template
name: "TTS Engine Entity ID"
id: tts_engine_entity
optimistic: true
restore_value: true
initial_value: "tts.home_assistant_cloud"
mode: text
entity_category: config
icon: mdi:text-to-speech
script:
- id: reset_led
then:
- if:
condition:
- lambda: |-
return id(wake_word_engine_location).current_option() == "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).current_option() == "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).current_option() == "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).current_option() == "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).current_option() == "In Home Assistant";
then:
- lambda: id(va).set_use_wake_word(false);
- voice_assistant.stop:
- if:
condition:
lambda: |-
return id(wake_word_engine_location).current_option() == "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
id: timer_ringing
optimistic: true
restore_mode: ALWAYS_OFF
on_turn_off:
- 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);
- media_player.stop:
announcement: true
on_turn_on:
- 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
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