"""YoLink mqtt client.""" import asyncio import logging from typing import Any from aiomqtt import Client, MqttError, ProtocolVersion from pydantic import ValidationError from .auth_mgr import YoLinkAuthMgr from .device import YoLinkDevice from .message_listener import MessageListener from .model import BRDP from .message_resolver import resolve_sub_message from .endpoint import Endpoint from .local_auth_mgr import YoLinkLocalAuthMgr _LOGGER = logging.getLogger(__name__) class YoLinkMqttClient: """YoLink mqtt client.""" def __init__( self, auth_manager: YoLinkAuthMgr, endpoint: Endpoint, broker_host: str, broker_port: int, devices: dict[str, YoLinkDevice], ) -> None: self._auth_mgr = auth_manager self._endpoint = endpoint self._broker_host = broker_host self._broker_port = broker_port self._topic = None self._devices = devices self._message_listener = None self._running = False self._listener_task = None async def connect(self, topic: str, listener: MessageListener) -> None: """Connect to yolink mqtt broker.""" self._topic = topic self._message_listener = listener self._listener_task = asyncio.create_task(self._listen()) async def _check_and_refresh_token(self) -> tuple[str, str]: """Check and refresh token.""" new_token = await self._auth_mgr.check_and_refresh_token() if isinstance(self._auth_mgr, YoLinkLocalAuthMgr): return self._auth_mgr._client_id, new_token else: return new_token, "" async def _listen(self): """Listen to yolink mqtt broker.""" reconnect_interval = 30 self._running = True while self._running: try: username, password = await self._check_and_refresh_token() async with Client( hostname=self._broker_host, port=self._broker_port, username=username, password=password, keepalive=60, protocol=ProtocolVersion.V311, ) as client: _LOGGER.info( "[%s] connecting to yolink mqtt broker.", self._endpoint.name ) if self._topic is not None: await client.subscribe(self._topic) _LOGGER.info( "[%s] yolink mqtt client connected.", self._endpoint.name ) async for message in client.messages: self._process_message(message) except MqttError: _LOGGER.error( "[%s] yolink mqtt client disconnected!", self._endpoint.name, exc_info=True, ) await asyncio.sleep(reconnect_interval) except Exception: _LOGGER.error( "[%s] unexcept exception:", self._endpoint.name, exc_info=True ) await asyncio.sleep(reconnect_interval) async def disconnect(self) -> None: """UnRegister listener""" if self._listener_task is None: return self._listener_task.cancel() self._listener_task = None self._running = False def _is_message_acceptable(self, eventType: str | None) -> bool: """Check if the message is acceptable.""" if eventType is None: return False if eventType == "THSensor.DataRecord": return True eventTypeList = [ "Report", "Alert", "StatusChange", "getState", "setState", "DevEvent", "waterReport", # Sprinkler ] for event in eventTypeList: if eventType.endswith(event): return True return False def _process_message(self, msg) -> None: """Mqtt on message.""" _LOGGER.debug( "Received message on %s%s: %s", msg.topic, " (retained)" if msg.retain else "", msg.payload[0:8192], ) keys = str(msg.topic).split("/") if len(keys) == 4 and keys[3] == "report": try: device_id = keys[2] msg_data = BRDP.parse_raw(msg.payload.decode("UTF-8")) if not self._is_message_acceptable(msg_data.event): return msg_event = msg_data.event.split(".") msg_type = msg_event[len(msg_event) - 1] device = self._devices.get(device_id) if device is None: return paired_device_id = device.get_paired_device_id() if paired_device_id is not None: paired_device = self._devices.get(paired_device_id) if paired_device is None: return # post current device state to paired device paired_device_state = {"state": msg_data.data.get("state")} self.__resolve_message(paired_device, paired_device_state, msg_type) self.__resolve_message(device, msg_data.data, msg_type) except ValidationError: # ignore invalidate message _LOGGER.debug("Message invalidate.") def __resolve_message( self, device: YoLinkDevice, msg_data: dict[str, Any], msg_type: str ) -> None: """Resolve device message.""" resolve_sub_message(device, msg_data, msg_type) if self._message_listener is not None: self._message_listener.on_message(device, msg_data)