Goal: satellite.py currently uses a set of boolean flags (_is_streaming_audio, _tts_played, _run_finished, _is_speaking) to figure out what it's doing. This is hard to read and easy to break. Replaced this with a clean, formal state machine (using an Enum). This makes the logic explicit and much easier to manage. added the SatelliteState Enum at the top and removed the media_player_entity from ServerState as it's better managed by the satellite itself
111 lines
3.2 KiB
Python
111 lines
3.2 KiB
Python
"""Shared models."""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
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, Union
|
|
|
|
from .event_bus import EventBus
|
|
|
|
if TYPE_CHECKING:
|
|
from .entity import ESPHomeEntity, MediaPlayerEntity
|
|
from .microwakeword import MicroWakeWord
|
|
from .mpv_player import MpvMediaPlayer
|
|
from .openwakeword import OpenWakeWord
|
|
from .satellite import VoiceSatelliteProtocol
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class SatelliteState(str, Enum):
|
|
"""Voice satellite state."""
|
|
IDLE = "idle"
|
|
LISTENING = "listening" # Wake word detected
|
|
THINKING = "thinking" # VAD, waiting for STT/intent
|
|
RESPONDING = "responding" # Playing TTS
|
|
ERROR = "error"
|
|
|
|
|
|
class WakeWordType(str, Enum):
|
|
MICRO_WAKE_WORD = "micro"
|
|
OPEN_WAKE_WORD = "openWakeWord"
|
|
|
|
|
|
@dataclass
|
|
class AvailableWakeWord:
|
|
id: str
|
|
type: WakeWordType
|
|
wake_word: str
|
|
trained_languages: List[str]
|
|
config_path: Path
|
|
|
|
def load(
|
|
self, libtensorflowlite_c_path: Path
|
|
) -> "Union[MicroWakeWord, OpenWakeWord]":
|
|
if self.type == WakeWordType.MICRO_WAKE_WORD:
|
|
from .microwakeword import MicroWakeWord
|
|
|
|
return MicroWakeWord.from_config(
|
|
config_path=self.config_path,
|
|
libtensorflowlite_c_path=libtensorflowlite_c_path,
|
|
)
|
|
|
|
if self.type == WakeWordType.OPEN_WAKE_WORD:
|
|
from .openwakeword import OpenWakeWord
|
|
|
|
return OpenWakeWord.from_config(
|
|
config_path=self.config_path,
|
|
libtensorflowlite_c_path=libtensorflowlite_c_path,
|
|
)
|
|
|
|
raise ValueError(f"Unexpected wake word type: {self.type}")
|
|
|
|
|
|
@dataclass
|
|
class Preferences:
|
|
active_wake_words: List[str] = field(default_factory=list)
|
|
volume_level: float = 1.0
|
|
num_leds: int = 3
|
|
|
|
|
|
@dataclass
|
|
class ServerState:
|
|
# --- Fields WITHOUT default values ---
|
|
name: str
|
|
mac_address: str
|
|
audio_queue: "Queue[Optional[bytes]]"
|
|
entities: "List[ESPHomeEntity]"
|
|
available_wake_words: "Dict[str, AvailableWakeWord]"
|
|
wake_words: "Dict[str, Union[MicroWakeWord, OpenWakeWord]]"
|
|
stop_word: "MicroWakeWord"
|
|
music_player: "MpvMediaPlayer"
|
|
tts_player: "MpvMediaPlayer"
|
|
wakeup_sound: str
|
|
timer_finished_sound: str
|
|
preferences: Preferences
|
|
preferences_path: Path
|
|
libtensorflowlite_c_path: Path
|
|
event_bus: EventBus
|
|
loop: asyncio.AbstractEventLoop
|
|
oww_melspectrogram_path: Path
|
|
oww_embedding_path: Path
|
|
|
|
# --- Fields WITH default values ---
|
|
satellite: "Optional[VoiceSatelliteProtocol]" = None
|
|
wake_words_changed: bool = False
|
|
refractory_seconds: float = 2.0
|
|
mic_muted: bool = False
|
|
|
|
def save_preferences(self) -> None:
|
|
"""Save preferences as JSON."""
|
|
_LOGGER.debug("Saving preferences: %s", self.preferences_path)
|
|
self.preferences_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(self.preferences_path, "w", encoding="utf-8") as preferences_file:
|
|
json.dump(
|
|
asdict(self.preferences), preferences_file, ensure_ascii=False, indent=4
|
|
)
|