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 hon import Hon

View file

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

View file

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

View file

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

View file

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

View file

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

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)