Ryan Malloy 9cdb312baf
Some checks are pending
Validate / HACS validation (push) Waiting to run
Validate / Hassfest (push) Waiting to run
program writeback: DownloadProgram wire + HA write API + Clear/Clone UI
The program viewer goes from read-only to write-capable. Three layers
land together because a partial implementation isn't actionable.

D1 — wire path:

* OmniClient.download_program(slot, program) — sends opcode 8
  (clsOLMsg2DownloadProgram, clsHAC.cs:1133-1140) with the 2-byte BE
  slot + Program.encode_wire_bytes(). Validates slot range 1..1500
  client-side. Maps Ack → success, Nak → CommandFailedError, any
  other opcode → OmniConnectionError.
* OmniClient.clear_program(slot) — convenience that writes an all-zero
  body. Mock treats this as deletion (removes the slot from
  state.programs) so subsequent reads see it as undefined.
* MockPanel handles DownloadProgram on the v2 dispatch path —
  receive 2-byte slot + 14-byte body, store in state.programs, ack.
* OmniClientV1.download_program raises NotImplementedError. v1 only
  has the bulk DownloadPrograms flow which clears everything before
  rewriting — destructive for HA's edit-one-program use case.
  Documented in the docstring so callers know to route v1 users to
  a v2 connection.

Tests cover: write-then-read round-trip, overwrite of existing slot,
clear deletes the slot, range validation, v1 not-implemented.

D2 — HA websocket commands:

* omni_pca/programs/clear — writes zero body, updates coordinator.
  data.programs immediately so the next list call shows the deletion.
  Returns ``{slot, cleared: true}``. Maps NotImplementedError on v1
  panels to the ``not_supported`` error code.
* omni_pca/programs/clone — copies source_slot → target_slot, with
  the slot field re-stamped. Refuses identical source/target,
  refuses missing source. Same coordinator update pattern.

5 new HA-integration tests covering clear, clone happy path, clone
to same slot, clone from missing source.

D3 — Clear/Clone UI in the side panel:

* "Clone…" button reveals an inline target-slot input (number,
  1..1500). Enter or "Clone" button calls the WS command, then
  navigates the detail panel to the new clone so the user sees the
  result.
* "Clear" button shows an inline confirmation row ("Clear slot N?
  This deletes the program from the panel.") with Yes/Cancel. Yes
  closes the detail panel and refreshes the list — the slot is gone.
* Both surface feedback via the same _writeFeedback state used by
  Fire now (auto-clears after 4 seconds).
* Three new button styles (.primary, .secondary, .danger) and the
  .action-row composite used for both inline prompts.

What's NOT shipped here: a real visual editor for trigger/condition/
action fields. That's a follow-up (~600 lines of new TS + careful
validation work). The current "Cut 1" UX is enough for the common
"I accidentally created a program, clear it" and "I want a variant
of this program, give me a copy in an empty slot" workflows.

Full suite: 643 passed, 1 skipped (up from 634).
Frontend bundle: 38 KB minified (up from 34 KB with the write UI).
2026-05-16 01:14:54 -06:00

534 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// omni_pca side panel — generated by frontend/build.mjs. Edit src/, not this file.
var xe=Object.defineProperty;var we=Object.getOwnPropertyDescriptor;var p=(i,e,t,r)=>{for(var s=r>1?void 0:r?we(e,t):e,o=i.length-1,n;o>=0;o--)(n=i[o])&&(s=(r?n(e,t,s):n(s))||s);return r&&s&&xe(e,t,s),s};var F=globalThis,z=F.ShadowRoot&&(F.ShadyCSS===void 0||F.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,V=Symbol(),ie=new WeakMap,T=class{constructor(e,t,r){if(this._$cssResult$=!0,r!==V)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=ie.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),r&&ie.set(t,e))}return e}toString(){return this.cssText}},oe=i=>new T(typeof i=="string"?i:i+"",void 0,V),B=(i,...e)=>{let t=i.length===1?i[0]:e.reduce((r,s,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.")})(s)+i[o+1],i[0]);return new T(t,i,V)},ne=(i,e)=>{if(z)i.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(let t of e){let r=document.createElement("style"),s=F.litNonce;s!==void 0&&r.setAttribute("nonce",s),r.textContent=t.cssText,i.appendChild(r)}},W=z?i=>i:i=>i instanceof CSSStyleSheet?(e=>{let t="";for(let r of e.cssRules)t+=r.cssText;return oe(t)})(i):i;var{is:Ae,defineProperty:Se,getOwnPropertyDescriptor:Ee,getOwnPropertyNames:ke,getOwnPropertySymbols:Te,getPrototypeOf:Ce}=Object,D=globalThis,ae=D.trustedTypes,Re=ae?ae.emptyScript:"",Me=D.reactiveElementPolyfillSupport,C=(i,e)=>i,R={toAttribute(i,e){switch(e){case Boolean:i=i?Re:null;break;case Object:case Array:i=i==null?i:JSON.stringify(i)}return i},fromAttribute(i,e){let t=i;switch(e){case Boolean:t=i!==null;break;case Number:t=i===null?null:Number(i);break;case Object:case Array:try{t=JSON.parse(i)}catch{t=null}}return t}},O=(i,e)=>!Ae(i,e),le={attribute:!0,type:String,converter:R,reflect:!1,useDefault:!1,hasChanged:O};Symbol.metadata??=Symbol("metadata"),D.litPropertyMetadata??=new WeakMap;var v=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=le){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(),s=this.getPropertyDescriptor(e,r,t);s!==void 0&&Se(this.prototype,e,s)}}static getPropertyDescriptor(e,t,r){let{get:s,set:o}=Ee(this.prototype,e)??{get(){return this[t]},set(n){this[t]=n}};return{get:s,set(n){let c=s?.call(this);o?.call(this,n),this.requestUpdate(e,c,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??le}static _$Ei(){if(this.hasOwnProperty(C("elementProperties")))return;let e=Ce(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=[...ke(t),...Te(t)];for(let s of r)this.createProperty(s,t[s])}let e=this[Symbol.metadata];if(e!==null){let t=litPropertyMetadata.get(e);if(t!==void 0)for(let[r,s]of t)this.elementProperties.set(r,s)}this._$Eh=new Map;for(let[t,r]of this.elementProperties){let s=this._$Eu(t,r);s!==void 0&&this._$Eh.set(s,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 s of r)t.unshift(W(s))}else e!==void 0&&t.push(W(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 ne(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),s=this.constructor._$Eu(e,r);if(s!==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(s):this.setAttribute(s,o),this._$Em=null}}_$AK(e,t){let r=this.constructor,s=r._$Eh.get(e);if(s!==void 0&&this._$Em!==s){let o=r.getPropertyOptions(s),n=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:R;this._$Em=s;let c=n.fromAttribute(t,o.type);this[s]=c??this._$Ej?.get(s)??c,this._$Em=null}}requestUpdate(e,t,r,s=!1,o){if(e!==void 0){let n=this.constructor;if(s===!1&&(o=this[e]),r??=n.getPropertyOptions(e),!((r.hasChanged??O)(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:s,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)),s===!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[s,o]of this._$Ep)this[s]=o;this._$Ep=void 0}let r=this.constructor.elementProperties;if(r.size>0)for(let[s,o]of r){let{wrapped:n}=o,c=this[s];n!==!0||this._$AL.has(s)||c===void 0||this.C(s,void 0,o,c)}}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){}};v.elementStyles=[],v.shadowRootOptions={mode:"open"},v[C("elementProperties")]=new Map,v[C("finalized")]=new Map,Me?.({ReactiveElement:v}),(D.reactiveElementVersions??=[]).push("2.1.2");var X=globalThis,ce=i=>i,j=X.trustedTypes,de=j?j.createPolicy("lit-html",{createHTML:i=>i}):void 0,_e="$lit$",$=`lit$${Math.random().toFixed(9).slice(2)}$`,me="?"+$,Le=`<${me}>`,A=document,L=()=>A.createComment(""),U=i=>i===null||typeof i!="object"&&typeof i!="function",ee=Array.isArray,Ue=i=>ee(i)||typeof i?.[Symbol.iterator]=="function",K=`[
\f\r]`,M=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,he=/-->/g,pe=/>/g,x=RegExp(`>|${K}(?:([^\\s"'>=/]+)(${K}*=${K}*(?:[^
\f\r"'\`<>=]|("|')|))|$)`,"g"),ue=/'/g,fe=/"/g,ve=/^(?:script|style|textarea|title)$/i,te=i=>(e,...t)=>({_$litType$:i,strings:e,values:t}),l=te(1),Ke=te(2),Ye=te(3),S=Symbol.for("lit-noChange"),f=Symbol.for("lit-nothing"),ge=new WeakMap,w=A.createTreeWalker(A,129);function ye(i,e){if(!ee(i)||!i.hasOwnProperty("raw"))throw Error("invalid template strings array");return de!==void 0?de.createHTML(e):e}var Ie=(i,e)=>{let t=i.length-1,r=[],s,o=e===2?"<svg>":e===3?"<math>":"",n=M;for(let c=0;c<t;c++){let a=i[c],u,_,d=-1,m=0;for(;m<a.length&&(n.lastIndex=m,_=n.exec(a),_!==null);)m=n.lastIndex,n===M?_[1]==="!--"?n=he:_[1]!==void 0?n=pe:_[2]!==void 0?(ve.test(_[2])&&(s=RegExp("</"+_[2],"g")),n=x):_[3]!==void 0&&(n=x):n===x?_[0]===">"?(n=s??M,d=-1):_[1]===void 0?d=-2:(d=n.lastIndex-_[2].length,u=_[1],n=_[3]===void 0?x:_[3]==='"'?fe:ue):n===fe||n===ue?n=x:n===he||n===pe?n=M:(n=x,s=void 0);let y=n===x&&i[c+1].startsWith("/>")?" ":"";o+=n===M?a+Le:d>=0?(r.push(u),a.slice(0,d)+_e+a.slice(d)+$+y):a+$+(d===-2?c:y)}return[ye(i,o+(i[t]||"<?>")+(e===2?"</svg>":e===3?"</math>":"")),r]},I=class i{constructor({strings:e,_$litType$:t},r){let s;this.parts=[];let o=0,n=0,c=e.length-1,a=this.parts,[u,_]=Ie(e,t);if(this.el=i.createElement(u,r),w.currentNode=this.el.content,t===2||t===3){let d=this.el.content.firstChild;d.replaceWith(...d.childNodes)}for(;(s=w.nextNode())!==null&&a.length<c;){if(s.nodeType===1){if(s.hasAttributes())for(let d of s.getAttributeNames())if(d.endsWith(_e)){let m=_[n++],y=s.getAttribute(d).split($),N=/([.?@])?(.*)/.exec(m);a.push({type:1,index:o,name:N[2],strings:y,ctor:N[1]==="."?G:N[1]==="?"?J:N[1]==="@"?Z:k}),s.removeAttribute(d)}else d.startsWith($)&&(a.push({type:6,index:o}),s.removeAttribute(d));if(ve.test(s.tagName)){let d=s.textContent.split($),m=d.length-1;if(m>0){s.textContent=j?j.emptyScript:"";for(let y=0;y<m;y++)s.append(d[y],L()),w.nextNode(),a.push({type:2,index:++o});s.append(d[m],L())}}}else if(s.nodeType===8)if(s.data===me)a.push({type:2,index:o});else{let d=-1;for(;(d=s.data.indexOf($,d+1))!==-1;)a.push({type:7,index:o}),d+=$.length-1}o++}}static createElement(e,t){let r=A.createElement("template");return r.innerHTML=e,r}};function E(i,e,t=i,r){if(e===S)return e;let s=r!==void 0?t._$Co?.[r]:t._$Cl,o=U(e)?void 0:e._$litDirective$;return s?.constructor!==o&&(s?._$AO?.(!1),o===void 0?s=void 0:(s=new o(i),s._$AT(i,t,r)),r!==void 0?(t._$Co??=[])[r]=s:t._$Cl=s),s!==void 0&&(e=E(i,s._$AS(i,e.values),s,r)),e}var Y=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,s=(e?.creationScope??A).importNode(t,!0);w.currentNode=s;let o=w.nextNode(),n=0,c=0,a=r[0];for(;a!==void 0;){if(n===a.index){let u;a.type===2?u=new P(o,o.nextSibling,this,e):a.type===1?u=new a.ctor(o,a.name,a.strings,this,e):a.type===6&&(u=new Q(o,this,e)),this._$AV.push(u),a=r[++c]}n!==a?.index&&(o=w.nextNode(),n++)}return w.currentNode=A,s}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++}},P=class i{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(e,t,r,s){this.type=2,this._$AH=f,this._$AN=void 0,this._$AA=e,this._$AB=t,this._$AM=r,this.options=s,this._$Cv=s?.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),U(e)?e===f||e==null||e===""?(this._$AH!==f&&this._$AR(),this._$AH=f):e!==this._$AH&&e!==S&&this._(e):e._$litType$!==void 0?this.$(e):e.nodeType!==void 0?this.T(e):Ue(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!==f&&U(this._$AH)?this._$AA.nextSibling.data=e:this.T(A.createTextNode(e)),this._$AH=e}$(e){let{values:t,_$litType$:r}=e,s=typeof r=="number"?this._$AC(e):(r.el===void 0&&(r.el=I.createElement(ye(r.h,r.h[0]),this.options)),r);if(this._$AH?._$AD===s)this._$AH.p(t);else{let o=new Y(s,this),n=o.u(this.options);o.p(t),this.T(n),this._$AH=o}}_$AC(e){let t=ge.get(e.strings);return t===void 0&&ge.set(e.strings,t=new I(e)),t}k(e){ee(this._$AH)||(this._$AH=[],this._$AR());let t=this._$AH,r,s=0;for(let o of e)s===t.length?t.push(r=new i(this.O(L()),this.O(L()),this,this.options)):r=t[s],r._$AI(o),s++;s<t.length&&(this._$AR(r&&r._$AB.nextSibling,s),t.length=s)}_$AR(e=this._$AA.nextSibling,t){for(this._$AP?.(!1,!0,t);e!==this._$AB;){let r=ce(e).nextSibling;ce(e).remove(),e=r}}setConnected(e){this._$AM===void 0&&(this._$Cv=e,this._$AP?.(e))}},k=class{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(e,t,r,s,o){this.type=1,this._$AH=f,this._$AN=void 0,this.element=e,this.name=t,this._$AM=s,this.options=o,r.length>2||r[0]!==""||r[1]!==""?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=f}_$AI(e,t=this,r,s){let o=this.strings,n=!1;if(o===void 0)e=E(this,e,t,0),n=!U(e)||e!==this._$AH&&e!==S,n&&(this._$AH=e);else{let c=e,a,u;for(e=o[0],a=0;a<o.length-1;a++)u=E(this,c[r+a],t,a),u===S&&(u=this._$AH[a]),n||=!U(u)||u!==this._$AH[a],u===f?e=f:e!==f&&(e+=(u??"")+o[a+1]),this._$AH[a]=u}n&&!s&&this.j(e)}j(e){e===f?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,e??"")}},G=class extends k{constructor(){super(...arguments),this.type=3}j(e){this.element[this.name]=e===f?void 0:e}},J=class extends k{constructor(){super(...arguments),this.type=4}j(e){this.element.toggleAttribute(this.name,!!e&&e!==f)}},Z=class extends k{constructor(e,t,r,s,o){super(e,t,r,s,o),this.type=5}_$AI(e,t=this){if((e=E(this,e,t,0)??f)===S)return;let r=this._$AH,s=e===f&&r!==f||e.capture!==r.capture||e.once!==r.once||e.passive!==r.passive,o=e!==f&&(r===f||s);s&&this.element.removeEventListener(this.name,this,r),o&&this.element.addEventListener(this.name,this,e),this._$AH=e}handleEvent(e){typeof this._$AH=="function"?this._$AH.call(this.options?.host??this.element,e):this._$AH.handleEvent(e)}},Q=class{constructor(e,t,r){this.element=e,this.type=6,this._$AN=void 0,this._$AM=t,this.options=r}get _$AU(){return this._$AM._$AU}_$AI(e){E(this,e)}};var Pe=X.litHtmlPolyfillSupport;Pe?.(I,P),(X.litHtmlVersions??=[]).push("3.3.3");var $e=(i,e,t)=>{let r=t?.renderBefore??e,s=r._$litPart$;if(s===void 0){let o=t?.renderBefore??null;r._$litPart$=s=new P(e.insertBefore(L(),o),o,void 0,t??{})}return s._$AI(i),s};var re=globalThis,b=class extends v{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=$e(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return S}};b._$litElement$=!0,b.finalized=!0,re.litElementHydrateSupport?.({LitElement:b});var He=re.litElementPolyfillSupport;He?.({LitElement:b});(re.litElementVersions??=[]).push("4.2.2");var be=i=>(e,t)=>{t!==void 0?t.addInitializer(()=>{customElements.define(i,e)}):customElements.define(i,e)};var Ne={attribute:!0,type:String,converter:R,reflect:!1,hasChanged:O},Fe=(i=Ne,e,t)=>{let{kind:r,metadata:s}=t,o=globalThis.litPropertyMetadata.get(s);if(o===void 0&&globalThis.litPropertyMetadata.set(s,o=new Map),r==="setter"&&((i=Object.create(i)).wrapped=!0),o.set(t.name,i),r==="accessor"){let{name:n}=t;return{set(c){let a=e.get.call(this);e.set.call(this,c),this.requestUpdate(n,a,i,!0,c)},init(c){return c!==void 0&&this.C(n,void 0,i,c),c}}}if(r==="setter"){let{name:n}=t;return function(c){let a=this[n];e.call(this,c),this.requestUpdate(n,a,i,!0,c)}}throw Error("Unsupported decorator location: "+r)};function H(i){return(e,t)=>typeof t=="object"?Fe(i,e,t):((r,s,o)=>{let n=s.hasOwnProperty(o);return s.constructor.createProperty(o,r),n?Object.getOwnPropertyDescriptor(s,o):void 0})(i,e,t)}function g(i){return H({...i,state:!0,attribute:!1})}function se(i,e){return l`${i.map(t=>ze(t,e))}`}function ze(i,e){switch(i.k){case"newline":return l`<br />`;case"indent":return l`<span class="indent">${i.t}</span>`;case"keyword":return l`<span class="keyword">${i.t}</span>`;case"operator":return l`<span class="operator">${i.t}</span>`;case"value":return l`<span class="value">${i.t}</span>`;case"ref":{let t=e&&i.ek&&typeof i.ei=="number"?()=>e(i.ek,i.ei):void 0;return l`<button
type="button"
class="ref ref-${i.ek}"
title=${i.ek??""}
@click=${t}
>
<span class="ref-name">${i.t}</span>
${i.s?l`<span class="ref-state">${i.s}</span>`:""}
</button>`}default:return l`<span>${i.t}</span>`}}var De=["TIMED","EVENT","YEARLY","WHEN","AT","EVERY","REMARK"],Oe=5e3,h=class extends b{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._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(s=>s.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 s=r instanceof Error?r.message:String(r);this._writeFeedback=`error: ${s}`}setTimeout(()=>{this._writeFeedback=null},4e3)}}async _cloneProgram(t){if(!this._entryId)return;let r=this._cloneTargetSlot.trim(),s=parseInt(r,10);if(!Number.isFinite(s)||s<1||s>1500){this._writeFeedback="target slot must be 1..1500",setTimeout(()=>{this._writeFeedback=null},4e3);return}if(s===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:s}),this._writeFeedback=`cloned to slot ${s}`,this._showCloneInput=!1,this._cloneTargetSlot="",this._selectedSlot=s,await this._loadList(),await this._loadDetail(s)}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}_startRefreshTimer(){this._refreshTimer===null&&(this._refreshTimer=window.setInterval(()=>{this._loadList(),this._selectedSlot!==null&&this._loadDetail(this._selectedSlot)},Oe))}_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`
<div class="header">
<div class="title">
<ha-icon icon="mdi:script-text-outline"></ha-icon>
<span>Omni Programs</span>
${this._total>0?l`
<span class="count">
${this._filteredTotal===this._total?`${this._total} programs`:`${this._filteredTotal} of ${this._total} shown`}
</span>`:""}
</div>
</div>
${this._error?l`
<div class="error">${this._error}</div>`:""}
${this._renderFilters()}
<div class="body" data-narrow=${this.narrow}>
${this._renderList()}
${this._selectedSlot!==null?this._renderDetail():""}
</div>
`}_renderFilters(){return l`
<div class="filters">
<input
type="search"
class="search"
placeholder="search programs..."
.value=${this._searchTerm}
@input=${this._onSearchInput}
/>
<div class="chips">
${De.map(t=>l`
<button
type="button"
class="chip ${this._activeTriggerTypes.has(t)?"active":""}"
@click=${()=>this._toggleTriggerFilter(t)}
>${t}</button>
`)}
</div>
${this._referenceFilter?l`
<div class="ref-filter">
<span>filtering on <strong>${this._referenceFilter}</strong></span>
<button type="button" @click=${this._clearReferenceFilter}>clear</button>
</div>`:""}
</div>
`}_renderList(){return this._loading&&this._rows.length===0?l`<div class="loading">loading…</div>`:this._rows.length===0?l`<div class="empty">No programs match the current filters.</div>`:l`
<div class="list">
${this._rows.map(t=>l`
<div
class="row ${this._selectedSlot===t.slot?"selected":""}"
@click=${()=>this._onRowClick(t.slot)}
>
<div class="row-slot">#${t.slot}</div>
<div class="row-summary">
${se(t.summary,(r,s)=>this._onRefClick(r,s))}
</div>
<div class="row-meta">
<span class="trigger-badge trigger-${t.trigger_type.toLowerCase()}">
${t.trigger_type}
</span>
${t.condition_count>0?l`
<span class="meta-pill">${t.condition_count} cond</span>`:""}
${t.action_count>1?l`
<span class="meta-pill">${t.action_count} actions</span>`:""}
</div>
</div>
`)}
</div>
`}_renderDetail(){if(this._detailLoading)return l`<aside class="detail"><div class="loading">loading…</div></aside>`;if(this._detail===null)return l`<aside class="detail"></aside>`;let t=this._detail;return l`
<aside class="detail">
<header>
<div>
<span class="trigger-badge trigger-${t.trigger_type.toLowerCase()}">
${t.trigger_type}
</span>
<span class="slot">slot #${t.slot}</span>
</div>
<button type="button" class="close" @click=${this._closeDetail}>×</button>
</header>
<pre class="detail-body">${se(t.tokens,(r,s)=>this._onRefClick(r,s))}</pre>
<footer>
<button
type="button"
class="fire"
@click=${()=>this._fireProgram(t.slot)}
>▶ Fire now</button>
<button
type="button"
class="secondary"
@click=${()=>{this._showCloneInput=!this._showCloneInput,this._confirmingClear=!1}}
>Clone…</button>
<button
type="button"
class="danger"
@click=${()=>{this._confirmingClear=!this._confirmingClear,this._showCloneInput=!1}}
>Clear</button>
${this._fireFeedback?l`
<span class="fire-feedback">${this._fireFeedback}</span>`:""}
${this._writeFeedback?l`
<span class="fire-feedback">${this._writeFeedback}</span>`:""}
</footer>
${this._showCloneInput?l`
<div class="action-row">
<label>Clone slot ${t.slot} → target slot:
<input
type="number"
min="1"
max="1500"
.value=${this._cloneTargetSlot}
@input=${this._onCloneTargetInput}
@keydown=${r=>{r.key==="Enter"&&this._cloneProgram(t.slot)}}
/>
</label>
<button
type="button"
class="primary"
@click=${()=>this._cloneProgram(t.slot)}
>Clone</button>
<button
type="button"
@click=${()=>{this._showCloneInput=!1}}
>Cancel</button>
</div>`:""}
${this._confirmingClear?l`
<div class="action-row danger-row">
<span>
<strong>Clear slot ${t.slot}?</strong>
This deletes the program from the panel.
</span>
<button
type="button"
class="danger"
@click=${()=>this._clearProgram(t.slot)}
>Yes, clear</button>
<button
type="button"
@click=${()=>{this._confirmingClear=!1}}
>Cancel</button>
</div>`:""}
${t.chain_slots&&t.chain_slots.length>1?l`
<div class="chain-info">
spans slots
${t.chain_slots.map((r,s)=>l`
${s>0?"\u2192":""}#${r}`)}
</div>`:""}
</aside>
`}};h.styles=B`
:host {
display: block;
min-height: 100vh;
background: var(--primary-background-color, #fafafa);
color: var(--primary-text-color, #000);
font-family: var(--paper-font-body1_-_font-family, sans-serif);
}
.header {
display: flex; align-items: center;
padding: 16px 20px;
background: var(--primary-color, #03a9f4);
color: var(--text-primary-color, #fff);
}
.header .title { display: flex; align-items: center; gap: 10px; font-size: 1.2rem; }
.header .count {
margin-left: 12px;
font-size: 0.85rem; opacity: 0.85; font-weight: normal;
}
.error {
margin: 12px 16px;
padding: 10px 14px;
background: var(--error-color, #db4437);
color: white;
border-radius: 4px;
}
.filters {
padding: 12px 16px 8px;
border-bottom: 1px solid var(--divider-color, #ddd);
}
.search {
width: 100%;
padding: 8px 10px;
font-size: 0.95rem;
border: 1px solid var(--divider-color, #ccc);
border-radius: 4px;
background: var(--card-background-color, #fff);
color: inherit;
box-sizing: border-box;
}
.chips {
display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;
}
.chip {
border: 1px solid var(--divider-color, #ccc);
background: var(--card-background-color, #fff);
color: var(--secondary-text-color, #555);
padding: 4px 10px;
border-radius: 12px;
font-size: 0.78rem;
cursor: pointer;
font-family: inherit;
}
.chip:hover { background: var(--secondary-background-color, #eee); }
.chip.active {
background: var(--primary-color, #03a9f4);
color: var(--text-primary-color, #fff);
border-color: transparent;
}
.ref-filter {
margin-top: 8px;
font-size: 0.85rem;
color: var(--secondary-text-color, #555);
display: flex; align-items: center; gap: 8px;
}
.ref-filter button {
border: 1px solid var(--divider-color, #ccc);
background: transparent; color: inherit;
padding: 2px 8px; border-radius: 8px;
font-size: 0.75rem; cursor: pointer;
}
.body {
display: grid;
grid-template-columns: 1fr;
gap: 0;
}
.body[data-narrow="false"] { grid-template-columns: 1fr 380px; }
.list {
max-height: calc(100vh - 200px);
overflow-y: auto;
}
.row {
display: grid;
grid-template-columns: 60px 1fr auto;
align-items: start;
gap: 12px;
padding: 10px 16px;
border-bottom: 1px solid var(--divider-color, #eee);
cursor: pointer;
}
.row:hover { background: var(--secondary-background-color, #f5f5f5); }
.row.selected { background: var(--state-active-color, #e3f2fd); }
.row-slot {
font-family: var(--code-font-family, monospace);
font-size: 0.78rem;
color: var(--secondary-text-color, #888);
padding-top: 2px;
}
.row-summary {
font-size: 0.92rem;
line-height: 1.45;
}
.row-meta {
display: flex; flex-direction: column; align-items: flex-end; gap: 4px;
}
/* trigger-type badges */
.trigger-badge {
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.5px;
padding: 2px 6px;
border-radius: 3px;
text-transform: uppercase;
}
.trigger-timed { background: #e3f2fd; color: #1565c0; }
.trigger-event { background: #fff3e0; color: #e65100; }
.trigger-yearly { background: #f3e5f5; color: #6a1b9a; }
.trigger-when { background: #e8f5e9; color: #2e7d32; }
.trigger-at { background: #e3f2fd; color: #1565c0; }
.trigger-every { background: #fce4ec; color: #ad1457; }
.trigger-remark { background: #f5f5f5; color: #616161; }
.meta-pill {
font-size: 0.7rem;
color: var(--secondary-text-color, #888);
background: var(--secondary-background-color, #eee);
padding: 1px 6px;
border-radius: 8px;
}
/* token-renderer styles */
.row-summary, .detail-body {
font-family: var(--paper-font-body1_-_font-family, system-ui, sans-serif);
}
.keyword { font-weight: 600; color: var(--primary-color, #1565c0); }
.operator { color: var(--secondary-text-color, #666); font-style: italic; }
.value { font-family: var(--code-font-family, monospace); color: var(--accent-color, #ff6f00); }
.ref {
display: inline-flex; align-items: baseline; gap: 4px;
border: none; background: transparent; padding: 0 2px;
cursor: pointer; font: inherit; color: inherit;
border-bottom: 1px dotted var(--secondary-text-color, #999);
}
.ref:hover { background: var(--secondary-background-color, #eee); }
.ref-name { font-weight: 500; }
.ref-state {
font-size: 0.72rem;
padding: 1px 5px;
border-radius: 3px;
background: var(--secondary-background-color, #eee);
color: var(--secondary-text-color, #666);
vertical-align: 1px;
}
.ref-zone .ref-name { color: var(--info-color, #0288d1); }
.ref-unit .ref-name { color: var(--warning-color, #f57c00); }
.ref-area .ref-name { color: var(--success-color, #388e3c); }
.ref-thermostat .ref-name { color: var(--accent-color, #c2185b); }
.ref-button .ref-name { color: var(--state-light-color, #7e57c2); }
.indent { display: inline-block; width: 1.5em; }
/* detail panel */
.detail {
border-left: 1px solid var(--divider-color, #ddd);
padding: 16px;
max-height: calc(100vh - 200px);
overflow-y: auto;
box-sizing: border-box;
}
.body[data-narrow="true"] .detail {
border-left: none;
border-top: 1px solid var(--divider-color, #ddd);
}
.detail header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 12px;
}
.detail header .slot {
margin-left: 8px;
font-family: var(--code-font-family, monospace);
font-size: 0.85rem;
color: var(--secondary-text-color, #888);
}
.detail .close {
background: transparent; border: none;
font-size: 1.4rem; cursor: pointer;
color: var(--secondary-text-color, #888);
}
.detail-body {
font-size: 0.95rem;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
background: var(--card-background-color, #fff);
padding: 12px;
border-radius: 4px;
border: 1px solid var(--divider-color, #eee);
margin: 0;
}
.detail footer {
display: flex; align-items: center; gap: 12px; margin-top: 14px;
}
.fire, .primary, .secondary, .danger {
border: none;
padding: 8px 16px;
font-size: 0.92rem;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
}
.fire, .primary {
background: var(--primary-color, #03a9f4);
color: var(--text-primary-color, #fff);
}
.secondary {
background: var(--secondary-background-color, #eee);
color: var(--primary-text-color, #000);
}
.danger {
background: transparent;
color: var(--error-color, #db4437);
border: 1px solid var(--error-color, #db4437);
}
.fire:hover, .primary:hover, .secondary:hover, .danger:hover {
filter: brightness(0.9);
}
.action-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 10px;
background: var(--secondary-background-color, #f5f5f5);
border-radius: 4px;
font-size: 0.88rem;
}
.action-row.danger-row {
background: var(--error-color, #db4437);
color: white;
}
.action-row input[type="number"] {
width: 70px;
padding: 4px 6px;
font-size: 0.9rem;
border: 1px solid var(--divider-color, #ccc);
border-radius: 3px;
margin-left: 6px;
}
.action-row button {
padding: 4px 12px;
font-size: 0.85rem;
}
.fire-feedback {
font-size: 0.85rem; color: var(--secondary-text-color, #666);
}
.chain-info {
margin-top: 12px;
font-size: 0.8rem;
color: var(--secondary-text-color, #888);
}
.loading, .empty {
padding: 40px 20px;
text-align: center;
color: var(--secondary-text-color, #888);
}
`,p([H({attribute:!1})],h.prototype,"hass",2),p([H({attribute:!1})],h.prototype,"narrow",2),p([g()],h.prototype,"_entryId",2),p([g()],h.prototype,"_rows",2),p([g()],h.prototype,"_total",2),p([g()],h.prototype,"_filteredTotal",2),p([g()],h.prototype,"_loading",2),p([g()],h.prototype,"_error",2),p([g()],h.prototype,"_activeTriggerTypes",2),p([g()],h.prototype,"_referenceFilter",2),p([g()],h.prototype,"_searchTerm",2),p([g()],h.prototype,"_selectedSlot",2),p([g()],h.prototype,"_detail",2),p([g()],h.prototype,"_detailLoading",2),p([g()],h.prototype,"_fireFeedback",2),p([g()],h.prototype,"_writeFeedback",2),p([g()],h.prototype,"_cloneTargetSlot",2),p([g()],h.prototype,"_showCloneInput",2),p([g()],h.prototype,"_confirmingClear",2),h=p([be("omni-panel-programs")],h);export{h as OmniPanelPrograms};
/*! Bundled license information:
@lit/reactive-element/css-tag.js:
(**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
@lit/reactive-element/reactive-element.js:
(**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
lit-html/lit-html.js:
(**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
lit-element/lit-element.js:
(**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
lit-html/is-server.js:
(**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
@lit/reactive-element/decorators/custom-element.js:
(**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
@lit/reactive-element/decorators/property.js:
(**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
@lit/reactive-element/decorators/state.js:
(**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
@lit/reactive-element/decorators/event-options.js:
(**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
@lit/reactive-element/decorators/base.js:
(**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
@lit/reactive-element/decorators/query.js:
(**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
@lit/reactive-element/decorators/query-all.js:
(**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
@lit/reactive-element/decorators/query-async.js:
(**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
@lit/reactive-element/decorators/query-assigned-elements.js:
(**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
@lit/reactive-element/decorators/query-assigned-nodes.js:
(**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*)
*/