Compare commits

...

9 Commits

Author SHA1 Message Date
Andre Basche 11da4ebfbc Improve mqtt client 2024-03-29 01:10:27 +01:00
Andre Basche bef55f7abc Fix checks 2024-03-26 00:19:54 +01:00
Andre Basche 79cabfd7b2 Bump version 2024-03-26 00:15:56 +01:00
Andre Basche 1583e6beaa Fix missing update when using same client_id 2024-03-26 00:15:26 +01:00
Andre Basche 33f34e1c20 Bump version 2024-03-25 02:15:25 +01:00
Andre Basche 7e59f76784 subscribe to updates 2024-03-25 02:14:17 +01:00
Andre Basche f108005a4d Support cloud push with wss mqtt 2024-03-18 19:59:38 +01:00
Andre Basche a1347f7a46 Bump version 2024-03-18 01:08:52 +01:00
Vadym 8ef5bd9889 Range.min is always skipped (#23) 2024-03-18 01:06:46 +01:00
8 changed files with 171 additions and 5 deletions
+8
View File
@@ -18,6 +18,7 @@ from pyhon.connection.handler.hon import HonConnectionHandler
_LOGGER = logging.getLogger(__name__)
# pylint: disable=too-many-instance-attributes
class HonAPI:
def __init__(
self,
@@ -191,6 +192,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,
+137
View File
@@ -0,0 +1,137 @@
import asyncio
import json
import logging
import secrets
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 Hon
_LOGGER = logging.getLogger(__name__)
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
@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))
def _on_lifecycle_connection_success(
self,
lifecycle_connect_success_data: mqtt5.LifecycleConnectSuccessData,
) -> None:
self._connection = True
_LOGGER.info(
"Lifecycle Connection Success: %s", str(lifecycle_connect_success_data)
)
def _on_lifecycle_attempting_connect(
self,
lifecycle_attempting_connect_data: mqtt5.LifecycleAttemptingConnectData,
) -> None:
_LOGGER.info(
"Lifecycle Attempting Connect - %s", str(lifecycle_attempting_connect_data)
)
def _on_lifecycle_connection_failure(
self,
lifecycle_connection_failure_data: mqtt5.LifecycleConnectFailureData,
) -> None:
_LOGGER.info(
"Lifecycle Connection Failure - %s", str(lifecycle_connection_failure_data)
)
def _on_lifecycle_disconnection(
self,
lifecycle_disconnect_data: mqtt5.LifecycleDisconnectData,
) -> None:
self._connection = False
_LOGGER.info("Lifecycle Disconnection - %s", str(lifecycle_disconnect_data))
def _on_publish_received(self, 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
appliance = next(
a for a in self._appliances if topic in a.info["topics"]["subscribe"]
)
if topic and "appliancestatus" in topic:
for parameter in payload["parameters"]:
appliance.attributes["parameters"][parameter["parName"]].update(
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)
async def _start(self) -> None:
self._client = mqtt5_client_builder.websockets_with_custom_authorizer(
endpoint=const.AWS_ENDPOINT,
auth_authorizer_name=const.AWS_AUTHORIZER,
auth_authorizer_signature=await self._api.load_aws_token(),
auth_token_key_name="token",
auth_token_value=self._api.auth.id_token,
client_id=f"{const.MOBILE_ID}_{secrets.token_hex(8)}",
on_lifecycle_stopped=self._on_lifecycle_stopped,
on_lifecycle_connection_success=self._on_lifecycle_connection_success,
on_lifecycle_attempting_connect=self._on_lifecycle_attempting_connect,
on_lifecycle_connection_failure=self._on_lifecycle_connection_failure,
on_lifecycle_disconnection=self._on_lifecycle_disconnection,
on_publish_received=self._on_publish_received,
)
self.client.start()
def _subscribe_appliances(self) -> None:
for appliance in self._appliances:
self._subscribe(appliance)
def _subscribe(self, appliance: HonAppliance) -> None:
for topic in appliance.info.get("topics", {}).get("subscribe", []):
self.client.subscribe(
mqtt5.SubscribePacket([mqtt5.Subscription(topic)])
).result(10)
_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 _watchdog(self) -> None:
while True:
await asyncio.sleep(5)
if not self._connection:
_LOGGER.info("Restart mqtt connection")
await self._start()
self._subscribe_appliances()
+2
View File
@@ -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."
+13 -1
View File
@@ -2,7 +2,7 @@ import asyncio
import logging
from pathlib import Path
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 typing_extensions import Self
@@ -10,6 +10,7 @@ from typing_extensions import Self
from pyhon.appliance import HonAppliance
from pyhon.connection.api import HonAPI
from pyhon.connection.api import TestAPI
from pyhon.connection.mqtt import MQTTClient
from pyhon.exceptions import NoAuthenticationException
_LOGGER = logging.getLogger(__name__)
@@ -33,6 +34,8 @@ class Hon:
self._test_data_path: Path = test_data_path or Path().cwd()
self._mobile_id: str = mobile_id
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:
return await self.create()
@@ -120,6 +123,15 @@ class Hon:
api = TestAPI(test_data)
for appliance in await api.load_appliances():
await self._create_appliance(appliance, api)
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:
await self.api.close()
+2 -2
View File
@@ -71,7 +71,7 @@ class HonParameterRange(HonParameter):
def values(self) -> List[str]:
result = []
i = self.min
while i < self.max:
i += self.step
while i <= self.max:
result.append(str(i))
i += self.step
return result
+1
View File
@@ -1,3 +1,4 @@
aiohttp>=3.8.6
yarl>=1.8
typing-extensions>=4.8
awsiotsdk>=1.21.0
+1
View File
@@ -3,3 +3,4 @@ flake8>=6.0
mypy>=0.991
pylint>=2.15
setuptools>=62.3
types-awscrt
+7 -2
View File
@@ -7,7 +7,7 @@ with open("README.md", "r", encoding="utf-8") as f:
setup(
name="pyhOn",
version="0.16.0",
version="0.17.2",
author="Andre Basche",
description="Control hOn devices with python",
long_description=long_description,
@@ -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",