Skip to content

Commit 251ea8e

Browse files
authored
Merge pull request #74 from cryptk/cli_enhancement
More CLI functionality
2 parents 9db600e + d3fbceb commit 251ea8e

21 files changed

Lines changed: 907 additions & 194 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,5 @@ cython_debug/
170170
# Testing files for PCAP parseer
171171
*.pcap
172172
*.pcapng
173+
174+
dev_files/

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,5 @@ repos:
5353
hooks:
5454
- id: mypy
5555
exclude: cli.py
56-
additional_dependencies: [ "pydantic>=1.10.17", "pytest>=8.0.0" ]
56+
additional_dependencies: [ "pydantic>=2.0.0", "pytest>=8.0.0" ]
5757
args: [ "--config-file=./pyproject.toml", "--follow-imports=silent", "--strict", "--ignore-missing-imports", "--disallow-subclassing-any", "--no-warn-return-any" ]

poetry.lock

Lines changed: 169 additions & 57 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Need to figure out how to resolve the 'Untyped decorator makes function "..." untyped' errors in mypy when using click decorators
2+
# mypy: disable-error-code="misc"
3+
4+
from typing import Any
5+
6+
import click
7+
8+
from pyomnilogic_local.models.mspconfig import (
9+
MSPBackyard,
10+
MSPConfig,
11+
)
12+
from pyomnilogic_local.models.telemetry import (
13+
Telemetry,
14+
TelemetryType,
15+
)
16+
from pyomnilogic_local.omnitypes import (
17+
BackyardState,
18+
)
19+
20+
21+
@click.command()
22+
@click.pass_context
23+
def backyard(ctx: click.Context) -> None:
24+
"""Display backyard-level information and equipment summary.
25+
26+
Shows overall backyard status including air temperature, system state,
27+
configuration checksum, MSP firmware version, and a summary of all
28+
installed equipment.
29+
30+
Example:
31+
omnilogic get backyard
32+
"""
33+
mspconfig: MSPConfig = ctx.obj["MSPCONFIG"]
34+
telemetry: Telemetry = ctx.obj["TELEMETRY"]
35+
36+
_print_backyard_info(mspconfig.backyard, telemetry.get_telem_by_systemid(mspconfig.backyard.system_id))
37+
38+
39+
def _print_backyard_info(backyardconfig: MSPBackyard, telemetry: TelemetryType | None) -> None:
40+
"""Format and print backyard information in a nice table format.
41+
42+
Args:
43+
backyard: Backyard object from MSPConfig with attributes to display
44+
telemetry: Telemetry object containing current state information
45+
"""
46+
click.echo("\n" + "=" * 60)
47+
click.echo("BACKYARD")
48+
click.echo("=" * 60)
49+
50+
# Combine config and telemetry data
51+
backyard_data: dict[Any, Any] = {**dict(backyardconfig), **dict(telemetry)} if telemetry else dict(backyardconfig)
52+
53+
# Fields to exclude from main display (we'll show equipment counts instead)
54+
exclude_fields = {"sensor", "bow", "colorlogic_light", "relay"}
55+
56+
for attr_name, value in backyard_data.items():
57+
if attr_name in exclude_fields:
58+
continue
59+
60+
if attr_name == "state":
61+
value = BackyardState(value).pretty()
62+
elif isinstance(value, list):
63+
# Format lists nicely
64+
value = ", ".join(str(v) for v in value) if value else "None"
65+
66+
# Format the attribute name to be more readable
67+
display_name = attr_name.replace("_", " ").title()
68+
click.echo(f"{display_name:20} : {value}")
69+
70+
# Show equipment summary
71+
click.echo("\nAttached Equipment:")
72+
click.echo("-" * 60)
73+
74+
equipment_counts = []
75+
76+
if backyardconfig.bow:
77+
equipment_counts.append(f"Bodies of Water: {len(backyardconfig.bow)}")
78+
for bow in backyardconfig.bow:
79+
equipment_counts.append(f" - {bow.name} ({bow.type})")
80+
81+
if backyardconfig.sensor:
82+
equipment_counts.append(f"Backyard Sensors: {len(backyardconfig.sensor)}")
83+
84+
if backyardconfig.colorlogic_light:
85+
equipment_counts.append(f"Backyard ColorLogic Lights: {len(backyardconfig.colorlogic_light)}")
86+
87+
if backyardconfig.relay:
88+
equipment_counts.append(f"Backyard Relays: {len(backyardconfig.relay)}")
89+
90+
if equipment_counts:
91+
for count in equipment_counts:
92+
click.echo(f" {count}")
93+
else:
94+
click.echo(" None")
95+
96+
click.echo("=" * 60)

pyomnilogic_local/cli/get/bows.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Need to figure out how to resolve the 'Untyped decorator makes function "..." untyped' errors in mypy when using click decorators
2+
# mypy: disable-error-code="misc"
3+
4+
from typing import Any
5+
6+
import click
7+
8+
from pyomnilogic_local.models.mspconfig import (
9+
MSPBoW,
10+
MSPConfig,
11+
)
12+
from pyomnilogic_local.models.telemetry import (
13+
Telemetry,
14+
TelemetryType,
15+
)
16+
from pyomnilogic_local.omnitypes import (
17+
BodyOfWaterType,
18+
)
19+
20+
21+
@click.command()
22+
@click.pass_context
23+
def bows(ctx: click.Context) -> None:
24+
"""List all Bodies of Water (BOWs) and their current status.
25+
26+
Displays information about all bodies of water including their system IDs,
27+
names, types (pool/spa), water temperature, flow status, and attached equipment.
28+
29+
Example:
30+
omnilogic get bows
31+
"""
32+
mspconfig: MSPConfig = ctx.obj["MSPCONFIG"]
33+
telemetry: Telemetry = ctx.obj["TELEMETRY"]
34+
35+
bows_found = False
36+
37+
# Check for BOWs in the backyard
38+
if mspconfig.backyard.bow:
39+
for bow in mspconfig.backyard.bow:
40+
bows_found = True
41+
_print_bow_info(bow, telemetry.get_telem_by_systemid(bow.system_id))
42+
43+
if not bows_found:
44+
click.echo("No Bodies of Water found in the system configuration.")
45+
46+
47+
def _print_bow_info(bow: MSPBoW, telemetry: TelemetryType | None) -> None:
48+
"""Format and print Body of Water information in a nice table format.
49+
50+
Args:
51+
bow: BOW object from MSPConfig with attributes to display
52+
telemetry: Telemetry object containing current state information
53+
"""
54+
click.echo("\n" + "=" * 60)
55+
click.echo("BODY OF WATER")
56+
click.echo("=" * 60)
57+
58+
# Combine config and telemetry data
59+
bow_data: dict[Any, Any] = {**dict(bow), **dict(telemetry)} if telemetry else dict(bow)
60+
61+
# Fields to exclude from main display (we'll show equipment counts instead)
62+
exclude_fields = {"filter", "relay", "heater", "sensor", "colorlogic_light", "pump", "chlorinator", "csad"}
63+
64+
for attr_name, value in bow_data.items():
65+
if attr_name in exclude_fields:
66+
continue
67+
68+
if attr_name == "type":
69+
value = BodyOfWaterType(value).pretty()
70+
elif isinstance(value, list):
71+
# Format lists nicely
72+
value = ", ".join(str(v) for v in value) if value else "None"
73+
74+
# Format the attribute name to be more readable
75+
display_name = attr_name.replace("_", " ").title()
76+
click.echo(f"{display_name:20} : {value}")
77+
78+
# Show equipment summary
79+
click.echo("\nAttached Equipment:")
80+
click.echo("-" * 60)
81+
82+
equipment_counts = []
83+
if bow.filter:
84+
equipment_counts.append(f"Filters: {len(bow.filter)}")
85+
if bow.pump:
86+
equipment_counts.append(f"Pumps: {len(bow.pump)}")
87+
if bow.heater:
88+
equipment_counts.append("Heater: 1 (virtual)")
89+
if bow.heater.heater_equipment:
90+
equipment_counts.append(f" - Physical Heaters: {len(bow.heater.heater_equipment)}")
91+
if bow.sensor:
92+
equipment_counts.append(f"Sensors: {len(bow.sensor)}")
93+
if bow.colorlogic_light:
94+
equipment_counts.append(f"ColorLogic Lights: {len(bow.colorlogic_light)}")
95+
if bow.relay:
96+
equipment_counts.append(f"Relays: {len(bow.relay)}")
97+
if bow.chlorinator:
98+
equipment_counts.append("Chlorinator: 1")
99+
if bow.csad:
100+
equipment_counts.append(f"CSADs: {len(bow.csad)}")
101+
102+
if equipment_counts:
103+
for count in equipment_counts:
104+
click.echo(f" {count}")
105+
else:
106+
click.echo(" None")
107+
108+
click.echo("=" * 60)

pyomnilogic_local/cli/get/commands.py

Lines changed: 13 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
import click
55

66
from pyomnilogic_local.cli import ensure_connection
7+
from pyomnilogic_local.cli.get.backyard import backyard
8+
from pyomnilogic_local.cli.get.bows import bows
9+
from pyomnilogic_local.cli.get.filters import filters
10+
from pyomnilogic_local.cli.get.heaters import heaters
11+
from pyomnilogic_local.cli.get.lights import lights
12+
from pyomnilogic_local.cli.get.valves import valves
713

814

915
@click.group()
@@ -19,62 +25,10 @@ def get(ctx: click.Context) -> None:
1925
ensure_connection(ctx)
2026

2127

22-
@get.command()
23-
@click.pass_context
24-
def lights(ctx: click.Context) -> None:
25-
"""List all ColorLogic lights and their current settings.
26-
27-
Displays information about all lights including their system IDs, names,
28-
current state, and available light shows.
29-
30-
Example:
31-
omnilogic get lights
32-
"""
33-
mspconfig = ctx.obj["MSPCONFIG"]
34-
35-
lights_found = False
36-
37-
# Check for lights in the backyard
38-
if mspconfig.backyard.colorlogic_light:
39-
for light in mspconfig.backyard.colorlogic_light:
40-
lights_found = True
41-
_print_light_info(light)
42-
43-
# Check for lights in Bodies of Water
44-
if mspconfig.backyard.bow:
45-
for bow in mspconfig.backyard.bow:
46-
if bow.colorlogic_light:
47-
for cl_light in bow.colorlogic_light:
48-
lights_found = True
49-
_print_light_info(cl_light)
50-
51-
if not lights_found:
52-
click.echo("No ColorLogic lights found in the system configuration.")
53-
54-
55-
def _print_light_info(light: object) -> None:
56-
"""Format and print light information in a nice table format.
57-
58-
Args:
59-
light: Light object from MSPConfig with attributes to display
60-
"""
61-
click.echo("\n" + "=" * 60)
62-
for attr_name in dir(light):
63-
# Skip private/magic attributes and methods
64-
if attr_name.startswith("_") or callable(getattr(light, attr_name)):
65-
continue
66-
67-
value = getattr(light, attr_name)
68-
69-
# Special handling for show lists - convert to readable format
70-
if attr_name == "current_show" and isinstance(value, list):
71-
show_names = [show.name if hasattr(show, "name") else str(show) for show in value]
72-
value = ", ".join(show_names) if show_names else "None"
73-
elif isinstance(value, list):
74-
# Format other lists nicely
75-
value = ", ".join(str(v) for v in value) if value else "None"
76-
77-
# Format the attribute name to be more readable
78-
display_name = attr_name.replace("_", " ").title()
79-
click.echo(f"{display_name:20} : {value}")
80-
click.echo("=" * 60)
28+
# Register subcommands
29+
get.add_command(backyard)
30+
get.add_command(bows)
31+
get.add_command(filters)
32+
get.add_command(heaters)
33+
get.add_command(lights)
34+
get.add_command(valves)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Need to figure out how to resolve the 'Untyped decorator makes function "..." untyped' errors in mypy when using click decorators
2+
# mypy: disable-error-code="misc"
3+
4+
from typing import Any
5+
6+
import click
7+
8+
from pyomnilogic_local.models.mspconfig import (
9+
MSPConfig,
10+
MSPFilter,
11+
)
12+
from pyomnilogic_local.models.telemetry import (
13+
Telemetry,
14+
TelemetryType,
15+
)
16+
from pyomnilogic_local.omnitypes import (
17+
FilterState,
18+
FilterType,
19+
FilterValvePosition,
20+
FilterWhyOn,
21+
)
22+
23+
24+
@click.command()
25+
@click.pass_context
26+
def filters(ctx: click.Context) -> None:
27+
"""List all filters and their current settings.
28+
29+
Displays information about all filters including their system IDs, names,
30+
current state, speed, valve position, and power usage.
31+
32+
Example:
33+
omnilogic get filters
34+
"""
35+
mspconfig: MSPConfig = ctx.obj["MSPCONFIG"]
36+
telemetry: Telemetry = ctx.obj["TELEMETRY"]
37+
38+
filters_found = False
39+
40+
# Check for filters in Bodies of Water
41+
if mspconfig.backyard.bow:
42+
for bow in mspconfig.backyard.bow:
43+
if bow.filter:
44+
for filt in bow.filter:
45+
filters_found = True
46+
_print_filter_info(filt, telemetry.get_telem_by_systemid(filt.system_id))
47+
48+
if not filters_found:
49+
click.echo("No filters found in the system configuration.")
50+
51+
52+
def _print_filter_info(filt: MSPFilter, telemetry: TelemetryType | None) -> None:
53+
"""Format and print filter information in a nice table format.
54+
55+
Args:
56+
filt: Filter object from MSPConfig with attributes to display
57+
telemetry: Telemetry object containing current state information
58+
"""
59+
click.echo("\n" + "=" * 60)
60+
click.echo("FILTER")
61+
click.echo("=" * 60)
62+
63+
filter_data: dict[Any, Any] = {**dict(filt), **dict(telemetry)} if telemetry else dict(filt)
64+
for attr_name, value in filter_data.items():
65+
if attr_name == "state":
66+
value = FilterState(value).pretty()
67+
elif attr_name == "type":
68+
value = FilterType(value).pretty()
69+
elif attr_name == "valve_position":
70+
value = FilterValvePosition(value).pretty()
71+
elif attr_name == "why_on":
72+
value = FilterWhyOn(value).pretty()
73+
elif isinstance(value, list):
74+
# Format lists nicely
75+
value = ", ".join(str(v) for v in value) if value else "None"
76+
77+
# Format the attribute name to be more readable
78+
display_name = attr_name.replace("_", " ").title()
79+
click.echo(f"{display_name:20} : {value}")
80+
click.echo("=" * 60)

0 commit comments

Comments
 (0)