diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 770fa81..bf91e09 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,11 +40,14 @@ repos: - id: ruff args: - --fix - - repo: https://github.com/pylint-dev/pylint - rev: v3.3.3 + - repo: local hooks: - id: pylint - additional_dependencies: [ "pydantic>=1.10.17", "xmltodict" ] + name: pylint + entry: poetry run pylint + language: system + types: [python] + require_serial: true - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.14.1 hooks: diff --git a/poetry.lock b/poetry.lock index c9cef3a..1b1efc0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -27,18 +27,33 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "click" +version = "8.0.4" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, + {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "sys_platform == \"win32\"" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} [[package]] name = "coverage" @@ -680,4 +695,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.14" -content-hash = "a45fe7e3663ca631c330c66d842daf44e8e7539df3400651a504d9bea03537ef" +content-hash = "3e04d075b9e339582ccd9b1bdaea768e72e93e5ca74ffc5e5e84b34dfc740a10" diff --git a/pyomnilogic_local/cli/__init__.py b/pyomnilogic_local/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyomnilogic_local/cli/cli.py b/pyomnilogic_local/cli/cli.py new file mode 100644 index 0000000..68a77df --- /dev/null +++ b/pyomnilogic_local/cli/cli.py @@ -0,0 +1,25 @@ +import asyncio + +import click + +from pyomnilogic_local.api import OmniLogicAPI +from pyomnilogic_local.cli.debug import commands as debug +from pyomnilogic_local.cli.get import commands as get + + +async def get_omni(host: str) -> OmniLogicAPI: + return OmniLogicAPI(host, 10444, 5.0) + + +@click.group() +@click.pass_context +@click.option("--host", default="127.0.0.1", help="Hostname or IP address of omnilogic system") +def entrypoint(ctx: click.Context, host: str) -> None: + ctx.ensure_object(dict) + omni = asyncio.run(get_omni(host)) + + ctx.obj["OMNI"] = omni + + +entrypoint.add_command(debug.debug) +entrypoint.add_command(get.get) diff --git a/pyomnilogic_local/cli/debug/__init__.py b/pyomnilogic_local/cli/debug/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyomnilogic_local/cli/debug/commands.py b/pyomnilogic_local/cli/debug/commands.py new file mode 100644 index 0000000..4245e8e --- /dev/null +++ b/pyomnilogic_local/cli/debug/commands.py @@ -0,0 +1,95 @@ +# Need to figure out how to resolve the 'Untyped decorator makes function "..." untyped' errors in mypy when using click decorators +# mypy: disable-error-code="misc" +import asyncio +from typing import Literal, overload + +import click + +from pyomnilogic_local.api import OmniLogicAPI +from pyomnilogic_local.cli.utils import async_get_mspconfig, async_get_telemetry +from pyomnilogic_local.models.filter_diagnostics import FilterDiagnostics + + +@click.group() +@click.option("--raw/--no-raw", default=False, help="Output the raw XML from the OmniLogic, do not parse the response") +@click.pass_context +def debug(ctx: click.Context, raw: bool) -> None: + # Container for all get commands + + ctx.ensure_object(dict) + ctx.obj["RAW"] = raw + + +@debug.command() +@click.pass_context +def get_mspconfig(ctx: click.Context) -> None: + mspconfig = asyncio.run(async_get_mspconfig(ctx.obj["OMNI"], ctx.obj["RAW"])) + click.echo(mspconfig) + + +@debug.command() +@click.pass_context +def get_telemetry(ctx: click.Context) -> None: + telemetry = asyncio.run(async_get_telemetry(ctx.obj["OMNI"], ctx.obj["RAW"])) + click.echo(telemetry) + + +@debug.command() +@click.pass_context +def get_alarm_list(ctx: click.Context) -> None: + alarm_list = asyncio.run(async_get_alarm_list(ctx.obj["OMNI"])) + click.echo(alarm_list) + + +async def async_get_alarm_list(omni: OmniLogicAPI) -> str: + alarm_list = await omni.async_get_alarm_list() + return alarm_list + + +@debug.command() +@click.option("--pool-id", help="System ID of the Body Of Water the filter is associated with") +@click.option("--filter-id", help="System ID of the filter to request diagnostics for") +@click.pass_context +def get_filter_diagnostics(ctx: click.Context, pool_id: int, filter_id: int) -> None: + filter_diags = asyncio.run(async_get_filter_diagnostics(ctx.obj["OMNI"], pool_id, filter_id, ctx.obj["RAW"])) + if ctx.obj["RAW"]: + click.echo(filter_diags) + else: + drv1 = chr(filter_diags.get_param_by_name("DriveFWRevisionB1")) + drv2 = chr(filter_diags.get_param_by_name("DriveFWRevisionB2")) + drv3 = chr(filter_diags.get_param_by_name("DriveFWRevisionB3")) + drv4 = chr(filter_diags.get_param_by_name("DriveFWRevisionB4")) + dfw1 = chr(filter_diags.get_param_by_name("DisplayFWRevisionB1")) + dfw2 = chr(filter_diags.get_param_by_name("DisplayFWRevisionB2")) + dfw3 = chr(filter_diags.get_param_by_name("DisplayFWRevisionB3")) + dfw4 = chr(filter_diags.get_param_by_name("DisplayFWRevisionB4")) + pow1 = filter_diags.get_param_by_name("PowerMSB") + pow2 = filter_diags.get_param_by_name("PowerLSB") + errs = filter_diags.get_param_by_name("ErrorStatus") + click.echo( + f"DRIVE FW REV: {drv1}{drv2}.{drv3}{drv4}\n" + f"DISPLAY FW REV: {dfw1}{dfw2}.{dfw3}.{dfw4}\n" + f"POWER: {pow1:x}{pow2:x}W\n" + f"ERROR STATUS: {errs}" + ) + + +@overload +async def async_get_filter_diagnostics(omni: OmniLogicAPI, pool_id: int, filter_id: int, raw: Literal[True]) -> str: ... +@overload +async def async_get_filter_diagnostics(omni: OmniLogicAPI, pool_id: int, filter_id: int, raw: Literal[False]) -> FilterDiagnostics: ... +async def async_get_filter_diagnostics(omni: OmniLogicAPI, pool_id: int, filter_id: int, raw: bool) -> FilterDiagnostics | str: + filter_diags = await omni.async_get_filter_diagnostics(pool_id, filter_id, raw=raw) + return filter_diags + + +@debug.command() +@click.pass_context +def get_log_config(ctx: click.Context) -> None: + log_config = asyncio.run(async_get_log_config(ctx.obj["OMNI"])) + click.echo(log_config) + + +async def async_get_log_config(omni: OmniLogicAPI) -> str: + log_config = await omni.async_get_log_config() + return log_config diff --git a/pyomnilogic_local/cli/get/__init__.py b/pyomnilogic_local/cli/get/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyomnilogic_local/cli/get/commands.py b/pyomnilogic_local/cli/get/commands.py new file mode 100644 index 0000000..49aa93c --- /dev/null +++ b/pyomnilogic_local/cli/get/commands.py @@ -0,0 +1,33 @@ +# Need to figure out how to resolve the 'Untyped decorator makes function "..." untyped' errors in mypy when using click decorators +# mypy: disable-error-code="misc" +import asyncio + +import click + +from pyomnilogic_local.cli.utils import async_get_mspconfig + + +@click.group() +@click.pass_context +def get(ctx: click.Context) -> None: + # Container for all get commands + + ctx.ensure_object(dict) + + +@get.command() +@click.pass_context +def lights(ctx: click.Context) -> None: + mspconfig = asyncio.run(async_get_mspconfig(ctx.obj["OMNI"])) + # Return data about lights in the backyard + if mspconfig.backyard.colorlogic_light: + for light in mspconfig.backyard.colorlogic_light: + click.echo(light) + + # Return data about lights in the Body of Water + if mspconfig.backyard.bow: + for bow in mspconfig.backyard.bow: + if bow.colorlogic_light: + for cl_light in bow.colorlogic_light: + for k, v in cl_light: + click.echo(f"{k:15}\t{str(v)}") diff --git a/pyomnilogic_local/cli/utils.py b/pyomnilogic_local/cli/utils.py new file mode 100644 index 0000000..b1e06f8 --- /dev/null +++ b/pyomnilogic_local/cli/utils.py @@ -0,0 +1,15 @@ +from pyomnilogic_local.api import OmniLogicAPI +from pyomnilogic_local.models.mspconfig import MSPConfig +from pyomnilogic_local.models.telemetry import Telemetry + + +async def async_get_mspconfig(omni: OmniLogicAPI, raw: bool = False) -> MSPConfig: + mspconfig: MSPConfig + mspconfig = await omni.async_get_config(raw=raw) + return mspconfig + + +async def async_get_telemetry(omni: OmniLogicAPI, raw: bool = False) -> Telemetry: + telemetry: Telemetry + telemetry = await omni.async_get_telemetry(raw=raw) + return telemetry diff --git a/pyomnilogic_local/cli.py b/pyomnilogic_local/cli_legacy.py similarity index 100% rename from pyomnilogic_local/cli.py rename to pyomnilogic_local/cli_legacy.py diff --git a/pyomnilogic_local/omnitypes.py b/pyomnilogic_local/omnitypes.py index 5652032..f6c6cdf 100644 --- a/pyomnilogic_local/omnitypes.py +++ b/pyomnilogic_local/omnitypes.py @@ -64,6 +64,9 @@ class OmniType(str, Enum): VALVE_ACTUATOR = "ValveActuator" VIRT_HEATER = "VirtualHeater" + def __str__(self) -> str: + return OmniType[self.name].value + # Backyard/BoW class BackyardState(PrettyEnum): @@ -173,6 +176,9 @@ class ColorLogicShow(PrettyEnum): WARM_WHITE = 25 BRIGHT_YELLOW = 26 + def __str__(self) -> str: + return self.name + class ColorLogicPowerState(PrettyEnum): OFF = 0 @@ -188,6 +194,9 @@ class ColorLogicLightType(str, PrettyEnum): FOUR_ZERO = "COLOR_LOGIC_4_0" TWO_FIVE = "COLOR_LOGIC_2_5" + def __str__(self) -> str: + return ColorLogicLightType[self.name].value + class CSADType(str, PrettyEnum): ACID = "ACID" diff --git a/pyproject.toml b/pyproject.toml index a366ee7..b619470 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,11 +12,12 @@ readme = "README.md" requires-python = ">=3.10,<3.14" dependencies = [ "pydantic (>=1.10.17)", - "xmltodict (>=0.13.0,<0.14.0)" + "xmltodict (>=0.13.0,<0.14.0)", + "click (>=8.0.0,<8.1.0)", ] [project.scripts] -omnilogic = "pyomnilogic_local.cli:main" +omnilogic = "pyomnilogic_local.cli.cli:entrypoint" [tool.poetry] packages = [{include = "pyomnilogic_local"}] @@ -41,7 +42,9 @@ profile = "black" [tool.mypy] python_version = "3.13" -plugins = "pydantic.mypy" +plugins = [ + "pydantic.mypy", +] follow_imports = "silent" strict = true ignore_missing_imports = true @@ -56,6 +59,9 @@ warn_untyped_fields = true [tool.pylint.MAIN] py-version = "3.13" +extension-pkg-allow-list = [ + "pydantic", +] ignore = [ "tests", ]