Compare commits

...

7 Commits

Author SHA1 Message Date
fe4f6e766e Fix connection loose 2023-03-11 00:28:53 +01:00
6b346f766f Fix command start 2023-03-08 23:01:59 +01:00
f52f84711f Fix bugs 2023-03-08 22:18:44 +01:00
c4d21be388 Print all keys 2023-03-08 21:53:53 +01:00
5acc81acc3 Clean up attribute structure 2023-03-08 21:13:19 +01:00
43d61ab853 Show more command data 2023-03-06 19:45:46 +01:00
79a121263f Fix connection issues 2023-03-06 18:57:08 +01:00
10 changed files with 131 additions and 127 deletions

2
.gitignore vendored
View File

@ -3,4 +3,4 @@ venv/
__pycache__/ __pycache__/
dist/ dist/
**/*.egg-info/ **/*.egg-info/
test.py test*

View File

@ -4,8 +4,8 @@
[![PyPI - Status](https://img.shields.io/pypi/status/pyhOn)](https://pypi.org/project/pyhOn) [![PyPI - Status](https://img.shields.io/pypi/status/pyhOn)](https://pypi.org/project/pyhOn)
[![PyPI](https://img.shields.io/pypi/v/pyhOn?color=blue)](https://pypi.org/project/pyhOn) [![PyPI](https://img.shields.io/pypi/v/pyhOn?color=blue)](https://pypi.org/project/pyhOn)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyhOn)](https://www.python.org/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyhOn)](https://www.python.org/)
[![PyPI - License](https://img.shields.io/pypi/l/pyhOn)](https://github.com/Andre0512/pyhOn/blob/main/LICENCE) [![PyPI - License](https://img.shields.io/pypi/l/pyhOn)](https://github.com/Andre0512/pyhOn/blob/main/LICENSE)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/pyhOn)](https://pypistats.org/packages/pyhOn) [![PyPI - Downloads](https://img.shields.io/pypi/dm/pyhOn)](https://pypistats.org/packages/pyhon)
Control your Haier appliances with python! Control your Haier appliances with python!
The idea behind this library is, to make the use of all available commands as simple as possible. The idea behind this library is, to make the use of all available commands as simple as possible.
@ -19,17 +19,28 @@ To get an idea of what is possible, use the commandline-tool `pyhOn`. This comma
```commandline ```commandline
$ pyhOn --user example@mail.com --password pass123 $ pyhOn --user example@mail.com --password pass123
========== WM - Waschmaschine ========== ========== WM - Waschmaschine ==========
commands:
pauseProgram: pauseProgram command
resumeProgram: resumeProgram command
startProgram: startProgram command
stopProgram: stopProgram command
data: data:
actualWeight: 0 attributes:
airWashTempLevel: 0 parameters:
airWashTime: 0 ...
antiAllergyStatus: 0 texture: 1
... totalElectricityUsed: 28.71
totalWashCycle: 35
totalWaterUsed: 2494
transMode: 0
...
settings:
startProgram:
rinseIterations:
max: 5
min: 3
step: 1
spinSpeed:
- 0
- 400
- 600
- 800
...
``` ```
## Python-API ## Python-API
@ -55,8 +66,6 @@ async with HonConnection(USER, PASSWORD) as hon:
``` ```
### Set command parameter ### Set command parameter
Use `device.settings` to get all variable parameters.
Use `device.parmeters` to get also fixed parameters.
```python ```python
async with HonConnection(USER, PASSWORD) as hon: async with HonConnection(USER, PASSWORD) as hon:
washing_machine = hon.devices[0] washing_machine = hon.devices[0]

View File

@ -21,10 +21,14 @@ def get_arguments():
parser = argparse.ArgumentParser(description="pyhOn: Command Line Utility") parser = argparse.ArgumentParser(description="pyhOn: Command Line Utility")
parser.add_argument("-u", "--user", help="user for haier hOn account") parser.add_argument("-u", "--user", help="user for haier hOn account")
parser.add_argument("-p", "--password", help="password for haier hOn account") parser.add_argument("-p", "--password", help="password for haier hOn account")
subparser = parser.add_subparsers(title="commands", metavar="COMMAND")
keys = subparser.add_parser("keys", help="print as key format")
keys.add_argument("keys", help="print as key format", action="store_true")
keys.add_argument("--all", help="print also full keys", action="store_true")
return vars(parser.parse_args()) return vars(parser.parse_args())
# yaml.dump() would be done the same, but needs an additional import... # yaml.dump() would be done the same, but needs an additional dependency...
def pretty_print(data, key="", intend=0, is_list=False): def pretty_print(data, key="", intend=0, is_list=False):
if type(data) is list: if type(data) is list:
if key: if key:
@ -47,6 +51,36 @@ def pretty_print(data, key="", intend=0, is_list=False):
print(f"{' ' * intend}{'- ' if is_list else ''}{key}{': ' if key else ''}{data}") print(f"{' ' * intend}{'- ' if is_list else ''}{key}{': ' if key else ''}{data}")
def key_print(data, key="", start=True):
if type(data) is list:
for i, value in enumerate(data):
key_print(value, key=f"{key}.{i}", start=False)
elif type(data) is dict:
for k, value in sorted(data.items()):
key_print(value, key=k if start else f"{key}.{k}", start=False)
else:
print(f"{key}: {data}")
def create_command(commands, concat=False):
result = {}
for name, command in commands.items():
if not concat:
result[name] = {}
for parameter, data in command.parameters.items():
if data.typology == "enum":
value = data.values
elif data.typology == "range":
value = {"min": data.min, "max": data.max, "step": data.step}
else:
continue
if not concat:
result[name][parameter] = value
else:
result[f"{name}.{parameter}"] = value
return result
async def main(): async def main():
args = get_arguments() args = get_arguments()
if not (user := args["user"]): if not (user := args["user"]):
@ -55,9 +89,18 @@ async def main():
password = getpass("Password for hOn account: ") password = getpass("Password for hOn account: ")
async with HonConnection(user, password) as hon: async with HonConnection(user, password) as hon:
for device in hon.devices: for device in hon.devices:
print("=" * 10, device.appliance_type_name, "-", device.nick_name, "=" * 10) print("=" * 10, device.appliance_type, "-", device.nick_name, "=" * 10)
pretty_print({"commands": device.commands}) if args.get("keys"):
data = device.data.copy()
attr = "get" if args.get("all") else "pop"
key_print(data["attributes"].__getattribute__(attr)("parameters"))
key_print(data.__getattribute__(attr)("appliance"))
key_print(data.__getattribute__(attr)("commands"))
key_print(data)
pretty_print(create_command(device.commands, concat=True))
else:
pretty_print({"data": device.data}) pretty_print({"data": device.data})
pretty_print({"settings": create_command(device.commands)})
def start(): def start():

View File

@ -67,15 +67,15 @@ class HonConnection:
async def load_commands(self, device: HonDevice): async def load_commands(self, device: HonDevice):
params = { params = {
"applianceType": device.appliance_type_name, "applianceType": device.appliance_type,
"code": device.code, "code": device.appliance["code"],
"applianceModelId": device.appliance_model_id, "applianceModelId": device.appliance_model_id,
"firmwareId": "41", "firmwareId": "41",
"macAddress": device.mac_address, "macAddress": device.mac_address,
"fwVersion": device.fw_version, "fwVersion": device.appliance["fwVersion"],
"os": const.OS, "os": const.OS,
"appVersion": const.APP_VERSION, "appVersion": const.APP_VERSION,
"series": device.series, "series": device.appliance["series"],
} }
url = f"{const.API_URL}/commands/v1/retrieve" url = f"{const.API_URL}/commands/v1/retrieve"
async with self._session.get(url, params=params, headers=await self._headers) as response: async with self._session.get(url, params=params, headers=await self._headers) as response:
@ -84,20 +84,25 @@ class HonConnection:
return {} return {}
return result return result
async def load_attributes(self, device: HonDevice): async def load_attributes(self, device: HonDevice, loop=False):
params = { params = {
"macAddress": device.mac_address, "macAddress": device.mac_address,
"applianceType": device.appliance_type_name, "applianceType": device.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._session.get(url, params=params, headers=await self._headers) as response: async with self._session.get(url, params=params, headers=await self._headers) as response:
if response.status == 403 and not loop:
_LOGGER.error("%s - Error %s - %s", url, response.status, await response.text())
self._request_headers.pop("cognito-token", None)
self._request_headers.pop("id-token", None)
return await self.load_attributes(device, loop=True)
return (await response.json()).get("payload", {}) return (await response.json()).get("payload", {})
async def load_statistics(self, device: HonDevice): async def load_statistics(self, device: HonDevice):
params = { params = {
"macAddress": device.mac_address, "macAddress": device.mac_address,
"applianceType": device.appliance_type_name "applianceType": device.appliance_type
} }
url = f"{const.API_URL}/commands/v1/statistics" url = f"{const.API_URL}/commands/v1/statistics"
async with self._session.get(url, params=params, headers=await self._headers) as response: async with self._session.get(url, params=params, headers=await self._headers) as response:
@ -125,7 +130,7 @@ class HonConnection:
}, },
"ancillaryParameters": ancillary_parameters, "ancillaryParameters": ancillary_parameters,
"parameters": parameters, "parameters": parameters,
"applianceType": device.appliance_type_name "applianceType": device.appliance_type
} }
url = f"{const.API_URL}/commands/v1/send" url = f"{const.API_URL}/commands/v1/send"
async with self._session.post(url, headers=await self._headers, json=data) as resp: async with self._session.post(url, headers=await self._headers, json=data) as resp:

View File

View File

@ -3,5 +3,8 @@ class Appliance:
self._data = data self._data = data
def get(self): def get(self):
self._data["connected"] = self._data["lastConnEvent.category"] == "CONNECTED" if self._data["attributes"]["lastConnEvent"]["category"] == "DISCONNECTED":
self._data["attributes"]["parameters"]["machMode"] = "0"
self._data["active"] = bool(self._data.get("activity"))
self._data["pause"] = self._data["attributes"]["parameters"]["machMode"] == "3"
return self._data return self._data

View File

@ -31,18 +31,15 @@ class HonCommand:
@property @property
def parameters(self): def parameters(self):
result = {key: parameter.value for key, parameter in self._parameters.items()} return self._parameters
if self._multi:
result |= {"program": self._category}
return result
@property @property
def ancillary_parameters(self): def ancillary_parameters(self):
return {key: parameter.value for key, parameter in self._ancillary_parameters.items()} return {key: parameter.value for key, parameter in self._ancillary_parameters.items()}
async def send(self): async def send(self):
return await self._connector.send_command(self._device, self._name, self.parameters, parameters = {name: parameter.value for name, parameter in self._parameters.items()}
self.ancillary_parameters) return await self._connector.send_command(self._device, self._name, parameters, self.ancillary_parameters)
def get_programs(self): def get_programs(self):
return self._multi return self._multi

View File

@ -1,14 +1,12 @@
import importlib import importlib
from pprint import pprint
from pyhon.commands import HonCommand from pyhon.commands import HonCommand
class HonDevice: class HonDevice:
def __init__(self, connector, appliance): def __init__(self, connector, appliance):
appliance["attributes"] = {v["parName"]: v["parValue"] for v in appliance["attributes"]}
self._appliance = appliance self._appliance = appliance
for values in self._appliance.pop("attributes"):
self._appliance[values["parName"]] = values["parValue"]
self._connector = connector self._connector = connector
self._appliance_model = {} self._appliance_model = {}
@ -16,74 +14,41 @@ class HonDevice:
self._statistics = {} self._statistics = {}
self._attributes = {} self._attributes = {}
@property try:
def appliance_id(self): self._extra = importlib.import_module(f'pyhon.appliances.{self.appliance_type.lower()}')
return self._appliance.get("applianceId") except ModuleNotFoundError:
self._extra = None
def __getitem__(self, item):
if "." in item:
result = self.data
for key in item.split("."):
if all([k in "0123456789" for k in key]) and type(result) is list:
result = result[int(key)]
else:
result = result[key]
return result
else:
if item in self.data:
return self.data[item]
if item in self.attributes["parameters"]:
return self.attributes["parameters"].get(item)
return self.appliance[item]
def get(self, item, default=None):
try:
return self[item]
except (KeyError, IndexError):
return default
@property @property
def appliance_model_id(self): def appliance_model_id(self):
return self._appliance.get("applianceModelId") return self._appliance.get("applianceModelId")
@property @property
def appliance_status(self): def appliance_type(self):
return self._appliance.get("applianceStatus")
@property
def appliance_type_id(self):
return self._appliance.get("applianceTypeId")
@property
def appliance_type_name(self):
return self._appliance.get("applianceTypeName") return self._appliance.get("applianceTypeName")
@property
def brand(self):
return self._appliance.get("brand")
@property
def code(self):
return self._appliance.get("code")
@property
def connectivity(self):
return self._appliance.get("connectivity")
@property
def coords(self):
return self._appliance.get("coords")
@property
def eeprom_id(self):
return self._appliance.get("eepromId")
@property
def eeprom_name(self):
return self._appliance.get("eepromName")
@property
def enrollment_date(self):
return self._appliance.get("enrollmentDate")
@property
def first_enrollment(self):
return self._appliance.get("firstEnrollment")
@property
def first_enrollment_tbc(self):
return self._appliance.get("firstEnrollmentTBC")
@property
def fw_version(self):
return self._appliance.get("fwVersion")
@property
def id(self):
return self._appliance.get("id")
@property
def last_update(self):
return self._appliance.get("lastUpdate")
@property @property
def mac_address(self): def mac_address(self):
return self._appliance.get("macAddress") return self._appliance.get("macAddress")
@ -96,22 +61,6 @@ class HonDevice:
def nick_name(self): def nick_name(self):
return self._appliance.get("nickName") return self._appliance.get("nickName")
@property
def purchase_date(self):
return self._appliance.get("purchaseDate")
@property
def serial_number(self):
return self._appliance.get("serialNumber")
@property
def series(self):
return self._appliance.get("series")
@property
def water_hard(self):
return self._appliance.get("waterHard")
@property @property
def commands_options(self): def commands_options(self):
return self._appliance_model.get("options") return self._appliance_model.get("options")
@ -162,15 +111,13 @@ class HonDevice:
result = {} result = {}
for name, command in self._commands.items(): for name, command in self._commands.items():
for key, parameter in command.parameters.items(): for key, parameter in command.parameters.items():
result[f"{name}.{key}"] = parameter result.setdefault(name, {})[key] = parameter.value
return result return result
async def load_attributes(self): async def load_attributes(self):
data = await self._connector.load_attributes(self) self._attributes = await self._connector.load_attributes(self)
for name, values in data.get("shadow").get("parameters").items(): for name, values in self._attributes.pop("shadow").get("parameters").items():
self._attributes[name] = values["parNewVal"] self._attributes.setdefault("parameters", {})[name] = values["parNewVal"]
for name, value in data.get("lastConnEvent").items():
self._attributes[f"lastConnEvent.{name}"] = value
async def load_statistics(self): async def load_statistics(self):
self._statistics = await self._connector.load_statistics(self) self._statistics = await self._connector.load_statistics(self)
@ -180,9 +127,8 @@ class HonDevice:
@property @property
def data(self): def data(self):
result = self.attributes | self.parameters | self.appliance | self._statistics result = {"attributes": self.attributes, "appliance": self.appliance, "statistics": self.statistics,
try: **self.parameters}
extra = importlib.import_module(f'appliances.{self.appliance_type_name.lower()}') if self._extra:
return result | extra.Appliance(result).get() return result | self._extra.Appliance(result).get()
except ModuleNotFoundError:
return result return result

View File

@ -113,6 +113,7 @@ class HonParameterProgram(HonParameterEnum):
self._command = command self._command = command
self._value = command._category self._value = command._category
self._values = command._multi self._values = command._multi
self._typology = "enum"
@property @property
def value(self): def value(self):

View File

@ -7,7 +7,7 @@ with open("README.md", "r") as f:
setup( setup(
name="pyhOn", name="pyhOn",
version="0.2.4", version="0.3.4",
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,
@ -23,7 +23,7 @@ setup(
python_requires=">=3.10", python_requires=">=3.10",
install_requires=["aiohttp"], install_requires=["aiohttp"],
classifiers=[ classifiers=[
"Development Status :: 3 - Alpha", "Development Status :: 4 - Beta",
"Environment :: Console", "Environment :: Console",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Natural Language :: English", "Natural Language :: English",