Skip to content

Commit 13e98ab

Browse files
authored
feat: add decoding of chlorinator status, alert and error values (#75)
1 parent 251ea8e commit 13e98ab

4 files changed

Lines changed: 515 additions & 11 deletions

File tree

pyomnilogic_local/models/telemetry.py

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
from ..exceptions import OmniParsingException
99
from ..omnitypes import (
1010
BackyardState,
11+
ChlorinatorAlert,
12+
ChlorinatorError,
1113
ChlorinatorOperatingMode,
14+
ChlorinatorStatus,
1215
ColorLogicBrightness,
1316
ColorLogicPowerState,
1417
ColorLogicShow,
@@ -73,18 +76,95 @@ class TelemetryChlorinator(BaseModel):
7376
status_raw: int = Field(alias="@status")
7477
instant_salt_level: int = Field(alias="@instantSaltLevel")
7578
avg_salt_level: int = Field(alias="@avgSaltLevel")
76-
chlr_alert: int = Field(alias="@chlrAlert")
77-
chlr_error: int = Field(alias="@chlrError")
79+
chlr_alert_raw: int = Field(alias="@chlrAlert")
80+
chlr_error_raw: int = Field(alias="@chlrError")
7881
sc_mode: int = Field(alias="@scMode")
7982
operating_state: int = Field(alias="@operatingState")
8083
timed_percent: int | None = Field(alias="@Timed-Percent", default=None)
8184
operating_mode: ChlorinatorOperatingMode | int = Field(alias="@operatingMode")
8285
enable: bool = Field(alias="@enable")
8386

84-
# Still need to do a bit more work to determine if a chlorinator is actively chlorinating
85-
# @property
86-
# def active(self) -> bool:
87-
# return self.status_raw & 4 == 4 # Check if bit 4 is set, which means the chlorinator is currently chlorinating
87+
@property
88+
def status(self) -> list[str]:
89+
"""Decode status bitmask into a list of active status flag names.
90+
91+
Returns:
92+
List of active ChlorinatorStatus flag names as strings
93+
94+
Example:
95+
>>> chlorinator.status
96+
['ALERT_PRESENT', 'GENERATING', 'K1_ACTIVE']
97+
"""
98+
return [flag.name for flag in ChlorinatorStatus if self.status_raw & flag.value and flag.name is not None]
99+
100+
@property
101+
def alerts(self) -> list[str]:
102+
"""Decode chlrAlert bitmask into a list of active alert flag names.
103+
104+
Returns:
105+
List of active ChlorinatorAlert flag names as strings
106+
107+
Note:
108+
When both CELL_TEMP_LOW and CELL_TEMP_SCALEBACK are set (bits 5:4 = 11),
109+
they are replaced with "CELL_TEMP_HIGH" for semantic correctness.
110+
111+
Example:
112+
>>> chlorinator.alerts
113+
['SALT_LOW', 'HIGH_CURRENT']
114+
"""
115+
116+
flags = ChlorinatorAlert(self.chlr_alert_raw)
117+
high_temp_bits = ChlorinatorAlert.CELL_TEMP_LOW | ChlorinatorAlert.CELL_TEMP_SCALEBACK
118+
cell_temp_high = False
119+
120+
if flags & high_temp_bits == high_temp_bits:
121+
cell_temp_high = True
122+
flags = flags & ~high_temp_bits
123+
124+
final_flags = [flag.name for flag in ChlorinatorAlert if flags & flag and flag.name is not None]
125+
if cell_temp_high:
126+
final_flags.append("CELL_TEMP_HIGH")
127+
128+
return final_flags
129+
130+
@property
131+
def errors(self) -> list[str]:
132+
"""Decode chlrError bitmask into a list of active error flag names.
133+
134+
Returns:
135+
List of active ChlorinatorError flag names as strings
136+
137+
Note:
138+
When both CELL_ERROR_TYPE and CELL_ERROR_AUTH are set (bits 13:12 = 11),
139+
they are replaced with "CELL_COMM_LOSS" for semantic correctness.
140+
141+
Example:
142+
>>> chlorinator.errors
143+
['CURRENT_SENSOR_SHORT', 'VOLTAGE_SENSOR_OPEN']
144+
"""
145+
146+
flags = ChlorinatorError(self.chlr_error_raw)
147+
cell_comm_loss_bits = ChlorinatorError.CELL_ERROR_TYPE | ChlorinatorError.CELL_ERROR_AUTH
148+
cell_comm_loss = False
149+
150+
if flags & cell_comm_loss_bits == cell_comm_loss_bits:
151+
cell_comm_loss = True
152+
flags = flags & ~cell_comm_loss_bits
153+
154+
final_flags = [flag.name for flag in ChlorinatorError if flags & flag and flag.name is not None]
155+
if cell_comm_loss:
156+
final_flags.append("CELL_COMM_LOSS")
157+
158+
return final_flags
159+
160+
@property
161+
def active(self) -> bool:
162+
"""Check if the chlorinator is actively generating chlorine.
163+
164+
Returns:
165+
True if the GENERATING status flag is set, False otherwise
166+
"""
167+
return ChlorinatorStatus.GENERATING.value & self.status_raw == ChlorinatorStatus.GENERATING.value
88168

89169

90170
class TelemetryCSAD(BaseModel):

pyomnilogic_local/omnitypes.py

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from enum import Enum, IntEnum
1+
from enum import Enum, Flag, IntEnum
22

33
from .util import PrettyEnum
44

@@ -85,12 +85,66 @@ class BodyOfWaterType(str, PrettyEnum):
8585

8686

8787
# Chlorinators
88-
# Chlorinator status is a bitmask that we still need to figure out
89-
# class ChlorinatorStatus(str,Enum):
90-
# pass
88+
class ChlorinatorStatus(Flag):
89+
"""Chlorinator status flags.
90+
91+
These flags represent the current operational state of the chlorinator
92+
and can be combined (multiple flags can be active simultaneously).
93+
"""
94+
95+
ERROR_PRESENT = 1 << 0 # Error present, check chlrError value
96+
ALERT_PRESENT = 1 << 1 # Alert present, check chlrAlert value
97+
GENERATING = 1 << 2 # Power is applied to T-Cell (actively chlorinating)
98+
SYSTEM_PAUSED = 1 << 3 # System processor is pausing chlorination
99+
LOCAL_PAUSED = 1 << 4 # Local processor is pausing chlorination
100+
AUTHENTICATED = 1 << 5 # T-Cell is authenticated
101+
K1_ACTIVE = 1 << 6 # K1 relay is active
102+
K2_ACTIVE = 1 << 7 # K2 relay is active
103+
104+
105+
class ChlorinatorAlert(Flag):
106+
"""Chlorinator alert flags.
107+
108+
Multi-bit fields are represented by their individual values.
109+
Use the helper properties on TelemetryChlorinator for semantic interpretation.
110+
"""
111+
112+
SALT_LOW = 1 << 0 # Salt level is low
113+
SALT_TOO_LOW = 1 << 1 # Salt level is too low
114+
HIGH_CURRENT = 1 << 2 # High current alert
115+
LOW_VOLTAGE = 1 << 3 # Low voltage alert
116+
CELL_TEMP_LOW = 1 << 4 # Cell water temperature low
117+
CELL_TEMP_SCALEBACK = 1 << 5 # Cell water temperature scaleback
118+
# CELL_TEMP_LOW and CELL_TEMP_SCALEBACK = CELL_TEMP_HIGH
119+
BOARD_TEMP_HIGH = 1 << 6 # Board temperature high
120+
BOARD_TEMP_CLEARING = 1 << 7 # Board temperature clearing
121+
CELL_CLEAN = 1 << 11 # Cell cleaning runtime alert
122+
123+
124+
class ChlorinatorError(Flag):
125+
"""Chlorinator error flags.
126+
127+
Multi-bit fields are represented by their individual values.
128+
Use the helper properties on TelemetryChlorinator for semantic interpretation.
129+
"""
130+
131+
CURRENT_SENSOR_SHORT = 1 << 0
132+
CURRENT_SENSOR_OPEN = 1 << 1
133+
VOLTAGE_SENSOR_SHORT = 1 << 2
134+
VOLTAGE_SENSOR_OPEN = 1 << 3
135+
CELL_TEMP_SENSOR_SHORT = 1 << 4
136+
CELL_TEMP_SENSOR_OPEN = 1 << 5
137+
BOARD_TEMP_SENSOR_SHORT = 1 << 6
138+
BOARD_TEMP_SENSOR_OPEN = 1 << 7
139+
K1_RELAY_SHORT = 1 << 8
140+
K1_RELAY_OPEN = 1 << 9
141+
K2_RELAY_SHORT = 1 << 10
142+
K2_RELAY_OPEN = 1 << 11
143+
CELL_ERROR_TYPE = 1 << 12
144+
CELL_ERROR_AUTH = 1 << 13
145+
AQUARITE_PCB_ERROR = 1 << 14
91146

92147

93-
# I have seen one pool that had an operatingMode of 3, I am not sure what that means, perhaps that is an OFF mode
94148
class ChlorinatorOperatingMode(IntEnum):
95149
DISABLED = 0
96150
TIMED = 1

tests/test_chlorinator_bitmask.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Tests for chlorinator bitmask decoding."""
2+
3+
from pyomnilogic_local.models.telemetry import TelemetryChlorinator
4+
5+
6+
def test_chlorinator_status_decoding() -> None:
7+
"""Test decoding of chlorinator status bitmask."""
8+
# Create a chlorinator with status = 134 (0b10000110)
9+
# Bit 1: ALERT_PRESENT (2)
10+
# Bit 2: GENERATING (4)
11+
# Bit 7: K2_ACTIVE (128)
12+
# Total: 2 + 4 + 128 = 134
13+
data = {
14+
"@systemId": 5,
15+
"@status": 134,
16+
"@instantSaltLevel": 4082,
17+
"@avgSaltLevel": 4042,
18+
"@chlrAlert": 0,
19+
"@chlrError": 0,
20+
"@scMode": 0,
21+
"@operatingState": 1,
22+
"@Timed-Percent": 70,
23+
"@operatingMode": 1,
24+
"@enable": True,
25+
}
26+
27+
chlorinator = TelemetryChlorinator.model_validate(data)
28+
29+
# Check raw value
30+
assert chlorinator.status_raw == 134
31+
32+
# Check decoded status (returns list of string names)
33+
status_flags = chlorinator.status
34+
assert "ALERT_PRESENT" in status_flags
35+
assert "GENERATING" in status_flags
36+
assert "K2_ACTIVE" in status_flags
37+
assert len(status_flags) == 3
38+
39+
# Verify active property
40+
assert chlorinator.active is True
41+
42+
43+
def test_chlorinator_alert_decoding() -> None:
44+
"""Test decoding of chlorinator alert bitmask."""
45+
# Create a chlorinator with chlrAlert = 32 (0b00100000)
46+
# Bit 5: CELL_TEMP_SCALEBACK (32)
47+
data = {
48+
"@systemId": 5,
49+
"@status": 2, # ALERT_PRESENT
50+
"@instantSaltLevel": 4082,
51+
"@avgSaltLevel": 4042,
52+
"@chlrAlert": 32,
53+
"@chlrError": 0,
54+
"@scMode": 0,
55+
"@operatingState": 1,
56+
"@operatingMode": 1,
57+
"@enable": True,
58+
}
59+
60+
chlorinator = TelemetryChlorinator.model_validate(data)
61+
62+
# Check raw value
63+
assert chlorinator.chlr_alert_raw == 32
64+
65+
# Check decoded alerts (returns list of string names)
66+
alert_flags = chlorinator.alerts
67+
assert "CELL_TEMP_SCALEBACK" in alert_flags
68+
assert len(alert_flags) == 1
69+
70+
71+
def test_chlorinator_error_decoding() -> None:
72+
"""Test decoding of chlorinator error bitmask."""
73+
# Create a chlorinator with chlrError = 257 (0b100000001)
74+
# Bit 0: CURRENT_SENSOR_SHORT (1)
75+
# Bit 8: K1_RELAY_SHORT (256)
76+
# Total: 1 + 256 = 257
77+
data = {
78+
"@systemId": 5,
79+
"@status": 1, # ERROR_PRESENT
80+
"@instantSaltLevel": 4082,
81+
"@avgSaltLevel": 4042,
82+
"@chlrAlert": 0,
83+
"@chlrError": 257,
84+
"@scMode": 0,
85+
"@operatingState": 1,
86+
"@operatingMode": 1,
87+
"@enable": True,
88+
}
89+
90+
chlorinator = TelemetryChlorinator.model_validate(data)
91+
92+
# Check raw value
93+
assert chlorinator.chlr_error_raw == 257
94+
95+
# Check decoded errors (returns list of string names)
96+
error_flags = chlorinator.errors
97+
assert "CURRENT_SENSOR_SHORT" in error_flags
98+
assert "K1_RELAY_SHORT" in error_flags
99+
assert len(error_flags) == 2
100+
101+
102+
def test_chlorinator_no_flags() -> None:
103+
"""Test chlorinator with no status/alert/error flags set."""
104+
data = {
105+
"@systemId": 5,
106+
"@status": 0,
107+
"@instantSaltLevel": 4082,
108+
"@avgSaltLevel": 4042,
109+
"@chlrAlert": 0,
110+
"@chlrError": 0,
111+
"@scMode": 0,
112+
"@operatingState": 1,
113+
"@operatingMode": 1,
114+
"@enable": True,
115+
}
116+
117+
chlorinator = TelemetryChlorinator.model_validate(data)
118+
119+
# All should be empty
120+
assert chlorinator.status == []
121+
assert chlorinator.alerts == []
122+
assert chlorinator.errors == []
123+
assert chlorinator.active is False
124+
125+
126+
def test_chlorinator_complex_alerts() -> None:
127+
"""Test complex multi-bit alert combinations."""
128+
# chlrAlert = 67 (0b01000011)
129+
# Bit 0: SALT_LOW (1)
130+
# Bit 1: SALT_VERY_LOW (2)
131+
# Bit 6: BOARD_TEMP_HIGH (64)
132+
# Total: 1 + 2 + 64 = 67
133+
data = {
134+
"@systemId": 5,
135+
"@status": 2,
136+
"@instantSaltLevel": 4082,
137+
"@avgSaltLevel": 4042,
138+
"@chlrAlert": 67,
139+
"@chlrError": 0,
140+
"@scMode": 0,
141+
"@operatingState": 1,
142+
"@operatingMode": 1,
143+
"@enable": True,
144+
}
145+
146+
chlorinator = TelemetryChlorinator.model_validate(data)
147+
148+
alert_flags = chlorinator.alerts
149+
assert "SALT_LOW" in alert_flags
150+
assert "SALT_TOO_LOW" in alert_flags
151+
assert "BOARD_TEMP_HIGH" in alert_flags
152+
assert len(alert_flags) == 3
153+
154+
155+
def test_chlorinator_all_status_flags() -> None:
156+
"""Test chlorinator with all status flags set."""
157+
# status = 255 (0b11111111) - all 8 bits set
158+
data = {
159+
"@systemId": 5,
160+
"@status": 255,
161+
"@instantSaltLevel": 4082,
162+
"@avgSaltLevel": 4042,
163+
"@chlrAlert": 0,
164+
"@chlrError": 0,
165+
"@scMode": 0,
166+
"@operatingState": 1,
167+
"@operatingMode": 1,
168+
"@enable": True,
169+
}
170+
171+
chlorinator = TelemetryChlorinator.model_validate(data)
172+
173+
status_flags = chlorinator.status
174+
assert "ERROR_PRESENT" in status_flags
175+
assert "ALERT_PRESENT" in status_flags
176+
assert "GENERATING" in status_flags
177+
assert "SYSTEM_PAUSED" in status_flags
178+
assert "LOCAL_PAUSED" in status_flags
179+
assert "AUTHENTICATED" in status_flags
180+
assert "K1_ACTIVE" in status_flags
181+
assert "K2_ACTIVE" in status_flags
182+
assert len(status_flags) == 8

0 commit comments

Comments
 (0)