Sendspin_Zero-Button-Digital v2026.08.08.42

Install Firmware

Connect your device via USB and click the button below to install.

Browser not supported.
ESP Web Tools requires a Chromium-based browser (Chrome, Edge) with Web Serial support. Please use one of these browsers, or follow the manual installation instructions below.
Browser-based installation requires HTTPS.
This page is not served over a secure connection, so the install button won't work. Use web.esphome.io to install the firmware instead.
Firmware Download
Configuration Download
## SendspinZero Button Digital version
substitutions:
  name: sendspinzero-button
  friendly_name: Sendspin Zero Button
  media_sample_rate: 44100
  startup_sound_file: Home_Connected

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  min_version: 2026.6.0
  name_add_mac_suffix: true
  project:
    name: realdeco.sendspin_zero
    version: "2026.6.0"
  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

speaker:
  - platform: i2s_audio
    id: box_speaker
    sample_rate: ${media_sample_rate}
    i2s_dout_pin: GPIO4
    spdif_mode: true
    i2s_audio_id: i2s_audio_bus
    dac_type: external
    channel: stereo
    use_apll: true

  - 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"
OTA extension Download
# Factory firmware - generated by ewt-gen
# Users should import Sendspin_Zero-Button-Digital.yaml directly, not this file.

packages:
  original: !include Sendspin_Zero-Button-Digital.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-Digital/manifest.json

http_request:

dashboard_import:
  package_import_url: github://RealDeco/SendspinZero/Sendspin_Zero-Button-Digital.yaml@main