Ryan Malloy 9cdb312baf
Some checks are pending
Validate / HACS validation (push) Waiting to run
Validate / Hassfest (push) Waiting to run
program writeback: DownloadProgram wire + HA write API + Clear/Clone UI
The program viewer goes from read-only to write-capable. Three layers
land together because a partial implementation isn't actionable.

D1 — wire path:

* OmniClient.download_program(slot, program) — sends opcode 8
  (clsOLMsg2DownloadProgram, clsHAC.cs:1133-1140) with the 2-byte BE
  slot + Program.encode_wire_bytes(). Validates slot range 1..1500
  client-side. Maps Ack → success, Nak → CommandFailedError, any
  other opcode → OmniConnectionError.
* OmniClient.clear_program(slot) — convenience that writes an all-zero
  body. Mock treats this as deletion (removes the slot from
  state.programs) so subsequent reads see it as undefined.
* MockPanel handles DownloadProgram on the v2 dispatch path —
  receive 2-byte slot + 14-byte body, store in state.programs, ack.
* OmniClientV1.download_program raises NotImplementedError. v1 only
  has the bulk DownloadPrograms flow which clears everything before
  rewriting — destructive for HA's edit-one-program use case.
  Documented in the docstring so callers know to route v1 users to
  a v2 connection.

Tests cover: write-then-read round-trip, overwrite of existing slot,
clear deletes the slot, range validation, v1 not-implemented.

D2 — HA websocket commands:

* omni_pca/programs/clear — writes zero body, updates coordinator.
  data.programs immediately so the next list call shows the deletion.
  Returns ``{slot, cleared: true}``. Maps NotImplementedError on v1
  panels to the ``not_supported`` error code.
* omni_pca/programs/clone — copies source_slot → target_slot, with
  the slot field re-stamped. Refuses identical source/target,
  refuses missing source. Same coordinator update pattern.

5 new HA-integration tests covering clear, clone happy path, clone
to same slot, clone from missing source.

D3 — Clear/Clone UI in the side panel:

* "Clone…" button reveals an inline target-slot input (number,
  1..1500). Enter or "Clone" button calls the WS command, then
  navigates the detail panel to the new clone so the user sees the
  result.
* "Clear" button shows an inline confirmation row ("Clear slot N?
  This deletes the program from the panel.") with Yes/Cancel. Yes
  closes the detail panel and refreshes the list — the slot is gone.
* Both surface feedback via the same _writeFeedback state used by
  Fire now (auto-clears after 4 seconds).
* Three new button styles (.primary, .secondary, .danger) and the
  .action-row composite used for both inline prompts.

What's NOT shipped here: a real visual editor for trigger/condition/
action fields. That's a follow-up (~600 lines of new TS + careful
validation work). The current "Cut 1" UX is enough for the common
"I accidentally created a program, clear it" and "I want a variant
of this program, give me a copy in an empty slot" workflows.

Full suite: 643 passed, 1 skipped (up from 634).
Frontend bundle: 38 KB minified (up from 34 KB with the write UI).
2026-05-16 01:14:54 -06:00
2026-05-14 03:12:06 -06:00
2026-05-10 12:46:26 -06:00
2026-05-14 03:12:06 -06:00

omni-pca

Async Python client for HAI/Leviton Omni-Link II home automation panels — Omni Pro II, Omni IIe, Omni LTe, Lumina.

Includes a Home Assistant custom component (custom_components/omni_pca/).

Project home: https://github.com/rsp2k/omni-pca Documentation: https://hai-omni-pro-ii.warehack.ing/

Status

Alpha. Built from a full reverse-engineering of HAI's PC Access 3.17 (the Windows installer/programmer app). The protocol layer captures two non-public quirks that public Omni-Link clients miss:

  1. Session key is not the ControllerKey. Last 5 bytes are XORed with a controller-supplied SessionID nonce.
  2. Per-block XOR pre-whitening before AES. First two bytes of every 16-byte block are XORed with the packet's sequence number.

The full byte-level protocol spec lives at https://hai-omni-pro-ii.warehack.ing/reference/protocol/.

Install

pip install omni-pca

# Or with uv
uv add omni-pca

For Home Assistant users, install the integration through HACS — see the HA install how-to.

Quick start (library)

import asyncio
from omni_pca import OmniClient

async def main():
    async with OmniClient(
        host="192.168.1.9",
        port=4369,
        controller_key=bytes.fromhex("6ba7b4e9b4656de3cd7edd4c650cdb09"),
    ) as panel:
        info = await panel.get_system_information()
        print(info.model_name, info.firmware_version)

asyncio.run(main())

For the panel walkthrough — connect, list zones, react to push events — see the tutorial.

Two wire dialects — TCP/v2 vs UDP/v1

The Omni network module is configurable at the panel keypad to listen on TCP, UDP, or both. Each transport speaks a different wire dialect — OmniClient above handles the TCP path (OmniLink2, the modern wire format used by PC Access ≥ 3); panels configured UDP-only fall back to the legacy v1 protocol with typed RequestZoneStatus / RequestUnitStatus opcodes, no RequestProperties, and streaming name downloads. For those, use OmniClientV1 from the omni_pca.v1 subpackage:

from omni_pca.v1 import OmniClientV1

async with OmniClientV1(
    host="192.168.1.9",
    controller_key=bytes.fromhex("..."),
) as panel:
    info = await panel.get_system_information()      # same dataclass as v2
    names = await panel.list_all_names()             # streaming UploadNames
    zones = await panel.get_zone_status(1, 16)       # typed status by range
    await panel.execute_security_command(area=1, mode=SecurityMode.AWAY, code=1234)

The HA integration picks the right client automatically based on the Transport dropdown in the config flow (TCP vs UDP). See zone & unit numbering for why v1 panels need the long-form RequestUnitStatus for unit indices > 255.

Quick start (Home Assistant)

# Manual install — works on every HA flavour
cd /path/to/your/homeassistant/config/
mkdir -p custom_components
cd custom_components
git clone https://github.com/rsp2k/omni-pca tmp-omni
cp -r tmp-omni/custom_components/omni_pca .
rm -rf tmp-omni

Restart HA, then add the integration via Settings → Devices & Services. You'll need:

  • Panel IP / hostname
  • TCP port (default 4369)
  • ControllerKey as 32 hex chars

Get the ControllerKey from your .pca file using the bundled CLI:

omni-pca decode-pca '/path/to/Your.pca' --field controller_key

The integration creates one HA device per panel plus typed entities for every named object on the controller: alarm_control_panel for areas, light for units, binary_sensor + switch for zones (state + bypass), climate for thermostats, sensor for analog zones and panel telemetry, button for panel macros, and event for the typed push-notification stream. See custom_components/omni_pca/README.md for the full entity + service catalog, or the HA install how-to for the step-by-step.

Without a panel — mock controller

The library ships a stateful MockPanel that emulates the controller side of the protocol over real TCP. Useful for offline development, integration tests, and demos:

from omni_pca.mock_panel import MockPanel

async with MockPanel(controller_key=...).serve(port=14369):
    # Connect a real OmniClient to localhost:14369 — full handshake + AES
    ...

The local dev stack (dev/docker-compose.yml) packages a real Home Assistant container and the mock panel side-by-side so you can click through the integration without touching real hardware. See the dev-stack tutorial.

Tests

uv sync --group ha
uv run pytest -q

351 tests across the protocol primitives, the mock panel, the OmniClient ↔ MockPanel end-to-end roundtrip, and an in-process Home Assistant harness driving the integration via the real config flow + service calls.

Versioning

Date-based (CalVer): YYYY.M.D. Bumped on backwards-incompatible changes. See CHANGELOG.md.

License

MIT. See LICENSE.

Acknowledgments

This client is independent and not affiliated with Leviton or HAI. Protocol details derived from clean-room analysis of the publicly-distributed PC Access installer. The reverse-engineering arc is documented at https://hai-omni-pro-ii.warehack.ing/journey/.

Description
Async Python library and Home Assistant integration for HAI/Leviton Omni Pro II / Omni IIe / Omni LTe / Lumina panels. Reverse-engineered from PC Access 3.17.
Readme MIT 10 MiB
2026-05-11 19:40:34 +00:00
Languages
Python 89.9%
TypeScript 9.9%
JavaScript 0.1%