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))
예제 #2
0
    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,
            )
예제 #3
0
    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)
예제 #4
0
def test_hotword_toggle_off():
    """Test HotwordToggleOff."""
    assert HotwordToggleOff.topic() == "hermes/hotword/toggleOff"