Compare commits

...

6 Commits

Author SHA1 Message Date
4888f2b1d0 Add oven climate support 2023-05-28 17:41:20 +02:00
7c6ac15901 Add and improve fridge 2023-05-28 07:37:38 +02:00
eea79e28b9 Fix for fridge program names 2023-05-22 01:07:55 +02:00
ecbf438889 Bump version 2023-05-21 20:48:27 +02:00
9cd12e3234 Fixes for fridge 2023-05-21 20:33:08 +02:00
c2765fe953 Add rule handling 2023-05-21 02:25:43 +02:00
14 changed files with 222 additions and 19 deletions

View File

@ -11,6 +11,7 @@ from pyhon import helper
from pyhon.commands import HonCommand from pyhon.commands import HonCommand
from pyhon.parameter.base import HonParameter from pyhon.parameter.base import HonParameter
from pyhon.parameter.fixed import HonParameterFixed from pyhon.parameter.fixed import HonParameterFixed
from pyhon.parameter.range import HonParameterRange
if TYPE_CHECKING: if TYPE_CHECKING:
from pyhon import HonAPI from pyhon import HonAPI
@ -106,8 +107,8 @@ class HonAppliance:
return serial_number[:8] if len(serial_number) < 18 else serial_number[:11] return serial_number[:8] if len(serial_number) < 18 else serial_number[:11]
@property @property
def commands_options(self): def options(self):
return self._appliance_model.get("options") return self._appliance_model.get("options", {})
@property @property
def commands(self): def commands(self):
@ -287,11 +288,33 @@ class HonAppliance:
data.get("appliance", {}).pop(sensible, None) data.get("appliance", {}).pop(sensible, None)
result = helper.pretty_print({"data": data}, whitespace=whitespace) result = helper.pretty_print({"data": data}, whitespace=whitespace)
result += helper.pretty_print( result += helper.pretty_print(
{"commands": helper.create_command(self.commands)}, {
"commands": helper.create_command(self.commands),
"rules": helper.create_rules(self.commands),
},
whitespace=whitespace, whitespace=whitespace,
) )
return result.replace(self.mac_address, "xx-xx-xx-xx-xx-xx") return result.replace(self.mac_address, "xx-xx-xx-xx-xx-xx")
def sync_command(self, main, target=None) -> None:
base: HonCommand = self.commands.get(main)
for command, data in self.commands.items():
if command == main or target and command not in target:
continue
for name, parameter in data.parameters.items():
if base_value := base.parameters.get(name):
if isinstance(base_value, HonParameterRange) and isinstance(
parameter, HonParameterRange
):
parameter.max = base_value.max
parameter.min = base_value.min
parameter.step = base_value.step
elif isinstance(parameter, HonParameterRange):
parameter.max = int(base_value.value)
parameter.min = int(base_value.value)
parameter.step = 1
parameter.value = base_value.value
class HonApplianceTest(HonAppliance): class HonApplianceTest(HonAppliance):
def __init__(self, name): def __init__(self, name):

View File

@ -11,6 +11,10 @@ class Appliance:
data["active"] = data["attributes"]["parameters"]["onOffStatus"] == "1" data["active"] = data["attributes"]["parameters"]["onOffStatus"] == "1"
if program := int(data["attributes"]["parameters"]["prCode"]):
ids = self.parent.settings["startProgram.program"].ids
data["programName"] = ids.get(program, "")
return data return data
def settings(self, settings): def settings(self, settings):

28
pyhon/appliances/ref.py Normal file
View File

@ -0,0 +1,28 @@
from pyhon.parameter.fixed import HonParameterFixed
class Appliance:
def __init__(self, appliance):
self.parent = appliance
def data(self, data):
if data["attributes"]["parameters"]["holidayMode"] == "1":
data["modeZ1"] = "holiday"
elif data["attributes"]["parameters"]["intelligenceMode"] == "1":
data["modeZ1"] = "auto_set"
elif data["attributes"]["parameters"]["quickModeZ1"] == "1":
data["modeZ1"] = "super_cool"
else:
data["modeZ1"] = "no_mode"
if data["attributes"]["parameters"]["quickModeZ2"] == "1":
data["modeZ2"] = "super_freeze"
elif data["attributes"]["parameters"]["intelligenceMode"] == "1":
data["modeZ2"] = "auto_set"
else:
data["modeZ2"] = "no_mode"
return data
def settings(self, settings):
return settings

View File

@ -1,3 +1,4 @@
import logging
from typing import Optional, Dict, Any, List, TYPE_CHECKING, Union from typing import Optional, Dict, Any, List, TYPE_CHECKING, Union
from pyhon import exceptions from pyhon import exceptions
@ -6,11 +7,14 @@ from pyhon.parameter.enum import HonParameterEnum
from pyhon.parameter.fixed import HonParameterFixed from pyhon.parameter.fixed import HonParameterFixed
from pyhon.parameter.program import HonParameterProgram from pyhon.parameter.program import HonParameterProgram
from pyhon.parameter.range import HonParameterRange from pyhon.parameter.range import HonParameterRange
from pyhon.rules import HonRuleSet
if TYPE_CHECKING: if TYPE_CHECKING:
from pyhon import HonAPI from pyhon import HonAPI
from pyhon.appliance import HonAppliance from pyhon.appliance import HonAppliance
_LOGGER = logging.getLogger(__name__)
class HonCommand: class HonCommand:
def __init__( def __init__(
@ -31,6 +35,7 @@ class HonCommand:
self._parameters: Dict[str, HonParameter] = {} self._parameters: Dict[str, HonParameter] = {}
self._data: Dict[str, Any] = {} self._data: Dict[str, Any] = {}
self._available_settings: Dict[str, HonParameter] = {} self._available_settings: Dict[str, HonParameter] = {}
self._rules: List[HonRuleSet] = []
self._load_parameters(attributes) self._load_parameters(attributes)
def __repr__(self) -> str: def __repr__(self) -> str:
@ -43,9 +48,13 @@ class HonCommand:
@property @property
def api(self) -> "HonAPI": def api(self) -> "HonAPI":
if self._api is None: if self._api is None:
raise exceptions.NoAuthenticationException raise exceptions.NoAuthenticationException("Missing hOn login")
return self._api return self._api
@property
def appliance(self) -> "HonAppliance":
return self._appliance
@property @property
def data(self): def data(self):
return self._data return self._data
@ -62,7 +71,7 @@ class HonCommand:
def parameter_groups(self) -> Dict[str, Dict[str, Union[str, float]]]: def parameter_groups(self) -> Dict[str, Dict[str, Union[str, float]]]:
result: Dict[str, Dict[str, Union[str, float]]] = {} result: Dict[str, Dict[str, Union[str, float]]] = {}
for name, parameter in self._parameters.items(): for name, parameter in self._parameters.items():
result.setdefault(parameter.group, {})[name] = parameter.value result.setdefault(parameter.group, {})[name] = parameter.intern_value
return result return result
@property @property
@ -73,10 +82,17 @@ class HonCommand:
for key, items in attributes.items(): for key, items in attributes.items():
for name, data in items.items(): for name, data in items.items():
self._create_parameters(data, name, key) self._create_parameters(data, name, key)
for rule in self._rules:
rule.patch()
def _create_parameters(self, data: Dict, name: str, parameter: str) -> None: def _create_parameters(self, data: Dict, name: str, parameter: str) -> None:
if name == "zoneMap" and self._appliance.zone: if name == "zoneMap" and self._appliance.zone:
data["default"] = self._appliance.zone data["default"] = self._appliance.zone
if data.get("category") == "rule":
if "fixedValue" not in data:
_LOGGER.error("Rule not supported: %s", data)
else:
self._rules.append(HonRuleSet(self, data["fixedValue"]))
match data.get("typology"): match data.get("typology"):
case "range": case "range":
self._parameters[name] = HonParameterRange(name, data, parameter) self._parameters[name] = HonParameterRange(name, data, parameter)

View File

@ -171,7 +171,7 @@ class HonAPI:
"timestamp": f"{now[:-3]}Z", "timestamp": f"{now[:-3]}Z",
"commandName": command, "commandName": command,
"transactionId": f"{appliance.mac_address}_{now[:-3]}Z", "transactionId": f"{appliance.mac_address}_{now[:-3]}Z",
"applianceOptions": appliance.commands_options, "applianceOptions": appliance.options,
"device": self._hon.device.get(mobile=True), "device": self._hon.device.get(mobile=True),
"attributes": { "attributes": {
"channel": "mobileApp", "channel": "mobileApp",

View File

@ -4,7 +4,7 @@ API_KEY = "GRCqFhC6Gk@ikWXm1RmnSmX1cm,MxY-configuration"
APP = "hon" APP = "hon"
# All seen id's (different accounts, different devices) are the same, so I guess this hash is static # All seen id's (different accounts, different devices) are the same, so I guess this hash is static
CLIENT_ID = "3MVG9QDx8IX8nP5T2Ha8ofvlmjLZl5L_gvfbT9.HJvpHGKoAS_dcMN8LYpTSYeVFCraUnV.2Ag1Ki7m4znVO6" CLIENT_ID = "3MVG9QDx8IX8nP5T2Ha8ofvlmjLZl5L_gvfbT9.HJvpHGKoAS_dcMN8LYpTSYeVFCraUnV.2Ag1Ki7m4znVO6"
APP_VERSION = "1.53.7" APP_VERSION = "2.0.9"
OS_VERSION = 31 OS_VERSION = 31
OS = "android" OS = "android"
DEVICE_MODEL = "exynos9820" DEVICE_MODEL = "exynos9820"

View File

@ -47,8 +47,6 @@ def pretty_print(data, key="", intend=0, is_list=False, whitespace=" "):
def create_command(commands, concat=False): def create_command(commands, concat=False):
result = {} result = {}
for name, command in commands.items(): for name, command in commands.items():
if not concat:
result[name] = {}
for parameter, data in command.available_settings.items(): for parameter, data in command.available_settings.items():
if data.typology == "enum": if data.typology == "enum":
value = data.values value = data.values
@ -57,7 +55,21 @@ def create_command(commands, concat=False):
else: else:
continue continue
if not concat: if not concat:
result[name][parameter] = value result.setdefault(name, {})[parameter] = value
else:
result[f"{name}.{parameter}"] = value
return result
def create_rules(commands, concat=False):
result = {}
for name, command in commands.items():
for parameter, data in command.available_settings.items():
value = data.triggers
if not value:
continue
if not concat:
result.setdefault(name, {})[parameter] = value
else: else:
result[f"{name}.{parameter}"] = value result[f"{name}.{parameter}"] = value
return result return result

View File

@ -1,4 +1,7 @@
from typing import Dict, Any, List from typing import Dict, Any, List, Tuple, Callable, TYPE_CHECKING
if TYPE_CHECKING:
from pyhon.rules import HonRule
class HonParameter: class HonParameter:
@ -9,6 +12,7 @@ class HonParameter:
self._mandatory: int = attributes.get("mandatory", 0) self._mandatory: int = attributes.get("mandatory", 0)
self._value: str | float = "" self._value: str | float = ""
self._group: str = group self._group: str = group
self._triggers: Dict[str, List[Tuple[Callable, "HonRule"]]] = {}
@property @property
def key(self) -> str: def key(self) -> str:
@ -18,6 +22,15 @@ class HonParameter:
def value(self) -> str | float: def value(self) -> str | float:
return self._value if self._value is not None else "0" return self._value if self._value is not None else "0"
@value.setter
def value(self, value: str | float) -> None:
self._value = value
self.check_trigger(value)
@property
def intern_value(self) -> str | float:
return str(self._value) if self._value is not None else ""
@property @property
def values(self) -> List[str]: def values(self) -> List[str]:
return [str(self.value)] return [str(self.value)]
@ -37,3 +50,21 @@ class HonParameter:
@property @property
def group(self) -> str: def group(self) -> str:
return self._group return self._group
def add_trigger(self, value, func, data):
if self._value == value:
func(data)
self._triggers.setdefault(value, []).append((func, data))
def check_trigger(self, value) -> None:
if str(value) in self._triggers:
for trigger in self._triggers[str(value)]:
func, args = trigger
func(args)
@property
def triggers(self):
result = {}
for value, rules in self._triggers.items():
result[value] = {rule.param_key: rule.param_value for _, rule in rules}
return result

View File

@ -3,13 +3,17 @@ from typing import Dict, Any, List
from pyhon.parameter.base import HonParameter from pyhon.parameter.base import HonParameter
def clean_value(value):
return str(value).strip("[]").replace("|", "_").lower()
class HonParameterEnum(HonParameter): class HonParameterEnum(HonParameter):
def __init__(self, key: str, attributes: Dict[str, Any], group: str) -> None: def __init__(self, key: str, attributes: Dict[str, Any], group: str) -> None:
super().__init__(key, attributes, group) super().__init__(key, attributes, group)
self._default = attributes.get("defaultValue") self._default = attributes.get("defaultValue")
self._value = self._default or "0" self._value = self._default or "0"
self._values: List[str] = attributes.get("enumValues", []) self._values: List[str] = attributes.get("enumValues", [])
if self._default and str(self._default) not in self.values: if self._default and clean_value(self._default.strip("[]")) not in self.values:
self._values.append(self._default) self._values.append(self._default)
def __repr__(self) -> str: def __repr__(self) -> str:
@ -17,15 +21,20 @@ class HonParameterEnum(HonParameter):
@property @property
def values(self) -> List[str]: def values(self) -> List[str]:
return [str(value) for value in self._values] return [clean_value(value) for value in self._values]
@values.setter
def values(self, values) -> None:
self._values = values
@property @property
def value(self) -> str | float: def value(self) -> str | float:
return self._value if self._value is not None else self.values[0] return clean_value(self._value) if self._value is not None else self.values[0]
@value.setter @value.setter
def value(self, value: str) -> None: def value(self, value: str) -> None:
if value in self.values: if value in self.values:
self._value = value self._value = value
self.check_trigger(value)
else: else:
raise ValueError(f"Allowed values {self._value}") raise ValueError(f"Allowed values {self._values}")

View File

@ -19,3 +19,4 @@ class HonParameterFixed(HonParameter):
def value(self, value: str | float) -> None: def value(self, value: str | float) -> None:
# Fixed values seems being not so fixed as thought # Fixed values seems being not so fixed as thought
self._value = value self._value = value
self.check_trigger(value)

View File

@ -35,8 +35,12 @@ class HonParameterProgram(HonParameterEnum):
values = [v for v in self._programs if all(f not in v for f in self._FILTER)] values = [v for v in self._programs if all(f not in v for f in self._FILTER)]
return sorted(values) return sorted(values)
@values.setter
def values(self, values) -> None:
return
@property @property
def ids(self): def ids(self) -> Dict[int, str]:
values = { values = {
int(p.parameters["prCode"].value): n int(p.parameters["prCode"].value): n
for i, (n, p) in enumerate(self._programs.items()) for i, (n, p) in enumerate(self._programs.items())

View File

@ -26,25 +26,38 @@ class HonParameterRange(HonParameter):
def min(self) -> float: def min(self) -> float:
return self._min return self._min
@min.setter
def min(self, min: float) -> None:
self._min = min
@property @property
def max(self) -> float: def max(self) -> float:
return self._max return self._max
@max.setter
def max(self, max: float) -> None:
self._max = max
@property @property
def step(self) -> float: def step(self) -> float:
if not self._step: if not self._step:
return 1 return 1
return self._step return self._step
@step.setter
def step(self, step: float) -> None:
self._step = step
@property @property
def value(self) -> float: def value(self) -> str | float:
return self._value if self._value is not None else self._min return self._value if self._value is not None else self._min
@value.setter @value.setter
def value(self, value: float) -> None: def value(self, value: str | float) -> None:
value = str_to_float(value) value = str_to_float(value)
if self._min <= value <= self._max and not (value - self._min) % self._step: if self._min <= value <= self._max and not (value - self._min) % self._step:
self._value = value self._value = value
self.check_trigger(value)
else: else:
raise ValueError( raise ValueError(
f"Allowed: min {self._min} max {self._max} step {self._step}" f"Allowed: min {self._min} max {self._max} step {self._step}"

62
pyhon/rules.py Normal file
View File

@ -0,0 +1,62 @@
from dataclasses import dataclass
from typing import List, Dict, TYPE_CHECKING
from pyhon.parameter.enum import HonParameterEnum
from pyhon.parameter.range import HonParameterRange
if TYPE_CHECKING:
from pyhon.commands import HonCommand
@dataclass
class HonRule:
trigger_key: str
trigger_value: str
param_key: str
param_value: str
class HonRuleSet:
def __init__(self, command: "HonCommand", rule):
self._command: "HonCommand" = command
self._rules: Dict[str, List[HonRule]] = {}
self._parse_rule(rule)
def _parse_rule(self, rule):
for entity_key, params in rule.items():
entity_key = self._command.appliance.options.get(entity_key, entity_key)
for trigger_key, values in params.items():
trigger_key = trigger_key.replace("@", "")
trigger_key = self._command.appliance.options.get(
trigger_key, trigger_key
)
for trigger_value, entity_value in values.items():
if entity_value.get("fixedValue") == f"@{entity_key}":
continue
self._rules.setdefault(trigger_key, []).append(
HonRule(
trigger_key,
trigger_value,
entity_key,
entity_value.get("fixedValue"),
)
)
def patch(self):
for name, parameter in self._command.parameters.items():
if name not in self._rules:
continue
for data in self._rules.get(name):
def apply(rule):
if param := self._command.parameters.get(rule.param_key):
if isinstance(param, HonParameterEnum) and set(
param.values
) != {str(rule.param_value)}:
param.values = [str(rule.param_value)]
elif isinstance(param, HonParameterRange):
param.value = float(rule.param_value)
return
param.value = str(rule.param_value)
parameter.add_trigger(data.trigger_value, apply, data)

View File

@ -7,7 +7,7 @@ with open("README.md", "r") as f:
setup( setup(
name="pyhOn", name="pyhOn",
version="0.10.10", version="0.11.3",
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,