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.
71 lines
2.2 KiB
Markdown
71 lines
2.2 KiB
Markdown
# 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`](docs/PROTOCOL.md) for the full byte-level spec.
|
|
|
|
## Quick start (library)
|
|
|
|
```bash
|
|
uv add omni-pca
|
|
```
|
|
|
|
```python
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```python
|
|
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](https://calver.org/)): `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.
|