Skip to content

Commit 2d45357

Browse files
committed
feat: add configuration dataclass, allow squelching duplicate name warnings
BREAKING CHANGE: The OmniLogic class now accepts a single configuration parameter which is an instance of OmniLogicConfiguration. The host to connect to must now be passed as a parameter from OmniLogicConfiguration. This allows more advanced configuration of the library to support future functionality.
1 parent 47fa524 commit 2d45357

10 files changed

Lines changed: 137 additions & 76 deletions

File tree

README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,14 @@ pip install python-omnilogic-local[cli]
6565

6666
```python
6767
import asyncio
68-
from pyomnilogic_local import OmniLogic
68+
from pyomnilogic_local import OmniLogic, OmniLogicConfig
6969

7070
async def main():
7171
# Connect to your OmniLogic controller
72-
omni = OmniLogic("192.168.1.100")
72+
config = OmniLogicConfig(
73+
host="192.168.1.100"
74+
)
75+
omni = OmniLogic(config)
7376

7477
# Initial refresh to load configuration and state
7578
await omni.refresh()
@@ -110,7 +113,11 @@ asyncio.run(main())
110113

111114
```python
112115
async def monitor_pool():
113-
omni = OmniLogic("192.168.1.100")
116+
config = OmniLogicConfig(
117+
host="192.168.1.100"
118+
)
119+
omni = OmniLogic(config)
120+
114121
await omni.refresh()
115122

116123
pool = omni.backyard.bow["Pool"]
@@ -140,8 +147,11 @@ asyncio.run(monitor_pool())
140147
The library includes intelligent state management to minimize unnecessary API calls:
141148

142149
```python
143-
# Force immediate refresh
144-
await omni.refresh(force=True)
150+
# Force immediate refresh of Telemetry
151+
await omni.refresh(force_telemetry=True)
152+
153+
# Force immediate refresh of MSP Config
154+
await omni.refresh(force_mspconfig=True)
145155

146156
# Refresh only if data is older than 30 seconds
147157
await omni.refresh(if_older_than=30.0)

pyomnilogic_local/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .groups import Group
1616
from .heater import Heater
1717
from .heater_equip import HeaterEquipment
18-
from .omnilogic import OmniLogic
18+
from .omnilogic import OmniLogic, OmniLogicConfig
1919
from .pump import Pump
2020
from .relay import Relay
2121
from .schedule import Schedule
@@ -47,6 +47,7 @@
4747
"OmniEquipmentNotInitializedError",
4848
"OmniEquipmentNotReadyError",
4949
"OmniLogic",
50+
"OmniLogicConfig",
5051
"OmniLogicLocalError",
5152
"Pump",
5253
"Relay",

pyomnilogic_local/backyard.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ class Backyard(OmniEquipment[MSPBackyard, TelemetryBackyard]):
9797

9898
mspconfig: MSPBackyard
9999
telemetry: TelemetryBackyard
100-
bow: EquipmentDict[Bow] = EquipmentDict()
101-
lights: EquipmentDict[ColorLogicLight] = EquipmentDict()
102-
relays: EquipmentDict[Relay] = EquipmentDict()
103-
sensors: EquipmentDict[Sensor] = EquipmentDict()
100+
bow: EquipmentDict[Bow]
101+
lights: EquipmentDict[ColorLogicLight]
102+
relays: EquipmentDict[Relay]
103+
sensors: EquipmentDict[Sensor]
104104

105105
def __init__(self, omni: OmniLogic, mspconfig: MSPBackyard, telemetry: Telemetry) -> None:
106106
super().__init__(omni, mspconfig, telemetry)
@@ -169,28 +169,30 @@ def _update_bows(self, mspconfig: MSPBackyard, telemetry: Telemetry) -> None:
169169
self.bow = EquipmentDict()
170170
return
171171

172-
self.bow = EquipmentDict([Bow(self._omni, bow, telemetry) for bow in mspconfig.bow])
172+
self.bow = self._omni._make_equipment_dict([Bow(self._omni, bow, telemetry) for bow in mspconfig.bow])
173173

174174
def _update_lights(self, mspconfig: MSPBackyard, telemetry: Telemetry) -> None:
175175
"""Update the lights based on the MSP configuration."""
176176
if mspconfig.colorlogic_light is None:
177177
self.lights = EquipmentDict()
178178
return
179179

180-
self.lights = EquipmentDict([ColorLogicLight(self._omni, light, telemetry) for light in mspconfig.colorlogic_light])
180+
self.lights = self._omni._make_equipment_dict(
181+
[ColorLogicLight(self._omni, light, telemetry) for light in mspconfig.colorlogic_light]
182+
)
181183

182184
def _update_relays(self, mspconfig: MSPBackyard, telemetry: Telemetry) -> None:
183185
"""Update the relays based on the MSP configuration."""
184186
if mspconfig.relay is None:
185187
self.relays = EquipmentDict()
186188
return
187189

188-
self.relays = EquipmentDict([Relay(self._omni, relay, telemetry) for relay in mspconfig.relay])
190+
self.relays = self._omni._make_equipment_dict([Relay(self._omni, relay, telemetry) for relay in mspconfig.relay])
189191

190192
def _update_sensors(self, mspconfig: MSPBackyard, telemetry: Telemetry) -> None:
191193
"""Update the sensors based on the MSP configuration."""
192194
if mspconfig.sensor is None:
193195
self.sensors = EquipmentDict()
194196
return
195197

196-
self.sensors = EquipmentDict([Sensor(self._omni, sensor, telemetry) for sensor in mspconfig.sensor])
198+
self.sensors = self._omni._make_equipment_dict([Sensor(self._omni, sensor, telemetry) for sensor in mspconfig.sensor])

pyomnilogic_local/bow.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,12 @@ class Bow(OmniEquipment[MSPBoW, TelemetryBoW]):
132132

133133
mspconfig: MSPBoW
134134
telemetry: TelemetryBoW
135-
filters: EquipmentDict[Filter] = EquipmentDict()
135+
filters: EquipmentDict[Filter]
136136
heater: Heater | None = None
137-
relays: EquipmentDict[Relay] = EquipmentDict()
138-
sensors: EquipmentDict[Sensor] = EquipmentDict()
139-
lights: EquipmentDict[ColorLogicLight] = EquipmentDict()
140-
pumps: EquipmentDict[Pump] = EquipmentDict()
137+
relays: EquipmentDict[Relay]
138+
sensors: EquipmentDict[Sensor]
139+
lights: EquipmentDict[ColorLogicLight]
140+
pumps: EquipmentDict[Pump]
141141
chlorinator: Chlorinator | None = None
142142
csad: CSAD | None = None
143143

@@ -288,7 +288,7 @@ def _update_filters(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
288288
self.filters = EquipmentDict()
289289
return
290290

291-
self.filters = EquipmentDict([Filter(self._omni, filter_, telemetry) for filter_ in mspconfig.filter])
291+
self.filters = self._omni._make_equipment_dict([Filter(self._omni, filter_, telemetry) for filter_ in mspconfig.filter])
292292

293293
def _update_heater(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
294294
"""Update the heater based on the MSP configuration."""
@@ -304,28 +304,30 @@ def _update_lights(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
304304
self.lights = EquipmentDict()
305305
return
306306

307-
self.lights = EquipmentDict([ColorLogicLight(self._omni, light, telemetry) for light in mspconfig.colorlogic_light])
307+
self.lights = self._omni._make_equipment_dict(
308+
[ColorLogicLight(self._omni, light, telemetry) for light in mspconfig.colorlogic_light]
309+
)
308310

309311
def _update_pumps(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
310312
"""Update the pumps based on the MSP configuration."""
311313
if mspconfig.pump is None:
312314
self.pumps = EquipmentDict()
313315
return
314316

315-
self.pumps = EquipmentDict([Pump(self._omni, pump, telemetry) for pump in mspconfig.pump])
317+
self.pumps = self._omni._make_equipment_dict([Pump(self._omni, pump, telemetry) for pump in mspconfig.pump])
316318

317319
def _update_relays(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
318320
"""Update the relays based on the MSP configuration."""
319321
if mspconfig.relay is None:
320322
self.relays = EquipmentDict()
321323
return
322324

323-
self.relays = EquipmentDict([Relay(self._omni, relay, telemetry) for relay in mspconfig.relay])
325+
self.relays = self._omni._make_equipment_dict([Relay(self._omni, relay, telemetry) for relay in mspconfig.relay])
324326

325327
def _update_sensors(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
326328
"""Update the sensors based on the MSP configuration."""
327329
if mspconfig.sensor is None:
328330
self.sensors = EquipmentDict()
329331
return
330332

331-
self.sensors = EquipmentDict([Sensor(self._omni, sensor, telemetry) for sensor in mspconfig.sensor])
333+
self.sensors = self._omni._make_equipment_dict([Sensor(self._omni, sensor, telemetry) for sensor in mspconfig.sensor])

pyomnilogic_local/chlorinator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class Chlorinator(OmniEquipment[MSPChlorinator, TelemetryChlorinator]):
4545

4646
mspconfig: MSPChlorinator
4747
telemetry: TelemetryChlorinator
48-
chlorinator_equipment: EquipmentDict[ChlorinatorEquipment] = EquipmentDict()
48+
chlorinator_equipment: EquipmentDict[ChlorinatorEquipment]
4949

5050
def __init__(self, omni: OmniLogic, mspconfig: MSPChlorinator, telemetry: Telemetry) -> None:
5151
super().__init__(omni, mspconfig, telemetry)
@@ -62,7 +62,7 @@ def _update_chlorinator_equipment(self, mspconfig: MSPChlorinator, telemetry: Te
6262
self.chlorinator_equipment = EquipmentDict()
6363
return
6464

65-
self.chlorinator_equipment = EquipmentDict(
65+
self.chlorinator_equipment = self._omni._make_equipment_dict(
6666
[ChlorinatorEquipment(self._omni, equip, telemetry) for equip in mspconfig.chlorinator_equipment]
6767
)
6868

pyomnilogic_local/cli/cli.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import click
77

8-
from pyomnilogic_local import OmniLogic
8+
from pyomnilogic_local import OmniLogic, OmniLogicConfig
99
from pyomnilogic_local.cli.debug import commands as debug
1010
from pyomnilogic_local.cli.get import commands as get
1111

@@ -39,16 +39,22 @@ def entrypoint(ctx: click.Context, host: str, port: int, timeout: int, debug: bo
3939
"""
4040
ctx.ensure_object(dict)
4141

42-
if debug:
43-
logging.basicConfig(level=logging.DEBUG)
42+
logging.basicConfig(
43+
level=logging.DEBUG if debug else logging.INFO,
44+
)
4445

4546
# Store the host for later connection, but don't connect yet
46-
ctx.obj["HOST"] = host
47-
ctx.obj["PORT"] = port
48-
ctx.obj["TIMEOUT"] = timeout
49-
omnilogic = OmniLogic(host, port, timeout) # Store the OmniLogic instance for later use
50-
51-
asyncio.run(omnilogic.refresh(force=True))
47+
config = OmniLogicConfig(
48+
host=host,
49+
port=port,
50+
timeout=timeout,
51+
# The CLI should only ever reference things by their system_id
52+
# so we can ignore duplicate name warnings
53+
warn_duplicate_equipment_names=False,
54+
)
55+
omnilogic = OmniLogic(config) # Store the OmniLogic instance for later use
56+
57+
asyncio.run(omnilogic.refresh())
5258

5359
ctx.obj["OMNILOGIC"] = omnilogic
5460

pyomnilogic_local/collections.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,18 @@ class EquipmentDict[OE: OmniEquipment[Any, Any]]:
5757
always lookup by name. This type-based differentiation prevents ambiguity.
5858
"""
5959

60-
def __init__(self, items: list[OE] | None = None) -> None:
60+
def __init__(self, items: list[OE] | None = None, warn_duplicates: bool = True) -> None:
6161
"""Initialize the equipment collection.
6262
6363
Args:
6464
items: Optional list of equipment items to populate the collection.
65+
warn_duplicates: Whether to log warnings for duplicate names.
6566
6667
Raises:
6768
ValueError: If any item has neither a system_id nor a name.
6869
"""
6970
self._items: list[OE] = items if items is not None else []
71+
self._warn_duplicates = warn_duplicates
7072
self._validate()
7173

7274
def _validate(self) -> None:
@@ -89,21 +91,22 @@ def _validate(self) -> None:
8991
raise ValueError(msg)
9092

9193
# Find duplicate names that we haven't warned about yet
92-
name_counts = Counter(item.name for item in self._items if item.name is not None)
93-
duplicate_names = {name for name, count in name_counts.items() if count > 1}
94-
unwarned_duplicates = duplicate_names.difference(_WARNED_DUPLICATE_NAMES)
95-
96-
# Log warnings for new duplicates
97-
for name in unwarned_duplicates:
98-
_LOGGER.warning(
99-
"Equipment collection contains %d items with the same name '%s'. "
100-
"Name-based lookups will return the first match. "
101-
"Consider using system_id-based lookups for reliability "
102-
"or renaming equipment to avoid duplicates.",
103-
name_counts[name],
104-
name,
105-
)
106-
_WARNED_DUPLICATE_NAMES.add(name)
94+
if self._warn_duplicates:
95+
name_counts = Counter(item.name for item in self._items if item.name is not None)
96+
duplicate_names = {name for name, count in name_counts.items() if count > 1}
97+
unwarned_duplicates = duplicate_names.difference(_WARNED_DUPLICATE_NAMES)
98+
99+
# Log warnings for new duplicates
100+
for name in unwarned_duplicates:
101+
_LOGGER.warning(
102+
"Equipment collection contains %d items with the same name '%s'. "
103+
"Name-based lookups will return the first match. "
104+
"Consider using system_id-based lookups for reliability "
105+
"or renaming equipment to avoid duplicates.",
106+
name_counts[name],
107+
name,
108+
)
109+
_WARNED_DUPLICATE_NAMES.add(name)
107110

108111
@property
109112
def _by_name(self) -> dict[str, OE]:

pyomnilogic_local/csad.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class CSAD(OmniEquipment[MSPCSAD, TelemetryCSAD]):
4545

4646
mspconfig: MSPCSAD
4747
telemetry: TelemetryCSAD
48-
csad_equipment: EquipmentDict[CSADEquipment] = EquipmentDict()
48+
csad_equipment: EquipmentDict[CSADEquipment]
4949

5050
def __init__(self, omni: OmniLogic, mspconfig: MSPCSAD, telemetry: Telemetry) -> None:
5151
super().__init__(omni, mspconfig, telemetry)
@@ -62,7 +62,9 @@ def _update_csad_equipment(self, mspconfig: MSPCSAD, telemetry: Telemetry) -> No
6262
self.csad_equipment = EquipmentDict()
6363
return
6464

65-
self.csad_equipment = EquipmentDict([CSADEquipment(self._omni, equip, telemetry) for equip in mspconfig.csad_equipment])
65+
self.csad_equipment = self._omni._make_equipment_dict(
66+
[CSADEquipment(self._omni, equip, telemetry) for equip in mspconfig.csad_equipment]
67+
)
6668

6769
# Expose MSPConfig attributes
6870
@property

pyomnilogic_local/heater.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class Heater(OmniEquipment[MSPVirtualHeater, TelemetryVirtualHeater]):
102102

103103
mspconfig: MSPVirtualHeater
104104
telemetry: TelemetryVirtualHeater
105-
heater_equipment: EquipmentDict[HeaterEquipment] = EquipmentDict()
105+
heater_equipment: EquipmentDict[HeaterEquipment]
106106

107107
def __init__(self, omni: OmniLogic, mspconfig: MSPVirtualHeater, telemetry: Telemetry) -> None:
108108
super().__init__(omni, mspconfig, telemetry)
@@ -119,7 +119,9 @@ def _update_heater_equipment(self, mspconfig: MSPVirtualHeater, telemetry: Telem
119119
self.heater_equipment = EquipmentDict()
120120
return
121121

122-
self.heater_equipment = EquipmentDict([HeaterEquipment(self._omni, equip, telemetry) for equip in mspconfig.heater_equipment])
122+
self.heater_equipment = self._omni._make_equipment_dict(
123+
[HeaterEquipment(self._omni, equip, telemetry) for equip in mspconfig.heater_equipment]
124+
)
123125

124126
@property
125127
def max_temp(self) -> int:

0 commit comments

Comments
 (0)