Peripheral volume display (#411)
Adds the volume display effect to these boards that have LED Ring, when controlling the volume for LVA: - Satellite 1 HAT Board - ReSpeaker 4-Mic Array HAT - ReSpeaker USB Mic Array v2.0 This animation appears on the LED ring temporarily to display an arc that sweeps clockwise when the volume increases and anti-clockwise when the volume decreases. Triggers by the volume_changed event.
This commit is contained in:
1 parent
d1f5761f75
commit
adcef575b7
6 files changed
+195
-42
No files matched your search
@@ -43,19 +43,20 @@ The controller runs as a separate Docker container on the same Raspberry Pi. It
|
||||
|
||||
All animations mirror the Home Assistant Voice PE ESPHome firmware exactly.
|
||||
|
||||
| LVA state | Animation | Description |
|
||||
|---|---|---|
|
||||
| No HA connection / not ready | Red twinkle | Random red sparkle across all LEDs |
|
||||
| Idle | Off | All LEDs off |
|
||||
| Wake word detected | Slow clockwise spin | Two trailing arcs at opposing positions |
|
||||
| Listening | Fast clockwise spin | Same dual-arc pattern at 50 ms interval |
|
||||
| Thinking | Pulsing pair | Two opposing LEDs fade in and out |
|
||||
| TTS speaking | Anticlockwise spin | Dual-arc spin in reverse direction |
|
||||
| Muted | Solid ring + red indicators | Full ring on; red at LEDs 1, 4, 7, 10 (mic corners) |
|
||||
| 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 corners if muted |
|
||||
| Volume muted | Solid ring + red indicator | Red at the bottom-mid LED |
|
||||
| LVA state | Animation | Description |
|
||||
| ---------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| No HA connection / not ready | Red twinkle | Random red sparkle across all LEDs |
|
||||
| Idle | Off | All LEDs off |
|
||||
| Wake word detected | Slow clockwise spin | Two trailing arcs at opposing positions |
|
||||
| Listening | Fast clockwise spin | Same dual-arc pattern at 50 ms interval |
|
||||
| Thinking | Pulsing pair | Two opposing LEDs fade in and out |
|
||||
| TTS speaking | Anticlockwise spin | Dual-arc spin in reverse direction |
|
||||
| Muted | Solid ring + red indicators | Full ring on; red at LEDs 1, 4, 7, 10 (mic corners) |
|
||||
| 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 corners if muted |
|
||||
| Volume changed | Volume Display arc | Temporary arc (2.5s) showing the current volume level, starting from LED 0 and sweeping clockwise |
|
||||
| Volume muted | Solid ring + red indicator | Red at the bottom-mid LED |
|
||||
|
||||
"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.
|
||||
|
||||
|
||||
@@ -93,6 +93,13 @@ SPI_DEVICE = 1
|
||||
# 10 ≈ 32 % (gentle default), 31 = maximum.
|
||||
LED_BRIGHTNESS = 10
|
||||
|
||||
# Volume Display effect
|
||||
# Mirrors the ESPHome Voice PE "Volume Display" addressable_lambda. Origin
|
||||
# is LED 6 (bottom-mid) — the same reference LED already used for the
|
||||
# volume_muted indicator (VOLUME_MUTED_LED_INDEX) — so the arc grows from
|
||||
# the same spot that turns solid red when volume is silenced.
|
||||
VOLUME_DISPLAY_SECONDS = 2.5
|
||||
|
||||
# LED that lights up solid red while the media player (speaker) output is
|
||||
# muted at volume 0 — the `volume_muted` peripheral event. Ring index 6 is
|
||||
# the bottom-middle position (180°, directly opposite index 0 at the top),
|
||||
@@ -147,6 +154,10 @@ class SharedState:
|
||||
self.ha_connected: bool = False
|
||||
self.muted: bool = False # mic mute
|
||||
self.volume: float = 1.0
|
||||
# Monotonic deadline until which the Volume Display arc takes over
|
||||
# the ring, set on each volume_changed event. 0 (or in the past)
|
||||
# means the arc is not showing.
|
||||
self.volume_display_until: float = 0.0
|
||||
self.volume_muted: bool = False # media player (speaker) mute
|
||||
self.timer_total_seconds: int = 0
|
||||
self.timer_seconds_left: int = 0
|
||||
@@ -173,6 +184,7 @@ class SharedState:
|
||||
"ha_connected": self.ha_connected,
|
||||
"muted": self.muted,
|
||||
"volume": self.volume,
|
||||
"volume_display_until": self.volume_display_until,
|
||||
"volume_muted": self.volume_muted,
|
||||
"timer_total_seconds": self.timer_total_seconds,
|
||||
"timer_seconds_left": self.timer_seconds_left,
|
||||
@@ -478,6 +490,32 @@ class LEDRing:
|
||||
self._write()
|
||||
return 0.1
|
||||
|
||||
def _anim_volume_display(self, color: RGB, volume: float) -> float:
|
||||
"""
|
||||
Arc showing the current volume level, originating at LED 6
|
||||
(bottom-mid — same LED as VOLUME_MUTED_LED_INDEX) and sweeping
|
||||
clockwise around the ring.
|
||||
|
||||
Mirrors the ESPHome Voice PE "Volume Display" addressable_lambda —
|
||||
proportional arc with a partial-brightness leading LED, plus a red
|
||||
indicator on the origin LED when volume is silenced.
|
||||
"""
|
||||
silenced_color = RED
|
||||
volume_ratio = LED_COUNT * max(0.0, min(1.0, volume))
|
||||
|
||||
for i in range(LED_COUNT):
|
||||
if i <= volume_ratio:
|
||||
brightness = min(volume_ratio - i, 1.0)
|
||||
self._set((VOLUME_MUTED_LED_INDEX + i) % LED_COUNT, _scale(color, brightness))
|
||||
else:
|
||||
self._set((VOLUME_MUTED_LED_INDEX + i) % LED_COUNT, BLACK)
|
||||
|
||||
if volume <= 0.0:
|
||||
self._set(VOLUME_MUTED_LED_INDEX, silenced_color)
|
||||
|
||||
self._write()
|
||||
return 0.05 # matches the 50ms update_interval of the ESPHome effect
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Animation loop
|
||||
# ------------------------------------------------------------------
|
||||
@@ -494,7 +532,12 @@ class LEDRing:
|
||||
t_total = snap["timer_total_seconds"]
|
||||
t_left = snap["timer_seconds_left"]
|
||||
|
||||
if anim == self.ANIM_OFF:
|
||||
# Volume Display takes over the ring temporarily, regardless of
|
||||
# the current pipeline state, then falls back to the normal
|
||||
# animation once its deadline passes.
|
||||
if snap["volume_display_until"] > time.monotonic():
|
||||
sleep = self._anim_volume_display(color, snap["volume"])
|
||||
elif anim == self.ANIM_OFF:
|
||||
sleep = self._anim_off()
|
||||
elif anim == self.ANIM_IDLE:
|
||||
sleep = self._anim_idle()
|
||||
@@ -716,7 +759,10 @@ class LVAClient:
|
||||
self._state.update(assist_state=AssistState.MEDIA_PLAYING)
|
||||
|
||||
elif event == "volume_changed":
|
||||
self._state.update(volume=data.get("volume", 1.0))
|
||||
self._state.update(
|
||||
volume=data.get("volume", 1.0),
|
||||
volume_display_until=time.monotonic() + VOLUME_DISPLAY_SECONDS,
|
||||
)
|
||||
|
||||
elif event == "volume_muted":
|
||||
# Media player (speaker) mute state, carried as {"muted": bool}
|
||||
|
||||
@@ -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 changed | Volume Display arc | Temporary arc (2.5s) showing the current volume level, starting from LED 9 (bottom, between the micro-USB and AUX jack) and sweeping clockwise |
|
||||
| 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,14 @@ USB_PRODUCT_ID = 0x0018
|
||||
LED_COUNT = 12
|
||||
LED_BRIGHTNESS = 10 # 0–31 (APA102 5-bit global brightness register)
|
||||
|
||||
# Volume Display effect
|
||||
# Mirrors the ESPHome Voice PE "Volume Display" addressable_lambda. Origin
|
||||
# is LED 9 (bottom, DOA 270°) — the same reference LED already used for
|
||||
# the volume_muted indicator (VOLUME_MUTED_LED_INDEX), sitting between the
|
||||
# micro-USB connector and the 3.5 mm AUX jack — so the arc grows from the
|
||||
# same spot that turns solid red when volume is silenced.
|
||||
VOLUME_DISPLAY_SECONDS = 2.5
|
||||
|
||||
# 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
|
||||
@@ -134,6 +142,10 @@ class SharedState:
|
||||
self.ha_connected: bool = False
|
||||
self.muted: bool = False # mic mute
|
||||
self.volume: float = 1.0
|
||||
# Monotonic deadline until which the Volume Display arc takes over
|
||||
# the ring, set on each volume_changed event. 0 (or in the past)
|
||||
# means the arc is not showing.
|
||||
self.volume_display_until: float = 0.0
|
||||
self.volume_muted: bool = False # media player volume zero
|
||||
self.timer_total_seconds: int = 0
|
||||
self.timer_seconds_left: int = 0
|
||||
@@ -160,6 +172,7 @@ class SharedState:
|
||||
"ha_connected": self.ha_connected,
|
||||
"muted": self.muted,
|
||||
"volume": self.volume,
|
||||
"volume_display_until": self.volume_display_until,
|
||||
"volume_muted": self.volume_muted,
|
||||
"timer_total_seconds": self.timer_total_seconds,
|
||||
"timer_seconds_left": self.timer_seconds_left,
|
||||
@@ -417,6 +430,32 @@ class LEDRing:
|
||||
self._leds.set(6, _scale(RED, factor))
|
||||
self._leds.set(9, _scale(RED, factor))
|
||||
|
||||
def _anim_volume_display(self, color: RGB, volume: float) -> float:
|
||||
"""
|
||||
Arc showing the current volume level, originating at LED 9
|
||||
(bottom, between the micro-USB and AUX jack — same LED as
|
||||
VOLUME_MUTED_LED_INDEX) and sweeping clockwise around the ring.
|
||||
|
||||
Mirrors the ESPHome Voice PE "Volume Display" addressable_lambda —
|
||||
proportional arc with a partial-brightness leading LED, plus a red
|
||||
indicator on the origin LED when volume is silenced.
|
||||
"""
|
||||
silenced_color = RED
|
||||
volume_ratio = LED_COUNT * max(0.0, min(1.0, volume))
|
||||
|
||||
for i in range(LED_COUNT):
|
||||
if i <= volume_ratio:
|
||||
brightness = min(volume_ratio - i, 1.0)
|
||||
self._leds.set((VOLUME_MUTED_LED_INDEX + i) % LED_COUNT, _scale(color, brightness))
|
||||
else:
|
||||
self._leds.set((VOLUME_MUTED_LED_INDEX + i) % LED_COUNT, BLACK)
|
||||
|
||||
if volume <= 0.0:
|
||||
self._leds.set(VOLUME_MUTED_LED_INDEX, silenced_color)
|
||||
|
||||
self._leds.show()
|
||||
return 0.05 # matches the 50ms update_interval of the ESPHome effect
|
||||
|
||||
def _apply_volume_muted_indicator(self) -> None:
|
||||
"""
|
||||
Overlay a solid red dot on the bottom-middle LED (index
|
||||
@@ -585,7 +624,12 @@ class LEDRing:
|
||||
t_total = snap["timer_total_seconds"]
|
||||
t_left = snap["timer_seconds_left"]
|
||||
|
||||
if anim == self.ANIM_OFF:
|
||||
# Volume Display takes over the ring temporarily, regardless of
|
||||
# the current pipeline state, then falls back to the normal
|
||||
# animation once its deadline passes.
|
||||
if snap["volume_display_until"] > time.monotonic():
|
||||
sleep = self._anim_volume_display(color, snap["volume"])
|
||||
elif anim == self.ANIM_OFF:
|
||||
sleep = self._anim_off()
|
||||
elif anim == self.ANIM_IDLE:
|
||||
sleep = self._anim_idle()
|
||||
@@ -810,7 +854,10 @@ class LVAClient:
|
||||
self._state.update(assist_state=AssistState.MEDIA_PLAYING)
|
||||
|
||||
elif event == "volume_changed":
|
||||
self._state.update(volume=data.get("volume", 1.0))
|
||||
self._state.update(
|
||||
volume=data.get("volume", 1.0),
|
||||
volume_display_until=time.monotonic() + VOLUME_DISPLAY_SECONDS,
|
||||
)
|
||||
|
||||
elif event == "volume_muted":
|
||||
Volume_muted = data.get("muted", True)
|
||||
@@ -818,7 +865,7 @@ class LVAClient:
|
||||
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)
|
||||
self._state.update(assist_state=AssistState.IDLE)
|
||||
|
||||
elif event == "zeroconf":
|
||||
status = data.get("status", "")
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Peripheral controller for the [Satellite1 HAT Board](https://futureproofhomes.net/products/satellite1-top-microphone-board) by FutureProofHomes, running alongside the Linux Voice Assistant (LVA) container.
|
||||
|
||||
The controller runs as a separate Docker container on the same Raspberry Pi. It connects to LVA's peripheral WebSocket API, drives the 12-LED SK6812 ring with animations that mirror the [Home Assistant Voice PE](https://www.home-assistant.io/voice-pe/) LED behaviour, and maps the four hardware buttons to LVA commands with multipress support on the context action button.
|
||||
The controller runs as a separate Docker container on the same Raspberry Pi. It connects to LVA's peripheral WebSocket API, drives the 24-LED WS2812 ring with animations that mirror the [Home Assistant Voice PE](https://www.home-assistant.io/voice-pe/) LED behaviour, and maps the four hardware buttons to LVA commands with multipress support on the context action button.
|
||||
|
||||
---
|
||||
|
||||
@@ -13,7 +13,7 @@ The controller runs as a separate Docker container on the same Raspberry Pi. It
|
||||
| Component | Details |
|
||||
|---|---|
|
||||
| Microphone array | XMOS XVF3800 (USB, far-field, onboard DSP) |
|
||||
| LED ring | 12 × SK6812 RGBW NeoPixels |
|
||||
| LED ring | 24 × WS2812 RGBW NeoPixels |
|
||||
| Buttons | 4 × tactile (top, bottom, left, right) |
|
||||
| Audio output | I2S line-out (optional) |
|
||||
| Interface | Raspberry Pi 40-pin HAT connector |
|
||||
@@ -189,6 +189,7 @@ All animations mirror the Home Assistant Voice PE ESPHome firmware exactly. The
|
||||
| Error | Red pulse | All LEDs red, pulsing |
|
||||
| Timer ticking | Countdown arc | Arc length proportional to `seconds_left / total_seconds`, tinted by your color |
|
||||
| Timer ringing | Pulse + optional red | Full ring pulsing in your color; red at 3 & 9 if muted |
|
||||
| Volume changed | Volume Display arc | Temporary arc (2.5s) showing the current volume level, starting from the AUX jack corner LED (D1, index 0) and sweeping clockwise across all 24 LEDs |
|
||||
| Volume muted | Solid ring + red indicator | red at the AUX jack corner LEDs positions 11, 0, 1 to indicate the media player volume is zero |
|
||||
|
||||
**Note:** All animations except twinkle, off, and error animations use the color from the HA Light entity. This means you can customize the entire LED experience from Home Assistant by adjusting the light's RGB color and brightness.
|
||||
@@ -204,7 +205,7 @@ All animations mirror the Home Assistant Voice PE ESPHome firmware exactly. The
|
||||
Edit `/boot/firmware/config.txt` (or `/boot/config.txt` on older Raspberry Pi OS):
|
||||
|
||||
```ini
|
||||
# Enable PWM on GPIO 12 for the SK6812 LED ring
|
||||
# Enable PWM on GPIO 12 for the WS2812 LED ring
|
||||
dtoverlay=pwm,pin=12,func=4
|
||||
|
||||
# Disable onboard audio — it shares PWM0 with GPIO 12
|
||||
@@ -301,7 +302,7 @@ LONG_PRESS_MS = 1000 # Duration to detect long press (ms)
|
||||
|
||||
|
||||
# LED ring
|
||||
LED_COUNT = 12
|
||||
LED_COUNT = 24
|
||||
LED_BRIGHTNESS = 168 # 0–255, default is 66 % (168)
|
||||
|
||||
# HA Light entity registration
|
||||
|
||||
@@ -7,7 +7,7 @@ hardware buttons to LVA peripheral API commands.
|
||||
|
||||
Hardware layout (Satellite 1 HAT on Raspberry Pi)
|
||||
--------------------------------------------------
|
||||
LED ring : 12 × SK6812 RGBW NeoPixels → GPIO 12 (PWM0)
|
||||
LED ring : 24 x WS2812 RGBW NeoPixels → GPIO 12 (PWM0)
|
||||
Right btn : Volume Up → GPIO 17
|
||||
Left btn : Volume Down → GPIO 27
|
||||
Top btn : Mute / Unmute mic → GPIO 22
|
||||
@@ -91,21 +91,33 @@ BTN_ACTION = 23 # Bottom button
|
||||
BTN_DEBOUNCE_MS = 30 # Milliseconds
|
||||
|
||||
# LED ring
|
||||
LED_COUNT = 12
|
||||
LED_COUNT = 24
|
||||
LED_FREQ_HZ = 800_000
|
||||
LED_DMA = 10
|
||||
LED_BRIGHTNESS = 168 # 66 % of 255 – matches ESPHome default
|
||||
LED_INVERT = False
|
||||
LED_CHANNEL = 0
|
||||
|
||||
# Four mic positions at the cardinal points (top, right, bottom, left),
|
||||
# spaced LED_COUNT/4 apart. On the 24-LED ring: LEDs 0, 6, 12, 18.
|
||||
MIC_LED_POSITIONS = tuple(i * (LED_COUNT // 4) for i in range(4))
|
||||
|
||||
# LEDs physically sitting in the AUX (3.5 mm) audio jack corner of the board,
|
||||
# identified by their silkscreen designators on the Satellite1 HAT (D35, D1,
|
||||
# D2). These three ring positions light up solid red whenever the media
|
||||
# player volume is at zero (the `volume_muted` peripheral event), independent
|
||||
# of whatever pipeline animation is currently running. Verify the mapping
|
||||
# against your board revision — adjust if your unit's jack corner sits at a
|
||||
# different position on the ring.
|
||||
AUX_JACK_LED_INDICES = (11, 0, 1)
|
||||
# of whatever pipeline animation is currently running. Scaled to the 24-LED
|
||||
# ring to cover the same angular span as the original 12-LED mapping —
|
||||
# verify against your board revision and adjust if it differs.
|
||||
AUX_JACK_LED_INDICES = (22, 23, 0, 1, 2)
|
||||
|
||||
# Volume Display effect
|
||||
# Mirrors the ESPHome Voice PE "Volume Display" addressable_lambda, but
|
||||
# rotated so the arc originates at the AUX jack corner LED (D1, index 0)
|
||||
# instead of Voice PE's origin — matching this board's physical AUX jack
|
||||
# placement. Shown as a temporary overlay for this many seconds after
|
||||
# volume_changed.
|
||||
VOLUME_DISPLAY_SECONDS = 2.5
|
||||
|
||||
# Default ring color (ESPHome default: 9.4 % R, 73.3 % G, 94.9 % B)
|
||||
DEFAULT_R, DEFAULT_G, DEFAULT_B = 24, 187, 242
|
||||
@@ -246,6 +258,10 @@ class SharedState:
|
||||
self.ha_connected: bool = False
|
||||
self.muted: bool = False # mic mute
|
||||
self.volume: float = 1.0
|
||||
# Monotonic deadline until which the Volume Display arc takes over
|
||||
# the ring, set on each volume_changed event. 0 (or in the past)
|
||||
# means the arc is not showing.
|
||||
self.volume_display_until: float = 0.0
|
||||
self.volume_muted: bool = False # media player volume zero
|
||||
self.timer_total_seconds: int = 0
|
||||
self.timer_seconds_left: int = 0
|
||||
@@ -276,6 +292,7 @@ class SharedState:
|
||||
"ha_connected": self.ha_connected,
|
||||
"muted": self.muted,
|
||||
"volume": self.volume,
|
||||
"volume_display_until": self.volume_display_until,
|
||||
"volume_muted": self.volume_muted,
|
||||
"timer_total_seconds": self.timer_total_seconds,
|
||||
"timer_seconds_left": self.timer_seconds_left,
|
||||
@@ -499,18 +516,12 @@ class LEDRing:
|
||||
def _anim_muted(self, color: RGB, muted: bool) -> float:
|
||||
"""
|
||||
Solid ring with red indicators at all 4 mic positions when muted.
|
||||
Satellite 1 HAT has mics at 12, 3, 6 and 9 o'clock → LEDs 0, 3, 6, 9.
|
||||
"""
|
||||
for i in range(LED_COUNT):
|
||||
self._set(i, color)
|
||||
|
||||
if muted:
|
||||
# 4 mic positions: top (0), right (3), bottom (6), left (9)
|
||||
# Blank the immediate neighbours so the red indicators stand out
|
||||
self._set(11, BLACK); self._set(0, RED); self._set(1, BLACK)
|
||||
self._set(2, BLACK); self._set(3, RED); self._set(4, BLACK)
|
||||
self._set(5, BLACK); self._set(6, RED); self._set(7, BLACK)
|
||||
self._set(8, BLACK); self._set(9, RED); self._set(10, BLACK)
|
||||
self._apply_mic_indicators()
|
||||
|
||||
self._write()
|
||||
return 0.016
|
||||
@@ -567,16 +578,54 @@ class LEDRing:
|
||||
self._set(i, BLACK)
|
||||
|
||||
if muted:
|
||||
# 4 mic positions: LEDs 0, 3, 6, 9
|
||||
self._set(11, BLACK); self._set(0, RED); self._set(1, BLACK)
|
||||
self._set(2, BLACK); self._set(3, RED); self._set(4, BLACK)
|
||||
self._set(5, BLACK); self._set(6, RED); self._set(7, BLACK)
|
||||
self._set(8, BLACK); self._set(9, RED); self._set(10, BLACK)
|
||||
self._apply_mic_indicators()
|
||||
|
||||
self._index = (LED_COUNT + self._index - 1) % LED_COUNT
|
||||
self._write()
|
||||
return 0.1
|
||||
|
||||
def _anim_volume_display(self, color: RGB, volume: float) -> float:
|
||||
"""
|
||||
Arc showing the current volume level, originating at the AUX jack
|
||||
corner LED (D1, index 0) and sweeping clockwise around the ring.
|
||||
|
||||
Mirrors the ESPHome Voice PE "Volume Display" addressable_lambda
|
||||
exactly, except the arc's origin is rotated from LED 6 to LED 0 to
|
||||
match this board's AUX jack corner LED placement.
|
||||
"""
|
||||
silenced_color = RED
|
||||
volume_ratio = LED_COUNT * max(0.0, min(1.0, volume))
|
||||
|
||||
for i in range(LED_COUNT):
|
||||
if i <= volume_ratio:
|
||||
brightness = min(volume_ratio - i, 1.0)
|
||||
self._set(i, _scale(color, brightness))
|
||||
else:
|
||||
self._set(i, BLACK)
|
||||
|
||||
if volume <= 0.0:
|
||||
self._set(0, silenced_color)
|
||||
|
||||
self._write()
|
||||
return 0.05 # matches the 50ms update_interval of the ESPHome effect
|
||||
|
||||
def _apply_mic_indicators(self) -> None:
|
||||
"""
|
||||
Mark all 4 mic positions (cardinal points) red on the already
|
||||
rendered frame, blanking immediate neighbours so each indicator
|
||||
reads as a distinct dot. Positions are LED_COUNT/4 apart —
|
||||
LEDs 0, 6, 12, 18 on the 24-LED ring.
|
||||
"""
|
||||
for pos in MIC_LED_POSITIONS:
|
||||
self._set((pos - 1) % LED_COUNT, BLACK)
|
||||
self._set(pos, RED)
|
||||
self._set((pos + 1) % LED_COUNT, BLACK)
|
||||
|
||||
def _apply_mic_indicators_pulsed(self, factor: float) -> None:
|
||||
"""Pulsed version for timer_ring — keeps indicators in sync with ring brightness."""
|
||||
for pos in MIC_LED_POSITIONS:
|
||||
self._set(pos, _scale(RED, factor))
|
||||
|
||||
def _apply_volume_muted_indicator(self) -> None:
|
||||
"""
|
||||
Overlay a solid red indicator on the AUX jack corner LEDs
|
||||
@@ -609,7 +658,12 @@ class LEDRing:
|
||||
t_left = snap["timer_seconds_left"]
|
||||
ha_connected = snap["ha_connected"]
|
||||
|
||||
if anim == self.ANIM_IDLE:
|
||||
# Volume Display takes over the ring temporarily, regardless of
|
||||
# the current pipeline state, then falls back to the normal
|
||||
# animation once its deadline passes.
|
||||
if snap["volume_display_until"] > time.monotonic():
|
||||
sleep = self._anim_volume_display(color, snap["volume"])
|
||||
elif anim == self.ANIM_IDLE:
|
||||
sleep = self._anim_idle()
|
||||
|
||||
elif anim == self.ANIM_OFF:
|
||||
@@ -1103,7 +1157,10 @@ class LVAClient:
|
||||
self._state.update(assist_state=AssistState.MEDIA_PLAYING)
|
||||
|
||||
elif event == "volume_changed":
|
||||
self._state.update(volume=data.get("volume", 1.0))
|
||||
self._state.update(
|
||||
volume=data.get("volume", 1.0),
|
||||
volume_display_until=time.monotonic() + VOLUME_DISPLAY_SECONDS,
|
||||
)
|
||||
|
||||
elif event == "volume_muted":
|
||||
# Media player (speaker) mute state, distinct from the
|
||||
|
||||
Reference in new issue
Block a user