Ryan Malloy 9ca4da98e8
Some checks are pending
Validate / HACS validation (push) Waiting to run
Validate / Hassfest (push) Waiting to run
panel: clausal chain editor (WHEN/AT/EVERY + AND/OR/THEN)
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).
2026-05-17 02:09:04 -06:00
..

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:

  1. Settings → Devices & Services → Add Integration
  2. Search for HAI/Leviton Omni Panel
  3. Fill in:
    • host: host.docker.internal
    • port: 14369
    • controller key: 000102030405060708090a0b0c0d0e0f
  4. 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:14369 inside the container. If you prefer to talk to it from the host directly (e.g. with omni-pca CLI), use make dev-mock to run it natively.