Added Raspiaudio Muse Luxe again as a sendspin player.
This commit is contained in:
@@ -0,0 +1,485 @@
|
||||
substitutions:
|
||||
startup_sound_file: https://github.com/RASPIAUDIO/esphomeLuxe/raw/refs/heads/main/wav/sounds_timer_finished.wav
|
||||
device_description: "Raspiaudio Muse Luxe Music player for Music Assistant using sendspin"
|
||||
name: "muse-luxe-music"
|
||||
friendly_name: "Raspiaudio Muse Luxe Music"
|
||||
|
||||
|
||||
esphome:
|
||||
comment: ${device_description}
|
||||
name: ${name}
|
||||
friendly_name: ${friendly_name}
|
||||
min_version: 2026.8.2
|
||||
name_add_mac_suffix: true
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
# GPIO21 commande la mise en sourdine de l'amplificateur de la Luxe.
|
||||
- output.turn_off: dac_mute
|
||||
# Laisse le mixer terminer une annonce avant la reprise d'un flux MA.
|
||||
- speaker_source.set_playlist_delay:
|
||||
id: external_media_player
|
||||
pipeline: announcement
|
||||
delay: 500ms
|
||||
- speaker_source.set_playlist_delay:
|
||||
id: external_media_player
|
||||
pipeline: media
|
||||
delay: 1500ms
|
||||
- script.execute: led_red
|
||||
project:
|
||||
name: raspiaudio.music-assistant
|
||||
version: "2026.9.3"
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
include_builtin_idf_components:
|
||||
- esp_http_client
|
||||
sram1_as_iram: true
|
||||
|
||||
logger:
|
||||
level: warn
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: !secret apikey
|
||||
# La lecture Sendspin doit continuer même sans client Home Assistant connecté.
|
||||
reboot_timeout: 0s
|
||||
on_client_connected:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(boot_sound_played);
|
||||
then:
|
||||
- lambda: id(boot_sound_played) = true;
|
||||
# Laisse MA restaurer son groupe avant de décider si l'annonce peut jouer.
|
||||
- delay: 5s
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
using media_player::MediaPlayerState;
|
||||
return id(startup_sound_switch).state &&
|
||||
id(external_media_player).state != MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING &&
|
||||
id(external_media_player).state != MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING;
|
||||
then:
|
||||
- script.execute:
|
||||
id: play_sound
|
||||
priority: true
|
||||
sound_file: ready_sound
|
||||
|
||||
wifi:
|
||||
ssid: "Voltage-legacy"
|
||||
password: !secret voltage_legacy_psk
|
||||
#use_address: ${name}.home
|
||||
power_save_mode: high
|
||||
fast_connect: on
|
||||
min_auth_mode: WPA2
|
||||
|
||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||
ap:
|
||||
ssid: "Raspiaudio Fallback Hotspot"
|
||||
password: !secret fallback_psk
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
speed: 80MHz
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
id: ota_esphome
|
||||
encryption:
|
||||
- platform: http_request
|
||||
id: ota_http_request
|
||||
|
||||
http_request:
|
||||
|
||||
captive_portal:
|
||||
|
||||
improv_serial:
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: components
|
||||
components: [luxe_ip5306]
|
||||
# Initialisation spécifique du codec de la museLuxe (sorties différentielles
|
||||
# et horloge MCLK sur GPIO0). Sendspin et la chaîne audio sont désormais natifs.
|
||||
- source:
|
||||
type: git
|
||||
url: https://github.com/RASPIAUDIO/esphomeLuxe
|
||||
ref: 192372ff59b560a74aa547dc26af609918fcc9d6
|
||||
components: [es8388]
|
||||
|
||||
i2c:
|
||||
sda: GPIO18
|
||||
scl: GPIO23
|
||||
frequency: 100kHz
|
||||
id: bus_a
|
||||
|
||||
es8388:
|
||||
|
||||
i2s_audio:
|
||||
- id: i2s_audio_bus
|
||||
i2s_lrclk_pin: GPIO25
|
||||
i2s_bclk_pin: GPIO5
|
||||
#####################
|
||||
# Internal Components
|
||||
output:
|
||||
- platform: gpio
|
||||
id: dac_mute
|
||||
pin:
|
||||
number: GPIO21
|
||||
inverted: true
|
||||
mode:
|
||||
output: true
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: box_speaker
|
||||
i2s_audio_id: i2s_audio_bus
|
||||
i2s_dout_pin: GPIO26
|
||||
dac_type: external
|
||||
channel: stereo
|
||||
sample_rate: 48000
|
||||
buffer_duration: 300ms
|
||||
|
||||
- platform: mixer
|
||||
id: mixing_speaker
|
||||
output_speaker: box_speaker
|
||||
num_channels: 2
|
||||
source_speakers:
|
||||
- id: announcement_mixer_input
|
||||
- id: media_mixer_input
|
||||
|
||||
- platform: resampler
|
||||
id: media_resampling_input
|
||||
output_speaker: media_mixer_input
|
||||
sample_rate: 48000
|
||||
|
||||
- platform: resampler
|
||||
id: announcement_resampling_input
|
||||
output_speaker: announcement_mixer_input
|
||||
sample_rate: 48000
|
||||
|
||||
sendspin:
|
||||
id: sendspin_hub
|
||||
|
||||
audio_file:
|
||||
- id: ready_sound
|
||||
file: ${startup_sound_file}
|
||||
|
||||
media_source:
|
||||
- platform: sendspin
|
||||
id: sendspin_source
|
||||
buffer_size: 2000000
|
||||
- platform: audio_file
|
||||
id: file_source
|
||||
|
||||
media_player:
|
||||
- platform: sendspin
|
||||
id: sendspin_media_player
|
||||
name: museLuxe
|
||||
volume_increment: 2%
|
||||
- platform: speaker_source
|
||||
name: museLuxe Audio
|
||||
id: external_media_player
|
||||
internal: true
|
||||
|
||||
announcement_pipeline:
|
||||
speaker: announcement_resampling_input
|
||||
sources:
|
||||
- file_source
|
||||
format: FLAC
|
||||
num_channels: 1
|
||||
sample_rate: 48000
|
||||
media_pipeline:
|
||||
speaker: media_resampling_input
|
||||
sources:
|
||||
- sendspin_source
|
||||
format: FLAC
|
||||
num_channels: 2
|
||||
sample_rate: 48000
|
||||
|
||||
volume_increment: 0.05
|
||||
volume_min: 0.05
|
||||
volume_max: 0.95
|
||||
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: update_battery_led
|
||||
mode: queued
|
||||
max_runs: 2
|
||||
then:
|
||||
- lambda: |-
|
||||
const bool critical = id(battery_critical).state;
|
||||
if (critical && (!id(led).remote_values.is_on() || id(led).get_effect_name() != "Battery alert")) {
|
||||
auto call = id(led).turn_on();
|
||||
call.set_effect("Battery alert");
|
||||
call.set_brightness(1.0f);
|
||||
call.perform();
|
||||
} else if (!critical && id(led).get_effect_name() == "Battery alert") {
|
||||
auto call = id(led).turn_on();
|
||||
call.set_effect("None");
|
||||
call.perform();
|
||||
}
|
||||
- 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: ip5306_charge_switch
|
||||
name: "Allow charging"
|
||||
entity_category: config
|
||||
icon: mdi:battery-charging
|
||||
restore_mode: DISABLED
|
||||
optimistic: false
|
||||
lambda: return id(battery_manager).charge_enabled();
|
||||
turn_on_action:
|
||||
- lambda: id(battery_manager).set_charge_enabled(true);
|
||||
turn_off_action:
|
||||
- lambda: id(battery_manager).set_charge_enabled(false);
|
||||
- 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: template
|
||||
id: battery_critical
|
||||
name: Battery low
|
||||
device_class: battery
|
||||
entity_category: diagnostic
|
||||
lambda: |-
|
||||
if (!id(battery_percent).has_state() || !std::isfinite(id(battery_percent).state)) return {};
|
||||
const float percent = id(battery_percent).state;
|
||||
const float threshold = id(battery_alert_threshold).state;
|
||||
if (percent < threshold) return true;
|
||||
if (percent >= threshold + 5.0f) return false;
|
||||
return {};
|
||||
on_state:
|
||||
- script.execute: update_battery_led
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: GPIO12
|
||||
inverted: true
|
||||
mode:
|
||||
input: true
|
||||
output: false
|
||||
open_drain: false
|
||||
pullup: true
|
||||
pulldown: false
|
||||
drive_strength: 20.0
|
||||
name: Play or Pause
|
||||
on_press:
|
||||
then:
|
||||
- media_player.toggle: sendspin_media_player
|
||||
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: GPIO19
|
||||
inverted: true
|
||||
mode:
|
||||
input: true
|
||||
pullup: true
|
||||
name: Volume Up
|
||||
on_click:
|
||||
- media_player.volume_up:
|
||||
id: sendspin_media_player
|
||||
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: GPIO32
|
||||
inverted: true
|
||||
mode:
|
||||
input: true
|
||||
pullup: true
|
||||
name: Volume Down
|
||||
on_click:
|
||||
- media_player.volume_down:
|
||||
id: sendspin_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
|
||||
channel_colors: GRB
|
||||
# Conserve l'alerte après une commande HA ou un changement lecture/arrêt.
|
||||
on_state:
|
||||
- script.execute: update_battery_led
|
||||
effects:
|
||||
- addressable_lambda:
|
||||
name: "Battery alert"
|
||||
update_interval: 500ms
|
||||
lambda: |-
|
||||
static bool lit = false;
|
||||
if (initial_run) lit = false;
|
||||
lit = !lit;
|
||||
it.all() = lit ? current_color : Color::BLACK;
|
||||
- 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
|
||||
id: battery_voltage
|
||||
name: Battery voltage
|
||||
icon: "mdi:battery-outline"
|
||||
update_interval: 15s
|
||||
accuracy_decimals: 3
|
||||
attenuation: 12db
|
||||
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
|
||||
on_value:
|
||||
- sensor.template.publish:
|
||||
id: battery_percent
|
||||
state: !lambda return x;
|
||||
- platform: template
|
||||
id: battery_percent
|
||||
name: Battery level
|
||||
device_class: battery
|
||||
unit_of_measurement: "%"
|
||||
accuracy_decimals: 0
|
||||
state_class: measurement
|
||||
entity_category: diagnostic
|
||||
update_interval: never
|
||||
# Même estimation tension/charge que la Radio, après calibration Luxe.
|
||||
filters:
|
||||
- calibrate_polynomial:
|
||||
degree: 3
|
||||
datapoints:
|
||||
- 4.20 -> 100.0
|
||||
- 4.13 -> 97.1
|
||||
- 4.10 -> 94.2
|
||||
- 4.07 -> 88.4
|
||||
- 4.05 -> 82.7
|
||||
- 4.04 -> 76.9
|
||||
- 4.04 -> 71.1
|
||||
- 4.01 -> 65.3
|
||||
- 3.99 -> 59.5
|
||||
- 3.95 -> 53.8
|
||||
- 3.93 -> 48.0
|
||||
- 3.91 -> 42.2
|
||||
- 3.88 -> 36.4
|
||||
- 3.86 -> 30.6
|
||||
- 3.84 -> 24.9
|
||||
- 3.82 -> 19.1
|
||||
- 3.76 -> 13.3
|
||||
- 3.73 -> 10.4
|
||||
- 3.70 -> 7.5
|
||||
- 3.64 -> 4.6
|
||||
- 3.50 -> 1.7
|
||||
- 3.00 -> 0.0
|
||||
- lambda: return clamp(x, 0.0f, 100.0f);
|
||||
- platform: wifi_signal
|
||||
name: "museLuxe WiFi signal"
|
||||
update_interval: 10s
|
||||
|
||||
# Paramètre logiciel : conserve une hystérésis fixe de 5 points.
|
||||
number:
|
||||
- platform: template
|
||||
id: battery_alert_threshold
|
||||
name: "Low battery threshold"
|
||||
entity_category: config
|
||||
unit_of_measurement: "%"
|
||||
min_value: 5
|
||||
max_value: 50
|
||||
step: 1
|
||||
initial_value: 20
|
||||
restore_value: true
|
||||
optimistic: true
|
||||
mode: box
|
||||
|
||||
luxe_ip5306:
|
||||
id: battery_manager
|
||||
i2c_id: bus_a
|
||||
address: 0x75
|
||||
update_interval: 10s
|
||||
available:
|
||||
name: "IP5306 available"
|
||||
id: ip5306_available
|
||||
charging:
|
||||
name: "Charging"
|
||||
full:
|
||||
name: "Battery full"
|
||||
charge_allowed:
|
||||
name: "Charging enabled (hardware)"
|
||||
level:
|
||||
name: "IP5306 battery level"
|
||||
current_limit:
|
||||
name: "IP5306 charge current limit"
|
||||
Reference in New Issue
Block a user