python-omnilogic-local 1.1.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/PKG-INFO +1 -1
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/api.py +1 -1
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/bow.py +5 -5
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/chlorinator.py +1 -1
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/debug/commands.py +1 -1
- python_omnilogic_local-2.0.0/pyomnilogic_local/cli/get/bows.py +53 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/csad.py +58 -77
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/models/mspconfig.py +18 -6
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/omnilogic.py +1 -3
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/omnitypes.py +7 -7
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyproject.toml +1 -1
- python_omnilogic_local-1.1.0/pyomnilogic_local/cli/get/bows.py +0 -100
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/LICENSE +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/README.md +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/__init__.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/_base.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/__init__.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/constants.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/exceptions.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/message.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/mock_api.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/protocol.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/backyard.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/chlorinator_equip.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/__init__.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/cli.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/debug/__init__.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/__init__.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/backyard.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/chlorinators.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/commands.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/csads.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/filters.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/groups.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/heaters.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/lights.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/pumps.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/relays.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/schedules.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/sensors.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/valves.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/pcap_utils.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/utils.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/collections.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/colorlogiclight.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/csad_equip.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/decorators.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/filter.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/groups.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/heater.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/heater_equip.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/models/__init__.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/models/const.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/models/exceptions.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/models/filter_diagnostics.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/models/leadmessage.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/models/telemetry.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/pump.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/py.typed +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/relay.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/schedule.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/sensor.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/system.py +0 -0
- {python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-omnilogic-local
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: A library for local control of Hayward OmniHub/OmniLogic pool controllers using their local API
|
|
5
5
|
Author: Chris Jowett, djtimca, garionphx
|
|
6
6
|
Author-email: Chris Jowett <421501+cryptk@users.noreply.github.com>
|
|
@@ -599,7 +599,7 @@ class OmniLogicAPI:
|
|
|
599
599
|
return await self.async_send(MessageType.SET_CSAD_ORP_TARGET, req_body)
|
|
600
600
|
|
|
601
601
|
# This is used to set the pH target value on a CSAD
|
|
602
|
-
async def
|
|
602
|
+
async def async_set_csad_ph_target_value(
|
|
603
603
|
self,
|
|
604
604
|
pool_id: int,
|
|
605
605
|
csad_id: int,
|
|
@@ -139,7 +139,7 @@ class Bow(OmniEquipment[MSPBoW, TelemetryBoW]):
|
|
|
139
139
|
lights: EquipmentDict[ColorLogicLight] = EquipmentDict()
|
|
140
140
|
pumps: EquipmentDict[Pump] = EquipmentDict()
|
|
141
141
|
chlorinator: Chlorinator | None = None
|
|
142
|
-
|
|
142
|
+
csad: CSAD | None = None
|
|
143
143
|
|
|
144
144
|
def __init__(self, omni: OmniLogic, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
|
|
145
145
|
super().__init__(omni, mspconfig, telemetry)
|
|
@@ -164,8 +164,8 @@ class Bow(OmniEquipment[MSPBoW, TelemetryBoW]):
|
|
|
164
164
|
parts.append("heater=True")
|
|
165
165
|
if self.chlorinator is not None:
|
|
166
166
|
parts.append("chlorinator=True")
|
|
167
|
-
if
|
|
168
|
-
parts.append(
|
|
167
|
+
if self.csad is not None:
|
|
168
|
+
parts.append("csad=True")
|
|
169
169
|
|
|
170
170
|
return f"Bow({', '.join(parts)})"
|
|
171
171
|
|
|
@@ -277,10 +277,10 @@ class Bow(OmniEquipment[MSPBoW, TelemetryBoW]):
|
|
|
277
277
|
def _update_csads(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
|
|
278
278
|
"""Update the CSADs based on the MSP configuration."""
|
|
279
279
|
if mspconfig.csad is None:
|
|
280
|
-
self.
|
|
280
|
+
self.csad = None
|
|
281
281
|
return
|
|
282
282
|
|
|
283
|
-
self.
|
|
283
|
+
self.csad = CSAD(self._omni, mspconfig.csad, telemetry)
|
|
284
284
|
|
|
285
285
|
def _update_filters(self, mspconfig: MSPBoW, telemetry: Telemetry) -> None:
|
|
286
286
|
"""Update the filters based on the MSP configuration."""
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/chlorinator.py
RENAMED
|
@@ -239,7 +239,7 @@ class Chlorinator(OmniEquipment[MSPChlorinator, TelemetryChlorinator]):
|
|
|
239
239
|
See Also:
|
|
240
240
|
is_generating: Check if actively producing chlorine right now
|
|
241
241
|
"""
|
|
242
|
-
return self.
|
|
242
|
+
return self.telemetry.enable
|
|
243
243
|
|
|
244
244
|
@property
|
|
245
245
|
def is_generating(self) -> bool:
|
|
@@ -306,7 +306,7 @@ def set_csad_ph(ctx: click.Context, bow_id: int, csad_id: int, target: float) ->
|
|
|
306
306
|
|
|
307
307
|
# Execute the command
|
|
308
308
|
try:
|
|
309
|
-
asyncio.run(omnilogic._api.
|
|
309
|
+
asyncio.run(omnilogic._api.async_set_csad_ph_target_value(pool_id=bow_id, csad_id=csad_id, ph_target=target))
|
|
310
310
|
click.echo(f"Successfully set CSAD {csad_id} in BOW {bow_id} pH target to {target}")
|
|
311
311
|
except Exception as e:
|
|
312
312
|
click.echo(f"Error setting CSAD pH target: {e}", err=True)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Need to figure out how to resolve the 'Untyped decorator makes function "..." untyped' errors in mypy when using click decorators
|
|
2
|
+
# mypy: disable-error-code="misc"
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from pyomnilogic_local.cli.utils import echo_properties
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pyomnilogic_local import OmniLogic
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command()
|
|
17
|
+
@click.pass_context
|
|
18
|
+
def bows(ctx: click.Context) -> None:
|
|
19
|
+
"""List all Bodies of Water (BOWs) and their current status.
|
|
20
|
+
|
|
21
|
+
Displays information about all bodies of water including their system IDs,
|
|
22
|
+
names, types (pool/spa), water temperature, flow status, and attached equipment.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
omnilogic get bows
|
|
26
|
+
"""
|
|
27
|
+
omnilogic: OmniLogic = ctx.obj["OMNILOGIC"]
|
|
28
|
+
all_bows = omnilogic.all_bows
|
|
29
|
+
for bow in all_bows:
|
|
30
|
+
echo_properties(bow)
|
|
31
|
+
click.echo("\n Equipment Counts:")
|
|
32
|
+
|
|
33
|
+
_print_equipment_count("Filter", len(bow.filters))
|
|
34
|
+
_print_equipment_count("Pump", len(bow.pumps))
|
|
35
|
+
_print_equipment_count("Heater (virtual)", 1 if bow.heater else 0)
|
|
36
|
+
_print_equipment_count("Heaters (physical)", len(bow.heater.heater_equipment) if bow.heater else 0)
|
|
37
|
+
_print_equipment_count("Sensors", len(bow.sensors))
|
|
38
|
+
_print_equipment_count("Lights", len(bow.lights))
|
|
39
|
+
_print_equipment_count("Relays", len(bow.relays))
|
|
40
|
+
_print_equipment_count("Chlorinator (virtual)", 1 if bow.chlorinator else 0)
|
|
41
|
+
_print_equipment_count("Chlorinators (physical)", len(bow.chlorinator.chlorinator_equipment) if bow.chlorinator else 0)
|
|
42
|
+
_print_equipment_count("CSAD (virtual)", 1 if bow.csad else 0)
|
|
43
|
+
_print_equipment_count("CSADs (physical)", len(bow.csad.csad_equipment) if bow.csad else 0)
|
|
44
|
+
|
|
45
|
+
click.echo("=" * 60)
|
|
46
|
+
|
|
47
|
+
if len(all_bows) == 0:
|
|
48
|
+
click.echo("No Bodies of Water found in the system configuration.")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _print_equipment_count(name: str, count: int) -> None:
|
|
52
|
+
"""Helper function to print equipment counts with styling."""
|
|
53
|
+
click.echo(f" - {click.style(name, fg='green')}: {count}")
|
|
@@ -5,9 +5,11 @@ from typing import TYPE_CHECKING
|
|
|
5
5
|
from pyomnilogic_local._base import OmniEquipment
|
|
6
6
|
from pyomnilogic_local.collections import EquipmentDict
|
|
7
7
|
from pyomnilogic_local.csad_equip import CSADEquipment
|
|
8
|
+
from pyomnilogic_local.decorators import control_method
|
|
8
9
|
from pyomnilogic_local.models.mspconfig import MSPCSAD
|
|
9
10
|
from pyomnilogic_local.models.telemetry import TelemetryCSAD
|
|
10
11
|
from pyomnilogic_local.omnitypes import CSADMode, CSADStatus
|
|
12
|
+
from pyomnilogic_local.util import OmniEquipmentNotInitializedError
|
|
11
13
|
|
|
12
14
|
if TYPE_CHECKING:
|
|
13
15
|
from pyomnilogic_local.models.telemetry import Telemetry
|
|
@@ -74,30 +76,45 @@ class CSAD(OmniEquipment[MSPCSAD, TelemetryCSAD]):
|
|
|
74
76
|
return self.mspconfig.equip_type
|
|
75
77
|
|
|
76
78
|
@property
|
|
77
|
-
def
|
|
79
|
+
def ph_target_level(self) -> float:
|
|
78
80
|
"""Target pH level that the CSAD aims to maintain."""
|
|
79
81
|
return self.mspconfig.target_value
|
|
80
82
|
|
|
81
83
|
@property
|
|
82
|
-
def
|
|
84
|
+
def ph_current_value(self) -> float:
|
|
85
|
+
"""Current pH level reading from the sensor, including calibration offset."""
|
|
86
|
+
return self.telemetry.ph + self.ph_calibration_value
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def ph_current_value_raw(self) -> float:
|
|
90
|
+
"""Current pH level reading from the sensor without calibration offset."""
|
|
91
|
+
return self.telemetry.ph
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def ph_calibration_value(self) -> float:
|
|
83
95
|
"""Calibration offset value for pH sensor."""
|
|
84
96
|
return self.mspconfig.calibration_value
|
|
85
97
|
|
|
86
98
|
@property
|
|
87
|
-
def
|
|
99
|
+
def ph_low_alarm_level(self) -> float:
|
|
88
100
|
"""Low pH threshold for triggering an alarm."""
|
|
89
|
-
return self.mspconfig.
|
|
101
|
+
return self.mspconfig.ph_low_alarm_level
|
|
90
102
|
|
|
91
103
|
@property
|
|
92
|
-
def
|
|
104
|
+
def ph_high_alarm_level(self) -> float:
|
|
93
105
|
"""High pH threshold for triggering an alarm."""
|
|
94
|
-
return self.mspconfig.
|
|
106
|
+
return self.mspconfig.ph_high_alarm_level
|
|
95
107
|
|
|
96
108
|
@property
|
|
97
109
|
def orp_target_level(self) -> int:
|
|
98
110
|
"""Target ORP (Oxidation-Reduction Potential) level in millivolts."""
|
|
99
111
|
return self.mspconfig.orp_target_level
|
|
100
112
|
|
|
113
|
+
@property
|
|
114
|
+
def orp_current_level(self) -> int:
|
|
115
|
+
"""Current ORP (Oxidation-Reduction Potential) reading in millivolts."""
|
|
116
|
+
return self.telemetry.orp
|
|
117
|
+
|
|
101
118
|
@property
|
|
102
119
|
def orp_runtime_level(self) -> int:
|
|
103
120
|
"""ORP runtime level threshold in millivolts."""
|
|
@@ -129,36 +146,6 @@ class CSAD(OmniEquipment[MSPCSAD, TelemetryCSAD]):
|
|
|
129
146
|
"""Raw status value from telemetry."""
|
|
130
147
|
return self.telemetry.status
|
|
131
148
|
|
|
132
|
-
@property
|
|
133
|
-
def current_ph(self) -> float:
|
|
134
|
-
"""Current pH level reading from the sensor.
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
Current pH level (typically 0-14, where 7 is neutral)
|
|
138
|
-
|
|
139
|
-
Example:
|
|
140
|
-
>>> print(f"pH: {csad.current_ph:.2f}")
|
|
141
|
-
"""
|
|
142
|
-
return self.telemetry.ph
|
|
143
|
-
|
|
144
|
-
@property
|
|
145
|
-
def current_orp(self) -> int:
|
|
146
|
-
"""Current ORP (Oxidation-Reduction Potential) reading in millivolts.
|
|
147
|
-
|
|
148
|
-
Note:
|
|
149
|
-
ORP readings in CSAD telemetry are provided for monitoring purposes to
|
|
150
|
-
assess chlorinator effectiveness. The ORP sensor output is primarily
|
|
151
|
-
used by the chlorinator function for ORP-based automatic chlorine
|
|
152
|
-
generation control. The CSAD system focuses on pH control.
|
|
153
|
-
|
|
154
|
-
Returns:
|
|
155
|
-
Current ORP level in mV (typically 400-800 mV for pools)
|
|
156
|
-
|
|
157
|
-
Example:
|
|
158
|
-
>>> print(f"ORP: {csad.current_orp} mV")
|
|
159
|
-
"""
|
|
160
|
-
return self.telemetry.orp
|
|
161
|
-
|
|
162
149
|
@property
|
|
163
150
|
def mode(self) -> CSADMode:
|
|
164
151
|
"""Current operating mode of the CSAD.
|
|
@@ -234,8 +221,8 @@ class CSAD(OmniEquipment[MSPCSAD, TelemetryCSAD]):
|
|
|
234
221
|
>>> if csad.has_alert:
|
|
235
222
|
... print(f"Alert! {csad.alert_status}")
|
|
236
223
|
"""
|
|
237
|
-
ph_alert = self.
|
|
238
|
-
orp_alert = self.
|
|
224
|
+
ph_alert = self.ph_current_value < self.ph_low_alarm_level or self.ph_current_value > self.ph_high_alarm_level
|
|
225
|
+
orp_alert = self.orp_current_level < self.orp_low_alarm_level or self.orp_current_level > self.orp_high_alarm_level
|
|
239
226
|
return ph_alert or orp_alert
|
|
240
227
|
|
|
241
228
|
@property
|
|
@@ -252,54 +239,48 @@ class CSAD(OmniEquipment[MSPCSAD, TelemetryCSAD]):
|
|
|
252
239
|
"""
|
|
253
240
|
alerts = []
|
|
254
241
|
|
|
255
|
-
if self.
|
|
256
|
-
alerts.append(f"pH too low ({self.
|
|
257
|
-
elif self.
|
|
258
|
-
alerts.append(f"pH too high ({self.
|
|
242
|
+
if self.ph_current_value < self.ph_low_alarm_level:
|
|
243
|
+
alerts.append(f"pH too low ({self.ph_current_value:.2f} < {self.ph_low_alarm_level:.2f})")
|
|
244
|
+
elif self.ph_current_value > self.ph_high_alarm_level:
|
|
245
|
+
alerts.append(f"pH too high ({self.ph_current_value:.2f} > {self.ph_high_alarm_level:.2f})")
|
|
259
246
|
|
|
260
|
-
if self.
|
|
261
|
-
alerts.append(f"ORP too low ({self.
|
|
262
|
-
elif self.
|
|
263
|
-
alerts.append(f"ORP too high ({self.
|
|
247
|
+
if self.orp_current_level < self.orp_low_alarm_level:
|
|
248
|
+
alerts.append(f"ORP too low ({self.orp_current_level} < {self.orp_low_alarm_level} mV)")
|
|
249
|
+
elif self.orp_current_level > self.orp_high_alarm_level:
|
|
250
|
+
alerts.append(f"ORP too high ({self.orp_current_level} > {self.orp_high_alarm_level} mV)")
|
|
264
251
|
|
|
265
252
|
return "; ".join(alerts) if alerts else "OK"
|
|
266
253
|
|
|
267
|
-
@
|
|
268
|
-
def
|
|
269
|
-
"""
|
|
270
|
-
|
|
271
|
-
Returns:
|
|
272
|
-
Current pH level
|
|
254
|
+
@control_method
|
|
255
|
+
async def set_ph_target(self, ph_target: float) -> None:
|
|
256
|
+
"""Set the target pH for the CSAD.
|
|
273
257
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
Use current_orp property for ORP readings.
|
|
258
|
+
Raises:
|
|
259
|
+
OmniEquipmentNotInitializedError: If bow_id or system_id is None.
|
|
277
260
|
"""
|
|
278
|
-
|
|
261
|
+
if self.bow_id is None or self.system_id is None:
|
|
262
|
+
msg = "Cannot set pH: bow_id or system_id is None"
|
|
263
|
+
raise OmniEquipmentNotInitializedError(msg)
|
|
279
264
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
265
|
+
if not 7.0 <= ph_target <= 8.0:
|
|
266
|
+
msg = f"Invalid pH target: {ph_target}. Target pH must be between 7.0 and 8.0"
|
|
267
|
+
raise ValueError(msg)
|
|
283
268
|
|
|
284
|
-
|
|
285
|
-
Target pH level
|
|
269
|
+
await self._api.async_set_csad_ph_target_value(pool_id=self.bow_id, csad_id=self.system_id, ph_target=ph_target)
|
|
286
270
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
"""
|
|
291
|
-
return self.target_ph
|
|
271
|
+
@control_method
|
|
272
|
+
async def set_orp_target(self, orp_target: int) -> None:
|
|
273
|
+
"""Set the target ORP for the CSAD.
|
|
292
274
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
"""
|
|
275
|
+
Raises:
|
|
276
|
+
OmniEquipmentNotInitializedError: If bow_id or system_id is None.
|
|
277
|
+
"""
|
|
278
|
+
if self.bow_id is None or self.system_id is None:
|
|
279
|
+
msg = "Cannot set ORP: bow_id or system_id is None"
|
|
280
|
+
raise OmniEquipmentNotInitializedError(msg)
|
|
296
281
|
|
|
297
|
-
|
|
298
|
-
|
|
282
|
+
if not 400 <= orp_target <= 900:
|
|
283
|
+
msg = f"Invalid ORP target: {orp_target}. Target ORP must be between 400 and 900 mV"
|
|
284
|
+
raise ValueError(msg)
|
|
299
285
|
|
|
300
|
-
|
|
301
|
-
>>> offset = csad.ph_offset
|
|
302
|
-
>>> if offset > 0:
|
|
303
|
-
... print(f"pH is {offset:.2f} points above target")
|
|
304
|
-
"""
|
|
305
|
-
return self.current_ph - self.target_ph
|
|
286
|
+
await self._api.async_set_csad_orp_target_level(pool_id=self.bow_id, csad_id=self.system_id, orp_target=orp_target)
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/models/mspconfig.py
RENAMED
|
@@ -53,6 +53,7 @@ class OmniBase(BaseModel):
|
|
|
53
53
|
model_config = ConfigDict(from_attributes=True)
|
|
54
54
|
|
|
55
55
|
_sub_devices: set[str] | None = None
|
|
56
|
+
_propagate_bow_id_devices: set[str] | None = None
|
|
56
57
|
system_id: int = Field(alias="System-Id")
|
|
57
58
|
name: str | None = Field(alias="Name", default=None)
|
|
58
59
|
bow_id: int = -1
|
|
@@ -68,7 +69,9 @@ class OmniBase(BaseModel):
|
|
|
68
69
|
# If we have no devices under us, we have nothing to do
|
|
69
70
|
if self._sub_devices is None:
|
|
70
71
|
return
|
|
71
|
-
|
|
72
|
+
# If we have a specific list of devices that should receive the bow_id, use that, otherwise propagate to all sub_devices
|
|
73
|
+
propagate_devices = self._propagate_bow_id_devices if self._propagate_bow_id_devices is not None else self._sub_devices
|
|
74
|
+
for subdevice_name in propagate_devices:
|
|
72
75
|
subdevice = getattr(self, subdevice_name)
|
|
73
76
|
# If our subdevice is a list of subdevices ...
|
|
74
77
|
if isinstance(subdevice, list):
|
|
@@ -263,8 +266,8 @@ class MSPCSAD(OmniBase):
|
|
|
263
266
|
equip_type: CSADType = Field(alias="Type")
|
|
264
267
|
target_value: float = Field(alias="TargetValue")
|
|
265
268
|
calibration_value: float = Field(alias="CalibrationValue")
|
|
266
|
-
|
|
267
|
-
|
|
269
|
+
ph_low_alarm_level: float = Field(alias="PHLowAlarmLevel")
|
|
270
|
+
ph_high_alarm_level: float = Field(alias="PHHighAlarmLevel")
|
|
268
271
|
orp_target_level: int = Field(alias="ORP-Target-Level")
|
|
269
272
|
orp_runtime_level: int = Field(alias="ORP-Runtime-Level")
|
|
270
273
|
orp_low_alarm_level: int = Field(alias="ORP-Low-Alarm-Level")
|
|
@@ -333,7 +336,7 @@ class MSPBoW(OmniBase):
|
|
|
333
336
|
colorlogic_light: list[MSPColorLogicLight] | None = Field(alias="ColorLogic-Light", default=None)
|
|
334
337
|
pump: list[MSPPump] | None = Field(alias="Pump", default=None)
|
|
335
338
|
chlorinator: MSPChlorinator | None = Field(alias="Chlorinator", default=None)
|
|
336
|
-
csad:
|
|
339
|
+
csad: MSPCSAD | None = Field(alias="CSAD", default=None)
|
|
337
340
|
|
|
338
341
|
# We override the __init__ here so that we can trigger the propagation of the bow_id down to all of it's sub devices after the bow
|
|
339
342
|
# itself is initialized
|
|
@@ -346,7 +349,9 @@ class MSPBoW(OmniBase):
|
|
|
346
349
|
|
|
347
350
|
class MSPBackyard(OmniBase):
|
|
348
351
|
_sub_devices = {"sensor", "bow", "colorlogic_light", "relay"}
|
|
349
|
-
bow_id
|
|
352
|
+
# Only propagate bow_id to sub-devices that are not BoWs as they have their own bow_id that should be used instead
|
|
353
|
+
_propagate_bow_id_devices = {"sensor", "colorlogic_light", "relay"}
|
|
354
|
+
bow_id: int = 0
|
|
350
355
|
|
|
351
356
|
omni_type: OmniType = OmniType.BACKYARD
|
|
352
357
|
|
|
@@ -355,6 +360,14 @@ class MSPBackyard(OmniBase):
|
|
|
355
360
|
relay: list[MSPRelay] | None = Field(alias="Relay", default=None)
|
|
356
361
|
sensor: list[MSPSensor] | None = Field(alias="Sensor", default=None)
|
|
357
362
|
|
|
363
|
+
# We override the __init__ here so that we can trigger the propagation of the bow_id down to all of it's sub devices after the backyard
|
|
364
|
+
# itself is initialized
|
|
365
|
+
def __init__(self, **data: Any) -> None:
|
|
366
|
+
# As we are requiring a bow_id on everything in OmniBase, we need to propagate it down now
|
|
367
|
+
# before calling super().__init__() so that it will be present for validation.
|
|
368
|
+
super().__init__(**data)
|
|
369
|
+
self.propagate_bow_id(self.system_id)
|
|
370
|
+
|
|
358
371
|
|
|
359
372
|
class MSPSchedule(OmniBase):
|
|
360
373
|
omni_type: OmniType = OmniType.SCHEDULE
|
|
@@ -449,7 +462,6 @@ class MSPConfig(BaseModel):
|
|
|
449
462
|
# everything that *could* be a list into a list to make the parsing more consistent.
|
|
450
463
|
force_list=(
|
|
451
464
|
OmniType.BOW_MSP,
|
|
452
|
-
OmniType.CSAD,
|
|
453
465
|
OmniType.CL_LIGHT,
|
|
454
466
|
OmniType.FAVORITES,
|
|
455
467
|
OmniType.FILTER,
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/omnilogic.py
RENAMED
|
@@ -271,9 +271,7 @@ class OmniLogic:
|
|
|
271
271
|
@property
|
|
272
272
|
def all_csads(self) -> EquipmentDict[CSAD]:
|
|
273
273
|
"""Returns all CSAD instances across all bows in the backyard."""
|
|
274
|
-
csads
|
|
275
|
-
for bow in self.backyard.bow.values():
|
|
276
|
-
csads.extend(bow.csads.values())
|
|
274
|
+
csads = [bow.csad for bow in self.backyard.bow.values() if bow.csad is not None]
|
|
277
275
|
return EquipmentDict(csads)
|
|
278
276
|
|
|
279
277
|
@property
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/omnitypes.py
RENAMED
|
@@ -90,7 +90,7 @@ class BodyOfWaterType(PrettyEnum, StrEnum):
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
# Chlorinators
|
|
93
|
-
class ChlorinatorStatus(Flag):
|
|
93
|
+
class ChlorinatorStatus(PrettyEnum, Flag):
|
|
94
94
|
"""Chlorinator status flags.
|
|
95
95
|
|
|
96
96
|
These flags represent the current operational state of the chlorinator
|
|
@@ -107,7 +107,7 @@ class ChlorinatorStatus(Flag):
|
|
|
107
107
|
K2_ACTIVE = 1 << 7 # K2 relay is active
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
class ChlorinatorAlert(Flag):
|
|
110
|
+
class ChlorinatorAlert(PrettyEnum, Flag):
|
|
111
111
|
"""Chlorinator alert flags.
|
|
112
112
|
|
|
113
113
|
Multi-bit fields are represented by their individual values.
|
|
@@ -126,7 +126,7 @@ class ChlorinatorAlert(Flag):
|
|
|
126
126
|
CELL_CLEAN = 1 << 11 # Cell cleaning runtime alert
|
|
127
127
|
|
|
128
128
|
|
|
129
|
-
class ChlorinatorError(Flag):
|
|
129
|
+
class ChlorinatorError(PrettyEnum, Flag):
|
|
130
130
|
"""Chlorinator error flags.
|
|
131
131
|
|
|
132
132
|
Multi-bit fields are represented by their individual values.
|
|
@@ -419,8 +419,8 @@ class FilterWhyOn(PrettyEnum, IntEnum):
|
|
|
419
419
|
GROUP_COMMAND = 18
|
|
420
420
|
SPILLOVER_INTERLOCK = 19
|
|
421
421
|
MAX_VALUE = 20
|
|
422
|
-
UNKNOWN_1 = 21
|
|
423
|
-
UNKNOWN_2 = 22
|
|
422
|
+
UNKNOWN_1 = 21 # https://github.com/cryptk/python-omnilogic-local/issues/101
|
|
423
|
+
UNKNOWN_2 = 22 # https://github.com/cryptk/python-omnilogic-local/issues/101
|
|
424
424
|
|
|
425
425
|
|
|
426
426
|
class FilterSpeedPresets(PrettyEnum, StrEnum):
|
|
@@ -457,7 +457,7 @@ class HeaterMode(PrettyEnum, IntEnum):
|
|
|
457
457
|
HEAT = 0
|
|
458
458
|
COOL = 1
|
|
459
459
|
AUTO = 2
|
|
460
|
-
UNKNOWN_1 = 3
|
|
460
|
+
UNKNOWN_1 = 3 # https://github.com/cryptk/haomnilogic-local/issues/172
|
|
461
461
|
|
|
462
462
|
|
|
463
463
|
# Pumps
|
|
@@ -465,7 +465,7 @@ class PumpState(PrettyEnum, IntEnum):
|
|
|
465
465
|
OFF = 0
|
|
466
466
|
ON = 1
|
|
467
467
|
FREEZE_PROTECT = 2 # This is an assumption that 2 means freeze protect, ref: https://github.com/cryptk/haomnilogic-local/issues/147
|
|
468
|
-
UNKNOWN_1 = 3
|
|
468
|
+
UNKNOWN_1 = 3 # We assume this value exists as we have evidence of a state of 4 existing
|
|
469
469
|
PRIMING = 4 # https://github.com/cryptk/haomnilogic-local/issues/223
|
|
470
470
|
|
|
471
471
|
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# Need to figure out how to resolve the 'Untyped decorator makes function "..." untyped' errors in mypy when using click decorators
|
|
2
|
-
# mypy: disable-error-code="misc"
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
7
|
-
|
|
8
|
-
import click
|
|
9
|
-
|
|
10
|
-
from pyomnilogic_local.cli.utils import echo_properties
|
|
11
|
-
from pyomnilogic_local.omnitypes import BodyOfWaterType
|
|
12
|
-
|
|
13
|
-
if TYPE_CHECKING:
|
|
14
|
-
from pyomnilogic_local import OmniLogic
|
|
15
|
-
from pyomnilogic_local.models.mspconfig import MSPBoW
|
|
16
|
-
from pyomnilogic_local.models.telemetry import TelemetryType
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@click.command()
|
|
20
|
-
@click.pass_context
|
|
21
|
-
def bows(ctx: click.Context) -> None:
|
|
22
|
-
"""List all Bodies of Water (BOWs) and their current status.
|
|
23
|
-
|
|
24
|
-
Displays information about all bodies of water including their system IDs,
|
|
25
|
-
names, types (pool/spa), water temperature, flow status, and attached equipment.
|
|
26
|
-
|
|
27
|
-
Example:
|
|
28
|
-
omnilogic get bows
|
|
29
|
-
"""
|
|
30
|
-
omnilogic: OmniLogic = ctx.obj["OMNILOGIC"]
|
|
31
|
-
all_bows = omnilogic.all_bows
|
|
32
|
-
for bow in all_bows:
|
|
33
|
-
echo_properties(bow)
|
|
34
|
-
|
|
35
|
-
if len(all_bows) == 0:
|
|
36
|
-
click.echo("No Bodies of Water found in the system configuration.")
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _print_bow_info(bow: MSPBoW, telemetry: TelemetryType | None) -> None:
|
|
40
|
-
"""Format and print Body of Water information in a nice table format.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
bow: BOW object from MSPConfig with attributes to display
|
|
44
|
-
telemetry: Telemetry object containing current state information
|
|
45
|
-
"""
|
|
46
|
-
click.echo("\n" + "=" * 60)
|
|
47
|
-
click.echo("BODY OF WATER")
|
|
48
|
-
click.echo("=" * 60)
|
|
49
|
-
|
|
50
|
-
# Combine config and telemetry data
|
|
51
|
-
bow_data: dict[Any, Any] = {**dict(bow), **dict(telemetry)} if telemetry else dict(bow)
|
|
52
|
-
|
|
53
|
-
# Fields to exclude from main display (we'll show equipment counts instead)
|
|
54
|
-
exclude_fields = {"filter", "relay", "heater", "sensor", "colorlogic_light", "pump", "chlorinator", "csad"}
|
|
55
|
-
|
|
56
|
-
for attr_name, value in bow_data.items():
|
|
57
|
-
if attr_name in exclude_fields:
|
|
58
|
-
continue
|
|
59
|
-
|
|
60
|
-
if attr_name == "type":
|
|
61
|
-
value = str(BodyOfWaterType(value))
|
|
62
|
-
elif isinstance(value, list):
|
|
63
|
-
# Format lists nicely
|
|
64
|
-
value = ", ".join(str(v) for v in value) if value else "None"
|
|
65
|
-
|
|
66
|
-
# Format the attribute name to be more readable
|
|
67
|
-
display_name = attr_name.replace("_", " ").title()
|
|
68
|
-
click.echo(f"{display_name:20} : {value}")
|
|
69
|
-
|
|
70
|
-
# Show equipment summary
|
|
71
|
-
click.echo("\nAttached Equipment:")
|
|
72
|
-
click.echo("-" * 60)
|
|
73
|
-
|
|
74
|
-
equipment_counts = []
|
|
75
|
-
if bow.filter:
|
|
76
|
-
equipment_counts.append(f"Filters: {len(bow.filter)}")
|
|
77
|
-
if bow.pump:
|
|
78
|
-
equipment_counts.append(f"Pumps: {len(bow.pump)}")
|
|
79
|
-
if bow.heater:
|
|
80
|
-
equipment_counts.append("Heater: 1 (virtual)")
|
|
81
|
-
if bow.heater.heater_equipment:
|
|
82
|
-
equipment_counts.append(f" - Physical Heaters: {len(bow.heater.heater_equipment)}")
|
|
83
|
-
if bow.sensor:
|
|
84
|
-
equipment_counts.append(f"Sensors: {len(bow.sensor)}")
|
|
85
|
-
if bow.colorlogic_light:
|
|
86
|
-
equipment_counts.append(f"ColorLogic Lights: {len(bow.colorlogic_light)}")
|
|
87
|
-
if bow.relay:
|
|
88
|
-
equipment_counts.append(f"Relays: {len(bow.relay)}")
|
|
89
|
-
if bow.chlorinator:
|
|
90
|
-
equipment_counts.append("Chlorinator: 1")
|
|
91
|
-
if bow.csad:
|
|
92
|
-
equipment_counts.append(f"CSADs: {len(bow.csad)}")
|
|
93
|
-
|
|
94
|
-
if equipment_counts:
|
|
95
|
-
for count in equipment_counts:
|
|
96
|
-
click.echo(f" {count}")
|
|
97
|
-
else:
|
|
98
|
-
click.echo(" None")
|
|
99
|
-
|
|
100
|
-
click.echo("=" * 60)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/__init__.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/constants.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/exceptions.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/message.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/mock_api.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/api/protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/chlorinator_equip.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/__init__.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/backyard.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/commands.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/csads.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/filters.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/groups.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/heaters.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/lights.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/pumps.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/relays.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/schedules.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/sensors.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/get/valves.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/pcap_utils.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/cli/utils.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/collections.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/colorlogiclight.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/csad_equip.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/decorators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/heater_equip.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/models/__init__.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/models/const.py
RENAMED
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/models/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_omnilogic_local-1.1.0 → python_omnilogic_local-2.0.0}/pyomnilogic_local/models/telemetry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|