Update satellite.py
Fix wake word change bug
This commit is contained in:
@@ -113,8 +113,13 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
# Track if current audio is an announcement
|
# Track if current audio is an announcement
|
||||||
self._is_announcement: bool = False
|
self._is_announcement: bool = False
|
||||||
|
|
||||||
|
# External wake words announced by Home Assistant
|
||||||
self._external_wake_words: Dict[str, VoiceAssistantExternalWakeWord] = {}
|
self._external_wake_words: Dict[str, VoiceAssistantExternalWakeWord] = {}
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# State machine helpers
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
def _set_state(self, new_state: SatelliteState):
|
def _set_state(self, new_state: SatelliteState):
|
||||||
if self._state == new_state:
|
if self._state == new_state:
|
||||||
return
|
return
|
||||||
@@ -137,6 +142,10 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
elif new_state == SatelliteState.ERROR:
|
elif new_state == SatelliteState.ERROR:
|
||||||
self.state.event_bus.publish("voice_error")
|
self.state.event_bus.publish("voice_error")
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Voice assistant events
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
def handle_voice_event(
|
def handle_voice_event(
|
||||||
self, event_type: VoiceAssistantEventType, data: Dict[str, str]
|
self, event_type: VoiceAssistantEventType, data: Dict[str, str]
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -197,6 +206,10 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
self.duck()
|
self.duck()
|
||||||
self._play_timer_finished()
|
self._play_timer_finished()
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Main message handler (called by APIServer)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
def handle_message(self, msg: message.Message) -> Iterable[message.Message]:
|
def handle_message(self, msg: message.Message) -> Iterable[message.Message]:
|
||||||
"""
|
"""
|
||||||
Handles incoming messages from Home Assistant.
|
Handles incoming messages from Home Assistant.
|
||||||
@@ -256,6 +269,7 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
yield ListEntitiesDoneResponse()
|
yield ListEntitiesDoneResponse()
|
||||||
|
|
||||||
elif isinstance(msg, VoiceAssistantConfigurationRequest):
|
elif isinstance(msg, VoiceAssistantConfigurationRequest):
|
||||||
|
# Build list of available wake words (built-in + external)
|
||||||
available_wake_words: List[VoiceAssistantWakeWord] = [
|
available_wake_words: List[VoiceAssistantWakeWord] = [
|
||||||
VoiceAssistantWakeWord(
|
VoiceAssistantWakeWord(
|
||||||
id=ww.id,
|
id=ww.id,
|
||||||
@@ -285,13 +299,17 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
)
|
)
|
||||||
self._external_wake_words[eww.id] = eww
|
self._external_wake_words[eww.id] = eww
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"VoiceAssistantConfigurationRequest: external_wake_words=%s",
|
||||||
|
[eww.id for eww in msg.external_wake_words],
|
||||||
|
)
|
||||||
|
|
||||||
|
# IMPORTANT: Use self.state.active_wake_words directly here, instead of
|
||||||
|
# filtering through self.state.wake_words (which may not yet contain
|
||||||
|
# newly requested models while they're still loading).
|
||||||
yield VoiceAssistantConfigurationResponse(
|
yield VoiceAssistantConfigurationResponse(
|
||||||
available_wake_words=available_wake_words,
|
available_wake_words=available_wake_words,
|
||||||
active_wake_words=[
|
active_wake_words=sorted(self.state.active_wake_words),
|
||||||
ww.id
|
|
||||||
for ww in self.state.wake_words.values()
|
|
||||||
if ww.id in self.state.active_wake_words
|
|
||||||
],
|
|
||||||
max_active_wake_words=2,
|
max_active_wake_words=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -300,18 +318,41 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
self.state.event_bus.publish("ha_connected")
|
self.state.event_bus.publish("ha_connected")
|
||||||
|
|
||||||
elif isinstance(msg, VoiceAssistantSetConfiguration):
|
elif isinstance(msg, VoiceAssistantSetConfiguration):
|
||||||
# Offload to background task to prevent blocking the loop during download
|
requested_ids = list(msg.active_wake_words)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"VoiceAssistantSetConfiguration received: active_wake_words=%s",
|
||||||
|
requested_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update the active_wake_words set immediately so that the *next*
|
||||||
|
# VoiceAssistantConfigurationRequest sees the new state, even while
|
||||||
|
# we are still downloading/loading models in the background.
|
||||||
|
self.state.active_wake_words = set(requested_ids)
|
||||||
|
|
||||||
|
# Offload heavy work (downloads + model loading) to a background task
|
||||||
self.state.loop.create_task(self._handle_set_configuration_task(msg))
|
self.state.loop.create_task(self._handle_set_configuration_task(msg))
|
||||||
# Yield nothing immediately; response will be sent asynchronously
|
# Yield nothing immediately; response to ConfigurationRequest is handled
|
||||||
|
# separately in the VoiceAssistantConfigurationRequest branch.
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# SetConfiguration handler (async, runs in background)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
async def _handle_set_configuration_task(
|
async def _handle_set_configuration_task(
|
||||||
self, msg: VoiceAssistantSetConfiguration
|
self, msg: VoiceAssistantSetConfiguration
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Asynchronous handler for SetConfiguration to avoid blocking I/O."""
|
"""Asynchronous handler for SetConfiguration to avoid blocking I/O."""
|
||||||
|
requested_ids = list(msg.active_wake_words)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Applying SetConfiguration: requested active_wake_words=%s",
|
||||||
|
requested_ids,
|
||||||
|
)
|
||||||
|
|
||||||
active_wake_words: Set[str] = set()
|
active_wake_words: Set[str] = set()
|
||||||
|
|
||||||
for wake_word_id in msg.active_wake_words:
|
for wake_word_id in requested_ids:
|
||||||
if wake_word_id in self.state.wake_words:
|
if wake_word_id in self.state.wake_words:
|
||||||
|
# Already loaded in this process; just mark it active.
|
||||||
active_wake_words.add(wake_word_id)
|
active_wake_words.add(wake_word_id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -335,15 +376,22 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
|
|
||||||
_LOGGER.info("Wake word set: %s", wake_word_id)
|
_LOGGER.info("Wake word set: %s", wake_word_id)
|
||||||
active_wake_words.add(wake_word_id)
|
active_wake_words.add(wake_word_id)
|
||||||
break
|
# Do NOT break; we want to process all requested wake words.
|
||||||
|
|
||||||
|
# Finalize active wake words with the subset that actually succeeded
|
||||||
self.state.active_wake_words = active_wake_words
|
self.state.active_wake_words = active_wake_words
|
||||||
_LOGGER.debug("Active wake words: %s", active_wake_words)
|
_LOGGER.debug(
|
||||||
|
"Active wake words after SetConfiguration: %s", active_wake_words
|
||||||
|
)
|
||||||
|
|
||||||
self.state.preferences.active_wake_words = list(active_wake_words)
|
self.state.preferences.active_wake_words = list(active_wake_words)
|
||||||
self.state.save_preferences()
|
self.state.save_preferences()
|
||||||
self.state.wake_words_changed = True
|
self.state.wake_words_changed = True
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Audio handling and wake word triggers
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
def handle_audio(self, audio_chunk: bytes) -> None:
|
def handle_audio(self, audio_chunk: bytes) -> None:
|
||||||
if self._is_streaming_audio:
|
if self._is_streaming_audio:
|
||||||
self.send_messages([VoiceAssistantAudio(data=audio_chunk)])
|
self.send_messages([VoiceAssistantAudio(data=audio_chunk)])
|
||||||
@@ -430,6 +478,10 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# External wake word download helpers
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
async def _download_external_wake_word(
|
async def _download_external_wake_word(
|
||||||
self, external_wake_word: VoiceAssistantExternalWakeWord
|
self, external_wake_word: VoiceAssistantExternalWakeWord
|
||||||
) -> Optional[AvailableWakeWord]:
|
) -> Optional[AvailableWakeWord]:
|
||||||
@@ -516,6 +568,10 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
wake_word_path=config_path,
|
wake_word_path=config_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Connection lifecycle
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
def connection_lost(self, exc):
|
def connection_lost(self, exc):
|
||||||
super().connection_lost(exc)
|
super().connection_lost(exc)
|
||||||
self._set_state(SatelliteState.ERROR)
|
self._set_state(SatelliteState.ERROR)
|
||||||
|
|||||||
Reference in New Issue
Block a user