Connect your device via USB and click the button below to install.
---
substitutions:
name: zero-va-sendspin
friendly_name: Zero-VA-Sendspin
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2026.5.0
name_add_mac_suffix: true
project:
name: realdeco.zero_va
version: "2026.5.0"
esp32:
variant: esp32s3
flash_size: 4MB
cpu_frequency: 240MHz
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"
psram:
mode: quad
speed: 80MHz
api:
on_client_connected:
- script.execute: stop_wake_word
- delay: 2000ms
- lambda: |-
if (!id(boot_sound_played)) {
id(boot_sound_played) = true;
if (id(startup_sound_switch).state) {
id(play_sound)->execute(true, std::string("ready_sound"));
}
}
ota:
- platform: esphome
id: ota_esphome
logger:
level: WARN
wifi:
ap:
improv_serial:
captive_portal:
globals:
- id: playing_internal_sound
type: bool
restore_value: no
initial_value: "false"
- id: boot_sound_played
type: bool
restore_value: no
initial_value: "false"
button:
- platform: factory_reset
id: factory_reset_btn
internal: true
binary_sensor:
- platform: gpio
pin:
number: GPIO0
ignore_strapping_warning: true
mode: INPUT_PULLUP
inverted: true
id: center_button
internal: true
on_multi_click:
- timing:
- ON for at least 50ms
- OFF for at least 50ms
then:
- script.execute: reset_led
- if:
condition:
switch.is_on: timer_ringing
then:
- switch.turn_off: timer_ringing
else:
- if:
condition:
not:
voice_assistant.is_running:
then:
- voice_assistant.start:
else:
- voice_assistant.stop:
- timing:
- ON for at least 10s
then:
- button.press: factory_reset_btn
i2c:
- id: i2c_adc_dac
scl: GPIO8
sda: GPIO7
i2s_audio:
- id: i2s_audio_bus
i2s_lrclk_pin:
number: GPIO3
ignore_strapping_warning: true
i2s_bclk_pin: GPIO5
i2s_mclk_pin: GPIO6
audio_dac:
- platform: es8311
i2c_id: i2c_adc_dac
id: es8311_dac
bits_per_sample: 16bit
sample_rate: 48000
microphone:
- platform: i2s_audio
id: box_mic
sample_rate: 16000
i2s_din_pin: GPIO4
bits_per_sample: 16bit
adc_type: external
speaker:
- platform: i2s_audio
id: box_speaker
i2s_dout_pin: GPIO2
dac_type: external
sample_rate: 48000
bits_per_sample: 16bit
channel: left
audio_dac: es8311_dac
buffer_duration: 100ms
- platform: mixer
id: mixing_speaker
output_speaker: box_speaker
num_channels: 2
task_stack_in_psram: true
source_speakers:
- id: announcement_mixing_input
- id: media_mixing_input
- 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
sendspin:
id: sendspin_hub
task_stack_in_psram: false
audio_file:
- id: timer_finished_sound
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac
- id: ready_sound
file: https://github.com/RealDeco/xiaozhi-esphome/raw/main/sounds/popup.flac
media_source:
- platform: audio_file
id: audio_file_announcement_source
- platform: audio_http
id: http_announcement_source
- platform: audio_http
id: http_media_source
- platform: sendspin
id: sendspin_media_source
media_player:
- platform: sendspin
id: sendspin_group_media_player
name: Sendspin Group Media Player
- platform: speaker_source
name: None
id: speaker_media_player
announcement_pipeline:
format: FLAC
sample_rate: 48000
num_channels: 1
speaker: announcement_resampling_speaker
sources:
- audio_file_announcement_source
- http_announcement_source
media_pipeline:
format: FLAC
sample_rate: 48000
num_channels: 2
speaker: media_resampling_speaker
sources:
- http_media_source
- sendspin_media_source
volume_increment: 0.05
volume_min: 0.4
volume_max: 0.85
on_announcement:
- script.stop: mp_idle_debounce
- if:
condition:
- microphone.is_capturing:
then:
- script.execute: stop_wake_word
- if:
condition:
- lambda: return id(wake_word_engine_location).current_option() == "In Home Assistant";
then:
- wait_until:
- not:
voice_assistant.is_running:
on_play:
- script.stop: mp_idle_debounce
- if:
condition:
- microphone.is_capturing:
then:
- script.execute: stop_wake_word
- if:
condition:
- lambda: return id(wake_word_engine_location).current_option() == "In Home Assistant";
then:
- wait_until:
condition:
- not:
voice_assistant.is_running:
- light.turn_on:
id: led
blue: 0%
red: 0%
green: 100%
effect: "Slow Pulse"
- if:
condition:
lambda: 'return id(playing_internal_sound);'
then:
- lambda: 'id(playing_internal_sound) = false;'
on_idle:
- script.execute: mp_idle_debounce
micro_wake_word:
id: mww
models:
- okay_nabu
- hey_mycroft
- hey_jarvis
on_wake_word_detected:
- voice_assistant.start:
wake_word: !lambda return wake_word;
voice_assistant:
id: va
microphone: box_mic
media_player: speaker_media_player
micro_wake_word: mww
noise_suppression_level: 2
auto_gain: 31dBFS
volume_multiplier: 2.0
on_listening:
- if:
condition:
- switch.is_on: use_listen_light
then:
- light.turn_on:
id: led
blue: 100%
red: 0%
green: 0%
effect: "Slow Pulse"
on_stt_vad_end:
- if:
condition:
- switch.is_on: use_listen_light
then:
- light.turn_on:
id: led
blue: 100%
red: 0%
green: 0%
effect: "Fast Pulse"
on_tts_start:
- if:
condition:
- switch.is_on: use_listen_light
then:
- light.turn_on:
id: led
blue: 100%
red: 0%
green: 0%
brightness: 100%
effect: none
on_end:
- wait_until:
condition:
media_player.is_announcing:
id: speaker_media_player
timeout: 0.5s
- wait_until:
- and:
- not:
media_player.is_announcing:
id: speaker_media_player
- not:
speaker.is_playing:
id: box_speaker
- if:
condition:
lambda: return id(wake_word_engine_location).current_option() == "On device";
then:
- lambda: id(va).set_use_wake_word(false);
- script.execute: start_wake_word
on_error:
- if:
condition:
switch.is_off: mute
then:
- script.execute: start_wake_word
- if:
condition:
- switch.is_on: use_listen_light
then:
- light.turn_on:
id: led
red: 100%
green: 0%
blue: 0%
brightness: 100%
effect: none
- delay: 2s
- script.execute: reset_led
on_client_connected:
- script.execute: start_wake_word
on_client_disconnected:
- script.execute: stop_wake_word
on_timer_finished:
- switch.turn_on: timer_ringing
- if:
condition:
- switch.is_on: use_listen_light
then:
- light.turn_on:
id: led
red: 0%
green: 100%
blue: 0%
brightness: 100%
effect: "Fast Pulse"
- wait_until:
media_player.is_announcing:
id: speaker_media_player
- if:
condition:
- switch.is_on: use_listen_light
then:
- light.turn_on:
id: led
blue: 0%
red: 100%
green: 0%
effect: "Fast Pulse"
script:
- id: mp_idle_debounce
mode: restart
then:
- delay: 1s
- if:
condition:
and:
- media_player.is_idle: speaker_media_player
- not:
voice_assistant.is_running:
then:
- script.execute: reset_led
- script.execute: start_wake_word
- 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: 30%
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: 30%
effect: none
else:
- light.turn_off: led
- id: start_wake_word
then:
- if:
condition:
and:
- switch.is_off: mute
- not:
- voice_assistant.is_running:
- not:
- micro_wake_word.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:
- switch.is_off: mute
- 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:
- id: ring_timer
then:
- script.execute: enable_repeat_one
- script.execute:
id: play_sound
priority: true
sound_file: "timer_finished_sound"
# Script executed when the timer is ringing, to repeat the timer finished sound.
- id: enable_repeat_one
then:
- media_player.repeat_one:
id: speaker_media_player
announcement: true
# Turn on the repeat mode and pause for 500 ms between playlist items/repeats
- speaker_source.set_playlist_delay:
id: speaker_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: speaker_media_player
announcement: true
- speaker_source.set_playlist_delay:
id: speaker_media_player
pipeline: announcement
delay: 0ms
- id: play_sound
parameters:
priority: bool
sound_file: string
then:
- lambda: |-
id(playing_internal_sound) = true;
- if:
condition:
lambda: return priority;
then:
- media_player.stop:
id: speaker_media_player
announcement: true
- lambda: |-
if ( (id(speaker_media_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING ) || priority) {
id(speaker_media_player)
->make_call()
.set_media_url("audio-file://" + sound_file)
.set_announcement(true)
.perform();
}
switch:
- platform: template
id: startup_sound_switch
name: Startup sound
icon: "mdi:card-text-outline"
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
- 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: gpio
name: Speaker Enable
pin:
number: GPIO1
restore_mode: RESTORE_DEFAULT_ON
entity_category: config
disabled_by_default: true
- platform: template
name: Mute
id: mute
icon: "mdi:microphone-off"
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
entity_category: config
on_turn_off:
- microphone.unmute:
- script.execute: start_wake_word
on_turn_on:
- script.execute: stop_wake_word
- microphone.mute:
- platform: template
id: timer_ringing
optimistic: true
internal: true
restore_mode: ALWAYS_OFF
on_turn_off:
- script.execute: disable_repeat
- if:
condition:
media_player.is_announcing:
id: speaker_media_player
then:
media_player.stop:
announcement: true
id: speaker_media_player
on_turn_on:
- script.execute: ring_timer
- delay: 15min
- switch.turn_off: timer_ringing
select:
- platform: template
entity_category: config
name: Wake word engine location
id: wake_word_engine_location
icon: "mdi:account-voice"
optimistic: true
restore_value: true
options:
- In Home Assistant
- On device
initial_option: On device
on_value:
- script.execute: stop_wake_word
- delay: 500ms
- if:
condition:
switch.is_off: mute
then:
- script.execute: start_wake_word
light:
- platform: esp32_rmt_led_strip
id: led
name: LED
disabled_by_default: false
entity_category: config
pin: GPIO21
default_transition_length: 0s
chipset: WS2812
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%
# Factory firmware - generated by ewt-gen
# Users should import Zero-VA-Sendspin.yaml directly, not this file.
packages:
original: !include Zero-VA-Sendspin.yaml
esphome:
project:
version: "2026.06.16.9"
ota:
- platform: http_request
id: ota_http_request
update:
- platform: http_request
id: update_http_request
name: Firmware
source: https://realdeco.github.io/Zero-VA/Zero-VA-Sendspin/manifest.json
http_request:
dashboard_import:
package_import_url: github://RealDeco/Zero-VA/Zero-VA-Sendspin.yaml@main