Ryan Malloy 92c8b695b4 v1-over-UDP: parallel OmniClientV1 for panels that listen UDP-only
Some Omni network modules are configured for UDP, in which case PC Access
falls back to the v1 wire protocol (OmniLinkMessage outer = 0x10, inner
StartChar 0x5A, typed Request*Status opcodes) instead of v2's TCP path
(OmniLink2Message + StartChar 0x21 + parameterised RequestProperties).
This adds a parallel implementation rather than overloading the v2 path.

omni_pca/v1/
  connection.py   UDP-only OmniConnectionV1; reuses crypto + handshake,
                  routes post-handshake messages through OmniLinkMessage
                  (0x10) wrapping v1 inner format. Adds iter_streaming
                  for the lock-step UploadNames/Acknowledge/EOD pattern.
  messages.py     Block parsers for the typed v1 status replies (zone,
                  unit, thermostat, aux), v1 SystemStatus, and NameData
                  (handles both one-byte and two-byte NameNumber forms).
  client.py       OmniClientV1: read API (get_system_information,
                  get_*_status), discovery (iter_names + list_*_names),
                  write API (execute_command, execute_security_command,
                  turn_unit_*, set_unit_level, bypass/restore_zone,
                  execute_button, set_thermostat_*). acknowledge_alerts
                  is a no-op (v1 has no equivalent opcode).

Discovery uses bare UploadNames; panel streams every defined name across
all types in a fixed order with per-record Acknowledge. Verified against
firmware 2.12 — pulled 16 zones, 44 units, 16 buttons, 8 codes,
2 thermostats, 8 messages in one stream.

src/omni_pca/message.py
  Fix flipped START_CHAR_V1_* constants. enuOmniLinkMessageFormat says
  Addressable=0x41 and NonAddressable=0x5A; our names had them swapped.
  Wire bytes were unchanged, so existing tests kept passing — but
  encode_v1() with no serial_address now correctly emits 0x5A, which
  is what UDP needs.

tests/
  test_v1_messages.py        22 cases; payloads are real wire captures
                              from a firmware-2.12 panel via probe_v1_recon.
  test_v1_client_commands.py 20 cases; payload-packing for the Command
                              and ExecuteSecurityCommand opcodes,
                              including BE u16 parameter2 and the
                              digit-by-digit security code form.

dev/
  probe_v1.py        Phase-1 smoke: handshake + RequestSystemInformation.
  probe_v1_recon.py  Raw opcode dump for protocol reconnaissance.
  probe_v1_stream.py Streaming UploadNames flow exploration.
  probe_v1_client.py Full read-path smoke test via OmniClientV1.
  probe_v1_write.py  Live no-op execute_command round-trip.

.gitignore: ignore dev/.omni_key (probe scripts read controller key from
this file as one fallback option).

Discovery on firmware 2.12: Request*ExtendedStatus opcodes (63/65/69)
NAK on this firmware — only the basic Request*Status opcodes are
implemented, so OmniClientV1 uses those (3 bytes/unit, 7 bytes/tstat,
4 bytes/aux records). HA still gets enough signal for polling; full
properties discovery uses streaming UploadNames instead.

Test totals: 387 passed, 1 skipped (existing fixture skip).
2026-05-11 01:08:01 -06:00
..

Dev stack

Local Home Assistant + MockPanel for clicking around the integration without a real Omni controller. Useful for screenshots, manual smoke tests, and seeing what the entity layout looks like.

Quick start

cd dev/
make dev-up         # docker compose up -d
# wait ~30s for HA to boot
open http://localhost:8123

First time: HA onboarding wizard (any name / location works). Then:

  1. Settings → Devices & Services → Add Integration
  2. Search for HAI/Leviton Omni Panel
  3. Fill in:
    • host: host.docker.internal
    • port: 14369
    • controller key: 000102030405060708090a0b0c0d0e0f
  4. Submit. Within a few seconds you should see the Omni Pro II device with ~25 entities (binary sensors, lights, alarm panel, climate, sensors, buttons, switches, the events entity).

What the mock simulates

Five named zones, four units, two areas, two thermostats, three button macros. User codes 1234 (master, code index 1) and 5678 (code index 2).

Arming the alarm with code 1234 will succeed and the alarm_control_panel entity transitions through ARMING → ARMED_AWAY in real time via the panel's push-event simulation. Wrong code → HA error toast, panel stays disarmed.

Other targets

make dev-logs       # tail HA + mock logs
make dev-mock       # run only the mock on the host (no docker)
make dev-down       # stop the stack
make dev-reset      # wipe HA config and start fresh

Notes

  • The HA container mounts ../custom_components/omni_pca/ read-only, so edits to the integration need a restart (docker compose restart homeassistant) to take effect.
  • The mock panel binds 0.0.0.0:14369 inside the container. If you prefer to talk to it from the host directly (e.g. with omni-pca CLI), use make dev-mock to run it natively.