Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,5 @@ cython_debug/
# Testing files for PCAP parseer
*.pcap
*.pcapng

dev_files/
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,5 @@ repos:
hooks:
- id: mypy
exclude: cli.py
additional_dependencies: [ "pydantic>=1.10.17", "pytest>=8.0.0" ]
additional_dependencies: [ "pydantic>=2.0.0", "pytest>=8.0.0" ]
args: [ "--config-file=./pyproject.toml", "--follow-imports=silent", "--strict", "--ignore-missing-imports", "--disallow-subclassing-any", "--no-warn-return-any" ]
226 changes: 169 additions & 57 deletions poetry.lock

Large diffs are not rendered by default.

96 changes: 96 additions & 0 deletions pyomnilogic_local/cli/get/backyard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# 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 typing import Any

import click

from pyomnilogic_local.models.mspconfig import (
MSPBackyard,
MSPConfig,
)
from pyomnilogic_local.models.telemetry import (
Telemetry,
TelemetryType,
)
from pyomnilogic_local.omnitypes import (
BackyardState,
)


@click.command()
@click.pass_context
def backyard(ctx: click.Context) -> None:
"""Display backyard-level information and equipment summary.

Shows overall backyard status including air temperature, system state,
configuration checksum, MSP firmware version, and a summary of all
installed equipment.

Example:
omnilogic get backyard
"""
mspconfig: MSPConfig = ctx.obj["MSPCONFIG"]
telemetry: Telemetry = ctx.obj["TELEMETRY"]

_print_backyard_info(mspconfig.backyard, telemetry.get_telem_by_systemid(mspconfig.backyard.system_id))


def _print_backyard_info(backyardconfig: MSPBackyard, telemetry: TelemetryType | None) -> None:
"""Format and print backyard information in a nice table format.

Args:
backyard: Backyard object from MSPConfig with attributes to display
telemetry: Telemetry object containing current state information
"""
click.echo("\n" + "=" * 60)
click.echo("BACKYARD")
click.echo("=" * 60)

# Combine config and telemetry data
backyard_data: dict[Any, Any] = {**dict(backyardconfig), **dict(telemetry)} if telemetry else dict(backyardconfig)

# Fields to exclude from main display (we'll show equipment counts instead)
exclude_fields = {"sensor", "bow", "colorlogic_light", "relay"}

for attr_name, value in backyard_data.items():
if attr_name in exclude_fields:
continue

if attr_name == "state":
value = BackyardState(value).pretty()
elif isinstance(value, list):
# Format lists nicely
value = ", ".join(str(v) for v in value) if value else "None"

# Format the attribute name to be more readable
display_name = attr_name.replace("_", " ").title()
click.echo(f"{display_name:20} : {value}")

# Show equipment summary
click.echo("\nAttached Equipment:")
click.echo("-" * 60)

equipment_counts = []

if backyardconfig.bow:
equipment_counts.append(f"Bodies of Water: {len(backyardconfig.bow)}")
for bow in backyardconfig.bow:
equipment_counts.append(f" - {bow.name} ({bow.type})")

if backyardconfig.sensor:
equipment_counts.append(f"Backyard Sensors: {len(backyardconfig.sensor)}")

if backyardconfig.colorlogic_light:
equipment_counts.append(f"Backyard ColorLogic Lights: {len(backyardconfig.colorlogic_light)}")

if backyardconfig.relay:
equipment_counts.append(f"Backyard Relays: {len(backyardconfig.relay)}")

if equipment_counts:
for count in equipment_counts:
click.echo(f" {count}")
else:
click.echo(" None")

click.echo("=" * 60)
108 changes: 108 additions & 0 deletions pyomnilogic_local/cli/get/bows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# 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 typing import Any

import click

from pyomnilogic_local.models.mspconfig import (
MSPBoW,
MSPConfig,
)
from pyomnilogic_local.models.telemetry import (
Telemetry,
TelemetryType,
)
from pyomnilogic_local.omnitypes import (
BodyOfWaterType,
)


@click.command()
@click.pass_context
def bows(ctx: click.Context) -> None:
"""List all Bodies of Water (BOWs) and their current status.

Displays information about all bodies of water including their system IDs,
names, types (pool/spa), water temperature, flow status, and attached equipment.

Example:
omnilogic get bows
"""
mspconfig: MSPConfig = ctx.obj["MSPCONFIG"]
telemetry: Telemetry = ctx.obj["TELEMETRY"]

bows_found = False

# Check for BOWs in the backyard
if mspconfig.backyard.bow:
for bow in mspconfig.backyard.bow:
bows_found = True
_print_bow_info(bow, telemetry.get_telem_by_systemid(bow.system_id))

if not bows_found:
click.echo("No Bodies of Water found in the system configuration.")


def _print_bow_info(bow: MSPBoW, telemetry: TelemetryType | None) -> None:
"""Format and print Body of Water information in a nice table format.

Args:
bow: BOW object from MSPConfig with attributes to display
telemetry: Telemetry object containing current state information
"""
click.echo("\n" + "=" * 60)
click.echo("BODY OF WATER")
click.echo("=" * 60)

# Combine config and telemetry data
bow_data: dict[Any, Any] = {**dict(bow), **dict(telemetry)} if telemetry else dict(bow)

# Fields to exclude from main display (we'll show equipment counts instead)
exclude_fields = {"filter", "relay", "heater", "sensor", "colorlogic_light", "pump", "chlorinator", "csad"}

for attr_name, value in bow_data.items():
if attr_name in exclude_fields:
continue

if attr_name == "type":
value = BodyOfWaterType(value).pretty()
elif isinstance(value, list):
# Format lists nicely
value = ", ".join(str(v) for v in value) if value else "None"

# Format the attribute name to be more readable
display_name = attr_name.replace("_", " ").title()
click.echo(f"{display_name:20} : {value}")

# Show equipment summary
click.echo("\nAttached Equipment:")
click.echo("-" * 60)

equipment_counts = []
if bow.filter:
equipment_counts.append(f"Filters: {len(bow.filter)}")
if bow.pump:
equipment_counts.append(f"Pumps: {len(bow.pump)}")
if bow.heater:
equipment_counts.append("Heater: 1 (virtual)")
if bow.heater.heater_equipment:
equipment_counts.append(f" - Physical Heaters: {len(bow.heater.heater_equipment)}")
if bow.sensor:
equipment_counts.append(f"Sensors: {len(bow.sensor)}")
if bow.colorlogic_light:
equipment_counts.append(f"ColorLogic Lights: {len(bow.colorlogic_light)}")
if bow.relay:
equipment_counts.append(f"Relays: {len(bow.relay)}")
if bow.chlorinator:
equipment_counts.append("Chlorinator: 1")
if bow.csad:
equipment_counts.append(f"CSADs: {len(bow.csad)}")

if equipment_counts:
for count in equipment_counts:
click.echo(f" {count}")
else:
click.echo(" None")

click.echo("=" * 60)
72 changes: 13 additions & 59 deletions pyomnilogic_local/cli/get/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
import click

from pyomnilogic_local.cli import ensure_connection
from pyomnilogic_local.cli.get.backyard import backyard
from pyomnilogic_local.cli.get.bows import bows
from pyomnilogic_local.cli.get.filters import filters
from pyomnilogic_local.cli.get.heaters import heaters
from pyomnilogic_local.cli.get.lights import lights
from pyomnilogic_local.cli.get.valves import valves


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


@get.command()
@click.pass_context
def lights(ctx: click.Context) -> None:
"""List all ColorLogic lights and their current settings.

Displays information about all lights including their system IDs, names,
current state, and available light shows.

Example:
omnilogic get lights
"""
mspconfig = ctx.obj["MSPCONFIG"]

lights_found = False

# Check for lights in the backyard
if mspconfig.backyard.colorlogic_light:
for light in mspconfig.backyard.colorlogic_light:
lights_found = True
_print_light_info(light)

# Check for lights in Bodies of Water
if mspconfig.backyard.bow:
for bow in mspconfig.backyard.bow:
if bow.colorlogic_light:
for cl_light in bow.colorlogic_light:
lights_found = True
_print_light_info(cl_light)

if not lights_found:
click.echo("No ColorLogic lights found in the system configuration.")


def _print_light_info(light: object) -> None:
"""Format and print light information in a nice table format.

Args:
light: Light object from MSPConfig with attributes to display
"""
click.echo("\n" + "=" * 60)
for attr_name in dir(light):
# Skip private/magic attributes and methods
if attr_name.startswith("_") or callable(getattr(light, attr_name)):
continue

value = getattr(light, attr_name)

# Special handling for show lists - convert to readable format
if attr_name == "current_show" and isinstance(value, list):
show_names = [show.name if hasattr(show, "name") else str(show) for show in value]
value = ", ".join(show_names) if show_names else "None"
elif isinstance(value, list):
# Format other lists nicely
value = ", ".join(str(v) for v in value) if value else "None"

# Format the attribute name to be more readable
display_name = attr_name.replace("_", " ").title()
click.echo(f"{display_name:20} : {value}")
click.echo("=" * 60)
# Register subcommands
get.add_command(backyard)
get.add_command(bows)
get.add_command(filters)
get.add_command(heaters)
get.add_command(lights)
get.add_command(valves)
80 changes: 80 additions & 0 deletions pyomnilogic_local/cli/get/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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 typing import Any

import click

from pyomnilogic_local.models.mspconfig import (
MSPConfig,
MSPFilter,
)
from pyomnilogic_local.models.telemetry import (
Telemetry,
TelemetryType,
)
from pyomnilogic_local.omnitypes import (
FilterState,
FilterType,
FilterValvePosition,
FilterWhyOn,
)


@click.command()
@click.pass_context
def filters(ctx: click.Context) -> None:
"""List all filters and their current settings.

Displays information about all filters including their system IDs, names,
current state, speed, valve position, and power usage.

Example:
omnilogic get filters
"""
mspconfig: MSPConfig = ctx.obj["MSPCONFIG"]
telemetry: Telemetry = ctx.obj["TELEMETRY"]

filters_found = False

# Check for filters in Bodies of Water
if mspconfig.backyard.bow:
for bow in mspconfig.backyard.bow:
if bow.filter:
for filt in bow.filter:
filters_found = True
_print_filter_info(filt, telemetry.get_telem_by_systemid(filt.system_id))

if not filters_found:
click.echo("No filters found in the system configuration.")


def _print_filter_info(filt: MSPFilter, telemetry: TelemetryType | None) -> None:
"""Format and print filter information in a nice table format.

Args:
filt: Filter object from MSPConfig with attributes to display
telemetry: Telemetry object containing current state information
"""
click.echo("\n" + "=" * 60)
click.echo("FILTER")
click.echo("=" * 60)

filter_data: dict[Any, Any] = {**dict(filt), **dict(telemetry)} if telemetry else dict(filt)
for attr_name, value in filter_data.items():
if attr_name == "state":
value = FilterState(value).pretty()
elif attr_name == "type":
value = FilterType(value).pretty()
elif attr_name == "valve_position":
value = FilterValvePosition(value).pretty()
elif attr_name == "why_on":
value = FilterWhyOn(value).pretty()
elif isinstance(value, list):
# Format lists nicely
value = ", ".join(str(v) for v in value) if value else "None"

# Format the attribute name to be more readable
display_name = attr_name.replace("_", " ").title()
click.echo(f"{display_name:20} : {value}")
click.echo("=" * 60)
Loading