From f108005a4dfc2a4e3328c5d3a9bf195b3c3a35be Mon Sep 17 00:00:00 2001 From: Andre Basche Date: Sat, 17 Feb 2024 04:07:28 +0100 Subject: [PATCH] Support cloud push with wss mqtt --- pyhon/connection/api.py | 15 ++++++++ pyhon/connection/mqtt.py | 76 ++++++++++++++++++++++++++++++++++++++++ pyhon/const.py | 2 ++ pyhon/hon.py | 1 + requirements.txt | 1 + requirements_dev.txt | 1 + setup.py | 7 +++- 7 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 pyhon/connection/mqtt.py diff --git a/pyhon/connection/api.py b/pyhon/connection/api.py index 30a9cd2..f99fd5d 100644 --- a/pyhon/connection/api.py +++ b/pyhon/connection/api.py @@ -7,10 +7,12 @@ from types import TracebackType from typing import Dict, Optional, Any, List, no_type_check, Type from aiohttp import ClientSession +from awscrt import mqtt5 from typing_extensions import Self from pyhon import const, exceptions from pyhon.appliance import HonAppliance +from pyhon.connection import mqtt from pyhon.connection.auth import HonAuth from pyhon.connection.handler.anonym import HonAnonymousConnectionHandler from pyhon.connection.handler.hon import HonConnectionHandler @@ -18,6 +20,7 @@ from pyhon.connection.handler.hon import HonConnectionHandler _LOGGER = logging.getLogger(__name__) +# pylint: disable=too-many-instance-attributes class HonAPI: def __init__( self, @@ -37,6 +40,7 @@ class HonAPI: self._hon_handler: Optional[HonConnectionHandler] = None self._hon_anonymous_handler: Optional[HonAnonymousConnectionHandler] = None self._session: Optional[ClientSession] = session + self._mqtt_client: mqtt5.Client | None = None async def __aenter__(self) -> Self: return await self.create() @@ -191,6 +195,13 @@ class HonAPI: maintenance: Dict[str, Any] = (await response.json()).get("payload", {}) return maintenance + async def load_aws_token(self) -> str: + url: str = f"{const.API_URL}/auth/v1/introspection" + async with self._hon.get(url) as response: + introspection: Dict[str, Any] = (await response.json()).get("payload", {}) + result: str = introspection.get("tokenSigned", "") + return result + async def send_command( self, appliance: HonAppliance, @@ -258,6 +269,10 @@ class HonAPI: result: Dict[str, Any] = await response.json() 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: if self._hon_handler is not None: await self._hon_handler.close() diff --git a/pyhon/connection/mqtt.py b/pyhon/connection/mqtt.py new file mode 100644 index 0000000..389a5aa --- /dev/null +++ b/pyhon/connection/mqtt.py @@ -0,0 +1,76 @@ +import json +import logging +from typing import TYPE_CHECKING + +from awscrt import mqtt5 +from awsiot import mqtt5_client_builder # type: ignore[import-untyped] + +from pyhon import const +from pyhon.appliance import HonAppliance + +if TYPE_CHECKING: + from pyhon import HonAPI + +_LOGGER = logging.getLogger(__name__) + +appliances: list[HonAppliance] = [] + + +def on_lifecycle_stopped(lifecycle_stopped_data: mqtt5.LifecycleStoppedData) -> None: + print("Lifecycle Stopped") + print(lifecycle_stopped_data) + + +def on_lifecycle_connection_success( + lifecycle_connect_success_data: mqtt5.LifecycleConnectSuccessData, +) -> None: + print("Lifecycle Connection Success") + print(lifecycle_connect_success_data.connack_packet) + print(lifecycle_connect_success_data.negotiated_settings) + + +def on_publish_received(data: mqtt5.PublishReceivedData) -> None: + if not (data and data.publish_packet and data.publish_packet.payload): + return + payload = json.loads(data.publish_packet.payload.decode()) + topic = data.publish_packet.topic + if topic and "appliancestatus" in topic: + appliance = next( + a for a in appliances if topic in a.info["topics"]["subscribe"] + ) + for parameter in payload["parameters"]: + appliance.attributes["parameters"][parameter["parName"]].update(parameter) + print(parameter) + else: + print(topic, payload) + + +async def create_mqtt_client(api: "HonAPI") -> mqtt5.Client: + client: mqtt5.Client = mqtt5_client_builder.websockets_with_custom_authorizer( + endpoint=const.AWS_ENDPOINT, + auth_authorizer_name=const.AWS_AUTHORIZER, + auth_authorizer_signature=await api.load_aws_token(), + auth_token_key_name="token", + auth_token_value=api.auth.id_token, + client_id=const.MOBILE_ID, + on_lifecycle_stopped=on_lifecycle_stopped, + on_lifecycle_connection_success=on_lifecycle_connection_success, + on_publish_received=on_publish_received, + ) + client.start() + return client + + +def subscribe(client: mqtt5.Client, appliance: HonAppliance) -> None: + for topic in appliance.info.get("topics", {}).get("subscribe", []): + client.subscribe(mqtt5.SubscribePacket([mqtt5.Subscription(topic)])).result(10) + _LOGGER.error("Subscribed to topic %s", topic) + + +async def start(api: "HonAPI", app: list[HonAppliance]) -> mqtt5.Client: + client = await create_mqtt_client(api) + global appliances # pylint: disable=global-statement + appliances = app + for appliance in appliances: + subscribe(client, appliance) + return client diff --git a/pyhon/const.py b/pyhon/const.py index 0bc1a02..a63da63 100644 --- a/pyhon/const.py +++ b/pyhon/const.py @@ -1,6 +1,8 @@ AUTH_API = "https://account2.hon-smarthome.com" API_URL = "https://api-iot.he.services" API_KEY = "GRCqFhC6Gk@ikWXm1RmnSmX1cm,MxY-configuration" +AWS_ENDPOINT = "a30f6tqw0oh1x0-ats.iot.eu-west-1.amazonaws.com" +AWS_AUTHORIZER = "candy-iot-authorizer" APP = "hon" CLIENT_ID = ( "3MVG9QDx8IX8nP5T2Ha8ofvlmjLZl5L_gvfbT9." diff --git a/pyhon/hon.py b/pyhon/hon.py index 7f53fb4..acdd92c 100644 --- a/pyhon/hon.py +++ b/pyhon/hon.py @@ -120,6 +120,7 @@ class Hon: api = TestAPI(test_data) for appliance in await api.load_appliances(): await self._create_appliance(appliance, api) + await self.api.subscribe_mqtt(self.appliances) async def close(self) -> None: await self.api.close() diff --git a/requirements.txt b/requirements.txt index 81e74ff..6d63be7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ aiohttp>=3.8.6 yarl>=1.8 typing-extensions>=4.8 +awsiotsdk>=1.21.0 \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index 153a20d..277568c 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,3 +3,4 @@ flake8>=6.0 mypy>=0.991 pylint>=2.15 setuptools>=62.3 +types-awscrt \ No newline at end of file diff --git a/setup.py b/setup.py index 6d19182..e57e57d 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,12 @@ setup( packages=find_packages(), include_package_data=True, python_requires=">=3.10", - install_requires=["aiohttp>=3.8.6", "typing-extensions>=4.8", "yarl>=1.8"], + install_requires=[ + "aiohttp>=3.8.6", + "typing-extensions>=4.8", + "yarl>=1.8", + "awsiotsdk>=1.21.0", + ], classifiers=[ "Development Status :: 4 - Beta", "Environment :: Console",