omni-pca/README.md
Ryan Malloy 9a024181ae Initial scaffold + protocol primitives
Project scaffold (uv, pyproject.toml CalVer 2026.5.10, ruff, pytest, mypy
strict config, MIT, README, .gitignore protecting any .pca / panel keys).

Library primitives (src/omni_pca/):
- crypto.py     AES-128-ECB + per-block XOR seq pre-whitening, session-key
                derivation (CK[0:11] || (CK[11:16] XOR SessionID))
- opcodes.py    Byte-exact PacketType (12), v1 MessageType (104),
                v2 MessageType (83), ConnectionType, ProtocolVersion
- packet.py     Outer Packet dataclass with encode/decode
- message.py    Inner Message + CRC-16/MODBUS, helpers for v1/v2
- pca_file.py   Borland LCG XOR cipher, PcaReader, .pca + .CFG parsers
                (KEY_PC01 = 0x14326573, KEY_EXPORT = 0x17569237 — fixed
                from initial typo; verified via test_keys_match_decompiled)
- __main__.py   CLI: 'omni-pca decode-pca <file> --field {host,port,...}'
                PII opt-in via --include-pii

49 tests pass, 1 skipped (live fixture). Ruff clean.
2026-05-10 12:46:26 -06:00

2.2 KiB

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/).

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.

See docs/PROTOCOL.md for the full byte-level spec.

Quick start (library)

uv add omni-pca
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_info()
        print(info.model_name, info.firmware_version)

asyncio.run(main())

Quick start (Home Assistant)

Copy custom_components/omni_pca/ into your HA config/custom_components/, 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 included parser:

uvx --from omni-pca omni-pca decode-pca path/to/Your.pca --field controller_key

Without a panel — mock controller

For testing, the library ships a minimal Omni controller emulator:

from omni_pca.mock_panel import MockPanel

async with MockPanel(controller_key=...).serve(port=14369):
    # connect a real OmniClient to localhost:14369 — works end-to-end
    ...

Versioning

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

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.