omni-pca/src/omni_pca/v1/__init__.py
Ryan Malloy 30b482a8cb HA integration: wire v1+UDP into the coordinator + config flow
OmniClientV1Adapter (src/omni_pca/v1/adapter.py)
  V2-shape facade over OmniClientV1. Exposes the OmniClient surface the
  HA coordinator was written against — get_system_information,
  list_*_names, get_object_properties (synthesized from streamed names),
  get_extended_status (chunked, routed to v1 typed status opcodes),
  get_object_status(AREA, ...) (derived from SystemStatus.area_alarms),
  events() (EventStream on v1 SystemEvents opcode 35), plus all the
  write-method shims.

  Chunks unit/zone/thermostat/aux polls per-type because firmware 2.12
  NAKs Request*Status with >~62 records in one shot (verified live).
  Falls back to "Area 1".."Area 8" when the UploadNames stream returns
  zero areas — common on panels where the installer didn't name them.

custom_components/omni_pca/coordinator.py
  _ensure_connected picks OmniClientV1Adapter for transport=udp. New
  _walk_properties_v1 replaces the v2 RequestProperties walk with a
  name-stream + synthesized-Properties pass.

custom_components/omni_pca/config_flow.py
  _probe routes to OmniClientV1Adapter for transport=udp instead of
  trying to drive v2 OmniClient over UDP (which silently dropped after
  handshake, per the earlier diagnosis).

src/omni_pca/events.py
  parse_events / _ensure_system_events / EventStream now take an
  expected_opcode arg (default v2 SystemEvents=55, v1 callers pass 35).
  Word format is byte-identical between v1 and v2, so the typed-event
  decoder is unchanged.

src/omni_pca/v1/client.py
  _range_status supports the long-form RequestUnitStatus (BE u16
  start/end) so panels with unit indices > 255 (sprinklers, flags) work.

Verified end-to-end against firmware 2.12 panel at 192.168.1.9:
  config entries:
    state=loaded  Omni Pro II (host.docker.internal)  (mock)
    state=loaded  Omni Pro II (192.168.1.9)           (real, v1+UDP)
  real-panel entities created in HA: 96 (30 binary_sensor, 26 light,
  15 switch, 13 button, 9 sensor, 3 climate)
  cross-check: light.omni_pro_ii_front_porch_2 = on  (matches live
  probe: unit #2 'FRONT PORCH' state=0x01 brightness=100)

dev/probe_v1_coordinator.py
  Coordinator-shaped end-to-end smoke test against the real panel
  without HA — drives the full discovery + poll cycle through the
  adapter. Useful for regression-checking the v1 wire path.

dev/add_real_panel.py
  Programmatically adds the real-panel config entry to the dev HA
  stack via the REST config-flow endpoints. Idempotent.
2026-05-11 01:30:49 -06:00

53 lines
1.5 KiB
Python

"""V1 (legacy) Omni-Link protocol over UDP.
The v2 path in :mod:`omni_pca` (TCP, OmniLink2Message, StartChar 0x21,
parameterised RequestProperties / RequestExtendedStatus) is what most
modern firmware speaks. This subpackage exists because some panels are
configured at the network module to listen on **UDP only**, in which case
PC Access falls back to the v1 wire protocol (typed RequestZoneStatus,
RequestUnitStatus, etc., StartChar 0x5A, OmniLinkMessage outer = 0x10).
Reference: clsOmniLinkConnection.cs:353-360 (ConnectionProtocol() returns
V1 for Modem/UDP/Serial, V2 only for TCP).
"""
from __future__ import annotations
from .adapter import OmniClientV1Adapter
from .client import OmniClientV1, OmniNakError, OmniProtocolError
from .connection import (
HandshakeError,
InvalidEncryptionKeyError,
OmniConnectionV1,
RequestTimeoutError,
)
from .messages import (
NameRecord,
NameType,
parse_v1_aux_status,
parse_v1_namedata,
parse_v1_system_status,
parse_v1_thermostat_status,
parse_v1_unit_status,
parse_v1_zone_status,
)
__all__ = [
"HandshakeError",
"InvalidEncryptionKeyError",
"NameRecord",
"NameType",
"OmniClientV1",
"OmniClientV1Adapter",
"OmniConnectionV1",
"OmniNakError",
"OmniProtocolError",
"RequestTimeoutError",
"parse_v1_aux_status",
"parse_v1_namedata",
"parse_v1_system_status",
"parse_v1_thermostat_status",
"parse_v1_unit_status",
"parse_v1_zone_status",
]