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).
Dev stack
Local Home Assistant + MockPanel for clicking around the integration without a real Omni controller. Useful for screenshots, manual smoke tests, and seeing what the entity layout looks like.
Quick start
cd dev/
make dev-up # docker compose up -d
# wait ~30s for HA to boot
open http://localhost:8123
First time: HA onboarding wizard (any name / location works). Then:
- Settings → Devices & Services → Add Integration
- Search for HAI/Leviton Omni Panel
- Fill in:
- host:
host.docker.internal - port:
14369 - controller key:
000102030405060708090a0b0c0d0e0f
- host:
- Submit. Within a few seconds you should see the Omni Pro II device with ~25 entities (binary sensors, lights, alarm panel, climate, sensors, buttons, switches, the events entity).
What the mock simulates
Five named zones, four units, two areas, two thermostats, three button
macros. User codes 1234 (master, code index 1) and 5678 (code index 2).
Arming the alarm with code 1234 will succeed and the
alarm_control_panel entity transitions through ARMING → ARMED_AWAY in
real time via the panel's push-event simulation. Wrong code → HA error
toast, panel stays disarmed.
Other targets
make dev-logs # tail HA + mock logs
make dev-mock # run only the mock on the host (no docker)
make dev-down # stop the stack
make dev-reset # wipe HA config and start fresh
Notes
- The HA container mounts
../custom_components/omni_pca/read-only, so edits to the integration need a restart (docker compose restart homeassistant) to take effect. - The mock panel binds
0.0.0.0:14369inside the container. If you prefer to talk to it from the host directly (e.g. withomni-pcaCLI), usemake dev-mockto run it natively.