Compare commits

...

19 Commits

Author SHA1 Message Date
715d8a96fc Set versions of dependant packages to 'compatible releases' 2023-07-27 19:14:19 +02:00
ec358034e1 Bump version 2023-07-22 11:48:47 +02:00
6cdac99ce8 Not loading favorite if base program renamed 2023-07-22 11:48:40 +02:00
0c1670733f Update requirements 2023-07-22 11:24:27 +02:00
744094dc07 Bump version 2023-07-19 19:55:10 +02:00
bf2015fcb9 Fix empty value in settings 2023-07-19 19:52:53 +02:00
bc7e8994c9 Bump version 2023-07-11 00:21:37 +02:00
8ca40d7ad0 Add missing requirement typing-extensions 2023-07-10 23:59:55 +02:00
9a6a07fd46 Sync enum values of commands 2023-07-10 23:58:24 +02:00
f1818bbc5d Bump version 2023-07-09 23:59:27 +02:00
3d5c8405ea Improve error handling 2023-07-09 23:58:55 +02:00
e234ef3bbb Remove old code in ov hon#88 2023-07-09 01:36:03 +02:00
e00e147ecd Bump version 2023-07-01 16:29:29 +02:00
26bc35c8a6 Fix issue in locking attribute updates 2023-07-01 16:27:50 +02:00
17d73cdeb8 Sync parameter to settings 2023-07-01 16:04:34 +02:00
a10ab4423e Add stricter mypy rules 2023-07-01 14:59:09 +02:00
0553e6c17d Improvements 2023-07-01 14:31:37 +02:00
44f40c531e Fix some minor issues 2023-06-29 22:08:51 +02:00
4e88bc7a9f Fix error in archive again 2023-06-29 18:49:33 +02:00
17 changed files with 133 additions and 79 deletions

View File

@ -2,3 +2,8 @@
check_untyped_defs = True check_untyped_defs = True
disallow_any_generics = True disallow_any_generics = True
disallow_untyped_defs = True disallow_untyped_defs = True
disallow_any_unimported = True
no_implicit_optional = True
warn_return_any = True
show_error_codes = True
warn_unused_ignores = True

View File

@ -99,7 +99,7 @@ async def main() -> None:
print(printer.key_print(data)) print(printer.key_print(data))
print( print(
printer.pretty_print( printer.pretty_print(
printer.create_command(device.commands, concat=True) printer.create_commands(device.commands, concat=True)
) )
) )
else: else:

View File

@ -6,10 +6,12 @@ from pathlib import Path
from typing import Optional, Dict, Any, TYPE_CHECKING, List from typing import Optional, Dict, Any, TYPE_CHECKING, List
from pyhon import diagnose, exceptions from pyhon import diagnose, exceptions
from pyhon.appliances.base import ApplianceBase
from pyhon.attributes import HonAttribute from pyhon.attributes import HonAttribute
from pyhon.command_loader import HonCommandLoader from pyhon.command_loader import HonCommandLoader
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.enum import HonParameterEnum
from pyhon.parameter.range import HonParameterRange from pyhon.parameter.range import HonParameterRange
from pyhon.typedefs import Parameter from pyhon.typedefs import Parameter
@ -40,7 +42,7 @@ class HonAppliance:
self._default_setting = HonParameter("", {}, "") self._default_setting = HonParameter("", {}, "")
try: try:
self._extra = importlib.import_module( self._extra: Optional[ApplianceBase] = importlib.import_module(
f"pyhon.appliances.{self.appliance_type.lower()}" f"pyhon.appliances.{self.appliance_type.lower()}"
).Appliance(self) ).Appliance(self)
except ModuleNotFoundError: except ModuleNotFoundError:
@ -71,7 +73,8 @@ class HonAppliance:
def _check_name_zone(self, name: str, frontend: bool = True) -> str: def _check_name_zone(self, name: str, frontend: bool = True) -> str:
zone = " Z" if frontend else "_z" zone = " Z" if frontend else "_z"
if (attribute := self._info.get(name, "")) and self._zone: attribute: str = self._info.get(name, "")
if attribute and self._zone:
return f"{attribute}{zone}{self._zone}" return f"{attribute}{zone}{self._zone}"
return attribute return attribute
@ -101,20 +104,22 @@ class HonAppliance:
@property @property
def brand(self) -> str: def brand(self) -> str:
return self._check_name_zone("brand") brand = self._check_name_zone("brand")
return brand[0].upper() + brand[1:]
@property @property
def nick_name(self) -> str: def nick_name(self) -> str:
result = self._check_name_zone("nickName") result = self._check_name_zone("nickName")
if not result or re.findall("^[xX\s]+$", result): if not result or re.findall("^[xX1\\s-]+$", result):
return self.model_name return self.model_name
return result return result
@property @property
def code(self) -> str: def code(self) -> str:
if code := self.info.get("code"): code: str = self.info.get("code", "")
if code:
return code return code
serial_number = self.info.get("serialNumber", "") serial_number: str = self.info.get("serialNumber", "")
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
@ -162,16 +167,18 @@ class HonAppliance:
self._commands = command_loader.commands self._commands = command_loader.commands
self._additional_data = command_loader.additional_data self._additional_data = command_loader.additional_data
self._appliance_model = command_loader.appliance_data self._appliance_model = command_loader.appliance_data
self.sync_params_to_command("settings")
async def load_attributes(self) -> None: async def load_attributes(self) -> None:
self._attributes = await self.api.load_attributes(self) attributes = await self.api.load_attributes(self)
for name, values in self._attributes.pop("shadow").get("parameters").items(): for name, values in attributes.pop("shadow", {}).get("parameters", {}).items():
if name in self._attributes.get("parameters", {}): if name in self._attributes.get("parameters", {}):
self._attributes["parameters"][name].update(values) self._attributes["parameters"][name].update(values)
else: else:
self._attributes.setdefault("parameters", {})[name] = HonAttribute( self._attributes.setdefault("parameters", {})[name] = HonAttribute(
values values
) )
self._attributes |= attributes
if self._extra: if self._extra:
self._attributes = self._extra.attributes(self._attributes) self._attributes = self._extra.attributes(self._attributes)
@ -189,6 +196,7 @@ class HonAppliance:
): ):
self._last_update = now self._last_update = now
await self.load_attributes() await self.load_attributes()
self.sync_params_to_command("settings")
@property @property
def command_parameters(self) -> Dict[str, Dict[str, str | float]]: def command_parameters(self) -> Dict[str, Dict[str, str | float]]:
@ -196,7 +204,7 @@ class HonAppliance:
@property @property
def settings(self) -> Dict[str, Parameter]: def settings(self) -> Dict[str, Parameter]:
result = {} result: Dict[str, Parameter] = {}
for name, command in self._commands.items(): for name, command in self._commands.items():
for key in command.setting_keys: for key in command.setting_keys:
setting = command.settings.get(key, self._default_setting) setting = command.settings.get(key, self._default_setting)
@ -232,16 +240,34 @@ class HonAppliance:
async def data_archive(self, path: Path) -> str: async def data_archive(self, path: Path) -> str:
return await diagnose.zip_archive(self, path, anonymous=True) return await diagnose.zip_archive(self, path, anonymous=True)
def sync_to_params(self, command_name: str) -> None: def sync_command_to_params(self, command_name: str) -> None:
if not (command := self.commands.get(command_name)): if not (command := self.commands.get(command_name)):
return return
for key, value in self.attributes.get("parameters", {}).items(): for key in self.attributes.get("parameters", {}):
if isinstance(value, str) and (new := command.parameters.get(key)): if new := command.parameters.get(key):
self.attributes["parameters"][key].update( self.attributes["parameters"][key].update(
str(new.intern_value), shield=True str(new.intern_value), shield=True
) )
def sync_command(self, main: str, target: Optional[List[str]] = None) -> None: def sync_params_to_command(self, command_name: str) -> None:
if not (command := self.commands.get(command_name)):
return
for key in command.setting_keys:
if (
new := self.attributes.get("parameters", {}).get(key)
) is None or new.value == "":
continue
setting = command.settings[key]
try:
if not isinstance(setting, HonParameterRange):
command.settings[key].value = str(new.value)
else:
command.settings[key].value = float(new.value)
except ValueError as error:
_LOGGER.info("Can't set %s - %s", key, error)
continue
def sync_command(self, main: str, target: Optional[List[str] | str] = None) -> None:
base: Optional[HonCommand] = self.commands.get(main) base: Optional[HonCommand] = self.commands.get(main)
if not base: if not base:
return return
@ -260,4 +286,6 @@ class HonAppliance:
parameter.max = int(base_value.value) parameter.max = int(base_value.value)
parameter.min = int(base_value.value) parameter.min = int(base_value.value)
parameter.step = 1 parameter.step = 1
elif isinstance(parameter, HonParameterEnum):
parameter.values = base_value.values
parameter.value = base_value.value parameter.value = base_value.value

View File

@ -14,11 +14,4 @@ class Appliance(ApplianceBase):
data["parameters"]["remainingTimeMM"].value = "0" data["parameters"]["remainingTimeMM"].value = "0"
data["active"] = data["parameters"]["onOffStatus"] == "1" data["active"] = data["parameters"]["onOffStatus"] == "1"
if program := int(data["parameters"]["prCode"]):
if (setting := self.parent.settings["startProgram.program"]) and isinstance(
setting, HonParameterProgram
):
data["programName"] = setting.ids.get(program, "")
return data return data

View File

@ -55,7 +55,7 @@ class HonCommandLoader:
async def load_commands(self) -> None: async def load_commands(self) -> None:
"""Trigger loading of command data""" """Trigger loading of command data"""
await self._load_data() await self._load_data()
self._appliance_data = self._api_commands.pop("applianceModel") self._appliance_data = self._api_commands.pop("applianceModel", {})
self._get_commands() self._get_commands()
self._add_favourites() self._add_favourites()
self._recover_last_command_states() self._recover_last_command_states()
@ -187,18 +187,19 @@ class HonCommandLoader:
command = favourite.get("command", {}) command = favourite.get("command", {})
command_name = command.get("commandName", "") command_name = command.get("commandName", "")
program_name = self._clean_name(command.get("programName", "")) program_name = self._clean_name(command.get("programName", ""))
base: HonCommand = copy( if not (base := self.commands[command_name].categories.get(program_name)):
self.commands[command_name].categories[program_name] continue
) base_command: HonCommand = copy(base)
for data in command.values(): for data in command.values():
if isinstance(data, str): if isinstance(data, str):
continue continue
for key, value in data.items(): for key, value in data.items():
if parameter := base.parameters.get(key): if parameter := base_command.parameters.get(key):
with suppress(ValueError): with suppress(ValueError):
parameter.value = value parameter.value = value
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 = base_command.parameters["program"]
if isinstance(program, HonParameterProgram):
program.set_value(name) program.set_value(name)
self.commands[command_name].categories[name] = base self.commands[command_name].categories[name] = base_command

View File

@ -115,7 +115,7 @@ class HonCommand:
params = self.parameter_groups.get("parameters", {}) params = self.parameter_groups.get("parameters", {})
ancillary_params = self.parameter_groups.get("ancillaryParameters", {}) ancillary_params = self.parameter_groups.get("ancillaryParameters", {})
ancillary_params.pop("programRules", None) ancillary_params.pop("programRules", None)
self.appliance.sync_to_params(self.name) self.appliance.sync_command_to_params(self.name)
try: try:
result = await self.api.send_command( result = await self.api.send_command(
self._appliance, self._name, params, ancillary_params self._appliance, self._name, params, ancillary_params

View File

@ -75,8 +75,12 @@ class HonAPI:
async def load_appliances(self) -> List[Dict[str, Any]]: async def load_appliances(self) -> List[Dict[str, Any]]:
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:
if result := await resp.json(): result = await resp.json()
return result.get("payload", {}).get("appliances", {}) if result:
appliances: List[Dict[str, Any]] = result.get("payload", {}).get(
"appliances", {}
)
return appliances
return [] return []
async def load_commands(self, appliance: HonAppliance) -> Dict[str, Any]: async def load_commands(self, appliance: HonAppliance) -> Dict[str, Any]:
@ -112,7 +116,8 @@ class HonAPI:
result: Dict[str, Any] = await response.json() result: Dict[str, Any] = 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"] command_history: List[Dict[str, Any]] = result["payload"]["history"]
return command_history
async def load_favourites(self, appliance: HonAppliance) -> List[Dict[str, Any]]: async def load_favourites(self, appliance: HonAppliance) -> List[Dict[str, Any]]:
url: str = ( url: str = (
@ -122,14 +127,17 @@ class HonAPI:
result: Dict[str, Any] = await response.json() result: Dict[str, Any] = await response.json()
if not result or not result.get("payload"): if not result or not result.get("payload"):
return [] return []
return result["payload"]["favourites"] favourites: List[Dict[str, Any]] = result["payload"]["favourites"]
return favourites
async def load_last_activity(self, appliance: HonAppliance) -> Dict[str, Any]: async def load_last_activity(self, appliance: HonAppliance) -> Dict[str, Any]:
url: str = f"{const.API_URL}/commands/v1/retrieve-last-activity" url: str = f"{const.API_URL}/commands/v1/retrieve-last-activity"
params: Dict[str, str] = {"macAddress": appliance.mac_address} params: Dict[str, str] = {"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: Dict[str, Any] = await response.json() result: Dict[str, Any] = await response.json()
if result and (activity := result.get("attributes")): if result:
activity: Dict[str, Any] = result.get("attributes", "")
if activity:
return activity return activity
return {} return {}
@ -142,7 +150,10 @@ class HonAPI:
async with self._hon.get(url, params=params) as response: async with self._hon.get(url, params=params) as response:
result: Dict[str, Any] = await response.json() result: Dict[str, Any] = await response.json()
if result: if result:
return result.get("payload", {}).get("applianceModel", {}) appliance_data: Dict[str, Any] = result.get("payload", {}).get(
"applianceModel", {}
)
return appliance_data
return {} return {}
async def load_attributes(self, appliance: HonAppliance) -> Dict[str, Any]: async def load_attributes(self, appliance: HonAppliance) -> Dict[str, Any]:
@ -153,7 +164,8 @@ class HonAPI:
} }
url: str = f"{const.API_URL}/commands/v1/context" url: str = 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", {}) attributes: Dict[str, Any] = (await response.json()).get("payload", {})
return attributes
async def load_statistics(self, appliance: HonAppliance) -> Dict[str, Any]: async def load_statistics(self, appliance: HonAppliance) -> Dict[str, Any]:
params: Dict[str, str] = { params: Dict[str, str] = {
@ -162,13 +174,15 @@ class HonAPI:
} }
url: str = f"{const.API_URL}/commands/v1/statistics" url: str = 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", {}) statistics: Dict[str, Any] = (await response.json()).get("payload", {})
return statistics
async def load_maintenance(self, appliance: HonAppliance) -> Dict[str, Any]: async def load_maintenance(self, appliance: HonAppliance) -> Dict[str, Any]:
url = f"{const.API_URL}/commands/v1/maintenance-cycle" url = f"{const.API_URL}/commands/v1/maintenance-cycle"
params = {"macAddress": appliance.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:
return (await response.json()).get("payload", {}) maintenance: Dict[str, Any] = (await response.json()).get("payload", {})
return maintenance
async def send_command( async def send_command(
self, self,
@ -207,9 +221,8 @@ class HonAPI:
url: str = f"{const.API_URL}/config/v1/program-list-rules" url: str = f"{const.API_URL}/config/v1/program-list-rules"
async with self._hon_anonymous.get(url) as response: async with self._hon_anonymous.get(url) as response:
result: Dict[str, Any] = await response.json() result: Dict[str, Any] = await response.json()
if result and (data := result.get("payload")): data: Dict[str, Any] = result.get("payload", {})
return data return data
return {}
async def app_config( async def app_config(
self, language: str = "en", beta: bool = True self, language: str = "en", beta: bool = True
@ -223,17 +236,17 @@ class HonAPI:
} }
payload: str = json.dumps(payload_data, separators=(",", ":")) payload: str = json.dumps(payload_data, separators=(",", ":"))
async with self._hon_anonymous.post(url, data=payload) as response: async with self._hon_anonymous.post(url, data=payload) as response:
if (result := await response.json()) and (data := result.get("payload")): result = await response.json()
data: Dict[str, Any] = result.get("payload", {})
return data return data
return {}
async def translation_keys(self, language: str = "en") -> Dict[str, Any]: async def translation_keys(self, language: str = "en") -> Dict[str, Any]:
config = await self.app_config(language=language) config = await self.app_config(language=language)
if url := config.get("language", {}).get("jsonPath"): if not (url := config.get("language", {}).get("jsonPath")):
async with self._hon_anonymous.get(url) as response:
if result := await response.json():
return result
return {} return {}
async with self._hon_anonymous.get(url) as response:
result: Dict[str, Any] = await response.json()
return result
async def close(self) -> None: async def close(self) -> None:
if self._hon_handler is not None: if self._hon_handler is not None:
@ -250,9 +263,17 @@ class TestAPI(HonAPI):
def _load_json(self, appliance: HonAppliance, file: str) -> Dict[str, Any]: def _load_json(self, appliance: HonAppliance, file: str) -> Dict[str, Any]:
directory = f"{appliance.appliance_type}_{appliance.appliance_model_id}".lower() directory = f"{appliance.appliance_type}_{appliance.appliance_model_id}".lower()
path = f"{self._path}/{directory}/{file}.json" if not (path := self._path / directory / f"{file}.json").exists():
_LOGGER.warning("Can't open %s", str(path))
return {}
with open(path, "r", encoding="utf-8") as json_file: with open(path, "r", encoding="utf-8") as json_file:
return json.loads(json_file.read()) text = json_file.read()
try:
data: Dict[str, Any] = json.loads(text)
return data
except json.decoder.JSONDecodeError as error:
_LOGGER.error("%s - %s", str(path), error)
return {}
async def load_appliances(self) -> List[Dict[str, Any]]: async def load_appliances(self) -> List[Dict[str, Any]]:
result = [] result = []

View File

@ -6,7 +6,7 @@ import urllib
from contextlib import suppress from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, Optional, Any from typing import Dict, Optional, Any, List
from urllib import parse from urllib import parse
from urllib.parse import quote from urllib.parse import quote
@ -115,7 +115,8 @@ class HonAuth:
async with self._request.get(url) as response: async with self._request.get(url) as response:
text = await response.text() text = await response.text()
self._expires = datetime.utcnow() self._expires = datetime.utcnow()
if not (login_url := re.findall("url = '(.+?)'", text)): login_url: List[str] = re.findall("url = '(.+?)'", text)
if not login_url:
if "oauth/done#access_token=" in text: if "oauth/done#access_token=" in text:
self._parse_token_data(text) self._parse_token_data(text)
raise exceptions.HonNoAuthenticationNeeded() raise exceptions.HonNoAuthenticationNeeded()
@ -184,7 +185,8 @@ class HonAuth:
if response.status == 200: if response.status == 200:
with suppress(json.JSONDecodeError, KeyError): with suppress(json.JSONDecodeError, KeyError):
result = await response.json() result = await response.json()
return result["events"][0]["attributes"]["values"]["url"] url: str = result["events"][0]["attributes"]["values"]["url"]
return url
await self._error_logger(response) await self._error_logger(response)
return "" return ""

View File

@ -57,7 +57,7 @@ class HonConnectionHandler(ConnectionHandler):
async def _intercept( async def _intercept(
self, method: Callback, url: str | URL, *args: Any, **kwargs: Any self, method: Callback, url: str | URL, *args: Any, **kwargs: Any
) -> AsyncIterator[aiohttp.ClientResponse]: ) -> AsyncIterator[aiohttp.ClientResponse]:
loop: int = kwargs.get("loop", 0) loop: int = kwargs.pop("loop", 0)
kwargs["headers"] = await self._check_headers(kwargs.get("headers", {})) kwargs["headers"] = await self._check_headers(kwargs.get("headers", {}))
async with method(url, *args, **kwargs) as response: async with method(url, *args, **kwargs) as response:
if ( if (

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 = "2.0.10" APP_VERSION = "2.1.2"
OS_VERSION = 31 OS_VERSION = 31
OS = "android" OS = "android"
DEVICE_MODEL = "exynos9820" DEVICE_MODEL = "exynos9820"

View File

@ -70,7 +70,7 @@ async def zip_archive(
) -> str: ) -> str:
data = await appliance_data(appliance, path, anonymous) data = await appliance_data(appliance, path, anonymous)
archive = data[0].parent archive = data[0].parent
shutil.make_archive(str(archive.parent), "zip", archive) shutil.make_archive(str(archive), "zip", archive)
shutil.rmtree(archive) shutil.rmtree(archive)
return f"{archive.stem}.zip" return f"{archive.stem}.zip"
@ -89,12 +89,11 @@ def yaml_export(appliance: "HonAppliance", anonymous: bool = False) -> str:
if anonymous: if anonymous:
for sensible in ["serialNumber", "coords"]: for sensible in ["serialNumber", "coords"]:
data.get("appliance", {}).pop(sensible, None) data.get("appliance", {}).pop(sensible, None)
data = { result = printer.pretty_print({"data": data})
"data": data, if commands := printer.create_commands(appliance.commands):
"commands": printer.create_command(appliance.commands), result += printer.pretty_print({"commands": commands})
"rules": printer.create_rules(appliance.commands), if rules := printer.create_rules(appliance.commands):
} result += printer.pretty_print({"rules": rules})
result = printer.pretty_print(data)
if anonymous: if anonymous:
result = anonymize_data(result) result = anonymize_data(result)
return result return result

View File

@ -41,4 +41,4 @@ class HonParameterEnum(HonParameter):
self._value = value self._value = value
self.check_trigger(value) self.check_trigger(value)
else: else:
raise ValueError(f"Allowed values {self._values}") raise ValueError(f"Allowed values: {self._values} But was: {value}")

View File

@ -28,7 +28,7 @@ class HonParameterProgram(HonParameterEnum):
if value in self.values: if value in self.values:
self._command.category = value self._command.category = value
else: else:
raise ValueError(f"Allowed values {self.values}") raise ValueError(f"Allowed values: {self.values} But was: {value}")
@property @property
def values(self) -> List[str]: def values(self) -> List[str]:

View File

@ -49,11 +49,15 @@ class HonParameterRange(HonParameter):
@value.setter @value.setter
def value(self, value: str | 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) * 100) % (
self.step * 100
):
self._value = value self._value = value
self.check_trigger(value) self.check_trigger(value)
else: else:
raise ValueError(f"Allowed: min {self.min} max {self.max} step {self.step}") raise ValueError(
f"Allowed: min {self.min} max {self.max} step {self.step} But was: {value}"
)
@property @property
def values(self) -> List[str]: def values(self) -> List[str]:

View File

@ -59,7 +59,7 @@ def pretty_print(
return result return result
def create_command( def create_commands(
commands: Dict[str, "HonCommand"], concat: bool = False commands: Dict[str, "HonCommand"], concat: bool = False
) -> Dict[str, Any]: ) -> Dict[str, Any]:
result: Dict[str, Any] = {} result: Dict[str, Any] = {}

View File

@ -1,2 +1,3 @@
aiohttp==3.8.4 aiohttp~=3.8.5
yarl==1.8.2 yarl~=1.9.2
typing-extensions~=4.7.1

View File

@ -7,7 +7,7 @@ with open("README.md", "r") as f:
setup( setup(
name="pyhOn", name="pyhOn",
version="0.14.4", version="0.14.12",
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"], install_requires=["aiohttp~=3.8.5", "typing-extensions~=4.7.1", "yarl~=1.9.2"],
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Environment :: Console", "Environment :: Console",