Skip to content

[BUG] - ValveActuatorState only allows 0/1, but controllers report RLY_WAITING_FOR_FILTER (5) during transitions → OmniParsingError #155

Description

@bpinson999

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions