Skip to content

Commit e050e3e

Browse files
committed
feat: additional work on new interaction layer
1 parent ac80788 commit e050e3e

File tree

22 files changed

+488
-106
lines changed

22 files changed

+488
-106
lines changed

pyomnilogic_local/_base.py

Lines changed: 102 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,116 @@
1-
from typing import Any
1+
import logging
2+
from typing import Generic, TypeVar, cast
23

4+
from pyomnilogic_local.api.api import OmniLogicAPI
35
from pyomnilogic_local.models import MSPEquipmentType, Telemetry
6+
from pyomnilogic_local.models.telemetry import TelemetryType
47

8+
# Define type variables for generic equipment types
9+
MSPConfigT = TypeVar("MSPConfigT", bound=MSPEquipmentType)
10+
TelemetryT = TypeVar("TelemetryT", bound=TelemetryType | None)
511

6-
class OmniEquipment:
7-
"""Base class for OmniLogic equipment."""
812

9-
def __init__(self, mspconfig: MSPEquipmentType, telemetry: Telemetry | None = None) -> None:
10-
"""Initialize the equipment with configuration and telemetry data."""
11-
# If the Equipment has subdevices, we don't store those as part of this device's config
12-
# They will get parsed and stored as their own equipment instances
13+
_LOGGER = logging.getLogger(__name__)
14+
15+
16+
class OmniEquipment(Generic[MSPConfigT, TelemetryT]):
17+
"""Base class for OmniLogic equipment.
18+
19+
Generic parameters:
20+
MSPConfigT: The specific MSP configuration type (e.g., MSPBoW, MSPRelay)
21+
TelemetryT: The specific telemetry type (e.g., TelemetryBoW, TelemetryRelay, or None for equipment without telemetry)
22+
"""
23+
24+
mspconfig: MSPConfigT
25+
telemetry: TelemetryT
26+
27+
# Use a forward reference for the type hint to avoid issues with self-referential generics
28+
child_equipment: dict[int, "OmniEquipment[MSPConfigT, TelemetryT]"]
29+
30+
def __init__(self, _api: OmniLogicAPI, mspconfig: MSPConfigT, telemetry: Telemetry | None) -> None:
31+
"""Initialize the equipment with configuration and telemetry data.
32+
33+
Args:
34+
_api: The OmniLogic API instance
35+
mspconfig: The MSP configuration for this specific equipment
36+
telemetry: The full Telemetry object containing all equipment telemetry
37+
"""
38+
self._api = _api
39+
40+
self.update_config(mspconfig)
41+
42+
if telemetry is not None:
43+
self.update_telemetry(telemetry)
44+
45+
@property
46+
def bow_id(self) -> int | None:
47+
"""The bow ID of the equipment."""
48+
return self.mspconfig.bow_id
49+
50+
@property
51+
def name(self) -> str | None:
52+
"""The name of the equipment."""
53+
return self.mspconfig.name
54+
55+
@property
56+
def system_id(self) -> int | None:
57+
"""The system ID of the equipment."""
58+
return self.mspconfig.system_id
59+
60+
@property
61+
def omni_type(self) -> str | None:
62+
"""The OmniType of the equipment."""
63+
return self.mspconfig.omni_type
64+
65+
def update(self, mspconfig: MSPConfigT, telemetry: Telemetry | None) -> None:
66+
"""Update both the configuration and telemetry data for the equipment."""
67+
self.update_config(mspconfig)
68+
if telemetry is not None:
69+
self.update_telemetry(telemetry)
70+
71+
self._update_equipment(mspconfig, telemetry)
72+
73+
def _update_equipment(self, mspconfig: MSPConfigT, telemetry: Telemetry | None) -> None:
74+
"""Hook to allow classes to trigger updates of sub-equipment."""
75+
76+
def update_config(self, mspconfig: MSPConfigT) -> None:
77+
"""Update the configuration data for the equipment."""
1378
try:
14-
self.mspconfig = mspconfig.without_subdevices()
79+
# If the Equipment has subdevices, we don't store those as part of this device's config
80+
# They will get parsed and stored as their own equipment instances
81+
self.mspconfig = cast(MSPConfigT, mspconfig.without_subdevices())
1582
except AttributeError:
1683
self.mspconfig = mspconfig
1784

18-
if hasattr(self, "telemetry") and telemetry is not None:
19-
self.telemetry = telemetry.get_telem_by_systemid(self.mspconfig.system_id)
20-
21-
# Populate fields from MSP configuration and telemetry
22-
# This is some moderate magic to avoid having to manually set each field
23-
# The TL;DR is that we loop over all fields defined in the MSPConfig and Telemetry models
24-
# and set the corresponding attributes on this equipment instance.
25-
for field in self.mspconfig.__class__.model_fields:
26-
if getattr(self.mspconfig, field, None) is not None:
27-
setattr(self, field, self._from_mspconfig(field))
28-
for field in self.mspconfig.__class__.model_computed_fields:
29-
if getattr(self.mspconfig, field, None) is not None:
30-
setattr(self, field, self._from_mspconfig(field))
31-
if hasattr(self, "telemetry") and self.telemetry is not None:
32-
for field in self.telemetry.__class__.model_fields:
33-
if getattr(self.telemetry, field, None) is not None:
34-
setattr(self, field, self._from_telemetry(field))
35-
for field in self.telemetry.__class__.model_computed_fields:
36-
if getattr(self.telemetry, field, None) is not None:
37-
setattr(self, field, self._from_telemetry(field))
38-
39-
def update_config(self, mspconfig: MSPEquipmentType) -> None:
40-
"""Update the configuration data for the equipment."""
41-
if hasattr(self, "mspconfig"):
42-
self.mspconfig = mspconfig.without_subdevices()
43-
else:
44-
raise NotImplementedError("This equipment does not have MSP configuration.")
45-
4685
def update_telemetry(self, telemetry: Telemetry) -> None:
4786
"""Update the telemetry data for the equipment."""
48-
if hasattr(self, "telemetry"):
49-
self.telemetry = telemetry.get_telem_by_systemid(self.mspconfig.system_id)
87+
# Only update telemetry if this equipment type has telemetry
88+
# if hasattr(self, "telemetry"):
89+
# Extract the specific telemetry for this equipment from the full telemetry object
90+
# Note: Some equipment (like sensors) don't have their own telemetry, so this may be None
91+
if specific_telemetry := telemetry.get_telem_by_systemid(self.mspconfig.system_id) is not None:
92+
self.telemetry = cast(TelemetryT, specific_telemetry)
5093
else:
51-
raise NotImplementedError("This equipment does not have telemetry data.")
94+
self.telemetry = cast(TelemetryT, None)
95+
# else:
96+
# raise NotImplementedError("This equipment does not have telemetry data.")
5297

53-
def _from_mspconfig(self, attribute: str) -> Any:
54-
"""Helper method to get a value from the MSP configuration."""
55-
return getattr(self.mspconfig, attribute, None)
98+
# def _update_equipment(self, telemetry: Telemetry) -> None:
99+
# pass
56100

57-
def _from_telemetry(self, attribute: str) -> Any:
58-
"""Helper method to get a value from the telemetry data."""
59-
if hasattr(self, "telemetry"):
60-
return getattr(self.telemetry, attribute, None)
61-
return None
101+
# def _update_equipment(self, telemetry: Telemetry) -> None:
102+
# """Update any child equipment based on the latest MSPConfig and Telemetry data."""
103+
# for _, equipment_mspconfig in self.mspconfig:
104+
# system_id = equipment_mspconfig.system_id
105+
# if system_id is None:
106+
# _LOGGER.debug("Skipping equipment update: system_id is None: %s", equipment_mspconfig)
107+
# continue
108+
# if system_id in self.child_equipment:
109+
# # Update existing child equipment
110+
# child_equipment = self.child_equipment[system_id]
111+
# if child_equipment is not None:
112+
# child_equipment.update_config(equipment_mspconfig)
113+
# if hasattr(self, "telemetry"):
114+
# child_equipment.update_telemetry(telemetry)
115+
# else:
116+
# equipment = create_equipment(self, equipment_mspconfig, telemetry)

pyomnilogic_local/api/api.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,9 @@
1212

1313
from ..omnitypes import (
1414
ColorLogicBrightness,
15-
ColorLogicShow25,
16-
ColorLogicShow40,
17-
ColorLogicShowUCL,
18-
ColorLogicShowUCLV2,
1915
ColorLogicSpeed,
2016
HeaterMode,
17+
LightShows,
2118
MessageType,
2219
)
2320
from .protocol import OmniLogicProtocol
@@ -380,7 +377,7 @@ async def async_set_light_show(
380377
self,
381378
pool_id: int,
382379
equipment_id: int,
383-
show: ColorLogicShow25 | ColorLogicShow40 | ColorLogicShowUCL | ColorLogicShowUCLV2,
380+
show: LightShows,
384381
speed: ColorLogicSpeed = ColorLogicSpeed.ONE_TIMES,
385382
brightness: ColorLogicBrightness = ColorLogicBrightness.ONE_HUNDRED_PERCENT,
386383
reserved: int = 0,

pyomnilogic_local/backyard.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
from pyomnilogic_local.api.api import OmniLogicAPI
12
from pyomnilogic_local.models.mspconfig import MSPBackyard
2-
from pyomnilogic_local.models.telemetry import Telemetry
3+
from pyomnilogic_local.models.telemetry import Telemetry, TelemetryBackyard
34

45
from ._base import OmniEquipment
56
from .bow import Bow
@@ -8,16 +9,16 @@
89
from .sensor import Sensor
910

1011

11-
class Backyard(OmniEquipment):
12+
class Backyard(OmniEquipment[MSPBackyard, TelemetryBackyard]):
1213
"""Represents the backyard equipment in the OmniLogic system."""
1314

1415
bow: list[Bow] = []
1516
lights: list[ColorLogicLight] = []
1617
relays: list[Relay] = []
1718
sensors: list[Sensor] = []
1819

19-
def __init__(self, mspconfig: MSPBackyard, telemetry: Telemetry) -> None:
20-
super().__init__(mspconfig, telemetry)
20+
def __init__(self, _api: OmniLogicAPI, mspconfig: MSPBackyard, telemetry: Telemetry) -> None:
21+
super().__init__(_api, mspconfig, telemetry)
2122

2223
self._update_bows(mspconfig, telemetry)
2324
self._update_relays(mspconfig, telemetry)
@@ -29,20 +30,20 @@ def _update_bows(self, mspconfig: MSPBackyard, telemetry: Telemetry) -> None:
2930
self.bow = []
3031
return
3132

32-
self.bow = [Bow(bow, telemetry) for bow in mspconfig.bow]
33+
self.bow = [Bow(self._api, bow, telemetry) for bow in mspconfig.bow]
3334

3435
def _update_relays(self, mspconfig: MSPBackyard, telemetry: Telemetry) -> None:
3536
"""Update the relays based on the MSP configuration."""
3637
if mspconfig.relay is None:
3738
self.relays = []
3839
return
3940

40-
self.relays = [Relay(relay, telemetry) for relay in mspconfig.relay]
41+
self.relays = [Relay(self._api, relay, telemetry) for relay in mspconfig.relay]
4142

4243
def _update_sensors(self, mspconfig: MSPBackyard, telemetry: Telemetry) -> None:
43-
"""Update the sensors, bows, lights, and relays based on the MSP configuration."""
44+
"""Update the sensors based on the MSP configuration."""
4445
if mspconfig.sensor is None:
4546
self.sensors = []
4647
return
4748

48-
self.sensors = [Sensor(sensor, telemetry) for sensor in mspconfig.sensor]
49+
self.sensors = [Sensor(self._api, sensor, telemetry) for sensor in mspconfig.sensor]

pyomnilogic_local/bow.py

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,110 @@
11
from pyomnilogic_local._base import OmniEquipment
2+
from pyomnilogic_local.api.api import OmniLogicAPI
3+
from pyomnilogic_local.chlorinator import Chlorinator
4+
from pyomnilogic_local.colorlogiclight import _LOGGER, ColorLogicLight
5+
from pyomnilogic_local.csad import CSAD
6+
from pyomnilogic_local.filter import Filter
7+
from pyomnilogic_local.heater import Heater
8+
from pyomnilogic_local.models.mspconfig import MSPBoW
9+
from pyomnilogic_local.models.telemetry import Telemetry, TelemetryBoW
10+
from pyomnilogic_local.pump import Pump
11+
from pyomnilogic_local.relay import Relay
12+
from pyomnilogic_local.sensor import Sensor
213

314

4-
class Bow(OmniEquipment):
15+
class Bow(OmniEquipment[MSPBoW, TelemetryBoW]):
516
"""Represents a bow in the OmniLogic system."""
17+
18+
filters: list[Filter] = []
19+
heater: Heater | None = None
20+
relays: list[Relay] = []
21+
sensors: list[Sensor] = []
22+
lights: list[ColorLogicLight] = []
23+
pumps: list[Pump] = []
24+
chlorinator: Chlorinator | None = None
25+
csads: list[CSAD] = []
26+
27+
def __init__(self, _api: OmniLogicAPI, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
28+
super().__init__(_api, mspconfig, telemetry)
29+
30+
@property
31+
def equip_type(self) -> str:
32+
"""The equipment type of the bow."""
33+
return self.mspconfig.equip_type
34+
35+
def _update_equipment(self, mspconfig: MSPBoW, telemetry: Telemetry | None) -> None:
36+
"""Update both the configuration and telemetry data for the equipment."""
37+
if telemetry is None:
38+
_LOGGER.warning("No telemetry provided to update Bow equipment.")
39+
return
40+
self._update_filters(self.mspconfig, telemetry)
41+
self._update_heater(self.mspconfig, telemetry)
42+
self._update_sensors(self.mspconfig, telemetry)
43+
self._update_lights(self.mspconfig, telemetry)
44+
self._update_pumps(self.mspconfig, telemetry)
45+
self._update_chlorinators(self.mspconfig, telemetry)
46+
self._update_csads(self.mspconfig, telemetry)
47+
48+
def _update_filters(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
49+
"""Update the filters based on the MSP configuration."""
50+
if mspconfig.filter is None:
51+
self.filters = []
52+
return
53+
54+
self.filters = [Filter(self._api, filter_, telemetry) for filter_ in mspconfig.filter]
55+
56+
def _update_heater(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
57+
"""Update the heater based on the MSP configuration."""
58+
if mspconfig.heater is None:
59+
self.heater = None
60+
return
61+
62+
self.heater = Heater(self._api, mspconfig.heater, telemetry)
63+
64+
def _update_relays(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
65+
"""Update the relays based on the MSP configuration."""
66+
if mspconfig.relay is None:
67+
self.relays = []
68+
return
69+
70+
self.relays = [Relay(self._api, relay, telemetry) for relay in mspconfig.relay]
71+
72+
def _update_sensors(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
73+
"""Update the sensors based on the MSP configuration."""
74+
if mspconfig.sensor is None:
75+
self.sensors = []
76+
return
77+
78+
self.sensors = [Sensor(self._api, sensor, telemetry) for sensor in mspconfig.sensor]
79+
80+
def _update_lights(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
81+
"""Update the lights based on the MSP configuration."""
82+
if mspconfig.colorlogic_light is None:
83+
self.lights = []
84+
return
85+
86+
self.lights = [ColorLogicLight(self._api, light, telemetry) for light in mspconfig.colorlogic_light]
87+
88+
def _update_pumps(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
89+
"""Update the pumps based on the MSP configuration."""
90+
if mspconfig.pump is None:
91+
self.pumps = []
92+
return
93+
94+
self.pumps = [Pump(self._api, pump, telemetry) for pump in mspconfig.pump]
95+
96+
def _update_chlorinators(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
97+
"""Update the chlorinators based on the MSP configuration."""
98+
if mspconfig.chlorinator is None:
99+
self.chlorinator = None
100+
return
101+
102+
self.chlorinator = Chlorinator(self._api, mspconfig.chlorinator, telemetry)
103+
104+
def _update_csads(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
105+
"""Update the CSADs based on the MSP configuration."""
106+
if mspconfig.csad is None:
107+
self.csads = []
108+
return
109+
110+
self.csads = [CSAD(self._api, csad, telemetry) for csad in mspconfig.csad]

pyomnilogic_local/chlorinator.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from pyomnilogic_local._base import OmniEquipment
2+
from pyomnilogic_local.models.mspconfig import MSPChlorinator
3+
from pyomnilogic_local.models.telemetry import TelemetryChlorinator
4+
5+
6+
class Chlorinator(OmniEquipment[MSPChlorinator, TelemetryChlorinator]):
7+
"""Represents a chlorinator in the OmniLogic system."""

pyomnilogic_local/cli/get/backyard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def _print_backyard_info(backyardconfig: MSPBackyard, telemetry: TelemetryType |
7676
if backyardconfig.bow:
7777
equipment_counts.append(f"Bodies of Water: {len(backyardconfig.bow)}")
7878
for bow in backyardconfig.bow:
79-
equipment_counts.append(f" - {bow.name} ({bow.type})")
79+
equipment_counts.append(f" - {bow.name} ({bow.equip_type})")
8080

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

pyomnilogic_local/cli/get/lights.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def _print_light_info(light: MSPColorLogicLight, telemetry: TelemetryColorLogicL
7474
show_names = [show.pretty() if hasattr(show, "pretty") else str(show) for show in value]
7575
value = ", ".join(show_names) if show_names else "None"
7676
elif attr_name == "show" and value is not None:
77-
value = telemetry.show_name(light.type, light.v2_active, True) if telemetry else str(value)
77+
value = telemetry.show_name(light.equip_type, light.v2_active, True) if telemetry else str(value)
7878
elif attr_name == "speed":
7979
value = ColorLogicSpeed(value).pretty()
8080
elif attr_name == "state":

0 commit comments

Comments
 (0)