Skip to content

HAI Omni Pro II — omni-pca

Async Python library and a drop-in Home Assistant integration for the HAI/Leviton Omni-Link II protocol — clean-room reverse-engineered from PC Access 3.17, complete with two non-public crypto quirks no other public client implements.
OmniPro II Automation

One device per panel. Typed entities for every named object the controller knows about — alarm areas, lights and outputs, binary zones with bypass, thermostats with HVAC modes, programs and panel-button macros, plus a single event entity that relays the panel’s typed push-event stream into HA automations. Push updates arrive within one TCP round-trip; a 30-second poll backstops anything that didn’t push.

The library underneath is intentionally async-first and typed end-to-end:

import asyncio
from omni_pca import OmniClient
async def main() -> None:
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)
async for event in panel.events():
print(event) # ZoneStateChanged, ArmingChanged, AlarmActivated, …
asyncio.run(main())

What you get from the library:

  • Full opcode coverage — 104 v1 + 83 v2 message types, byte-exact to the decompiled C# enums.
  • 21 typed status/properties dataclasses, 26 typed SystemEvent subclasses, no untyped bytes leaking past the framing layer.
  • Stateful mock controller for offline development. The same MockPanel class powers the integration tests and the docker dev stack.
  • Async-firstOmniClient is an async context manager, events() is an async iterator, no callback soup.

The wire protocol — as actually implemented in PC Access 3.17 — has two quirks that public Omni-Link clients miss. Without them the panel will accept your TCP connection, complete the unencrypted handshake, and then silently drop you on the first encrypted message:

  1. Session key XOR mix. The AES-128 session key is not the panel’s ControllerKey directly. Bytes [11..16) of the ControllerKey are XORed with a 5-byte SessionID nonce that the controller sends in ControllerAckNewSession. Bytes [0..11) are the ControllerKey verbatim.
  2. Per-block XOR pre-whitening before AES. Before each 16-byte block is AES-encrypted, its first two bytes are XORed with the packet’s 16-bit sequence number (high byte first). The same mask is applied to every block of the packet. Decrypt reverses it.

Both are unambiguous in the decompiled C# (clsOmniLinkConnection.cs:1886-1892 and :396-401). Neither appears in jomnilinkII, pyomnilink, or any third-party Omni-Link writeup we found. See the quirks explainer for why they exist and the full visual breakdown.