The HA coordinator walks ObjectType.THERMOSTAT (6) and ObjectType.BUTTON (3) via raw RequestProperties to discover them — the high-level get_object_properties() path only knows zones/units/areas in v1.0. The mock was returning Nak for both, which made HA discover zero thermostats and zero buttons no matter how MockState was seeded. src/omni_pca/mock_panel.py: - New MockButtonState dataclass (just a name) - MockState gains buttons: dict[int, MockButtonState] (with the same bare-string -> dataclass __post_init__ promotion as the others) - _OBJ_BUTTON = 3, _BUTTON_NAME_LEN = 12, _THERMOSTAT_NAME_LEN = 12 constants - thermostat_name_bytes() / button_name_bytes() helpers - _build_thermostat_properties() emits the 23-byte Properties body matching ThermostatProperties.parse offsets (object number BE u16, communicating flag, current temp, heat/cool setpoints, system/fan/ hold modes, thermostat type, 12-byte NUL-padded name) - _build_button_properties() emits the 15-byte body (object number BE u16 + 12-byte name) - _reply_properties / _object_store dispatch both new types tests/test_e2e_client_mock.py — two new e2e tests drive raw RequestProperties walks for thermostats and buttons against a seeded mock and assert ThermostatProperties / ButtonProperties parse cleanly, mirroring what the HA coordinator's _walk_properties() does. 333 tests pass (was 331); ruff clean. Mock surface now matches every opcode the HA coordinator and entity platforms actually call.
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:
- 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.
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
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 entity table and service list.
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.