---
title: HAI Omni Pro II — omni-pca
description: Reverse-engineered Python library and Home Assistant integration for HAI/Leviton Omni Pro II home automation panels.
template: doc
hero:
tagline: |
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.
actions:
- text: Add to Home Assistant
link: /how-to/install-in-home-assistant/
icon: right-arrow
variant: primary
- text: Decode your .pca file
link: /tutorials/decrypt-your-pca/
icon: external
variant: secondary
- text: Source on Gitea
link: https://git.supported.systems/warehack.ing/omni-pca
icon: external
variant: minimal
---
import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components';
import { Image } from 'astro:assets';
import wordmark from '../../assets/manual/omnipro-ii-wordmark.png';
import shotIntegrations from '../../assets/screenshots/02-integrations-list.png';
import shotConfig from '../../assets/screenshots/03-omni-pca-config.png';
import shotDevice from '../../assets/screenshots/04-panel-device.png';
import shotStates from '../../assets/screenshots/06-developer-states.png';
## In Home Assistant
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.
## In Python
The library underneath is intentionally async-first and typed end-to-end:
```python
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-first** — `OmniClient` is an async context manager, `events()` is
an async iterator, no callback soup.
## Two non-public protocol quirks
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](/explanation/quirks/) for why they exist and the
full visual breakdown.
## Read the in-depth content