Multi-record clausal programs are now editable end-to-end. A chain
spans N consecutive slots — head (WHEN/AT/EVERY) + zero-or-more
AND/OR condition records + one-or-more THEN action records — so
"editing" means rewriting the whole run, validating that any
expansion doesn't trample adjacent programs, and clearing any old
slots when the chain shrinks.
H1 — backend:
* programs/get for chains now returns chain_members[] with each
member's slot + role + raw fields. The editor uses this to seed
one editable form-row per slot.
* New programs/chain/write command: takes head_slot + head dict +
conditions[] + actions[], does N sequential download_program
calls, then clears any old chain slots that fell outside the new
range. Validates:
- head_slot + new_len doesn't extend past slot 1500
- any expansion-into slot not already part of THIS chain is FREE
(anti-trample: refuse rather than overwrite an adjacent program)
- at least one THEN action present (empty chain rejected)
Updates coordinator.data.programs immediately so subsequent list
calls reflect the edit before the next poll.
H2 — TS helpers:
* AND-record encoding mirrors compact-form cond family bytes
(0x04 ZONE / 0x08 CTRL / 0x0C TIME / 0x00 OTHER + 0x10+ SEC) but
with a slightly different bit layout: the family byte lives at
fields.cond & 0xFF (disk byte 1) and the instance at
(fields.cond2 >> 8) & 0xFF (disk byte 3). The selector bit is
family's bit 0x02 instead of cond's 0x0200. decodeAndCondition /
encodeAndCondition handle both directions; round-trip exact.
* isStructuredAnd helper detects records with OP > 0 (TEMP > N
comparisons etc.); those render read-only in the chain editor
with a warning banner.
* emptyAndRecord / emptyOrRecord / emptyThenRecord helpers for
the add-condition / add-action buttons.
H3 — chain editor UI:
* New _chainDraft state (parallel to _editingDraft for compact form)
with head + conditions[] + actions[] arrays. Mutation helpers
preserve immutability via array-copy-then-patch.
* "Edit" button on chain detail now opens the chain editor instead
of returning early (previous read-only behaviour).
* Three sub-renderers: trigger section dispatches on prog_type
(WHEN→event-id builder reusing the EVENT helpers, AT→time+days
reusing TIMED layout, EVERY→single seconds input that packs into
cond+cond2), conditions section with per-row add/remove (separate
+ AND IF and + OR IF buttons in the legend), actions section with
per-row add/remove (+ THEN button; at least one action enforced).
* Structured-OP AND records render with an explanatory read-only
banner and a × button to drop the row entirely — preserves the
data when the user doesn't touch it, lets them remove it cleanly
when they want to.
* Each row picks objects via _bucketWithPreserve so out-of-range
zone/unit/area indices stay safe.
5 new HA integration tests:
* get-chain returns chain_members with correct roles + raw fields
* chain/write in-place rewrite preserves footprint, updates bytes
* chain/write shrink clears the trailing old slots
* chain/write refuses to trample an adjacent program on expansion
* chain/write rejects zero-actions submission
Live screenshot 11-chain-editor.png: state injection into the side
panel (real panel has no chains) shows the editor rendering a sample
WHEN zone-state → AND IF unit ON → 2x THEN action chain with every
control populated and functional.
Full suite: 653 passed, 1 skipped (up from 648, 5 new chain tests).
Frontend bundle: 82 KB minified (up from 63 KB).
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://github.com/rsp2k/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
pip install omni-pca
# Or with uv
uv add omni-pca
For Home Assistant users, install the integration through HACS — see the HA install how-to.
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.
Two wire dialects — TCP/v2 vs UDP/v1
The Omni network module is configurable at the panel keypad to listen on TCP, UDP, or both. Each transport speaks a different wire dialect — OmniClient above handles the TCP path (OmniLink2, the modern wire format used by PC Access ≥ 3); panels configured UDP-only fall back to the legacy v1 protocol with typed RequestZoneStatus / RequestUnitStatus opcodes, no RequestProperties, and streaming name downloads. For those, use OmniClientV1 from the omni_pca.v1 subpackage:
from omni_pca.v1 import OmniClientV1
async with OmniClientV1(
host="192.168.1.9",
controller_key=bytes.fromhex("..."),
) as panel:
info = await panel.get_system_information() # same dataclass as v2
names = await panel.list_all_names() # streaming UploadNames
zones = await panel.get_zone_status(1, 16) # typed status by range
await panel.execute_security_command(area=1, mode=SecurityMode.AWAY, code=1234)
The HA integration picks the right client automatically based on the Transport dropdown in the config flow (TCP vs UDP). See zone & unit numbering for why v1 panels need the long-form RequestUnitStatus for unit indices > 255.
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://github.com/rsp2k/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/.