Connect your device via USB and click the button below to install.
## SendspinZero Button Analog version
substitutions:
name: sendspinzero-button
friendly_name: Sendspin Zero Button
media_sample_rate: 48000
startup_sound_file: Home_Connected
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2026.7.4
name_add_mac_suffix: true
project:
name: realdeco.sendspin_zero
version: "2026.7.4"
on_boot:
priority: -100
then:
- script.execute: update_status_leds
esp32:
variant: esp32s3
framework:
type: esp-idf
logger:
level: warn
api:
on_client_connected:
- 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
wifi:
ap:
improv_serial:
psram:
mode: quad
speed: 80MHz
captive_portal:
sendspin:
id: sendspin_hub
i2s_audio:
- id: i2s_audio_bus
i2s_lrclk_pin: GPIO4
i2s_bclk_pin: GPIO5
speaker:
- platform: i2s_audio
id: box_speaker
i2s_audio_id: i2s_audio_bus
i2s_dout_pin: GPIO6
dac_type: external
channel: stereo
- platform: mixer
id: mixing_speaker
output_speaker: box_speaker
num_channels: 2
source_speakers:
- id: announcement_mixing_input
- id: media_mixing_input
- platform: resampler
id: announcement_resampling_speaker
output_speaker: announcement_mixing_input
sample_rate: ${media_sample_rate}
bits_per_sample: 16
- platform: resampler
id: media_resampling_speaker
output_speaker: media_mixing_input
sample_rate: ${media_sample_rate}
bits_per_sample: 16
audio_file:
- id: ready_sound
file: https://github.com/RealDeco/xiaozhi-esphome/raw/main/sounds/${startup_sound_file}.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
id: external_media_player
name: Media Player
announcement_pipeline:
format: FLAC
num_channels: 1
sample_rate: ${media_sample_rate}
speaker: announcement_resampling_speaker
sources:
- audio_file_announcement_source
- http_announcement_source
media_pipeline:
format: FLAC
num_channels: 2
sample_rate: ${media_sample_rate}
speaker: media_resampling_speaker
sources:
- http_media_source
- sendspin_media_source
volume_increment: 0.05
volume_min: 0.4
volume_max: 0.9
on_state:
then:
- script.execute: update_status_leds
script:
- id: play_sound
parameters:
priority: bool
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();
}
- id: update_status_leds
then:
- if:
condition:
lambda: 'return id(button_mode).current_option() == "HA Control";'
then:
- light.turn_on: red_status_light
- light.turn_on: green_status_light
else:
- lambda: |-
using media_player::MediaPlayerState;
auto state = id(external_media_player).state;
if (
state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING ||
state == MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING
) {
id(led_green).execute();
} else {
id(led_red).execute();
}
- id: led_red
then:
- light.turn_on: red_status_light
- light.turn_off: green_status_light
- id: led_green
then:
- light.turn_off: red_status_light
- light.turn_on: green_status_light
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
button:
- platform: restart
id: restart_button
name: "Restart"
entity_category: config
icon: "mdi:restart"
text_sensor:
- platform: sendspin
id: sendspin_title
name: "Song Title"
type: title
- platform: sendspin
id: sendspin_artist
name: "Song Artist"
type: artist
- platform: sendspin
id: sendspin_album
name: "Album Name"
type: album
- platform: template
id: button_action
name: Button Action
update_interval: never
select:
- platform: template
id: button_mode
name: Button Mode
optimistic: true
restore_value: true
initial_option: Player Control
options:
- Player Control
- HA Control
set_action:
- lambda: |-
id(button_mode).publish_state(x);
- script.execute: update_status_leds
globals:
- id: boot_sound_played
type: bool
restore_value: no
initial_value: "false"
- id: boot_announce_active
type: bool
restore_value: no
initial_value: "false"
output:
- platform: ledc
id: red_led
pin: GPIO3
- platform: ledc
id: green_led
pin: GPIO2
light:
- platform: esp32_rmt_led_strip
id: led
name: LED light
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%
- platform: monochromatic
id: red_status_light
name: Red Status LED
output: red_led
restore_mode: ALWAYS_OFF
- platform: monochromatic
id: green_status_light
name: Green Status LED
output: green_led
restore_mode: ALWAYS_OFF
binary_sensor:
- platform: gpio
pin:
number: 1
mode: INPUT_PULLUP
inverted: true
id: media_button
on_multi_click:
- timing:
- ON for at most 350ms
- OFF for at least 351ms
then:
- if:
condition:
lambda: 'return id(button_mode).current_option() == "Player Control";'
then:
- media_player.toggle: sendspin_group_media_player
else:
- text_sensor.template.publish:
id: button_action
state: "single"
# Double click
- timing:
- ON for at most 350ms
- OFF for at most 350ms
- ON for at most 350ms
- OFF for at least 351ms
then:
- if:
condition:
lambda: 'return id(button_mode).current_option() == "Player Control";'
then:
- media_player.next: sendspin_group_media_player
else:
- text_sensor.template.publish:
id: button_action
state: "double"
# Triple click
- timing:
- ON for at most 350ms
- OFF for at most 350ms
- ON for at most 350ms
- OFF for at most 350ms
- ON for at most 350ms
- OFF for at least 351ms
then:
- if:
condition:
lambda: 'return id(button_mode).current_option() == "Player Control";'
then:
- media_player.previous: sendspin_group_media_player
else:
- text_sensor.template.publish:
id: button_action
state: "triple"
# Hold => toggle between modes
- timing:
- ON for at least 700ms
then:
- if:
condition:
lambda: 'return id(button_mode).current_option() == "Player Control";'
then:
- select.set:
id: button_mode
option: "HA Control"
- text_sensor.template.publish:
id: button_action
state: "mode_ha"
else:
- select.set:
id: button_mode
option: "Player Control"
- text_sensor.template.publish:
id: button_action
state: "mode_player"
# Factory firmware - generated by ewt-gen
# Users should import Sendspin_Zero-Button-Analog.yaml directly, not this file.
packages:
original: !include Sendspin_Zero-Button-Analog.yaml
esphome:
project:
version: "2026.08.08.42"
ota:
- platform: http_request
id: ota_http_request
update:
- platform: http_request
id: update_http_request
name: Firmware
source: https://realdeco.github.io/SendspinZero/Sendspin_Zero-Button-Analog/manifest.json
http_request:
dashboard_import:
package_import_url: github://RealDeco/SendspinZero/Sendspin_Zero-Button-Analog.yaml@main