Leds and buttons events (#266)

Use events and commands to control custom onboard leds and buttons. Can be used to communicate with an external Python script via a WebSocket API running in a container alongside LVA.
This commit is contained in:
Omar Amin authored and GitHub committed 2026-07-03 19:58:45 +03:00
1 parent e1770d071c
commit 2d460b6728
51 files changed
+9251 -117

No files matched your search

+51 -1
View File
@@ -6,14 +6,16 @@ from dataclasses import asdict, dataclass, field
from enum import Enum
from pathlib import Path
from queue import Queue
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
if TYPE_CHECKING:
from pymicro_wakeword import MicroWakeWord
from pyopen_wakeword import OpenWakeWord
from .entity import (
ButtonEventSensorEntity,
ESPHomeEntity,
LEDLightEntity,
MediaPlayerEntity,
MicSettingEntity,
MuteSwitchEntity,
@@ -59,6 +61,23 @@ class AvailableWakeWord:
raise ValueError(f"Unexpected wake word type: {self.type}")
@dataclass
class LightRegistration:
"""Capabilities a peripheral declares for one of its Light entities.
The peripheral sends this with the register_light command after
connecting. LVA materialises a matching LEDLightEntity so HA can
control it.
"""
name: str
object_id: str
icon: str = "mdi:led-strip-variant"
effects: List[str] = field(default_factory=list)
supports_rgb: bool = True
supports_brightness: bool = True
@dataclass
class Preferences:
active_wake_words: List[Optional[str]] = field(default_factory=list)
@@ -91,10 +110,14 @@ class ServerState:
music_player: "MpvMediaPlayer"
tts_player: "MpvMediaPlayer"
wakeup_sound: str
start_listening_sound: str
processing_sound: str
timer_finished_sound: str
mute_sound: str
unmute_sound: str
button_double_press_sound: str
button_triple_press_sound: str
button_long_press_sound: str
preferences: Preferences
preferences_path: Path
download_dir: Path
@@ -104,6 +127,26 @@ class ServerState:
satellite: "Optional[VoiceSatelliteProtocol]" = None
mute_switch_entity: "Optional[MuteSwitchEntity]" = None
thinking_sound_entity: "Optional[ThinkingSoundEntity]" = None
button_event_sensor_entity: "Optional[ButtonEventSensorEntity]" = None
# Lights declared by peripherals via register_light. Survives HA
# reconnects so the satellite can rebuild its entities whenever it
# is constructed again.
pending_lights: "List[LightRegistration]" = field(default_factory=list)
# Materialised LightEntities keyed by object_id, so light_command
# events can be routed back to the right peripheral hardware.
led_light_entities: "Dict[str, LEDLightEntity]" = field(default_factory=dict)
# True once a peripheral sends register_button. Gates creation of
# ButtonEventSensorEntity so the HA device page only shows the button
# entity when hardware that actually supports button presses is present.
# Survives HA reconnects so the entity is re-registered automatically.
pending_button: bool = False
# Optional peripheral WebSocket API (LEDs, buttons, HAT boards).
# Assigned in __main__ before the event loop starts.
peripheral_api: "Optional[Any]" = None # PeripheralAPIServer at runtime
sensitivity_1_number_entity: "Optional[WakeWord1SensitivityNumberEntity]" = None
sensitivity_2_number_entity: "Optional[WakeWord2SensitivityNumberEntity]" = None
stop_sensitivity_number_entity: "Optional[StopWordSensitivityNumberEntity]" = None
@@ -161,6 +204,13 @@ class ServerState:
self.save_preferences()
_LOGGER.info("Volume saved successfully")
# Notify peripheral container (thread-safe; may be called from mpv callbacks)
api = self.peripheral_api
if api is not None:
from .peripheral_api import LVAEvent # local import avoids circular dep
api.emit_event_sync(LVAEvent.VOLUME_CHANGED, {"volume": round(clamped_volume, 3)})
def persist_mic_gain(self, gain: float) -> None:
"""Persist the microphone auto gain value."""
gain_int = int(gain)