Fix peripheral volume_muted state (#403)

Enable updating the `volume_muted` status when muting/unmuting the media player volume, and improve its tracking.
This commit is contained in:
Omar Amin authored and GitHub committed 2026-08-17 20:23:16 +03:00
1 parent e334aa3fb1
commit 8303a96279
12 files changed
+177 -18

No files matched your search

@@ -36,6 +36,7 @@ All animations mirror the Home Assistant Voice PE ESPHome firmware exactly.
| Error | Red pulse | All LEDs red, pulsing |
| Timer ticking | Countdown arc | Arc proportional to `seconds_left / total_seconds` |
| Timer ringing | Pulse + optional red | Full ring pulsing; red at mic positions if muted |
| Volume muted | Red mic indicator | LED 9 (bottom at the AUX jack corner) solid red to indicate the media player volume is zero; other LEDs off |
"User color" comes from the Home Assistant Light entity described below (default: HAVPE-style blue). HA brightness scales every animation in this table. Semantic colors (Waiting/Listening cyan spin, Thinking pulse, Replying anticlockwise, Muted solid + red, Timer pulse/arc, Error red pulse) are hardcoded so they remain recognisable across user customisation.
@@ -86,6 +86,13 @@ USB_PRODUCT_ID = 0x0018
LED_COUNT = 12
LED_BRIGHTNESS = 10 # 0–31 (APA102 5-bit global brightness register)
# LED that lights up solid red while the media player (speaker) output is
# muted at volume 0 — the `volume_muted` peripheral event. LED 9 is the
# board's bottom-middle position (DOA 270°), sitting right between the
# micro-USB connector and the 3.5 mm AUX jack on the board edge — the same
# "bottom" position already used by the mic indicator mapping below.
VOLUME_MUTED_LED_INDEX = 9
RECONNECT_DELAY_S = 3.0
@@ -113,6 +120,7 @@ class AssistState(str, Enum):
TIMER_TICKING = "timer_ticking"
TIMER_RINGING = "timer_ringing"
MEDIA_PLAYING = "media_player_playing"
VOLUME_MUTED = "volume_muted"
# ---------------------------------------------------------------------------
@@ -124,8 +132,9 @@ class SharedState:
self._lock = threading.Lock()
self.assist_state: AssistState = AssistState.NOT_READY
self.ha_connected: bool = False
self.muted: bool = False
self.muted: bool = False # mic mute
self.volume: float = 1.0
self.volume_muted: bool = False # media player volume zero
self.timer_total_seconds: int = 0
self.timer_seconds_left: int = 0
# Light entity state, driven by HA via light_command events.
@@ -151,6 +160,7 @@ class SharedState:
"ha_connected": self.ha_connected,
"muted": self.muted,
"volume": self.volume,
"volume_muted": self.volume_muted,
"timer_total_seconds": self.timer_total_seconds,
"timer_seconds_left": self.timer_seconds_left,
"light_is_on": self.light_is_on,
@@ -407,6 +417,22 @@ class LEDRing:
self._leds.set(6, _scale(RED, factor))
self._leds.set(9, _scale(RED, factor))
def _apply_volume_muted_indicator(self) -> None:
"""
Overlay a solid red dot on the bottom-middle LED (index
``VOLUME_MUTED_LED_INDEX``, DOA 270°) when the media player output
is muted at volume 0.
Sits right between the board's micro-USB connector and 3.5 mm AUX
jack. Applied after the current pipeline animation has already
rendered its frame, so the indicator shows up consistently no
matter which animation is active — mirrors how
``_apply_mic_indicators`` marks the microphone cardinal points, but
for the distinct speaker-mute state.
"""
self._leds.set(VOLUME_MUTED_LED_INDEX, RED)
self._leds.show()
# ------------------------------------------------------------------
# Animations — each returns sleep time in seconds
# ------------------------------------------------------------------
@@ -555,6 +581,7 @@ class LEDRing:
snap = self._state.snapshot
color = self._color()
muted = snap["muted"]
volume_muted = snap["volume_muted"]
t_total = snap["timer_total_seconds"]
t_left = snap["timer_seconds_left"]
@@ -585,6 +612,9 @@ class LEDRing:
else:
sleep = self._anim_off()
if volume_muted:
self._apply_volume_muted_indicator()
time.sleep(sleep)
@@ -783,7 +813,12 @@ class LVAClient:
self._state.update(volume=data.get("volume", 1.0))
elif event == "volume_muted":
self._state.update(muted=data.get("muted", False))
Volume_muted = data.get("muted", True)
self._state.update(volume_muted=Volume_muted)
if Volume_muted:
self._state.update(assist_state=AssistState.VOLUME_MUTED)
elif self._state.assist_state == AssistState.VOLUME_MUTED:
self._state.update(assist_state=AssistState.VOLUME_MUTED)
elif event == "zeroconf":
status = data.get("status", "")