Real-world programs reference object indexes well past the coordinator's discovery range — typical example from a live OmniPro II: a program that's "Turn ON Unit 33025" where the unit number is a raw byte value from undecoded extended-output addressing. The discovered units bucket only covers slots up to ~511, so 33025 doesn't match any entry. Before this commit the dropdown silently fell through to the first known unit (e.g. ROOM ONE), making it look like the user had selected that unit. The underlying draft.pr2 stayed at 33025, but a user who glanced at the form and clicked Save would either preserve the original (if they didn't touch the select) or accidentally clobber it with the first list item (if they did). Fix: _bucketWithPreserve prepends a synthesized option "(undiscovered <kind> <idx> — preserve original)" when the current value isn't represented. Applies in all four picker sites: * Action object picker (Unit / Zone / Area / Button for action commands) * EVENT trigger Button picker * EVENT trigger Zone picker * EVENT trigger Unit picker The synthesized entry sits at the top of the list (visually distinct) and is the selected default. Picking any other entry from the dropdown then becomes an explicit choice — no more silent coercion. Smoke-tested against the real panel: slot #1 (WHEN OPEN BIG GAR → Turn ON Unit 33025) now shows "#33025 (undiscovered unit 33025 — preserve original)" as the selected Unit option. Screenshots updated.
Omni Programs side panel — frontend
Lit/TypeScript source for the HA side panel registered by
websocket.py:async_register_side_panel. The build output
(../www/panel.js) is committed so end-users don't need Node installed.
Edit / rebuild
cd custom_components/omni_pca/frontend
npm install # one-time
npm run build # one-shot — drops a fresh ../www/panel.js
npm run watch # rebuild on change (use during HA dev)
The build script (build.mjs) bundles the entry point + Lit + all
imports into a single ESM file at ../www/panel.js. Source maps are
inlined in --watch mode and stripped in production builds. Output is
~34 KB minified.
Layout
| File | Purpose |
|---|---|
src/omni-panel-programs.ts |
The custom-element entry point. Defines <omni-panel-programs> (matching the panel_custom registration). |
src/token-renderer.ts |
Token stream → Lit TemplateResult. Each TokenKind gets distinctive styling; REF tokens become buttons that dispatch a click. |
src/types.ts |
TS interfaces mirroring the Phase-B websocket wire shapes. Short keys (k/t/ek/ei/s) match websocket.py:_tokens_to_json. |
Wire contract
The panel calls three websocket commands (all defined in
../websocket.py):
omni_pca/programs/list— paginated, filterable summaries.omni_pca/programs/get— full structured-English detail for one slot.omni_pca/programs/fire— sendsCommand.EXECUTE_PROGRAMover the wire.
The frontend doesn't subscribe to push events; live-state badges
refresh on a low-frequency poll (REFRESH_MS = 5000). That's a
deliberate scope choice — switching to per-entity event subscription
is a follow-up if the polling overhead becomes visible on huge installs.