Merge branch 'main' into issue_128
This commit is contained in:
5 files changed
+95
-67
No files matched your search
@@ -33,7 +33,7 @@ _SOUNDS_DIR = _REPO_DIR / "sounds"
|
|||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--name", required=True)
|
parser.add_argument("--name")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--audio-input-device",
|
"--audio-input-device",
|
||||||
help="soundcard name for input device (see --list-input-devices)",
|
help="soundcard name for input device (see --list-input-devices)",
|
||||||
@@ -82,17 +82,20 @@ async def main() -> None:
|
|||||||
"--timer-finished-sound", default=str(_SOUNDS_DIR / "timer_finished.flac")
|
"--timer-finished-sound", default=str(_SOUNDS_DIR / "timer_finished.flac")
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--processing-sound", default=str(_SOUNDS_DIR / "processing.wav"),
|
"--processing-sound",
|
||||||
help="Short sound to play while assistant is processing (thinking)"
|
default=str(_SOUNDS_DIR / "processing.wav"),
|
||||||
|
help="Short sound to play while assistant is processing (thinking)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--mute-sound", default=str(_SOUNDS_DIR / "mute_switch_on.flac"),
|
"--mute-sound",
|
||||||
help="Sound to play when muting the assistant"
|
default=str(_SOUNDS_DIR / "mute_switch_on.flac"),
|
||||||
|
help="Sound to play when muting the assistant",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--unmute-sound", default=str(_SOUNDS_DIR / "mute_switch_off.flac"),
|
"--unmute-sound",
|
||||||
help="Sound to play when unmuting the assistant"
|
default=str(_SOUNDS_DIR / "mute_switch_off.flac"),
|
||||||
)
|
help="Sound to play when unmuting the assistant",
|
||||||
|
)
|
||||||
#
|
#
|
||||||
parser.add_argument("--preferences-file", default=_REPO_DIR / "preferences.json")
|
parser.add_argument("--preferences-file", default=_REPO_DIR / "preferences.json")
|
||||||
#
|
#
|
||||||
@@ -105,8 +108,10 @@ async def main() -> None:
|
|||||||
"--port", type=int, default=6053, help="Port for ESPHome server (default: 6053)"
|
"--port", type=int, default=6053, help="Port for ESPHome server (default: 6053)"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--enable-thinking-sound", action="store_true", help="Enable thinking sound on startup"
|
"--enable-thinking-sound",
|
||||||
)
|
action="store_true",
|
||||||
|
help="Enable thinking sound on startup",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--debug", action="store_true", help="Print DEBUG messages to console"
|
"--debug", action="store_true", help="Print DEBUG messages to console"
|
||||||
)
|
)
|
||||||
@@ -133,6 +138,9 @@ async def main() -> None:
|
|||||||
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
|
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
|
||||||
_LOGGER.debug(args)
|
_LOGGER.debug(args)
|
||||||
|
|
||||||
|
if not args.name:
|
||||||
|
parser.error("the following arguments are required: --name")
|
||||||
|
|
||||||
args.download_dir = Path(args.download_dir)
|
args.download_dir = Path(args.download_dir)
|
||||||
args.download_dir.mkdir(parents=True, exist_ok=True)
|
args.download_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -242,7 +250,7 @@ async def main() -> None:
|
|||||||
timer_finished_sound=args.timer_finished_sound,
|
timer_finished_sound=args.timer_finished_sound,
|
||||||
processing_sound=args.processing_sound,
|
processing_sound=args.processing_sound,
|
||||||
mute_sound=args.mute_sound,
|
mute_sound=args.mute_sound,
|
||||||
unmute_sound=args.unmute_sound,
|
unmute_sound=args.unmute_sound,
|
||||||
preferences=preferences,
|
preferences=preferences,
|
||||||
preferences_path=preferences_path,
|
preferences_path=preferences_path,
|
||||||
refractory_seconds=args.refractory_seconds,
|
refractory_seconds=args.refractory_seconds,
|
||||||
@@ -250,7 +258,7 @@ async def main() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if args.enable_thinking_sound:
|
if args.enable_thinking_sound:
|
||||||
state.save_preferences()
|
state.save_preferences()
|
||||||
|
|
||||||
process_audio_thread = threading.Thread(
|
process_audio_thread = threading.Thread(
|
||||||
target=process_audio,
|
target=process_audio,
|
||||||
@@ -370,7 +378,11 @@ def process_audio(state: ServerState, mic, block_size: int):
|
|||||||
if state.stop_word.process_streaming(micro_input):
|
if state.stop_word.process_streaming(micro_input):
|
||||||
stopped = True
|
stopped = True
|
||||||
|
|
||||||
if stopped and (state.stop_word.id in state.active_wake_words) and not state.muted:
|
if (
|
||||||
|
stopped
|
||||||
|
and (state.stop_word.id in state.active_wake_words)
|
||||||
|
and not state.muted
|
||||||
|
):
|
||||||
state.satellite.stop()
|
state.satellite.stop()
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected error handling audio")
|
_LOGGER.exception("Unexpected error handling audio")
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class APIServer(asyncio.Protocol):
|
|||||||
self._writelines = None
|
self._writelines = None
|
||||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||||
self._loop_thread_id: Optional[int] = None
|
self._loop_thread_id: Optional[int] = None
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def handle_message(self, msg: message.Message) -> Iterable[message.Message]:
|
def handle_message(self, msg: message.Message) -> Iterable[message.Message]:
|
||||||
pass
|
pass
|
||||||
@@ -93,7 +93,7 @@ class APIServer(asyncio.Protocol):
|
|||||||
):
|
):
|
||||||
self._loop.call_soon_threadsafe(self._writelines, packet_bytes)
|
self._loop.call_soon_threadsafe(self._writelines, packet_bytes)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._writelines(packet_bytes)
|
self._writelines(packet_bytes)
|
||||||
|
|
||||||
def connection_made(self, transport) -> None:
|
def connection_made(self, transport) -> None:
|
||||||
@@ -105,7 +105,7 @@ class APIServer(asyncio.Protocol):
|
|||||||
self._loop = None
|
self._loop = None
|
||||||
else:
|
else:
|
||||||
self._loop_thread_id = threading.get_ident()
|
self._loop_thread_id = threading.get_ident()
|
||||||
|
|
||||||
def data_received(self, data: bytes):
|
def data_received(self, data: bytes):
|
||||||
if self._buffer is None:
|
if self._buffer is None:
|
||||||
self._buffer = data
|
self._buffer = data
|
||||||
@@ -160,7 +160,7 @@ class APIServer(asyncio.Protocol):
|
|||||||
self._writelines = None
|
self._writelines = None
|
||||||
self._loop = None
|
self._loop = None
|
||||||
self._loop_thread_id = None
|
self._loop_thread_id = None
|
||||||
|
|
||||||
def _read_varuint(self) -> int:
|
def _read_varuint(self) -> int:
|
||||||
"""Read a varuint from the buffer or -1 if the buffer runs out of bytes."""
|
"""Read a varuint from the buffer or -1 if the buffer runs out of bytes."""
|
||||||
if not self._buffer:
|
if not self._buffer:
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined]
|
|||||||
ListEntitiesMediaPlayerResponse,
|
ListEntitiesMediaPlayerResponse,
|
||||||
ListEntitiesRequest,
|
ListEntitiesRequest,
|
||||||
ListEntitiesSwitchResponse,
|
ListEntitiesSwitchResponse,
|
||||||
SwitchCommandRequest,
|
|
||||||
SwitchStateResponse,
|
|
||||||
MediaPlayerCommandRequest,
|
MediaPlayerCommandRequest,
|
||||||
MediaPlayerStateResponse,
|
MediaPlayerStateResponse,
|
||||||
SubscribeHomeAssistantStatesRequest,
|
SubscribeHomeAssistantStatesRequest,
|
||||||
|
SwitchCommandRequest,
|
||||||
|
SwitchStateResponse,
|
||||||
)
|
)
|
||||||
from aioesphomeapi.model import (
|
from aioesphomeapi.model import (
|
||||||
|
EntityCategory,
|
||||||
MediaPlayerCommand,
|
MediaPlayerCommand,
|
||||||
MediaPlayerEntityFeature,
|
MediaPlayerEntityFeature,
|
||||||
MediaPlayerState,
|
MediaPlayerState,
|
||||||
EntityCategory,
|
|
||||||
)
|
)
|
||||||
from google.protobuf import message
|
from google.protobuf import message
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ SUPPORTED_MEDIA_PLAYER_FEATURES = (
|
|||||||
| MediaPlayerEntityFeature.MEDIA_ANNOUNCE
|
| MediaPlayerEntityFeature.MEDIA_ANNOUNCE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ESPHomeEntity:
|
class ESPHomeEntity:
|
||||||
def __init__(self, server: APIServer) -> None:
|
def __init__(self, server: APIServer) -> None:
|
||||||
self.server = server
|
self.server = server
|
||||||
@@ -196,8 +197,10 @@ class MediaPlayerEntity(ESPHomeEntity):
|
|||||||
muted=self.muted,
|
muted=self.muted,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class MuteSwitchEntity(ESPHomeEntity):
|
class MuteSwitchEntity(ESPHomeEntity):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -215,12 +218,14 @@ class MuteSwitchEntity(ESPHomeEntity):
|
|||||||
self.object_id = object_id
|
self.object_id = object_id
|
||||||
self._get_muted = get_muted
|
self._get_muted = get_muted
|
||||||
self._set_muted = set_muted
|
self._set_muted = set_muted
|
||||||
self._switch_state = self._get_muted() # Sync internal state with actual muted value on init
|
self._switch_state = (
|
||||||
|
self._get_muted()
|
||||||
|
) # Sync internal state with actual muted value on init
|
||||||
|
|
||||||
def update_set_muted(self, set_muted: Callable[[bool], None]) -> None:
|
def update_set_muted(self, set_muted: Callable[[bool], None]) -> None:
|
||||||
# Update the callback used to change the mute state.
|
# Update the callback used to change the mute state.
|
||||||
self._set_muted = set_muted
|
self._set_muted = set_muted
|
||||||
|
|
||||||
def update_get_muted(self, get_muted: Callable[[], bool]) -> None:
|
def update_get_muted(self, get_muted: Callable[[], bool]) -> None:
|
||||||
# Update the callback used to read the mute state.
|
# Update the callback used to read the mute state.
|
||||||
self._get_muted = get_muted
|
self._get_muted = get_muted
|
||||||
@@ -249,7 +254,8 @@ class MuteSwitchEntity(ESPHomeEntity):
|
|||||||
# Always return our internal switch state
|
# Always return our internal switch state
|
||||||
self.sync_with_state()
|
self.sync_with_state()
|
||||||
yield SwitchStateResponse(key=self.key, state=self._switch_state)
|
yield SwitchStateResponse(key=self.key, state=self._switch_state)
|
||||||
|
|
||||||
|
|
||||||
class ThinkingSoundEntity(ESPHomeEntity):
|
class ThinkingSoundEntity(ESPHomeEntity):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -268,12 +274,16 @@ class ThinkingSoundEntity(ESPHomeEntity):
|
|||||||
self._get_thinking_sound_enabled = get_thinking_sound_enabled
|
self._get_thinking_sound_enabled = get_thinking_sound_enabled
|
||||||
self._set_thinking_sound_enabled = set_thinking_sound_enabled
|
self._set_thinking_sound_enabled = set_thinking_sound_enabled
|
||||||
self._switch_state = self._get_thinking_sound_enabled() # Sync internal state
|
self._switch_state = self._get_thinking_sound_enabled() # Sync internal state
|
||||||
|
|
||||||
def update_get_thinking_sound_enabled(self, get_thinking_sound_enabled: Callable[[], bool]) -> None:
|
def update_get_thinking_sound_enabled(
|
||||||
|
self, get_thinking_sound_enabled: Callable[[], bool]
|
||||||
|
) -> None:
|
||||||
# Update the callback used to read the thinking sound enabled state.
|
# Update the callback used to read the thinking sound enabled state.
|
||||||
self._get_thinking_sound_enabled = get_thinking_sound_enabled
|
self._get_thinking_sound_enabled = get_thinking_sound_enabled
|
||||||
|
|
||||||
def update_set_thinking_sound_enabled(self, set_thinking_sound_enabled: Callable[[bool], None]) -> None:
|
def update_set_thinking_sound_enabled(
|
||||||
|
self, set_thinking_sound_enabled: Callable[[bool], None]
|
||||||
|
) -> None:
|
||||||
# Update the callback used to change the thinking sound enabled state.
|
# Update the callback used to change the thinking sound enabled state.
|
||||||
self._set_thinking_sound_enabled = set_thinking_sound_enabled
|
self._set_thinking_sound_enabled = set_thinking_sound_enabled
|
||||||
|
|
||||||
@@ -300,4 +310,4 @@ class ThinkingSoundEntity(ESPHomeEntity):
|
|||||||
elif isinstance(msg, SubscribeHomeAssistantStatesRequest):
|
elif isinstance(msg, SubscribeHomeAssistantStatesRequest):
|
||||||
# Always return our internal switch state
|
# Always return our internal switch state
|
||||||
self.sync_with_state()
|
self.sync_with_state()
|
||||||
yield SwitchStateResponse(key=self.key, state=self._switch_state)
|
yield SwitchStateResponse(key=self.key, state=self._switch_state)
|
||||||
@@ -12,8 +12,12 @@ if TYPE_CHECKING:
|
|||||||
from pymicro_wakeword import MicroWakeWord
|
from pymicro_wakeword import MicroWakeWord
|
||||||
from pyopen_wakeword import OpenWakeWord
|
from pyopen_wakeword import OpenWakeWord
|
||||||
|
|
||||||
from .entity import ESPHomeEntity, MediaPlayerEntity
|
from .entity import (
|
||||||
from .entity import ESPHomeEntity, MediaPlayerEntity, MuteSwitchEntity, ThinkingSoundEntity
|
ESPHomeEntity,
|
||||||
|
MediaPlayerEntity,
|
||||||
|
MuteSwitchEntity,
|
||||||
|
ThinkingSoundEntity,
|
||||||
|
)
|
||||||
from .mpv_player import MpvMediaPlayer
|
from .mpv_player import MpvMediaPlayer
|
||||||
from .satellite import VoiceSatelliteProtocol
|
from .satellite import VoiceSatelliteProtocol
|
||||||
|
|
||||||
@@ -55,6 +59,7 @@ class Preferences:
|
|||||||
active_wake_words: List[str] = field(default_factory=list)
|
active_wake_words: List[str] = field(default_factory=list)
|
||||||
thinking_sound: int = 0 # 0 = disabled, 1 = enabled
|
thinking_sound: int = 0 # 0 = disabled, 1 = enabled
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ServerState:
|
class ServerState:
|
||||||
name: str
|
name: str
|
||||||
@@ -71,7 +76,7 @@ class ServerState:
|
|||||||
processing_sound: str
|
processing_sound: str
|
||||||
timer_finished_sound: str
|
timer_finished_sound: str
|
||||||
mute_sound: str
|
mute_sound: str
|
||||||
unmute_sound: str
|
unmute_sound: str
|
||||||
preferences: Preferences
|
preferences: Preferences
|
||||||
preferences_path: Path
|
preferences_path: Path
|
||||||
download_dir: Path
|
download_dir: Path
|
||||||
@@ -85,7 +90,7 @@ class ServerState:
|
|||||||
thinking_sound_enabled: bool = False
|
thinking_sound_enabled: bool = False
|
||||||
muted: bool = False
|
muted: bool = False
|
||||||
connected: bool = False
|
connected: bool = False
|
||||||
|
|
||||||
def save_preferences(self) -> None:
|
def save_preferences(self) -> None:
|
||||||
"""Save preferences as JSON."""
|
"""Save preferences as JSON."""
|
||||||
_LOGGER.debug("Saving preferences: %s", self.preferences_path)
|
_LOGGER.debug("Saving preferences: %s", self.preferences_path)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Voice satellite protocol."""
|
"""Voice satellite protocol."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import posixpath
|
import posixpath
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
@@ -14,6 +14,7 @@ from urllib.request import urlopen
|
|||||||
|
|
||||||
# pylint: disable=no-name-in-module
|
# pylint: disable=no-name-in-module
|
||||||
from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined]
|
from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined]
|
||||||
|
AuthenticationRequest,
|
||||||
DeviceInfoRequest,
|
DeviceInfoRequest,
|
||||||
DeviceInfoResponse,
|
DeviceInfoResponse,
|
||||||
ListEntitiesDoneResponse,
|
ListEntitiesDoneResponse,
|
||||||
@@ -32,7 +33,6 @@ from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined]
|
|||||||
VoiceAssistantSetConfiguration,
|
VoiceAssistantSetConfiguration,
|
||||||
VoiceAssistantTimerEventResponse,
|
VoiceAssistantTimerEventResponse,
|
||||||
VoiceAssistantWakeWord,
|
VoiceAssistantWakeWord,
|
||||||
AuthenticationRequest,
|
|
||||||
)
|
)
|
||||||
from aioesphomeapi.core import MESSAGE_TYPE_TO_PROTO
|
from aioesphomeapi.core import MESSAGE_TYPE_TO_PROTO
|
||||||
from aioesphomeapi.model import (
|
from aioesphomeapi.model import (
|
||||||
@@ -53,11 +53,12 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
PROTO_TO_MESSAGE_TYPE = {v: k for k, v in MESSAGE_TYPE_TO_PROTO.items()}
|
PROTO_TO_MESSAGE_TYPE = {v: k for k, v in MESSAGE_TYPE_TO_PROTO.items()}
|
||||||
|
|
||||||
|
|
||||||
class VoiceSatelliteProtocol(APIServer):
|
class VoiceSatelliteProtocol(APIServer):
|
||||||
|
|
||||||
def __init__(self, state: ServerState) -> None:
|
def __init__(self, state: ServerState) -> None:
|
||||||
super().__init__(state.name)
|
super().__init__(state.name)
|
||||||
|
|
||||||
self.state = state
|
self.state = state
|
||||||
self.state.satellite = self
|
self.state.satellite = self
|
||||||
self.state.connected = False
|
self.state.connected = False
|
||||||
@@ -82,7 +83,7 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
self.state.mute_switch_entity = existing_mute_switches[0]
|
self.state.mute_switch_entity = existing_mute_switches[0]
|
||||||
for extra in existing_mute_switches[1:]:
|
for extra in existing_mute_switches[1:]:
|
||||||
self.state.entities.remove(extra)
|
self.state.entities.remove(extra)
|
||||||
|
|
||||||
if self.state.media_player_entity is None:
|
if self.state.media_player_entity is None:
|
||||||
self.state.media_player_entity = MediaPlayerEntity(
|
self.state.media_player_entity = MediaPlayerEntity(
|
||||||
server=self,
|
server=self,
|
||||||
@@ -118,7 +119,7 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
mute_switch.update_get_muted(lambda: self.state.muted)
|
mute_switch.update_get_muted(lambda: self.state.muted)
|
||||||
mute_switch.update_set_muted(self._set_muted)
|
mute_switch.update_set_muted(self._set_muted)
|
||||||
mute_switch.sync_with_state()
|
mute_switch.sync_with_state()
|
||||||
|
|
||||||
existing_thinking_sound_switches = [
|
existing_thinking_sound_switches = [
|
||||||
entity
|
entity
|
||||||
for entity in self.state.entities
|
for entity in self.state.entities
|
||||||
@@ -146,14 +147,22 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
self.state.entities.append(thinking_sound_switch)
|
self.state.entities.append(thinking_sound_switch)
|
||||||
|
|
||||||
# Load thinking sound enabled state from preferences (default to False if not set or unknown)
|
# Load thinking sound enabled state from preferences (default to False if not set or unknown)
|
||||||
if hasattr(self.state.preferences, 'thinking_sound') and self.state.preferences.thinking_sound in (0, 1):
|
if hasattr(
|
||||||
self.state.thinking_sound_enabled = bool(self.state.preferences.thinking_sound)
|
self.state.preferences, "thinking_sound"
|
||||||
|
) and self.state.preferences.thinking_sound in (0, 1):
|
||||||
|
self.state.thinking_sound_enabled = bool(
|
||||||
|
self.state.preferences.thinking_sound
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.state.thinking_sound_enabled = False
|
self.state.thinking_sound_enabled = False
|
||||||
|
|
||||||
thinking_sound_switch.server = self
|
thinking_sound_switch.server = self
|
||||||
thinking_sound_switch.update_get_thinking_sound_enabled(lambda: self.state.thinking_sound_enabled)
|
thinking_sound_switch.update_get_thinking_sound_enabled(
|
||||||
thinking_sound_switch.update_set_thinking_sound_enabled(self._set_thinking_sound_enabled)
|
lambda: self.state.thinking_sound_enabled
|
||||||
|
)
|
||||||
|
thinking_sound_switch.update_set_thinking_sound_enabled(
|
||||||
|
self._set_thinking_sound_enabled
|
||||||
|
)
|
||||||
thinking_sound_switch.sync_with_state()
|
thinking_sound_switch.sync_with_state()
|
||||||
|
|
||||||
self._is_streaming_audio = False
|
self._is_streaming_audio = False
|
||||||
@@ -165,10 +174,12 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
self._pipeline_active = False
|
self._pipeline_active = False
|
||||||
self._external_wake_words: Dict[str, VoiceAssistantExternalWakeWord] = {}
|
self._external_wake_words: Dict[str, VoiceAssistantExternalWakeWord] = {}
|
||||||
self._disconnect_event = asyncio.Event()
|
self._disconnect_event = asyncio.Event()
|
||||||
|
|
||||||
def _set_thinking_sound_enabled(self, new_state: bool) -> None:
|
def _set_thinking_sound_enabled(self, new_state: bool) -> None:
|
||||||
self.state.thinking_sound_enabled = bool(new_state)
|
self.state.thinking_sound_enabled = bool(new_state)
|
||||||
self.state.preferences.thinking_sound = 1 if self.state.thinking_sound_enabled else 0
|
self.state.preferences.thinking_sound = (
|
||||||
|
1 if self.state.thinking_sound_enabled else 0
|
||||||
|
)
|
||||||
|
|
||||||
if self.state.thinking_sound_enabled:
|
if self.state.thinking_sound_enabled:
|
||||||
_LOGGER.debug("Thinking sound enabled")
|
_LOGGER.debug("Thinking sound enabled")
|
||||||
@@ -194,7 +205,7 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
self.state.tts_player.play(self.state.unmute_sound)
|
self.state.tts_player.play(self.state.unmute_sound)
|
||||||
# Resume normal operation - wake word detection will be active again
|
# Resume normal operation - wake word detection will be active again
|
||||||
pass
|
pass
|
||||||
|
|
||||||
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:
|
||||||
@@ -204,7 +215,10 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
self._tts_url = data.get("url")
|
self._tts_url = data.get("url")
|
||||||
self._tts_played = False
|
self._tts_played = False
|
||||||
self._continue_conversation = False
|
self._continue_conversation = False
|
||||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_START and self.state.thinking_sound_enabled:
|
elif (
|
||||||
|
event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_START
|
||||||
|
and self.state.thinking_sound_enabled
|
||||||
|
):
|
||||||
# Play short "thinking/processing" sound if configured
|
# Play short "thinking/processing" sound if configured
|
||||||
processing = getattr(self.state, "processing_sound", None)
|
processing = getattr(self.state, "processing_sound", None)
|
||||||
if processing:
|
if processing:
|
||||||
@@ -212,7 +226,7 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
self.state.stop_word.is_active = True
|
self.state.stop_word.is_active = True
|
||||||
self._processing = True
|
self._processing = True
|
||||||
self.duck()
|
self.duck()
|
||||||
self.state.tts_player.play(self.state.processing_sound)
|
self.state.tts_player.play(self.state.processing_sound)
|
||||||
elif event_type in (
|
elif event_type in (
|
||||||
VoiceAssistantEventType.VOICE_ASSISTANT_STT_VAD_END,
|
VoiceAssistantEventType.VOICE_ASSISTANT_STT_VAD_END,
|
||||||
VoiceAssistantEventType.VOICE_ASSISTANT_STT_END,
|
VoiceAssistantEventType.VOICE_ASSISTANT_STT_END,
|
||||||
@@ -273,24 +287,22 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
self._continue_conversation = msg.start_conversation
|
self._continue_conversation = msg.start_conversation
|
||||||
|
|
||||||
self.duck()
|
self.duck()
|
||||||
yield from self.state.media_player_entity.play(
|
self.state.tts_player.play(urls, done_callback=self._tts_finished)
|
||||||
urls, announcement=True, done_callback=self._tts_finished
|
|
||||||
)
|
|
||||||
elif isinstance(msg, VoiceAssistantTimerEventResponse):
|
elif isinstance(msg, VoiceAssistantTimerEventResponse):
|
||||||
self.handle_timer_event(VoiceAssistantTimerEventType(msg.event_type), msg)
|
self.handle_timer_event(VoiceAssistantTimerEventType(msg.event_type), msg)
|
||||||
elif isinstance(msg, DeviceInfoRequest):
|
elif isinstance(msg, DeviceInfoRequest):
|
||||||
# Compute dynamic device name
|
# Compute dynamic device name
|
||||||
base_name = re.sub(r'[\s-]+', '-', self.state.name.lower()).strip('-')
|
base_name = re.sub(r"[\s-]+", "-", self.state.name.lower()).strip("-")
|
||||||
mac_no_colon = self.state.mac_address.replace(":", "").lower()
|
mac_no_colon = self.state.mac_address.replace(":", "").lower()
|
||||||
mac_last6 = mac_no_colon[-6:]
|
mac_last6 = mac_no_colon[-6:]
|
||||||
device_name = f"{base_name}-{mac_last6}"
|
device_name = f"{base_name}-{mac_last6}"
|
||||||
|
|
||||||
yield DeviceInfoResponse(
|
yield DeviceInfoResponse(
|
||||||
uses_password=False,
|
uses_password=False,
|
||||||
name=device_name,
|
name=device_name,
|
||||||
mac_address=self.state.mac_address,
|
mac_address=self.state.mac_address,
|
||||||
manufacturer="Open Home Foundation",
|
manufacturer="Open Home Foundation",
|
||||||
model="Linux Voice Assistant",
|
model="Linux Voice Assistant",
|
||||||
voice_assistant_feature_flags=(
|
voice_assistant_feature_flags=(
|
||||||
VoiceAssistantFeature.VOICE_ASSISTANT
|
VoiceAssistantFeature.VOICE_ASSISTANT
|
||||||
| VoiceAssistantFeature.API_AUDIO
|
| VoiceAssistantFeature.API_AUDIO
|
||||||
@@ -402,7 +414,7 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
if self.state.muted:
|
if self.state.muted:
|
||||||
# Don't respond to wake words when muted (voice_assistant.stop behavior)
|
# Don't respond to wake words when muted (voice_assistant.stop behavior)
|
||||||
return
|
return
|
||||||
|
|
||||||
wake_word_phrase = wake_word.wake_word
|
wake_word_phrase = wake_word.wake_word
|
||||||
_LOGGER.debug("Detected wake word: %s", wake_word_phrase)
|
_LOGGER.debug("Detected wake word: %s", wake_word_phrase)
|
||||||
self.send_messages(
|
self.send_messages(
|
||||||
@@ -505,10 +517,11 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
# Send states after connect
|
# Send states after connect
|
||||||
states = []
|
states = []
|
||||||
for entity in self.state.entities:
|
for entity in self.state.entities:
|
||||||
states.extend(entity.handle_message(SubscribeHomeAssistantStatesRequest()))
|
states.extend(
|
||||||
|
entity.handle_message(SubscribeHomeAssistantStatesRequest())
|
||||||
|
)
|
||||||
self.send_messages(states)
|
self.send_messages(states)
|
||||||
_LOGGER.debug("Sent entity states after connect")
|
_LOGGER.debug("Sent entity states after connect")
|
||||||
_LOGGER.info("Disconnected from Home Assistant")
|
|
||||||
|
|
||||||
def _download_external_wake_word(
|
def _download_external_wake_word(
|
||||||
self, external_wake_word: VoiceAssistantExternalWakeWord
|
self, external_wake_word: VoiceAssistantExternalWakeWord
|
||||||
@@ -576,15 +589,3 @@ class VoiceSatelliteProtocol(APIServer):
|
|||||||
trained_languages=external_wake_word.trained_languages,
|
trained_languages=external_wake_word.trained_languages,
|
||||||
wake_word_path=config_path,
|
wake_word_path=config_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
def process_packet(self, msg_type: int, packet_data: bytes) -> None:
|
|
||||||
super().process_packet(msg_type, packet_data)
|
|
||||||
|
|
||||||
if msg_type == PROTO_TO_MESSAGE_TYPE[AuthenticationRequest]:
|
|
||||||
self.state.connected = True
|
|
||||||
# Send states after connect
|
|
||||||
states = []
|
|
||||||
for entity in self.state.entities:
|
|
||||||
states.extend(entity.handle_message(SubscribeHomeAssistantStatesRequest()))
|
|
||||||
self.send_messages(states)
|
|
||||||
_LOGGER.debug("Sent entity states after connect")
|
|
||||||
Reference in new issue
Block a user