async def handle_session_timeout(self, site_id: str, session_id: str, step: int): """Called when a session has timed out.""" try: # Pause execution until timeout await asyncio.sleep(self.session_timeout) # Check if we're still on the same session and step (i.e., no continues) site_session = self.all_sessions.get(session_id) if (site_session and (site_session.site_id == site_id) and (site_session.step == step)): _LOGGER.error("Session timed out for site %s: %s", site_id, session_id) # Abort session await self.publish_all( self.end_session( DialogueSessionTerminationReason.TIMEOUT, site_id=site_id, session_id=session_id, start_next_session=True, )) except Exception as e: _LOGGER.exception("session_timeout") self.publish( DialogueError( error=str(e), context="session_timeout", site_id=site_id, session_id=session_id, ))
async def handle_end( self, end_session: DialogueEndSession ) -> typing.AsyncIterable[typing.Union[EndSessionType, StartSessionType, SayType]]: """End the current session.""" site_session = self.all_sessions.get(end_session.session_id) if not site_session: _LOGGER.warning("No session for id %s. Cannot end.", end_session.session_id) return try: # Say text before ending session if end_session.text: # Forward to TTS async for tts_result in self.say( end_session.text, site_id=site_session.site_id, session_id=end_session.session_id, ): yield tts_result # Update fields if end_session.custom_data is not None: site_session.custom_data = end_session.custom_data _LOGGER.debug("Session ended nominally: %s", site_session.session_id) async for end_result in self.end_session( DialogueSessionTerminationReason.NOMINAL, site_id=site_session.site_id, session_id=site_session.session_id, start_next_session=True, ): yield end_result except Exception as e: _LOGGER.exception("handle_end") yield DialogueError( error=str(e), context=str(end_session), site_id=site_session.site_id, session_id=end_session.session_id, ) # Enable hotword on error yield HotwordToggleOn( site_id=site_session.site_id, reason=HotwordToggleReason.DIALOGUE_SESSION, )
async def handle_wake( self, wakeword_id: str, detected: HotwordDetected ) -> typing.AsyncIterable[typing.Union[EndSessionType, StartSessionType, SayType, SoundsType]]: """Wake word was detected.""" try: session_id = (detected.session_id or f"{detected.site_id}-{wakeword_id}-{uuid4()}") new_session = SessionInfo( session_id=session_id, site_id=detected.site_id, start_session=DialogueStartSession( site_id=detected.site_id, custom_data=wakeword_id, init=DialogueAction(can_be_enqueued=False), ), detected=detected, wakeword_id=wakeword_id, lang=detected.lang, ) # Play wake sound before ASR starts listening async for play_wake_result in self.maybe_play_sound( "wake", site_id=detected.site_id): yield play_wake_result if self.session: # Jump the queue self.session_queue.appendleft(new_session) # Abort previous session async for end_result in self.end_session( DialogueSessionTerminationReason.ABORTED_BY_USER, site_id=self.session.site_id, ): yield end_result else: # Start new session async for start_result in self.start_session(new_session): yield start_result except Exception as e: _LOGGER.exception("handle_wake") yield DialogueError(error=str(e), context=str(detected), site_id=detected.site_id)
async def handle_start( self, start_session: DialogueStartSession ) -> typing.AsyncIterable[typing.Union[StartSessionType, EndSessionType, SayType]]: """Starts or queues a new dialogue session.""" try: session_id = str(uuid4()) new_session = SessionInfo( session_id=session_id, site_id=start_session.site_id, start_session=start_session, ) async for start_result in self.start_session(new_session): yield start_result except Exception as e: _LOGGER.exception("handle_start") yield DialogueError( error=str(e), context=str(start_session), site_id=start_session.site_id )
async def handle_wake( self, wakeword_id: str, detected: HotwordDetected ) -> typing.AsyncIterable[ typing.Union[EndSessionType, StartSessionType, SayType, SoundsType] ]: """Wake word was detected.""" group_lock: typing.Optional[asyncio.Lock] = None try: group_id = "" if self.group_separator: # Split site_id into <GROUP>[separator]<NAME> site_id_parts = detected.site_id.split(self.group_separator, maxsplit=1) if len(site_id_parts) > 1: group_id = site_id_parts[0] if group_id: # Use a lock per group id to prevent multiple satellites from # starting sessions while the wake up sound is being played. async with self.global_wake_lock: group_lock = self.group_wake_lock.get(group_id) if group_lock is None: # Create new lock for group group_lock = asyncio.Lock() self.group_wake_lock[group_id] = group_lock assert group_lock is not None await group_lock.acquire() # Check if a session from the same group is already active. # If so, ignore this wake up. for session in self.all_sessions.values(): # Also check if text has already been captured for this session. # This prevents a new session for a group from being blocked # because a previous (completed) one has not timed out yet. if (session.group_id == group_id) and ( session.text_captured is None ): _LOGGER.debug( "Group %s already has a session (%s). Ignoring wake word detection from %s.", group_id, session.site_id, detected.site_id, ) return # Create new session session_id = ( detected.session_id or f"{detected.site_id}-{wakeword_id}-{uuid4()}" ) new_session = SessionInfo( session_id=session_id, site_id=detected.site_id, start_session=DialogueStartSession( site_id=detected.site_id, custom_data=wakeword_id, init=DialogueAction(can_be_enqueued=False), ), detected=detected, wakeword_id=wakeword_id, lang=detected.lang, group_id=group_id, ) # Play wake sound before ASR starts listening async for play_wake_result in self.maybe_play_sound( "wake", site_id=detected.site_id ): yield play_wake_result site_session = self.session_by_site.get(detected.site_id) if site_session: # Jump the queue self.session_queue_by_site[site_session.site_id].appendleft(new_session) # Abort previous session and start queued session async for end_result in self.end_session( DialogueSessionTerminationReason.ABORTED_BY_USER, site_id=site_session.site_id, session_id=site_session.session_id, start_next_session=True, ): yield end_result else: # Start new session async for start_result in self.start_session(new_session): yield start_result except Exception as e: _LOGGER.exception("handle_wake") yield DialogueError( error=str(e), context=str(detected), site_id=detected.site_id ) finally: if group_lock is not None: group_lock.release()
async def handle_continue( self, continue_session: DialogueContinueSession ) -> typing.AsyncIterable[ typing.Union[AsrStartListening, AsrStopListening, SayType, DialogueError] ]: """Continue the existing session.""" site_session = self.all_sessions.get(continue_session.session_id) if site_session is None: _LOGGER.warning( "No session for id %s. Cannot continue.", continue_session.session_id ) return try: if continue_session.custom_data is not None: # Overwrite custom data site_session.custom_data = continue_session.custom_data if continue_session.lang is not None: # Overwrite language site_session.lang = continue_session.lang site_session.intent_filter = continue_session.intent_filter site_session.send_intent_not_recognized = ( continue_session.send_intent_not_recognized ) site_session.step += 1 _LOGGER.debug( "Continuing session %s (step=%s)", site_session.session_id, site_session.step, ) # Stop listening yield AsrStopListening( site_id=site_session.site_id, session_id=site_session.session_id ) # Ensure hotword is disabled for session yield HotwordToggleOff( site_id=site_session.site_id, reason=HotwordToggleReason.DIALOGUE_SESSION, ) if continue_session.text: # Forward to TTS async for tts_result in self.say( continue_session.text, site_id=site_session.site_id, session_id=continue_session.session_id, ): yield tts_result # Start ASR listening _LOGGER.debug("Listening for session %s", site_session.session_id) yield AsrStartListening( site_id=site_session.site_id, session_id=site_session.session_id, send_audio_captured=site_session.send_audio_captured, lang=site_session.lang, ) # Set up timeout asyncio.create_task( self.handle_session_timeout( site_session.site_id, site_session.session_id, site_session.step ) ) except Exception as e: _LOGGER.exception("handle_continue") yield DialogueError( error=str(e), context=str(continue_session), site_id=site_session.site_id, session_id=continue_session.session_id, )
async def handle_wake( self, wakeword_id: str, detected: HotwordDetected ) -> typing.AsyncIterable[typing.Union[EndSessionType, StartSessionType, SayType, SoundsType]]: """Wake word was detected.""" try: group_id = "" if self.group_separator: # Split site_id into <GROUP>[separator]<NAME> site_id_parts = detected.site_id.split(self.group_separator, maxsplit=1) if len(site_id_parts) > 1: group_id = site_id_parts[0] if group_id: # Check if a session from the same group is already active. # If so, ignore this wake up. for session in self.all_sessions.values(): if session.group_id == group_id: _LOGGER.debug( "Group %s already has a session (%s). Ignoring wake word detection from %s.", group_id, session.site_id, detected.site_id, ) return # Create new session session_id = (detected.session_id or f"{detected.site_id}-{wakeword_id}-{uuid4()}") new_session = SessionInfo( session_id=session_id, site_id=detected.site_id, start_session=DialogueStartSession( site_id=detected.site_id, custom_data=wakeword_id, init=DialogueAction(can_be_enqueued=False), ), detected=detected, wakeword_id=wakeword_id, lang=detected.lang, group_id=group_id, ) # Play wake sound before ASR starts listening async for play_wake_result in self.maybe_play_sound( "wake", site_id=detected.site_id): yield play_wake_result site_session = self.session_by_site.get(detected.site_id) if site_session: # Jump the queue self.session_queue_by_site[site_session.site_id].appendleft( new_session) # Abort previous session and start queued session async for end_result in self.end_session( DialogueSessionTerminationReason.ABORTED_BY_USER, site_id=site_session.site_id, session_id=site_session.session_id, start_next_session=True, ): yield end_result else: # Start new session async for start_result in self.start_session(new_session): yield start_result except Exception as e: _LOGGER.exception("handle_wake") yield DialogueError(error=str(e), context=str(detected), site_id=detected.site_id)