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).
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://git.supported.systems/warehack.ing/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:
- Session key is not the ControllerKey. Last 5 bytes are XORed with a controller-supplied SessionID nonce.
- 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
The library isn't on PyPI yet (pending), so install directly from the Gitea release:
# Pinned to a specific release (recommended)
pip install "omni-pca @ git+https://git.supported.systems/warehack.ing/omni-pca.git@v2026.5.10"
# Or the wheel from the release page
pip install https://git.supported.systems/warehack.ing/omni-pca/releases/download/v2026.5.10/omni_pca-2026.5.10-py3-none-any.whl
# Or with uv
uv add "omni-pca @ git+https://git.supported.systems/warehack.ing/omni-pca.git@v2026.5.10"
Once published to PyPI, the canonical install will be pip install omni-pca.
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.
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://git.supported.systems/warehack.ing/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/.