Skip to content

Commit f748c54

Browse files
committed
feat: add csad-equipment support
1 parent 0e8237f commit f748c54

6 files changed

Lines changed: 156 additions & 2 deletions

File tree

pyomnilogic_local/csad.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
from typing import TYPE_CHECKING
2+
13
from pyomnilogic_local._base import OmniEquipment
4+
from pyomnilogic_local.collections import EquipmentDict
5+
from pyomnilogic_local.csad_equip import CSADEquipment
26
from pyomnilogic_local.models.mspconfig import MSPCSAD
3-
from pyomnilogic_local.models.telemetry import TelemetryCSAD
7+
from pyomnilogic_local.models.telemetry import Telemetry, TelemetryCSAD
48
from pyomnilogic_local.omnitypes import CSADMode, CSADStatus, CSADType
59

10+
if TYPE_CHECKING:
11+
from pyomnilogic_local.omnilogic import OmniLogic
12+
613

714
class CSAD(OmniEquipment[MSPCSAD, TelemetryCSAD]):
815
"""Represents a CSAD (Chemistry Sense and Dispense) system in the OmniLogic system.
@@ -20,6 +27,7 @@ class CSAD(OmniEquipment[MSPCSAD, TelemetryCSAD]):
2027
Attributes:
2128
mspconfig: The MSP configuration for this CSAD
2229
telemetry: Real-time telemetry data for this CSAD
30+
csad_equipment: Collection of physical CSAD equipment devices
2331
2432
Example:
2533
>>> csad = pool.get_csad()
@@ -31,6 +39,24 @@ class CSAD(OmniEquipment[MSPCSAD, TelemetryCSAD]):
3139

3240
mspconfig: MSPCSAD
3341
telemetry: TelemetryCSAD
42+
csad_equipment: EquipmentDict[CSADEquipment] = EquipmentDict()
43+
44+
def __init__(self, omni: "OmniLogic", mspconfig: MSPCSAD, telemetry: Telemetry) -> None:
45+
super().__init__(omni, mspconfig, telemetry)
46+
47+
def _update_equipment(self, mspconfig: MSPCSAD, telemetry: Telemetry | None) -> None:
48+
"""Update both the configuration and telemetry data for the equipment."""
49+
if telemetry is None:
50+
return
51+
self._update_csad_equipment(mspconfig, telemetry)
52+
53+
def _update_csad_equipment(self, mspconfig: MSPCSAD, telemetry: Telemetry) -> None:
54+
"""Update the CSAD equipment based on the MSP configuration."""
55+
if mspconfig.csad_equipment is None:
56+
self.csad_equipment = EquipmentDict()
57+
return
58+
59+
self.csad_equipment = EquipmentDict([CSADEquipment(self._omni, equip, telemetry) for equip in mspconfig.csad_equipment])
3460

3561
# Expose MSPConfig attributes
3662
@property

pyomnilogic_local/csad_equip.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""CSAD equipment classes for Omnilogic."""
2+
3+
from typing import TYPE_CHECKING, Literal
4+
5+
from pyomnilogic_local._base import OmniEquipment
6+
from pyomnilogic_local.models.mspconfig import MSPCSADEquip
7+
from pyomnilogic_local.models.telemetry import Telemetry
8+
from pyomnilogic_local.omnitypes import CSADEquipmentType
9+
10+
if TYPE_CHECKING:
11+
from pyomnilogic_local.omnilogic import OmniLogic
12+
13+
14+
class CSADEquipment(OmniEquipment[MSPCSADEquip, None]):
15+
"""Represents a CSAD (chemical automation) equipment device.
16+
17+
CSADEquipment represents an individual physical CSAD device (e.g., AQL-CHEM).
18+
It is controlled by a parent CSAD which manages one or more physical CSAD units.
19+
20+
The OmniLogic system uses a parent/child CSAD architecture:
21+
- CSAD: User-facing CSAD control (monitoring, dispensing)
22+
- CSADEquipment: Individual physical CSAD devices managed by the parent
23+
24+
CSAD Equipment Types:
25+
- AQL_CHEM: AquaLink Chemistry System
26+
27+
Note: Like chlorinator equipment, CSAD equipment does not have separate
28+
telemetry entries. All telemetry is reported through the parent CSAD.
29+
30+
Attributes:
31+
mspconfig: Configuration data for this physical CSAD equipment
32+
33+
Properties (Configuration):
34+
equip_type: Equipment type (always "PET_CSAD")
35+
csad_type: Type of CSAD equipment (e.g., AQL_CHEM)
36+
enabled: Whether this CSAD equipment is enabled
37+
38+
Example:
39+
>>> pool = omni.backyard.bow["Pool"]
40+
>>> csad = pool.get_csad()
41+
>>>
42+
>>> # Access physical CSAD equipment
43+
>>> for equip in csad.csad_equipment:
44+
... print(f"CSAD Equipment: {equip.name}")
45+
... print(f"Type: {equip.csad_type}")
46+
... print(f"Enabled: {equip.enabled}")
47+
... print(f"System ID: {equip.system_id}")
48+
49+
Important Notes:
50+
- CSADEquipment is read-only (no direct control methods)
51+
- Control CSAD equipment through the parent CSAD instance
52+
- Telemetry is accessed through the parent CSAD, not individual equipment
53+
- Equipment may be disabled but still configured in the system
54+
"""
55+
56+
mspconfig: MSPCSADEquip
57+
telemetry: None
58+
59+
def __init__(self, omni: "OmniLogic", mspconfig: MSPCSADEquip, telemetry: Telemetry | None) -> None:
60+
super().__init__(omni, mspconfig, telemetry)
61+
62+
@property
63+
def equip_type(self) -> Literal["PET_CSAD"]:
64+
"""Returns the equipment type (always 'PET_CSAD')."""
65+
return self.mspconfig.equip_type
66+
67+
@property
68+
def csad_type(self) -> CSADEquipmentType | str:
69+
"""Returns the type of CSAD equipment (e.g., AQL_CHEM)."""
70+
return self.mspconfig.csad_type
71+
72+
@property
73+
def enabled(self) -> bool:
74+
"""Returns whether the CSAD equipment is enabled in configuration."""
75+
return self.mspconfig.enabled

pyomnilogic_local/heater_equip.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class HeaterEquipment(OmniEquipment[MSPHeaterEquip, TelemetryHeater]):
3737
heater_type: Type of heating unit (GAS, HEAT_PUMP, SOLAR)
3838
min_filter_speed: Minimum filter speed required for operation
3939
sensor_id: System ID of the temperature sensor
40-
supports_cooling: Whether this unit can cool (heat pumps only)
40+
supports_cooling: Whether this unit can cool
4141
4242
Properties (Telemetry):
4343
state: Current heater state (OFF, ON, PAUSE)

pyomnilogic_local/models/mspconfig.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
ColorLogicShow40,
2929
ColorLogicShowUCL,
3030
ColorLogicShowUCLV2,
31+
CSADEquipmentType,
3132
CSADType,
3233
FilterType,
3334
HeaterType,
@@ -247,7 +248,18 @@ def __init__(self, **data: Any) -> None:
247248
self.chlorinator_equipment = [MSPChlorinatorEquip.model_validate(equip) for equip in chlorinator_equip_data]
248249

249250

251+
class MSPCSADEquip(OmniBase):
252+
_YES_NO_FIELDS = {"enabled"}
253+
254+
omni_type: OmniType = OmniType.CSAD_EQUIP
255+
256+
equip_type: Literal["PET_CSAD"] = Field(alias="Type")
257+
csad_type: CSADEquipmentType | str = Field(alias="CSAD-Type")
258+
enabled: bool = Field(alias="Enabled")
259+
260+
250261
class MSPCSAD(OmniBase):
262+
_sub_devices = {"csad_equipment"}
251263
_YES_NO_FIELDS = {"enabled"}
252264

253265
omni_type: OmniType = OmniType.CSAD
@@ -264,6 +276,30 @@ class MSPCSAD(OmniBase):
264276
orp_high_alarm_level: int = Field(alias="ORP-High-Alarm-Level")
265277
orp_forced_on_time: int = Field(alias="ORP-Forced-On-Time")
266278
orp_forced_enabled: bool = Field(alias="ORP-Forced-Enabled")
279+
csad_equipment: list[MSPCSADEquip] | None = None
280+
281+
def __init__(self, **data: Any) -> None:
282+
super().__init__(**data)
283+
284+
# The CSAD equipment are nested down inside a list of "Operations", which also includes non CSAD-Equipment items.
285+
# We need to first filter down to just the CSAD equipment items, then populate our self.csad_equipment with parsed
286+
# versions of those items.
287+
csad_equip_data = [op[OmniType.CSAD_EQUIP] for op in data.get("Operation", {}) if OmniType.CSAD_EQUIP in op]
288+
self.csad_equipment = [MSPCSADEquip.model_validate(equip) for equip in csad_equip_data]
289+
290+
# The CSAD equipment are nested down inside a list of "Operations", which also includes non CSAD-Equipment items.
291+
# We need to first filter down to just the CSAD equipment items, then populate our self.csad_equipment with parsed
292+
# versions of those items.
293+
# operations_list = data.get("Operation", [])
294+
# if operations_list:
295+
# csad_equip_ops = [op for op in operations_list if OmniType.CSAD_EQUIP in op]
296+
# if csad_equip_ops:
297+
# csad_equip_data = csad_equip_ops[0][OmniType.CSAD_EQUIP]
298+
# self.csad_equipment = [MSPCSADEquip.model_validate(equip) for equip in csad_equip_data]
299+
# else:
300+
# self.csad_equipment = []
301+
# else:
302+
# self.csad_equipment = []
267303

268304

269305
class MSPColorLogicLight(OmniBase):
@@ -355,6 +391,7 @@ class MSPSchedule(OmniBase):
355391
| MSPChlorinator
356392
| MSPChlorinatorEquip
357393
| MSPCSAD
394+
| MSPCSADEquip
358395
| MSPColorLogicLight
359396
)
360397

pyomnilogic_local/omnilogic.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pyomnilogic_local.collections import EquipmentDict
1212
from pyomnilogic_local.colorlogiclight import ColorLogicLight
1313
from pyomnilogic_local.csad import CSAD
14+
from pyomnilogic_local.csad_equip import CSADEquipment
1415
from pyomnilogic_local.filter import Filter
1516
from pyomnilogic_local.heater import Heater
1617
from pyomnilogic_local.heater_equip import HeaterEquipment
@@ -220,6 +221,14 @@ def all_chlorinator_equipment(self) -> EquipmentDict[ChlorinatorEquipment]:
220221
chlorinator_equipment.extend(chlorinator.chlorinator_equipment.values())
221222
return EquipmentDict(chlorinator_equipment)
222223

224+
@property
225+
def all_csad_equipment(self) -> EquipmentDict[CSADEquipment]:
226+
"""Returns all CSADEquipment instances across all CSADs in the backyard."""
227+
csad_equipment: list[CSADEquipment] = []
228+
for csad in self.all_csads.values():
229+
csad_equipment.extend(csad.csad_equipment.values())
230+
return EquipmentDict(csad_equipment)
231+
223232
@property
224233
def all_csads(self) -> EquipmentDict[CSAD]:
225234
"""Returns all CSAD instances across all bows in the backyard."""
@@ -251,6 +260,7 @@ def get_equipment_by_name(self, name: str) -> OmniEquipment[Any, Any] | None:
251260
all_equipment.extend(self.all_chlorinators.values())
252261
all_equipment.extend(self.all_chlorinator_equipment.values())
253262
all_equipment.extend(self.all_csads.values())
263+
all_equipment.extend(self.all_csad_equipment.values())
254264

255265
for equipment in all_equipment:
256266
if equipment.name == name:
@@ -280,6 +290,7 @@ def get_equipment_by_id(self, system_id: int) -> OmniEquipment[Any, Any] | None:
280290
all_equipment.extend(self.all_chlorinators.values())
281291
all_equipment.extend(self.all_chlorinator_equipment.values())
282292
all_equipment.extend(self.all_csads.values())
293+
all_equipment.extend(self.all_csad_equipment.values())
283294

284295
for equipment in all_equipment:
285296
if equipment.system_id == system_id:

pyomnilogic_local/omnitypes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class OmniType(StrEnum):
4545
CHLORINATOR = "Chlorinator"
4646
CHLORINATOR_EQUIP = "Chlorinator-Equipment"
4747
CSAD = "CSAD"
48+
CSAD_EQUIP = "CSAD-Equipment"
4849
CL_LIGHT = "ColorLogic-Light"
4950
FAVORITES = "Favorites"
5051
FILTER = "Filter"
@@ -338,6 +339,10 @@ class CSADType(StrEnum, PrettyEnum):
338339
CO2 = "CO2"
339340

340341

342+
class CSADEquipmentType(StrEnum, PrettyEnum):
343+
AQL_CHEM = "AQL-CHEM"
344+
345+
341346
# Chemistry Sense and Dispense
342347
class CSADStatus(IntEnum, PrettyEnum):
343348
NOT_DISPENSING = 0

0 commit comments

Comments
 (0)