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 = 1
versus 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 = 6
The 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
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.stateis typed asValveActuatorState, which defines onlyOFF = 0and
ON = 1. Real controllers put a valve actuator into intermediate relay states while acommand settles — I have controller-side logs showing
RLY_WAITING_FOR_FILTER(=5) forroughly one second on every turn-on.
Any telemetry fetched during that window fails validation and raises
OmniParsingErrorfor theentire
Telemetrydocument, not just that one field. Downstream, in the Home Assistantintegration, that means every entity from the config entry drops to
unavailableuntil the nextsuccessful poll.
This is trivially reproducible offline — no controller required (script below).
Environment
python-omnilogic-localmspVersion)R0502000, build revision 28706STATUS versionstatusVersion="12")haomnilogic-local2.0.1 on Home Assistant 2026.8.3Equipment: 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
ValveActuatorelement: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_FILTERand only reachesRLY_ONabout a second later:RLY_WAITING_FOR_FILTERcorresponds toRelayState.WAITING_FOR_FILTER = 5inomnitypes.py.The same log confirms the controller was perfectly healthy across the failure — it serviced cloud
GetTelemetryrequests immediately before and after with no gap or error. The controller answeredthe poll; the library rejected the answer.
Reproduction (offline, no controller needed)
Output:
Root cause
models/telemetry.py,TelemetryValveActuator(~L410–427):omnitypes.pyL642:versus L589:
The 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.
TelemetryRelayandTelemetryValveActuatorare otherwise identicalmodels, differing only in the XML attribute name and this enum.
Proposed fix
ValveActuatorStatethen has no remaining users and could be deprecated or aliased toRelayState. 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:
ValveActuator @valveActuatorStateValveActuatorStateRelayState)ValveActuator @whyOnRelayWhyOnFilter @valvePositionFilterValvePositionFilter @filterStateFilterStateFilter @valvePosition = 0is worth a look independently: the panel log above shows the samesheer turn-on triggers
BOW_FLT_SET_DEFAULT_SPEED_FOR_VALVEon the filter, so a valve-positionchange happens inside the very same one-second window.
FilterValvePositionhas no0member,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_validateis all-or-nothing, one unknown enum value in one fielddiscards 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 = 22members (#101) are the same pattern — it may beworth 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.availablederives fromcoordinator.last_update_success, so a singleOmniParsingErrormarks every entity for theconfig entry
unavailableuntil the next successful poll. The integration also schedules an extrarefresh 1.5 s after any command (
do_next_refresh_after), which lands squarely in the transitionwindow — 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
unavailablefor ~9.5 s; the other 8 happened to poll after thetransition 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