Skip to content

Instantly share code, notes, and snippets.

@tetele
Last active July 22, 2024 19:48
Show Gist options
  • Save tetele/5cac735174527c3b373b10db8d9c8d77 to your computer and use it in GitHub Desktop.
Save tetele/5cac735174527c3b373b10db8d9c8d77 to your computer and use it in GitHub Desktop.
ESPHome config - Raspiaudio Muse Luxe as a voice assistant satellite in Home Assistant

Introduction

The purpose of this ESPHome config is to be able to use the Raspiaudio Muse Luxe as a voice assistant satellite in Home Assistant.

Features

  • wake word, push to talk and continuous conversation support
  • response playback
  • service exposed in HA to start and stop the voice assistant from another device/trigger
  • visual feedback of the recording/success/error status via the Luxe's onboard LED

Pre-requisites

  • Home Assistant 2023.10.0 or newer
  • A voice assistant configured in HA with STT and TTS in a language of your choice
  • ESPHome 2023.10.1 or newer

Known issues and limitations

Installation instructions

If the Muse Luxe has already been added to ESPHome

Edit your config and paste the one below. Double check Wifi connection details, API encryption key and device name/friendly name to make sure you use your own.

If the Muse Luxe is not already in ESPHome

Add a new ESP32 device in ESPHome, using the configuration below.

After the device has been added to ESPHome

Compile and install the firmware on the speaker. If auto discovery is turned on, the device should appear in Home Assistant automatically. Otherwise, check out this guide.

Credits

GithubSponsor or BuyMeCoffee

substitutions:
name: "muse-luxe"
friendly_name: "RaspiAudio Muse Luxe"
wifi_ap_password: ""
esphome:
name: ${name}
friendly_name: ${friendly_name}
name_add_mac_suffix: false
min_version: 2023.10.1
on_boot:
then:
- output.turn_on: dac_mute
- light.turn_on:
id: top_led
effect: slow_pulse
red: 100%
green: 60%
blue: 0%
esp32:
board: esp-wrover-kit
framework:
type: arduino
logger:
api:
services:
- service: start_va
then:
- voice_assistant.start
- service: stop_va
then:
- voice_assistant.stop
ota:
i2c:
sda: GPIO18
scl: GPIO23
wifi:
ap:
password: "${wifi_ap_password}"
captive_portal:
improv_serial:
external_components:
- source: github://RASPIAUDIO/esphomeLuxe@main
components: [es8388]
refresh: 0s
es8388:
globals:
- id: wifi_connected
type: bool
initial_value: "false"
restore_value: false
interval:
- interval: 1s
then:
- if:
condition:
and:
- lambda: "return !id(wifi_connected);"
- wifi.connected:
then:
- globals.set:
id: wifi_connected
value: "true"
- light.turn_on:
id: top_led
effect: pulse
red: 0%
green: 100%
blue: 0%
- delay: 1s
- light.turn_off: top_led
output:
- platform: gpio
id: dac_mute
pin: GPIO21
inverted: true
i2s_audio:
- i2s_lrclk_pin: GPIO25
i2s_bclk_pin: GPIO5
media_player:
- platform: i2s_audio
name: None
id: luxe_out
dac_type: external
i2s_dout_pin: GPIO26
mode: stereo
on_state:
if:
condition:
media_player.is_playing:
then:
output.turn_off: dac_mute
else:
output.turn_on: dac_mute
microphone:
- platform: i2s_audio
id: luxe_microphone
i2s_din_pin: GPIO35
adc_type: external
pdm: false
voice_assistant:
id: va
microphone: luxe_microphone
media_player: luxe_out
use_wake_word: true
on_listening:
- light.turn_on:
id: top_led
blue: 100%
red: 0%
green: 0%
brightness: 100%
effect: pulse
on_tts_start:
- light.turn_on:
id: top_led
blue: 60%
red: 20%
green: 20%
effect: none
on_tts_end:
- media_player.play_media: !lambda return x;
- light.turn_on:
id: top_led
blue: 60%
red: 20%
green: 20%
effect: pulse
# This is useful when you want to stream the response on another media_player
# - homeassistant.service:
# service: media_player.play_media
# data:
# entity_id: media_player.some_speaker
# media_content_id: !lambda 'return x;'
# media_content_type: music
# announce: "true"
on_client_connected:
- if:
condition:
- switch.is_on: use_wake_word
then:
- voice_assistant.start_continuous:
on_client_disconnected:
- if:
condition:
- switch.is_on: use_wake_word
then:
- voice_assistant.stop:
on_end:
- delay: 100ms
- wait_until:
not:
media_player.is_playing: luxe_out
- script.execute: reset_led
on_error:
- light.turn_on:
id: top_led
blue: 0%
red: 100%
green: 0%
effect: none
- delay: 1s
- script.execute: reset_led
- script.wait: reset_led
- lambda: |-
if (code == "wake-provider-missing" || code == "wake-engine-missing") {
id(use_wake_word).turn_off();
}
sensor:
- platform: adc
pin: GPIO33
name: Battery voltage
device_class: voltage
unit_of_measurement: "V"
accuracy_decimals: 2
state_class: measurement
entity_category: diagnostic
update_interval: 15s
attenuation: auto
filters:
- multiply: 2 # https://forum.raspiaudio.com/t/esp-muse-luxe-bluetooth-speaker/294/12
- exponential_moving_average:
alpha: 0.2
send_every: 2
- delta: 0.002
on_value:
then:
- sensor.template.publish:
id: battery_percent
state: !lambda "return x;"
- platform: template
name: Battery
id: battery_percent
device_class: battery
unit_of_measurement: "%"
accuracy_decimals: 0
state_class: measurement
entity_category: diagnostic
update_interval: 15s
filters:
- calibrate_polynomial:
degree: 3
datapoints:
- 4.58 -> 100.0
- 4.5 -> 97.1
- 4.47 -> 94.2
- 4.44 -> 88.4
- 4.42 -> 82.7
- 4.41 -> 76.9
- 4.41 -> 71.1
- 4.37 -> 65.3
- 4.35 -> 59.5
- 4.31 -> 53.8
- 4.28 -> 48.0
- 4.26 -> 42.2
- 4.23 -> 36.4
- 4.21 -> 30.6
- 4.19 -> 24.9
- 4.16 -> 19.1
- 4.1 -> 13.3
- 4.07 -> 10.4
- 4.03 -> 7.5
- 3.97 -> 4.6
- 3.82 -> 1.7
- 3.27 -> 0.0
- lambda: return clamp(x, 0.0f, 100.0f);
binary_sensor:
- platform: gpio
pin:
number: GPIO19
inverted: true
mode:
input: true
pullup: true
name: Volume Up
on_click:
- media_player.volume_up: luxe_out
- platform: gpio
pin:
number: GPIO32
inverted: true
mode:
input: true
pullup: true
name: Volume Down
on_click:
- media_player.volume_down: luxe_out
- platform: gpio
pin:
number: GPIO12
inverted: true
mode:
input: true
pullup: true
name: Action
on_click:
- if:
condition:
switch.is_off: use_wake_word
then:
- if:
condition: voice_assistant.is_running
then:
- voice_assistant.stop:
- script.execute: reset_led
else:
- voice_assistant.start:
else:
- voice_assistant.stop
- delay: 1s
- script.execute: reset_led
- script.wait: reset_led
- voice_assistant.start_continuous:
light:
- platform: esp32_rmt_led_strip
name: None
id: top_led
pin: GPIO22
chipset: SK6812
num_leds: 1
rgb_order: grb
rmt_channel: 0
default_transition_length: 0s
gamma_correct: 2.8
effects:
- pulse:
name: pulse
transition_length: 250ms
update_interval: 250ms
- pulse:
name: slow_pulse
transition_length: 1s
update_interval: 2s
script:
- id: reset_led
then:
- if:
condition:
switch.is_on: use_wake_word
then:
- light.turn_on:
id: top_led
blue: 100%
red: 100%
green: 0%
brightness: 100%
effect: none
else:
- light.turn_off: top_led
switch:
- platform: template
name: Use Wake Word
id: use_wake_word
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
on_turn_on:
- lambda: id(va).set_use_wake_word(true);
- if:
condition:
not:
- voice_assistant.is_running
then:
- voice_assistant.start_continuous
- script.execute: reset_led
on_turn_off:
- voice_assistant.stop
- lambda: id(va).set_use_wake_word(false);
- script.execute: reset_led
@AnthonyJWinslow
Copy link

AnthonyJWinslow commented Jul 13, 2024

New to github but I think I fixed the issue @monsieurlatte . Looks like its something to do with how the current code is handling muting the dac:

output:
  - platform: gpio
    id: dac_mute
    pin: GPIO21
    inverted: true

I simply removed all references of dac_mute and used mute_pin from media-players/raspiaudio-muse-luxe.yaml:

media_player:
  - platform: i2s_audio
    name: None
    id: luxe_out
    dac_type: external
    i2s_dout_pin: GPIO26
    mode: stereo
    mute_pin:
      number: GPIO21
      inverted: true

This fixed my issue. dont forget to remove - output.turn_on: dac_mute from esphome: as well.

I am still receiving the following error BUT the audio from TTS is at least playing:

[01:54:45][W][component:237]: Component i2s_audio.media_player took a long time for an operation (542 ms).
[01:54:45][W][component:238]: Components should block for at most 30 ms.

EDIT: Guess I should include the whole thing...
EDIT 2: After sleeping on it and looking at some other examples, this is my latest yaml:

---
esphome:
  name: raspiaudio-muse-luxe
  friendly_name: RaspiAudio Muse Luxe
  min_version: 2024.6.0
  name_add_mac_suffix: true
  project:
    name: raspiaudio.muse-luxe-voice-assistant
    version: "24.7.4.1"

esp32:
  board: esp-wrover-kit
  framework:
    type: arduino

logger:
api:

ota:
  - platform: esphome
    id: ota_esphome

i2c:
  - id: i2c_bus
    sda: GPIO18
    scl: GPIO23

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    # Set this to the IP of the ESP
    static_ip: 10.0.0.100
    # Set this to the IP address of the router. Often ends with .1
    gateway: 10.0.0.1
    # The subnet of the network. 255.255.255.0 works for most home networks.
    subnet: 255.255.255.0
  ap:

improv_serial:

button:
  - platform: safe_mode
    id: button_safe_mode
    name: Safe Mode Boot

  - platform: factory_reset
    id: factory_reset_btn
    name: Factory reset

external_components:
  - source: github://pr#3552  # DAC support https://github.com/esphome/esphome/pull/3552
    components: [es8388]
    refresh: 0s

es8388:

i2s_audio:
  - id: i2s_audio_bus
    i2s_lrclk_pin: GPIO25
    i2s_bclk_pin: GPIO5

microphone:
  - platform: i2s_audio
    id: luxe_microphone
    i2s_din_pin: GPIO35
    adc_type: external
    pdm: false

media_player:
  - platform: i2s_audio
    name: None
    id: luxe_out
    dac_type: external
    i2s_dout_pin: GPIO26
    mode: stereo
    mute_pin:
      number: GPIO21
      inverted: true

voice_assistant:
  id: va
  microphone: luxe_microphone
  media_player: luxe_out
  use_wake_word: true
  noise_suppression_level: 2
  auto_gain: 31dBFS
  volume_multiplier: 2.0
  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
  on_end:
    - delay: 100ms
    - wait_until:
        not:
          media_player.is_playing:
    - script.execute: reset_led
  on_error:
    - light.turn_on:
        id: led
        blue: 0%
        red: 100%
        green: 0%
        brightness: 100%
        effect: none
    - delay: 1s
    - script.execute: reset_led
  on_client_connected:
    - if:
        condition:
          switch.is_on: use_wake_word
        then:
          - voice_assistant.start_continuous:
          - script.execute: reset_led
  on_client_disconnected:
    - if:
        condition:
          switch.is_on: use_wake_word
        then:
          - voice_assistant.stop:
          - light.turn_off: led

sensor:
  - platform: adc
    pin: GPIO33
    name: Battery
    id: battery_sensor
    icon: "mdi:battery-outline"
    device_class: voltage
    state_class: measurement
    unit_of_measurement: V
    update_interval: 15s
    accuracy_decimals: 3
    attenuation: 11db
    raw: true
    filters:
      - multiply: 0.00173913  # 2300 -> 4, for attenuation 11db, based on Olivier's code
      - exponential_moving_average:
          alpha: 0.2
          send_every: 2
      - delta: 0.002

  - platform: template
    name: Battery
    id: battery_percent
    device_class: battery
    unit_of_measurement: "%"
    accuracy_decimals: 0
    state_class: measurement
    entity_category: diagnostic
    update_interval: 15s
    filters:
      - calibrate_polynomial:
          degree: 3
          datapoints:
            - 4.58 -> 100.0
            - 4.5 -> 97.1
            - 4.47 -> 94.2
            - 4.44 -> 88.4
            - 4.42 -> 82.7
            - 4.41 -> 76.9
            - 4.41 -> 71.1
            - 4.37 -> 65.3
            - 4.35 -> 59.5
            - 4.31 -> 53.8
            - 4.28 -> 48.0
            - 4.26 -> 42.2
            - 4.23 -> 36.4
            - 4.21 -> 30.6
            - 4.19 -> 24.9
            - 4.16 -> 19.1
            - 4.1 -> 13.3
            - 4.07 -> 10.4
            - 4.03 -> 7.5
            - 3.97 -> 4.6
            - 3.82 -> 1.7
            - 3.27 -> 0.0
      - lambda: return clamp(x, 0.0f, 100.0f);

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO19
      inverted: true
      mode:
        input: true
        pullup: true
    name: Volume Up
    id: volume_up
    disabled_by_default: true
  - platform: gpio
    pin:
      number: GPIO32
      inverted: true
      mode:
        input: true
        pullup: true
    name: Volume Down
    id: volume_down
    disabled_by_default: true
  - platform: gpio
    pin:
      number: GPIO12
      inverted: true
      mode:
        input: true
        pullup: true
    name: Action
    id: action_button
    disabled_by_default: true
    on_multi_click:
      - timing:
          - ON for at least 250ms
          - OFF for at least 50ms
        then:
          - if:
              condition:
                switch.is_off: use_wake_word
              then:
                - if:
                    condition: voice_assistant.is_running
                    then:
                      - voice_assistant.stop:
                      - script.execute: reset_led
                    else:
                      - voice_assistant.start:
              else:
                - voice_assistant.stop
                - delay: 1s
                - script.execute: reset_led
                - script.wait: reset_led
                - voice_assistant.start_continuous:
      - timing:
          - ON for at least 10s
        then:
          - button.press: factory_reset_btn

light:
  - platform: esp32_rmt_led_strip
    name: None
    id: led
    pin: GPIO22
    chipset: SK6812
    num_leds: 1
    rgb_order: grb
    rmt_channel: 0
    default_transition_length: 0s
    gamma_correct: 2.8
    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%

script:
  - id: reset_led
    then:
      - if:
          condition:
            switch.is_on: use_wake_word
          then:
            - light.turn_on:
                id: led
                red: 100%
                green: 89%
                blue: 71%
                brightness: 60%
                effect: none
          else:
            - light.turn_off: led

switch:
  - platform: template
    name: Use wake word
    id: use_wake_word
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON
    entity_category: config
    on_turn_on:
      - lambda: id(va).set_use_wake_word(true);
      - if:
          condition:
            not:
              - voice_assistant.is_running
          then:
            - voice_assistant.start_continuous
      - script.execute: reset_led
    on_turn_off:
      - voice_assistant.stop
      - lambda: id(va).set_use_wake_word(false);
      - script.execute: reset_led

@MacrosorcH
Copy link

Thank you @AnthonyJWinslow! Your fix works for me too - and you made my day!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment