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).
This commit is contained in:
parent
9ca4da98e8
commit
486258a034
@ -12,13 +12,17 @@ import { LitElement, html, css, PropertyValues, TemplateResult } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
import { renderTokens } from "./token-renderer.js";
|
import { renderTokens } from "./token-renderer.js";
|
||||||
import {
|
import {
|
||||||
|
ARG_TYPES,
|
||||||
COMMAND_OPTIONS,
|
COMMAND_OPTIONS,
|
||||||
|
COND_OPS,
|
||||||
CommandOption,
|
CommandOption,
|
||||||
CondFamily,
|
CondFamily,
|
||||||
DAY_BITS,
|
DAY_BITS,
|
||||||
DecodedCondition,
|
DecodedCondition,
|
||||||
DecodedEvent,
|
DecodedEvent,
|
||||||
|
DecodedStructuredAnd,
|
||||||
EventCategory,
|
EventCategory,
|
||||||
|
FIELDS_BY_TYPE,
|
||||||
FIXED_EVENTS,
|
FIXED_EVENTS,
|
||||||
Hass,
|
Hass,
|
||||||
MISC_CONDITIONALS,
|
MISC_CONDITIONALS,
|
||||||
@ -37,18 +41,23 @@ import {
|
|||||||
ProgramListResponse,
|
ProgramListResponse,
|
||||||
ProgramRow,
|
ProgramRow,
|
||||||
SECURITY_MODE_NAMES,
|
SECURITY_MODE_NAMES,
|
||||||
|
argTypeKind,
|
||||||
commandOptionFor,
|
commandOptionFor,
|
||||||
decodeAndCondition,
|
decodeAndCondition,
|
||||||
decodeCondition,
|
decodeCondition,
|
||||||
decodeEventId,
|
decodeEventId,
|
||||||
|
decodeStructuredAnd,
|
||||||
emptyAndRecord,
|
emptyAndRecord,
|
||||||
emptyOrRecord,
|
emptyOrRecord,
|
||||||
emptyThenRecord,
|
emptyThenRecord,
|
||||||
encodeAndCondition,
|
encodeAndCondition,
|
||||||
encodeCondition,
|
encodeCondition,
|
||||||
encodeEventId,
|
encodeEventId,
|
||||||
|
encodeStructuredAnd,
|
||||||
eventIdFromFields,
|
eventIdFromFields,
|
||||||
|
isEditableStructuredAnd,
|
||||||
isStructuredAnd,
|
isStructuredAnd,
|
||||||
|
isUnaryOp,
|
||||||
packEventIdIntoFields,
|
packEventIdIntoFields,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
@ -566,10 +575,11 @@ export class OmniPanelPrograms extends LitElement {
|
|||||||
private _pickBucket(kind: string): NamedObject[] | null {
|
private _pickBucket(kind: string): NamedObject[] | null {
|
||||||
if (!this._objects) return null;
|
if (!this._objects) return null;
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case "zone": return this._objects.zones;
|
case "zone": return this._objects.zones;
|
||||||
case "unit": return this._objects.units;
|
case "unit": return this._objects.units;
|
||||||
case "area": return this._objects.areas;
|
case "area": return this._objects.areas;
|
||||||
case "button": return this._objects.buttons;
|
case "button": return this._objects.buttons;
|
||||||
|
case "thermostat": return this._objects.thermostats;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1820,19 +1830,7 @@ export class OmniPanelPrograms extends LitElement {
|
|||||||
): TemplateResult {
|
): TemplateResult {
|
||||||
const isOr = cond.prog_type === PROGRAM_TYPE_OR;
|
const isOr = cond.prog_type === PROGRAM_TYPE_OR;
|
||||||
if (isStructuredAnd(cond)) {
|
if (isStructuredAnd(cond)) {
|
||||||
return html`
|
return this._renderStructuredChainConditionRow(cond, idx, isOr);
|
||||||
<div class="cond-slot structured-cond">
|
|
||||||
<div>
|
|
||||||
<strong>${isOr ? "OR IF" : "AND IF"}</strong> (structured comparison — read-only)
|
|
||||||
<button type="button" class="mini-btn danger"
|
|
||||||
@click=${() => this._removeChainCondition(idx)}>×</button>
|
|
||||||
</div>
|
|
||||||
<div class="conditions-readonly">
|
|
||||||
This condition uses a structured comparison (TEMP > N etc.).
|
|
||||||
Editing structured-OP records is not yet supported; it's
|
|
||||||
preserved on save.
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
const decoded = decodeAndCondition(cond);
|
const decoded = decodeAndCondition(cond);
|
||||||
return html`
|
return html`
|
||||||
@ -1846,6 +1844,156 @@ export class OmniPanelPrograms extends LitElement {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _renderStructuredChainConditionRow(
|
||||||
|
cond: ProgramFields, idx: number, isOr: boolean,
|
||||||
|
): 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.
|
||||||
|
return html`
|
||||||
|
<div class="cond-slot structured-cond">
|
||||||
|
<div class="cond-row-header">
|
||||||
|
<strong>${isOr ? "OR IF" : "AND IF"}</strong>
|
||||||
|
<span class="readonly-tag">read-only</span>
|
||||||
|
<button type="button" class="mini-btn danger"
|
||||||
|
@click=${() => this._removeChainCondition(idx)}>×</button>
|
||||||
|
</div>
|
||||||
|
<div class="conditions-readonly">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div class="cond-slot structured-cond">
|
||||||
|
<div class="cond-row-header">
|
||||||
|
<strong>${isOr ? "OR IF" : "AND IF"}</strong>
|
||||||
|
<span class="structured-tag">structured</span>
|
||||||
|
<button type="button" class="mini-btn danger"
|
||||||
|
@click=${() => this._removeChainCondition(idx)}>×</button>
|
||||||
|
</div>
|
||||||
|
${this._renderStructuredAndForm(s, idx)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Render the editor for one structured-AND condition. Lays out as:
|
||||||
|
*
|
||||||
|
* Arg1 type ▸ object/picker ▸ field ▸ operator ▸ Arg2 constant
|
||||||
|
*
|
||||||
|
* Arg2 is locked to Constant in this pass. For unary operators
|
||||||
|
* (ODD / EVEN) the Arg2 input is hidden.
|
||||||
|
*/
|
||||||
|
private _renderStructuredAndForm(
|
||||||
|
s: DecodedStructuredAnd, idx: number,
|
||||||
|
): TemplateResult {
|
||||||
|
const update = (patch: Partial<DecodedStructuredAnd>) => {
|
||||||
|
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 showArg2 = !isUnaryOp(s.op);
|
||||||
|
return html`
|
||||||
|
<div class="structured-row">
|
||||||
|
<label class="block">
|
||||||
|
Arg1 type
|
||||||
|
<select @change=${(e: Event) => {
|
||||||
|
const newType = parseInt((e.target as HTMLSelectElement).value, 10);
|
||||||
|
// Reset arg1Ix + field when type changes — keeps the form
|
||||||
|
// self-consistent and avoids stale picker values.
|
||||||
|
const firstField = (FIELDS_BY_TYPE[newType] ?? [{ value: 0 }])[0].value;
|
||||||
|
const newKind = argTypeKind(newType);
|
||||||
|
let newIx = 0;
|
||||||
|
if (newKind === "zone") newIx = this._objects?.zones?.[0]?.index ?? 1;
|
||||||
|
else if (newKind === "unit") newIx = this._objects?.units?.[0]?.index ?? 1;
|
||||||
|
else if (newKind === "thermostat") newIx = this._objects?.thermostats?.[0]?.index ?? 1;
|
||||||
|
else if (newKind === "area") newIx = this._objects?.areas?.[0]?.index ?? 1;
|
||||||
|
update({
|
||||||
|
arg1Type: newType,
|
||||||
|
arg1Ix: newIx,
|
||||||
|
arg1Field: firstField,
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
${ARG_TYPES.filter((a) => a.value !== 0).map((a) => html`
|
||||||
|
<option .value=${String(a.value)} ?selected=${a.value === s.arg1Type}>
|
||||||
|
${a.label}
|
||||||
|
</option>`)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
${arg1Kind ? this._renderStructuredArg1Picker(s, arg1Kind, update) : ""}
|
||||||
|
|
||||||
|
${arg1Fields.length > 0 ? html`
|
||||||
|
<label class="block">
|
||||||
|
Field
|
||||||
|
<select @change=${(e: Event) => update({
|
||||||
|
arg1Field: parseInt((e.target as HTMLSelectElement).value, 10),
|
||||||
|
})}>
|
||||||
|
${arg1Fields.map((f) => html`
|
||||||
|
<option .value=${String(f.value)} ?selected=${f.value === s.arg1Field}>
|
||||||
|
${f.label}
|
||||||
|
</option>`)}
|
||||||
|
</select>
|
||||||
|
</label>` : ""}
|
||||||
|
|
||||||
|
<label class="block">
|
||||||
|
Operator
|
||||||
|
<select @change=${(e: Event) => update({
|
||||||
|
op: parseInt((e.target as HTMLSelectElement).value, 10),
|
||||||
|
})}>
|
||||||
|
${COND_OPS.map((o) => html`
|
||||||
|
<option .value=${String(o.value)} ?selected=${o.value === s.op}>
|
||||||
|
${o.label}
|
||||||
|
</option>`)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
${showArg2 ? html`
|
||||||
|
<label class="block">
|
||||||
|
Compare against (constant)
|
||||||
|
<input type="number" min="0" max="65535"
|
||||||
|
.value=${String(s.arg2Ix)}
|
||||||
|
@input=${(e: Event) => {
|
||||||
|
const v = parseInt((e.target as HTMLInputElement).value, 10);
|
||||||
|
if (Number.isFinite(v) && v >= 0 && v <= 0xFFFF) {
|
||||||
|
update({ arg2Ix: v });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>` : ""}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderStructuredArg1Picker(
|
||||||
|
s: DecodedStructuredAnd,
|
||||||
|
kind: string,
|
||||||
|
update: (p: Partial<DecodedStructuredAnd>) => void,
|
||||||
|
): TemplateResult {
|
||||||
|
const bucket = this._bucketWithPreserve(
|
||||||
|
this._pickBucket(kind), kind, s.arg1Ix,
|
||||||
|
);
|
||||||
|
const label = kind[0].toUpperCase() + kind.slice(1);
|
||||||
|
return html`
|
||||||
|
<label class="block">
|
||||||
|
${label}
|
||||||
|
<select @change=${(e: Event) => update({
|
||||||
|
arg1Ix: parseInt((e.target as HTMLSelectElement).value, 10),
|
||||||
|
})}>
|
||||||
|
${bucket.map((o) => html`
|
||||||
|
<option .value=${String(o.index)} ?selected=${o.index === s.arg1Ix}>
|
||||||
|
#${o.index} ${o.name}
|
||||||
|
</option>`)}
|
||||||
|
</select>
|
||||||
|
</label>`;
|
||||||
|
}
|
||||||
|
|
||||||
private _renderChainCondFamily(
|
private _renderChainCondFamily(
|
||||||
decoded: DecodedCondition, idx: number,
|
decoded: DecodedCondition, idx: number,
|
||||||
): TemplateResult {
|
): TemplateResult {
|
||||||
@ -2458,7 +2606,30 @@ export class OmniPanelPrograms extends LitElement {
|
|||||||
border-color: var(--error-color, #db4437);
|
border-color: var(--error-color, #db4437);
|
||||||
}
|
}
|
||||||
.structured-cond {
|
.structured-cond {
|
||||||
background: rgba(255, 152, 0, 0.08); /* subtle warning tint */
|
background: rgba(255, 152, 0, 0.08); /* subtle structured tint */
|
||||||
|
}
|
||||||
|
.structured-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.structured-tag, .readonly-tag {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 6px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.structured-tag {
|
||||||
|
background: rgba(255, 152, 0, 0.18);
|
||||||
|
color: #b35a00;
|
||||||
|
}
|
||||||
|
.readonly-tag {
|
||||||
|
background: var(--secondary-background-color, #eee);
|
||||||
|
color: var(--secondary-text-color, #888);
|
||||||
}
|
}
|
||||||
.chain-meta {
|
.chain-meta {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
|||||||
@ -547,6 +547,177 @@ export function emptyThenRecord(firstUnit: number = 1): ProgramFields {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Structured-OP AND record editing.
|
||||||
|
//
|
||||||
|
// When ``and_op`` (= ``(cond >> 8) & 0xFF``) is non-zero, the record
|
||||||
|
// encodes ``Arg1 OP Arg2`` where Arg1 and Arg2 are typed references
|
||||||
|
// (Zone, Unit, Thermostat, Area, TimeDate, Constant) plus per-type
|
||||||
|
// field selectors. This is fundamentally a different shape from the
|
||||||
|
// Traditional encoding handled by decodeAndCondition above.
|
||||||
|
//
|
||||||
|
// Wire layout (from programs.py decoders + clsProgram.cs):
|
||||||
|
//
|
||||||
|
// cond high byte (>>8) = and_op (CondOP)
|
||||||
|
// cond low byte (& FF) = and_arg1_argtype (CondArgType)
|
||||||
|
// cond2 (whole u16) = and_arg1_ix (object index or 0)
|
||||||
|
// cmd = and_arg1_field (per-type field selector)
|
||||||
|
// par = and_arg2_argtype (CondArgType — usually Constant)
|
||||||
|
// pr2 = and_arg2_ix (constant value OR second object idx)
|
||||||
|
// month = and_arg2_field (per-type field selector for arg2)
|
||||||
|
// day, days = and_compconst (BE u16 — extra constant, rarely used)
|
||||||
|
//
|
||||||
|
// Editor cuts:
|
||||||
|
// * Arg2 locked to Constant in this pass (other-object Arg2 stays
|
||||||
|
// read-only with a banner). Arg2-constant covers
|
||||||
|
// "TEMP > 70", "Zone.CurrentState == 1", "Hour == 22" etc.
|
||||||
|
// * Arg1 restricted to Zone / Unit / Thermostat / Area / TimeDate.
|
||||||
|
// Anything else (Aux / Audio / System / etc.) stays read-only.
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
// CondOP enum (matches omni_pca.programs.CondOP). 0=Traditional is
|
||||||
|
// excluded from the editor — picking it would switch to Traditional
|
||||||
|
// editing semantics.
|
||||||
|
export const COND_OPS: ReadonlyArray<{ value: number; label: string }> = [
|
||||||
|
{ value: 1, label: "==" },
|
||||||
|
{ value: 2, label: "!=" },
|
||||||
|
{ value: 3, label: "<" },
|
||||||
|
{ value: 4, label: ">" },
|
||||||
|
{ value: 5, label: "is odd" },
|
||||||
|
{ value: 6, label: "is even" },
|
||||||
|
{ value: 7, label: "is multiple of" },
|
||||||
|
{ value: 8, label: "in (bitmask)" },
|
||||||
|
{ value: 9, label: "not in (bitmask)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** True iff the operator only uses Arg1 (no Arg2). */
|
||||||
|
export function isUnaryOp(op: number): boolean {
|
||||||
|
return op === 5 || op === 6; // ODD, EVEN
|
||||||
|
}
|
||||||
|
|
||||||
|
// CondArgType enum (matches omni_pca.programs.CondArgType). Only the
|
||||||
|
// editor-supported subset; full list is in programs.py.
|
||||||
|
export const ARG_TYPES: ReadonlyArray<{
|
||||||
|
value: number; label: string; kind: string | null;
|
||||||
|
}> = [
|
||||||
|
{ value: 0, label: "Constant", kind: null },
|
||||||
|
{ value: 2, label: "Zone", kind: "zone" },
|
||||||
|
{ value: 3, label: "Unit", kind: "unit" },
|
||||||
|
{ value: 4, label: "Thermostat", kind: "thermostat" },
|
||||||
|
{ value: 6, label: "Area", kind: "area" },
|
||||||
|
{ value: 7, label: "Time / Date", kind: null }, // no object picker
|
||||||
|
];
|
||||||
|
|
||||||
|
export function isEditableArg1Type(argtype: number): boolean {
|
||||||
|
return [2, 3, 4, 6, 7].includes(argtype);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function argTypeKind(argtype: number): string | null {
|
||||||
|
const a = ARG_TYPES.find((x) => x.value === argtype);
|
||||||
|
return a ? a.kind : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-Arg1Type field menus. Numbers match omni_pca.programs enums
|
||||||
|
// (enuZoneField / enuUnitField / enuThermostatField / enuTimeDateField).
|
||||||
|
export const FIELDS_BY_TYPE: Readonly<Record<number, ReadonlyArray<{
|
||||||
|
value: number; label: string;
|
||||||
|
}>>> = {
|
||||||
|
// Zone (argtype 2) — enuZoneField
|
||||||
|
2: [
|
||||||
|
{ value: 1, label: "Loop reading" },
|
||||||
|
{ value: 2, label: "Current state" },
|
||||||
|
{ value: 3, label: "Arming state" },
|
||||||
|
{ value: 4, label: "Alarm state" },
|
||||||
|
],
|
||||||
|
// Unit (argtype 3) — enuUnitField
|
||||||
|
3: [
|
||||||
|
{ value: 1, label: "Current state" },
|
||||||
|
{ value: 2, label: "Previous state" },
|
||||||
|
{ value: 3, label: "Timer" },
|
||||||
|
{ value: 4, label: "Level" },
|
||||||
|
],
|
||||||
|
// Thermostat (argtype 4) — enuThermostatField
|
||||||
|
4: [
|
||||||
|
{ value: 1, label: "Current temperature" },
|
||||||
|
{ value: 2, label: "Heat setpoint" },
|
||||||
|
{ value: 3, label: "Cool setpoint" },
|
||||||
|
{ value: 4, label: "System mode" },
|
||||||
|
{ value: 5, label: "Fan mode" },
|
||||||
|
{ value: 6, label: "Hold mode" },
|
||||||
|
{ value: 7, label: "Freeze alarm" },
|
||||||
|
{ value: 8, label: "Comm error" },
|
||||||
|
{ value: 9, label: "Humidity" },
|
||||||
|
{ value: 10, label: "Humidify setpoint" },
|
||||||
|
{ value: 11, label: "Dehumidify setpoint" },
|
||||||
|
{ value: 12, label: "Outdoor temperature" },
|
||||||
|
{ value: 13, label: "System status" },
|
||||||
|
],
|
||||||
|
// Area (argtype 6) — single useful field
|
||||||
|
6: [
|
||||||
|
{ value: 1, label: "Security mode" },
|
||||||
|
],
|
||||||
|
// TimeDate (argtype 7) — enuTimeDateField
|
||||||
|
7: [
|
||||||
|
{ value: 2, label: "Year" },
|
||||||
|
{ value: 3, label: "Month" },
|
||||||
|
{ value: 4, label: "Day" },
|
||||||
|
{ value: 5, label: "Day of week (1=Mon..7=Sun)" },
|
||||||
|
{ value: 6, label: "Time (minutes since midnight)" },
|
||||||
|
{ value: 8, label: "Hour" },
|
||||||
|
{ value: 9, label: "Minute" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DecodedStructuredAnd {
|
||||||
|
op: number; // CondOP value (1..9)
|
||||||
|
arg1Type: number; // CondArgType
|
||||||
|
arg1Ix: number; // 1-based object index (0 for TimeDate)
|
||||||
|
arg1Field: number; // per-type field
|
||||||
|
arg2Type: number; // CondArgType (locked to Constant in editor)
|
||||||
|
arg2Ix: number; // constant value OR second object index
|
||||||
|
arg2Field: number; // per-type field (usually 0 for constants)
|
||||||
|
compConst: number; // extra constant; preserved verbatim
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeStructuredAnd(fields: ProgramFields): DecodedStructuredAnd {
|
||||||
|
return {
|
||||||
|
op: ((fields.cond ?? 0) >> 8) & 0xFF,
|
||||||
|
arg1Type: (fields.cond ?? 0) & 0xFF,
|
||||||
|
arg1Ix: fields.cond2 ?? 0,
|
||||||
|
arg1Field: fields.cmd ?? 0,
|
||||||
|
arg2Type: fields.par ?? 0,
|
||||||
|
arg2Ix: fields.pr2 ?? 0,
|
||||||
|
arg2Field: fields.month ?? 0,
|
||||||
|
compConst: ((fields.day ?? 0) << 8) | (fields.days ?? 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeStructuredAnd(s: DecodedStructuredAnd): Partial<ProgramFields> {
|
||||||
|
return {
|
||||||
|
cond: ((s.op & 0xFF) << 8) | (s.arg1Type & 0xFF),
|
||||||
|
cond2: s.arg1Ix & 0xFFFF,
|
||||||
|
cmd: s.arg1Field & 0xFF,
|
||||||
|
par: s.arg2Type & 0xFF,
|
||||||
|
pr2: s.arg2Ix & 0xFFFF,
|
||||||
|
month: s.arg2Field & 0xFF,
|
||||||
|
day: (s.compConst >> 8) & 0xFF,
|
||||||
|
days: s.compConst & 0xFF,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True iff the structured AND record is in a shape the editor can
|
||||||
|
* fully drive. Other shapes (Arg2=non-constant object, exotic Arg1
|
||||||
|
* types, or non-zero compConst) stay read-only — they're preserved
|
||||||
|
* on save but their fields aren't exposed as form controls. */
|
||||||
|
export function isEditableStructuredAnd(s: DecodedStructuredAnd): boolean {
|
||||||
|
if (!isEditableArg1Type(s.arg1Type)) return false;
|
||||||
|
if (!isUnaryOp(s.op) && s.arg2Type !== 0) return false;
|
||||||
|
if (s.compConst !== 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/** HA's hass object — minimal surface we use. */
|
/** HA's hass object — minimal surface we use. */
|
||||||
export interface Hass {
|
export interface Hass {
|
||||||
connection: {
|
connection: {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
BIN
dev/artifacts/screenshots/2026-05-16/12-structured-and.png
Normal file
BIN
dev/artifacts/screenshots/2026-05-16/12-structured-and.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
Loading…
x
Reference in New Issue
Block a user