Repository navigation
[BUG] - ValveActuatorState only allows 0/1, but controllers report RLY_WAITING_FOR_FILTER (5) during transitions → OmniParsingError #155
Description
Disclosure / Notes
Claude did all the heavy lifting for me from a claude code session running on my Home Assistant docker host to review logs, code and HA history. Filing the bug here and not in haomnilogic-local since the issue is part of python-omnilogic-local. I've reviewed what claude did and the information provided. I've also included my HA diagnostic output even though that's only requested on the HA integration. Also, of note, my controller is connected via ethernet, not wifi.
config_entry-omnilogic_local-01JH1DD490XGDDY547AHBRF42R (2).json
Summary
TelemetryValveActuator.state is typed as ValveActuatorState, which defines only OFF = 0
and ON = 1. Real controllers put a valve actuator into intermediate relay states while a
command settles — I have controller-side logs showing RLY_WAITING_FOR_FILTER (= 5) for
roughly one second on every turn-on.
Any telemetry fetched during that window fails validation and raises OmniParsingError for the
entire Telemetry document, not just that one field. Downstream, in the Home Assistant
integration, that means every entity from the config entry drops to unavailable until the next
successful poll.
This is trivially reproducible offline — no controller required (script below).
Environment
python-omnilogic-local |
5.0.0 |
| pydantic | 2.13.4 |
Controller firmware (mspVersion) |
R0502000, build revision 28706 |
Telemetry STATUS version |
1.12 (statusVersion="12") |
| Downstream | haomnilogic-local 2.0.1 on Home Assistant 2026.8.3 |
Equipment: Hayward OmniLogic, pool + spa, one valve-actuator relay driving a sheer-descent
waterfall (systemId=6).
Evidence: the controller emits state 5
The valve actuator is reported as a ValveActuator element:
<ValveActuator systemId="6" valveActuatorState="0" whyOn="0" />The controller's own panel log (downloaded via USB on the display) shows
the transition on a turn-on command. Note it enters RLY_WAITING_FOR_FILTER and only reaches
RLY_ON about a second later:
[2026/08/25 17:59:58][Notify][PoolLogic][RLY_AddActionToPrioritizedList: state: RLY_ON why: RLY_MANUAL_ON target: 0 eventId: 22103 offTime: 0]
[2026/08/25 17:59:58][Notify][PoolLogic][RLY_WriteControl (Sheer): state: RLY_WAITING_FOR_FILTER why: RLY_MANUAL_ON target: 0 eventId: 22103 offTime: 0]
[2026/08/25 17:59:59][Notify][PoolLogic][FLT_PerformBowCntlMsg (BOW_POOL / 3) received BOW_FLT_SET_DEFAULT_SPEED_FOR_VALVE]
[2026/08/25 17:59:59][Notify][PoolLogic][FLT_WriteTheControl: Pump(0) filterIsOn for FILTER_ON, whyFilterIsOn = FMT_MANUAL_ON filter speed 80, valve pos FLT_VALVES_POS_POOL_ONLY]
[2026/08/25 17:59:59][Notify][PoolLogic][RLY_RestoreEventAction (Sheer): : state: RLY_ON why: RLY_MANUAL_ON target: 0 eventId: 22103 offTime: 0]
[2026/08/25 17:59:59][Notify][PoolLogic][RLY_WriteControl (Sheer): state: RLY_ON why: RLY_MANUAL_ON target: 0 eventId: 22103 offTime: 0]
RLY_WAITING_FOR_FILTER corresponds to RelayState.WAITING_FOR_FILTER = 5 in omnitypes.py.
The same log confirms the controller was perfectly healthy across the failure — it serviced cloud
GetTelemetry requests immediately before and after with no gap or error. The controller answered
the poll; the library rejected the answer.
Reproduction (offline, no controller needed)
from pyomnilogic_local.models.telemetry import Telemetry
from pyomnilogic_local.models.exceptions import OmniParsingError
XML = """<?xml version="1.0" encoding="UTF-8" ?>
<STATUS version="1.12">
<Backyard systemId="0" statusVersion="12" airTemp="101" state="1" ConfigChksum="2759708" mspVersion="R0502000" />
<BodyOfWater systemId="1" waterTemp="93" flow="1" />
<ValveActuator systemId="6" valveActuatorState="{v}" whyOn="5" />
</STATUS>"""
for v in (0, 1, 5):
try:
Telemetry.load_xml(XML.format(v=v))
print(f"valveActuatorState={v}: parsed OK")
except OmniParsingError as e:
print(f"valveActuatorState={v}: {type(e).__name__}: {str(e).splitlines()[2].strip()}")Output:
valveActuatorState=0: parsed OK
valveActuatorState=1: parsed OK
valveActuatorState=5: OmniParsingError: Input should be 0 or 1 [type=enum, input_value=5, input_type=int]
Root cause
models/telemetry.py, TelemetryValveActuator (~L410–427):
omni_type: OmniType = OmniType.VALVE_ACTUATOR
system_id: int = Field(alias="@systemId")
state: ValveActuatorState = Field(alias="@valveActuatorState")
# Valve actuators are actually relays, so we can reuse the RelayWhyOn enum here
why_on: RelayWhyOn = Field(alias="@whyOn")omnitypes.py L642:
class ValveActuatorState(PrettyEnum, IntEnum):
OFF = 0
ON = 1versus L589:
class RelayState(PrettyEnum, IntEnum):
OFF = 0
ON = 1
ON_FREEZE_PROTECT = 2
WAITING_FOR_INTERLOCK = 3
PAUSED = 4
WAITING_FOR_FILTER = 5
STATE_MAX_ENTRY = 6The comment on the line directly below already makes the argument: "Valve actuators are actually
relays, so we can reuse the RelayWhyOn enum here." That reasoning applies equally to state —
it just wasn't carried over. TelemetryRelay and TelemetryValveActuator are otherwise identical
models, differing only in the XML attribute name and this enum.
Proposed fix
class TelemetryValveActuator(BaseModel):
...
omni_type: OmniType = OmniType.VALVE_ACTUATOR
system_id: int = Field(alias="@systemId")
# Valve actuators are actually relays, so we reuse the Relay enums here
state: RelayState = Field(alias="@valveActuatorState")
why_on: RelayWhyOn = Field(alias="@whyOn")ValveActuatorState then has no remaining users and could be deprecated or aliased to
RelayState. Happy to open a PR if that's the direction you'd prefer.
Related: other fields with the same exposure
Probing the same real telemetry document field-by-field with the offline harness above, these
also reject values the controller can plausibly emit:
| Field | Enum | Accepted | Rejected |
|---|---|---|---|
ValveActuator @valveActuatorState |
ValveActuatorState |
0–1 | 2, 3, 4, 5, 6 (every other RelayState) |
ValveActuator @whyOn |
RelayWhyOn |
0–10 | 11+ |
Filter @valvePosition |
FilterValvePosition |
1–5 | 0, 6+ |
Filter @filterState |
FilterState |
0–12 | 13+ |
Filter @valvePosition = 0 is worth a look independently: the panel log above shows the same
sheer turn-on triggers BOW_FLT_SET_DEFAULT_SPEED_FOR_VALVE on the filter, so a valve-position
change happens inside the very same one-second window. FilterValvePosition has no 0 member,
so if the controller reports an unset/transitioning position it would fail the same way.
Broader suggestion (separate from the fix above)
Because Telemetry.model_validate is all-or-nothing, one unknown enum value in one field
discards the whole telemetry document. Given that these enums are reverse-engineered and
controllers clearly emit values outside them — the existing FilterWhyOn.UNKNOWN_1 = 21 /
UNKNOWN_2 = 22 members (#101) are the same pattern — it may be
worth degrading gracefully: coerce an unrecognised enum value to a sentinel and log a warning,
rather than raising. That would turn "every entity unavailable" into "one field shows unknown",
and would surface new values in logs instead of as outages.
Downstream impact
In haomnilogic-local, OmniLogicEntity.available derives from
coordinator.last_update_success, so a single OmniParsingError marks every entity for the
config entry unavailable until the next successful poll. The integration also schedules an extra
refresh 1.5 s after any command (do_next_refresh_after), which lands squarely in the transition
window — so this fires on a large fraction of user-initiated commands.
Measured on my system: over 10 days of recorder history, 2 of 10 scheduled waterfall turn-ons
flapped all 73 entities to unavailable for ~9.5 s; the other 8 happened to poll after the
transition completed and were clean.
Identifiers (device ID, MSP ID, local IP) redacted from the log excerpts above; happy to supply
a full unredacted log privately if useful.
config_entry-omnilogic_local-01JH1DD490XGDDY547AHBRF42R (2).json
Activity
bpinson999 commentedon Aug 26, 2026
Additional entry from logging that claude added. This was right after I turned the waterfall/sheer on:
fix: use RelayState for parsing the state of Valve Actuators, fixes #155