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:
1 parent
e334aa3fb1
commit
8303a96279
12 files changed
+177
-18
No files matched your search
@@ -361,7 +361,12 @@
|
||||
volBar.style.width = pct + '%';
|
||||
}
|
||||
if (ev === 'volume_muted' && data) {
|
||||
metaMuted.textContent = data.muted ? 'yes' : 'no';
|
||||
const isMuted = !!data.muted;
|
||||
metaMuted.textContent = isMuted ? 'yes' : 'no';
|
||||
if (isMuted) {
|
||||
metaVol.textContent = '0%';
|
||||
volBar.style.width = '0%';
|
||||
}
|
||||
}
|
||||
if (ev === 'zeroconf' && data) {
|
||||
metaHa.textContent = data.status === 'connected' ? 'yes' : 'connecting…';
|
||||
|
||||
@@ -75,6 +75,7 @@ LED 0 sits above MIC_L and LED 2 sits above MIC_R. When the microphone is muted,
|
||||
| **Timer ringing** | **Blue flash (repeating)** | **All 3** |
|
||||
| Timer ticking | Dim cyan, brightness ∝ time left | All 3 |
|
||||
| Media playing | Dim green steady | All 3 |
|
||||
| **Volume muted** | **Solid red** | **LED 1 only (center 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 (Thinking yellow, Speaking green, Muted red, Timer cyan/blue) are hardcoded so they remain recognisable across user customisation.
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ LED behaviours
|
||||
pipeline_error : red flash on all 3 LEDs
|
||||
timer_ringing : blue flash on all 3 LEDs (repeating)
|
||||
timer_ticking : all 3 dim cyan, brightness proportional to time left
|
||||
media_playing : dim green steady on all 3 LEDs
|
||||
volume_muted : LED 1 solid red, LED 0 and LED 2 off
|
||||
not_ready/no_ha : dim red pulse on all 3 LEDs
|
||||
|
||||
On connect the script registers an HA Light entity with LVA via the
|
||||
@@ -130,7 +130,7 @@ RECONNECT_DELAY_S = 3.0
|
||||
# is used to route incoming light_command events back to this script.
|
||||
LIGHT_OBJECT_ID = "leds"
|
||||
LIGHT_NAME = "LEDs"
|
||||
LIGHT_ICON = "mdi:circle-outline"
|
||||
LIGHT_ICON = "mdi:led-strip-varient"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
@@ -157,6 +157,7 @@ class AssistState(str, Enum):
|
||||
TIMER_TICKING = "timer_ticking"
|
||||
TIMER_RINGING = "timer_ringing"
|
||||
MEDIA_PLAYING = "media_player_playing"
|
||||
VOLUME_MUTED = "volume_muted"
|
||||
|
||||
|
||||
# Effect name. Must match the LEDLightEntity effects list the peripheral
|
||||
@@ -175,8 +176,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 volume zero
|
||||
self.timer_total_seconds: int = 0
|
||||
self.timer_seconds_left: int = 0
|
||||
# Light entity state, driven by HA via light_command events.
|
||||
@@ -213,6 +215,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,
|
||||
@@ -379,6 +382,9 @@ class LEDAnimator:
|
||||
# Left and right LEDs red (mic positions), centre off
|
||||
self._task = asyncio.create_task(self._muted())
|
||||
|
||||
elif state == AssistState.VOLUME_MUTED:
|
||||
self._task = asyncio.create_task(self._volume_muted())
|
||||
|
||||
elif state == AssistState.ERROR:
|
||||
self._task = asyncio.create_task(
|
||||
self._flash_all(RED, flashes=3, on_ms=150, off_ms=100, then_off=True)
|
||||
@@ -456,6 +462,13 @@ class LEDAnimator:
|
||||
self._leds.set(2, RED)
|
||||
self._leds.show(self._brightness(0.6))
|
||||
|
||||
async def _volume_muted(self) -> None:
|
||||
"""Centre LED red; left and right off."""
|
||||
self._leds.set(0, OFF)
|
||||
self._leds.set(1, RED)
|
||||
self._leds.set(2, OFF)
|
||||
self._leds.show(self._brightness(0.6))
|
||||
|
||||
async def _flash_all(
|
||||
self,
|
||||
color: ColorSource,
|
||||
@@ -922,11 +935,11 @@ class LVAClient:
|
||||
self._state.update(volume=data.get("volume", 1.0))
|
||||
|
||||
elif event == "volume_muted":
|
||||
muted = data.get("muted", False)
|
||||
self._state.update(muted=muted)
|
||||
if muted:
|
||||
self._state.update(assist_state=AssistState.MUTED)
|
||||
elif self._state.assist_state == AssistState.MUTED:
|
||||
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.IDLE)
|
||||
|
||||
elif event == "zeroconf":
|
||||
|
||||
@@ -55,6 +55,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 corners if muted |
|
||||
| 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.
|
||||
|
||||
@@ -235,4 +236,4 @@ The seeed-voicecard driver uses I2S — it does **not** use SPI — so there is
|
||||
|
||||
1. Confirm LVA is running and port 6055 is open: `nc -zv localhost 6055`.
|
||||
2. With `network_mode: host`, `localhost` resolves to the Pi itself.
|
||||
3. If LVA runs in a separate Docker network, use its container IP or service name as `--host`.
|
||||
3. If LVA runs in a separate Docker network, use its container IP or service name as `--host`.
|
||||
@@ -93,6 +93,15 @@ SPI_DEVICE = 1
|
||||
# 10 ≈ 32 % (gentle default), 31 = maximum.
|
||||
LED_BRIGHTNESS = 10
|
||||
|
||||
# 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),
|
||||
# following the same 0=top / 3=right / 6=bottom / 9=left convention used for
|
||||
# the mic corner indicators below. Sits between the two bottom mic corners
|
||||
# (LEDs 4 and 7) so it reads as a distinct "speaker muted" dot rather than
|
||||
# overlapping a mic indicator.
|
||||
VOLUME_MUTED_LED_INDEX = 6
|
||||
|
||||
# Default ring colour (R, G, B) — matches HA Voice PE default
|
||||
DEFAULT_R, DEFAULT_G, DEFAULT_B = 24, 187, 242
|
||||
|
||||
@@ -124,6 +133,7 @@ class AssistState(str, Enum):
|
||||
TIMER_TICKING = "timer_ticking"
|
||||
TIMER_RINGING = "timer_ringing"
|
||||
MEDIA_PLAYING = "media_player_playing"
|
||||
VOLUME_MUTED = "volume_muted"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -135,8 +145,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 (speaker) mute
|
||||
self.timer_total_seconds: int = 0
|
||||
self.timer_seconds_left: int = 0
|
||||
# Light entity state, driven by HA via light_command events.
|
||||
@@ -162,6 +173,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,
|
||||
@@ -317,6 +329,19 @@ class LEDRing:
|
||||
self._set(7, _scale(RED, factor))
|
||||
self._set(10, _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``) when the media player output is muted.
|
||||
|
||||
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 corners, but for the distinct speaker-mute state.
|
||||
"""
|
||||
self._set(VOLUME_MUTED_LED_INDEX, RED)
|
||||
self._write()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Animation implementations
|
||||
# ------------------------------------------------------------------
|
||||
@@ -465,6 +490,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"]
|
||||
|
||||
@@ -495,6 +521,9 @@ class LEDRing:
|
||||
else:
|
||||
sleep = self._anim_off()
|
||||
|
||||
if volume_muted:
|
||||
self._apply_volume_muted_indicator()
|
||||
|
||||
time.sleep(sleep)
|
||||
|
||||
|
||||
@@ -690,7 +719,17 @@ class LVAClient:
|
||||
self._state.update(volume=data.get("volume", 1.0))
|
||||
|
||||
elif event == "volume_muted":
|
||||
self._state.update(muted=data.get("muted", False))
|
||||
# Media player (speaker) mute state, carried as {"muted": bool}
|
||||
# per the peripheral API — distinct from the microphone `muted`
|
||||
# event above. Drives the bottom-middle LED indicator rather
|
||||
# than replacing the whole ring animation, since the assist
|
||||
# pipeline can still be doing something else at the same time.
|
||||
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.IDLE)
|
||||
|
||||
elif event == "zeroconf":
|
||||
status = data.get("status", "")
|
||||
@@ -793,4 +832,4 @@ def main() -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
@@ -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", "")
|
||||
|
||||
@@ -68,6 +68,7 @@ In Home Assistant, you can:
|
||||
- Thinking → Pulse in your color
|
||||
- Speaking → Reverse spin in your color
|
||||
- Muted → Solid ring in your color + red at mic positions
|
||||
- Volume muted → Solid ring in your color + red at AUX jack corner LEDs
|
||||
- Timer → Countdown arc in your color
|
||||
|
||||
3. **Set brightness** — scales all animations by the chosen brightness (0–100%). Useful for adapting to ambient lighting.
|
||||
@@ -188,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 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.
|
||||
|
||||
|
||||
@@ -98,6 +98,15 @@ LED_BRIGHTNESS = 168 # 66 % of 255 – matches ESPHome default
|
||||
LED_INVERT = False
|
||||
LED_CHANNEL = 0
|
||||
|
||||
# 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)
|
||||
|
||||
# Default ring color (ESPHome default: 9.4 % R, 73.3 % G, 94.9 % B)
|
||||
DEFAULT_R, DEFAULT_G, DEFAULT_B = 24, 187, 242
|
||||
|
||||
@@ -223,6 +232,7 @@ class AssistState(str, Enum):
|
||||
TIMER_TICKING = "timer_ticking"
|
||||
TIMER_RINGING = "timer_ringing"
|
||||
MEDIA_PLAYING = "media_player_playing"
|
||||
VOLUME_MUTED = "volume_muted"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -234,8 +244,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.
|
||||
@@ -265,6 +276,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,
|
||||
@@ -565,6 +577,21 @@ class LEDRing:
|
||||
self._write()
|
||||
return 0.1
|
||||
|
||||
def _apply_volume_muted_indicator(self) -> None:
|
||||
"""
|
||||
Overlay a solid red indicator on the AUX jack corner LEDs
|
||||
(designators D35, D1, D2 — see ``AUX_JACK_LED_INDICES``) when the
|
||||
media player output is muted.
|
||||
|
||||
Applied as a post-render overlay after the current pipeline
|
||||
animation has already written its frame, so the indicator shows up
|
||||
consistently no matter which animation is active, without having to
|
||||
thread the flag through every individual ``_anim_*`` method.
|
||||
"""
|
||||
for i in AUX_JACK_LED_INDICES:
|
||||
self._set(i, RED)
|
||||
self._write()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Animation loop
|
||||
# ------------------------------------------------------------------
|
||||
@@ -577,6 +604,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"]
|
||||
ha_connected = snap["ha_connected"]
|
||||
@@ -620,6 +648,9 @@ class LEDRing:
|
||||
else:
|
||||
sleep = self._anim_off()
|
||||
|
||||
if volume_muted:
|
||||
self._apply_volume_muted_indicator()
|
||||
|
||||
time.sleep(sleep)
|
||||
|
||||
|
||||
@@ -986,6 +1017,9 @@ class LVAClient:
|
||||
self._state.update(
|
||||
muted=data.get("muted", False),
|
||||
volume=data.get("volume", 1.0),
|
||||
# Not currently sent by LVA's snapshot payload, but read it
|
||||
# defensively in case a future LVA version adds it.
|
||||
volume_muted=data.get("volume_muted", False),
|
||||
ha_connected=data.get("ha_connected", False),
|
||||
)
|
||||
snap = self._state.snapshot
|
||||
@@ -1072,7 +1106,17 @@ class LVAClient:
|
||||
self._state.update(volume=data.get("volume", 1.0))
|
||||
|
||||
elif event == "volume_muted":
|
||||
self._state.update(muted=data.get("muted", False))
|
||||
# Media player (speaker) mute state, distinct from the
|
||||
# microphone `muted` event above. Drives the red indicator on
|
||||
# the AUX jack corner LEDs (see AUX_JACK_LED_INDICES) rather
|
||||
# than swapping the whole ring animation, since the assist
|
||||
# pipeline can still be doing something else at the same time.
|
||||
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.IDLE)
|
||||
|
||||
# --- Zeroconf / connection events ----------------------------------
|
||||
elif event == "zeroconf":
|
||||
|
||||
@@ -217,6 +217,7 @@ class ServerState:
|
||||
_LOGGER.debug("Skipping save - volume unchanged")
|
||||
return
|
||||
|
||||
previous_muted = self.volume == 0.0
|
||||
self.volume = clamped_volume
|
||||
self.preferences.volume = clamped_volume
|
||||
_LOGGER.info("Saving volume %s to %s", clamped_volume, self.preferences_path)
|
||||
@@ -230,6 +231,10 @@ class ServerState:
|
||||
|
||||
api.emit_event_sync(LVAEvent.VOLUME_CHANGED, {"volume": round(clamped_volume, 3)})
|
||||
|
||||
new_muted = clamped_volume == 0.0
|
||||
if previous_muted != new_muted:
|
||||
api.emit_event_sync(LVAEvent.VOLUME_MUTED, {"muted": new_muted})
|
||||
|
||||
def persist_mic_gain(self, gain: float) -> None:
|
||||
"""Persist the microphone auto gain value."""
|
||||
gain_int = int(gain)
|
||||
|
||||
@@ -278,6 +278,7 @@ class PeripheralAPIServer:
|
||||
"data": {
|
||||
"muted": state.muted,
|
||||
"volume": round(state.volume, 3),
|
||||
"volume_muted": state.volume == 0.0,
|
||||
"ha_connected": state.connected,
|
||||
"last_stt_text": self._last_stt_text,
|
||||
"last_tts_text": self._last_tts_text,
|
||||
@@ -659,6 +660,7 @@ class PeripheralAPIServer:
|
||||
LVAEvent.MEDIA_PLAYER_PLAYING,
|
||||
LVAEvent.DISCONNECTED,
|
||||
LVAEvent.PIPELINE_ERROR,
|
||||
LVAEvent.VOLUME_MUTED,
|
||||
}
|
||||
if event in _STATE_EVENTS:
|
||||
self._current_state = event
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Unit tests for shared models."""
|
||||
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -192,6 +192,17 @@ class TestPersistVolume:
|
||||
state.persist_volume(1.0)
|
||||
assert state.volume == 1.0
|
||||
|
||||
def test_emits_volume_muted_event_when_volume_zero(self, tmp_path):
|
||||
from linux_voice_assistant.peripheral_api import LVAEvent
|
||||
|
||||
state = make_server_state(tmp_path)
|
||||
state.peripheral_api = Mock()
|
||||
|
||||
state.persist_volume(0.0)
|
||||
|
||||
state.peripheral_api.emit_event_sync.assert_any_call(LVAEvent.VOLUME_CHANGED, {"volume": 0.0})
|
||||
state.peripheral_api.emit_event_sync.assert_any_call(LVAEvent.VOLUME_MUTED, {"muted": True})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ServerState.persist_mic_gain()
|
||||
|
||||
Reference in new issue
Block a user