diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f85709..770fa81 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ default_stages: [ commit ] repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: debug-statements - id: check-builtin-literals @@ -18,35 +18,35 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/python-poetry/poetry - rev: 1.7.0 + rev: 2.0.0 hooks: - id: poetry-check - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort - repo: https://github.com/psf/black - rev: 23.11.0 + rev: 24.10.0 hooks: - id: black - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.3.0 hooks: - id: codespell exclude: poetry.lock - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.1.6 + rev: v0.9.0 hooks: - id: ruff args: - --fix - repo: https://github.com/pylint-dev/pylint - rev: v3.2.3 + rev: v3.3.3 hooks: - id: pylint additional_dependencies: [ "pydantic>=1.10.17", "xmltodict" ] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.0 + rev: v1.14.1 hooks: - id: mypy exclude: cli.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 69043f8..43d710e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "--cov-report=xml:coverage.xml", "tests" ], + "mypy-type-checker.importStrategy": "fromEnvironment", "python.analysis.typeCheckingMode": "basic", "[python]": { "editor.defaultFormatter": "ms-python.black-formatter", diff --git a/poetry.lock b/poetry.lock index c52cde0..d7f238e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,9 +12,6 @@ files = [ {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} - [[package]] name = "cfgv" version = "3.4.0" @@ -112,9 +109,6 @@ files = [ {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, ] -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - [package.extras] toml = ["tomli"] @@ -146,22 +140,6 @@ files = [ {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "filelock" version = "3.16.1" @@ -277,7 +255,6 @@ files = [ [package.dependencies] mypy_extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = ">=4.6.0" [package.extras] @@ -381,7 +358,7 @@ version = "1.10.20" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "pydantic-1.10.20-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:330d1f27b3e5a93e4b9bf5ceb6a4a4d58286b07be4ae67489413f51be300562f"}, {file = "pydantic-1.10.20-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7254ddf29252f373c1498e3f3eb7d99b030b01d714de2319e3bf70a18859f597"}, @@ -457,15 +434,10 @@ files = [ [package.dependencies] astroid = ">=3.3.8,<=3.4.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, -] +dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" [package.extras] @@ -486,11 +458,9 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -577,49 +547,6 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - [[package]] name = "tomlkit" version = "0.13.2" @@ -679,5 +606,5 @@ files = [ [metadata] lock-version = "2.1" -python-versions = "^3.10" -content-hash = "7f7c739bdc72823247d905d8597bb7b518c8d68fda747a440a437277917638ff" +python-versions = ">=3.12,<3.14" +content-hash = "1aee5fd77df7a7bb67586fae08094805fffb972de63445e2aa6a35e34d60922b" diff --git a/pyomnilogic_local/api.py b/pyomnilogic_local/api.py index f949c0b..9826820 100644 --- a/pyomnilogic_local/api.py +++ b/pyomnilogic_local/api.py @@ -1,22 +1,23 @@ +# pylint: disable=too-many-positional-arguments from __future__ import annotations import asyncio import logging -from typing import Literal, overload import xml.etree.ElementTree as ET +from typing import Literal, overload from .models.filter_diagnostics import FilterDiagnostics from .models.mspconfig import MSPConfig from .models.telemetry import Telemetry from .models.util import to_pydantic -from .protocol import OmniLogicProtocol -from .types import ( +from .omnitypes import ( ColorLogicBrightness, ColorLogicShow, ColorLogicSpeed, HeaterMode, MessageType, ) +from .protocol import OmniLogicProtocol _LOGGER = logging.getLogger(__name__) @@ -30,12 +31,10 @@ def __init__(self, controller_ip: str, controller_port: int, response_timeout: f self._protocol_factory = OmniLogicProtocol @overload - async def async_send_message(self, message_type: MessageType, message: str | None, need_response: Literal[True]) -> str: - ... + async def async_send_message(self, message_type: MessageType, message: str | None, need_response: Literal[True]) -> str: ... @overload - async def async_send_message(self, message_type: MessageType, message: str | None, need_response: Literal[False]) -> None: - ... + async def async_send_message(self, message_type: MessageType, message: str | None, need_response: Literal[False]) -> None: ... async def async_send_message(self, message_type: MessageType, message: str | None, need_response: bool = False) -> str | None: """Send a message via the Hayward Omni UDP protocol along with properly handling timeouts and responses. @@ -97,7 +96,11 @@ async def async_get_config(self) -> str: return await self.async_send_message(MessageType.REQUEST_CONFIGURATION, req_body, True) @to_pydantic(pydantic_type=FilterDiagnostics) - async def async_get_filter_diagnostics(self, pool_id: int, equipment_id: int) -> str: + async def async_get_filter_diagnostics( + self, + pool_id: int, + equipment_id: int, + ) -> str: """Retrieve filter diagnostics from the Omni, optionally parse it into a pydantic model. Args: @@ -146,7 +149,13 @@ async def async_get_telemetry(self) -> str: return await self.async_send_message(MessageType.GET_TELEMETRY, req_body, True) - async def async_set_heater(self, pool_id: int, equipment_id: int, temperature: int, unit: str) -> None: + async def async_set_heater( + self, + pool_id: int, + equipment_id: int, + temperature: int, + unit: str, + ) -> None: """Set the temperature for a heater on the Omni Args: @@ -175,7 +184,13 @@ async def async_set_heater(self, pool_id: int, equipment_id: int, temperature: i return await self.async_send_message(MessageType.SET_HEATER_COMMAND, req_body, False) - async def async_set_solar_heater(self, pool_id: int, equipment_id: int, temperature: int, unit: str) -> None: + async def async_set_solar_heater( + self, + pool_id: int, + equipment_id: int, + temperature: int, + unit: str, + ) -> None: """Set the solar set point for a heater on the Omni. Args: @@ -204,7 +219,12 @@ async def async_set_solar_heater(self, pool_id: int, equipment_id: int, temperat return await self.async_send_message(MessageType.SET_SOLAR_SET_POINT_COMMAND, req_body, False) - async def async_set_heater_mode(self, pool_id: int, equipment_id: int, mode: HeaterMode) -> None: + async def async_set_heater_mode( + self, + pool_id: int, + equipment_id: int, + mode: HeaterMode, + ) -> None: """Set what mode (Heat/Cool/Auto) the heater should use. Args: @@ -232,7 +252,12 @@ async def async_set_heater_mode(self, pool_id: int, equipment_id: int, mode: Hea return await self.async_send_message(MessageType.SET_HEATER_MODE_COMMAND, req_body, False) - async def async_set_heater_enable(self, pool_id: int, equipment_id: int, enabled: int | bool) -> None: + async def async_set_heater_enable( + self, + pool_id: int, + equipment_id: int, + enabled: int | bool, + ) -> None: """async_set_heater_enable handles sending a SetHeaterEnable XML API call to the Hayward Omni pool controller Args: @@ -472,7 +497,12 @@ async def async_set_chlorinator_params( return await self.async_send_message(MessageType.SET_CHLOR_PARAMS, req_body, False) - async def async_set_chlorinator_superchlorinate(self, pool_id: int, equipment_id: int, enabled: int | bool) -> None: + async def async_set_chlorinator_superchlorinate( + self, + pool_id: int, + equipment_id: int, + enabled: int | bool, + ) -> None: body_element = ET.Element("Request", {"xmlns": "http://nextgen.hayward.com/api"}) name_element = ET.SubElement(body_element, "Name") diff --git a/pyomnilogic_local/cli.py b/pyomnilogic_local/cli.py index c0b91b1..804209b 100755 --- a/pyomnilogic_local/cli.py +++ b/pyomnilogic_local/cli.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - import asyncio import logging import os diff --git a/pyomnilogic_local/models/filter_diagnostics.py b/pyomnilogic_local/models/filter_diagnostics.py index 6bd7c79..4920858 100644 --- a/pyomnilogic_local/models/filter_diagnostics.py +++ b/pyomnilogic_local/models/filter_diagnostics.py @@ -23,6 +23,7 @@ class Config: orm_mode = True def get_param_by_name(self, name: str) -> int: + # pylint: disable=not-an-iterable return [param.value for param in self.parameters if param.name == name][0] @staticmethod diff --git a/pyomnilogic_local/models/mspconfig.py b/pyomnilogic_local/models/mspconfig.py index 55696fc..e6a8167 100644 --- a/pyomnilogic_local/models/mspconfig.py +++ b/pyomnilogic_local/models/mspconfig.py @@ -13,7 +13,7 @@ from xmltodict import parse as xml_parse from ..exceptions import OmniParsingException -from ..types import ( +from ..omnitypes import ( BodyOfWaterType, ChlorinatorCellType, ChlorinatorDispenserType, @@ -219,7 +219,7 @@ class MSPBackyard(OmniBase): colorlogic_light: list[MSPColorLogicLight] | None = Field(alias="ColorLogic-Light") -class MSPSchedule(OmniBase): +class MSPSchedule(OmniBase): # type: ignore[override] omni_type: OmniType = OmniType.SCHEDULE system_id: int = Field(alias="schedule-system-id") bow_id: int = Field(alias="bow-system-id") diff --git a/pyomnilogic_local/models/telemetry.py b/pyomnilogic_local/models/telemetry.py index fe99f21..fe5c8f5 100644 --- a/pyomnilogic_local/models/telemetry.py +++ b/pyomnilogic_local/models/telemetry.py @@ -6,7 +6,7 @@ from xmltodict import parse as xml_parse from ..exceptions import OmniParsingException -from ..types import ( +from ..omnitypes import ( BackyardState, ChlorinatorOperatingMode, ColorLogicBrightness, @@ -71,7 +71,7 @@ class TelemetryChlorinator(BaseModel): sc_mode: int = Field(alias="@scMode") operating_state: int = Field(alias="@operatingState") timed_percent: int | None = Field(alias="@Timed-Percent") - operating_mode: ChlorinatorOperatingMode = Field(alias="@operatingMode") + operating_mode: ChlorinatorOperatingMode | int = Field(alias="@operatingMode") enable: bool = Field(alias="@enable") # Still need to do a bit more work to determine if a chlorinator is actively chlorinating @@ -200,12 +200,10 @@ def load_xml(xml: str) -> Telemetry: TypeVar("VT", SupportsInt, Any) @overload - def xml_postprocessor(path: Any, key: Any, value: SupportsInt) -> tuple[Any, SupportsInt]: - ... + def xml_postprocessor(path: Any, key: Any, value: SupportsInt) -> tuple[Any, SupportsInt]: ... @overload - def xml_postprocessor(path: Any, key: Any, value: Any) -> tuple[Any, Any]: - ... + def xml_postprocessor(path: Any, key: Any, value: Any) -> tuple[Any, Any]: ... def xml_postprocessor(path: Any, key: Any, value: SupportsInt | Any) -> tuple[Any, SupportsInt | Any]: """Post process XML to attempt to convert values to int. diff --git a/pyomnilogic_local/models/util.py b/pyomnilogic_local/models/util.py index 6b786d5..626534c 100644 --- a/pyomnilogic_local/models/util.py +++ b/pyomnilogic_local/models/util.py @@ -1,5 +1,5 @@ -from collections.abc import Awaitable, Callable import logging +from collections.abc import Awaitable, Callable from typing import Any, Literal, TypeVar, cast, overload from pydantic.v1.utils import GetterDict @@ -24,17 +24,17 @@ def get(self, key: str, default: Any = None) -> Any: TPydanticTypes = Telemetry | MSPConfig | FilterDiagnostics -def to_pydantic(pydantic_type: type[TPydanticTypes]) -> Callable[..., Any]: +def to_pydantic( + pydantic_type: type[TPydanticTypes], +) -> Callable[..., Any]: def inner(func: F, *args: Any, **kwargs: Any) -> F: """Wrap an API function that returns XML and parse it into a Pydantic model""" @overload - async def wrapper(*args: Any, raw: Literal[True], **kwargs: Any) -> str: - ... + async def wrapper(*args: Any, raw: Literal[True], **kwargs: Any) -> str: ... @overload - async def wrapper(*args: Any, raw: Literal[False], **kwargs: Any) -> TPydanticTypes: - ... + async def wrapper(*args: Any, raw: Literal[False], **kwargs: Any) -> TPydanticTypes: ... async def wrapper(*args: Any, raw: bool = False, **kwargs: Any) -> TPydanticTypes | str: resp_body = await func(*args, **kwargs) diff --git a/pyomnilogic_local/types.py b/pyomnilogic_local/omnitypes.py similarity index 97% rename from pyomnilogic_local/types.py rename to pyomnilogic_local/omnitypes.py index fedc35a..2cd23cf 100644 --- a/pyomnilogic_local/types.py +++ b/pyomnilogic_local/omnitypes.py @@ -87,14 +87,19 @@ class BodyOfWaterType(str, PrettyEnum): # Chlorinator status is a bitmask that we still need to figure out # class ChlorinatorStatus(str,Enum): # pass + + +# I have seen one pool that had an operatingMode of 3, I am not sure what that means, perhaps that is an OFF mode class ChlorinatorOperatingMode(IntEnum): DISABLED = 0 TIMED = 1 ORP = 2 + OFF = 3 class ChlorinatorDispenserType(str, PrettyEnum): SALT = "SALT_DISPENSING" + LIQUID = "LIQUID_DISPENSING" class ChlorinatorCellType(PrettyEnum): @@ -102,6 +107,7 @@ class ChlorinatorCellType(PrettyEnum): T5 = "CELL_TYPE_T5" T9 = "CELL_TYPE_T9" T15 = "CELL_TYPE_T15" + LIQUID = "CELL_TYPE_LIQUID" # There is probably an easier way to do this def __int__(self) -> int: diff --git a/pyomnilogic_local/protocol.py b/pyomnilogic_local/protocol.py index 717f2f1..ca35687 100644 --- a/pyomnilogic_local/protocol.py +++ b/pyomnilogic_local/protocol.py @@ -3,15 +3,15 @@ import random import struct import time -from typing import Any, cast import xml.etree.ElementTree as ET import zlib +from typing import Any, cast from typing_extensions import Self from .exceptions import OmniTimeoutException from .models.leadmessage import LeadMessage -from .types import ClientType, MessageType +from .omnitypes import ClientType, MessageType _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,13 @@ class OmniLogicMessage: compressed: bool = False reserved_2: int = 0 - def __init__(self, msg_id: int, msg_type: MessageType, payload: str | None = None, version: str = "1.19") -> None: + def __init__( + self, + msg_id: int, + msg_type: MessageType, + payload: str | None = None, + version: str = "1.19", + ) -> None: self.id = msg_id self.type = msg_type # If we are speaking the XML API, it seems like we need client_type 0, otherwise we need client_type 1 @@ -67,7 +73,7 @@ def from_bytes(cls, data: bytes) -> Self: header = data[:24] rdata: bytes = data[24:] - msg_id, tstamp, vers, msg_type, client_type, res1, compressed, res2 = struct.unpack(cls.header_format, header) + (msg_id, tstamp, vers, msg_type, client_type, res1, compressed, res2) = struct.unpack(cls.header_format, header) message = cls(msg_id=msg_id, msg_type=MessageType(msg_type), version=vers.decode("utf-8")) message.timestamp = tstamp message.client_type = ClientType(int(client_type)) @@ -125,7 +131,11 @@ async def _wait_for_ack(self, ack_id: int) -> None: # eventually time out waiting for it, that way we can deal with the dropped packets message = await self.data_queue.get() - async def _ensure_sent(self, message: OmniLogicMessage, max_attempts: int = 5) -> None: + async def _ensure_sent( + self, + message: OmniLogicMessage, + max_attempts: int = 5, + ) -> None: for attempt in range(0, max_attempts): self.transport.sendto(bytes(message)) @@ -143,12 +153,22 @@ async def _ensure_sent(self, message: OmniLogicMessage, max_attempts: int = 5) - else: raise OmniTimeoutException("Failed to receive acknowledgement of command, max retries exceeded") from exc - async def send_and_receive(self, msg_type: MessageType, payload: str | None, msg_id: int | None = None) -> str: + async def send_and_receive( + self, + msg_type: MessageType, + payload: str | None, + msg_id: int | None = None, + ) -> str: await self.send_message(msg_type, payload, msg_id) return await self._receive_file() # Send a message that you do NOT need a response to - async def send_message(self, msg_type: MessageType, payload: str | None, msg_id: int | None = None) -> None: + async def send_message( + self, + msg_type: MessageType, + payload: str | None, + msg_id: int | None = None, + ) -> None: # If we aren't sending a specific msg_id, lets randomize it if not msg_id: msg_id = random.randrange(2**32) diff --git a/pyomnilogic_local/util.py b/pyomnilogic_local/util.py index 6c0246d..b2b0cfb 100644 --- a/pyomnilogic_local/util.py +++ b/pyomnilogic_local/util.py @@ -1,5 +1,5 @@ -from enum import Enum import sys +from enum import Enum if sys.version_info >= (3, 11): from typing import Self diff --git a/pyproject.toml b/pyproject.toml index d6d53a8..06b2b75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,43 +1,30 @@ -[tool.poetry] +[project] name = "python-omnilogic-local" version = "0.14.6" description = "A library for local control of Hayward OmniHub/OmniLogic pool controllers using their local API" -authors = ["cryptk ", "djtimca", "garionphx"] -license = "Apache-2.0" +authors = [ + {name = "Chris Jowett",email = "421501+cryptk@users.noreply.github.com"} +] +license = {text = "Apache-2.0"} readme = "README.md" -repository = "https://github.com/cryptk/python-omnilogic-local" -classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Natural Language :: English", - "Operating System :: OS Independent", - "Topic :: Software Development :: Libraries", +requires-python = ">=3.12,<3.14" +dependencies = [ + "pydantic (>=1.10.17)", + "xmltodict (>=0.13.0,<0.14.0)" ] -packages = [{include = "pyomnilogic_local"}] -[tool.poetry.scripts] -omnilogic = "pyomnilogic_local.cli:main" - -[tool.poetry.dependencies] -python = "^3.10" -pydantic = ">=1.10.17" -xmltodict = "^0.13.0" +[tool.poetry] +packages = [{include = "pyomnilogic_local"}] [tool.poetry.group.dev.dependencies] -pre-commit = "^3.0.0" -mypy = "^1.2.0" -pylint = "^3.2.3" -pydantic = "^1.10.7" -pytest = "^7.3.1" +pre-commit = "^3.8.0" +mypy = "^1.14.0" +pylint = "^3.3.3" +pytest = "^7.4.4" pytest-cov = "^4.1.0" -[tool.pytest.ini_options] -addopts = [ - "--import-mode=importlib", -] - [build-system] -requires = ["poetry-core"] +requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.black] @@ -46,43 +33,15 @@ line-length=140 [tool.isort] # https://github.com/PyCQA/isort/wiki/isort-Settings profile = "black" -# will group `import x` and `from x import` of the same module. -force_sort_within_sections = true -known_first_party = [ - "homeassistant", - "tests", -] -forced_separate = [ - "tests", -] -combine_as_imports = true [tool.mypy] -python_version = "3.10" +python_version = "3.13" plugins = "pydantic.mypy" follow_imports = "silent" strict = true ignore_missing_imports = true disallow_subclassing_any = false warn_return_any = false -# local_partial_types = true -# strict_equality = true -# no_implicit_optional = true -# warn_incomplete_stub = true -# warn_redundant_casts = true -# warn_unused_configs = true -# warn_unused_ignores = true -# enable_error_code = "ignore-without-code, redundant-self, truthy-iterable" -# disable_error_code = "annotation-unchecked" -# strict_concatenate = false -# check_untyped_defs = true -# disallow_incomplete_defs = true -# disallow_untyped_calls = true -# disallow_untyped_decorators = true -# disallow_untyped_defs = true -# warn_unreachable = true -# no_implicit_reexport = true -# disallow_any_generics = true [tool.pydantic-mypy] init_forbid_extra = true @@ -91,7 +50,7 @@ warn_required_dynamic_aliases = true warn_untyped_fields = true [tool.pylint.MAIN] -py-version = "3.11" +py-version = "3.13" ignore = [ "tests", ] @@ -102,31 +61,6 @@ load-plugins = [ "pylint.extensions.code_style", "pylint.extensions.typing", ] -persistent = false -extension-pkg-allow-list = [ - "pydantic", -] -fail-on = [ - "I", -] - -[tool.pylint.BASIC] -class-const-naming-style = "any" -good-names = [ - "_", - "ev", - "ex", - "fp", - "i", - "id", - "j", - "k", - "Run", - "ip", -] - -[tool.pylint.CODE_STYLE] -max-line-length-suggestions = 72 [tool.pylint."FORMAT"] expected-line-ending-format = "LF" @@ -175,25 +109,20 @@ disable = [ "wrong-import-order", "wrong-import-position", "consider-using-f-string", - "consider-using-namedtuple-or-dataclass", - "consider-using-assignment-expr", # The below are only here for now, we should fully document once the codebase stops fluctuating so much "missing-class-docstring", "missing-function-docstring", "missing-module-docstring", ] enable = [ - "useless-suppression", # temporarily every now and then to clean them up + "useless-suppression", "use-symbolic-message-instead", ] -[tool.pylint.REPORTS] -score = false - [tool.ruff] line-length = 140 [tool.semantic_release] branch = "main" -version_toml = "pyproject.toml:tool.poetry.version" +version_toml = "pyproject.toml:project.version" build_command = "pip install poetry && poetry build" diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 8044da7..24913ca 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -1,5 +1,5 @@ +from pyomnilogic_local.omnitypes import ClientType, MessageType from pyomnilogic_local.protocol import OmniLogicMessage -from pyomnilogic_local.types import ClientType, MessageType def test_parse_basic_ack() -> None: