Improve mqtt client

This commit is contained in:
Andre Basche 2024-03-29 01:10:27 +01:00
parent bef55f7abc
commit 11da4ebfbc
5 changed files with 125 additions and 93 deletions

View file

@ -3,7 +3,7 @@ import logging
import re import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any, TYPE_CHECKING, List, TypeVar, overload, Callable from typing import Optional, Dict, Any, TYPE_CHECKING, List, TypeVar, overload
from pyhon import diagnose, exceptions from pyhon import diagnose, exceptions
from pyhon.appliances.base import ApplianceBase from pyhon.appliances.base import ApplianceBase
@ -43,7 +43,6 @@ class HonAppliance:
self._additional_data: Dict[str, Any] = {} self._additional_data: Dict[str, Any] = {}
self._last_update: Optional[datetime] = None self._last_update: Optional[datetime] = None
self._default_setting = HonParameter("", {}, "") self._default_setting = HonParameter("", {}, "")
self._notify_function: Optional[Callable[[Any], None]] = None
try: try:
self._extra: Optional[ApplianceBase] = importlib.import_module( self._extra: Optional[ApplianceBase] = importlib.import_module(
@ -313,11 +312,3 @@ class HonAppliance:
elif isinstance(target, HonParameterEnum): elif isinstance(target, HonParameterEnum):
target.values = main.values target.values = main.values
target.value = main.value target.value = main.value
def subscribe(self, notify_function: Callable[[Any], None]) -> None:
self._notify_function = notify_function
def notify(self) -> None:
self.sync_params_to_command("settings")
if self._notify_function:
self._notify_function(self.attributes)

View file

@ -7,12 +7,10 @@ from types import TracebackType
from typing import Dict, Optional, Any, List, no_type_check, Type from typing import Dict, Optional, Any, List, no_type_check, Type
from aiohttp import ClientSession from aiohttp import ClientSession
from awscrt import mqtt5
from typing_extensions import Self from typing_extensions import Self
from pyhon import const, exceptions from pyhon import const, exceptions
from pyhon.appliance import HonAppliance from pyhon.appliance import HonAppliance
from pyhon.connection import mqtt
from pyhon.connection.auth import HonAuth from pyhon.connection.auth import HonAuth
from pyhon.connection.handler.anonym import HonAnonymousConnectionHandler from pyhon.connection.handler.anonym import HonAnonymousConnectionHandler
from pyhon.connection.handler.hon import HonConnectionHandler from pyhon.connection.handler.hon import HonConnectionHandler
@ -40,7 +38,6 @@ class HonAPI:
self._hon_handler: Optional[HonConnectionHandler] = None self._hon_handler: Optional[HonConnectionHandler] = None
self._hon_anonymous_handler: Optional[HonAnonymousConnectionHandler] = None self._hon_anonymous_handler: Optional[HonAnonymousConnectionHandler] = None
self._session: Optional[ClientSession] = session self._session: Optional[ClientSession] = session
self._mqtt_client: mqtt5.Client | None = None
async def __aenter__(self) -> Self: async def __aenter__(self) -> Self:
return await self.create() return await self.create()
@ -269,10 +266,6 @@ class HonAPI:
result: Dict[str, Any] = await response.json() result: Dict[str, Any] = await response.json()
return result return result
async def subscribe_mqtt(self, appliances: list[HonAppliance]) -> None:
if not self._mqtt_client:
self._mqtt_client = await mqtt.start(self, appliances)
async def close(self) -> None: async def close(self) -> None:
if self._hon_handler is not None: if self._hon_handler is not None:
await self._hon_handler.close() await self._hon_handler.close()

View file

@ -1,3 +1,4 @@
import asyncio
import json import json
import logging import logging
import secrets import secrets
@ -10,91 +11,127 @@ from pyhon import const
from pyhon.appliance import HonAppliance from pyhon.appliance import HonAppliance
if TYPE_CHECKING: if TYPE_CHECKING:
from pyhon import HonAPI from pyhon import Hon
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
appliances: list[HonAppliance] = []
class MQTTClient:
def __init__(self, hon: "Hon"):
self._client: mqtt5.Client | None = None
self._hon = hon
self._api = hon.api
self._appliances = hon.appliances
self._connection = False
self._watchdog_task: asyncio.Task[None] | None = None
def on_lifecycle_stopped(lifecycle_stopped_data: mqtt5.LifecycleStoppedData) -> None: @property
def client(self) -> mqtt5.Client:
if self._client is not None:
return self._client
raise AttributeError("Client is not set")
async def create(self) -> "MQTTClient":
await self._start()
self._subscribe_appliances()
return self
def _on_lifecycle_stopped(
self, lifecycle_stopped_data: mqtt5.LifecycleStoppedData
) -> None:
_LOGGER.info("Lifecycle Stopped: %s", str(lifecycle_stopped_data)) _LOGGER.info("Lifecycle Stopped: %s", str(lifecycle_stopped_data))
def _on_lifecycle_connection_success(
def on_lifecycle_connection_success( self,
lifecycle_connect_success_data: mqtt5.LifecycleConnectSuccessData, lifecycle_connect_success_data: mqtt5.LifecycleConnectSuccessData,
) -> None: ) -> None:
self._connection = True
_LOGGER.info( _LOGGER.info(
"Lifecycle Connection Success: %s", str(lifecycle_connect_success_data) "Lifecycle Connection Success: %s", str(lifecycle_connect_success_data)
) )
def _on_lifecycle_attempting_connect(
def on_lifecycle_attempting_connect( self,
lifecycle_attempting_connect_data: mqtt5.LifecycleAttemptingConnectData, lifecycle_attempting_connect_data: mqtt5.LifecycleAttemptingConnectData,
) -> None: ) -> None:
_LOGGER.info( _LOGGER.info(
"Lifecycle Attempting Connect - %s", str(lifecycle_attempting_connect_data) "Lifecycle Attempting Connect - %s", str(lifecycle_attempting_connect_data)
) )
def _on_lifecycle_connection_failure(
def on_lifecycle_connection_failure( self,
lifecycle_connection_failure_data: mqtt5.LifecycleConnectFailureData, lifecycle_connection_failure_data: mqtt5.LifecycleConnectFailureData,
) -> None: ) -> None:
_LOGGER.info( _LOGGER.info(
"Lifecycle Connection Failure - %s", str(lifecycle_connection_failure_data) "Lifecycle Connection Failure - %s", str(lifecycle_connection_failure_data)
) )
def _on_lifecycle_disconnection(
def on_lifecycle_disconnection( self,
lifecycle_disconnect_data: mqtt5.LifecycleDisconnectData, lifecycle_disconnect_data: mqtt5.LifecycleDisconnectData,
) -> None: ) -> None:
self._connection = False
_LOGGER.info("Lifecycle Disconnection - %s", str(lifecycle_disconnect_data)) _LOGGER.info("Lifecycle Disconnection - %s", str(lifecycle_disconnect_data))
def _on_publish_received(self, data: mqtt5.PublishReceivedData) -> None:
def on_publish_received(data: mqtt5.PublishReceivedData) -> None:
if not (data and data.publish_packet and data.publish_packet.payload): if not (data and data.publish_packet and data.publish_packet.payload):
return return
payload = json.loads(data.publish_packet.payload.decode()) payload = json.loads(data.publish_packet.payload.decode())
topic = data.publish_packet.topic topic = data.publish_packet.topic
if topic and "appliancestatus" in topic:
appliance = next( appliance = next(
a for a in appliances if topic in a.info["topics"]["subscribe"] a for a in self._appliances if topic in a.info["topics"]["subscribe"]
) )
if topic and "appliancestatus" in topic:
for parameter in payload["parameters"]: for parameter in payload["parameters"]:
appliance.attributes["parameters"][parameter["parName"]].update(parameter) appliance.attributes["parameters"][parameter["parName"]].update(
appliance.notify() parameter
)
appliance.sync_params_to_command("settings")
self._hon.notify()
elif topic and "connected" in topic:
_LOGGER.info("Connected %s", appliance.nick_name)
elif topic and "disconnected" in topic:
_LOGGER.info("Disconnected %s", appliance.nick_name)
elif topic and "discovery" in topic:
_LOGGER.info("Discovered %s", appliance.nick_name)
_LOGGER.info("%s - %s", topic, payload) _LOGGER.info("%s - %s", topic, payload)
async def _start(self) -> None:
async def create_mqtt_client(api: "HonAPI") -> mqtt5.Client: self._client = mqtt5_client_builder.websockets_with_custom_authorizer(
client: mqtt5.Client = mqtt5_client_builder.websockets_with_custom_authorizer(
endpoint=const.AWS_ENDPOINT, endpoint=const.AWS_ENDPOINT,
auth_authorizer_name=const.AWS_AUTHORIZER, auth_authorizer_name=const.AWS_AUTHORIZER,
auth_authorizer_signature=await api.load_aws_token(), auth_authorizer_signature=await self._api.load_aws_token(),
auth_token_key_name="token", auth_token_key_name="token",
auth_token_value=api.auth.id_token, auth_token_value=self._api.auth.id_token,
client_id=f"{const.MOBILE_ID}_{secrets.token_hex(8)}", client_id=f"{const.MOBILE_ID}_{secrets.token_hex(8)}",
on_lifecycle_stopped=on_lifecycle_stopped, on_lifecycle_stopped=self._on_lifecycle_stopped,
on_lifecycle_connection_success=on_lifecycle_connection_success, on_lifecycle_connection_success=self._on_lifecycle_connection_success,
on_lifecycle_attempting_connect=on_lifecycle_attempting_connect, on_lifecycle_attempting_connect=self._on_lifecycle_attempting_connect,
on_lifecycle_connection_failure=on_lifecycle_connection_failure, on_lifecycle_connection_failure=self._on_lifecycle_connection_failure,
on_lifecycle_disconnection=on_lifecycle_disconnection, on_lifecycle_disconnection=self._on_lifecycle_disconnection,
on_publish_received=on_publish_received, on_publish_received=self._on_publish_received,
) )
client.start() self.client.start()
return client
def _subscribe_appliances(self) -> None:
for appliance in self._appliances:
self._subscribe(appliance)
def subscribe(client: mqtt5.Client, appliance: HonAppliance) -> None: def _subscribe(self, appliance: HonAppliance) -> None:
for topic in appliance.info.get("topics", {}).get("subscribe", []): for topic in appliance.info.get("topics", {}).get("subscribe", []):
client.subscribe(mqtt5.SubscribePacket([mqtt5.Subscription(topic)])).result(10) self.client.subscribe(
mqtt5.SubscribePacket([mqtt5.Subscription(topic)])
).result(10)
_LOGGER.info("Subscribed to topic %s", topic) _LOGGER.info("Subscribed to topic %s", topic)
async def start_watchdog(self) -> None:
if not self._watchdog_task or self._watchdog_task.done():
await asyncio.create_task(self._watchdog())
async def start(api: "HonAPI", app: list[HonAppliance]) -> mqtt5.Client: async def _watchdog(self) -> None:
client = await create_mqtt_client(api) while True:
global appliances # pylint: disable=global-statement await asyncio.sleep(5)
appliances = app if not self._connection:
for appliance in appliances: _LOGGER.info("Restart mqtt connection")
subscribe(client, appliance) await self._start()
return client self._subscribe_appliances()

View file

@ -2,7 +2,7 @@ import asyncio
import logging import logging
from pathlib import Path from pathlib import Path
from types import TracebackType from types import TracebackType
from typing import List, Optional, Dict, Any, Type from typing import List, Optional, Dict, Any, Type, Callable
from aiohttp import ClientSession from aiohttp import ClientSession
from typing_extensions import Self from typing_extensions import Self
@ -10,6 +10,7 @@ from typing_extensions import Self
from pyhon.appliance import HonAppliance from pyhon.appliance import HonAppliance
from pyhon.connection.api import HonAPI from pyhon.connection.api import HonAPI
from pyhon.connection.api import TestAPI from pyhon.connection.api import TestAPI
from pyhon.connection.mqtt import MQTTClient
from pyhon.exceptions import NoAuthenticationException from pyhon.exceptions import NoAuthenticationException
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -33,6 +34,8 @@ class Hon:
self._test_data_path: Path = test_data_path or Path().cwd() self._test_data_path: Path = test_data_path or Path().cwd()
self._mobile_id: str = mobile_id self._mobile_id: str = mobile_id
self._refresh_token: str = refresh_token self._refresh_token: str = refresh_token
self._mqtt_client: MQTTClient | None = None
self._notify_function: Optional[Callable[[Any], None]] = None
async def __aenter__(self) -> Self: async def __aenter__(self) -> Self:
return await self.create() return await self.create()
@ -120,7 +123,15 @@ class Hon:
api = TestAPI(test_data) api = TestAPI(test_data)
for appliance in await api.load_appliances(): for appliance in await api.load_appliances():
await self._create_appliance(appliance, api) await self._create_appliance(appliance, api)
await self.api.subscribe_mqtt(self.appliances) if not self._mqtt_client:
self._mqtt_client = await MQTTClient(self).create()
def subscribe_updates(self, notify_function: Callable[[Any], None]) -> None:
self._notify_function = notify_function
def notify(self) -> None:
if self._notify_function:
self._notify_function(None)
async def close(self) -> None: async def close(self) -> None:
await self.api.close() await self.api.close()

View file

@ -7,7 +7,7 @@ with open("README.md", "r", encoding="utf-8") as f:
setup( setup(
name="pyhOn", name="pyhOn",
version="0.17.1", version="0.17.2",
author="Andre Basche", author="Andre Basche",
description="Control hOn devices with python", description="Control hOn devices with python",
long_description=long_description, long_description=long_description,