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 ab97abe..7effcd5 100644 --- a/custom_components/omni_pca/frontend/src/omni-panel-programs.ts +++ b/custom_components/omni_pca/frontend/src/omni-panel-programs.ts @@ -137,18 +137,37 @@ export class OmniPanelPrograms extends LitElement { // Best-effort: walk known config entries via HA's standard // config_entries/get command. If that fails we surface a friendly // error pointing at integration setup. + // + // NOTE: this runs fire-and-forget from connectedCallback (and from + // the hass-update path), so we need to kick off the initial list + // *here*, after _entryId lands. Earlier versions checked _entryId + // synchronously right after calling discover, which always saw + // null and silently skipped loadList — the panel rendered "no + // programs" forever until the first manual refresh. try { const entries = await this.hass.connection.sendMessagePromise< - Array<{ entry_id: string; domain: string; title: string }> + Array<{ entry_id: string; domain: string; title: string; state?: string }> >({ type: "config_entries/get" }); const ours = entries.filter((e) => e.domain === "omni_pca"); if (ours.length === 0) { this._error = "No Omni panel configured. Add one via Settings → Devices & Services."; return; } - // First entry wins for v1; multi-panel selector is a follow-up. - this._entryId = ours[0].entry_id; + // Prefer entries that are actually loaded — a config entry in + // setup_retry or migration_error has no live coordinator, and + // the websocket commands return "panel not configured" against + // it. Multi-loaded-panel installs still pick the first loaded + // one; a multi-panel selector is a follow-up. + const loaded = ours.find((e) => e.state === "loaded"); + this._entryId = (loaded ?? ours[0]).entry_id; this._error = null; + // Kick off the initial list + start the live-state refresh timer. + // Both are safe to call from here regardless of whether the + // caller (connectedCallback / updated) also expected to start + // them; _loadList is reentrant-safe and _startRefreshTimer is + // idempotent. + void this._loadList(); + this._startRefreshTimer(); } catch (err) { this._error = `Could not discover panels: ${err instanceof Error ? err.message : String(err)}`; } diff --git a/custom_components/omni_pca/www/panel.js b/custom_components/omni_pca/www/panel.js index 040000a..4b31ce6 100644 --- a/custom_components/omni_pca/www/panel.js +++ b/custom_components/omni_pca/www/panel.js @@ -9,7 +9,7 @@ var Ie=Object.defineProperty;var ze=Object.getOwnPropertyDescriptor;var h=(n,t,e > ${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` + `}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(s=>s.domain==="omni_pca");if(r.length===0){this._error="No Omni panel configured. Add one via Settings \u2192 Devices & Services.";return}let i=r.find(s=>s.state==="loaded");this._entryId=(i??r[0]).entry_id,this._error=null,this._loadList(),this._startRefreshTimer()}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`
diff --git a/dev/artifacts/screenshots/2026-05-16/01-overview.png b/dev/artifacts/screenshots/2026-05-16/01-overview.png new file mode 100644 index 0000000..a150fd7 Binary files /dev/null and b/dev/artifacts/screenshots/2026-05-16/01-overview.png differ diff --git a/dev/artifacts/screenshots/2026-05-16/02-integrations-list.png b/dev/artifacts/screenshots/2026-05-16/02-integrations-list.png new file mode 100644 index 0000000..c7a98a1 Binary files /dev/null and b/dev/artifacts/screenshots/2026-05-16/02-integrations-list.png differ diff --git a/dev/artifacts/screenshots/2026-05-16/03-omni-pca-config.png b/dev/artifacts/screenshots/2026-05-16/03-omni-pca-config.png new file mode 100644 index 0000000..5b5fe13 Binary files /dev/null and b/dev/artifacts/screenshots/2026-05-16/03-omni-pca-config.png differ diff --git a/dev/artifacts/screenshots/2026-05-16/04-panel-device.png b/dev/artifacts/screenshots/2026-05-16/04-panel-device.png new file mode 100644 index 0000000..0c3ff32 Binary files /dev/null and b/dev/artifacts/screenshots/2026-05-16/04-panel-device.png differ diff --git a/dev/artifacts/screenshots/2026-05-16/05-entities-omni.png b/dev/artifacts/screenshots/2026-05-16/05-entities-omni.png new file mode 100644 index 0000000..c4f0970 Binary files /dev/null and b/dev/artifacts/screenshots/2026-05-16/05-entities-omni.png differ diff --git a/dev/artifacts/screenshots/2026-05-16/06-developer-states.png b/dev/artifacts/screenshots/2026-05-16/06-developer-states.png new file mode 100644 index 0000000..1b8d162 Binary files /dev/null and b/dev/artifacts/screenshots/2026-05-16/06-developer-states.png differ diff --git a/dev/artifacts/screenshots/2026-05-16/07-side-panel-empty.png b/dev/artifacts/screenshots/2026-05-16/07-side-panel-empty.png new file mode 100644 index 0000000..6703674 Binary files /dev/null and b/dev/artifacts/screenshots/2026-05-16/07-side-panel-empty.png differ diff --git a/dev/artifacts/screenshots/2026-05-16/08-side-panel-programs.png b/dev/artifacts/screenshots/2026-05-16/08-side-panel-programs.png new file mode 100644 index 0000000..7aa74da Binary files /dev/null and b/dev/artifacts/screenshots/2026-05-16/08-side-panel-programs.png differ diff --git a/dev/artifacts/screenshots/2026-05-16/09-side-panel-detail.png b/dev/artifacts/screenshots/2026-05-16/09-side-panel-detail.png new file mode 100644 index 0000000..929ba80 Binary files /dev/null and b/dev/artifacts/screenshots/2026-05-16/09-side-panel-detail.png differ diff --git a/dev/artifacts/screenshots/2026-05-16/10-side-panel-editor.png b/dev/artifacts/screenshots/2026-05-16/10-side-panel-editor.png new file mode 100644 index 0000000..4830f03 Binary files /dev/null and b/dev/artifacts/screenshots/2026-05-16/10-side-panel-editor.png differ diff --git a/dev/screenshot.py b/dev/screenshot.py index 41f4698..d73282b 100644 --- a/dev/screenshot.py +++ b/dev/screenshot.py @@ -298,6 +298,72 @@ async def _take_screenshots(ha_url: str, token: str, outdir: Path) -> list[Path] await shot("06-developer-states.png", "/developer-tools/state", wait_secs=4.0) + # The side panel registered by panel_custom (websocket.py: + # async_register_side_panel). If pointed at a real panel the + # program list is whatever the homeowner has authored; against + # the mock it's whatever ``run_mock_panel.py`` seeded. We + # deliberately do NOT write programs from here because the + # screenshot script may be aimed at real hardware. + await shot("08-side-panel-programs.png", + "/omni-panel-programs", wait_secs=6.0) + # Helper: locate the omni-panel-programs element regardless of + # what shadow-DOM path HA's panel host wraps it in. Recursive + # walk because partial-panel-resolver / hui-view / etc. can + # vary between HA versions. + find_panel_js = """ + (() => { + function find(root, depth=0) { + if (!root || depth > 15) return null; + if (root.tagName === 'OMNI-PANEL-PROGRAMS') return root; + for (const k of Array.from(root.children || [])) { + const r = find(k, depth+1); + if (r) return r; + } + if (root.shadowRoot) { + const r = find(root.shadowRoot, depth+1); + if (r) return r; + } + return null; + } + return find(document.body); + })() + """ + + # Click into the first program row to capture the detail panel. + try: + await page.evaluate(f"""() => {{ + const panel = {find_panel_js}; + if (!panel) {{ console.warn('omni panel not found'); return; }} + const row = panel.shadowRoot.querySelector('.row'); + if (row) row.click(); + }}""") + await page.wait_for_timeout(800) + except Exception as e: + print(f" click-into-row warning: {e}") + # Re-shoot WITHOUT a navigate (page.goto would reset selection). + await page.screenshot(path=str(outdir / "09-side-panel-detail.png"), + full_page=False) + shots.append(outdir / "09-side-panel-detail.png") + print(f" → 09-side-panel-detail.png (in-place)") + + # Click "Edit" to capture the editor mode. + try: + await page.evaluate(f"""() => {{ + const panel = {find_panel_js}; + if (!panel) {{ console.warn('omni panel not found'); return; }} + const buttons = panel.shadowRoot.querySelectorAll('.detail button'); + for (const b of buttons) {{ + if (b.textContent.trim() === 'Edit') {{ b.click(); break; }} + }} + }}""") + await page.wait_for_timeout(800) + except Exception as e: + print(f" click-edit warning: {e}") + await page.screenshot(path=str(outdir / "10-side-panel-editor.png"), + full_page=False) + shots.append(outdir / "10-side-panel-editor.png") + print(f" → 10-side-panel-editor.png (in-place)") + await browser.close() return shots