Compare commits

...

17 Commits

13 changed files with 121 additions and 53 deletions

View File

@ -34,7 +34,7 @@ jobs:
mypy pyhon/ mypy pyhon/
- name: Analysing the code with pylint - name: Analysing the code with pylint
run: | run: |
pylint $(git ls-files '*.py') pylint $(git ls-files '*.py')
- name: Check black style - name: Check black style
run: | run: |
black . --check black . --check

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
include pyhon/py.typed

View File

@ -1,9 +1,24 @@
[mypy] [mypy]
check_untyped_defs = True check_untyped_defs = true
disallow_any_generics = True disallow_any_generics = true
disallow_untyped_defs = True disallow_any_unimported = true
disallow_any_unimported = True disallow_incomplete_defs = true
no_implicit_optional = True disallow_subclassing_any = true
warn_return_any = True disallow_untyped_calls = true
show_error_codes = True disallow_untyped_decorators = true
warn_unused_ignores = True disallow_untyped_defs = true
disable_error_code = annotation-unchecked
enable_error_code = ignore-without-code, redundant-self, truthy-iterable
follow_imports = silent
local_partial_types = true
no_implicit_optional = true
no_implicit_reexport = true
show_error_codes = true
strict_concatenate = false
strict_equality = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
warn_unused_ignores = true

View File

@ -3,7 +3,7 @@ import logging
import re import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any, TYPE_CHECKING, List from typing import Optional, Dict, Any, TYPE_CHECKING, List, TypeVar, overload
from pyhon import diagnose, exceptions from pyhon import diagnose, exceptions
from pyhon.appliances.base import ApplianceBase from pyhon.appliances.base import ApplianceBase
@ -20,6 +20,8 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
T = TypeVar("T")
# pylint: disable=too-many-public-methods,too-many-instance-attributes # pylint: disable=too-many-public-methods,too-many-instance-attributes
class HonAppliance: class HonAppliance:
@ -49,24 +51,35 @@ class HonAppliance:
except ModuleNotFoundError: except ModuleNotFoundError:
self._extra = None self._extra = None
def _get_nested_item(self, item: str) -> Any:
result: List[Any] | Dict[str, Any] = self.data
for key in item.split("."):
if all(k in "0123456789" for k in key) and isinstance(result, list):
result = result[int(key)]
elif isinstance(result, dict):
result = result[key]
return result
def __getitem__(self, item: str) -> Any: def __getitem__(self, item: str) -> Any:
if self._zone: if self._zone:
item += f"Z{self._zone}" item += f"Z{self._zone}"
if "." in item: if "." in item:
result = self.data return self._get_nested_item(item)
for key in item.split("."):
if all(k in "0123456789" for k in key) and isinstance(result, list):
result = result[int(key)]
else:
result = result[key]
return result
if item in self.data: if item in self.data:
return self.data[item] return self.data[item]
if item in self.attributes["parameters"]: if item in self.attributes["parameters"]:
return self.attributes["parameters"][item].value return self.attributes["parameters"][item].value
return self.info[item] return self.info[item]
def get(self, item: str, default: Any = None) -> Any: @overload
def get(self, item: str, default: None = None) -> Any:
...
@overload
def get(self, item: str, default: T) -> T:
...
def get(self, item: str, default: Optional[T] = None) -> Any:
try: try:
return self[item] return self[item]
except (KeyError, IndexError): except (KeyError, IndexError):
@ -250,7 +263,9 @@ class HonAppliance:
if not (command := self.commands.get(command_name)): if not (command := self.commands.get(command_name)):
return return
for key in command.setting_keys: for key in command.setting_keys:
if (new := self.attributes.get("parameters", {}).get(key)) is None: if (
new := self.attributes.get("parameters", {}).get(key)
) is None or new.value == "":
continue continue
setting = command.settings[key] setting = command.settings[key]
try: try:

View File

@ -10,12 +10,12 @@ class Appliance(ApplianceBase):
data["modeZ1"] = "holiday" data["modeZ1"] = "holiday"
elif data["parameters"]["intelligenceMode"] == "1": elif data["parameters"]["intelligenceMode"] == "1":
data["modeZ1"] = "auto_set" data["modeZ1"] = "auto_set"
elif data["parameters"]["quickModeZ1"] == "1": elif data["parameters"].get("quickModeZ1") == "1":
data["modeZ1"] = "super_cool" data["modeZ1"] = "super_cool"
else: else:
data["modeZ1"] = "no_mode" data["modeZ1"] = "no_mode"
if data["parameters"]["quickModeZ2"] == "1": if data["parameters"].get("quickModeZ2") == "1":
data["modeZ2"] = "super_freeze" data["modeZ2"] = "super_freeze"
elif data["parameters"]["intelligenceMode"] == "1": elif data["parameters"]["intelligenceMode"] == "1":
data["modeZ2"] = "auto_set" data["modeZ2"] = "auto_set"

View File

@ -184,22 +184,44 @@ class HonCommandLoader:
def _add_favourites(self) -> None: def _add_favourites(self) -> None:
"""Patch program categories with favourites""" """Patch program categories with favourites"""
for favourite in self._favourites: for favourite in self._favourites:
name = favourite.get("favouriteName", {}) name, command_name, base = self._get_favourite_info(favourite)
command = favourite.get("command", {}) if not base:
command_name = command.get("commandName", "") continue
program_name = self._clean_name(command.get("programName", "")) base_command: HonCommand = copy(base)
base: HonCommand = copy( self._update_base_command_with_data(base_command, favourite)
self.commands[command_name].categories[program_name] self._update_base_command_with_favourite(base_command)
) self._update_program_categories(command_name, name, base_command)
for data in command.values():
if isinstance(data, str): def _get_favourite_info(
self, favourite: Dict[str, Any]
) -> tuple[str, str, HonCommand | None]:
name: str = favourite.get("favouriteName", {})
command = favourite.get("command", {})
command_name: str = command.get("commandName", "")
program_name = self._clean_name(command.get("programName", ""))
base_command = self.commands[command_name].categories.get(program_name)
return name, command_name, base_command
def _update_base_command_with_data(
self, base_command: HonCommand, command: Dict[str, Any]
) -> None:
for data in command.values():
if isinstance(data, str):
continue
for key, value in data.items():
if not (parameter := base_command.parameters.get(key)):
continue continue
for key, value in data.items(): with suppress(ValueError):
if parameter := base.parameters.get(key): parameter.value = value
with suppress(ValueError):
parameter.value = value def _update_base_command_with_favourite(self, base_command: HonCommand) -> None:
extra_param = HonParameterFixed("favourite", {"fixedValue": "1"}, "custom") extra_param = HonParameterFixed("favourite", {"fixedValue": "1"}, "custom")
base.parameters.update(favourite=extra_param) base_command.parameters.update(favourite=extra_param)
if isinstance(program := base.parameters["program"], HonParameterProgram):
program.set_value(name) def _update_program_categories(
self.commands[command_name].categories[name] = base self, command_name: str, name: str, base_command: HonCommand
) -> None:
program = base_command.parameters["program"]
if isinstance(program, HonParameterProgram):
program.set_value(name)
self.commands[command_name].categories[name] = base_command

View File

@ -102,10 +102,12 @@ class HonCommand:
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 data.get("category") == "rule":
if "fixedValue" not in data: if "fixedValue" in data:
_LOGGER.error("Rule not supported: %s", data)
else:
self._rules.append(HonRuleSet(self, data["fixedValue"])) self._rules.append(HonRuleSet(self, data["fixedValue"]))
elif "enumValues" in data:
self._rules.append(HonRuleSet(self, data["enumValues"]))
else:
_LOGGER.warning("Rule not supported: %s", data)
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

@ -278,10 +278,12 @@ class TestAPI(HonAPI):
async def load_appliances(self) -> List[Dict[str, Any]]: async def load_appliances(self) -> List[Dict[str, Any]]:
result = [] result = []
for appliance in self._path.glob("*/"): for appliance in self._path.glob("*/"):
with open( file = appliance / "appliance_data.json"
appliance / "appliance_data.json", "r", encoding="utf-8" with open(file, "r", encoding="utf-8") as json_file:
) as json_file: try:
result.append(json.loads(json_file.read())) result.append(json.loads(json_file.read()))
except json.decoder.JSONDecodeError as error:
_LOGGER.error("%s - %s", str(file), error)
return result return result
async def load_commands(self, appliance: HonAppliance) -> Dict[str, Any]: async def load_commands(self, appliance: HonAppliance) -> Dict[str, Any]:
@ -318,4 +320,5 @@ class TestAPI(HonAPI):
parameters: Dict[str, Any], parameters: Dict[str, Any],
ancillary_parameters: Dict[str, Any], ancillary_parameters: Dict[str, Any],
) -> bool: ) -> bool:
_LOGGER.info("%s - %s", str(parameters), str(ancillary_parameters))
return True return True

View File

@ -68,8 +68,9 @@ class HonParameter:
self._triggers.setdefault(value, []).append((func, data)) self._triggers.setdefault(value, []).append((func, data))
def check_trigger(self, value: str | float) -> None: def check_trigger(self, value: str | float) -> None:
if str(value) in self._triggers: triggers = {str(k).lower(): v for k, v in self._triggers.items()}
for trigger in self._triggers[str(value)]: if str(value).lower() in triggers:
for trigger in triggers[str(value)]:
func, args = trigger func, args = trigger
func(args) func(args)

0
pyhon/py.typed Normal file
View File

View File

@ -56,6 +56,11 @@ class HonRuleSet:
extra[trigger_key] = trigger_value extra[trigger_key] = trigger_value
for extra_key, extra_data in param_data.items(): for extra_key, extra_data in param_data.items():
self._parse_conditions(param_key, extra_key, extra_data, extra) self._parse_conditions(param_key, extra_key, extra_data, extra)
else:
param_data = {"typology": "fixed", "fixedValue": param_data}
self._create_rule(
param_key, trigger_key, trigger_value, param_data, extra
)
def _create_rule( def _create_rule(
self, self,
@ -102,6 +107,10 @@ class HonRuleSet:
param.values = [str(value)] param.values = [str(value)]
param.value = str(value) param.value = str(value)
elif isinstance(param, HonParameterRange): elif isinstance(param, HonParameterRange):
if float(value) < param.min:
param.min = float(value)
elif float(value) > param.max:
param.max = float(value)
param.value = float(value) param.value = float(value)
return return
param.value = str(value) param.value = str(value)

View File

@ -1,3 +1,3 @@
aiohttp==3.8.4 aiohttp~=3.8
yarl==1.8.2 yarl~=1.9
typing-extensions==4.7.1 typing-extensions~=4.7

View File

@ -7,7 +7,7 @@ with open("README.md", "r", encoding="utf-8") as f:
setup( setup(
name="pyhOn", name="pyhOn",
version="0.15.0", version="0.15.8",
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,
@ -21,7 +21,7 @@ setup(
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
python_requires=">=3.10", python_requires=">=3.10",
install_requires=["aiohttp==3.8.4", "typing-extensions==4.7.1"], install_requires=["aiohttp~=3.8", "typing-extensions~=4.7", "yarl~=1.9"],
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Environment :: Console", "Environment :: Console",