Remove awsiotsdk dependency

This commit is contained in:
Kuba Sawulski 2024-08-09 15:05:02 +02:00
parent 63752e13d6
commit f3496c59c9
5 changed files with 60 additions and 92 deletions

View file

@ -6,7 +6,7 @@ import urllib
from contextlib import suppress from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, Optional, Any, List from typing import Any, Dict, List, Optional
from urllib import parse from urllib import parse
from urllib.parse import quote from urllib.parse import quote
@ -120,18 +120,25 @@ class HonAuth:
async with self._request.get(url) as response: async with self._request.get(url) as response:
text = await response.text() text = await response.text()
self._expires = datetime.utcnow() self._expires = datetime.utcnow()
login_url: List[str] = re.findall("url = '(.+?)'", text) login_url: List[str] = re.findall("(?:url|href) ?= ?'(.+?)'", text)
if not login_url: if not login_url:
if "oauth/done#access_token=" in text: if "oauth/done#access_token=" in text:
self._parse_token_data(text) self._parse_token_data(text)
raise exceptions.HonNoAuthenticationNeeded() raise exceptions.HonNoAuthenticationNeeded()
await self._error_logger(response) await self._error_logger(response)
# As of July 2024 the login page has changed,
# and we started getting a /NewhOnLogin based relative URL in JS to parse
if login_url[0].startswith("/NewhOnLogin"):
# Force use of the old login page to avoid having
# to make the new one work..
login_url[0] = f"{const.AUTH_API}/s/login{login_url[0]}"
return login_url[0] return login_url[0]
async def _manual_redirect(self, url: str) -> str: async def _manual_redirect(self, url: str) -> str:
async with self._request.get(url, allow_redirects=False) as response: async with self._request.get(url, allow_redirects=False) as response:
if not (new_location := response.headers.get("Location", "")): new_location = response.headers.get("Location", "")
await self._error_logger(response) if not new_location:
return url
return new_location return new_location
async def _handle_redirects(self, login_url: str) -> str: async def _handle_redirects(self, login_url: str) -> str:

View file

@ -1,33 +1,32 @@
import asyncio
import json import json
import logging import logging
import secrets import secrets
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from urllib.parse import urlencode
from awscrt import mqtt5 from paho.mqtt.client import Client, MQTTv5
from awsiot import mqtt5_client_builder # type: ignore[import-untyped]
from pyhon import const from pyhon import const
from pyhon.appliance import HonAppliance
if TYPE_CHECKING: if TYPE_CHECKING:
from paho.mqtt.client import MQTTMessage, _UserData
from pyhon import Hon from pyhon import Hon
from pyhon.appliance import HonAppliance
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class MQTTClient: class MQTTClient:
def __init__(self, hon: "Hon", mobile_id: str) -> None: def __init__(self, hon: "Hon", mobile_id: str) -> None:
self._client: mqtt5.Client | None = None self._client: Client | None = None
self._hon = hon self._hon = hon
self._mobile_id = mobile_id or const.MOBILE_ID self._mobile_id = mobile_id or const.MOBILE_ID
self._api = hon.api self._api = hon.api
self._appliances = hon.appliances self._appliances = hon.appliances
self._connection = False
self._watchdog_task: asyncio.Task[None] | None = None
@property @property
def client(self) -> mqtt5.Client: def client(self) -> Client:
if self._client is not None: if self._client is not None:
return self._client return self._client
raise AttributeError("Client is not set") raise AttributeError("Client is not set")
@ -35,112 +34,74 @@ class MQTTClient:
async def create(self) -> "MQTTClient": async def create(self) -> "MQTTClient":
await self._start() await self._start()
self._subscribe_appliances() self._subscribe_appliances()
await self.start_watchdog()
return self return self
def _on_lifecycle_stopped( def _on_message(
self, lifecycle_stopped_data: mqtt5.LifecycleStoppedData
) -> None:
_LOGGER.info("Lifecycle Stopped: %s", str(lifecycle_stopped_data))
def _on_lifecycle_connection_success(
self, self,
lifecycle_connect_success_data: mqtt5.LifecycleConnectSuccessData, client: Client, # pylint: disable=unused-argument
userdata: "_UserData", # pylint: disable=unused-argument
message: "MQTTMessage",
) -> None: ) -> None:
self._connection = True if not message.payload or not message.topic:
_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:
self._connection = False
_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 return
payload = json.loads(data.publish_packet.payload.decode())
topic = data.publish_packet.topic payload = json.loads(message.payload)
topic = message.topic
appliance = next( appliance = next(
a for a in self._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:
topic_parts = topic.split("/")
if "appliancestatus" in topic_parts:
for parameter in payload["parameters"]: for parameter in payload["parameters"]:
appliance.attributes["parameters"][parameter["parName"]].update( appliance.attributes["parameters"][parameter["parName"]].update(
parameter parameter
) )
appliance.sync_params_to_command("settings") appliance.sync_params_to_command("settings")
elif topic and "disconnected" in topic: elif "disconnected" in topic_parts:
_LOGGER.info( _LOGGER.info(
"Disconnected %s: %s", "Disconnected %s: %s",
appliance.nick_name, appliance.nick_name,
payload.get("disconnectReason"), payload.get("disconnectReason"),
) )
appliance.connection = False appliance.connection = False
elif topic and "connected" in topic: elif "connected" in topic_parts:
appliance.connection = True appliance.connection = True
_LOGGER.info("Connected %s", appliance.nick_name) _LOGGER.info("Connected %s", appliance.nick_name)
elif topic and "discovery" in topic: elif "discovery" in topic_parts:
_LOGGER.info("Discovered %s", appliance.nick_name) _LOGGER.info("Discovered %s", appliance.nick_name)
self._hon.notify() self._hon.notify()
_LOGGER.info("%s - %s", topic, payload)
async def _start(self) -> None: async def _start(self) -> None:
self._client = mqtt5_client_builder.websockets_with_custom_authorizer( self._client = Client(
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"{self._mobile_id}_{secrets.token_hex(8)}", client_id=f"{self._mobile_id}_{secrets.token_hex(8)}",
on_lifecycle_stopped=self._on_lifecycle_stopped, protocol=MQTTv5,
on_lifecycle_connection_success=self._on_lifecycle_connection_success, reconnect_on_failure=True,
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()
self._client.on_message = self._on_message
self._client.enable_logger(_LOGGER)
query_params = urlencode(
{
"x-amz-customauthorizer-name": const.AWS_AUTHORIZER,
"x-amz-customauthorizer-signature": await self._api.load_aws_token(),
"token": self._api.auth.id_token,
}
)
self._client.username_pw_set(f"?{query_params}")
self._client.connect_async(const.AWS_ENDPOINT, 443)
self._client.loop_start()
def _subscribe_appliances(self) -> None: def _subscribe_appliances(self) -> None:
for appliance in self._appliances: for appliance in self._appliances:
self._subscribe(appliance) self._subscribe(appliance)
def _subscribe(self, 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", []):
self.client.subscribe( if self._client:
mqtt5.SubscribePacket([mqtt5.Subscription(topic)]) self._client.subscribe(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():
self._watchdog_task = 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()

View file

@ -1,4 +1,4 @@
aiohttp>=3.8.6 aiohttp>=3.8.6
yarl>=1.8 yarl>=1.8
typing-extensions>=4.8 typing-extensions>=4.8
awsiotsdk>=1.21.0 paho-mqtt==1.6.1

View file

@ -3,4 +3,4 @@ flake8>=6.0
mypy>=0.991 mypy>=0.991
pylint>=2.15 pylint>=2.15
setuptools>=62.3 setuptools>=62.3
types-awscrt types-paho-mqtt

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.4", version="0.17.5",
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,
@ -25,7 +25,7 @@ setup(
"aiohttp>=3.8.6", "aiohttp>=3.8.6",
"typing-extensions>=4.8", "typing-extensions>=4.8",
"yarl>=1.8", "yarl>=1.8",
"awsiotsdk>=1.21.0", "paho-mqtt==1.6.1",
], ],
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",