From 8a0fb1e4fe5b34a7dd5d6dfbf7bc1b312ed221d7 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sun, 17 May 2026 13:06:07 -0600 Subject: [PATCH] panel: Arg2-as-object editor for structured-AND records Lets structured-AND IF rows compare a typed field against another typed field, not just a constant. Authoring "Thermostat 1.Temp > Thermostat 2.Temp" now works in-place; previously Arg2 was locked to Constant in the editor. - types.ts: relax isEditableStructuredAnd to permit Zone/Unit/ Thermostat/Area/TimeDate as Arg2 types (the same editable set already accepted for Arg1). - omni-panel-programs.ts: replace the lone constant input with Arg2 type/object/field controls that mirror the Arg1 layout; switching Arg2 between Constant and a reference type swaps the sub-controls and resets defaults sensibly. - _renderStructuredArg1Picker generalised to _renderStructuredObjectPicker driving both sides; _defaultIxForKind extracted as a shared helper. - Bundle rebuilt. - dev/screenshot_arg2_object.py: targeted playwright helper that opens the chain at slot 200 and screenshots the editor for visual verification. --- .../frontend/src/omni-panel-programs.ts | 141 +++++++++----- .../omni_pca/frontend/src/types.ts | 21 ++- custom_components/omni_pca/www/panel.js | 173 ++++++++++-------- .../2026-05-17/arg2-object-editor.png | Bin 0 -> 138825 bytes dev/screenshot_arg2_object.py | 150 +++++++++++++++ 5 files changed, 357 insertions(+), 128 deletions(-) create mode 100644 dev/artifacts/screenshots/2026-05-17/arg2-object-editor.png create mode 100644 dev/screenshot_arg2_object.py diff --git a/custom_components/omni_pca/frontend/src/omni-panel-programs.ts b/custom_components/omni_pca/frontend/src/omni-panel-programs.ts index e089efd..6d0a8f3 100644 --- a/custom_components/omni_pca/frontend/src/omni-panel-programs.ts +++ b/custom_components/omni_pca/frontend/src/omni-panel-programs.ts @@ -1849,9 +1849,9 @@ export class OmniPanelPrograms extends LitElement { ): TemplateResult { const s = decodeStructuredAnd(cond); if (!isEditableStructuredAnd(s)) { - // Out of editor scope (non-constant Arg2, unsupported Arg1 type, - // or non-zero compConst). Surface as preserve-only so the user - // can still remove the row but can't damage the encoded data. + // Out of editor scope (unsupported Arg1/Arg2 type or non-zero + // CompConst). Surface as preserve-only so the user can still + // remove the row but can't damage the encoded data. return html`
@@ -1862,8 +1862,8 @@ export class OmniPanelPrograms extends LitElement {
Structured comparison with a shape the editor can't drive - yet (Arg2 references another object, Arg1 is an unsupported - type, or a CompConst value is present). Preserved on save. + yet (Arg1 or Arg2 is an unsupported type, or a CompConst + value is present). Preserved on save.
`; } @@ -1881,24 +1881,24 @@ export class OmniPanelPrograms extends LitElement { /** Render the editor for one structured-AND condition. Lays out as: * - * Arg1 type ▸ object/picker ▸ field ▸ operator ▸ Arg2 constant + * Arg1 type ▸ object/picker ▸ field ▸ operator ▸ + * Arg2 type ▸ (constant | object/picker ▸ field) * - * Arg2 is locked to Constant in this pass. For unary operators - * (ODD / EVEN) the Arg2 input is hidden. + * Both Arg1 and Arg2 support Zone / Unit / Thermostat / Area / + * TimeDate references; Arg2 also supports plain Constant. For + * unary operators (ODD / EVEN) the Arg2 controls are hidden. */ private _renderStructuredAndForm( s: DecodedStructuredAnd, idx: number, ): TemplateResult { const update = (patch: Partial) => { const merged = { ...s, ...patch }; - // Force Arg2 = Constant in editor scope so nothing accidentally - // promotes to an object reference. - merged.arg2Type = 0; - merged.arg2Field = 0; this._patchChainCondition(idx, encodeStructuredAnd(merged)); }; const arg1Fields = FIELDS_BY_TYPE[s.arg1Type] ?? []; const arg1Kind = argTypeKind(s.arg1Type); + const arg2Fields = FIELDS_BY_TYPE[s.arg2Type] ?? []; + const arg2Kind = argTypeKind(s.arg2Type); const showArg2 = !isUnaryOp(s.op); return html`
@@ -1906,19 +1906,10 @@ export class OmniPanelPrograms extends LitElement { Arg1 type - ${arg1Kind ? this._renderStructuredArg1Picker(s, arg1Kind, update) : ""} + ${arg1Kind ? this._renderStructuredObjectPicker( + arg1Kind, s.arg1Ix, (v) => update({ arg1Ix: v }), "Arg1", + ) : ""} ${arg1Fields.length > 0 ? html` + + ${s.arg2Type === 0 ? html` + ` : ""} + + ${arg2Kind ? this._renderStructuredObjectPicker( + arg2Kind, s.arg2Ix, (v) => update({ arg2Ix: v }), "Arg2", + ) : ""} + + ${s.arg2Type !== 0 && arg2Fields.length > 0 ? html` + ` : ""} + ` : ""}
`; } - private _renderStructuredArg1Picker( - s: DecodedStructuredAnd, + /** First discovered object index for a given kind, falling back to 1 + * for reference kinds (TimeDate / null returns 0 — "no object"). */ + private _defaultIxForKind(kind: string | null): number { + switch (kind) { + case "zone": return this._objects?.zones?.[0]?.index ?? 1; + case "unit": return this._objects?.units?.[0]?.index ?? 1; + case "thermostat": return this._objects?.thermostats?.[0]?.index ?? 1; + case "area": return this._objects?.areas?.[0]?.index ?? 1; + default: return 0; + } + } + + private _renderStructuredObjectPicker( kind: string, - update: (p: Partial) => void, + current: number, + onChange: (v: number) => void, + labelPrefix: string, ): TemplateResult { const bucket = this._bucketWithPreserve( - this._pickBucket(kind), kind, s.arg1Ix, + this._pickBucket(kind), kind, current, ); - const label = kind[0].toUpperCase() + kind.slice(1); + const kindLabel = kind[0].toUpperCase() + kind.slice(1); return html` + + ${e.arg2Type===0?o` + `:""} + + ${l?this._renderStructuredObjectPicker(l,e.arg2Ix,c=>i({arg2Ix:c}),"Arg2"):""} + + ${e.arg2Type!==0&&u.length>0?o` + `:""} + `:""} + `}_defaultIxForKind(e){switch(e){case"zone":return this._objects?.zones?.[0]?.index??1;case"unit":return this._objects?.units?.[0]?.index??1;case"thermostat":return this._objects?.thermostats?.[0]?.index??1;case"area":return this._objects?.areas?.[0]?.index??1;default:return 0}}_renderStructuredObjectPicker(e,t,i,r){let s=this._bucketWithPreserve(this._pickBucket(e),e,t),u=e[0].toUpperCase()+e.slice(1);return o` `}_renderChainCondFamily(e,t){let i=s=>{let c=this._objects?.zones?.[0]?.index??1,l=this._objects?.units?.[0]?.index??1,d=this._objects?.areas?.[0]?.index??1,p;switch(s){case"none":p={family:"none"};break;case"misc":p={family:"misc",misc:1};break;case"zone":p={family:"zone",index:c,active:!1};break;case"unit":p={family:"unit",index:l,active:!0};break;case"time":p={family:"time",index:1,active:!0};break;case"sec":p={family:"sec",index:d,mode:0};break}let u=ve(p);this._patchChainCondition(t,u)},r=s=>{this._patchChainCondition(t,ve(s))};return o` + `}_renderChainCondFamily(e,t){let i=s=>{let u=this._objects?.zones?.[0]?.index??1,l=this._objects?.units?.[0]?.index??1,p=this._objects?.areas?.[0]?.index??1,c;switch(s){case"none":c={family:"none"};break;case"misc":c={family:"misc",misc:1};break;case"zone":c={family:"zone",index:u,active:!1};break;case"unit":c={family:"unit",index:l,active:!0};break;case"time":c={family:"time",index:1,active:!0};break;case"sec":c={family:"sec",index:p,mode:0};break}let d=$e(c);this._patchChainCondition(t,d)},r=s=>{this._patchChainCondition(t,$e(s))};return o`