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
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,14 @@ pip install python-omnilogic-local[cli]

```python
import asyncio
from pyomnilogic_local import OmniLogic
from pyomnilogic_local import OmniLogic, OmniLogicConfig

async def main():
# Connect to your OmniLogic controller
omni = OmniLogic("192.168.1.100")
config = OmniLogicConfig(
host="192.168.1.100"
)
omni = OmniLogic(config)

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

```python
async def monitor_pool():
omni = OmniLogic("192.168.1.100")
config = OmniLogicConfig(
host="192.168.1.100"
)
omni = OmniLogic(config)

await omni.refresh()

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

```python
# Force immediate refresh
await omni.refresh(force=True)
# Force immediate refresh of Telemetry
await omni.refresh(force_telemetry=True)

# Force immediate refresh of MSP Config
await omni.refresh(force_mspconfig=True)

# Refresh only if data is older than 30 seconds
await omni.refresh(if_older_than=30.0)
Expand Down
3 changes: 2 additions & 1 deletion pyomnilogic_local/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from .groups import Group
from .heater import Heater
from .heater_equip import HeaterEquipment
from .omnilogic import OmniLogic
from .omnilogic import OmniLogic, OmniLogicConfig
from .pump import Pump
from .relay import Relay
from .schedule import Schedule
Expand Down Expand Up @@ -47,6 +47,7 @@
"OmniEquipmentNotInitializedError",
"OmniEquipmentNotReadyError",
"OmniLogic",
"OmniLogicConfig",
"OmniLogicLocalError",
"Pump",
"Relay",
Expand Down
18 changes: 10 additions & 8 deletions pyomnilogic_local/backyard.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ class Backyard(OmniEquipment[MSPBackyard, TelemetryBackyard]):

mspconfig: MSPBackyard
telemetry: TelemetryBackyard
bow: EquipmentDict[Bow] = EquipmentDict()
lights: EquipmentDict[ColorLogicLight] = EquipmentDict()
relays: EquipmentDict[Relay] = EquipmentDict()
sensors: EquipmentDict[Sensor] = EquipmentDict()
bow: EquipmentDict[Bow]
lights: EquipmentDict[ColorLogicLight]
relays: EquipmentDict[Relay]
sensors: EquipmentDict[Sensor]

def __init__(self, omni: OmniLogic, mspconfig: MSPBackyard, telemetry: Telemetry) -> None:
super().__init__(omni, mspconfig, telemetry)
Expand Down Expand Up @@ -169,28 +169,30 @@ def _update_bows(self, mspconfig: MSPBackyard, telemetry: Telemetry) -> None:
self.bow = EquipmentDict()
return

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

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

self.lights = EquipmentDict([ColorLogicLight(self._omni, light, telemetry) for light in mspconfig.colorlogic_light])
self.lights = self._omni._make_equipment_dict(
[ColorLogicLight(self._omni, light, telemetry) for light in mspconfig.colorlogic_light]
)

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

self.relays = EquipmentDict([Relay(self._omni, relay, telemetry) for relay in mspconfig.relay])
self.relays = self._omni._make_equipment_dict([Relay(self._omni, relay, telemetry) for relay in mspconfig.relay])

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

self.sensors = EquipmentDict([Sensor(self._omni, sensor, telemetry) for sensor in mspconfig.sensor])
self.sensors = self._omni._make_equipment_dict([Sensor(self._omni, sensor, telemetry) for sensor in mspconfig.sensor])
22 changes: 12 additions & 10 deletions pyomnilogic_local/bow.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,12 @@ class Bow(OmniEquipment[MSPBoW, TelemetryBoW]):

mspconfig: MSPBoW
telemetry: TelemetryBoW
filters: EquipmentDict[Filter] = EquipmentDict()
filters: EquipmentDict[Filter]
heater: Heater | None = None
relays: EquipmentDict[Relay] = EquipmentDict()
sensors: EquipmentDict[Sensor] = EquipmentDict()
lights: EquipmentDict[ColorLogicLight] = EquipmentDict()
pumps: EquipmentDict[Pump] = EquipmentDict()
relays: EquipmentDict[Relay]
sensors: EquipmentDict[Sensor]
lights: EquipmentDict[ColorLogicLight]
pumps: EquipmentDict[Pump]
chlorinator: Chlorinator | None = None
csad: CSAD | None = None

Expand Down Expand Up @@ -288,7 +288,7 @@ def _update_filters(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
self.filters = EquipmentDict()
return

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

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

self.lights = EquipmentDict([ColorLogicLight(self._omni, light, telemetry) for light in mspconfig.colorlogic_light])
self.lights = self._omni._make_equipment_dict(
[ColorLogicLight(self._omni, light, telemetry) for light in mspconfig.colorlogic_light]
)

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

self.pumps = EquipmentDict([Pump(self._omni, pump, telemetry) for pump in mspconfig.pump])
self.pumps = self._omni._make_equipment_dict([Pump(self._omni, pump, telemetry) for pump in mspconfig.pump])

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

self.relays = EquipmentDict([Relay(self._omni, relay, telemetry) for relay in mspconfig.relay])
self.relays = self._omni._make_equipment_dict([Relay(self._omni, relay, telemetry) for relay in mspconfig.relay])

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

self.sensors = EquipmentDict([Sensor(self._omni, sensor, telemetry) for sensor in mspconfig.sensor])
self.sensors = self._omni._make_equipment_dict([Sensor(self._omni, sensor, telemetry) for sensor in mspconfig.sensor])
4 changes: 2 additions & 2 deletions pyomnilogic_local/chlorinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Chlorinator(OmniEquipment[MSPChlorinator, TelemetryChlorinator]):

mspconfig: MSPChlorinator
telemetry: TelemetryChlorinator
chlorinator_equipment: EquipmentDict[ChlorinatorEquipment] = EquipmentDict()
chlorinator_equipment: EquipmentDict[ChlorinatorEquipment]

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

self.chlorinator_equipment = EquipmentDict(
self.chlorinator_equipment = self._omni._make_equipment_dict(
[ChlorinatorEquipment(self._omni, equip, telemetry) for equip in mspconfig.chlorinator_equipment]
)

Expand Down
24 changes: 15 additions & 9 deletions pyomnilogic_local/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import click

from pyomnilogic_local import OmniLogic
from pyomnilogic_local import OmniLogic, OmniLogicConfig
from pyomnilogic_local.cli.debug import commands as debug
from pyomnilogic_local.cli.get import commands as get

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

if debug:
logging.basicConfig(level=logging.DEBUG)
logging.basicConfig(
level=logging.DEBUG if debug else logging.INFO,
)

# Store the host for later connection, but don't connect yet
ctx.obj["HOST"] = host
ctx.obj["PORT"] = port
ctx.obj["TIMEOUT"] = timeout
omnilogic = OmniLogic(host, port, timeout) # Store the OmniLogic instance for later use

asyncio.run(omnilogic.refresh(force=True))
config = OmniLogicConfig(
host=host,
port=port,
timeout=timeout,
# The CLI should only ever reference things by their system_id
# so we can ignore duplicate name warnings
warn_duplicate_equipment_names=False,
)
omnilogic = OmniLogic(config) # Store the OmniLogic instance for later use

asyncio.run(omnilogic.refresh())

ctx.obj["OMNILOGIC"] = omnilogic

Expand Down
35 changes: 19 additions & 16 deletions pyomnilogic_local/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,18 @@ class EquipmentDict[OE: OmniEquipment[Any, Any]]:
always lookup by name. This type-based differentiation prevents ambiguity.
"""

def __init__(self, items: list[OE] | None = None) -> None:
def __init__(self, items: list[OE] | None = None, warn_duplicates: bool = True) -> None:
"""Initialize the equipment collection.

Args:
items: Optional list of equipment items to populate the collection.
warn_duplicates: Whether to log warnings for duplicate names.

Raises:
ValueError: If any item has neither a system_id nor a name.
"""
self._items: list[OE] = items if items is not None else []
self._warn_duplicates = warn_duplicates
self._validate()

def _validate(self) -> None:
Expand All @@ -89,21 +91,22 @@ def _validate(self) -> None:
raise ValueError(msg)

# Find duplicate names that we haven't warned about yet
name_counts = Counter(item.name for item in self._items if item.name is not None)
duplicate_names = {name for name, count in name_counts.items() if count > 1}
unwarned_duplicates = duplicate_names.difference(_WARNED_DUPLICATE_NAMES)

# Log warnings for new duplicates
for name in unwarned_duplicates:
_LOGGER.warning(
"Equipment collection contains %d items with the same name '%s'. "
"Name-based lookups will return the first match. "
"Consider using system_id-based lookups for reliability "
"or renaming equipment to avoid duplicates.",
name_counts[name],
name,
)
_WARNED_DUPLICATE_NAMES.add(name)
if self._warn_duplicates:
name_counts = Counter(item.name for item in self._items if item.name is not None)
duplicate_names = {name for name, count in name_counts.items() if count > 1}
unwarned_duplicates = duplicate_names.difference(_WARNED_DUPLICATE_NAMES)

# Log warnings for new duplicates
for name in unwarned_duplicates:
_LOGGER.warning(
"Equipment collection contains %d items with the same name '%s'. "
"Name-based lookups will return the first match. "
"Consider using system_id-based lookups for reliability "
"or renaming equipment to avoid duplicates.",
name_counts[name],
name,
)
_WARNED_DUPLICATE_NAMES.add(name)

@property
def _by_name(self) -> dict[str, OE]:
Expand Down
6 changes: 4 additions & 2 deletions pyomnilogic_local/csad.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class CSAD(OmniEquipment[MSPCSAD, TelemetryCSAD]):

mspconfig: MSPCSAD
telemetry: TelemetryCSAD
csad_equipment: EquipmentDict[CSADEquipment] = EquipmentDict()
csad_equipment: EquipmentDict[CSADEquipment]

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

self.csad_equipment = EquipmentDict([CSADEquipment(self._omni, equip, telemetry) for equip in mspconfig.csad_equipment])
self.csad_equipment = self._omni._make_equipment_dict(
[CSADEquipment(self._omni, equip, telemetry) for equip in mspconfig.csad_equipment]
)

# Expose MSPConfig attributes
@property
Expand Down
6 changes: 4 additions & 2 deletions pyomnilogic_local/heater.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class Heater(OmniEquipment[MSPVirtualHeater, TelemetryVirtualHeater]):

mspconfig: MSPVirtualHeater
telemetry: TelemetryVirtualHeater
heater_equipment: EquipmentDict[HeaterEquipment] = EquipmentDict()
heater_equipment: EquipmentDict[HeaterEquipment]

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

self.heater_equipment = EquipmentDict([HeaterEquipment(self._omni, equip, telemetry) for equip in mspconfig.heater_equipment])
self.heater_equipment = self._omni._make_equipment_dict(
[HeaterEquipment(self._omni, equip, telemetry) for equip in mspconfig.heater_equipment]
)

@property
def max_temp(self) -> int:
Expand Down
Loading
Loading