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,
            )
Example #3
0
    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)