Merge branch 'main' into issue_128

This commit is contained in:
Omar Amin authored and GitHub committed 2026-02-10 20:39:06 +02:00
commit 72dfc6db35
5 files changed
+95 -67

No files matched your search

+25 -13
View File
@@ -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")
+4 -4
View File
@@ -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:
+20 -10
View File
@@ -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)
+9 -4
View File
@@ -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)
+37 -36
View File
@@ -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")