Connect your device via USB and click the button below to install.
substitutions:
startup_sound_file: Home_Connected
esphome:
name: muse-luxe
friendly_name: Muse Luxe
min_version: 2026.5.0
name_add_mac_suffix: true
project:
name: realdeco.muse_wrover
version: "2026.5.0"
on_boot:
priority: -100
then:
- script.execute: led_red
esp32:
variant: esp32
framework:
type: esp-idf
sdkconfig_options:
CONFIG_ESP_WIFI_RX_IRAM_OPT: "y" # Unnecessary on an ESP32-S3; uses more internal memory
CONFIG_ESP_WIFI_IRAM_OPT: "y" # Unnecessary on an ESP32-S3; uses more internal memory
CONFIG_ESP_WIFI_EXTRA_IRAM_OPT: "y" # Unnecessary on an ESP32-S3; uses more internal memory
CONFIG_LWIP_IRAM_OPTIMIZATION: "y" # Unnecessary on an ESP32-S3; uses more internal memory
advanced:
sram1_as_iram: true
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:
i2c:
sda: GPIO18
scl: GPIO23
frequency: 100kHz
id: bus_a
# USING EXTERNAL COMPONENT
external_components:
- source: github://pr#3552
components: [es8388]
refresh: 0s
es8388:
# USING INTERNAL COMPONENT
#audio_dac:
# - platform: es8388
# id: es8388_dac
output:
- platform: gpio
pin:
number: GPIO21
inverted: true
id: mute_pin
audio:
codecs:
flac:
buffer_memory: internal # Unnecessary on an ESP32-S3; uses more internal memory
opus:
state_memory: internal # Unnecessary on an ESP32-S3; uses more internal memory
pseudostack:
threadsafe: false # Unnecessary on an ESP32-S3; prevents decoding two Opus streams simultaneously
sendspin:
id: sendspin_hub
task_stack_in_psram: false
i2s_audio:
- id: i2s_audio_bus
i2s_lrclk_pin: GPIO25
i2s_bclk_pin: GPIO5
speaker:
- platform: i2s_audio
id: box_speaker
i2s_audio_id: i2s_audio_bus
i2s_dout_pin: GPIO26
dac_type: external
channel: stereo
buffer_duration: 300ms
# audio_dac: es8388_dac # <<< adding this line breaks volume control in MA.
- platform: mixer
id: mixing_speaker
output_speaker: box_speaker
num_channels: 2
task_stack_in_psram: false
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
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
decode_memory: internal
media_player:
- platform: sendspin
id: sendspin_group_media_player
name: Sendspin Group Media Player
- platform: speaker_source
id: external_media_player
name: none
announcement_pipeline:
format: FLAC
num_channels: 1
sample_rate: 48000
speaker: announcement_resampling_speaker
sources:
- audio_file_announcement_source
- http_announcement_source
media_pipeline:
format: FLAC
num_channels: 2
sample_rate: 48000
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:
- 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();
}
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: led_red
then:
- light.turn_on:
id: led
red: 100%
green: 0%
blue: 0%
brightness: 100%
- id: led_green
then:
- light.turn_on:
id: led
red: 0%
green: 100%
blue: 0%
brightness: 100%
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
globals:
- id: boot_sound_played
type: bool
restore_value: no
initial_value: "false"
binary_sensor:
- platform: gpio
pin:
number: GPIO12
inverted: true
mode:
input: true
pullup: true
name: Button
on_multi_click:
# Triple click = previous track
- timing:
- ON for at most 500ms
- OFF for at most 400ms
- ON for at most 500ms
- OFF for at most 400ms
- ON for at most 500ms
- OFF for at least 200ms
then:
- media_player.previous: sendspin_group_media_player
# Double click = next track
- timing:
- ON for at most 500ms
- OFF for at most 400ms
- ON for at most 500ms
- OFF for at least 200ms
then:
- media_player.next: sendspin_group_media_player
# Single click = play/pause
- timing:
- ON for at most 500ms
- OFF for at least 500ms
then:
- media_player.toggle: sendspin_group_media_player
- platform: gpio
pin:
number: GPIO19
inverted: true
mode:
input: true
pullup: true
name: Volume Up
on_click:
- media_player.volume_up:
id: external_media_player
- platform: gpio
pin:
number: GPIO32
inverted: true
mode:
input: true
pullup: true
name: Volume Down
on_click:
- media_player.volume_down:
id: external_media_player
light:
- platform: esp32_rmt_led_strip
id: led
name: LED light
disabled_by_default: false
entity_category: config
pin: GPIO22
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%
sensor:
- platform: adc
pin: GPIO33
name: "Battery Voltage"
id: battery_voltage
attenuation: 12db
unit_of_measurement: "V"
device_class: voltage
accuracy_decimals: 2
update_interval: 10s
filters:
# Smooth noise first
- sliding_window_moving_average:
window_size: 10
send_every: 1
# Scale for divider (start with 1.90; adjust after you see results)
- multiply: 1.90
# Don’t spam HA
- throttle: 1min
on_value:
then:
- sensor.template.publish:
id: battery_percentage
state: !lambda 'return x;'
- platform: template
id: battery_percentage
name: "Battery Percentage"
update_interval: never
unit_of_measurement: "%"
accuracy_decimals: 0
icon: mdi:battery-medium
filters:
- calibrate_linear:
method: exact
datapoints:
- 2.80 -> 0.0
- 3.10 -> 10.0
- 3.30 -> 20.0
- 3.45 -> 30.0
- 3.60 -> 40.0
- 3.70 -> 50.0
- 3.75 -> 60.0
- 3.80 -> 70.0
- 3.90 -> 80.0
- 4.00 -> 90.0
- 4.20 -> 100.0
- lambda: |-
if (x > 100) return 100;
if (x < 0) return 0;
return x;
# Factory firmware - generated by ewt-gen
# Users should import Muse_Luxe.yaml directly, not this file.
packages:
original: !include Muse_Luxe.yaml
esphome:
project:
version: "2026.06.16.8"
ota:
- platform: http_request
id: ota_http_request
update:
- platform: http_request
id: update_http_request
name: Firmware
source: https://realdeco.github.io/muse_wrover/Muse_Luxe/manifest.json
http_request:
dashboard_import:
package_import_url: github://RealDeco/muse_wrover/Muse_Luxe.yaml@main