async def start_session( self, new_session: SessionInfo ) -> typing.AsyncIterable[typing.Union[StartSessionType, EndSessionType, SayType]]: """Start a new session.""" start_session = new_session.start_session site_session = self.session_by_site.get(new_session.site_id) if start_session.init.type == DialogueActionType.NOTIFICATION: # Notification session notification = start_session.init assert isinstance( notification, DialogueNotification), "Not a DialogueNotification" if not site_session: # Create new session just for TTS _LOGGER.debug("Starting new session (id=%s)", new_session.session_id) self.all_sessions[new_session.session_id] = new_session self.session_by_site[new_session.site_id] = new_session yield DialogueSessionStarted( site_id=new_session.site_id, session_id=new_session.session_id, custom_data=new_session.custom_data, lang=new_session.lang, ) site_session = new_session if notification.text: async for say_result in self.say( notification.text, site_id=site_session.site_id, session_id=site_session.session_id, ): yield say_result # End notification session immedately _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 else: # Action session action = start_session.init assert isinstance(action, DialogueAction), "Not a DialogueAction" new_session.custom_data = start_session.custom_data new_session.intent_filter = action.intent_filter new_session.send_intent_not_recognized = action.send_intent_not_recognized start_new_session = True if site_session: if action.can_be_enqueued: # Queue session for later session_queue = self.session_queue_by_site[ new_session.site_id] start_new_session = False session_queue.append(new_session) yield DialogueSessionQueued( session_id=new_session.session_id, site_id=new_session.site_id, custom_data=new_session.custom_data, ) else: # Abort existing session _LOGGER.debug("Session aborted: %s", site_session.session_id) 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=False, ): yield end_result if start_new_session: # Start new session _LOGGER.debug("Starting new session (id=%s)", new_session.session_id) self.all_sessions[new_session.session_id] = new_session self.session_by_site[new_session.site_id] = new_session yield DialogueSessionStarted( site_id=new_session.site_id, session_id=new_session.session_id, custom_data=new_session.custom_data, lang=new_session.lang, ) # Disable hotword for session yield HotwordToggleOff( site_id=new_session.site_id, reason=HotwordToggleReason.DIALOGUE_SESSION, ) if action.text: # Forward to TTS async for say_result in self.say( action.text, site_id=new_session.site_id, session_id=new_session.session_id, ): yield say_result # Start ASR listening _LOGGER.debug("Listening for session %s", new_session.session_id) if (new_session.detected and new_session.detected.send_audio_captured is not None): # Use setting from hotword detection new_session.send_audio_captured = ( new_session.detected.send_audio_captured) yield AsrStartListening( site_id=new_session.site_id, session_id=new_session.session_id, send_audio_captured=new_session.send_audio_captured, wakeword_id=new_session.wakeword_id, lang=new_session.lang, ) # Set up timeout asyncio.create_task( self.handle_session_timeout(new_session.site_id, new_session.session_id, new_session.step))
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 maybe_play_sound( self, sound_name: str, site_id: typing.Optional[str] = None, request_id: typing.Optional[str] = None, block: bool = True, ) -> typing.AsyncIterable[SoundsType]: """Play WAV sound through audio out if it exists.""" if site_id in self.no_sound: _LOGGER.debug("Sound is disabled for site %s", site_id) return site_id = site_id or self.site_id wav_path = self.sound_paths.get(sound_name) if wav_path: if wav_path.is_dir(): wav_path = random.choice(os.listdir(wav_path)) elif not wav_path.is_file(): _LOGGER.error("WAV does not exist: %s", str(wav_path)) return _LOGGER.debug("Playing WAV %s", str(wav_path)) wav_bytes = wav_path.read_bytes() if (self.volume is not None) and (self.volume != 1.0): wav_bytes = DialogueHermesMqtt.change_volume( wav_bytes, self.volume) # Send messages request_id = request_id or str(uuid4()) finished_event = asyncio.Event() finished_id = request_id self.message_events[AudioPlayFinished][ finished_id] = finished_event # Disable ASR/hotword at site yield HotwordToggleOff(site_id=site_id, reason=HotwordToggleReason.PLAY_AUDIO) yield AsrToggleOff(site_id=site_id, reason=AsrToggleReason.PLAY_AUDIO) # Wait for messages to be delivered await asyncio.sleep(self.toggle_delay) try: yield ( AudioPlayBytes(wav_bytes=wav_bytes), { "site_id": site_id, "request_id": request_id }, ) # Wait for finished event or WAV duration if block: wav_duration = get_wav_duration(wav_bytes) wav_timeout = wav_duration + self.sound_timeout_extra _LOGGER.debug( "Waiting for playFinished (id=%s, timeout=%s)", finished_id, wav_timeout, ) await asyncio.wait_for(finished_event.wait(), timeout=wav_timeout) except asyncio.TimeoutError: _LOGGER.warning("Did not receive sayFinished before timeout") except Exception: _LOGGER.exception("maybe_play_sound") finally: # Wait for audio to finish playing await asyncio.sleep(self.toggle_delay) # Re-enable ASR/hotword at site yield HotwordToggleOn(site_id=site_id, reason=HotwordToggleReason.PLAY_AUDIO) yield AsrToggleOn(site_id=site_id, reason=AsrToggleReason.PLAY_AUDIO)
def test_hotword_toggle_off(): """Test HotwordToggleOff.""" assert HotwordToggleOff.topic() == "hermes/hotword/toggleOff"