Refactor api access

This commit is contained in:
Andre Basche 2023-04-09 20:50:28 +02:00
parent eb6741145a
commit 36fad84ee2
7 changed files with 107 additions and 85 deletions

View File

@ -1 +1,2 @@
from .connection.api import HonAPI from .connection.api import HonAPI
from hon import Hon

View File

@ -11,7 +11,7 @@ from pprint import pprint
if __name__ == "__main__": if __name__ == "__main__":
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
from pyhon import HonAPI from pyhon import Hon, HonAPI
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -104,8 +104,8 @@ async def main():
user = input("User for hOn account: ") user = input("User for hOn account: ")
if not (password := args["password"]): if not (password := args["password"]):
password = getpass("Password for hOn account: ") password = getpass("Password for hOn account: ")
async with HonAPI(user, password) as hon: async with Hon(user, password) as hon:
for device in hon.devices: for device in hon.appliances:
print("=" * 10, device.appliance_type, "-", device.nick_name, "=" * 10) print("=" * 10, device.appliance_type, "-", device.nick_name, "=" * 10)
if args.get("keys"): if args.get("keys"):
data = device.data.copy() data = device.data.copy()

View File

@ -6,11 +6,11 @@ from pyhon.parameter import HonParameterFixed
class HonAppliance: class HonAppliance:
def __init__(self, connector, appliance): def __init__(self, api, info):
if attributes := appliance.get("attributes"): if attributes := info.get("attributes"):
appliance["attributes"] = {v["parName"]: v["parValue"] for v in attributes} info["attributes"] = {v["parName"]: v["parValue"] for v in attributes}
self._appliance = appliance self._info = info
self._connector = connector self._api = api
self._appliance_model = {} self._appliance_model = {}
self._commands = {} self._commands = {}
@ -36,7 +36,7 @@ class HonAppliance:
return self.data[item] return self.data[item]
if item in self.attributes["parameters"]: if item in self.attributes["parameters"]:
return self.attributes["parameters"].get(item) return self.attributes["parameters"].get(item)
return self.appliance[item] return self.info[item]
def get(self, item, default=None): def get(self, item, default=None):
try: try:
@ -46,23 +46,23 @@ class HonAppliance:
@property @property
def appliance_model_id(self): def appliance_model_id(self):
return self._appliance.get("applianceModelId") return self._info.get("applianceModelId")
@property @property
def appliance_type(self): def appliance_type(self):
return self._appliance.get("applianceTypeName") return self._info.get("applianceTypeName")
@property @property
def mac_address(self): def mac_address(self):
return self._appliance.get("macAddress") return self._info.get("macAddress")
@property @property
def model_name(self): def model_name(self):
return self._appliance.get("modelName") return self._info.get("modelName")
@property @property
def nick_name(self): def nick_name(self):
return self._appliance.get("nickName") return self._info.get("nickName")
@property @property
def commands_options(self): def commands_options(self):
@ -81,11 +81,11 @@ class HonAppliance:
return self._statistics return self._statistics
@property @property
def appliance(self): def info(self):
return self._appliance return self._info
async def _recover_last_command_states(self, commands): 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(): for name, command in commands.items():
last = next((index for (index, d) in enumerate(command_history) if d.get("command", {}).get("commandName") == name), None) last = next((index for (index, d) in enumerate(command_history) if d.get("command", {}).get("commandName") == name), None)
if last is None: if last is None:
@ -100,19 +100,19 @@ class HonAppliance:
data.value = parameters.get(key) data.value = parameters.get(key)
async def load_commands(self): 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") self._appliance_model = raw.pop("applianceModel")
for item in ["settings", "options", "dictionaryId"]: for item in ["settings", "options", "dictionaryId"]:
raw.pop(item) raw.pop(item)
commands = {} commands = {}
for command, attr in raw.items(): for command, attr in raw.items():
if "parameters" in attr: 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]]: elif "parameters" in attr[list(attr)[0]]:
multi = {} multi = {}
for program, attr2 in attr.items(): for program, attr2 in attr.items():
program = program.split(".")[-1].lower() 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 multi[program] = cmd
commands[command] = cmd commands[command] = cmd
self._commands = commands self._commands = commands
@ -137,19 +137,19 @@ class HonAppliance:
return result return result
async def load_attributes(self): 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(): for name, values in self._attributes.pop("shadow").get("parameters").items():
self._attributes.setdefault("parameters", {})[name] = values["parNewVal"] self._attributes.setdefault("parameters", {})[name] = values["parNewVal"]
async def load_statistics(self): 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): async def update(self):
await self.load_attributes() await self.load_attributes()
@property @property
def data(self): 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} **self.parameters}
if self._extra: if self._extra:
return self._extra.data(result) return self._extra.data(result)

View File

@ -1,12 +1,10 @@
import asyncio
import json import json
import logging import logging
from datetime import datetime from datetime import datetime
from typing import List
from pyhon import const from pyhon import const
from pyhon.appliance import HonAppliance from pyhon.appliance import HonAppliance
from pyhon.connection.connection import HonConnectionHandler, HonAnonymousConnectionHandler from pyhon.connection.handler import HonConnectionHandler, HonAnonymousConnectionHandler
_LOGGER = logging.getLogger() _LOGGER = logging.getLogger()
@ -16,53 +14,34 @@ class HonAPI:
super().__init__() super().__init__()
self._email = email self._email = email
self._password = password self._password = password
self._devices = []
self._hon = None self._hon = None
self._hon_anonymous = HonAnonymousConnectionHandler() self._hon_anonymous = HonAnonymousConnectionHandler()
async def __aenter__(self): async def __aenter__(self):
self._hon = HonConnectionHandler(self._email, self._password) return await self.create()
await self._hon.create()
await self.setup()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb): async def __aexit__(self, exc_type, exc_val, exc_tb):
await self._hon.close() await self._hon.close()
@property async def create(self):
def devices(self) -> List[HonAppliance]: self._hon = await HonConnectionHandler(self._email, self._password).create()
return self._devices 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: async with self._hon.get(f"{const.API_URL}/commands/v1/appliance") as resp:
try: return await resp.json()
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
async def load_commands(self, device: HonAppliance): async def load_commands(self, appliance: HonAppliance):
params = { params = {
"applianceType": device.appliance_type, "applianceType": appliance.appliance_type,
"code": device.appliance["code"], "code": appliance.info["code"],
"applianceModelId": device.appliance_model_id, "applianceModelId": appliance.appliance_model_id,
"firmwareId": device.appliance["eepromId"], "firmwareId": appliance.info["eepromId"],
"macAddress": device.mac_address, "macAddress": appliance.mac_address,
"fwVersion": device.appliance["fwVersion"], "fwVersion": appliance.info["fwVersion"],
"os": const.OS, "os": const.OS,
"appVersion": const.APP_VERSION, "appVersion": const.APP_VERSION,
"series": device.appliance["series"], "series": appliance.info["series"],
} }
url = f"{const.API_URL}/commands/v1/retrieve" url = f"{const.API_URL}/commands/v1/retrieve"
async with self._hon.get(url, params=params) as response: async with self._hon.get(url, params=params) as response:
@ -71,51 +50,51 @@ class HonAPI:
return {} return {}
return result return result
async def command_history(self, device: HonAppliance): async def command_history(self, appliance: HonAppliance):
url = f"{const.API_URL}/commands/v1/appliance/{device.mac_address}/history" url = f"{const.API_URL}/commands/v1/appliance/{appliance.mac_address}/history"
async with self._hon.get(url) as response: async with self._hon.get(url) as response:
result = await response.json() result = await response.json()
if not result or not result.get("payload"): if not result or not result.get("payload"):
return {} return {}
return result["payload"]["history"] 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" 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: async with self._hon.get(url, params=params) as response:
result = await response.json() result = await response.json()
if result and (activity := result.get("attributes")): if result and (activity := result.get("attributes")):
return activity return activity
return {} return {}
async def load_attributes(self, device: HonAppliance): async def load_attributes(self, appliance: HonAppliance):
params = { params = {
"macAddress": device.mac_address, "macAddress": appliance.mac_address,
"applianceType": device.appliance_type, "applianceType": appliance.appliance_type,
"category": "CYCLE" "category": "CYCLE"
} }
url = f"{const.API_URL}/commands/v1/context" url = f"{const.API_URL}/commands/v1/context"
async with self._hon.get(url, params=params) as response: async with self._hon.get(url, params=params) as response:
return (await response.json()).get("payload", {}) return (await response.json()).get("payload", {})
async def load_statistics(self, device: HonAppliance): async def load_statistics(self, appliance: HonAppliance):
params = { params = {
"macAddress": device.mac_address, "macAddress": appliance.mac_address,
"applianceType": device.appliance_type "applianceType": appliance.appliance_type
} }
url = f"{const.API_URL}/commands/v1/statistics" url = f"{const.API_URL}/commands/v1/statistics"
async with self._hon.get(url, params=params) as response: async with self._hon.get(url, params=params) as response:
return (await response.json()).get("payload", {}) 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() now = datetime.utcnow().isoformat()
data = { data = {
"macAddress": device.mac_address, "macAddress": appliance.mac_address,
"timestamp": f"{now[:-3]}Z", "timestamp": f"{now[:-3]}Z",
"commandName": command, "commandName": command,
"transactionId": f"{device.mac_address}_{now[:-3]}Z", "transactionId": f"{appliance.mac_address}_{now[:-3]}Z",
"applianceOptions": device.commands_options, "applianceOptions": appliance.commands_options,
"device": self._hon.device.get(), "appliance": self._hon.device.get(),
"attributes": { "attributes": {
"channel": "mobileApp", "channel": "mobileApp",
"origin": "standardProgram", "origin": "standardProgram",
@ -123,15 +102,12 @@ class HonAPI:
}, },
"ancillaryParameters": ancillary_parameters, "ancillaryParameters": ancillary_parameters,
"parameters": parameters, "parameters": parameters,
"applianceType": device.appliance_type "applianceType": appliance.appliance_type
} }
url = f"{const.API_URL}/commands/v1/send" url = f"{const.API_URL}/commands/v1/send"
async with self._hon.post(url, json=data) as resp: async with self._hon.post(url, json=data) as resp:
try:
json_data = await resp.json() json_data = await resp.json()
except json.JSONDecodeError: if json_data.get("payload", {}).get("resultCode") == "0":
return False
if json_data["payload"]["resultCode"] == "0":
return True return True
return False return False
@ -164,3 +140,6 @@ class HonAPI:
if result := await response.json(): if result := await response.json():
return result return result
return {} return {}
async def close(self):
await self._hon.close()

View File

@ -1,10 +1,8 @@
import datetime
import json import json
import logging import logging
import re import re
import secrets import secrets
import urllib import urllib
from pprint import pprint
from urllib import parse from urllib import parse
from yarl import URL from yarl import URL

View File

@ -1,3 +1,4 @@
import json
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import aiohttp import aiohttp
@ -15,14 +16,14 @@ class HonBaseConnectionHandler:
self._auth = None self._auth = None
async def __aenter__(self): async def __aenter__(self):
await self.create() return await self.create()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb): async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close() await self.close()
async def create(self): async def create(self):
self._session = aiohttp.ClientSession(headers=self._HEADERS) self._session = aiohttp.ClientSession(headers=self._HEADERS)
return self
@asynccontextmanager @asynccontextmanager
async def get(self, *args, **kwargs): async def get(self, *args, **kwargs):
@ -55,6 +56,7 @@ class HonConnectionHandler(HonBaseConnectionHandler):
async def create(self): async def create(self):
await super().create() await super().create()
self._auth = HonAuth(self._session, self._email, self._password, self._device) self._auth = HonAuth(self._session, self._email, self._password, self._device)
return self
async def _check_headers(self, headers): async def _check_headers(self, headers):
if "cognito-token" not in self._request_headers or "id-token" not in self._request_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()) _LOGGER.error("%s - Error %s - %s", response.request_info.url, response.status, await response.text())
raise PermissionError("Login failure") raise PermissionError("Login failure")
else: else:
try:
await response.json()
yield response yield response
except json.JSONDecodeError:
_LOGGER.warning("%s - JsonDecodeError %s - %s", response.request_info.url, response.status,
await response.text())
yield {}
@asynccontextmanager @asynccontextmanager
async def get(self, *args, **kwargs): async def get(self, *args, **kwargs):

36
pyhon/hon.py Normal file
View File

@ -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)