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 65aa4fd..ab97abe 100644 --- a/custom_components/omni_pca/frontend/src/omni-panel-programs.ts +++ b/custom_components/omni_pca/frontend/src/omni-panel-programs.ts @@ -15,17 +15,33 @@ import { COMMAND_OPTIONS, CommandOption, DAY_BITS, + DecodedEvent, + EventCategory, + FIXED_EVENTS, Hass, + MONTH_NAMES, NamedObject, ObjectListResponse, + PROGRAM_TYPE_EVENT, PROGRAM_TYPE_TIMED, + PROGRAM_TYPE_YEARLY, ProgramDetail, ProgramFields, ProgramListResponse, ProgramRow, commandOptionFor, + decodeEventId, + encodeEventId, + eventIdFromFields, + packEventIdIntoFields, } from "./types.js"; +// Which compact-form trigger types the editor knows how to render. +// REMARK is intentionally excluded (it's a text annotation, not a +// runnable program). Clausal types (WHEN/AT/EVERY) are kind="chain" +// not "compact" so they're filtered out earlier in _beginEdit. +const EDITABLE_PROG_TYPES = new Set(["TIMED", "EVENT", "YEARLY"]); + const TRIGGER_TYPES = [ "TIMED", "EVENT", "YEARLY", "WHEN", "AT", "EVERY", "REMARK", ] as const; @@ -284,58 +300,55 @@ export class OmniPanelPrograms extends LitElement { private async _beginEdit(): Promise { if (!this._detail || this._detail.kind !== "compact") return; - // Only TIMED programs are editable in this pass; the others render - // a "not yet editable" banner instead. - if (this._detail.trigger_type !== "TIMED") return; + // The frontend supports editing compact-form TIMED / EVENT / YEARLY + // programs. Other compact types (REMARK) and clausal chains remain + // read-only — the editor pathway returns early without seeding a + // draft so the read-only view stays visible. + if (!EDITABLE_PROG_TYPES.has(this._detail.trigger_type)) return; await this._ensureObjectsLoaded(); - // Seed the draft from the currently-loaded compact-form Program. - // The detail response doesn't include raw fields, so query the - // coordinator-cached program by re-fetching via list (which gives - // us trigger_type) plus a follow-up "get" for full tokens. The - // simplest path: read the underlying Program off the most-recent - // list row's metadata. References-only data is not enough — we - // need raw cmd/par/pr2/days/etc. Reach for it via a fresh ws call. if (!this._entryId) return; - const programDict = await this._fetchProgramFields( - this._entryId, this._detail.slot, + // The detail response now carries raw fields directly. If they're + // missing (panel returned only tokens) we fall back to sensible + // defaults so the form at least opens — better than a hard error. + const fields = this._detail.fields ?? this._defaultFieldsForType( + this._detail.trigger_type, ); - if (programDict === null) return; - this._editingDraft = programDict; - this._stopRefreshTimer(); // pause polling while editing + if (fields === null) return; + this._editingDraft = { ...fields }; + this._stopRefreshTimer(); } - private async _fetchProgramFields( - entryId: string, slot: number, - ): Promise { - // The list command returns rendered summaries; we need the raw - // Program fields to seed the form. The websocket layer doesn't - // currently expose raw fields, so we use a brief inline hack: - // re-fetch the list filtered to this exact slot via the references - // dimension, then read the underlying ProgramRow. But ProgramRow - // only carries trigger_type and counts, not raw bytes... - // - // Simplest path: add a brief endpoint or include raw fields in - // the get detail response. The wire side already has the bytes; - // we just need to send them. Doing this inline by piggy-backing - // on the list row would require a server change. For now, render - // a fresh form from sensible defaults (hour 6, minute 0, - // weekdays, UNIT_ON, pr2=first unit) and let the user adjust — - // this works for the new-program-via-clone flow. - // - // TODO: extend get-detail to include raw program fields so the - // editor seeds from real values when editing existing programs. - void entryId; void slot; + private _defaultFieldsForType(triggerType: string): ProgramFields | null { const firstUnit = this._objects?.units?.[0]?.index ?? 1; - return { - prog_type: PROGRAM_TYPE_TIMED, - cmd: 1, // UNIT_ON - par: 0, - pr2: firstUnit, - hour: 6, minute: 0, - days: 0x02 | 0x04 | 0x08 | 0x10 | 0x20, // Mon-Fri default - cond: 0, cond2: 0, - month: 0, day: 0, - }; + if (triggerType === "TIMED") { + return { + prog_type: PROGRAM_TYPE_TIMED, + cmd: 1, par: 0, pr2: firstUnit, + hour: 6, minute: 0, + days: 0x02 | 0x04 | 0x08 | 0x10 | 0x20, // Mon-Fri + cond: 0, cond2: 0, month: 0, day: 0, + }; + } + if (triggerType === "EVENT") { + const firstButton = this._objects?.buttons?.[0]?.index ?? 1; + return { + prog_type: PROGRAM_TYPE_EVENT, + cmd: 1, par: 0, pr2: firstUnit, + // Default to a button-press event; month+day pack the event_id. + month: 0, day: firstButton & 0xFF, + hour: 0, minute: 0, days: 0, + cond: 0, cond2: 0, + }; + } + if (triggerType === "YEARLY") { + return { + prog_type: PROGRAM_TYPE_YEARLY, + cmd: 1, par: 0, pr2: firstUnit, + month: 1, day: 1, hour: 0, minute: 0, + days: 0, cond: 0, cond2: 0, + }; + } + return null; } private async _saveDraft(): Promise { @@ -435,6 +448,116 @@ export class OmniPanelPrograms extends LitElement { } } + // ---- YEARLY handlers (month / day) --------------------------------- + + private _onMonthChange(e: Event): void { + const value = parseInt((e.target as HTMLSelectElement).value, 10); + if (Number.isFinite(value) && value >= 1 && value <= 12) { + this._patchDraft({ month: value }); + } + } + + private _onDayChange(e: Event): void { + const value = parseInt((e.target as HTMLInputElement).value, 10); + if (Number.isFinite(value) && value >= 1 && value <= 31) { + this._patchDraft({ day: value }); + } + } + + // ---- EVENT handlers (event-id builder) ----------------------------- + // + // The event_id is packed into the program's month/day bytes + // (eventId >> 8 = month, eventId & 0xFF = day) — that's the wire + // encoding for EVENT records. The UI works in terms of "category + + // sub-fields" and re-encodes on every change. + + private _patchEvent(decoded: DecodedEvent): void { + if (!this._editingDraft) return; + const eventId = encodeEventId(decoded); + this._editingDraft = packEventIdIntoFields(this._editingDraft, eventId); + } + + private _onEventCategoryChange(e: Event): void { + const cat = (e.target as HTMLSelectElement).value as EventCategory; + // Switching category — seed sensible defaults for the new category + // so the sub-fields below have valid initial values. + if (cat === "button") { + const firstButton = this._objects?.buttons?.[0]?.index ?? 1; + this._patchEvent({ category: "button", button: firstButton }); + } else if (cat === "zone") { + const firstZone = this._objects?.zones?.[0]?.index ?? 1; + this._patchEvent({ category: "zone", zone: firstZone, zoneState: 1 }); + } else if (cat === "unit") { + const firstUnit = this._objects?.units?.[0]?.index ?? 1; + this._patchEvent({ category: "unit", unit: firstUnit, unitOn: true }); + } else if (cat === "fixed") { + this._patchEvent({ category: "fixed", fixedId: 772 }); // AC lost + } + // "raw" isn't user-selectable from the dropdown — only appears when + // an existing event ID doesn't match a known pattern. + } + + private _onEventButtonChange(e: Event): void { + const button = parseInt((e.target as HTMLSelectElement).value, 10); + if (Number.isFinite(button)) { + this._patchEvent({ category: "button", button }); + } + } + + private _onEventZoneChange(e: Event): void { + if (!this._editingDraft) return; + const zone = parseInt((e.target as HTMLSelectElement).value, 10); + if (!Number.isFinite(zone)) return; + const existing = decodeEventId(eventIdFromFields(this._editingDraft)); + this._patchEvent({ + category: "zone", + zone, + zoneState: existing.zoneState ?? 1, + }); + } + + private _onEventZoneStateChange(e: Event): void { + if (!this._editingDraft) return; + const state = parseInt((e.target as HTMLSelectElement).value, 10); + if (!Number.isFinite(state)) return; + const existing = decodeEventId(eventIdFromFields(this._editingDraft)); + this._patchEvent({ + category: "zone", + zone: existing.zone ?? 1, + zoneState: state, + }); + } + + private _onEventUnitChange(e: Event): void { + if (!this._editingDraft) return; + const unit = parseInt((e.target as HTMLSelectElement).value, 10); + if (!Number.isFinite(unit)) return; + const existing = decodeEventId(eventIdFromFields(this._editingDraft)); + this._patchEvent({ + category: "unit", + unit, + unitOn: existing.unitOn ?? true, + }); + } + + private _onEventUnitOnChange(e: Event): void { + if (!this._editingDraft) return; + const on = (e.target as HTMLSelectElement).value === "1"; + const existing = decodeEventId(eventIdFromFields(this._editingDraft)); + this._patchEvent({ + category: "unit", + unit: existing.unit ?? 1, + unitOn: on, + }); + } + + private _onEventFixedChange(e: Event): void { + const id = parseInt((e.target as HTMLSelectElement).value, 10); + if (Number.isFinite(id)) { + this._patchEvent({ category: "fixed", fixedId: id }); + } + } + // -- refresh timer ---------------------------------------------------- private _startRefreshTimer(): void { @@ -610,7 +733,7 @@ export class OmniPanelPrograms extends LitElement { class="fire" @click=${() => this._fireProgram(d.slot)} >▶ Fire now - ${d.trigger_type === "TIMED" && d.kind === "compact" ? html` + ${d.kind === "compact" && EDITABLE_PROG_TYPES.has(d.trigger_type) ? html`
- -
- Time -
- - : - -
-
- - -
- Days -
- ${DAY_BITS.map((d) => { - const active = ((draft.days ?? 0) & d.bit) !== 0; - return html` - - `; - })} -
-
- - -
- Action - - ${cmdOpt?.ref_kind ? html` - ` : ""} - ${showsLevelPercent ? html` - ` : ""} -
- + ${this._renderTriggerSection(draft)} + ${this._renderActionSection(draft)} ${draft.cond || draft.cond2 ? html`
Inline conditions: - this program has up to two inline AND-IF conditions on the - source record. They're preserved when saving but editing + this program carries up to two inline AND-IF conditions on + the source record. They're preserved on save but editing condition fields is not yet supported.
` : ""}
@@ -804,6 +851,274 @@ export class OmniPanelPrograms extends LitElement { `; } + private _renderTriggerSection(draft: ProgramFields): TemplateResult { + switch (draft.prog_type) { + case PROGRAM_TYPE_TIMED: + return this._renderTimedTrigger(draft); + case PROGRAM_TYPE_EVENT: + return this._renderEventTrigger(draft); + case PROGRAM_TYPE_YEARLY: + return this._renderYearlyTrigger(draft); + default: + return html`
+ Editing program type ${draft.prog_type} is not supported. +
`; + } + } + + private _renderTimedTrigger(draft: ProgramFields): TemplateResult { + return html` +
+ Time +
+ + : + +
+
+
+ Days +
+ ${DAY_BITS.map((d) => { + const active = ((draft.days ?? 0) & d.bit) !== 0; + return html` + + `; + })} +
+
+ `; + } + + private _renderEventTrigger(draft: ProgramFields): TemplateResult { + const eventId = eventIdFromFields(draft); + const decoded = decodeEventId(eventId); + return html` +
+ Trigger event + + ${this._renderEventCategoryFields(decoded)} +
+ `; + } + + private _renderEventCategoryFields(decoded: DecodedEvent): TemplateResult { + if (decoded.category === "button") { + return html` + `; + } + if (decoded.category === "zone") { + return html` + + `; + } + if (decoded.category === "unit") { + return html` + + `; + } + if (decoded.category === "fixed") { + return html` + `; + } + // raw — render as informational; the user picked another category + // from the dropdown if they want to change it. + return html` +
+ Unrecognised event ID. Switch category above to redefine. +
`; + } + + private _renderYearlyTrigger(draft: ProgramFields): TemplateResult { + return html` +
+ Date +
+ + +
+
+
+ Time of day +
+ + : + +
+
+ `; + } + + private _renderActionSection(draft: ProgramFields): TemplateResult { + const cmdOpt: CommandOption | undefined = commandOptionFor(draft.cmd ?? 0); + const objectBucket = cmdOpt?.ref_kind ? this._pickBucket(cmdOpt.ref_kind) : null; + const showsLevelPercent = (draft.cmd === 9); // UNIT_LEVEL + return html` +
+ Action + + ${cmdOpt?.ref_kind ? html` + ` : ""} + ${showsLevelPercent ? html` + ` : ""} +
+ `; + } + // -- styles ----------------------------------------------------------- static styles = css` diff --git a/custom_components/omni_pca/frontend/src/types.ts b/custom_components/omni_pca/frontend/src/types.ts index 83c8a1b..df922ef 100644 --- a/custom_components/omni_pca/frontend/src/types.ts +++ b/custom_components/omni_pca/frontend/src/types.ts @@ -47,6 +47,9 @@ export interface ProgramDetail { references: string[]; /** For chain detail: every slot the chain spans. */ chain_slots?: number[]; + /** Raw Program field values; included for compact-form programs so + * the editor can seed its form from real data rather than defaults. */ + fields?: ProgramFields; } export interface ProgramListRequest { @@ -156,6 +159,127 @@ export const PROGRAM_TYPE_EVENT = 2; export const PROGRAM_TYPE_YEARLY = 3; export const PROGRAM_TYPE_REMARK = 4; + +// -------------------------------------------------------------------------- +// Event-ID encode/decode for the EVENT-program editor. +// +// Mirrors the Python helpers in omni_pca.program_engine — the 16-bit +// event_id uses different bit patterns per category. Each "category" +// in the UI maps to a different chunk of the ID space. +// -------------------------------------------------------------------------- + + +export type EventCategory = + | "button" // USER_MACRO_BUTTON (evt & 0xFF00) == 0x0000 + | "zone" // ZONE_STATE_CHANGE (evt & 0xFC00) == 0x0400 + | "unit" // UNIT_STATE_CHANGE (evt & 0xFC00) == 0x0800 + | "fixed" // hard-coded IDs (phone / AC power) + | "raw"; // anything else — show numeric + +export interface DecodedEvent { + category: EventCategory; + /** For "button": 1..255 */ + button?: number; + /** For "zone": 1..256, plus state 0=secure / 1=not-ready / 2=trouble / 3=tamper */ + zone?: number; + zoneState?: number; + /** For "unit": 1..511 plus on bool */ + unit?: number; + unitOn?: boolean; + /** For "fixed": the literal event ID. */ + fixedId?: number; + /** For "raw": the literal event ID we couldn't classify. */ + raw?: number; +} + +// Hand-rolled fixed IDs and labels (matches Python EVENT_* constants). +export const FIXED_EVENTS: ReadonlyArray<{ id: number; label: string }> = [ + { id: 768, label: "Phone line dead" }, + { id: 769, label: "Phone ringing" }, + { id: 770, label: "Phone off hook" }, + { id: 771, label: "Phone on hook" }, + { id: 772, label: "AC power lost" }, + { id: 773, label: "AC power restored" }, +]; + +const ZONE_STATE_LABELS = ["secure", "not ready", "trouble", "tamper"]; + +export function decodeEventId(eventId: number): DecodedEvent { + // FIXED first — the bit patterns below would otherwise collapse + // 768..773 into the "zone state change" category since their top + // bits look the same. + if (FIXED_EVENTS.some((f) => f.id === eventId)) { + return { category: "fixed", fixedId: eventId }; + } + if ((eventId & 0xFF00) === 0x0000) { + return { category: "button", button: eventId & 0xFF }; + } + if ((eventId & 0xFC00) === 0x0400) { + const zs = eventId & 0x03FF; + return { + category: "zone", + zone: Math.floor(zs / 4) + 1, + zoneState: zs % 4, + }; + } + if ((eventId & 0xFC00) === 0x0800) { + const us = eventId & 0x03FF; + return { + category: "unit", + unit: Math.floor(us / 2) + 1, + unitOn: (us & 1) === 1, + }; + } + return { category: "raw", raw: eventId }; +} + +export function encodeEventId(ev: DecodedEvent): number { + switch (ev.category) { + case "button": + return (ev.button ?? 1) & 0xFF; + case "zone": { + const zone = (ev.zone ?? 1) - 1; + const state = (ev.zoneState ?? 0) & 0x03; + return 0x0400 | ((zone * 4 + state) & 0x03FF); + } + case "unit": { + const unit = (ev.unit ?? 1) - 1; + const on = ev.unitOn ? 1 : 0; + return 0x0800 | ((unit * 2 + on) & 0x03FF); + } + case "fixed": + return ev.fixedId ?? 768; + case "raw": + default: + return ev.raw ?? 0; + } +} + +export function eventIdFromFields(fields: ProgramFields): number { + return ((fields.month ?? 0) << 8) | (fields.day ?? 0); +} + +export function packEventIdIntoFields( + fields: ProgramFields, eventId: number, +): ProgramFields { + return { + ...fields, + month: (eventId >> 8) & 0xFF, + day: eventId & 0xFF, + }; +} + +export function zoneStateLabel(state: number): string { + return ZONE_STATE_LABELS[state] ?? `state ${state}`; +} + + +// Month abbreviations for the YEARLY editor. +export const MONTH_NAMES = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", +]; + /** HA's hass object — minimal surface we use. */ export interface Hass { connection: { diff --git a/custom_components/omni_pca/websocket.py b/custom_components/omni_pca/websocket.py index ec468a8..f628801 100644 --- a/custom_components/omni_pca/websocket.py +++ b/custom_components/omni_pca/websocket.py @@ -378,9 +378,34 @@ async def _ws_get_program( "trigger_type": _classify_trigger(target), "tokens": _tokens_to_json(tokens), "references": _extract_references(tokens), + # Raw program fields for the editor to seed its form. The + # rendered token stream is for *display*; the form needs the + # underlying integer values to round-trip cleanly. + "fields": _program_to_fields(target), }) +def _program_to_fields(program: Program) -> dict[str, Any]: + """Serialise a Program for the editor form. Mirrors the field + layout of :func:`_PROGRAM_FIELD_SCHEMA` so a round-trip + fetch → edit → save is straightforward. + """ + return { + "prog_type": program.prog_type, + "cond": program.cond, + "cond2": program.cond2, + "cmd": program.cmd, + "par": program.par, + "pr2": program.pr2, + "month": program.month, + "day": program.day, + "days": program.days, + "hour": program.hour, + "minute": program.minute, + "remark_id": program.remark_id, + } + + _PROGRAM_FIELD_SCHEMA = vol.Schema( { vol.Required("prog_type"): vol.All(int, vol.Range(min=0, max=10)), diff --git a/custom_components/omni_pca/www/panel.js b/custom_components/omni_pca/www/panel.js index 781335d..040000a 100644 --- a/custom_components/omni_pca/www/panel.js +++ b/custom_components/omni_pca/www/panel.js @@ -1,33 +1,33 @@ // omni_pca side panel — generated by frontend/build.mjs. Edit src/, not this file. -var Ee=Object.defineProperty;var Se=Object.getOwnPropertyDescriptor;var h=(s,e,t,r)=>{for(var i=r>1?void 0:r?Se(e,t):e,o=s.length-1,n;o>=0;o--)(n=s[o])&&(i=(r?n(e,t,i):n(i))||i);return r&&i&&Ee(e,t,i),i};var O=globalThis,z=O.ShadowRoot&&(O.ShadyCSS===void 0||O.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,B=Symbol(),ne=new WeakMap,T=class{constructor(e,t,r){if(this._$cssResult$=!0,r!==B)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o,t=this.t;if(z&&e===void 0){let r=t!==void 0&&t.length===1;r&&(e=ne.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),r&&ne.set(t,e))}return e}toString(){return this.cssText}},ae=s=>new T(typeof s=="string"?s:s+"",void 0,B),V=(s,...e)=>{let t=s.length===1?s[0]:e.reduce((r,i,o)=>r+(n=>{if(n._$cssResult$===!0)return n.cssText;if(typeof n=="number")return n;throw Error("Value passed to 'css' function must be a 'css' function result: "+n+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+s[o+1],s[0]);return new T(t,s,B)},le=(s,e)=>{if(z)s.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(let t of e){let r=document.createElement("style"),i=O.litNonce;i!==void 0&&r.setAttribute("nonce",i),r.textContent=t.cssText,s.appendChild(r)}},Y=z?s=>s:s=>s instanceof CSSStyleSheet?(e=>{let t="";for(let r of e.cssRules)t+=r.cssText;return ae(t)})(s):s;var{is:Te,defineProperty:Ce,getOwnPropertyDescriptor:Re,getOwnPropertyNames:Pe,getOwnPropertySymbols:Me,getPrototypeOf:De}=Object,j=globalThis,ce=j.trustedTypes,Ie=ce?ce.emptyScript:"",Fe=j.reactiveElementPolyfillSupport,C=(s,e)=>s,R={toAttribute(s,e){switch(e){case Boolean:s=s?Ie:null;break;case Object:case Array:s=s==null?s:JSON.stringify(s)}return s},fromAttribute(s,e){let t=s;switch(e){case Boolean:t=s!==null;break;case Number:t=s===null?null:Number(s);break;case Object:case Array:try{t=JSON.parse(s)}catch{t=null}}return t}},H=(s,e)=>!Te(s,e),de={attribute:!0,type:String,converter:R,reflect:!1,useDefault:!1,hasChanged:H};Symbol.metadata??=Symbol("metadata"),j.litPropertyMetadata??=new WeakMap;var b=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=de){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){let r=Symbol(),i=this.getPropertyDescriptor(e,r,t);i!==void 0&&Ce(this.prototype,e,i)}}static getPropertyDescriptor(e,t,r){let{get:i,set:o}=Re(this.prototype,e)??{get(){return this[t]},set(n){this[t]=n}};return{get:i,set(n){let a=i?.call(this);o?.call(this,n),this.requestUpdate(e,a,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??de}static _$Ei(){if(this.hasOwnProperty(C("elementProperties")))return;let e=De(this);e.finalize(),e.l!==void 0&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(C("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(C("properties"))){let t=this.properties,r=[...Pe(t),...Me(t)];for(let i of r)this.createProperty(i,t[i])}let e=this[Symbol.metadata];if(e!==null){let t=litPropertyMetadata.get(e);if(t!==void 0)for(let[r,i]of t)this.elementProperties.set(r,i)}this._$Eh=new Map;for(let[t,r]of this.elementProperties){let i=this._$Eu(t,r);i!==void 0&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){let t=[];if(Array.isArray(e)){let r=new Set(e.flat(1/0).reverse());for(let i of r)t.unshift(Y(i))}else e!==void 0&&t.push(Y(e));return t}static _$Eu(e,t){let r=t.attribute;return r===!1?void 0:typeof r=="string"?r:typeof e=="string"?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),this.renderRoot!==void 0&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){let e=new Map,t=this.constructor.elementProperties;for(let r of t.keys())this.hasOwnProperty(r)&&(e.set(r,this[r]),delete this[r]);e.size>0&&(this._$Ep=e)}createRenderRoot(){let e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return le(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,r){this._$AK(e,r)}_$ET(e,t){let r=this.constructor.elementProperties.get(e),i=this.constructor._$Eu(e,r);if(i!==void 0&&r.reflect===!0){let o=(r.converter?.toAttribute!==void 0?r.converter:R).toAttribute(t,r.type);this._$Em=e,o==null?this.removeAttribute(i):this.setAttribute(i,o),this._$Em=null}}_$AK(e,t){let r=this.constructor,i=r._$Eh.get(e);if(i!==void 0&&this._$Em!==i){let o=r.getPropertyOptions(i),n=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:R;this._$Em=i;let a=n.fromAttribute(t,o.type);this[i]=a??this._$Ej?.get(i)??a,this._$Em=null}}requestUpdate(e,t,r,i=!1,o){if(e!==void 0){let n=this.constructor;if(i===!1&&(o=this[e]),r??=n.getPropertyOptions(e),!((r.hasChanged??H)(o,t)||r.useDefault&&r.reflect&&o===this._$Ej?.get(e)&&!this.hasAttribute(n._$Eu(e,r))))return;this.C(e,t,r)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(e,t,{useDefault:r,reflect:i,wrapped:o},n){r&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,n??t??this[e]),o!==!0||n!==void 0)||(this._$AL.has(e)||(this.hasUpdated||r||(t=void 0),this._$AL.set(e,t)),i===!0&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}let e=this.scheduleUpdate();return e!=null&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[i,o]of this._$Ep)this[i]=o;this._$Ep=void 0}let r=this.constructor.elementProperties;if(r.size>0)for(let[i,o]of r){let{wrapped:n}=o,a=this[i];n!==!0||this._$AL.has(i)||a===void 0||this.C(i,void 0,o,a)}}let e=!1,t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(r=>r.hostUpdate?.()),this.update(t)):this._$EM()}catch(r){throw e=!1,this._$EM(),r}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(e){}firstUpdated(e){}};b.elementStyles=[],b.shadowRootOptions={mode:"open"},b[C("elementProperties")]=new Map,b[C("finalized")]=new Map,Fe?.({ReactiveElement:b}),(j.reactiveElementVersions??=[]).push("2.1.2");var X=globalThis,pe=s=>s,U=X.trustedTypes,he=U?U.createPolicy("lit-html",{createHTML:s=>s}):void 0,be="$lit$",y=`lit$${Math.random().toFixed(9).slice(2)}$`,ve="?"+y,Ne=`<${ve}>`,w=document,M=()=>w.createComment(""),D=s=>s===null||typeof s!="object"&&typeof s!="function",ee=Array.isArray,Le=s=>ee(s)||typeof s?.[Symbol.iterator]=="function",W=`[ -\f\r]`,P=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,ue=/-->/g,fe=/>/g,x=RegExp(`>|${W}(?:([^\\s"'>=/]+)(${W}*=${W}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`,"g"),me=/'/g,ge=/"/g,ye=/^(?:script|style|textarea|title)$/i,te=s=>(e,...t)=>({_$litType$:s,strings:e,values:t}),l=te(1),Ze=te(2),Qe=te(3),A=Symbol.for("lit-noChange"),m=Symbol.for("lit-nothing"),_e=new WeakMap,k=w.createTreeWalker(w,129);function $e(s,e){if(!ee(s)||!s.hasOwnProperty("raw"))throw Error("invalid template strings array");return he!==void 0?he.createHTML(e):e}var Oe=(s,e)=>{let t=s.length-1,r=[],i,o=e===2?"":e===3?"":"",n=P;for(let a=0;a"?(n=i??P,p=-1):g[1]===void 0?p=-2:(p=n.lastIndex-g[2].length,f=g[1],n=g[3]===void 0?x:g[3]==='"'?ge:me):n===ge||n===me?n=x:n===ue||n===fe?n=P:(n=x,i=void 0);let v=n===x&&s[a+1].startsWith("/>")?" ":"";o+=n===P?c+Ne:p>=0?(r.push(f),c.slice(0,p)+be+c.slice(p)+y+v):c+y+(p===-2?a:v)}return[$e(s,o+(s[t]||"")+(e===2?"":e===3?"":"")),r]},I=class s{constructor({strings:e,_$litType$:t},r){let i;this.parts=[];let o=0,n=0,a=e.length-1,c=this.parts,[f,g]=Oe(e,t);if(this.el=s.createElement(f,r),k.currentNode=this.el.content,t===2||t===3){let p=this.el.content.firstChild;p.replaceWith(...p.childNodes)}for(;(i=k.nextNode())!==null&&c.length0){i.textContent=U?U.emptyScript:"";for(let v=0;v<_;v++)i.append(p[v],M()),k.nextNode(),c.push({type:2,index:++o});i.append(p[_],M())}}}else if(i.nodeType===8)if(i.data===ve)c.push({type:2,index:o});else{let p=-1;for(;(p=i.data.indexOf(y,p+1))!==-1;)c.push({type:7,index:o}),p+=y.length-1}o++}}static createElement(e,t){let r=w.createElement("template");return r.innerHTML=e,r}};function E(s,e,t=s,r){if(e===A)return e;let i=r!==void 0?t._$Co?.[r]:t._$Cl,o=D(e)?void 0:e._$litDirective$;return i?.constructor!==o&&(i?._$AO?.(!1),o===void 0?i=void 0:(i=new o(s),i._$AT(s,t,r)),r!==void 0?(t._$Co??=[])[r]=i:t._$Cl=i),i!==void 0&&(e=E(s,i._$AS(s,e.values),i,r)),e}var G=class{constructor(e,t){this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=t}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(e){let{el:{content:t},parts:r}=this._$AD,i=(e?.creationScope??w).importNode(t,!0);k.currentNode=i;let o=k.nextNode(),n=0,a=0,c=r[0];for(;c!==void 0;){if(n===c.index){let f;c.type===2?f=new F(o,o.nextSibling,this,e):c.type===1?f=new c.ctor(o,c.name,c.strings,this,e):c.type===6&&(f=new Q(o,this,e)),this._$AV.push(f),c=r[++a]}n!==c?.index&&(o=k.nextNode(),n++)}return k.currentNode=w,i}p(e){let t=0;for(let r of this._$AV)r!==void 0&&(r.strings!==void 0?(r._$AI(e,r,t),t+=r.strings.length-2):r._$AI(e[t])),t++}},F=class s{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(e,t,r,i){this.type=2,this._$AH=m,this._$AN=void 0,this._$AA=e,this._$AB=t,this._$AM=r,this.options=i,this._$Cv=i?.isConnected??!0}get parentNode(){let e=this._$AA.parentNode,t=this._$AM;return t!==void 0&&e?.nodeType===11&&(e=t.parentNode),e}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(e,t=this){e=E(this,e,t),D(e)?e===m||e==null||e===""?(this._$AH!==m&&this._$AR(),this._$AH=m):e!==this._$AH&&e!==A&&this._(e):e._$litType$!==void 0?this.$(e):e.nodeType!==void 0?this.T(e):Le(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==m&&D(this._$AH)?this._$AA.nextSibling.data=e:this.T(w.createTextNode(e)),this._$AH=e}$(e){let{values:t,_$litType$:r}=e,i=typeof r=="number"?this._$AC(e):(r.el===void 0&&(r.el=I.createElement($e(r.h,r.h[0]),this.options)),r);if(this._$AH?._$AD===i)this._$AH.p(t);else{let o=new G(i,this),n=o.u(this.options);o.p(t),this.T(n),this._$AH=o}}_$AC(e){let t=_e.get(e.strings);return t===void 0&&_e.set(e.strings,t=new I(e)),t}k(e){ee(this._$AH)||(this._$AH=[],this._$AR());let t=this._$AH,r,i=0;for(let o of e)i===t.length?t.push(r=new s(this.O(M()),this.O(M()),this,this.options)):r=t[i],r._$AI(o),i++;i2||r[0]!==""||r[1]!==""?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=m}_$AI(e,t=this,r,i){let o=this.strings,n=!1;if(o===void 0)e=E(this,e,t,0),n=!D(e)||e!==this._$AH&&e!==A,n&&(this._$AH=e);else{let a=e,c,f;for(e=o[0],c=0;c{let r=t?.renderBefore??e,i=r._$litPart$;if(i===void 0){let o=t?.renderBefore??null;r._$litPart$=i=new F(e.insertBefore(M(),o),o,void 0,t??{})}return i._$AI(s),i};var re=globalThis,$=class extends b{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){let t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=xe(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return A}};$._$litElement$=!0,$.finalized=!0,re.litElementHydrateSupport?.({LitElement:$});var je=re.litElementPolyfillSupport;je?.({LitElement:$});(re.litElementVersions??=[]).push("4.2.2");var ke=s=>(e,t)=>{t!==void 0?t.addInitializer(()=>{customElements.define(s,e)}):customElements.define(s,e)};var He={attribute:!0,type:String,converter:R,reflect:!1,hasChanged:H},Ue=(s=He,e,t)=>{let{kind:r,metadata:i}=t,o=globalThis.litPropertyMetadata.get(i);if(o===void 0&&globalThis.litPropertyMetadata.set(i,o=new Map),r==="setter"&&((s=Object.create(s)).wrapped=!0),o.set(t.name,s),r==="accessor"){let{name:n}=t;return{set(a){let c=e.get.call(this);e.set.call(this,a),this.requestUpdate(n,c,s,!0,a)},init(a){return a!==void 0&&this.C(n,void 0,s,a),a}}}if(r==="setter"){let{name:n}=t;return function(a){let c=this[n];e.call(this,a),this.requestUpdate(n,c,s,!0,a)}}throw Error("Unsupported decorator location: "+r)};function N(s){return(e,t)=>typeof t=="object"?Ue(s,e,t):((r,i,o)=>{let n=i.hasOwnProperty(o);return i.constructor.createProperty(o,r),n?Object.getOwnPropertyDescriptor(i,o):void 0})(s,e,t)}function u(s){return N({...s,state:!0,attribute:!1})}function ie(s,e){return l`${s.map(t=>qe(t,e))}`}function qe(s,e){switch(s.k){case"newline":return l`
`;case"indent":return l`${s.t}`;case"keyword":return l`${s.t}`;case"operator":return l`${s.t}`;case"value":return l`${s.t}`;case"ref":{let t=e&&s.ek&&typeof s.ei=="number"?()=>e(s.ek,s.ei):void 0;return l``}default:return l`${s.t}`}}var se=[{value:0,label:"Turn OFF unit",ref_kind:"unit"},{value:1,label:"Turn ON unit",ref_kind:"unit"},{value:2,label:"All OFF",ref_kind:null},{value:3,label:"All ON",ref_kind:null},{value:4,label:"Bypass zone",ref_kind:"zone"},{value:5,label:"Restore zone",ref_kind:"zone"},{value:7,label:"Execute button",ref_kind:"button"},{value:9,label:"Set unit level %",ref_kind:"unit"},{value:48,label:"Disarm area",ref_kind:"area"},{value:49,label:"Arm area Day",ref_kind:"area"},{value:50,label:"Arm area Night",ref_kind:"area"},{value:51,label:"Arm area Away",ref_kind:"area"},{value:52,label:"Arm area Vacation",ref_kind:"area"}];function oe(s){return se.find(e=>e.value===s)}var we=[{bit:2,label:"Mon"},{bit:4,label:"Tue"},{bit:8,label:"Wed"},{bit:16,label:"Thu"},{bit:32,label:"Fri"},{bit:64,label:"Sat"},{bit:128,label:"Sun"}],Ae=1;var Be=["TIMED","EVENT","YEARLY","WHEN","AT","EVERY","REMARK"],Ve=5e3,d=class extends ${constructor(){super(...arguments);this.narrow=!1;this._entryId=null;this._rows=[];this._total=0;this._filteredTotal=0;this._loading=!1;this._error=null;this._activeTriggerTypes=new Set;this._referenceFilter=null;this._searchTerm="";this._selectedSlot=null;this._detail=null;this._detailLoading=!1;this._fireFeedback=null;this._writeFeedback=null;this._cloneTargetSlot="";this._showCloneInput=!1;this._confirmingClear=!1;this._editingDraft=null;this._objects=null;this._refreshTimer=null}connectedCallback(){super.connectedCallback(),this._discoverEntry(),this._entryId&&(this._loadList(),this._startRefreshTimer())}disconnectedCallback(){super.disconnectedCallback(),this._stopRefreshTimer()}updated(t){t.has("hass")&&this._entryId===null&&(this._discoverEntry(),this._entryId&&(this._loadList(),this._startRefreshTimer()))}_discoverEntry(){this.hass?.connection&&this._discoverViaList()}async _discoverViaList(){try{let r=(await this.hass.connection.sendMessagePromise({type:"config_entries/get"})).filter(i=>i.domain==="omni_pca");if(r.length===0){this._error="No Omni panel configured. Add one via Settings \u2192 Devices & Services.";return}this._entryId=r[0].entry_id,this._error=null}catch(t){this._error=`Could not discover panels: ${t instanceof Error?t.message:String(t)}`}}async _loadList(){if(this._entryId){this._loading=!0,this._error=null;try{let t={type:"omni_pca/programs/list",entry_id:this._entryId};this._activeTriggerTypes.size>0&&(t.trigger_types=[...this._activeTriggerTypes]),this._referenceFilter&&(t.references_entity=this._referenceFilter),this._searchTerm&&(t.search=this._searchTerm);let r=await this.hass.connection.sendMessagePromise(t);this._rows=r.programs,this._total=r.total,this._filteredTotal=r.filtered_total}catch(t){this._error=t instanceof Error?t.message:String(t)}finally{this._loading=!1}}}async _loadDetail(t){if(this._entryId){this._detailLoading=!0,this._detail=null;try{this._detail=await this.hass.connection.sendMessagePromise({type:"omni_pca/programs/get",entry_id:this._entryId,slot:t})}catch(r){this._error=r instanceof Error?r.message:String(r)}finally{this._detailLoading=!1}}}async _fireProgram(t){if(this._entryId){this._fireFeedback="firing\u2026";try{await this.hass.connection.sendMessagePromise({type:"omni_pca/programs/fire",entry_id:this._entryId,slot:t}),this._fireFeedback=`fired slot ${t}`}catch(r){this._fireFeedback=`error: ${r instanceof Error?r.message:r}`}setTimeout(()=>{this._fireFeedback=null},4e3)}}async _clearProgram(t){if(this._entryId){this._writeFeedback="clearing\u2026";try{await this.hass.connection.sendMessagePromise({type:"omni_pca/programs/clear",entry_id:this._entryId,slot:t}),this._writeFeedback=`cleared slot ${t}`,this._confirmingClear=!1,this._selectedSlot=null,this._detail=null,await this._loadList()}catch(r){let i=r instanceof Error?r.message:String(r);this._writeFeedback=`error: ${i}`}setTimeout(()=>{this._writeFeedback=null},4e3)}}async _cloneProgram(t){if(!this._entryId)return;let r=this._cloneTargetSlot.trim(),i=parseInt(r,10);if(!Number.isFinite(i)||i<1||i>1500){this._writeFeedback="target slot must be 1..1500",setTimeout(()=>{this._writeFeedback=null},4e3);return}if(i===t){this._writeFeedback="target must differ from source",setTimeout(()=>{this._writeFeedback=null},4e3);return}this._writeFeedback="cloning\u2026";try{await this.hass.connection.sendMessagePromise({type:"omni_pca/programs/clone",entry_id:this._entryId,source_slot:t,target_slot:i}),this._writeFeedback=`cloned to slot ${i}`,this._showCloneInput=!1,this._cloneTargetSlot="",this._selectedSlot=i,await this._loadList(),await this._loadDetail(i)}catch(o){let n=o instanceof Error?o.message:String(o);this._writeFeedback=`error: ${n}`}setTimeout(()=>{this._writeFeedback=null},4e3)}_onCloneTargetInput(t){this._cloneTargetSlot=t.target.value}async _ensureObjectsLoaded(){if(!(this._objects!==null||!this._entryId))try{this._objects=await this.hass.connection.sendMessagePromise({type:"omni_pca/objects/list",entry_id:this._entryId})}catch(t){let r=t instanceof Error?t.message:String(t);console.warn("omni_pca: objects/list failed",r)}}async _beginEdit(){if(!this._detail||this._detail.kind!=="compact"||this._detail.trigger_type!=="TIMED"||(await this._ensureObjectsLoaded(),!this._entryId))return;let t=await this._fetchProgramFields(this._entryId,this._detail.slot);t!==null&&(this._editingDraft=t,this._stopRefreshTimer())}async _fetchProgramFields(t,r){let i=this._objects?.units?.[0]?.index??1;return{prog_type:Ae,cmd:1,par:0,pr2:i,hour:6,minute:0,days:62,cond:0,cond2:0,month:0,day:0}}async _saveDraft(){if(!(!this._editingDraft||!this._detail||!this._entryId)){this._writeFeedback="saving\u2026";try{await this.hass.connection.sendMessagePromise({type:"omni_pca/programs/write",entry_id:this._entryId,slot:this._detail.slot,program:this._editingDraft}),this._writeFeedback=`saved slot ${this._detail.slot}`,this._editingDraft=null,this._startRefreshTimer(),await this._loadList(),await this._loadDetail(this._detail.slot)}catch(t){let r=t instanceof Error?t.message:String(t);this._writeFeedback=`error: ${r}`}setTimeout(()=>{this._writeFeedback=null},4e3)}}_cancelEdit(){this._editingDraft=null,this._startRefreshTimer()}_patchDraft(t){this._editingDraft&&(this._editingDraft={...this._editingDraft,...t})}_toggleDayBit(t){if(!this._editingDraft)return;let i=(this._editingDraft.days??0)^t;this._patchDraft({days:i})}_onCommandChange(t){let r=parseInt(t.target.value,10);if(!Number.isFinite(r))return;let i=oe(r),o=this._editingDraft?.pr2??0;if(i?.ref_kind&&this._objects){let n=this._pickBucket(i.ref_kind);n&&n.length>0&&!n.some(a=>a.index===o)&&(o=n[0].index)}else i?.ref_kind||(o=0);this._patchDraft({cmd:r,pr2:o})}_pickBucket(t){if(!this._objects)return null;switch(t){case"zone":return this._objects.zones;case"unit":return this._objects.units;case"area":return this._objects.areas;case"button":return this._objects.buttons;default:return null}}_onObjectChange(t){let r=parseInt(t.target.value,10);Number.isFinite(r)&&this._patchDraft({pr2:r})}_onHourChange(t){let r=parseInt(t.target.value,10);Number.isFinite(r)&&r>=0&&r<=23&&this._patchDraft({hour:r})}_onMinuteChange(t){let r=parseInt(t.target.value,10);Number.isFinite(r)&&r>=0&&r<=59&&this._patchDraft({minute:r})}_onParChange(t){let r=parseInt(t.target.value,10);Number.isFinite(r)&&r>=0&&r<=255&&this._patchDraft({par:r})}_startRefreshTimer(){this._refreshTimer===null&&(this._refreshTimer=window.setInterval(()=>{this._loadList(),this._selectedSlot!==null&&this._loadDetail(this._selectedSlot)},Ve))}_stopRefreshTimer(){this._refreshTimer!==null&&(window.clearInterval(this._refreshTimer),this._refreshTimer=null)}_toggleTriggerFilter(t){let r=new Set(this._activeTriggerTypes);r.has(t)?r.delete(t):r.add(t),this._activeTriggerTypes=r,this._loadList()}_onSearchInput(t){this._searchTerm=t.target.value,this._loadList()}_clearReferenceFilter(){this._referenceFilter=null,this._loadList()}_onRowClick(t){this._selectedSlot=t,this._loadDetail(t)}_onRefClick(t,r){this._referenceFilter=`${t}:${r}`,this._selectedSlot=null,this._detail=null,this._loadList()}_closeDetail(){this._selectedSlot=null,this._detail=null}render(){return l` + ${n.t} + ${n.s?a`${n.s}`:""} + `}default:return a`${n.t}`}}var oe=[{value:0,label:"Turn OFF unit",ref_kind:"unit"},{value:1,label:"Turn ON unit",ref_kind:"unit"},{value:2,label:"All OFF",ref_kind:null},{value:3,label:"All ON",ref_kind:null},{value:4,label:"Bypass zone",ref_kind:"zone"},{value:5,label:"Restore zone",ref_kind:"zone"},{value:7,label:"Execute button",ref_kind:"button"},{value:9,label:"Set unit level %",ref_kind:"unit"},{value:48,label:"Disarm area",ref_kind:"area"},{value:49,label:"Arm area Day",ref_kind:"area"},{value:50,label:"Arm area Night",ref_kind:"area"},{value:51,label:"Arm area Away",ref_kind:"area"},{value:52,label:"Arm area Vacation",ref_kind:"area"}];function ae(n){return oe.find(t=>t.value===n)}var Fe=[{bit:2,label:"Mon"},{bit:4,label:"Tue"},{bit:8,label:"Wed"},{bit:16,label:"Thu"},{bit:32,label:"Fri"},{bit:64,label:"Sat"},{bit:128,label:"Sun"}],le=1,ce=2,de=3;var pe=[{id:768,label:"Phone line dead"},{id:769,label:"Phone ringing"},{id:770,label:"Phone off hook"},{id:771,label:"Phone on hook"},{id:772,label:"AC power lost"},{id:773,label:"AC power restored"}];function T(n){if(pe.some(t=>t.id===n))return{category:"fixed",fixedId:n};if(!(n&65280))return{category:"button",button:n&255};if((n&64512)===1024){let t=n&1023;return{category:"zone",zone:Math.floor(t/4)+1,zoneState:t%4}}if((n&64512)===2048){let t=n&1023;return{category:"unit",unit:Math.floor(t/2)+1,unitOn:(t&1)===1}}return{category:"raw",raw:n}}function Re(n){switch(n.category){case"button":return(n.button??1)&255;case"zone":{let t=(n.zone??1)-1,e=(n.zoneState??0)&3;return 1024|t*4+e&1023}case"unit":{let t=(n.unit??1)-1,e=n.unitOn?1:0;return 2048|t*2+e&1023}case"fixed":return n.fixedId??768;case"raw":default:return n.raw??0}}function C(n){return(n.month??0)<<8|(n.day??0)}function De(n,t){return{...n,month:t>>8&255,day:t&255}}var Me=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];var Pe=new Set(["TIMED","EVENT","YEARLY"]),Qe=["TIMED","EVENT","YEARLY","WHEN","AT","EVERY","REMARK"],et=5e3,d=class extends ${constructor(){super(...arguments);this.narrow=!1;this._entryId=null;this._rows=[];this._total=0;this._filteredTotal=0;this._loading=!1;this._error=null;this._activeTriggerTypes=new Set;this._referenceFilter=null;this._searchTerm="";this._selectedSlot=null;this._detail=null;this._detailLoading=!1;this._fireFeedback=null;this._writeFeedback=null;this._cloneTargetSlot="";this._showCloneInput=!1;this._confirmingClear=!1;this._editingDraft=null;this._objects=null;this._refreshTimer=null}connectedCallback(){super.connectedCallback(),this._discoverEntry(),this._entryId&&(this._loadList(),this._startRefreshTimer())}disconnectedCallback(){super.disconnectedCallback(),this._stopRefreshTimer()}updated(e){e.has("hass")&&this._entryId===null&&(this._discoverEntry(),this._entryId&&(this._loadList(),this._startRefreshTimer()))}_discoverEntry(){this.hass?.connection&&this._discoverViaList()}async _discoverViaList(){try{let r=(await this.hass.connection.sendMessagePromise({type:"config_entries/get"})).filter(i=>i.domain==="omni_pca");if(r.length===0){this._error="No Omni panel configured. Add one via Settings \u2192 Devices & Services.";return}this._entryId=r[0].entry_id,this._error=null}catch(e){this._error=`Could not discover panels: ${e instanceof Error?e.message:String(e)}`}}async _loadList(){if(this._entryId){this._loading=!0,this._error=null;try{let e={type:"omni_pca/programs/list",entry_id:this._entryId};this._activeTriggerTypes.size>0&&(e.trigger_types=[...this._activeTriggerTypes]),this._referenceFilter&&(e.references_entity=this._referenceFilter),this._searchTerm&&(e.search=this._searchTerm);let r=await this.hass.connection.sendMessagePromise(e);this._rows=r.programs,this._total=r.total,this._filteredTotal=r.filtered_total}catch(e){this._error=e instanceof Error?e.message:String(e)}finally{this._loading=!1}}}async _loadDetail(e){if(this._entryId){this._detailLoading=!0,this._detail=null;try{this._detail=await this.hass.connection.sendMessagePromise({type:"omni_pca/programs/get",entry_id:this._entryId,slot:e})}catch(r){this._error=r instanceof Error?r.message:String(r)}finally{this._detailLoading=!1}}}async _fireProgram(e){if(this._entryId){this._fireFeedback="firing\u2026";try{await this.hass.connection.sendMessagePromise({type:"omni_pca/programs/fire",entry_id:this._entryId,slot:e}),this._fireFeedback=`fired slot ${e}`}catch(r){this._fireFeedback=`error: ${r instanceof Error?r.message:r}`}setTimeout(()=>{this._fireFeedback=null},4e3)}}async _clearProgram(e){if(this._entryId){this._writeFeedback="clearing\u2026";try{await this.hass.connection.sendMessagePromise({type:"omni_pca/programs/clear",entry_id:this._entryId,slot:e}),this._writeFeedback=`cleared slot ${e}`,this._confirmingClear=!1,this._selectedSlot=null,this._detail=null,await this._loadList()}catch(r){let i=r instanceof Error?r.message:String(r);this._writeFeedback=`error: ${i}`}setTimeout(()=>{this._writeFeedback=null},4e3)}}async _cloneProgram(e){if(!this._entryId)return;let r=this._cloneTargetSlot.trim(),i=parseInt(r,10);if(!Number.isFinite(i)||i<1||i>1500){this._writeFeedback="target slot must be 1..1500",setTimeout(()=>{this._writeFeedback=null},4e3);return}if(i===e){this._writeFeedback="target must differ from source",setTimeout(()=>{this._writeFeedback=null},4e3);return}this._writeFeedback="cloning\u2026";try{await this.hass.connection.sendMessagePromise({type:"omni_pca/programs/clone",entry_id:this._entryId,source_slot:e,target_slot:i}),this._writeFeedback=`cloned to slot ${i}`,this._showCloneInput=!1,this._cloneTargetSlot="",this._selectedSlot=i,await this._loadList(),await this._loadDetail(i)}catch(s){let o=s instanceof Error?s.message:String(s);this._writeFeedback=`error: ${o}`}setTimeout(()=>{this._writeFeedback=null},4e3)}_onCloneTargetInput(e){this._cloneTargetSlot=e.target.value}async _ensureObjectsLoaded(){if(!(this._objects!==null||!this._entryId))try{this._objects=await this.hass.connection.sendMessagePromise({type:"omni_pca/objects/list",entry_id:this._entryId})}catch(e){let r=e instanceof Error?e.message:String(e);console.warn("omni_pca: objects/list failed",r)}}async _beginEdit(){if(!this._detail||this._detail.kind!=="compact"||!Pe.has(this._detail.trigger_type)||(await this._ensureObjectsLoaded(),!this._entryId))return;let e=this._detail.fields??this._defaultFieldsForType(this._detail.trigger_type);e!==null&&(this._editingDraft={...e},this._stopRefreshTimer())}_defaultFieldsForType(e){let r=this._objects?.units?.[0]?.index??1;if(e==="TIMED")return{prog_type:le,cmd:1,par:0,pr2:r,hour:6,minute:0,days:62,cond:0,cond2:0,month:0,day:0};if(e==="EVENT"){let i=this._objects?.buttons?.[0]?.index??1;return{prog_type:ce,cmd:1,par:0,pr2:r,month:0,day:i&255,hour:0,minute:0,days:0,cond:0,cond2:0}}return e==="YEARLY"?{prog_type:de,cmd:1,par:0,pr2:r,month:1,day:1,hour:0,minute:0,days:0,cond:0,cond2:0}:null}async _saveDraft(){if(!(!this._editingDraft||!this._detail||!this._entryId)){this._writeFeedback="saving\u2026";try{await this.hass.connection.sendMessagePromise({type:"omni_pca/programs/write",entry_id:this._entryId,slot:this._detail.slot,program:this._editingDraft}),this._writeFeedback=`saved slot ${this._detail.slot}`,this._editingDraft=null,this._startRefreshTimer(),await this._loadList(),await this._loadDetail(this._detail.slot)}catch(e){let r=e instanceof Error?e.message:String(e);this._writeFeedback=`error: ${r}`}setTimeout(()=>{this._writeFeedback=null},4e3)}}_cancelEdit(){this._editingDraft=null,this._startRefreshTimer()}_patchDraft(e){this._editingDraft&&(this._editingDraft={...this._editingDraft,...e})}_toggleDayBit(e){if(!this._editingDraft)return;let i=(this._editingDraft.days??0)^e;this._patchDraft({days:i})}_onCommandChange(e){let r=parseInt(e.target.value,10);if(!Number.isFinite(r))return;let i=ae(r),s=this._editingDraft?.pr2??0;if(i?.ref_kind&&this._objects){let o=this._pickBucket(i.ref_kind);o&&o.length>0&&!o.some(c=>c.index===s)&&(s=o[0].index)}else i?.ref_kind||(s=0);this._patchDraft({cmd:r,pr2:s})}_pickBucket(e){if(!this._objects)return null;switch(e){case"zone":return this._objects.zones;case"unit":return this._objects.units;case"area":return this._objects.areas;case"button":return this._objects.buttons;default:return null}}_onObjectChange(e){let r=parseInt(e.target.value,10);Number.isFinite(r)&&this._patchDraft({pr2:r})}_onHourChange(e){let r=parseInt(e.target.value,10);Number.isFinite(r)&&r>=0&&r<=23&&this._patchDraft({hour:r})}_onMinuteChange(e){let r=parseInt(e.target.value,10);Number.isFinite(r)&&r>=0&&r<=59&&this._patchDraft({minute:r})}_onParChange(e){let r=parseInt(e.target.value,10);Number.isFinite(r)&&r>=0&&r<=255&&this._patchDraft({par:r})}_onMonthChange(e){let r=parseInt(e.target.value,10);Number.isFinite(r)&&r>=1&&r<=12&&this._patchDraft({month:r})}_onDayChange(e){let r=parseInt(e.target.value,10);Number.isFinite(r)&&r>=1&&r<=31&&this._patchDraft({day:r})}_patchEvent(e){if(!this._editingDraft)return;let r=Re(e);this._editingDraft=De(this._editingDraft,r)}_onEventCategoryChange(e){let r=e.target.value;if(r==="button"){let i=this._objects?.buttons?.[0]?.index??1;this._patchEvent({category:"button",button:i})}else if(r==="zone"){let i=this._objects?.zones?.[0]?.index??1;this._patchEvent({category:"zone",zone:i,zoneState:1})}else if(r==="unit"){let i=this._objects?.units?.[0]?.index??1;this._patchEvent({category:"unit",unit:i,unitOn:!0})}else r==="fixed"&&this._patchEvent({category:"fixed",fixedId:772})}_onEventButtonChange(e){let r=parseInt(e.target.value,10);Number.isFinite(r)&&this._patchEvent({category:"button",button:r})}_onEventZoneChange(e){if(!this._editingDraft)return;let r=parseInt(e.target.value,10);if(!Number.isFinite(r))return;let i=T(C(this._editingDraft));this._patchEvent({category:"zone",zone:r,zoneState:i.zoneState??1})}_onEventZoneStateChange(e){if(!this._editingDraft)return;let r=parseInt(e.target.value,10);if(!Number.isFinite(r))return;let i=T(C(this._editingDraft));this._patchEvent({category:"zone",zone:i.zone??1,zoneState:r})}_onEventUnitChange(e){if(!this._editingDraft)return;let r=parseInt(e.target.value,10);if(!Number.isFinite(r))return;let i=T(C(this._editingDraft));this._patchEvent({category:"unit",unit:r,unitOn:i.unitOn??!0})}_onEventUnitOnChange(e){if(!this._editingDraft)return;let r=e.target.value==="1",i=T(C(this._editingDraft));this._patchEvent({category:"unit",unit:i.unit??1,unitOn:r})}_onEventFixedChange(e){let r=parseInt(e.target.value,10);Number.isFinite(r)&&this._patchEvent({category:"fixed",fixedId:r})}_startRefreshTimer(){this._refreshTimer===null&&(this._refreshTimer=window.setInterval(()=>{this._loadList(),this._selectedSlot!==null&&this._loadDetail(this._selectedSlot)},et))}_stopRefreshTimer(){this._refreshTimer!==null&&(window.clearInterval(this._refreshTimer),this._refreshTimer=null)}_toggleTriggerFilter(e){let r=new Set(this._activeTriggerTypes);r.has(e)?r.delete(e):r.add(e),this._activeTriggerTypes=r,this._loadList()}_onSearchInput(e){this._searchTerm=e.target.value,this._loadList()}_clearReferenceFilter(){this._referenceFilter=null,this._loadList()}_onRowClick(e){this._selectedSlot=e,this._loadDetail(e)}_onRefClick(e,r){this._referenceFilter=`${e}:${r}`,this._selectedSlot=null,this._detail=null,this._loadList()}_closeDetail(){this._selectedSlot=null,this._detail=null}render(){return a`
Omni Programs - ${this._total>0?l` + ${this._total>0?a` ${this._filteredTotal===this._total?`${this._total} programs`:`${this._filteredTotal} of ${this._total} shown`} `:""}
- ${this._error?l` + ${this._error?a`
${this._error}
`:""} ${this._renderFilters()}
${this._renderList()} ${this._selectedSlot!==null?this._renderDetail():""}
- `}_renderFilters(){return l` + `}_renderFilters(){return a`
- ${Be.map(t=>l` + ${Qe.map(e=>a` + class="chip ${this._activeTriggerTypes.has(e)?"active":""}" + @click=${()=>this._toggleTriggerFilter(e)} + >${e} `)}
- ${this._referenceFilter?l` + ${this._referenceFilter?a`
filtering on ${this._referenceFilter}
`:""}
- `}_renderList(){return this._loading&&this._rows.length===0?l`
loading…
`:this._rows.length===0?l`
No programs match the current filters.
`:l` + `}_renderList(){return this._loading&&this._rows.length===0?a`
loading…
`:this._rows.length===0?a`
No programs match the current filters.
`:a`
- ${this._rows.map(t=>l` + ${this._rows.map(e=>a`
this._onRowClick(t.slot)} + class="row ${this._selectedSlot===e.slot?"selected":""}" + @click=${()=>this._onRowClick(e.slot)} > -
#${t.slot}
+
#${e.slot}
- ${ie(t.summary,(r,i)=>this._onRefClick(r,i))} + ${se(e.summary,(r,i)=>this._onRefClick(r,i))}
- - ${t.trigger_type} + + ${e.trigger_type} - ${t.condition_count>0?l` - ${t.condition_count} cond`:""} - ${t.action_count>1?l` - ${t.action_count} actions`:""} + ${e.condition_count>0?a` + ${e.condition_count} cond`:""} + ${e.action_count>1?a` + ${e.action_count} actions`:""}
`)}
- `}_renderDetail(){if(this._detailLoading)return l``;if(this._detail===null)return l``;let t=this._detail;return this._editingDraft!==null?this._renderEditor(t):l` + `}_renderDetail(){if(this._detailLoading)return a``;if(this._detail===null)return a``;let e=this._detail;return this._editingDraft!==null?this._renderEditor(e):a` - `}_renderEditor(t){let r=this._editingDraft,i=oe(r.cmd??0),o=i?.ref_kind?this._pickBucket(i.ref_kind):null,n=r.cmd===9;return l` + `}_renderEditor(e){let r=this._editingDraft,i=e.trigger_type;return a` - `}};d.styles=V` + `}_renderTriggerSection(e){switch(e.prog_type){case le:return this._renderTimedTrigger(e);case ce:return this._renderEventTrigger(e);case de:return this._renderYearlyTrigger(e);default:return a`
+ Editing program type ${e.prog_type} is not supported. +
`}}_renderTimedTrigger(e){return a` +
+ Time +
+ + : + +
+
+
+ Days +
+ ${Fe.map(r=>{let i=((e.days??0)&r.bit)!==0;return a` + + `})} +
+
+ `}_renderEventTrigger(e){let r=C(e),i=T(r);return a` +
+ Trigger event + + ${this._renderEventCategoryFields(i)} +
+ `}_renderEventCategoryFields(e){return e.category==="button"?a` + `:e.category==="zone"?a` + + `:e.category==="unit"?a` + + `:e.category==="fixed"?a` + `:a` +
+ Unrecognised event ID. Switch category above to redefine. +
`}_renderYearlyTrigger(e){return a` +
+ Date +
+ + +
+
+
+ Time of day +
+ + : + +
+
+ `}_renderActionSection(e){let r=ae(e.cmd??0),i=r?.ref_kind?this._pickBucket(r.ref_kind):null,s=e.cmd===9;return a` +
+ Action + + ${r?.ref_kind?a` + `:""} + ${s?a` + `:""} +
+ `}};d.styles=G` :host { display: block; min-height: 100vh; @@ -597,7 +739,7 @@ var Ee=Object.defineProperty;var Se=Object.getOwnPropertyDescriptor;var h=(s,e,t font-size: 0.82rem; color: var(--secondary-text-color, #666); } - `,h([N({attribute:!1})],d.prototype,"hass",2),h([N({attribute:!1})],d.prototype,"narrow",2),h([u()],d.prototype,"_entryId",2),h([u()],d.prototype,"_rows",2),h([u()],d.prototype,"_total",2),h([u()],d.prototype,"_filteredTotal",2),h([u()],d.prototype,"_loading",2),h([u()],d.prototype,"_error",2),h([u()],d.prototype,"_activeTriggerTypes",2),h([u()],d.prototype,"_referenceFilter",2),h([u()],d.prototype,"_searchTerm",2),h([u()],d.prototype,"_selectedSlot",2),h([u()],d.prototype,"_detail",2),h([u()],d.prototype,"_detailLoading",2),h([u()],d.prototype,"_fireFeedback",2),h([u()],d.prototype,"_writeFeedback",2),h([u()],d.prototype,"_cloneTargetSlot",2),h([u()],d.prototype,"_showCloneInput",2),h([u()],d.prototype,"_confirmingClear",2),h([u()],d.prototype,"_editingDraft",2),h([u()],d.prototype,"_objects",2),d=h([ke("omni-panel-programs")],d);export{d as OmniPanelPrograms}; + `,h([L({attribute:!1})],d.prototype,"hass",2),h([L({attribute:!1})],d.prototype,"narrow",2),h([u()],d.prototype,"_entryId",2),h([u()],d.prototype,"_rows",2),h([u()],d.prototype,"_total",2),h([u()],d.prototype,"_filteredTotal",2),h([u()],d.prototype,"_loading",2),h([u()],d.prototype,"_error",2),h([u()],d.prototype,"_activeTriggerTypes",2),h([u()],d.prototype,"_referenceFilter",2),h([u()],d.prototype,"_searchTerm",2),h([u()],d.prototype,"_selectedSlot",2),h([u()],d.prototype,"_detail",2),h([u()],d.prototype,"_detailLoading",2),h([u()],d.prototype,"_fireFeedback",2),h([u()],d.prototype,"_writeFeedback",2),h([u()],d.prototype,"_cloneTargetSlot",2),h([u()],d.prototype,"_showCloneInput",2),h([u()],d.prototype,"_confirmingClear",2),h([u()],d.prototype,"_editingDraft",2),h([u()],d.prototype,"_objects",2),d=h([Ce("omni-panel-programs")],d);export{d as OmniPanelPrograms}; /*! Bundled license information: @lit/reactive-element/css-tag.js: diff --git a/tests/ha_integration/test_program_websocket.py b/tests/ha_integration/test_program_websocket.py index bbddcc4..a131771 100644 --- a/tests/ha_integration/test_program_websocket.py +++ b/tests/ha_integration/test_program_websocket.py @@ -198,6 +198,45 @@ async def test_ws_get_program_returns_full_token_stream( assert "KITCHEN_OVER" in text +async def test_ws_get_program_returns_raw_fields_for_editor( + hass: HomeAssistant, configured_panel, hass_ws_client +) -> None: + """The detail response includes a 'fields' dict carrying raw Program + integer values, so the editor can seed forms from actual data rather + than defaults. Round-trip: get → fields → write back should preserve + every byte (idempotent under no-op edits).""" + client = await hass_ws_client(hass) + await client.send_json_auto_id({ + "type": "omni_pca/programs/get", + "entry_id": configured_panel.entry_id, + "slot": 42, + }) + response = await client.receive_json() + assert response["success"] is True + fields = response["result"]["fields"] + # Slot 42 is the seeded TIMED 22:30 Sunday → Turn ON unit 2 program. + assert fields["prog_type"] == 1 + assert fields["hour"] == 22 + assert fields["minute"] == 30 + assert fields["days"] == int(Days.SUNDAY) + assert fields["cmd"] == int(Command.UNIT_ON) + assert fields["pr2"] == 2 + + # Round-trip: write those same fields back; nothing should change. + coordinator = hass.data[DOMAIN][configured_panel.entry_id] + before = coordinator.data.programs[42] + await client.send_json_auto_id({ + "type": "omni_pca/programs/write", + "entry_id": configured_panel.entry_id, + "slot": 42, + "program": fields, + }) + write_response = await client.receive_json() + assert write_response["success"] is True + after = coordinator.data.programs[42] + assert before.encode_wire_bytes() == after.encode_wire_bytes() + + async def test_ws_get_program_missing_slot_returns_error( hass: HomeAssistant, configured_panel, hass_ws_client ) -> None: