Ryan Malloy 486258a034
Some checks are pending
Validate / HACS validation (push) Waiting to run
Validate / Hassfest (push) Waiting to run
panel: structured-OP AND record editor (TEMP > 70 etc.)
Replaces the read-only "structured comparison" banner with a real
editor. Structured AND records encode ``Arg1 OP Arg2`` where Arg1 is
a typed reference (Zone / Unit / Thermostat / Area / TimeDate) plus a
per-type field selector, and Arg2 is either another typed reference
or a literal constant.

I1 — TS types + decoders:

Wire layout (programs.py decoders, clsProgram.cs):
  cond  high byte  = and_op           (CondOP: 1=EQ, 2=NE, 3=LT,
                                       4=GT, 5=ODD, 6=EVEN, 7=MULT,
                                       8=IN, 9=NOT_IN)
  cond  low byte   = and_arg1_argtype (CondArgType)
  cond2 (whole)    = and_arg1_ix      (object idx; 0 for TimeDate)
  cmd              = and_arg1_field   (per-type field selector)
  par              = and_arg2_argtype (Constant most common)
  pr2              = and_arg2_ix      (constant value or 2nd obj idx)
  month            = and_arg2_field
  day,days         = and_compconst    (BE u16; usually 0)

decodeStructuredAnd / encodeStructuredAnd handle both directions;
round-trip exact.

Per-Arg1Type field menus in FIELDS_BY_TYPE — exact 1:1 with the
Python enuZoneField / enuUnitField / enuThermostatField /
enuTimeDateField enums in omni_pca.programs and the field handling
in StateEvaluator. Areas only expose "Security mode" (single useful
field). TimeDate exposes Year / Month / Day / DoW / Time / Hour /
Minute (skips the rarely-used Date / DST / SunriseSunset fields).

I2 — editor UI:

isEditableStructuredAnd guard: only opens the editor for records
matching the editor's scope (Arg1 in supported types, Arg2=Constant,
compConst=0). Out-of-scope structured records render with a
"read-only" tag — preserved on save, still removable.

Structured rows render with a "structured" tag and an orange-tinted
background to distinguish them from Traditional rows. Layout:

  Arg1 type ▸ object picker ▸ Field ▸ Operator ▸ Compare against

Unary operators (ODD / EVEN) hide the Arg2 input. Changing Arg1 type
resets the Arg1 index + field to defaults so the form stays self-
consistent (no stale picker values from a previous type).

Arg2 is locked to Constant in this pass. Editing record-vs-record
comparisons (e.g. "Thermostat 1 temp > Thermostat 2 temp") is a
future cut — current real-world programs use the Constant form
exclusively per my homeowner-panel sample.

_pickBucket gains the missing "thermostat" branch (was missed in
earlier passes; only mattered now that thermostat is an Arg1Type).

Live screenshot 12-structured-and.png shows an injected chain with
both a Traditional AND (CTRL UNIT 1 ON) and a Structured AND
(Thermostat(1).Temperature > 70) — both editable end-to-end.

Frontend bundle: 88 KB minified (up from 82 KB).
Full suite: 653 passed, 1 skipped (no test changes).
2026-05-17 02:24:59 -06:00
..

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 — sends Command.EXECUTE_PROGRAM over 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.