The program viewer goes from read-only to write-capable. Three layers
land together because a partial implementation isn't actionable.
D1 — wire path:
* OmniClient.download_program(slot, program) — sends opcode 8
(clsOLMsg2DownloadProgram, clsHAC.cs:1133-1140) with the 2-byte BE
slot + Program.encode_wire_bytes(). Validates slot range 1..1500
client-side. Maps Ack → success, Nak → CommandFailedError, any
other opcode → OmniConnectionError.
* OmniClient.clear_program(slot) — convenience that writes an all-zero
body. Mock treats this as deletion (removes the slot from
state.programs) so subsequent reads see it as undefined.
* MockPanel handles DownloadProgram on the v2 dispatch path —
receive 2-byte slot + 14-byte body, store in state.programs, ack.
* OmniClientV1.download_program raises NotImplementedError. v1 only
has the bulk DownloadPrograms flow which clears everything before
rewriting — destructive for HA's edit-one-program use case.
Documented in the docstring so callers know to route v1 users to
a v2 connection.
Tests cover: write-then-read round-trip, overwrite of existing slot,
clear deletes the slot, range validation, v1 not-implemented.
D2 — HA websocket commands:
* omni_pca/programs/clear — writes zero body, updates coordinator.
data.programs immediately so the next list call shows the deletion.
Returns ``{slot, cleared: true}``. Maps NotImplementedError on v1
panels to the ``not_supported`` error code.
* omni_pca/programs/clone — copies source_slot → target_slot, with
the slot field re-stamped. Refuses identical source/target,
refuses missing source. Same coordinator update pattern.
5 new HA-integration tests covering clear, clone happy path, clone
to same slot, clone from missing source.
D3 — Clear/Clone UI in the side panel:
* "Clone…" button reveals an inline target-slot input (number,
1..1500). Enter or "Clone" button calls the WS command, then
navigates the detail panel to the new clone so the user sees the
result.
* "Clear" button shows an inline confirmation row ("Clear slot N?
This deletes the program from the panel.") with Yes/Cancel. Yes
closes the detail panel and refreshes the list — the slot is gone.
* Both surface feedback via the same _writeFeedback state used by
Fire now (auto-clears after 4 seconds).
* Three new button styles (.primary, .secondary, .danger) and the
.action-row composite used for both inline prompts.
What's NOT shipped here: a real visual editor for trigger/condition/
action fields. That's a follow-up (~600 lines of new TS + careful
validation work). The current "Cut 1" UX is enough for the common
"I accidentally created a program, clear it" and "I want a variant
of this program, give me a copy in an empty slot" workflows.
Full suite: 643 passed, 1 skipped (up from 634).
Frontend bundle: 38 KB minified (up from 34 KB with the write UI).
Phase B of the program viewer. Three websocket commands and a stub
side-panel registration wire the HA integration to consume the
program_renderer library.
Websocket commands (all namespaced ``omni_pca/programs/``):
* ``list`` — paginated, filterable summaries. Filters: trigger_types
(TIMED / EVENT / YEARLY / WHEN / AT / EVERY), references_entity
(e.g. ``"unit:7"``), case-insensitive substring search. Each row
carries summary tokens + a flat ``references`` list for filter UI.
* ``get`` — full structured-English detail for a slot. Clausal
chains return as one logical unit even when the user clicked an
interior slot.
* ``fire`` — sends ``Command.EXECUTE_PROGRAM`` over the wire so the
panel runs the program now. Returns ``{slot, fired: true}`` on
success or a structured error.
Token serialisation uses short keys (k/t/ek/ei/s) for compact wire
format — the panel's 1500-slot table on a busy install fits in a few
hundred KB of JSON.
Coordinator-backed resolvers:
* ``_CoordinatorNameResolver`` — pulls names from data.zones / units /
areas / thermostats / buttons (HA-side ZoneProperties etc.)
* ``_CoordinatorStateResolver`` — pulls live state from *_status maps
so every websocket call sees the freshest available overlay without
round-tripping the panel. SECURE / NOT READY / BYPASSED for zones,
OFF / ON / ON 60% for units, Day / Night / Away for areas,
°F for thermostats.
Side-panel registration: ``async_register_side_panel`` registers a
custom panel under ``Omni Programs`` in HA's sidebar with a
``mdi:script-text-outline`` icon. Bundle is served at
``/api/omni_pca/panel.js`` via a static-path registration. A
working stub panel.js ships now so the wiring is exercisable;
Phase C will drop the real Lit/TS bundle into the same path.
Panel registration is wrapped in a try/except + a once-per-HA-boot
guard so test environments without ``hass_frontend`` installed don't
break the rest of the integration. The manifest only lists ``http``
and ``websocket_api`` as hard dependencies for the same reason —
panel_custom is opportunistic.
10 new HA-integration tests cover list/get/fire end-to-end plus
filters, pagination, search, live-state overlay, and structured-error
returns for bad entry_id / missing slot.
Full suite: 634 passed, 1 skipped (up from 624).