pyisyox.runtime.events module

WebSocket event parsing and dispatch.

Two transports — legacy /rest/subscribe (raw XML) and modern /api/events/subscribe (JSON-wrapped, adds PG3 spolisy channel) — wrap the same <Event> payload, so parse_event_frame() accepts either. System events use underscore-prefixed control ids (_5, _28, …) — see SystemEventControl.

Decoupled from the WS reader (pyisyox.runtime.ws) so the dispatcher can be tested with synthetic frames.

class SystemEventControl(*values)[source]

Bases: StrEnum

IoX WebSocket “system” control codes (underscore-prefixed).

Property updates use the property id ("ST", "GV1", …) with a populated node_address. System events use one of these underscore-prefixed codes with an empty node_address.

Codes _0-_23 are the full ISY-994 set from the ISY994 Developer Cookbook §8.5; _24-_28 are IoX-6 additions (system editors, the modern Z-Wave / ZigBee / Matter drivers, system upgrade) not in that document — tracked from UDI’s internal UDEvents.h taxonomy. Newer IoX firmware may emit further codes; those aren’t enumerated, and label() passes them through verbatim so logs still identify them.

HEARTBEAT

Periodic heartbeat. <action> is the duration in seconds until the next expected heartbeat (use it to detect a stalled stream). No <eventInfo>.

TRIGGER

Trigger events — program status, variable change/init, schedule change, key/info-string pushes, “get status” refresh signal. <action> discriminates; see TriggerAction.

DRIVER_SPECIFIC

Driver-specific events — payload depends on the underlying protocol driver. Not modelled.

NODE_LIFECYCLE

Node / scene / folder lifecycle — add / remove / rename / enable / revise / comm-error / etc. <action> carries the verb; see NodeLifecycleAction and NODE_LIFECYCLE_EVENT_INFO_TAGS.

SYSTEM_CONFIG

System configuration updated — time / NTP / notifications / batch-mode / battery-write-mode. <action> 0-6; see SystemConfigAction.

SYSTEM_STATUS

Controller-side busy/idle/safe-mode status. <action> 0-3; see pyisyox.constants.SystemStatus.

INTERNET_ACCESS

Internet-access status — disabled / enabled (<eventInfo> = external URL) / failed. See InternetAccessStatus.

PROGRESS

Progress report during long-running operations (device programming, restore, device-adder). <action> 1 / 2.1 / 2.2 / 2.3; see ProgressAction. The _7A / _7M device-write sub-codes also ride through on this control — see DeviceWriteAction.

SECURITY_SYSTEM

Security-system event — connected / disconnected / armed-* / disarmed. See SecuritySystemAction.

SYSTEM_ALERT

System alert event — “not implemented and should be ignored” per the cookbook.

OPENADR

OpenADR / Flex-Your-Power events — ISY994 Z-Series demand-response.

CLIMATE

Climate / weather events — required the ISY994 WeatherBug module; not present on eisy.

AMI_SEP

AMI/SEP energy events — ISY994 only (see the Energy Management Developer’s Manual).

ENERGY_MONITORING

External energy-monitoring (Brultech) — ISY994 only; on later firmware these are folded into node events instead.

UPB_LINKER

UPB linker events — UPB-enabled units only.

UPB_DEVICE_ADDER

UPB device-adder state — UPB-enabled units only.

UPB_DEVICE_STATUS

UPB device-status events — UPB-enabled units only.

GAS_METER

Gas-meter events — ISY994 only.

ZIGBEE

Legacy ZigBee events — ISY994-era driver. See ZIGBEE_UYB (_27) for the IoX-6+ ZigBee driver used on eisy.

ELK

ELK alarm-panel events — requires the ELK module (see the ELK Integration Developer’s Manual).

DEVICE_LINKER

Device-linker events — <action> 1 (status) / 2 (cleared). See DeviceLinkerAction.

ZWAVE

Legacy Z-Wave integration events — ISY994-era driver. See ZMATTER_ZWAVE (_25) for the IoX-6+ ZMatter Z-Wave driver used on eisy.

BILLING

Billing events — ISY994 ZS-series only.

PORTAL

Portal events — portal socket-connection / account-registration status when a portal module is installed.

SYSTEM_EDITOR

System editor changed — fired when a “system editor” (e.g. _sys_notify_short) is updated. <node> carries the editor name. <action> is SystemEditorAction. IoX-6 addition.

ZMATTER_ZWAVE

ZMatter Z-Wave events — IoX-6+ Z-Wave driver on eisy hardware. <action> is dotted ("{category}.{type}"); category numbers are system-status (1), discovery (2), general-status (3), general-error (4), S2 (5), OTA (6), backup/restore (7), device- interview (8), button-detect (9), logger (10). Sub-action details aren’t modelled — the dotted string passes through verbatim. Distinct from ZWAVE (_21, ISY994-era driver).

SYSTEM_UPGRADE

System-upgrade lifecycle — <action> is SystemUpgradeAction (active / inactive / available / reboot-required). IoX-6 addition.

ZIGBEE_UYB

ZigBee events — IoX-6+ ZigBee driver on eisy hardware. Same dotted "{category}.{type}" action shape as ZMATTER_ZWAVE (minus the logger sub-category). Distinct from ZIGBEE (_18, ISY994-era driver).

MATTER_STATUS

Matter network status — IoX-6+ Matter driver. <action> is dotted; active sub-categories are 1 (system status), 2 (discovery), 3 (RX/TX), 8 (device interview). Not in the ISY994 cookbook.

Note: _28 is also reserved in UDI’s source for Profile change events (actions 1-8 — profile/editor/nodedef/linkdef updated/deleted) — but no firmware path fires those today (placeholders since Dec 2024 per UDI). Don’t subscribe expecting them.

classmethod label(control)[source]

Friendly name for a system control code, or the raw code if unknown — so a log line reads node_lifecycle = ND instead of _3 = ND.

Parameters:

control (str)

Return type:

str

class TriggerAction(*values)[source]

Bases: StrEnum

Action codes carried in SystemEventControl.TRIGGER (_1) frames — ISY994 Developer Cookbook §8.5.3. <action> discriminates what the frame is; pyisyox only routes on PROGRAM_STATUS / VARIABLE_VALUE / VARIABLE_INIT.

PROGRAM_STATUS

Program status changed — handled by _apply_program_status. <eventInfo> carries the program <id>, enabled/run-at-reboot flags, last run/finish times, and a bitwise <s> status.

GET_STATUS

“Get status” — the controller is telling subscribers to re-poll everything (e.g. after a config change). No payload.

KEY_CHANGED

A key changed. node carries the key.

INFO_STRING

An info string. node carries the key; <eventInfo> is the text.

IR_LEARN_MODE

IR learn mode toggled. No payload.

SCHEDULE

A schedule’s status changed. node carries the key.

VARIABLE_VALUE

Variable value changed — handled by _apply_variable_change. <eventInfo> carries <var type id><val><ts>.

VARIABLE_INIT

Variable init (restore-on-startup) value changed — same handler / payload shape as VARIABLE_VALUE, applied to init.

KEY

The current subscription key, sent once right after a new subscription is established. <eventInfo> is the key.

VARIABLE_TABLE_CHANGED

Variable table structurally changed — fires when a variable is added or removed, or when its precision is changed (a metadata change that the per-value VARIABLE_VALUE / VARIABLE_INIT frames don’t cover). <eventInfo> carries <var><type>N</type><id>0</id></var> (id=0 is the wildcard sentinel — “this whole type’s table changed”). The right response is to re-fetch /api/variables/{type} for the affected type so the registry mirrors the controller’s metadata (precision in particular — the wire val from the per-value frames is raw, and a stale precision will mis-render the value). The dispatcher recognises it and fires EventDispatcher.add_variable_table_change_listener() callbacks; the dispatcher itself does not re-fetch.

classmethod label(value)[source]

Friendly lower-case name for a trigger-action code, or the raw value if it isn’t one we know.

Parameters:

value (str)

Return type:

str

class ProgressAction(*values)[source]

Bases: StrEnum

Action codes on SystemEventControl.PROGRESS (_7) frames — Cookbook §8.5.9. <eventInfo> is free-text progress detail.

UPDATE

Generic progress update.

DEVICE_ADDER_INFO

Device-adder info (UPB only).

DEVICE_ADDER_WARN

Device-adder warning (UPB only).

DEVICE_ADDER_ERROR

Device-adder error (UPB only).

classmethod label(value)[source]

Friendly lower-case name, or the raw value.

Parameters:

value (str)

Return type:

str

class SystemConfigAction(*values)[source]

Bases: StrEnum

Action codes on SystemEventControl.SYSTEM_CONFIG (_4) frames — Cookbook §8.5.6.

TIME_CHANGED
TIME_CONFIG_CHANGED
NTP_SETTINGS_UPDATED
NOTIFICATIONS_SETTINGS_UPDATED
NTP_COMM_ERROR
BATCH_MODE_UPDATED

Batch mode toggled — <eventInfo><status> is "1"/"0".

BATTERY_WRITE_MODE_UPDATED

Battery-powered-write mode toggled — <eventInfo><status> is "1"/"0".

classmethod label(value)[source]

Friendly lower-case name, or the raw value.

Parameters:

value (str)

Return type:

str

class InternetAccessStatus(*values)[source]

Bases: StrEnum

Action codes on SystemEventControl.INTERNET_ACCESS (_6) frames — Cookbook §8.5.8.

DISABLED
ENABLED

Enabled — <eventInfo> is the external URL.

FAILED
classmethod label(value)[source]

Friendly lower-case name, or the raw value.

Parameters:

value (str)

Return type:

str

class SecuritySystemAction(*values)[source]

Bases: StrEnum

Action codes on SystemEventControl.SECURITY_SYSTEM (_8) frames — Cookbook §8.5.10. node and <eventInfo> are null.

DISCONNECTED
CONNECTED
DISARMED
ARMED_AWAY
ARMED_STAY
ARMED_STAY_INSTANT
ARMED_NIGHT
ARMED_NIGHT_INSTANT
ARMED_VACATION
classmethod label(value)[source]

Friendly lower-case name, or the raw value.

Parameters:

value (str)

Return type:

str

class DeviceLinkerAction(*values)[source]

Bases: StrEnum

Action codes on SystemEventControl.DEVICE_LINKER (_20) frames — Cookbook §8.5.22 (udievnts.xsd).

STATUS

Linking status update — <eventInfo> carries device-linker info.

CLEARED

The device-linking list was cleared. No payload.

classmethod label(value)[source]

Friendly lower-case name, or the raw value.

Parameters:

value (str)

Return type:

str

class SystemUpgradeAction(*values)[source]

Bases: StrEnum

Action codes on SystemEventControl.SYSTEM_UPGRADE (_26) frames — IoX-6 firmware-upgrade lifecycle.

ACTIVE

Upgrade in progress.

INACTIVE

Upgrade not active (post-completion or idle).

AVAILABLE

A new upgrade is available to install.

REBOOT_REQUIRED

Upgrade applied; reboot required to take effect.

classmethod label(value)[source]

Friendly lower-case name, or the raw value.

Parameters:

value (str)

Return type:

str

class SystemEditorAction(*values)[source]

Bases: StrEnum

Action codes on SystemEventControl.SYSTEM_EDITOR (_24) frames. The <node> slot carries the editor name (e.g. _sys_notify_short).

EDITOR_CHANGED

A system editor’s contents changed.

classmethod label(value)[source]

Friendly lower-case name, or the raw value.

Parameters:

value (str)

Return type:

str

class DeviceWriteAction(*values)[source]

Bases: StrEnum

Device-write sub-codes that ride through on _7 (SystemEventControl.PROGRESS) frames — PyISY 3.x surfaced these as NodeChangeAction.DEVICE_WRITING / DEVICE_MEMORY.

Unlike the other action enums, these are control-value sub-codes (they have the _ prefix and arrive in the <control> slot), not <action> values — the dispatcher doesn’t route them; they pass through as plain control events. <eventInfo> child tags per code are in DEVICE_WRITE_PROGRESS_EVENT_INFO_TAGS.

PROGRESS

Device-writing progress message — <eventInfo> carries <message>.

MEMORY

Raw Insteon memory write — <eventInfo> carries <memory> / <cmd1> / <cmd2> / <value>. hacs-udi-iox’s backlight entities subscribe to this to catch memory-write echoes.

classmethod label(value)[source]

Friendly lower-case name, or the raw value.

Parameters:

value (str)

Return type:

str

class NodeLifecycleAction(*values)[source]

Bases: StrEnum

Verbs the eisy emits via <control>_3</control> events — ISY994 Developer Cookbook §8.5.5 (“Node Changed/Updated”). PyISY 3.x keeps the same mapping. <eventInfo> child tags per verb are in NODE_LIFECYCLE_EVENT_INFO_TAGS.

EN carries an enabled boolean in <eventInfo> — there’s no separate “disabled” verb; the same code handles both transitions.

NODE_ADDED

Node added. <eventInfo> carries <nodeName> plus a <nodeType> that is itself the full <node> element — see NodeLifecycleEvent.node_xml.

NODE_REMOVED

Node removed (device deleted from the controller).

NODE_RENAMED

Node renamed (display name changed).

NODE_MOVED

Node moved into a Scene.

Link changed (within a scene). Not supported by the controller — kept for documentation; never observed.

NODE_REMOVED_FROM_GROUP

Node removed from a Scene.

PARENT_CHANGED

Parent (primary node) changed.

NODE_ENABLED

Node enabled/disabled — direction is in eventInfo.enabled.

POWER_INFO_CHANGED

Power-info changed — <eventInfo> carries <deviceClass> / <wattage> / <dcPeriod>.

DEVICE_ID_CHANGED

Device ID changed. Not implemented by the controller — kept for documentation.

DEVICE_PROPERTY_CHANGED

Device property changed — UPB only.

PENDING_DEVICE_OP

Pending device operation queued, awaiting commit. On Insteon a write (e.g. changing backlight level) surfaces WH first, then PROGRAMMING_DEVICE (WD) while the value is written; a property-update event arrives separately once it lands.

PROGRAMMING_DEVICE

The controller is carrying out a programming/write operation on this node (follows PENDING_DEVICE_OP). Cookbook name: “Programming Device”. Not a completion signal — watch the subsequent property-update event for the new value.

NODE_REVISED

Node revised — drastically changed (UPB-style); the consumer should discard cached info for the node and rebuild it. <eventInfo> carries the full <node> structure.

NODE_TYPE_INFO_CHANGED

Supported-type info changed — the node’s nodedef assignment was reassigned (e.g. a node server’s changeNode, or a device driver detecting new capabilities). The primary signal that a cached nodedef → entity mapping is stale. Not fired for /rest/profiles definition updates or moves, or at startup migration — those rewrite the profile DB without notifying.

ALL_NODES_ADDED

All nodes for a single device have been added (bulk). Fired after an include / re-pair so consumers can coalesce a single refresh per device instead of per child node.

Scene link updated — a link’s properties (on-level / ramp rate) changed for an existing scene member.

DISCOVERING_NODES

Discovering nodes (linking in progress). No node.

NODE_DISCOVERY_COMPLETE

Node discovery complete. No node.

NODE_ERROR

Node communication error (device unreachable).

NODE_ERROR_CLEARED

A previously-reported node communication error was cleared (cookbook: “Clear Node Error / Comm. Errors Cleared”) — the companion to NODE_ERROR.

FOLDER_ADDED

Folder added.

FOLDER_REMOVED

Folder removed.

FOLDER_RENAMED

Folder renamed — <eventInfo> carries <newName>.

GROUP_ADDED

Scene (group) added — <eventInfo> carries <groupName> / <groupType>.

GROUP_REMOVED

Scene (group) removed.

GROUP_RENAMED

Scene (group) renamed — <eventInfo> carries <newName>.

NET_RENAMED

A networking-module resource was renamed (node = the new name). Doesn’t affect the node registry.

classmethod label(value)[source]

Friendly lower-case name for a lifecycle verb, or the raw code if it isn’t one we know.

Parameters:

value (str)

Return type:

str

NODE_LIFECYCLE_EVENT_INFO_TAGS: dict[NodeLifecycleAction, tuple[str, ...]]

<eventInfo> child element names carried by each lifecycle verb (per the UDI notification table). An empty tuple means the frame carries only the node address. Reference metadata for consumers that want to parse the payload — pyisyox itself only parses the <node> element on NODE_ADDED (see NodeLifecycleEvent.node_xml).

DEVICE_WRITE_PROGRESS_EVENT_INFO_TAGS: dict[DeviceWriteAction, tuple[str, ...]]

<eventInfo> child tags carried by each DeviceWriteAction control code. The dispatcher doesn’t route these — reference metadata for consumers that subscribe to _7A / _7M control events directly.

describe_system_event(control, action)[source]

Render a <control> / <action> pair from a system event frame as a friendly "<control_label> = <action_label>" string.

Resolves both halves to their enum names where one applies:

  • "_5" / "0""system_status = not_busy"

  • "_1" / "0""trigger = program_status"

  • "_3" / "WH""node_lifecycle = pending_device_op"

  • "_4" / "5""system_config = batch_mode_updated"

  • "_8" / "AW""security_system = armed_away"

  • "_20" / "2""device_linker = cleared"

  • "_0" / "90""heartbeat = 90" (action = seconds to the next heartbeat; not enumerated)

  • "_28" / "1.3""matter_status = 1.3" (no enum)

  • "_26" / "2""system_upgrade = inactive"

  • "_24" / "1""system_editor = editor_changed"

  • "_99" / "x""_99 = x" (control we don’t recognise — both halves pass through verbatim)

Intended for the debug logging consumers do over raw event frames (so a line reads system_status = busy instead of system_status = 1); not part of any dispatch path. Property- update frames (non-underscore control) aren’t system events — this just echoes them back unchanged if you pass one.

Parameters:
Return type:

str

class NodeLifecycleEvent(action, node_address, raw_action, seqnum, node_xml=None, enabled=None)[source]

Bases: object

A high-level summary of a <control>_3</control> lifecycle frame.

Emitted alongside the raw Event whenever the dispatcher sees one of the actions in NodeLifecycleAction. Consumers subscribe via pyisyox.controller.Controller.add_node_lifecycle_listener() to drive their own reload UX (HA Core’s Repair issue, etc.).

Variables:
  • action (pyisyox.runtime.events.NodeLifecycleAction | str) – The lifecycle verb (typed enum). Unknown verbs come through as a plain string via raw_action.

  • node_address (str) – Wire address of the affected node. Empty string only for system-wide signals (none observed yet).

  • raw_action (str) – The string action value verbatim, in case a new verb appears that isn’t yet in NodeLifecycleAction.

  • seqnum (int) – Sequence number of the underlying Event.

  • node_xml (str | None) – For ND actions, the inner <node> element text from <eventInfo>. None for verbs that don’t include the full element. Consumers wanting the parsed shape can pass this to parse_lifecycle_node_xml().

  • enabled (bool | None) – For EN (NODE_ENABLED) actions, the new enabled/disabled state from <eventInfo><enabled> — the same value already written back to Node.enabled. None for every other verb (and for EN frames that omit the flag).

Parameters:
action: NodeLifecycleAction | str
node_address: str
raw_action: str
seqnum: int
node_xml: str | None
enabled: bool | None
property requires_reload: bool

True for verbs that invalidate the cached node/group/folder registry.

Reload-worthy: ND / NR / NN (node added/removed/renamed — the registry’s set or display names are stale), EN (enabled/disabled — the entity’s property shape may change), RV (revised — discard and rebuild this node), NI (supported-type info changed — the node’s nodedef assignment was reassigned, so the cached nodedef→entity mapping is stale; per UDI’s notification taxonomy this is the primary signal for profile-related node changes), AA (all-nodes-added bulk signal after a device include), RG (removed from scene — membership changed), SC (node-discovery complete — new nodes may have appeared), and the folder/scene tree verbs FD / FR / FN / GD / GR / GN (the groups / folders registries are stale).

Softer signals — informational, don’t trigger reload UX: MV (added to scene), CL (link changed — not supported), LU (scene link’s on-level/ramp updated — property change, not shape change), PC (parent changed), PI (power info), DI (device id — not implemented), DP (UPB property), WH (pending op), WD (programming device — a property-update event follows), SN (discovering nodes — wait for SC), CE / NE (comm error/cleared — no shape change), WR (a networking resource was renamed — doesn’t touch nodes).

class Event(seqnum, timestamp, control, action, node_address, formatted_action='', formatted_name='', uom='', precision=None, event_info='')[source]

Bases: object

One parsed event frame.

Variables:
  • seqnum (int) – Event sequence number from the eisy. Monotonic per connection; resets on reconnect.

  • timestamp (str) – ISO 8601 timestamp string from the frame (preserved verbatim — consumer parses if needed).

  • control (str) – Property id ("ST", "GV1", …) or system code ("_5", "_28", …).

  • action (str) – Raw value as reported (string form preserves the controller’s precision representation).

  • node_address (str) – Wire address of the affected node, or empty string for system events.

  • formatted_action (str) – Human-readable display value (e.g. "0.6839 US gallons"). Empty when the controller didn’t supply one (system events typically don’t).

  • formatted_name (str) – Display name of the property (e.g. "Current"). Empty when not provided.

  • uom (str) – Unit-of-measure id from <action uom="...">.

  • precision (int | None) – Decimal precision from <action prec="...">, or None if absent. (Wire keys it as "prec"; Python attribute spells it out.)

  • event_info (str) – Inner <eventInfo> XML preserved verbatim. Empty string when the frame had no <eventInfo> element or when its content was empty. Consumers that need the structured payload (e.g. variable change frames carrying <var type="..." id="...">, or controller logs in CDATA) parse this themselves — the IoX wire schema differs across system control codes and pyisyox stays neutral.

Parameters:
  • seqnum (int)

  • timestamp (str)

  • control (str)

  • action (str)

  • node_address (str)

  • formatted_action (str)

  • formatted_name (str)

  • uom (str)

  • precision (int | None)

  • event_info (str)

seqnum: int
timestamp: str
control: str
action: str
node_address: str
formatted_action: str
formatted_name: str
uom: str
precision: int | None
event_info: str
property is_system: bool

True for system control codes (_5, _28, …).

property is_node_property: bool

True when this event should overlay onto a node’s property dict.

parse_event_frame(raw)[source]

Decode a single WebSocket frame to an Event.

Accepts either:

  • Raw XML — <?xml...?><Event...>...</Event> (legacy /rest/subscribe).

  • JSON envelope — {"type": "event", "data": "<xml>"} (modern /api/events/subscribe). Other type values (e.g. "spolisy" PG3 service status) return None — they’re not property updates and the dispatcher ignores them.

Returns None for keep-alive nulls, malformed XML, or non-event JSON envelopes. Does not raise on parse failures so a single bad frame can’t crash the read loop.

Parameters:

raw (str)

Return type:

Event | None

class ProgramRunState(*values)[source]

Bases: IntEnum

Low-nibble of the <s> byte on a program-status frame.

Cookbook §8.5.3: exactly one of three run-clause states per frame, ORed with a ProgramEvalState (high nibble) in the byte. Absent (None on the event) when the high nibble is ProgramEvalState.NOT_LOADED — the program errored so there’s no clause currently running.

IDLE
THEN
ELSE
class ProgramEvalState(*values)[source]

Bases: IntEnum

High-nibble of the <s> byte on a program-status frame.

Cookbook §8.5.3: the program’s last if-clause evaluation result. The status: bool field on ProgramStatusEvent derives from the same source (the <on/>/<off/> element) but this enum disambiguates the three “not really True/False” cases that the bool collapses.

Note

NOT_LOADED is the cookbook’s literal label, but in practice the controller emits 0xF0 when the program failed to compile or hit a runtime error — not (only) when it hasn’t been loaded yet. Treat this as the program-error sentinel; see ProgramRunState for why run_state is None in this case.

UNKNOWN
TRUE
FALSE
NOT_LOADED
class ProgramStatusEvent(address, status, running, seqnum, run_state=None, eval_state=None, enabled=None, run_at_startup=None)[source]

Bases: object

A program toggled true/false on the controller.

Emitted by EventDispatcher whenever a <control>_1</control> frame with <action>0</action> arrives carrying a program id in its <eventInfo>. The matching pyisyox.client.ProgramRecord is mutated in place before listeners fire, so consumers reading program.status from a callback see the updated value.

Variables:
  • address (str) – Program id (4-character hex, zero-padded to match /api/programs).

  • status (bool) – True when the cookbook <s> byte’s eval state is ProgramEvalState.TRUE (the if-clause matched on the most recent evaluation); False for ProgramEvalState.FALSE. For ProgramEvalState.UNKNOWN / ProgramEvalState.NOT_LOADED (and frames with no <s> byte) the dispatcher carries forward the prior record.status so a transient unknown doesn’t flip the entity. Wire-shape note: the <on/> / <off/> elements that ride along on the same frame are the enabled flag, not the status — see enabled.

  • running (int | None) – Raw <s> byte the eisy sent, or None if absent. Cookbook §8.5.3: the byte is a bitwise OR of a ProgramRunState (low nibble) and a ProgramEvalState (high nibble); use run_state / eval_state for the typed view.

  • run_state (pyisyox.runtime.events.ProgramRunState | None) – Decoded low nibble — IDLE / THEN / ELSE, or None when the program isn’t loaded (eval_state == NOT_LOADED) or the wire byte was absent / unrecognised.

  • eval_state (pyisyox.runtime.events.ProgramEvalState | None) – Decoded high nibble — UNKNOWN / TRUE / FALSE / NOT_LOADED, or None when the wire byte was absent / unrecognised. Disambiguates the three “not really True/False” cases that status: bool collapses. NOT_LOADED is the cookbook label for what is in practice the program-errored sentinel — see ProgramEvalState.

  • enabled (bool | None) – New enabled state when the frame carried an <on/> / <off/> element — True for <on/>, False for <off/>. None when the frame omitted both (some “ran”-only frames carry only <r> / <f> / <s> — see cookbook §8.5.3). The matching record’s pyisyox.client.ProgramRecord.enabled is updated in-place before listeners fire when this is non-None.

  • run_at_startup (bool | None) – New run_at_startup state when the frame carried an <rr/> (True) or <nr/> (False) element. None when the frame omitted both. Mirror of the enabled-flag pattern; the record’s pyisyox.client.ProgramRecord.run_at_startup is updated in-place before listeners fire.

  • seqnum (int) – Sequence number of the underlying Event.

Parameters:
address: str
status: bool
running: int | None
seqnum: int
run_state: ProgramRunState | None
eval_state: ProgramEvalState | None
enabled: bool | None
run_at_startup: bool | None
class VariableTableChangeEvent(type_id, seqnum)[source]

Bases: object

A _1 / action "9" system event — variable table changed.

The eisy fires this when a variable is added, removed, or has its precision changed (a structural / metadata change that the per-value TriggerAction.VARIABLE_VALUE / VARIABLE_INIT frames don’t carry). <eventInfo> payload:

<var><type>N</type><id>0</id></var>

id=0 is the wildcard sentinel — “this whole type’s table changed”, not a specific variable. Consumers should re-fetch /api/variables/{type_id} to pick up the new metadata (precision in particular — the per-value frames carry raw val and a stale precision will mis-render every variable of this type until the registry is refreshed).

Parameters:
type_id: str

Variable type — "1" (integer) or "2" (state).

seqnum: int
class EventDispatcher(nodes, programs=None, variables=None, groups=None)[source]

Bases: object

Routes parsed Event instances into a node registry + listener callbacks.

The dispatcher is intentionally not coupled to the WebSocket transport — feed() accepts a raw frame and does the parse + route + emit dance. The actual WS read loop lives in pyisyox.runtime.ws; tests can drive the dispatcher directly with synthetic frames.

Parameters:
update_groups(groups)[source]

(Re)build the member→groups reverse index from a group registry.

Called from __init__ and again by pyisyox.controller.Controller.refresh()refresh() replaces LoadResult.groups with a fresh dict (unlike nodes, which is mutated in place), so the index has to be rebuilt or scene-membership changes from a reload lifecycle event would be missed (new members never re-emit; removed members still would).

Parameters:

groups (dict[str, GroupRecord])

Return type:

None

add_listener(callback)[source]

Register callback to fire on every parsed event.

Returns:

An unsubscribe function. Calling it removes callback from the listener list. Safe to call from inside a callback (the dispatcher iterates a snapshot).

Parameters:

callback (Callable[[Event], None])

Return type:

Callable[[], None]

add_program_status_listener(callback)[source]

Register callback to fire on every program-status frame (<control>_1</control> action "0").

The dispatcher updates the matching pyisyox.client.ProgramRecord in place before firing, so consumers reading program.status from the callback see the new value.

Returns:

An unsubscribe function.

Parameters:

callback (Callable[[ProgramStatusEvent], None])

Return type:

Callable[[], None]

add_variable_table_change_listener(callback)[source]

Register callback to fire on every variable-table-change frame (<control>_1</control> action "9").

Fired when a variable is added, removed, or has its precision changed on the controller. The dispatcher itself does not re-fetch the variable table — the listener is the seam where consumers wire in a focused re-fetch (e.g. by calling Controller.refresh()) so the registry mirrors the new metadata. See VariableTableChangeEvent for the payload.

Returns:

An unsubscribe function.

Parameters:

callback (Callable[[VariableTableChangeEvent], None])

Return type:

Callable[[], None]

add_lifecycle_listener(callback)[source]

Register callback to fire on every parsed NodeLifecycleEvent (<control>_3</control> frames).

Use this to drive reload UX: HA Core typically registers a Repair issue when it sees a lifecycle event with requires_reload=True, prompting the user to reload the integration when convenient. The dispatcher does not update the node registry on lifecycle events — consumers decide whether to call pyisyox.controller.Controller.refresh() or live with a stale view until manual reload.

Returns:

An unsubscribe function.

Parameters:

callback (Callable[[NodeLifecycleEvent], None])

Return type:

Callable[[], None]

feed(raw_frame)[source]

Parse one frame, apply the property update, fan out to listeners.

Returns the parsed Event for callers that want to peek (e.g. for sequence-number tracking), or None when the frame couldn’t be parsed (malformed XML, non-event envelope, keep-alive null). Never raises on bad input — a single bad frame must not crash the read loop.

Parameters:

raw_frame (str)

Return type:

Event | None