From 36fad84ee206ff7ed12daff7e05a684efa8295a7 Mon Sep 17 00:00:00 2001 From: Andre Basche Date: Sun, 9 Apr 2023 20:50:28 +0200 Subject: [PATCH] Refactor api access --- pyhon/__init__.py | 1 + pyhon/__main__.py | 6 +- pyhon/appliance.py | 40 ++++---- pyhon/connection/api.py | 93 +++++++------------ pyhon/connection/auth.py | 2 - .../connection/{connection.py => handler.py} | 14 ++- pyhon/hon.py | 36 +++++++ 7 files changed, 107 insertions(+), 85 deletions(-) rename pyhon/connection/{connection.py => handler.py} (90%) create mode 100644 pyhon/hon.py diff --git a/pyhon/__init__.py b/pyhon/__init__.py index c761612..e2b5e87 100644 --- a/pyhon/__init__.py +++ b/pyhon/__init__.py @@ -1 +1,2 @@ from .connection.api import HonAPI +from hon import Hon diff --git a/pyhon/__main__.py b/pyhon/__main__.py index cc0c1be..aed3a5d 100644 --- a/pyhon/__main__.py +++ b/pyhon/__main__.py @@ -11,7 +11,7 @@ from pprint import pprint if __name__ == "__main__": sys.path.insert(0, str(Path(__file__).parent.parent)) -from pyhon import HonAPI +from pyhon import Hon, HonAPI _LOGGER = logging.getLogger(__name__) @@ -104,8 +104,8 @@ async def main(): user = input("User for hOn account: ") if not (password := args["password"]): password = getpass("Password for hOn account: ") - async with HonAPI(user, password) as hon: - for device in hon.devices: + async with Hon(user, password) as hon: + for device in hon.appliances: print("=" * 10, device.appliance_type, "-", device.nick_name, "=" * 10) if args.get("keys"): data = device.data.copy() diff --git a/pyhon/appliance.py b/pyhon/appliance.py index 4137466..1670980 100644 --- a/pyhon/appliance.py +++ b/pyhon/appliance.py @@ -6,11 +6,11 @@ from pyhon.parameter import HonParameterFixed class HonAppliance: - def __init__(self, connector, appliance): - if attributes := appliance.get("attributes"): - appliance["attributes"] = {v["parName"]: v["parValue"] for v in attributes} - self._appliance = appliance - self._connector = connector + def __init__(self, api, info): + if attributes := info.get("attributes"): + info["attributes"] = {v["parName"]: v["parValue"] for v in attributes} + self._info = info + self._api = api self._appliance_model = {} self._commands = {} @@ -36,7 +36,7 @@ class HonAppliance: return self.data[item] if item in self.attributes["parameters"]: return self.attributes["parameters"].get(item) - return self.appliance[item] + return self.info[item] def get(self, item, default=None): try: @@ -46,23 +46,23 @@ class HonAppliance: @property def appliance_model_id(self): - return self._appliance.get("applianceModelId") + return self._info.get("applianceModelId") @property def appliance_type(self): - return self._appliance.get("applianceTypeName") + return self._info.get("applianceTypeName") @property def mac_address(self): - return self._appliance.get("macAddress") + return self._info.get("macAddress") @property def model_name(self): - return self._appliance.get("modelName") + return self._info.get("modelName") @property def nick_name(self): - return self._appliance.get("nickName") + return self._info.get("nickName") @property def commands_options(self): @@ -81,11 +81,11 @@ class HonAppliance: return self._statistics @property - def appliance(self): - return self._appliance + def info(self): + return self._info async def _recover_last_command_states(self, commands): - command_history = await self._connector.command_history(self) + command_history = await self._api.command_history(self) for name, command in commands.items(): last = next((index for (index, d) in enumerate(command_history) if d.get("command", {}).get("commandName") == name), None) if last is None: @@ -100,19 +100,19 @@ class HonAppliance: data.value = parameters.get(key) async def load_commands(self): - raw = await self._connector.load_commands(self) + raw = await self._api.load_commands(self) self._appliance_model = raw.pop("applianceModel") for item in ["settings", "options", "dictionaryId"]: raw.pop(item) commands = {} for command, attr in raw.items(): if "parameters" in attr: - commands[command] = HonCommand(command, attr, self._connector, self) + commands[command] = HonCommand(command, attr, self._api, self) elif "parameters" in attr[list(attr)[0]]: multi = {} for program, attr2 in attr.items(): program = program.split(".")[-1].lower() - cmd = HonCommand(command, attr2, self._connector, self, multi=multi, program=program) + cmd = HonCommand(command, attr2, self._api, self, multi=multi, program=program) multi[program] = cmd commands[command] = cmd self._commands = commands @@ -137,19 +137,19 @@ class HonAppliance: return result async def load_attributes(self): - self._attributes = await self._connector.load_attributes(self) + self._attributes = await self._api.load_attributes(self) for name, values in self._attributes.pop("shadow").get("parameters").items(): self._attributes.setdefault("parameters", {})[name] = values["parNewVal"] async def load_statistics(self): - self._statistics = await self._connector.load_statistics(self) + self._statistics = await self._api.load_statistics(self) async def update(self): await self.load_attributes() @property def data(self): - result = {"attributes": self.attributes, "appliance": self.appliance, "statistics": self.statistics, + result = {"attributes": self.attributes, "appliance": self.info, "statistics": self.statistics, **self.parameters} if self._extra: return self._extra.data(result) diff --git a/pyhon/connection/api.py b/pyhon/connection/api.py index 62196a3..34566cc 100644 --- a/pyhon/connection/api.py +++ b/pyhon/connection/api.py @@ -1,12 +1,10 @@ -import asyncio import json import logging from datetime import datetime -from typing import List from pyhon import const from pyhon.appliance import HonAppliance -from pyhon.connection.connection import HonConnectionHandler, HonAnonymousConnectionHandler +from pyhon.connection.handler import HonConnectionHandler, HonAnonymousConnectionHandler _LOGGER = logging.getLogger() @@ -16,53 +14,34 @@ class HonAPI: super().__init__() self._email = email self._password = password - self._devices = [] self._hon = None self._hon_anonymous = HonAnonymousConnectionHandler() async def __aenter__(self): - self._hon = HonConnectionHandler(self._email, self._password) - await self._hon.create() - await self.setup() - - return self + return await self.create() async def __aexit__(self, exc_type, exc_val, exc_tb): await self._hon.close() - @property - def devices(self) -> List[HonAppliance]: - return self._devices + async def create(self): + self._hon = await HonConnectionHandler(self._email, self._password).create() + return self - async def setup(self): + async def load_appliances(self): async with self._hon.get(f"{const.API_URL}/commands/v1/appliance") as resp: - try: - appliances = (await resp.json())["payload"]["appliances"] - for appliance in appliances: - device = HonAppliance(self, appliance) - if device.mac_address is None: - continue - await asyncio.gather(*[ - device.load_attributes(), - device.load_commands(), - device.load_statistics()]) - self._devices.append(device) - except json.JSONDecodeError: - _LOGGER.error("No JSON Data after GET: %s", await resp.text()) - return False - return True + return await resp.json() - async def load_commands(self, device: HonAppliance): + async def load_commands(self, appliance: HonAppliance): params = { - "applianceType": device.appliance_type, - "code": device.appliance["code"], - "applianceModelId": device.appliance_model_id, - "firmwareId": device.appliance["eepromId"], - "macAddress": device.mac_address, - "fwVersion": device.appliance["fwVersion"], + "applianceType": appliance.appliance_type, + "code": appliance.info["code"], + "applianceModelId": appliance.appliance_model_id, + "firmwareId": appliance.info["eepromId"], + "macAddress": appliance.mac_address, + "fwVersion": appliance.info["fwVersion"], "os": const.OS, "appVersion": const.APP_VERSION, - "series": device.appliance["series"], + "series": appliance.info["series"], } url = f"{const.API_URL}/commands/v1/retrieve" async with self._hon.get(url, params=params) as response: @@ -71,51 +50,51 @@ class HonAPI: return {} return result - async def command_history(self, device: HonAppliance): - url = f"{const.API_URL}/commands/v1/appliance/{device.mac_address}/history" + async def command_history(self, appliance: HonAppliance): + url = f"{const.API_URL}/commands/v1/appliance/{appliance.mac_address}/history" async with self._hon.get(url) as response: result = await response.json() if not result or not result.get("payload"): return {} return result["payload"]["history"] - async def last_activity(self, device: HonAppliance): + async def last_activity(self, appliance: HonAppliance): url = f"{const.API_URL}/commands/v1/retrieve-last-activity" - params = {"macAddress": device.mac_address} + params = {"macAddress": appliance.mac_address} async with self._hon.get(url, params=params) as response: result = await response.json() if result and (activity := result.get("attributes")): return activity return {} - async def load_attributes(self, device: HonAppliance): + async def load_attributes(self, appliance: HonAppliance): params = { - "macAddress": device.mac_address, - "applianceType": device.appliance_type, + "macAddress": appliance.mac_address, + "applianceType": appliance.appliance_type, "category": "CYCLE" } url = f"{const.API_URL}/commands/v1/context" async with self._hon.get(url, params=params) as response: return (await response.json()).get("payload", {}) - async def load_statistics(self, device: HonAppliance): + async def load_statistics(self, appliance: HonAppliance): params = { - "macAddress": device.mac_address, - "applianceType": device.appliance_type + "macAddress": appliance.mac_address, + "applianceType": appliance.appliance_type } url = f"{const.API_URL}/commands/v1/statistics" async with self._hon.get(url, params=params) as response: return (await response.json()).get("payload", {}) - async def send_command(self, device, command, parameters, ancillary_parameters): + async def send_command(self, appliance, command, parameters, ancillary_parameters): now = datetime.utcnow().isoformat() data = { - "macAddress": device.mac_address, + "macAddress": appliance.mac_address, "timestamp": f"{now[:-3]}Z", "commandName": command, - "transactionId": f"{device.mac_address}_{now[:-3]}Z", - "applianceOptions": device.commands_options, - "device": self._hon.device.get(), + "transactionId": f"{appliance.mac_address}_{now[:-3]}Z", + "applianceOptions": appliance.commands_options, + "appliance": self._hon.device.get(), "attributes": { "channel": "mobileApp", "origin": "standardProgram", @@ -123,15 +102,12 @@ class HonAPI: }, "ancillaryParameters": ancillary_parameters, "parameters": parameters, - "applianceType": device.appliance_type + "applianceType": appliance.appliance_type } url = f"{const.API_URL}/commands/v1/send" async with self._hon.post(url, json=data) as resp: - try: - json_data = await resp.json() - except json.JSONDecodeError: - return False - if json_data["payload"]["resultCode"] == "0": + json_data = await resp.json() + if json_data.get("payload", {}).get("resultCode") == "0": return True return False @@ -164,3 +140,6 @@ class HonAPI: if result := await response.json(): return result return {} + + async def close(self): + await self._hon.close() diff --git a/pyhon/connection/auth.py b/pyhon/connection/auth.py index 3066555..86960bc 100644 --- a/pyhon/connection/auth.py +++ b/pyhon/connection/auth.py @@ -1,10 +1,8 @@ -import datetime import json import logging import re import secrets import urllib -from pprint import pprint from urllib import parse from yarl import URL diff --git a/pyhon/connection/connection.py b/pyhon/connection/handler.py similarity index 90% rename from pyhon/connection/connection.py rename to pyhon/connection/handler.py index 66deb5b..2a3cf55 100644 --- a/pyhon/connection/connection.py +++ b/pyhon/connection/handler.py @@ -1,3 +1,4 @@ +import json from contextlib import asynccontextmanager import aiohttp @@ -15,14 +16,14 @@ class HonBaseConnectionHandler: self._auth = None async def __aenter__(self): - await self.create() - return self + return await self.create() async def __aexit__(self, exc_type, exc_val, exc_tb): await self.close() async def create(self): self._session = aiohttp.ClientSession(headers=self._HEADERS) + return self @asynccontextmanager async def get(self, *args, **kwargs): @@ -55,6 +56,7 @@ class HonConnectionHandler(HonBaseConnectionHandler): async def create(self): await super().create() self._auth = HonAuth(self._session, self._email, self._password, self._device) + return self async def _check_headers(self, headers): if "cognito-token" not in self._request_headers or "id-token" not in self._request_headers: @@ -81,7 +83,13 @@ class HonConnectionHandler(HonBaseConnectionHandler): _LOGGER.error("%s - Error %s - %s", response.request_info.url, response.status, await response.text()) raise PermissionError("Login failure") else: - yield response + try: + await response.json() + yield response + except json.JSONDecodeError: + _LOGGER.warning("%s - JsonDecodeError %s - %s", response.request_info.url, response.status, + await response.text()) + yield {} @asynccontextmanager async def get(self, *args, **kwargs): diff --git a/pyhon/hon.py b/pyhon/hon.py new file mode 100644 index 0000000..f30783c --- /dev/null +++ b/pyhon/hon.py @@ -0,0 +1,36 @@ +import asyncio +from typing import List + +from pyhon import HonAPI +from pyhon.appliance import HonAppliance + + +class Hon: + def __init__(self, email, password): + self._email = email + self._password = password + self._appliances = [] + self._api = None + + async def __aenter__(self): + self._api = await HonAPI(self._email, self._password).create() + await self.setup() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self._api.close() + + @property + def appliances(self) -> List[HonAppliance]: + return self._appliances + + async def setup(self): + for appliance in (await self._api.load_appliances())["payload"]["appliances"]: + appliance = HonAppliance(self._api, appliance) + if appliance.mac_address is None: + continue + await asyncio.gather(*[ + appliance.load_attributes(), + appliance.load_commands(), + appliance.load_statistics()]) + self._appliances.append(appliance)