From f436a017c98d329c5660565047a91383ec83eb08 Mon Sep 17 00:00:00 2001 From: Tony Fruzza Date: Tue, 19 May 2026 05:45:42 +0000 Subject: [PATCH 1/2] feat(cli): add set command group for equipment control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new 'set' command group to the CLI that exposes equipment control operations that were previously only available through the Python API: - set heater-temp — Set heater target temp (°F) - set solar-temp — Set solar heater target (°F) - set speed — Set pump/filter speed (0-100%) - set on — Turn equipment on - set off — Turn equipment off This allows scripting and automation of pool control without writing Python code, complementing the existing read-only 'get' and 'debug' command groups. Resolves #145 --- pyomnilogic_local/cli/cli.py | 2 + pyomnilogic_local/cli/set/__init__.py | 0 pyomnilogic_local/cli/set/commands.py | 31 ++++++++++++ pyomnilogic_local/cli/set/equipment.py | 61 ++++++++++++++++++++++++ pyomnilogic_local/cli/set/heater_temp.py | 57 ++++++++++++++++++++++ pyomnilogic_local/cli/set/speed.py | 46 ++++++++++++++++++ 6 files changed, 197 insertions(+) create mode 100644 pyomnilogic_local/cli/set/__init__.py create mode 100644 pyomnilogic_local/cli/set/commands.py create mode 100644 pyomnilogic_local/cli/set/equipment.py create mode 100644 pyomnilogic_local/cli/set/heater_temp.py create mode 100644 pyomnilogic_local/cli/set/speed.py diff --git a/pyomnilogic_local/cli/cli.py b/pyomnilogic_local/cli/cli.py index 7749cff..b489f02 100644 --- a/pyomnilogic_local/cli/cli.py +++ b/pyomnilogic_local/cli/cli.py @@ -8,6 +8,7 @@ from pyomnilogic_local import OmniLogic from pyomnilogic_local.cli.debug import commands as debug from pyomnilogic_local.cli.get import commands as get +from pyomnilogic_local.cli.set import commands as set_cmds @click.group() @@ -60,3 +61,4 @@ def entrypoint(ctx: click.Context, host: str, port: int, timeout: int, debug: bo entrypoint.add_command(debug.debug) entrypoint.add_command(get.get) +entrypoint.add_command(set_cmds.set) diff --git a/pyomnilogic_local/cli/set/__init__.py b/pyomnilogic_local/cli/set/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyomnilogic_local/cli/set/commands.py b/pyomnilogic_local/cli/set/commands.py new file mode 100644 index 0000000..0cc51e4 --- /dev/null +++ b/pyomnilogic_local/cli/set/commands.py @@ -0,0 +1,31 @@ +# 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" + +from __future__ import annotations + +import click + +from pyomnilogic_local.cli.set.equipment import equipment_off, equipment_on +from pyomnilogic_local.cli.set.heater_temp import heater_temp, solar_temp +from pyomnilogic_local.cli.set.speed import speed + + +@click.group() +@click.pass_context +def set(ctx: click.Context) -> None: + """Control pool equipment (turn on/off, set temperature, set speed). + + These commands send control signals to pool equipment. They require + equipment system IDs which can be found using the 'get' commands. + + Use with caution — these commands directly control physical equipment. + """ + ctx.ensure_object(dict) + + +# Register subcommands +set.add_command(equipment_on) +set.add_command(equipment_off) +set.add_command(heater_temp) +set.add_command(solar_temp) +set.add_command(speed) diff --git a/pyomnilogic_local/cli/set/equipment.py b/pyomnilogic_local/cli/set/equipment.py new file mode 100644 index 0000000..896fe5b --- /dev/null +++ b/pyomnilogic_local/cli/set/equipment.py @@ -0,0 +1,61 @@ +# mypy: disable-error-code="misc" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +import click + +if TYPE_CHECKING: + from pyomnilogic_local import OmniLogic + + +@click.command("on") +@click.argument("system_id", type=int) +@click.pass_context +def equipment_on(ctx: click.Context, system_id: int) -> None: + """Turn equipment on. + + SYSTEM_ID is the equipment's system ID. Works with heaters, pumps, filters, + lights, and relays. Use the appropriate 'get' command to find system IDs. + + Example: + omnilogic set on 5 + """ + omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] + + equipment = omnilogic.get_equipment_by_id(system_id) + if equipment is None: + raise click.ClickException(f"No equipment found with system_id {system_id}.") + + if not hasattr(equipment, "turn_on"): + raise click.ClickException(f"Equipment '{equipment.name}' (system_id={system_id}) does not support turn_on.") + + asyncio.run(equipment.turn_on()) + click.echo(f"Turned on '{equipment.name}' (system_id={system_id})") + + +@click.command("off") +@click.argument("system_id", type=int) +@click.pass_context +def equipment_off(ctx: click.Context, system_id: int) -> None: + """Turn equipment off. + + SYSTEM_ID is the equipment's system ID. Works with heaters, pumps, filters, + lights, and relays. Use the appropriate 'get' command to find system IDs. + + Example: + omnilogic set off 5 + """ + omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] + + equipment = omnilogic.get_equipment_by_id(system_id) + if equipment is None: + raise click.ClickException(f"No equipment found with system_id {system_id}.") + + if not hasattr(equipment, "turn_off"): + raise click.ClickException(f"Equipment '{equipment.name}' (system_id={system_id}) does not support turn_off.") + + asyncio.run(equipment.turn_off()) + click.echo(f"Turned off '{equipment.name}' (system_id={system_id})") diff --git a/pyomnilogic_local/cli/set/heater_temp.py b/pyomnilogic_local/cli/set/heater_temp.py new file mode 100644 index 0000000..ab75a50 --- /dev/null +++ b/pyomnilogic_local/cli/set/heater_temp.py @@ -0,0 +1,57 @@ +# mypy: disable-error-code="misc" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +import click + +if TYPE_CHECKING: + from pyomnilogic_local import OmniLogic + + +@click.command("heater-temp") +@click.argument("system_id", type=int) +@click.argument("temperature", type=int) +@click.pass_context +def heater_temp(ctx: click.Context, system_id: int, temperature: int) -> None: + """Set heater target temperature (Fahrenheit). + + SYSTEM_ID is the virtual heater's system ID (use 'get heaters' to find it). + TEMPERATURE is the target temperature in Fahrenheit. + + Example: + omnilogic set heater-temp 4 82 + """ + omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] + + heater = omnilogic.all_heaters.get_by_id(system_id) + if heater is None: + raise click.ClickException(f"No heater found with system_id {system_id}. Use 'omnilogic get heaters' to list available heaters.") + + asyncio.run(heater.set_temperature(temperature)) + click.echo(f"Set heater '{heater.name}' (system_id={system_id}) to {temperature}°F") + + +@click.command("solar-temp") +@click.argument("system_id", type=int) +@click.argument("temperature", type=int) +@click.pass_context +def solar_temp(ctx: click.Context, system_id: int, temperature: int) -> None: + """Set solar heater target temperature (Fahrenheit). + + SYSTEM_ID is the virtual heater's system ID (use 'get heaters' to find it). + TEMPERATURE is the target solar temperature in Fahrenheit. + + Example: + omnilogic set solar-temp 4 90 + """ + omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] + + heater = omnilogic.all_heaters.get_by_id(system_id) + if heater is None: + raise click.ClickException(f"No heater found with system_id {system_id}. Use 'omnilogic get heaters' to list available heaters.") + + asyncio.run(heater.set_solar_temperature(temperature)) + click.echo(f"Set solar temperature for '{heater.name}' (system_id={system_id}) to {temperature}°F") diff --git a/pyomnilogic_local/cli/set/speed.py b/pyomnilogic_local/cli/set/speed.py new file mode 100644 index 0000000..7d83272 --- /dev/null +++ b/pyomnilogic_local/cli/set/speed.py @@ -0,0 +1,46 @@ +# mypy: disable-error-code="misc" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +import click + +from pyomnilogic_local.filter import Filter +from pyomnilogic_local.pump import Pump + +if TYPE_CHECKING: + from pyomnilogic_local import OmniLogic + + +@click.command("speed") +@click.argument("system_id", type=int) +@click.argument("percent", type=int) +@click.pass_context +def speed(ctx: click.Context, system_id: int, percent: int) -> None: + """Set pump or filter speed (0-100 percent). + + SYSTEM_ID is the pump or filter's system ID (use 'get pumps' or 'get filters' to find it). + PERCENT is the speed percentage (0 will turn the pump off). + + Example: + omnilogic set speed 3 75 + """ + omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] + + equipment = omnilogic.all_pumps.get_by_id(system_id) + if equipment is None: + equipment = omnilogic.all_filters.get_by_id(system_id) + + if equipment is None: + raise click.ClickException( + f"No pump or filter found with system_id {system_id}. " + "Use 'omnilogic get pumps' or 'omnilogic get filters' to list available equipment." + ) + + if not isinstance(equipment, (Pump, Filter)): + raise click.ClickException(f"Equipment with system_id {system_id} is not a pump or filter.") + + asyncio.run(equipment.set_speed(percent)) + click.echo(f"Set '{equipment.name}' (system_id={system_id}) to {percent}%") From 67a217e3b8e4cf5e50ebc26b2bf7d3339f1d6161 Mon Sep 17 00:00:00 2001 From: Tony Fruzza Date: Fri, 29 May 2026 15:41:38 +0000 Subject: [PATCH 2/2] refactor(cli): redesign to equipment-centric command structure Address reviewer feedback to mirror the library layout in the CLI. Instead of a flat 'set' command group: omnilogic set on 5 omnilogic set heater-temp 4 82 omnilogic set speed 3 75 The CLI now uses equipment-type subcommands with control methods: omnilogic heater 4 (show heater info) omnilogic heater 4 turn_on omnilogic heater 4 set_temperature 82 omnilogic pump 3 set_speed 75 omnilogic filter 3 turn_off omnilogic relay 6 turn_on omnilogic light 5 turn_off Each equipment type is a Click group that: - Takes a system_id argument - Shows all properties when called without a subcommand - Exposes control methods as subcommands matching the library API Equipment commands added: heater, pump, filter, relay, light --- pyomnilogic_local/cli/cli.py | 12 +++- pyomnilogic_local/cli/filter_cmd.py | 69 +++++++++++++++++++ pyomnilogic_local/cli/heater_cmd.py | 84 ++++++++++++++++++++++++ pyomnilogic_local/cli/light_cmd.py | 55 ++++++++++++++++ pyomnilogic_local/cli/pump_cmd.py | 69 +++++++++++++++++++ pyomnilogic_local/cli/relay_cmd.py | 55 ++++++++++++++++ pyomnilogic_local/cli/set/__init__.py | 0 pyomnilogic_local/cli/set/commands.py | 31 --------- pyomnilogic_local/cli/set/equipment.py | 61 ----------------- pyomnilogic_local/cli/set/heater_temp.py | 57 ---------------- pyomnilogic_local/cli/set/speed.py | 46 ------------- 11 files changed, 342 insertions(+), 197 deletions(-) create mode 100644 pyomnilogic_local/cli/filter_cmd.py create mode 100644 pyomnilogic_local/cli/heater_cmd.py create mode 100644 pyomnilogic_local/cli/light_cmd.py create mode 100644 pyomnilogic_local/cli/pump_cmd.py create mode 100644 pyomnilogic_local/cli/relay_cmd.py delete mode 100644 pyomnilogic_local/cli/set/__init__.py delete mode 100644 pyomnilogic_local/cli/set/commands.py delete mode 100644 pyomnilogic_local/cli/set/equipment.py delete mode 100644 pyomnilogic_local/cli/set/heater_temp.py delete mode 100644 pyomnilogic_local/cli/set/speed.py diff --git a/pyomnilogic_local/cli/cli.py b/pyomnilogic_local/cli/cli.py index b489f02..4b2cc42 100644 --- a/pyomnilogic_local/cli/cli.py +++ b/pyomnilogic_local/cli/cli.py @@ -7,8 +7,12 @@ from pyomnilogic_local import OmniLogic from pyomnilogic_local.cli.debug import commands as debug +from pyomnilogic_local.cli.filter_cmd import filter from pyomnilogic_local.cli.get import commands as get -from pyomnilogic_local.cli.set import commands as set_cmds +from pyomnilogic_local.cli.heater_cmd import heater +from pyomnilogic_local.cli.light_cmd import light +from pyomnilogic_local.cli.pump_cmd import pump +from pyomnilogic_local.cli.relay_cmd import relay @click.group() @@ -61,4 +65,8 @@ def entrypoint(ctx: click.Context, host: str, port: int, timeout: int, debug: bo entrypoint.add_command(debug.debug) entrypoint.add_command(get.get) -entrypoint.add_command(set_cmds.set) +entrypoint.add_command(heater) +entrypoint.add_command(pump) +entrypoint.add_command(filter) +entrypoint.add_command(relay) +entrypoint.add_command(light) diff --git a/pyomnilogic_local/cli/filter_cmd.py b/pyomnilogic_local/cli/filter_cmd.py new file mode 100644 index 0000000..c7d6af7 --- /dev/null +++ b/pyomnilogic_local/cli/filter_cmd.py @@ -0,0 +1,69 @@ +# mypy: disable-error-code="misc" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +import click + +from pyomnilogic_local.cli.utils import echo_properties + +if TYPE_CHECKING: + from pyomnilogic_local import OmniLogic + + +@click.group(invoke_without_command=True) +@click.argument("system_id", type=int) +@click.pass_context +def filter(ctx: click.Context, system_id: int) -> None: + """Interact with a filter by system ID. + + If no subcommand is given, displays all properties of the filter. + + Example: + omnilogic filter 3 turn_on + omnilogic filter 3 set_speed 75 + """ + omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] + + equipment = omnilogic.all_filters.get_by_id(system_id) + if equipment is None: + raise click.ClickException(f"No filter found with system_id {system_id}. Use 'omnilogic get filters' to list available filters.") + + ctx.obj["EQUIPMENT"] = equipment + + if ctx.invoked_subcommand is None: + echo_properties(equipment) + + +@filter.command("turn_on") +@click.pass_context +def turn_on(ctx: click.Context) -> None: + """Turn the filter on.""" + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.turn_on()) + click.echo(f"Turned on '{equipment.name}' (system_id={equipment.system_id})") + + +@filter.command("turn_off") +@click.pass_context +def turn_off(ctx: click.Context) -> None: + """Turn the filter off.""" + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.turn_off()) + click.echo(f"Turned off '{equipment.name}' (system_id={equipment.system_id})") + + +@filter.command("set_speed") +@click.argument("percent", type=int) +@click.pass_context +def set_speed(ctx: click.Context, percent: int) -> None: + """Set the filter speed (0-100 percent). + + Example: + omnilogic filter 3 set_speed 75 + """ + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.set_speed(percent)) + click.echo(f"Set '{equipment.name}' (system_id={equipment.system_id}) to {percent}%") diff --git a/pyomnilogic_local/cli/heater_cmd.py b/pyomnilogic_local/cli/heater_cmd.py new file mode 100644 index 0000000..bbc4017 --- /dev/null +++ b/pyomnilogic_local/cli/heater_cmd.py @@ -0,0 +1,84 @@ +# mypy: disable-error-code="misc" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +import click + +from pyomnilogic_local.cli.utils import echo_properties + +if TYPE_CHECKING: + from pyomnilogic_local import OmniLogic + + +@click.group(invoke_without_command=True) +@click.argument("system_id", type=int) +@click.pass_context +def heater(ctx: click.Context, system_id: int) -> None: + """Interact with a heater by system ID. + + If no subcommand is given, displays all properties of the heater. + + Example: + omnilogic heater 4 + omnilogic heater 4 turn_on + omnilogic heater 4 set_temperature 82 + """ + omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] + + equipment = omnilogic.all_heaters.get_by_id(system_id) + if equipment is None: + raise click.ClickException(f"No heater found with system_id {system_id}. Use 'omnilogic get heaters' to list available heaters.") + + ctx.obj["EQUIPMENT"] = equipment + + if ctx.invoked_subcommand is None: + echo_properties(equipment) + + +@heater.command("turn_on") +@click.pass_context +def turn_on(ctx: click.Context) -> None: + """Turn the heater on.""" + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.turn_on()) + click.echo(f"Turned on '{equipment.name}' (system_id={equipment.system_id})") + + +@heater.command("turn_off") +@click.pass_context +def turn_off(ctx: click.Context) -> None: + """Turn the heater off.""" + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.turn_off()) + click.echo(f"Turned off '{equipment.name}' (system_id={equipment.system_id})") + + +@heater.command("set_temperature") +@click.argument("temperature", type=int) +@click.pass_context +def set_temperature(ctx: click.Context, temperature: int) -> None: + """Set the heater target temperature (Fahrenheit). + + Example: + omnilogic heater 4 set_temperature 82 + """ + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.set_temperature(temperature)) + click.echo(f"Set '{equipment.name}' (system_id={equipment.system_id}) to {temperature}°F") + + +@heater.command("set_solar_temperature") +@click.argument("temperature", type=int) +@click.pass_context +def set_solar_temperature(ctx: click.Context, temperature: int) -> None: + """Set the solar heater target temperature (Fahrenheit). + + Example: + omnilogic heater 4 set_solar_temperature 90 + """ + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.set_solar_temperature(temperature)) + click.echo(f"Set solar temperature for '{equipment.name}' (system_id={equipment.system_id}) to {temperature}°F") diff --git a/pyomnilogic_local/cli/light_cmd.py b/pyomnilogic_local/cli/light_cmd.py new file mode 100644 index 0000000..5bf32d0 --- /dev/null +++ b/pyomnilogic_local/cli/light_cmd.py @@ -0,0 +1,55 @@ +# mypy: disable-error-code="misc" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +import click + +from pyomnilogic_local.cli.utils import echo_properties + +if TYPE_CHECKING: + from pyomnilogic_local import OmniLogic + + +@click.group(invoke_without_command=True) +@click.argument("system_id", type=int) +@click.pass_context +def light(ctx: click.Context, system_id: int) -> None: + """Interact with a light by system ID. + + If no subcommand is given, displays all properties of the light. + + Example: + omnilogic light 5 turn_on + omnilogic light 5 turn_off + """ + omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] + + equipment = omnilogic.all_lights.get_by_id(system_id) + if equipment is None: + raise click.ClickException(f"No light found with system_id {system_id}. Use 'omnilogic get lights' to list available lights.") + + ctx.obj["EQUIPMENT"] = equipment + + if ctx.invoked_subcommand is None: + echo_properties(equipment) + + +@light.command("turn_on") +@click.pass_context +def turn_on(ctx: click.Context) -> None: + """Turn the light on.""" + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.turn_on()) + click.echo(f"Turned on '{equipment.name}' (system_id={equipment.system_id})") + + +@light.command("turn_off") +@click.pass_context +def turn_off(ctx: click.Context) -> None: + """Turn the light off.""" + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.turn_off()) + click.echo(f"Turned off '{equipment.name}' (system_id={equipment.system_id})") diff --git a/pyomnilogic_local/cli/pump_cmd.py b/pyomnilogic_local/cli/pump_cmd.py new file mode 100644 index 0000000..be035e9 --- /dev/null +++ b/pyomnilogic_local/cli/pump_cmd.py @@ -0,0 +1,69 @@ +# mypy: disable-error-code="misc" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +import click + +from pyomnilogic_local.cli.utils import echo_properties + +if TYPE_CHECKING: + from pyomnilogic_local import OmniLogic + + +@click.group(invoke_without_command=True) +@click.argument("system_id", type=int) +@click.pass_context +def pump(ctx: click.Context, system_id: int) -> None: + """Interact with a pump by system ID. + + If no subcommand is given, displays all properties of the pump. + + Example: + omnilogic pump 3 turn_on + omnilogic pump 3 set_speed 75 + """ + omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] + + equipment = omnilogic.all_pumps.get_by_id(system_id) + if equipment is None: + raise click.ClickException(f"No pump found with system_id {system_id}. Use 'omnilogic get pumps' to list available pumps.") + + ctx.obj["EQUIPMENT"] = equipment + + if ctx.invoked_subcommand is None: + echo_properties(equipment) + + +@pump.command("turn_on") +@click.pass_context +def turn_on(ctx: click.Context) -> None: + """Turn the pump on.""" + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.turn_on()) + click.echo(f"Turned on '{equipment.name}' (system_id={equipment.system_id})") + + +@pump.command("turn_off") +@click.pass_context +def turn_off(ctx: click.Context) -> None: + """Turn the pump off.""" + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.turn_off()) + click.echo(f"Turned off '{equipment.name}' (system_id={equipment.system_id})") + + +@pump.command("set_speed") +@click.argument("percent", type=int) +@click.pass_context +def set_speed(ctx: click.Context, percent: int) -> None: + """Set the pump speed (0-100 percent). + + Example: + omnilogic pump 3 set_speed 75 + """ + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.set_speed(percent)) + click.echo(f"Set '{equipment.name}' (system_id={equipment.system_id}) to {percent}%") diff --git a/pyomnilogic_local/cli/relay_cmd.py b/pyomnilogic_local/cli/relay_cmd.py new file mode 100644 index 0000000..8ae5c69 --- /dev/null +++ b/pyomnilogic_local/cli/relay_cmd.py @@ -0,0 +1,55 @@ +# mypy: disable-error-code="misc" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING + +import click + +from pyomnilogic_local.cli.utils import echo_properties + +if TYPE_CHECKING: + from pyomnilogic_local import OmniLogic + + +@click.group(invoke_without_command=True) +@click.argument("system_id", type=int) +@click.pass_context +def relay(ctx: click.Context, system_id: int) -> None: + """Interact with a relay by system ID. + + If no subcommand is given, displays all properties of the relay. + + Example: + omnilogic relay 6 turn_on + omnilogic relay 6 turn_off + """ + omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] + + equipment = omnilogic.all_relays.get_by_id(system_id) + if equipment is None: + raise click.ClickException(f"No relay found with system_id {system_id}. Use 'omnilogic get relays' to list available relays.") + + ctx.obj["EQUIPMENT"] = equipment + + if ctx.invoked_subcommand is None: + echo_properties(equipment) + + +@relay.command("turn_on") +@click.pass_context +def turn_on(ctx: click.Context) -> None: + """Turn the relay on.""" + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.turn_on()) + click.echo(f"Turned on '{equipment.name}' (system_id={equipment.system_id})") + + +@relay.command("turn_off") +@click.pass_context +def turn_off(ctx: click.Context) -> None: + """Turn the relay off.""" + equipment = ctx.obj["EQUIPMENT"] + asyncio.run(equipment.turn_off()) + click.echo(f"Turned off '{equipment.name}' (system_id={equipment.system_id})") diff --git a/pyomnilogic_local/cli/set/__init__.py b/pyomnilogic_local/cli/set/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pyomnilogic_local/cli/set/commands.py b/pyomnilogic_local/cli/set/commands.py deleted file mode 100644 index 0cc51e4..0000000 --- a/pyomnilogic_local/cli/set/commands.py +++ /dev/null @@ -1,31 +0,0 @@ -# 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" - -from __future__ import annotations - -import click - -from pyomnilogic_local.cli.set.equipment import equipment_off, equipment_on -from pyomnilogic_local.cli.set.heater_temp import heater_temp, solar_temp -from pyomnilogic_local.cli.set.speed import speed - - -@click.group() -@click.pass_context -def set(ctx: click.Context) -> None: - """Control pool equipment (turn on/off, set temperature, set speed). - - These commands send control signals to pool equipment. They require - equipment system IDs which can be found using the 'get' commands. - - Use with caution — these commands directly control physical equipment. - """ - ctx.ensure_object(dict) - - -# Register subcommands -set.add_command(equipment_on) -set.add_command(equipment_off) -set.add_command(heater_temp) -set.add_command(solar_temp) -set.add_command(speed) diff --git a/pyomnilogic_local/cli/set/equipment.py b/pyomnilogic_local/cli/set/equipment.py deleted file mode 100644 index 896fe5b..0000000 --- a/pyomnilogic_local/cli/set/equipment.py +++ /dev/null @@ -1,61 +0,0 @@ -# mypy: disable-error-code="misc" - -from __future__ import annotations - -import asyncio -from typing import TYPE_CHECKING - -import click - -if TYPE_CHECKING: - from pyomnilogic_local import OmniLogic - - -@click.command("on") -@click.argument("system_id", type=int) -@click.pass_context -def equipment_on(ctx: click.Context, system_id: int) -> None: - """Turn equipment on. - - SYSTEM_ID is the equipment's system ID. Works with heaters, pumps, filters, - lights, and relays. Use the appropriate 'get' command to find system IDs. - - Example: - omnilogic set on 5 - """ - omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] - - equipment = omnilogic.get_equipment_by_id(system_id) - if equipment is None: - raise click.ClickException(f"No equipment found with system_id {system_id}.") - - if not hasattr(equipment, "turn_on"): - raise click.ClickException(f"Equipment '{equipment.name}' (system_id={system_id}) does not support turn_on.") - - asyncio.run(equipment.turn_on()) - click.echo(f"Turned on '{equipment.name}' (system_id={system_id})") - - -@click.command("off") -@click.argument("system_id", type=int) -@click.pass_context -def equipment_off(ctx: click.Context, system_id: int) -> None: - """Turn equipment off. - - SYSTEM_ID is the equipment's system ID. Works with heaters, pumps, filters, - lights, and relays. Use the appropriate 'get' command to find system IDs. - - Example: - omnilogic set off 5 - """ - omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] - - equipment = omnilogic.get_equipment_by_id(system_id) - if equipment is None: - raise click.ClickException(f"No equipment found with system_id {system_id}.") - - if not hasattr(equipment, "turn_off"): - raise click.ClickException(f"Equipment '{equipment.name}' (system_id={system_id}) does not support turn_off.") - - asyncio.run(equipment.turn_off()) - click.echo(f"Turned off '{equipment.name}' (system_id={system_id})") diff --git a/pyomnilogic_local/cli/set/heater_temp.py b/pyomnilogic_local/cli/set/heater_temp.py deleted file mode 100644 index ab75a50..0000000 --- a/pyomnilogic_local/cli/set/heater_temp.py +++ /dev/null @@ -1,57 +0,0 @@ -# mypy: disable-error-code="misc" - -from __future__ import annotations - -import asyncio -from typing import TYPE_CHECKING - -import click - -if TYPE_CHECKING: - from pyomnilogic_local import OmniLogic - - -@click.command("heater-temp") -@click.argument("system_id", type=int) -@click.argument("temperature", type=int) -@click.pass_context -def heater_temp(ctx: click.Context, system_id: int, temperature: int) -> None: - """Set heater target temperature (Fahrenheit). - - SYSTEM_ID is the virtual heater's system ID (use 'get heaters' to find it). - TEMPERATURE is the target temperature in Fahrenheit. - - Example: - omnilogic set heater-temp 4 82 - """ - omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] - - heater = omnilogic.all_heaters.get_by_id(system_id) - if heater is None: - raise click.ClickException(f"No heater found with system_id {system_id}. Use 'omnilogic get heaters' to list available heaters.") - - asyncio.run(heater.set_temperature(temperature)) - click.echo(f"Set heater '{heater.name}' (system_id={system_id}) to {temperature}°F") - - -@click.command("solar-temp") -@click.argument("system_id", type=int) -@click.argument("temperature", type=int) -@click.pass_context -def solar_temp(ctx: click.Context, system_id: int, temperature: int) -> None: - """Set solar heater target temperature (Fahrenheit). - - SYSTEM_ID is the virtual heater's system ID (use 'get heaters' to find it). - TEMPERATURE is the target solar temperature in Fahrenheit. - - Example: - omnilogic set solar-temp 4 90 - """ - omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] - - heater = omnilogic.all_heaters.get_by_id(system_id) - if heater is None: - raise click.ClickException(f"No heater found with system_id {system_id}. Use 'omnilogic get heaters' to list available heaters.") - - asyncio.run(heater.set_solar_temperature(temperature)) - click.echo(f"Set solar temperature for '{heater.name}' (system_id={system_id}) to {temperature}°F") diff --git a/pyomnilogic_local/cli/set/speed.py b/pyomnilogic_local/cli/set/speed.py deleted file mode 100644 index 7d83272..0000000 --- a/pyomnilogic_local/cli/set/speed.py +++ /dev/null @@ -1,46 +0,0 @@ -# mypy: disable-error-code="misc" - -from __future__ import annotations - -import asyncio -from typing import TYPE_CHECKING - -import click - -from pyomnilogic_local.filter import Filter -from pyomnilogic_local.pump import Pump - -if TYPE_CHECKING: - from pyomnilogic_local import OmniLogic - - -@click.command("speed") -@click.argument("system_id", type=int) -@click.argument("percent", type=int) -@click.pass_context -def speed(ctx: click.Context, system_id: int, percent: int) -> None: - """Set pump or filter speed (0-100 percent). - - SYSTEM_ID is the pump or filter's system ID (use 'get pumps' or 'get filters' to find it). - PERCENT is the speed percentage (0 will turn the pump off). - - Example: - omnilogic set speed 3 75 - """ - omnilogic: OmniLogic = ctx.obj["OMNILOGIC"] - - equipment = omnilogic.all_pumps.get_by_id(system_id) - if equipment is None: - equipment = omnilogic.all_filters.get_by_id(system_id) - - if equipment is None: - raise click.ClickException( - f"No pump or filter found with system_id {system_id}. " - "Use 'omnilogic get pumps' or 'omnilogic get filters' to list available equipment." - ) - - if not isinstance(equipment, (Pump, Filter)): - raise click.ClickException(f"Equipment with system_id {system_id} is not a pump or filter.") - - asyncio.run(equipment.set_speed(percent)) - click.echo(f"Set '{equipment.name}' (system_id={system_id}) to {percent}%")