From 8f592ece87fcd5b85b7b3623f53282ad43281f20 Mon Sep 17 00:00:00 2001 From: Gamso Date: Sun, 27 Sep 2026 16:26:03 +0200 Subject: [PATCH 01/25] feat(card): rebuild the card as a labelled list with collapsible durations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dial showed the thermostat presets, but everything else had to live around it: times pinned to one corner, badges pinned to the other, two unlabelled dropdowns underneath. Adding the covers-left-open warning made the crowding obvious — its list of covers only fitted in a hover tooltip, which does not exist on a touch screen. The presets become a segmented control, one colour per mode rather than red for all of them, and everything else becomes a labelled row. Nothing floats against the card edge any more, and the covers left open are written out. The two duration settings were "Surcharge" and "Anticipation", which say nothing about what they do. They are now "Garder mon choix manuel" and "Anticiper les événements", collapsed to one line showing their value, and folded open on click to a minus/plus stepper over the same presets as before. Under the stepper, a sentence states the consequence rather than the setting: "Télétravail commencera à 20:17 au lieu de 21:17." One opens at a time and folds back after six seconds. When nothing is wrong the card says nothing: the alert row exists only while heat protection is on or covers were left up. The "Thermostat" header goes too, with the name and show_title options and the editor switch that drove them — the card sits under its own dashboard heading and repeated the word for nothing. Preset colours are read from the select's option_map keys, and the card no longer depends on that attribute being there: the active preset comes from current_key when present, and the colour class falls back to the display label ("Eteint", "Clim", "Fan" and their neighbours map back to a known mode) rather than producing a class no rule matches. The simulated selects in the test Home Assistant now publish option_map, option_keys and current_key, the same three the integration publishes. Also fixes a trap this uncovered: localize() returns the key when a string is missing, so every `localize(...) || fallback` in the card was dead and "Éteint" rendered as "thermostat.off". A t() helper treats an echoed key as missing, and the missing thermostat.off string is added to both languages. The dial is removed with its component, and with it _formatRelativeTime and the two select handlers the steppers replace. It is in the history if the layout is ever wanted back. Verified in a browser against the built bundle: presets localised with the active one marked and each colour rule shipping, settings collapsed by default and one-at-a-time, the consequence sentences correct, the steppers calling number.set_value with the neighbouring preset, and no alert markup at all in the calm state. Co-Authored-By: Claude Opus 5 --- .devcontainer/config/configuration.yaml | 8 + dist/homeshift-card.js | 694 ++++++++---------- src/components/circular-slider.ts | 282 -------- src/components/homeshift-card-editor.ts | 19 - src/components/homeshift-card.ts | 888 +++++++++++++----------- src/localize/en.json | 19 +- src/localize/fr.json | 19 +- 7 files changed, 819 insertions(+), 1110 deletions(-) delete mode 100644 src/components/circular-slider.ts diff --git a/.devcontainer/config/configuration.yaml b/.devcontainer/config/configuration.yaml index 1110923..07dbb9c 100644 --- a/.devcontainer/config/configuration.yaml +++ b/.devcontainer/config/configuration.yaml @@ -49,6 +49,10 @@ template: unique_id: homeshift_day_mode state: "{{ states('input_select.homeshift_day_mode_backing') }}" options: "{{ state_attr('input_select.homeshift_day_mode_backing', 'options') }}" + attributes: + option_map: "{{ {'home': 'Maison', 'work': 'Travail', 'remote': 'Télétravail', 'away': 'Absence'} }}" + option_keys: "{{ {'home': 'Maison', 'work': 'Travail', 'remote': 'Télétravail', 'away': 'Absence'}.keys() | list }}" + current_key: "{{ {'Maison': 'home', 'Travail': 'work', 'Télétravail': 'remote', 'Absence': 'away'}.get(states('input_select.homeshift_day_mode_backing')) }}" select_option: service: input_select.select_option data: @@ -58,6 +62,10 @@ template: unique_id: homeshift_thermostat_mode state: "{{ states('input_select.homeshift_thermostat_mode_backing') }}" options: "{{ state_attr('input_select.homeshift_thermostat_mode_backing', 'options') }}" + attributes: + option_map: "{{ {'off': 'Eteint', 'heating': 'Chauffage', 'cooling': 'Climatisation', 'ventilation': 'Ventilation'} }}" + option_keys: "{{ {'off': 'Eteint', 'heating': 'Chauffage', 'cooling': 'Climatisation', 'ventilation': 'Ventilation'}.keys() | list }}" + current_key: "{{ {'Eteint': 'off', 'Chauffage': 'heating', 'Climatisation': 'cooling', 'Ventilation': 'ventilation'}.get(states('input_select.homeshift_thermostat_mode_backing')) }}" select_option: service: input_select.select_option data: diff --git a/dist/homeshift-card.js b/dist/homeshift-card.js index 4a9b37c..3a9331c 100644 --- a/dist/homeshift-card.js +++ b/dist/homeshift-card.js @@ -4,141 +4,43 @@ function t(t,e,i,o){var s,n=arguments.length,r=n<3?e:null===o?o=Object.getOwnPro * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const e=globalThis,i=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,o=Symbol(),s=new WeakMap;let n=class{constructor(t,e,i){if(this._$cssResult$=!0,i!==o)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const e=this.t;if(i&&void 0===t){const i=void 0!==e&&1===e.length;i&&(t=s.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),i&&s.set(e,t))}return t}toString(){return this.cssText}};const r=(t,...e)=>{const i=1===t.length?t[0]:e.reduce((e,i,o)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[o+1],t[0]);return new n(i,t,o)},a=i?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new n("string"==typeof t?t:t+"",void 0,o))(e)})(t):t,{is:c,defineProperty:h,getOwnPropertyDescriptor:l,getOwnPropertyNames:d,getOwnPropertySymbols:p,getPrototypeOf:_}=Object,u=globalThis,f=u.trustedTypes,m=f?f.emptyScript:"",v=u.reactiveElementPolyfillSupport,g=(t,e)=>t,y={toAttribute(t,e){switch(e){case Boolean:t=t?m:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},$=(t,e)=>!c(t,e),b={attribute:!0,type:String,converter:y,reflect:!1,useDefault:!1,hasChanged:$}; +const e=globalThis,i=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,o=Symbol(),s=new WeakMap;let n=class{constructor(t,e,i){if(this._$cssResult$=!0,i!==o)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const e=this.t;if(i&&void 0===t){const i=void 0!==e&&1===e.length;i&&(t=s.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),i&&s.set(e,t))}return t}toString(){return this.cssText}};const r=(t,...e)=>{const i=1===t.length?t[0]:e.reduce((e,i,o)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[o+1],t[0]);return new n(i,t,o)},a=i?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new n("string"==typeof t?t:t+"",void 0,o))(e)})(t):t,{is:c,defineProperty:l,getOwnPropertyDescriptor:h,getOwnPropertyNames:d,getOwnPropertySymbols:_,getPrototypeOf:p}=Object,u=globalThis,m=u.trustedTypes,f=m?m.emptyScript:"",v=u.reactiveElementPolyfillSupport,g=(t,e)=>t,y={toAttribute(t,e){switch(e){case Boolean:t=t?f:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},$=(t,e)=>!c(t,e),b={attribute:!0,type:String,converter:y,reflect:!1,useDefault:!1,hasChanged:$}; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */Symbol.metadata??=Symbol("metadata"),u.litPropertyMetadata??=new WeakMap;let x=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=b){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const i=Symbol(),o=this.getPropertyDescriptor(t,i,e);void 0!==o&&h(this.prototype,t,o)}}static getPropertyDescriptor(t,e,i){const{get:o,set:s}=l(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:o,set(e){const n=o?.call(this);s?.call(this,e),this.requestUpdate(t,n,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(g("elementProperties")))return;const t=_(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(g("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(g("properties"))){const t=this.properties,e=[...d(t),...p(t)];for(const i of e)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,e]of this.elementProperties){const i=this._$Eu(t,e);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(a(t))}else void 0!==t&&e.push(a(t));return e}static _$Eu(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.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(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const i of e.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((t,o)=>{if(i)t.adoptedStyleSheets=o.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const i of o){const o=document.createElement("style"),s=e.litNonce;void 0!==s&&o.setAttribute("nonce",s),o.textContent=i.cssText,t.appendChild(o)}})(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ET(t,e){const i=this.constructor.elementProperties.get(t),o=this.constructor._$Eu(t,i);if(void 0!==o&&!0===i.reflect){const s=(void 0!==i.converter?.toAttribute?i.converter:y).toAttribute(e,i.type);this._$Em=t,null==s?this.removeAttribute(o):this.setAttribute(o,s),this._$Em=null}}_$AK(t,e){const i=this.constructor,o=i._$Eh.get(t);if(void 0!==o&&this._$Em!==o){const t=i.getPropertyOptions(o),s="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:y;this._$Em=o;const n=s.fromAttribute(e,t.type);this[o]=n??this._$Ej?.get(o)??n,this._$Em=null}}requestUpdate(t,e,i,o=!1,s){if(void 0!==t){const n=this.constructor;if(!1===o&&(s=this[t]),i??=n.getPropertyOptions(t),!((i.hasChanged??$)(s,e)||i.useDefault&&i.reflect&&s===this._$Ej?.get(t)&&!this.hasAttribute(n._$Eu(t,i))))return;this.C(t,e,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:i,reflect:o,wrapped:s},n){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,n??e??this[t]),!0!==s||void 0!==n)||(this._$AL.has(t)||(this.hasUpdated||i||(e=void 0),this._$AL.set(t,e)),!0===o&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,i]of t){const{wrapped:t}=i,o=this[e];!0!==t||this._$AL.has(e)||void 0===o||this.C(e,void 0,i,o)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}};x.elementStyles=[],x.shadowRootOptions={mode:"open"},x[g("elementProperties")]=new Map,x[g("finalized")]=new Map,v?.({ReactiveElement:x}),(u.reactiveElementVersions??=[]).push("2.1.2"); + */Symbol.metadata??=Symbol("metadata"),u.litPropertyMetadata??=new WeakMap;let x=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=b){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const i=Symbol(),o=this.getPropertyDescriptor(t,i,e);void 0!==o&&l(this.prototype,t,o)}}static getPropertyDescriptor(t,e,i){const{get:o,set:s}=h(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:o,set(e){const n=o?.call(this);s?.call(this,e),this.requestUpdate(t,n,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(g("elementProperties")))return;const t=p(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(g("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(g("properties"))){const t=this.properties,e=[...d(t),..._(t)];for(const i of e)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,e]of this.elementProperties){const i=this._$Eu(t,e);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(a(t))}else void 0!==t&&e.push(a(t));return e}static _$Eu(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.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(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const i of e.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((t,o)=>{if(i)t.adoptedStyleSheets=o.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const i of o){const o=document.createElement("style"),s=e.litNonce;void 0!==s&&o.setAttribute("nonce",s),o.textContent=i.cssText,t.appendChild(o)}})(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ET(t,e){const i=this.constructor.elementProperties.get(t),o=this.constructor._$Eu(t,i);if(void 0!==o&&!0===i.reflect){const s=(void 0!==i.converter?.toAttribute?i.converter:y).toAttribute(e,i.type);this._$Em=t,null==s?this.removeAttribute(o):this.setAttribute(o,s),this._$Em=null}}_$AK(t,e){const i=this.constructor,o=i._$Eh.get(t);if(void 0!==o&&this._$Em!==o){const t=i.getPropertyOptions(o),s="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:y;this._$Em=o;const n=s.fromAttribute(e,t.type);this[o]=n??this._$Ej?.get(o)??n,this._$Em=null}}requestUpdate(t,e,i,o=!1,s){if(void 0!==t){const n=this.constructor;if(!1===o&&(s=this[t]),i??=n.getPropertyOptions(t),!((i.hasChanged??$)(s,e)||i.useDefault&&i.reflect&&s===this._$Ej?.get(t)&&!this.hasAttribute(n._$Eu(t,i))))return;this.C(t,e,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:i,reflect:o,wrapped:s},n){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,n??e??this[t]),!0!==s||void 0!==n)||(this._$AL.has(t)||(this.hasUpdated||i||(e=void 0),this._$AL.set(t,e)),!0===o&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,i]of t){const{wrapped:t}=i,o=this[e];!0!==t||this._$AL.has(e)||void 0===o||this.C(e,void 0,i,o)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}};x.elementStyles=[],x.shadowRootOptions={mode:"open"},x[g("elementProperties")]=new Map,x[g("finalized")]=new Map,v?.({ReactiveElement:x}),(u.reactiveElementVersions??=[]).push("2.1.2"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const w=globalThis,E=t=>t,A=w.trustedTypes,S=A?A.createPolicy("lit-html",{createHTML:t=>t}):void 0,C="$lit$",k=`lit$${Math.random().toFixed(9).slice(2)}$`,P="?"+k,T=`<${P}>`,O=document,M=()=>O.createComment(""),R=t=>null===t||"object"!=typeof t&&"function"!=typeof t,N=Array.isArray,U="[ \t\n\f\r]",H=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,D=/-->/g,I=/>/g,j=RegExp(`>|${U}(?:([^\\s"'>=/]+)(${U}*=${U}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),L=/'/g,z=/"/g,V=/^(?:script|style|textarea|title)$/i,B=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),W=B(1),q=B(2),F=Symbol.for("lit-noChange"),Y=Symbol.for("lit-nothing"),J=new WeakMap,K=O.createTreeWalker(O,129);function X(t,e){if(!N(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==S?S.createHTML(e):e}const Z=(t,e)=>{const i=t.length-1,o=[];let s,n=2===e?"":3===e?"":"",r=H;for(let e=0;e"===c[0]?(r=s??H,h=-1):void 0===c[1]?h=-2:(h=r.lastIndex-c[2].length,a=c[1],r=void 0===c[3]?j:'"'===c[3]?z:L):r===z||r===L?r=j:r===D||r===I?r=H:(r=j,s=void 0);const d=r===j&&t[e+1].startsWith("/>")?" ":"";n+=r===H?i+T:h>=0?(o.push(a),i.slice(0,h)+C+i.slice(h)+k+d):i+k+(-2===h?e:d)}return[X(t,n+(t[i]||"")+(2===e?"":3===e?"":"")),o]};class G{constructor({strings:t,_$litType$:e},i){let o;this.parts=[];let s=0,n=0;const r=t.length-1,a=this.parts,[c,h]=Z(t,e);if(this.el=G.createElement(c,i),K.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(o=K.nextNode())&&a.length0){o.textContent=A?A.emptyScript:"";for(let i=0;iN(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==Y&&R(this._$AH)?this._$AA.nextSibling.data=t:this.T(O.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:i}=t,o="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=G.createElement(X(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===o)this._$AH.p(e);else{const t=new tt(o,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=J.get(t.strings);return void 0===e&&J.set(t.strings,e=new G(t)),e}k(t){N(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,o=0;for(const s of t)o===e.length?e.push(i=new et(this.O(M()),this.O(M()),this,this.options)):i=e[o],i._$AI(s),o++;o2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=Y}_$AI(t,e=this,i,o){const s=this.strings;let n=!1;if(void 0===s)t=Q(this,t,e,0),n=!R(t)||t!==this._$AH&&t!==F,n&&(this._$AH=t);else{const o=t;let r,a;for(t=s[0],r=0;rt,E=w.trustedTypes,S=E?E.createPolicy("lit-html",{createHTML:t=>t}):void 0,C="$lit$",k=`lit$${Math.random().toFixed(9).slice(2)}$`,T="?"+k,P=`<${T}>`,O=document,N=()=>O.createComment(""),M=t=>null===t||"object"!=typeof t&&"function"!=typeof t,U=Array.isArray,H="[ \t\n\f\r]",R=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,D=/-->/g,j=/>/g,z=RegExp(`>|${H}(?:([^\\s"'>=/]+)(${H}*=${H}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),L=/'/g,I=/"/g,B=/^(?:script|style|textarea|title)$/i,V=(t=>(e,...i)=>({_$litType$:t,strings:e,values:i}))(1),q=Symbol.for("lit-noChange"),W=Symbol.for("lit-nothing"),Y=new WeakMap,F=O.createTreeWalker(O,129);function K(t,e){if(!U(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==S?S.createHTML(e):e}const J=(t,e)=>{const i=t.length-1,o=[];let s,n=2===e?"":3===e?"":"",r=R;for(let e=0;e"===c[0]?(r=s??R,l=-1):void 0===c[1]?l=-2:(l=r.lastIndex-c[2].length,a=c[1],r=void 0===c[3]?z:'"'===c[3]?I:L):r===I||r===L?r=z:r===D||r===j?r=R:(r=z,s=void 0);const d=r===z&&t[e+1].startsWith("/>")?" ":"";n+=r===R?i+P:l>=0?(o.push(a),i.slice(0,l)+C+i.slice(l)+k+d):i+k+(-2===l?e:d)}return[K(t,n+(t[i]||"")+(2===e?"":3===e?"":"")),o]};class Z{constructor({strings:t,_$litType$:e},i){let o;this.parts=[];let s=0,n=0;const r=t.length-1,a=this.parts,[c,l]=J(t,e);if(this.el=Z.createElement(c,i),F.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(o=F.nextNode())&&a.length0){o.textContent=E?E.emptyScript:"";for(let i=0;iU(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==W&&M(this._$AH)?this._$AA.nextSibling.data=t:this.T(O.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:i}=t,o="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=Z.createElement(K(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===o)this._$AH.p(e);else{const t=new Q(o,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=Y.get(t.strings);return void 0===e&&Y.set(t.strings,e=new Z(t)),e}k(t){U(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,o=0;for(const s of t)o===e.length?e.push(i=new X(this.O(N()),this.O(N()),this,this.options)):i=e[o],i._$AI(s),o++;o2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=W}_$AI(t,e=this,i,o){const s=this.strings;let n=!1;if(void 0===s)t=G(this,t,e,0),n=!M(t)||t!==this._$AH&&t!==q,n&&(this._$AH=t);else{const o=t;let r,a;for(t=s[0],r=0;r{const o=i?.renderBefore??e;let s=o._$litPart$;if(void 0===s){const t=i?.renderBefore??null;o._$litPart$=s=new et(e.insertBefore(M(),t),t,void 0,i??{})}return s._$AI(t),s})(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return F}}ht._$litElement$=!0,ht.finalized=!0,ct.litElementHydrateSupport?.({LitElement:ht});const lt=ct.litElementPolyfillSupport;lt?.({LitElement:ht}),(ct.litElementVersions??=[]).push("4.2.2"); + */class at extends x{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=((t,e,i)=>{const o=i?.renderBefore??e;let s=o._$litPart$;if(void 0===s){const t=i?.renderBefore??null;o._$litPart$=s=new X(e.insertBefore(N(),t),t,void 0,i??{})}return s._$AI(t),s})(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return q}}at._$litElement$=!0,at.finalized=!0,rt.litElementHydrateSupport?.({LitElement:at});const ct=rt.litElementPolyfillSupport;ct?.({LitElement:at}),(rt.litElementVersions??=[]).push("4.2.2"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const dt={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:$},pt=(t=dt,e,i)=>{const{kind:o,metadata:s}=i;let n=globalThis.litPropertyMetadata.get(s);if(void 0===n&&globalThis.litPropertyMetadata.set(s,n=new Map),"setter"===o&&((t=Object.create(t)).wrapped=!0),n.set(i.name,t),"accessor"===o){const{name:o}=i;return{set(i){const s=e.get.call(this);e.set.call(this,i),this.requestUpdate(o,s,t,!0,i)},init(e){return void 0!==e&&this.C(o,void 0,t,e),e}}}if("setter"===o){const{name:o}=i;return function(i){const s=this[o];e.call(this,i),this.requestUpdate(o,s,t,!0,i)}}throw Error("Unsupported decorator location: "+o)};function _t(t){return(e,i)=>"object"==typeof i?pt(t,e,i):((t,e,i)=>{const o=e.hasOwnProperty(i);return e.constructor.createProperty(i,t),o?Object.getOwnPropertyDescriptor(e,i):void 0})(t,e,i)} +const lt={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:$},ht=(t=lt,e,i)=>{const{kind:o,metadata:s}=i;let n=globalThis.litPropertyMetadata.get(s);if(void 0===n&&globalThis.litPropertyMetadata.set(s,n=new Map),"setter"===o&&((t=Object.create(t)).wrapped=!0),n.set(i.name,t),"accessor"===o){const{name:o}=i;return{set(i){const s=e.get.call(this);e.set.call(this,i),this.requestUpdate(o,s,t,!0,i)},init(e){return void 0!==e&&this.C(o,void 0,t,e),e}}}if("setter"===o){const{name:o}=i;return function(i){const s=this[o];e.call(this,i),this.requestUpdate(o,s,t,!0,i)}}throw Error("Unsupported decorator location: "+o)};function dt(t){return(e,i)=>"object"==typeof i?ht(t,e,i):((t,e,i)=>{const o=e.hasOwnProperty(i);return e.constructor.createProperty(i,t),o?Object.getOwnPropertyDescriptor(e,i):void 0})(t,e,i)} /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */function ut(t){return _t({...t,state:!0,attribute:!1})} -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */const ft={en:{card:{title:"Thermostat",entity_not_found:"Entity not found: {entity}",off:"Off",next_scan:"Next scan",in:"in",today:"Today",tomorrow:"Tomorrow",past:"past",heat_protection_active:"Heat protection active",cover_open_time:"Cover opening time",cover_close_time:"Cover closing time",cover_open_action:"Open now",cover_close_action:"Close now",covers_left_open:"Covers left open: {covers}"},thermostat:{heating:"Heating",cooling:"Cooling",ventilation:"Ventilation"},editor:{name:"Name",day_mode_entity:"Day Mode Entity",hermostat_mode_tentity:"Thermostat Mode Entity",cover_open_time_entity:"Cover Open Time Entity",cover_close_time_entity:"Cover Close Time Entity",cover_entity:"Cover Entity (manual open/close)",covers_left_open_entity:"Covers Left Open Entity",show_title:"Show title"}},fr:{card:{title:"Thermostat",entity_not_found:"Entité introuvable: {entity}",off:"Éteint",next_scan:"Prochain scan",in:"dans",today:"Auj.",tomorrow:"Dem.",past:"passé",heat_protection_active:"Protection thermique active",cover_open_time:"Heure d'ouverture des volets",cover_close_time:"Heure de fermeture des volets",cover_open_action:"Ouvrir maintenant",cover_close_action:"Fermer maintenant",covers_left_open:"Volets non fermés : {covers}"},thermostat:{heating:"Chauffage",cooling:"Climatisation",ventilation:"Ventilation"},editor:{name:"Nom",day_mode_entity:"Entité mode jour",hermostat_mode_tentity:"Entité mode thermostat",cover_open_time_entity:"Entité heure d'ouverture des volets",cover_close_time_entity:"Entité heure de fermeture des volets",cover_entity:"Entité volet (ouverture/fermeture manuelle)",covers_left_open_entity:"Entité volets non fermés",show_title:"Afficher le titre"}}};function mt(t,e,i){const o=function(t){const e=t?.locale?.language||t?.language||"en",i=String(e).split("-")[0];return ft[e]?e:ft[i]?i:"en"}(t);let s=(n=ft[o]||ft.en,e.split(".").reduce((t,e)=>t&&null!=t[e]?t[e]:void 0,n));var n;return s?(i&&Object.entries(i).forEach(([t,e])=>s=s.replace(`{${t}}`,e)),s):e}const vt="M 30 150 A 85 85 0 1 1 170 150",gt=34.2,yt=-145.8,$t=["var(--disabled-text-color, #9e9e9e)","#e74c3c","var(--primary-color, #3b82f6)","#d4a574"];class bt extends ht{constructor(){super(...arguments),this.options=[],this.labels=[],this.selectedIndex=-1}_getColorForIndex(t){return $t[t%$t.length]??"var(--primary-color)"}_valueToPercentage(t){return t/this.options.length}_strokeDashArc(t,e){const i=this._valueToPercentage(t);return[`${this._valueToPercentage(e)-i} 10`,`-${i}`]}_getPercentageFromEvent(t){if(!this._svg)return-1;const e=this._svg.getBoundingClientRect(),i=2*(t.clientX-e.left-e.width/2)/e.width,o=2*(t.clientY-e.top-e.height/2)/e.height,s=180*Math.atan2(o,i)/Math.PI;return s>=yt&&s<=gt?(gt-s)/180:s>=214.2?(gt+(360-s))/180:-1}_onSvgClick(t){const e=this._getPercentageFromEvent(t);if(e<0||0===this.options.length)return;const i=Math.floor(e*this.options.length),o=Math.max(0,Math.min(i,this.options.length-1));this.dispatchEvent(new CustomEvent("option-selected",{detail:{option:this.options[o]},bubbles:!0,composed:!0}))}render(){if(!this.options||0===this.options.length)return W`
`;const t=this.currentValue?this.options.indexOf(this.currentValue):-1;return this.selectedIndex=-1!==t?t:-1,W` -
- - - - - - ${q` - - `} ${-1!==this.selectedIndex?(()=>{const[t,e]=this._strokeDashArc(this.selectedIndex,this.selectedIndex+1),i=this._getColorForIndex(this.selectedIndex);return q` - - `})():null} - ${(()=>{const t=this.options.length;if(t<2)return null;const e=.002,i=1/t,o=[0,i-e];for(let e=0;e - `})()} - ${this.options.map((t,e)=>{const[i,o]=this._strokeDashArc(e,e+1);return q` - {t.stopPropagation(),this.dispatchEvent(new CustomEvent("option-selected",{detail:{option:this.options[e]},bubbles:!0,composed:!0}))}} - /> - `})} - ${this.options.map((t,e)=>{const i=100*((this._valueToPercentage(e)+this._valueToPercentage(e+1))/2),o=this.labels[e]??this.options[e];return q` - {t.stopPropagation(),this.dispatchEvent(new CustomEvent("option-selected",{detail:{option:this.options[e]},bubbles:!0,composed:!0}))}} - > - - ${o} - - - `})} - -
- `}}bt.styles=r` - :host { - display: block; - } - - .slider-container { - display: flex; - justify-content: center; - padding: 8px 0; - } - - svg { - width: 100%; - max-width: 240px; - aspect-ratio: 1; - } - `,t([_t({attribute:!1})],bt.prototype,"hass",void 0),t([_t()],bt.prototype,"entityId",void 0),t([_t()],bt.prototype,"currentValue",void 0),t([_t({type:Array})],bt.prototype,"options",void 0),t([_t({type:Array})],bt.prototype,"labels",void 0),t([_t({type:Number})],bt.prototype,"selectedIndex",void 0),t([ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */ -function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect.decorate&&"object"!=typeof e&&Object.defineProperty(t,e,i),i))(e,i,{get(){return(e=>e.renderRoot?.querySelector(t)??null)(this)}})}("svg")],bt.prototype,"_svg",void 0),customElements.define("homeshift-circular-slider",bt);class xt extends ht{setConfig(t){this._config=t}_onNameChanged(t){if(!this._config||!this.hass)return;const e=t.target.value;if(this._config.name===e)return;const i={...this._config,name:e};this._config=i,this._dispatchConfigChanged(i)}_onEntityChanged(t,e){if(!this._config||!this.hass)return;const i=t.detail.value;if(this._config[e]===i)return;const o={...this._config,[e]:i};this._config=o,this._dispatchConfigChanged(o)}_onShowTitleChanged(t){if(!this._config||!this.hass)return;const e=t.target.checked;if(this._config.show_title===e)return;const i={...this._config,show_title:e};this._config=i,this._dispatchConfigChanged(i)}_dispatchConfigChanged(t){const e=new CustomEvent("config-changed",{detail:{config:t},bubbles:!0,composed:!0});this.dispatchEvent(e)}render(){return this.hass&&this._config?W` + */function _t(t){return dt({...t,state:!0,attribute:!1})}const pt={en:{card:{entity_not_found:"Entity not found: {entity}",off:"Off",next_scan:"Next scan",in:"in",today:"Today",tomorrow:"Tomorrow",past:"past",heat_protection_active:"Heat protection active",cover_open_time:"Cover opening time",cover_close_time:"Cover closing time",cover_open_action:"Open now",cover_close_action:"Close now",covers_left_open:"Not closed: {covers}",day_mode:"Day mode",next:"Next",covers:"Covers",duration_off:"No",override_label:"Keep my manual choice",override_none:"A manual change is replaced at the calendar's next pass.",override_effect:"After a change by hand, the calendar waits {duration} — until {time} if you change it now.",early_label:"Start events early",early_none:"{mode} will start at its scheduled time, {time}.",early_effect:"{mode} will start at {time} instead of {scheduled}.",early_no_event:"No event scheduled.",covers_left_open_count:"Not closed: {count} covers"},thermostat:{off:"Off",heating:"Heating",cooling:"Cooling",ventilation:"Ventilation"},editor:{name:"Name",day_mode_entity:"Day Mode Entity",hermostat_mode_tentity:"Thermostat Mode Entity",cover_open_time_entity:"Cover Open Time Entity",cover_close_time_entity:"Cover Close Time Entity",cover_entity:"Cover Entity (manual open/close)",covers_left_open_entity:"Covers Left Open Entity"}},fr:{card:{entity_not_found:"Entité introuvable: {entity}",off:"Éteint",next_scan:"Prochain scan",in:"dans",today:"Auj.",tomorrow:"Dem.",past:"passé",heat_protection_active:"Protection thermique active",cover_open_time:"Heure d'ouverture des volets",cover_close_time:"Heure de fermeture des volets",cover_open_action:"Ouvrir maintenant",cover_close_action:"Fermer maintenant",covers_left_open:"Non fermés : {covers}",day_mode:"Mode du jour",next:"Prochain",covers:"Volets",duration_off:"Non",override_label:"Garder mon choix manuel",override_none:"Un changement manuel est remplacé au prochain passage du calendrier.",override_effect:"Après un changement à la main, le calendrier attend {duration} — jusqu'à {time} si tu changes maintenant.",early_label:"Anticiper les événements",early_none:"{mode} commencera à l'heure prévue, {time}.",early_effect:"{mode} commencera à {time} au lieu de {scheduled}.",early_no_event:"Aucun événement prévu.",covers_left_open_count:"Non fermés : {count} volets"},thermostat:{off:"Éteint",heating:"Chauffage",cooling:"Climatisation",ventilation:"Ventilation"},editor:{name:"Nom",day_mode_entity:"Entité mode jour",hermostat_mode_tentity:"Entité mode thermostat",cover_open_time_entity:"Entité heure d'ouverture des volets",cover_close_time_entity:"Entité heure de fermeture des volets",cover_entity:"Entité volet (ouverture/fermeture manuelle)",covers_left_open_entity:"Entité volets non fermés"}}};function ut(t,e,i){const o=function(t){const e=t?.locale?.language||t?.language||"en",i=String(e).split("-")[0];return pt[e]?e:pt[i]?i:"en"}(t);let s=(n=pt[o]||pt.en,e.split(".").reduce((t,e)=>t&&null!=t[e]?t[e]:void 0,n));var n;return s?(i&&Object.entries(i).forEach(([t,e])=>s=s.replace(`{${t}}`,e)),s):e}class mt extends at{setConfig(t){this._config=t}_onNameChanged(t){if(!this._config||!this.hass)return;const e=t.target.value;if(this._config.name===e)return;const i={...this._config,name:e};this._config=i,this._dispatchConfigChanged(i)}_onEntityChanged(t,e){if(!this._config||!this.hass)return;const i=t.detail.value;if(this._config[e]===i)return;const o={...this._config,[e]:i};this._config=o,this._dispatchConfigChanged(o)}_dispatchConfigChanged(t){const e=new CustomEvent("config-changed",{detail:{config:t},bubbles:!0,composed:!0});this.dispatchEvent(e)}render(){return this.hass&&this._config?V`
this._onEntityChanged(t,"day_mode_entity")} @@ -146,7 +48,7 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. > this._onEntityChanged(t,"hermostat_mode_tentity")} @@ -154,7 +56,7 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. > this._onEntityChanged(t,"cover_open_time_entity")} @@ -162,7 +64,7 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. > this._onEntityChanged(t,"cover_close_time_entity")} @@ -170,7 +72,7 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. > ((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. > this._onEntityChanged(t,"covers_left_open_entity")} allow-custom-entity > - - - -
- `:W``}}xt.styles=r` + `:V``}}mt.styles=r` .card-config { display: flex; flex-direction: column; @@ -212,382 +107,366 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. align-items: center; padding: 8px 0; } - `,t([_t({attribute:!1})],xt.prototype,"hass",void 0),t([ut()],xt.prototype,"_config",void 0),customElements.get("homeshift-card-editor")||customElements.define("homeshift-card-editor",xt);class wt extends ht{constructor(){super(...arguments),this._tick=0}connectedCallback(){super.connectedCallback(),this._refreshInterval=setInterval(()=>{this._tick++},3e4)}disconnectedCallback(){super.disconnectedCallback(),clearInterval(this._refreshInterval)}static getStubConfig(){return{name:"Thermostat",day_mode_entity:"select.homeshift_day_mode",thermostat_mode_entity:"select.homeshift_thermostat_mode",override_duration_entity:"number.homeshift_override_duration",early_switch_entity:"number.homeshift_early_switch",next_mode_entity:"sensor.homeshift_next_mode",next_mode_at_entity:"sensor.homeshift_next_mode_at",heat_protection_entity:"binary_sensor.homeshift_cover_heat_active",cover_open_time_entity:"sensor.homeshift_cover_open_time",cover_close_time_entity:"sensor.homeshift_cover_close_time",cover_entity:"cover.homeshift_daily_covers",covers_left_open_entity:"binary_sensor.homeshift_covers_left_open",show_title:!0}}static getConfigElement(){return document.createElement("homeshift-card-editor")}setConfig(t){if(!t)throw new Error("Missing configuration");this._config={name:t.name??"Thermostat",day_mode_entity:t.day_mode_entity??"select.homeshift_day_mode",thermostat_mode_entity:t.thermostat_mode_entity??"select.homeshift_thermostat_mode",override_duration_entity:t.override_duration_entity??"number.homeshift_override_duration",early_switch_entity:t.early_switch_entity??"number.homeshift_early_switch",next_mode_entity:t.next_mode_entity??"sensor.homeshift_next_mode",next_mode_at_entity:t.next_mode_at_entity??"sensor.homeshift_next_mode_at",heat_protection_entity:t.heat_protection_entity??"binary_sensor.homeshift_cover_heat_active",cover_open_time_entity:t.cover_open_time_entity??"sensor.homeshift_cover_open_time",cover_close_time_entity:t.cover_close_time_entity??"sensor.homeshift_cover_close_time",cover_entity:t.cover_entity??"",covers_left_open_entity:t.covers_left_open_entity??"binary_sensor.homeshift_covers_left_open",show_title:!1!==t.show_title}}getCardSize(){return 4}shouldUpdate(t){if(t.has("_config"))return!0;if(t.has("_tick"))return!0;if(t.has("hass")){const e=t.get("hass");if(!e)return!0;return[this._config?.day_mode_entity,this._config?.thermostat_mode_entity,this._config?.override_duration_entity,this._config?.early_switch_entity,this._config?.next_mode_entity,this._config?.next_mode_at_entity,this._config?.heat_protection_entity,this._config?.cover_open_time_entity,this._config?.cover_close_time_entity,this._config?.cover_entity,this._config?.covers_left_open_entity].filter(Boolean).some(t=>e.states[t]!==this.hass.states[t])}return!1}getEntityState(t){if(t)return this.hass?.states?.[t]}getCoversLeftOpen(){const t=this.getEntityState(this._config?.covers_left_open_entity)?.attributes?.covers;return Array.isArray(t)?t.map(t=>this.hass?.states?.[t]?.attributes?.friendly_name??t):[]}onSelect(t,e){const i=e.target,o=i?.value;o&&this.hass.callService("select","select_option",{entity_id:t,option:o})}onCircularSliderSelect(t,e){this.hass.callService("select","select_option",{entity_id:t,option:e})}onOffButtonClick(t,e){e&&this.hass.callService("select","select_option",{entity_id:t,option:e})}onCoverAction(t){const e=this._config.cover_entity;e&&this.hass.callService("cover",t,{entity_id:e})}onOverrideDurationSelect(t){const e=this._config.override_duration_entity;if(!e)return;const i=Number(t.target.value);this.hass.callService("number","set_value",{entity_id:e,value:i})}onEarlySwitchSelect(t){const e=this._config.early_switch_entity;if(!e)return;const i=Number(t.target.value);this.hass.callService("number","set_value",{entity_id:e,value:i})}_formatRelativeTime(t){if(!t)return"";const e=new Date(t);if(isNaN(e.getTime()))return t;const i=e.getTime()-Date.now(),o=Math.round(i/6e4);if(o<=0)return mt(this.hass,"card.past")||"passé";if(o<60)return`${mt(this.hass,"card.in")||"dans"} ${o} min`;const s=Math.floor(o/60),n=o%60;return`${mt(this.hass,"card.in")||"dans"} ${s}h${n>0?n.toString().padStart(2,"0"):""}`}_formatAbsoluteTime(t){if(!t)return"";const e=new Date(t);if(isNaN(e.getTime()))return t;const i=new Date,o=new Date(i);o.setDate(o.getDate()+1);const s=e.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});return e.toDateString()===i.toDateString()?`${mt(this.hass,"card.today")||"Auj."} ${s}`:e.toDateString()===o.toDateString()?`${mt(this.hass,"card.tomorrow")||"Dem."} ${s}`:`${e.toLocaleDateString([],{month:"short",day:"numeric"})} ${s}`}_renderMain(t,e,i,o,s,n){const r=e.attributes?.option_map??{},a=Object.keys(r).length>0?Object.entries(r):(e.attributes?.options??[]).map(t=>[t,t]),c=t.attributes?.option_map??{},h=Object.keys(c).length>0,l=h?c.off??t.attributes?.options?.[0]??"":t.attributes?.options?.[0]??"",d=h?[l,...Object.entries(c).filter(([t])=>"off"!==t).map(([,t])=>t)]:t.attributes?.options??[],p=h?[mt(this.hass,"card.off")||l,...Object.keys(c).filter(t=>"off"!==t).map(t=>mt(this.hass,`thermostat.${t}`)||t)]:[mt(this.hass,"card.off")||(t.attributes?.options?.[0]??""),...["heating","cooling","ventilation"].map(t=>mt(this.hass,`thermostat.${t}`)||t)],_=this.getEntityState(this._config.next_mode_entity),u=this.getEntityState(this._config.next_mode_at_entity),f=Number(this.getEntityState(this._config.early_switch_entity)?.state??0),m=_?.state&&"unknown"!==_.state&&""!==_.state,v=s&&"unknown"!==s&&"unavailable"!==s,g=n&&"unknown"!==n&&"unavailable"!==n,y=Boolean(this._config.cover_entity);return W` -
- ${v||g?W`
- ${v?W`
this.onCoverAction("open_cover")} - > - - ${s} -
`:Y} - ${g?W`
this.onCoverAction("close_cover")} - > - - ${n} -
`:Y} -
`:Y} - ${i||o.length>0?W`
- ${i?W`
- -
`:Y} - ${o.length>0?W`
- -
`:Y} -
`:Y} - this.onCircularSliderSelect(t.entity_id,e.detail.option)} - > - -
- ${m||u?.state?W`
- ${m?W`${_.state}`:Y} - ${u?.state?W`${this._formatAbsoluteTime(u.state)}`:Y} -
`:Y} - ${this._config.early_switch_entity?W``:Y} + − + + ${this._settingLabel(n)} + +
+

${o}

+
`:W} + `}_renderMain(t,e,i,o,s,n){const r=e.attributes?.option_map??{},a=Object.keys(r).length>0?Object.entries(r):(e.attributes?.options??[]).map(t=>[t,t]),c=t.attributes?.option_map??{},l=Object.keys(c).length>0?Object.entries(c):(t.attributes?.options??[]).map(t=>[t,t]),h=this.getEntityState(this._config.next_mode_entity),d=this.getEntityState(this._config.next_mode_at_entity),_=Number(this.getEntityState(this._config.early_switch_entity)?.state??0),p=Number(this.getEntityState(this._config.override_duration_entity)?.state??0),u=h?.state&&"unknown"!==h.state&&""!==h.state,m=u?h.state:"",f=d?.state&&"unknown"!==d.state&&"unavailable"!==d.state,v=p?ut(this.hass,"card.override_effect",{duration:this._settingLabel(p),time:this._clockIn(p)}):ut(this.hass,"card.override_none"),g=f?_?ut(this.hass,"card.early_effect",{mode:m,time:this._clockAt(d.state),scheduled:this._clockAt(d.state,_)}):ut(this.hass,"card.early_none",{mode:m,time:this._clockAt(d.state)}):ut(this.hass,"card.early_no_event"),y=s&&"unknown"!==s&&"unavailable"!==s,$=n&&"unknown"!==n&&"unavailable"!==n,b=Boolean(this._config.cover_entity),x=o.length>3?ut(this.hass,"card.covers_left_open_count",{count:String(o.length)}):ut(this.hass,"card.covers_left_open",{covers:o.join(", ")}),w=t.attributes?.current_key;return V` +
+
+ ${l.map(([e,i])=>{const o=w?e===w:i===t.state;return V``})}
-
-
-
- this.onSelect(e.entity_id,t)} + > + ${a.map(([t,i])=>V``)} - -
+ ${i} + `)} + +
- ${(()=>{const t=Number(this.getEntityState(this._config.override_duration_entity)?.state??0);return W``})()} -
-
+ + ${x} + `:W} + `:W} - `}render(){if(!this.hass||!this._config)return Y;const t=this._config.name??mt(this.hass,"card.title"),e=this.getEntityState(this._config.day_mode_entity),i=this.getEntityState(this._config.thermostat_mode_entity),o=e??{entity_id:this._config.day_mode_entity??"select.homeshift_day_mode",state:mt(this.hass,"preview.day_mode_state")||"Travail",attributes:{options:["Maison","Travail","Télétravail","Absence"],option_map:{home:"Maison",work:"Travail",remote:"Télétravail",away:"Absence"}}},s=i??{entity_id:this._config.thermostat_mode_entity??"select.homeshift_thermostat_mode",state:mt(this.hass,"preview.thermostat_state")||"Chauffage",attributes:{options:["Eteint","Chauffage","Climatisation","Ventilation"],option_map:{off:"Eteint",heating:"Chauffage",cooling:"Climatisation",ventilation:"Ventilation"}}},n="on"===this.getEntityState(this._config.heat_protection_entity)?.state,r=this.getEntityState(this._config.cover_open_time_entity)?.state,a=this.getEntityState(this._config.cover_close_time_entity)?.state;return W` - + `}render(){if(!this.hass||!this._config)return W;const t=this.getEntityState(this._config.day_mode_entity),e=this.getEntityState(this._config.thermostat_mode_entity),i=t??{entity_id:this._config.day_mode_entity??"select.homeshift_day_mode",state:ut(this.hass,"preview.day_mode_state")||"Travail",attributes:{options:["Maison","Travail","Télétravail","Absence"],option_map:{home:"Maison",work:"Travail",remote:"Télétravail",away:"Absence"}}},o=e??{entity_id:this._config.thermostat_mode_entity??"select.homeshift_thermostat_mode",state:ut(this.hass,"preview.thermostat_state")||"Chauffage",attributes:{options:["Eteint","Chauffage","Climatisation","Ventilation"],option_map:{off:"Eteint",heating:"Chauffage",cooling:"Climatisation",ventilation:"Ventilation"}}},s="on"===this.getEntityState(this._config.heat_protection_entity)?.state,n=this.getEntityState(this._config.cover_open_time_entity)?.state,r=this.getEntityState(this._config.cover_close_time_entity)?.state;return V` +
- ${this._renderMain(s,o,n,this.getCoversLeftOpen(),r,a)} + ${this._renderMain(o,i,s,this.getCoversLeftOpen(),n,r)}
- `}}wt.OVERRIDE_PRESETS=[{label:"--",value:0},{label:"15min",value:15},{label:"30min",value:30},{label:"1h",value:60},{label:"2h",value:120},{label:"4h",value:240},{label:"8h",value:480},{label:"12h",value:720}],wt.EARLY_SWITCH_PRESETS=[{label:"--",value:0},{label:"15min",value:15},{label:"30min",value:30},{label:"45min",value:45},{label:"1h",value:60},{label:"1h30",value:90},{label:"2h",value:120},{label:"3h",value:180},{label:"4h",value:240}],wt.styles=r` + `}}ft.OVERRIDE_PRESETS=[{label:"--",value:0},{label:"15min",value:15},{label:"30min",value:30},{label:"1h",value:60},{label:"2h",value:120},{label:"4h",value:240},{label:"8h",value:480},{label:"12h",value:720}],ft.EARLY_SWITCH_PRESETS=[{label:"--",value:0},{label:"15min",value:15},{label:"30min",value:30},{label:"45min",value:45},{label:"1h",value:60},{label:"1h30",value:90},{label:"2h",value:120},{label:"3h",value:180},{label:"4h",value:240}],ft.styles=r` ha-card { - padding: 8px; + padding: 12px; position: relative; - min-height: 240px; - text-align: center; } .container { - display: flex; - justify-content: center; width: 100%; } - /* SLIDER VIEW */ - .thermo-section { - position: relative; - width: 100%; - max-width: 240px; + .rows { display: flex; flex-direction: column; - align-items: center; } - homeshift-circular-slider { - width: 100%; + /* Thermostat presets — one segmented control, one colour per mode. */ + .presets { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 1fr; + border: 1px solid var(--divider-color, #ccc); + border-radius: 8px; + overflow: hidden; + margin-bottom: 4px; } - .thermo-bottom { - position: absolute; - bottom: 16px; - left: 50%; - transform: translateX(-50%); - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - z-index: 2; - width: 90%; + .preset { + border: 0; + border-left: 1px solid var(--divider-color, #ccc); + background: transparent; + color: var(--secondary-text-color, #666); + font-size: 13px; + font-family: inherit; + padding: 8px 4px; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: + background 0.2s ease, + color 0.2s ease; } - .off-row { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; + .preset:first-child { + border-left: 0; } - .bottom-controls { - flex: 1; - display: flex; - align-items: center; - gap: 8px; + .preset:hover { + background: var(--secondary-background-color, rgba(127, 127, 127, 0.12)); } - .day-section { - flex: 1; - min-width: 0; + .preset.on { + background: var(--secondary-text-color, #666); + color: var(--text-primary-color, #fff); } - .day-section select { - width: 100%; + .preset--heating.on { + background: var(--state-climate-heat-color, #e24b4a); + } + + .preset--cooling.on { + background: var(--state-climate-cool-color, #378add); } - .center-button { + .preset--ventilation.on { + background: var(--state-fan-active-color, #1d9e75); + } + + .preset--off.on { + background: var(--divider-color, #9e9e9e); + color: var(--primary-text-color); + } + + .row { display: flex; align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 50%; - border: 1px solid var(--divider-color, #ccc); - background: var(--card-background-color, #ffffff); - cursor: pointer; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - transition: - background 0.2s, - border-color 0.2s; + justify-content: space-between; + gap: 8px; + padding: 8px 0; + border-top: 1px solid var(--divider-color, #e0e0e0); + font-size: 13px; } - .center-button ha-icon { + .row-key { color: var(--secondary-text-color, #666); } - .center-button.active { - background: var(--primary-color); - border-color: var(--primary-color); + /* Collapsed by default: the value is readable without opening anything. */ + .setting { + border-top: 1px solid var(--divider-color, #e0e0e0); } - .center-button.active ha-icon { - color: var(--text-primary-color, #fff); + .setting-head { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px 0; + border: 0; + background: transparent; + color: var(--primary-text-color); + font-size: 13px; + font-family: inherit; + text-align: left; + cursor: pointer; } - hui-card { - --ha-card-box-shadow: none; - --ha-card-border-width: 0; + .setting-head ha-icon { + --mdc-icon-size: 18px; + color: var(--secondary-text-color, #666); + flex-shrink: 0; } - @keyframes fadeIn { - from { - opacity: 0; - transform: translateY(5px); - } - to { - opacity: 1; - transform: translateY(0); - } + .setting-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - /* UI ELEMENTS */ - select { - padding: 8px; - border-radius: 6px; - border: 1px solid var(--divider-color, #ccc); - background: var(--card-background-color); - color: var(--primary-text-color); + .setting-value { + color: var(--secondary-text-color, #666); } - .list-select { - flex-shrink: 0; - align-self: stretch; - padding: 0 8px; - border-radius: 6px; - border: 1px solid var(--divider-color, #ccc); - background: var(--card-background-color); + .setting-value.set { color: var(--primary-text-color); - font-size: 13px; - cursor: pointer; - transition: - background 0.2s ease, - border-color 0.2s ease, - color 0.2s ease; + font-weight: 500; } - .list-select--override.active { - border-color: var(--primary-color); - background: var(--primary-color); - color: var(--text-primary-color, #fff); + .setting .chevron { + transition: transform 0.15s ease; } - .list-select--early.active { - border-color: var(--accent-color, var(--primary-color)); - background: var(--accent-color, var(--primary-color)); - color: var(--text-primary-color, #fff); + .setting.open .chevron { + transform: rotate(180deg); } - .list-select--early { - padding: 8px; + .setting-body { + padding: 0 0 10px 26px; + animation: fadeIn 0.15s ease; } - .next-info-group { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + .stepper { display: flex; - flex-direction: column; align-items: center; - gap: 6px; - z-index: 2; - text-align: center; - background: rgba(128, 128, 128, 0.12); - border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.1)); - border-radius: 10px; - padding: 6px 12px; + gap: 10px; } - .next-info { - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; - pointer-events: none; + .stepper button { + width: 34px; + height: 34px; + border-radius: 8px; + border: 1px solid var(--divider-color, #ccc); + background: var(--card-background-color); + color: var(--primary-text-color); + font-size: 18px; + line-height: 1; + cursor: pointer; } - .next-mode-state { - font-size: 13px; - font-weight: 600; - color: var(--primary-text-color); + .stepper button:hover:not(:disabled) { + background: var(--secondary-background-color, rgba(127, 127, 127, 0.12)); + } + + .stepper button:disabled { + opacity: 0.4; + cursor: default; } - .next-mode-at-state { - font-size: 11px; - color: var(--secondary-text-color); + .stepper-value { + min-width: 64px; + text-align: center; + font-size: 15px; + font-weight: 500; } - .error { - color: var(--error-color); - padding: 16px; + .setting-effect { + margin: 8px 0 0; + font-size: 12px; + line-height: 1.4; + color: var(--secondary-text-color, #666); } .cover-times { - position: absolute; - right: 100%; - margin-right: 8px; - top: 4%; display: flex; - flex-direction: column; - gap: 4px; - z-index: 2; - animation: fadeIn 0.3s ease; + gap: 6px; } - .cover-time-row { - display: flex; + .cover-time { + display: inline-flex; align-items: center; gap: 4px; - padding: 2px 6px; - border-radius: 10px; - background: rgba(128, 128, 128, 0.12); - border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.1)); - font-size: 11px; - color: var(--secondary-text-color); - white-space: nowrap; + padding: 4px 8px; + border-radius: 8px; + border: 1px solid var(--divider-color, #ccc); + background: var(--card-background-color); + color: var(--primary-text-color); + font-size: 13px; + font-family: inherit; } - .cover-time-row.actionable { - cursor: pointer; - transition: - border-color 0.2s ease, - background 0.2s ease; + .cover-time ha-icon { + --mdc-icon-size: 16px; + color: var(--secondary-text-color, #666); } - .cover-time-row.actionable:hover { - border-color: var(--primary-color); - background: rgba(128, 128, 128, 0.22); + .cover-time.actionable { + cursor: pointer; } - .cover-time-row ha-icon { - color: var(--secondary-text-color); - --mdi-icon-size: 14px; + .cover-time.actionable:hover { + border-color: var(--primary-color); } - /* Badges sit beside the dial and stack downwards, so heat protection - and covers left open can be raised at the same time without one - covering the other. */ - .badge-stack { - position: absolute; - left: 100%; - margin-left: 8px; - top: 4%; + /* Alerts only exist while something is wrong; nothing is shown otherwise. */ + .alerts { display: flex; - flex-direction: column; - gap: 8px; - z-index: 3; + flex-wrap: wrap; + gap: 6px; + padding-top: 10px; + border-top: 1px solid var(--divider-color, #e0e0e0); } - .badge { - display: flex; + .chip { + display: inline-flex; align-items: center; - justify-content: center; - width: 36px; - height: 36px; - border-radius: 50%; - color: var(--text-primary-color, #fff); + gap: 4px; + padding: 4px 8px; + border-radius: 8px; + font-size: 12px; animation: fadeIn 0.3s ease; } - .badge ha-icon { - color: var(--text-primary-color, #fff); - --mdi-icon-size: 20px; + .chip ha-icon { + --mdc-icon-size: 16px; } - .heat-protection-badge { + .chip--heat { background: var(--error-color, #e7973c); - pointer-events: none; + color: var(--text-primary-color, #fff); } /* Pulses between orange and yellow: a cover left up is something to act - on tonight, not a steady status. Hoverable, so the tooltip can name - the covers. */ - .covers-left-open-badge { + on tonight, not a steady status. */ + .chip--covers { background: var(--warning-color, #ff9800); + color: #412402; cursor: help; animation: fadeIn 0.3s ease, @@ -606,12 +485,33 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. } } - /* A blinking badge is exactly what reduced-motion asks us not to do; + /* A blinking chip is exactly what reduced-motion asks us not to do; the colour alone still reads as a warning. */ @media (prefers-reduced-motion: reduce) { - .covers-left-open-badge { + .chip--covers { animation: fadeIn 0.3s ease; } } - `,t([_t({attribute:!1})],wt.prototype,"hass",void 0),t([ut()],wt.prototype,"_config",void 0),t([ut()],wt.prototype,"_tick",void 0),customElements.get("homeshift-card")||customElements.define("homeshift-card",wt),window.customCards=window.customCards||[],window.customCards.push({type:"homeshift-card",name:"HomeShift Card",description:"Card to manage day mode and thermostat mode via the HomeShift integration.",preview:!0}); + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(3px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + select { + padding: 6px 8px; + border-radius: 8px; + border: 1px solid var(--divider-color, #ccc); + background: var(--card-background-color); + color: var(--primary-text-color); + font-size: 13px; + font-family: inherit; + } + `,t([dt({attribute:!1})],ft.prototype,"hass",void 0),t([_t()],ft.prototype,"_config",void 0),t([_t()],ft.prototype,"_tick",void 0),t([_t()],ft.prototype,"_openSetting",void 0),customElements.get("homeshift-card")||customElements.define("homeshift-card",ft),window.customCards=window.customCards||[],window.customCards.push({type:"homeshift-card",name:"HomeShift Card",description:"Card to manage day mode and thermostat mode via the HomeShift integration.",preview:!0}); //# sourceMappingURL=homeshift-card.js.map diff --git a/src/components/circular-slider.ts b/src/components/circular-slider.ts deleted file mode 100644 index 25f8cb2..0000000 --- a/src/components/circular-slider.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { LitElement, html, css, svg } from "lit"; -import { property, query } from "lit/decorators.js"; - -/* ======================= - CONFIGURATION -======================= */ - -const STROKE_WIDTH = 25; -const CLICK_AREA_PADDING = 10; - -// 180° arc (opening at the bottom) -const ARC_PATH = "M 30 150 A 85 85 0 1 1 170 150"; - -// Angles used for click calculations -const START_ANGLE = 34.2; -const TOTAL_ARC_DEGREES = 180; -const ARC_END_ANGLE = START_ANGLE - TOTAL_ARC_DEGREES; -const ARC_END_ANGLE_POSITIVE = ARC_END_ANGLE + 360; - -/* ======================= - COMPONENT -======================= */ - -// Segment colours, cycled by index so the component is not coupled to specific mode names. -const SEGMENT_COLORS = [ - "var(--disabled-text-color, #9e9e9e)", // Gray (first active mode, typically off) - "#e74c3c", // Red (second, typically heating) - "var(--primary-color, #3b82f6)", // Blue (third, typically cooling) - "#d4a574", // Beige (fourth, typically ventilation) -]; - -export class HomeShiftCircularSlider extends LitElement { - @property({ attribute: false }) public hass!: any; - @property() public entityId!: string; - @property() public currentValue!: string; - @property({ type: Array }) public options: string[] = []; - /** Optional display labels shown on the arc. Falls back to options[i] when not provided. */ - @property({ type: Array }) public labels: string[] = []; - @property({ type: Number }) private selectedIndex = -1; - @query("svg") private _svg?: SVGSVGElement; - - /* ======================= - UTILS - ======================= */ - - private _getColorForIndex(index: number): string { - return ( - SEGMENT_COLORS[index % SEGMENT_COLORS.length] ?? "var(--primary-color)" - ); - } - - private _valueToPercentage(index: number): number { - // Divide by total length to get equal segments - // Keep left-to-right mapping (0 -> 1) - return index / this.options.length; - } - - // Build dasharray for a total length of 1 (via pathLength="1") - private _strokeDashArc(fromIndex: number, toIndex: number): [string, string] { - const start = this._valueToPercentage(fromIndex); // e.g. 0.33 - const end = this._valueToPercentage(toIndex); // e.g. 0.66 - - const length = end - start; // Segment length (e.g. 0.33) - - // Dasharray: "segment_length large_gap" - // Dashoffset: "-start_point" (negative shifts the stroke to the right) - return [`${length} 10`, `-${start}`]; - } - - private _getPercentageFromEvent(e: MouseEvent): number { - if (!this._svg) return -1; - - const rect = this._svg.getBoundingClientRect(); - const x = (2 * (e.clientX - rect.left - rect.width / 2)) / rect.width; - const y = (2 * (e.clientY - rect.top - rect.height / 2)) / rect.height; - - const angle = (Math.atan2(y, x) * 180) / Math.PI; - - if (angle >= ARC_END_ANGLE && angle <= START_ANGLE) { - return (START_ANGLE - angle) / TOTAL_ARC_DEGREES; - } - - if (angle >= ARC_END_ANGLE_POSITIVE) { - return (START_ANGLE + (360 - angle)) / TOTAL_ARC_DEGREES; - } - - return -1; - } - - private _onSvgClick(e: MouseEvent) { - const p = this._getPercentageFromEvent(e); - if (p < 0 || this.options.length === 0) return; - - // Map percentage (0 to 1) directly to option index - const index = Math.floor(p * this.options.length); - - const selectedIdx = Math.max(0, Math.min(index, this.options.length - 1)); - - this.dispatchEvent( - new CustomEvent("option-selected", { - detail: { option: this.options[selectedIdx] }, - bubbles: true, - composed: true, - }), - ); - } - - /* ======================= - RENDER - ======================= */ - - protected render() { - // Guard: nothing to render if no options are provided yet - if (!this.options || this.options.length === 0) - return html`
`; - - // Sync selectedIndex with currentValue. - // When the thermostat is in its "off" state (e.g. "Eteint", "Off"), the off option - // is NOT included in this.options (filtered out by the parent card), so indexOf - // returns -1 → selectedIndex = -1 → no arc segment is highlighted. This replaces - // the previous explicit check against hardcoded strings like "Eteint" or "off". - const currentIndex = this.currentValue - ? this.options.indexOf(this.currentValue) - : -1; - if (currentIndex !== -1) { - this.selectedIndex = currentIndex; - } else { - this.selectedIndex = -1; - } - - return html` -
- - - - - - ${svg` - - `} ${this.selectedIndex !== -1 - ? (() => { - const [dasharray, dashoffset] = this._strokeDashArc( - this.selectedIndex, - this.selectedIndex + 1, - ); - - const strokeColor = this._getColorForIndex(this.selectedIndex); - - return svg` - - `; - })() - : null} - ${(() => { - const N = this.options.length; - if (N < 2) return null; - const eps = 0.002; // half-width of each separator mark in pathLength units - const seg = 1 / N; - // dasharray starts as "drawn", so prepend 0 to make the first value a gap. - // Pattern: 0 (first_gap) mark (gap) mark ... mark (large trailing gap) - const parts: number[] = [0, seg - eps]; - for (let s = 0; s < N - 2; s++) { - parts.push(2 * eps); // mark - parts.push(seg - 2 * eps); // gap - } - parts.push(2 * eps); // last mark - parts.push(100); // large trailing value to prevent pattern repetition - return svg` - - `; - })()} - ${this.options.map((_mode, i) => { - const [dasharray, dashoffset] = this._strokeDashArc(i, i + 1); - - return svg` - { - e.stopPropagation(); - this.dispatchEvent( - new CustomEvent("option-selected", { - detail: { option: this.options[i] }, - bubbles: true, - composed: true, - }), - ); - }} - /> - `; - })} - ${this.options.map((_mode, i) => { - // Compute the center position of the segment - const segmentStart = this._valueToPercentage(i); - const segmentEnd = this._valueToPercentage(i + 1); - const segmentCenter = (segmentStart + segmentEnd) / 2; - const startOffset = segmentCenter * 100; - // Prefer the translated label when provided, fall back to the raw option value - const displayLabel = this.labels[i] ?? this.options[i]; - - return svg` - { - e.stopPropagation(); - this.dispatchEvent( - new CustomEvent("option-selected", { - detail: { option: this.options[i] }, - bubbles: true, - composed: true, - }), - ); - }} - > - - ${displayLabel} - - - `; - })} - -
- `; - } - - static styles = css` - :host { - display: block; - } - - .slider-container { - display: flex; - justify-content: center; - padding: 8px 0; - } - - svg { - width: 100%; - max-width: 240px; - aspect-ratio: 1; - } - `; -} - -customElements.define("homeshift-circular-slider", HomeShiftCircularSlider); diff --git a/src/components/homeshift-card-editor.ts b/src/components/homeshift-card-editor.ts index 1c5069c..4a3f2be 100644 --- a/src/components/homeshift-card-editor.ts +++ b/src/components/homeshift-card-editor.ts @@ -10,7 +10,6 @@ interface HomeShiftCardConfig { cover_close_time_entity?: string; cover_entity?: string; covers_left_open_entity?: string; - show_title?: boolean; } class HomeShiftCardEditor extends LitElement { @@ -45,17 +44,6 @@ class HomeShiftCardEditor extends LitElement { this._dispatchConfigChanged(newConfig); } - private _onShowTitleChanged(ev: Event) { - if (!this._config || !this.hass) return; - const target = ev.target as any; - const newValue = target.checked; - if (this._config.show_title === newValue) return; - - const newConfig = { ...this._config, show_title: newValue }; - this._config = newConfig; - this._dispatchConfigChanged(newConfig); - } - private _dispatchConfigChanged(newConfig: HomeShiftCardConfig) { const event = new CustomEvent("config-changed", { detail: { config: newConfig }, @@ -133,13 +121,6 @@ class HomeShiftCardEditor extends LitElement { this._onEntityChanged(e, "covers_left_open_entity")} allow-custom-entity > - - - - `; } diff --git a/src/components/homeshift-card.ts b/src/components/homeshift-card.ts index b692805..e02eef4 100644 --- a/src/components/homeshift-card.ts +++ b/src/components/homeshift-card.ts @@ -1,7 +1,6 @@ import { LitElement, html, css, nothing } from "lit"; import { property, state } from "lit/decorators.js"; import { localize } from "../localize/localize"; -import "./circular-slider"; import "./homeshift-card-editor"; interface HomeShiftCardConfig { @@ -17,7 +16,6 @@ interface HomeShiftCardConfig { cover_close_time_entity?: string; cover_entity?: string; covers_left_open_entity?: string; - show_title?: boolean; } class HomeShiftCard extends LitElement { @@ -53,7 +51,9 @@ class HomeShiftCard extends LitElement { @property({ attribute: false }) public hass!: any; @state() private _config!: HomeShiftCardConfig; @state() private _tick = 0; + @state() private _openSetting: "override" | "early" | null = null; private _refreshInterval?: ReturnType; + private _settingTimeout?: ReturnType; connectedCallback() { super.connectedCallback(); @@ -65,6 +65,7 @@ class HomeShiftCard extends LitElement { disconnectedCallback() { super.disconnectedCallback(); clearInterval(this._refreshInterval); + clearTimeout(this._settingTimeout); } public static getStubConfig(): HomeShiftCardConfig { @@ -82,7 +83,6 @@ class HomeShiftCard extends LitElement { cover_close_time_entity: "sensor.homeshift_cover_close_time", cover_entity: "cover.homeshift_daily_covers", covers_left_open_entity: "binary_sensor.homeshift_covers_left_open", - show_title: true, }; } @@ -117,7 +117,6 @@ class HomeShiftCard extends LitElement { covers_left_open_entity: config.covers_left_open_entity ?? "binary_sensor.homeshift_covers_left_open", - show_title: config.show_title !== false, }; } @@ -128,6 +127,7 @@ class HomeShiftCard extends LitElement { protected shouldUpdate(changedProps: Map): boolean { if (changedProps.has("_config")) return true; if (changedProps.has("_tick")) return true; + if (changedProps.has("_openSetting")) return true; if (changedProps.has("hass")) { const oldHass = changedProps.get("hass") as any; if (!oldHass) return true; @@ -151,6 +151,47 @@ class HomeShiftCard extends LitElement { return false; } + /** localize() echoes the key when a string is missing — treat that as absent. */ + private t(key: string, fallback: string, params?: Record) { + const text = localize(this.hass, key, params); + return !text || text === key ? fallback : text; + } + + /** Colour class for a preset: its key, or the display name it was given. + + * A select exposing option_map hands over stable keys (off, heating, …). + * Without it, the only thing left is the display value, which is the + * user's own wording — so known labels are mapped back to a key, and + * anything else falls through to the neutral colour rather than breaking + * the class name. + */ + private _presetClass(key: string, display: string): string { + const slug = (text: string) => + text + .toLowerCase() + .normalize("NFD") + .replace(/[̀-ͯ]/g, "") + .replace(/[^a-z0-9]+/g, "-"); + const known = ["off", "heating", "cooling", "ventilation"]; + if (known.includes(slug(key))) return slug(key); + const byLabel: Record = { + eteint: "off", + arret: "off", + off: "off", + chauffage: "heating", + heat: "heating", + heating: "heating", + climatisation: "cooling", + clim: "cooling", + cool: "cooling", + cooling: "cooling", + ventilation: "ventilation", + ventilateur: "ventilation", + fan: "ventilation", + }; + return byLabel[slug(display)] ?? slug(key); + } + private getEntityState(entityId?: string) { if (!entityId) return undefined; return this.hass?.states?.[entityId]; @@ -177,7 +218,7 @@ class HomeShiftCard extends LitElement { }); } - private onCircularSliderSelect(entityId: string, option: string) { + private onThermostatSelect(entityId: string, option: string) { this.hass.callService("select", "select_option", { entity_id: entityId, option, @@ -198,40 +239,6 @@ class HomeShiftCard extends LitElement { this.hass.callService("cover", action, { entity_id: entityId }); } - private onOverrideDurationSelect(ev: Event) { - const entityId = this._config.override_duration_entity; - if (!entityId) return; - const value = Number((ev.target as HTMLSelectElement).value); - this.hass.callService("number", "set_value", { - entity_id: entityId, - value, - }); - } - - private onEarlySwitchSelect(ev: Event) { - const entityId = this._config.early_switch_entity; - if (!entityId) return; - const value = Number((ev.target as HTMLSelectElement).value); - this.hass.callService("number", "set_value", { - entity_id: entityId, - value, - }); - } - - private _formatRelativeTime(isoString?: string): string { - if (!isoString) return ""; - const dt = new Date(isoString); - if (isNaN(dt.getTime())) return isoString; - const diffMs = dt.getTime() - Date.now(); - const diffMin = Math.round(diffMs / 60000); - if (diffMin <= 0) return localize(this.hass, "card.past") || "passé"; - if (diffMin < 60) - return `${localize(this.hass, "card.in") || "dans"} ${diffMin} min`; - const h = Math.floor(diffMin / 60); - const m = diffMin % 60; - return `${localize(this.hass, "card.in") || "dans"} ${h}h${m > 0 ? m.toString().padStart(2, "0") : ""}`; - } - private _formatAbsoluteTime(isoString?: string): string { if (!isoString) return ""; const dt = new Date(isoString); @@ -252,6 +259,111 @@ class HomeShiftCard extends LitElement { return `${dt.toLocaleDateString([], { month: "short", day: "numeric" })} ${timeStr}`; } + private _settingLabel(minutes: number): string { + if (!minutes) return localize(this.hass, "card.duration_off"); + if (minutes < 60) return `${minutes} min`; + const h = Math.floor(minutes / 60); + const m = minutes % 60; + return m ? `${h}h${String(m).padStart(2, "0")}` : `${h}h`; + } + + private _clockIn(minutes: number): string { + const at = new Date(Date.now() + minutes * 60000); + return at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + } + + private _clockAt(isoString: string, shiftMinutes = 0): string { + const at = new Date(new Date(isoString).getTime() + shiftMinutes * 60000); + if (isNaN(at.getTime())) return isoString; + return at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + } + + /** Open one setting at a time, and fold it back once it is left alone. */ + private _toggleSetting(name: "override" | "early") { + this._openSetting = this._openSetting === name ? null : name; + this._armSettingTimeout(); + } + + private _armSettingTimeout() { + clearTimeout(this._settingTimeout); + if (!this._openSetting) return; + this._settingTimeout = setTimeout(() => { + this._openSetting = null; + }, 6000); + } + + private _stepSetting( + name: "override" | "early", + presets: { value: number }[], + current: number, + direction: number, + ) { + const entityId = + name === "override" + ? this._config.override_duration_entity + : this._config.early_switch_entity; + if (!entityId) return; + const values = presets.map((preset) => preset.value); + // The stored value may sit between two presets; land on the neighbour in + // the direction asked for rather than on a preset index that never matches. + const index = values.findIndex((value) => value >= current); + const from = index === -1 ? values.length - 1 : index; + const next = Math.min(values.length - 1, Math.max(0, from + direction)); + this._armSettingTimeout(); + this.hass.callService("number", "set_value", { + entity_id: entityId, + value: values[next], + }); + } + + private _renderSetting( + name: "override" | "early", + icon: string, + label: string, + effect: string, + presets: { value: number }[], + current: number, + ) { + const open = this._openSetting === name; + return html`
+ + ${open + ? html`
+
+ + ${this._settingLabel(current)} + +
+

${effect}

+
` + : nothing} +
`; + } + private _renderMain( thermo: any, day: any, @@ -261,7 +373,6 @@ class HomeShiftCard extends LitElement { coverCloseTime?: string, ) { // Day mode options from option_map attribute (HomeShift integration ≥ 1.1.0). - // Falls back to options list if option_map is not yet available. const dayModeMap: Record = day.attributes?.option_map ?? {}; const dayEntries: [string, string][] = Object.keys(dayModeMap).length > 0 @@ -270,222 +381,195 @@ class HomeShiftCard extends LitElement { (o: string) => [o, o] as [string, string], ); - // Thermostat mode options from option_map attribute (HomeShift integration ≥ 1.1.0). - // Falls back to attributes.options (first entry = "off" mode, rest = active modes). + // Thermostat presets, keyed so each can carry its own colour and its own + // translation regardless of the display values set in the integration. const thermoModeMap: Record = thermo.attributes?.option_map ?? {}; - const hasThermoMap = Object.keys(thermoModeMap).length > 0; - - // Display value for the "off" key — sent to select.select_option when off is pressed. - // The internal key for the off mode is always "off" (THERMOSTAT_OFF_KEY in the integration). - const offDisplay: string = hasThermoMap - ? (thermoModeMap["off"] ?? thermo.attributes?.options?.[0] ?? "") - : (thermo.attributes?.options?.[0] ?? ""); - - // Options for the circular slider: off mode first, then active modes. - const thermoSliderOptions: string[] = hasThermoMap - ? [ - offDisplay, - ...Object.entries(thermoModeMap) - .filter(([key]) => key !== "off") - .map(([, display]) => display), - ] - : (thermo.attributes?.options ?? []); - - // Localized labels for the circular slider arc (translated in the card, regardless - // of the display values configured in the integration). - // Standard internal keys from HomeShift: heating, cooling, ventilation. - const THERMOSTAT_ACTIVE_KEYS = ["heating", "cooling", "ventilation"]; - const thermoLocalizedLabels: string[] = hasThermoMap - ? [ - localize(this.hass, "card.off") || offDisplay, - ...Object.keys(thermoModeMap) - .filter((key) => key !== "off") - .map((key) => localize(this.hass, `thermostat.${key}`) || key), - ] - : [ - localize(this.hass, "card.off") || - (thermo.attributes?.options?.[0] ?? ""), - ...THERMOSTAT_ACTIVE_KEYS.map( - (key) => localize(this.hass, `thermostat.${key}`) || key, - ), - ]; + const thermoEntries: [string, string][] = + Object.keys(thermoModeMap).length > 0 + ? (Object.entries(thermoModeMap) as [string, string][]) + : (thermo.attributes?.options ?? []).map( + (o: string) => [o, o] as [string, string], + ); const nextMode = this.getEntityState(this._config.next_mode_entity); const nextModeAt = this.getEntityState(this._config.next_mode_at_entity); - const currentEarlySwitch = Number( + const earlySwitch = Number( this.getEntityState(this._config.early_switch_entity)?.state ?? 0, ); + const override = Number( + this.getEntityState(this._config.override_duration_entity)?.state ?? 0, + ); const hasNextMode = nextMode?.state && nextMode.state !== "unknown" && nextMode.state !== ""; + const nextModeName = hasNextMode ? nextMode!.state : ""; + const hasNextAt = + nextModeAt?.state && + nextModeAt.state !== "unknown" && + nextModeAt.state !== "unavailable"; + + const overrideEffect = override + ? localize(this.hass, "card.override_effect", { + duration: this._settingLabel(override), + time: this._clockIn(override), + }) + : localize(this.hass, "card.override_none"); + const earlyEffect = !hasNextAt + ? localize(this.hass, "card.early_no_event") + : earlySwitch + ? localize(this.hass, "card.early_effect", { + mode: nextModeName, + time: this._clockAt(nextModeAt!.state), + scheduled: this._clockAt(nextModeAt!.state, earlySwitch), + }) + : localize(this.hass, "card.early_none", { + mode: nextModeName, + time: this._clockAt(nextModeAt!.state), + }); const hasCoverOpenTime = coverOpenTime && coverOpenTime !== "unknown" && coverOpenTime !== "unavailable"; const hasCoverCloseTime = coverCloseTime && coverCloseTime !== "unknown" && coverCloseTime !== "unavailable"; - const canControlCover = Boolean(this._config.cover_entity); + // Past three, the names stop fitting on one line; the tooltip keeps them. + const coversSummary = + coversLeftOpen.length > 3 + ? localize(this.hass, "card.covers_left_open_count", { + count: String(coversLeftOpen.length), + }) + : localize(this.hass, "card.covers_left_open", { + covers: coversLeftOpen.join(", "), + }); + + const activeKey: string | undefined = thermo.attributes?.current_key; return html` -
+
+
+ ${thermoEntries.map(([key, display]) => { + const active = activeKey ? key === activeKey : display === thermo.state; + return html``; + })} +
+ +
+ ${localize(this.hass, "card.day_mode")} + +
+ + ${hasNextMode || hasNextAt + ? html`
+ ${localize(this.hass, "card.next")} + ${nextModeName}${hasNextMode && hasNextAt ? " · " : ""}${hasNextAt + ? this._formatAbsoluteTime(nextModeAt!.state) + : ""} +
` + : nothing} + + ${this._config.override_duration_entity + ? this._renderSetting( + "override", + "mdi:hand-back-left", + localize(this.hass, "card.override_label"), + overrideEffect, + HomeShiftCard.OVERRIDE_PRESETS, + override, + ) + : nothing} + ${this._config.early_switch_entity + ? this._renderSetting( + "early", + "mdi:clock-fast", + localize(this.hass, "card.early_label"), + earlyEffect, + HomeShiftCard.EARLY_SWITCH_PRESETS, + earlySwitch, + ) + : nothing} + ${hasCoverOpenTime || hasCoverCloseTime - ? html`
- ${hasCoverOpenTime - ? html`
this.onCoverAction("open_cover")} - > - - ${coverOpenTime} -
` - : nothing} - ${hasCoverCloseTime - ? html`
this.onCoverAction("close_cover")} - > - - ${coverCloseTime} -
` - : nothing} + ? html`
+ ${localize(this.hass, "card.covers")} + + ${hasCoverOpenTime + ? html`` + : nothing} + ${hasCoverCloseTime + ? html`` + : nothing} +
` : nothing} + ${heatProtectionActive || coversLeftOpen.length > 0 - ? html`
+ ? html`
${heatProtectionActive - ? html`
+ ? html` -
` + ${localize(this.hass, "card.heat_protection_active")} + ` : nothing} ${coversLeftOpen.length > 0 - ? html`
-
` + ${coversSummary} + ` : nothing}
` : nothing} - - this.onCircularSliderSelect(thermo.entity_id, e.detail.option)} - > - -
- ${hasNextMode || nextModeAt?.state - ? html`
- ${hasNextMode - ? html`${nextMode!.state}` - : nothing} - ${nextModeAt?.state - ? html`${this._formatAbsoluteTime(nextModeAt.state)}` - : nothing} -
` - : nothing} - ${this._config.early_switch_entity - ? html`` - : nothing} -
- -
-
-
- -
- - ${(() => { - const currentOverride = Number( - this.getEntityState(this._config.override_duration_entity) - ?.state ?? 0, - ); - return html``; - })()} -
-
`; } - protected render() { if (!this.hass || !this._config) return nothing; - const title = this._config.name ?? localize(this.hass, "card.title"); // Use real entities when available, fall back to stub data for the picker preview const dayRaw = this.getEntityState(this._config.day_mode_entity); @@ -531,7 +615,7 @@ class HomeShiftCard extends LitElement { )?.state; return html` - +
${this._renderMain( thermo, @@ -548,277 +632,252 @@ class HomeShiftCard extends LitElement { static styles = css` ha-card { - padding: 8px; + padding: 12px; position: relative; - min-height: 240px; - text-align: center; } .container { - display: flex; - justify-content: center; width: 100%; } - /* SLIDER VIEW */ - .thermo-section { - position: relative; - width: 100%; - max-width: 240px; + .rows { display: flex; flex-direction: column; - align-items: center; } - homeshift-circular-slider { - width: 100%; + /* Thermostat presets — one segmented control, one colour per mode. */ + .presets { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 1fr; + border: 1px solid var(--divider-color, #ccc); + border-radius: 8px; + overflow: hidden; + margin-bottom: 4px; } - .thermo-bottom { - position: absolute; - bottom: 16px; - left: 50%; - transform: translateX(-50%); - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - z-index: 2; - width: 90%; + .preset { + border: 0; + border-left: 1px solid var(--divider-color, #ccc); + background: transparent; + color: var(--secondary-text-color, #666); + font-size: 13px; + font-family: inherit; + padding: 8px 4px; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: + background 0.2s ease, + color 0.2s ease; } - .off-row { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; + .preset:first-child { + border-left: 0; } - .bottom-controls { - flex: 1; - display: flex; - align-items: center; - gap: 8px; + .preset:hover { + background: var(--secondary-background-color, rgba(127, 127, 127, 0.12)); } - .day-section { - flex: 1; - min-width: 0; + .preset.on { + background: var(--secondary-text-color, #666); + color: var(--text-primary-color, #fff); } - .day-section select { - width: 100%; + .preset--heating.on { + background: var(--state-climate-heat-color, #e24b4a); + } + + .preset--cooling.on { + background: var(--state-climate-cool-color, #378add); + } + + .preset--ventilation.on { + background: var(--state-fan-active-color, #1d9e75); } - .center-button { + .preset--off.on { + background: var(--divider-color, #9e9e9e); + color: var(--primary-text-color); + } + + .row { display: flex; align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 50%; - border: 1px solid var(--divider-color, #ccc); - background: var(--card-background-color, #ffffff); - cursor: pointer; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - transition: - background 0.2s, - border-color 0.2s; + justify-content: space-between; + gap: 8px; + padding: 8px 0; + border-top: 1px solid var(--divider-color, #e0e0e0); + font-size: 13px; } - .center-button ha-icon { + .row-key { color: var(--secondary-text-color, #666); } - .center-button.active { - background: var(--primary-color); - border-color: var(--primary-color); + /* Collapsed by default: the value is readable without opening anything. */ + .setting { + border-top: 1px solid var(--divider-color, #e0e0e0); } - .center-button.active ha-icon { - color: var(--text-primary-color, #fff); + .setting-head { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px 0; + border: 0; + background: transparent; + color: var(--primary-text-color); + font-size: 13px; + font-family: inherit; + text-align: left; + cursor: pointer; } - hui-card { - --ha-card-box-shadow: none; - --ha-card-border-width: 0; + .setting-head ha-icon { + --mdc-icon-size: 18px; + color: var(--secondary-text-color, #666); + flex-shrink: 0; } - @keyframes fadeIn { - from { - opacity: 0; - transform: translateY(5px); - } - to { - opacity: 1; - transform: translateY(0); - } + .setting-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - /* UI ELEMENTS */ - select { - padding: 8px; - border-radius: 6px; - border: 1px solid var(--divider-color, #ccc); - background: var(--card-background-color); - color: var(--primary-text-color); + .setting-value { + color: var(--secondary-text-color, #666); } - .list-select { - flex-shrink: 0; - align-self: stretch; - padding: 0 8px; - border-radius: 6px; - border: 1px solid var(--divider-color, #ccc); - background: var(--card-background-color); + .setting-value.set { color: var(--primary-text-color); - font-size: 13px; - cursor: pointer; - transition: - background 0.2s ease, - border-color 0.2s ease, - color 0.2s ease; + font-weight: 500; } - .list-select--override.active { - border-color: var(--primary-color); - background: var(--primary-color); - color: var(--text-primary-color, #fff); + .setting .chevron { + transition: transform 0.15s ease; } - .list-select--early.active { - border-color: var(--accent-color, var(--primary-color)); - background: var(--accent-color, var(--primary-color)); - color: var(--text-primary-color, #fff); + .setting.open .chevron { + transform: rotate(180deg); } - .list-select--early { - padding: 8px; + .setting-body { + padding: 0 0 10px 26px; + animation: fadeIn 0.15s ease; } - .next-info-group { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + .stepper { display: flex; - flex-direction: column; align-items: center; - gap: 6px; - z-index: 2; - text-align: center; - background: rgba(128, 128, 128, 0.12); - border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.1)); - border-radius: 10px; - padding: 6px 12px; + gap: 10px; } - .next-info { - display: flex; - flex-direction: column; - align-items: center; - gap: 2px; - pointer-events: none; + .stepper button { + width: 34px; + height: 34px; + border-radius: 8px; + border: 1px solid var(--divider-color, #ccc); + background: var(--card-background-color); + color: var(--primary-text-color); + font-size: 18px; + line-height: 1; + cursor: pointer; } - .next-mode-state { - font-size: 13px; - font-weight: 600; - color: var(--primary-text-color); + .stepper button:hover:not(:disabled) { + background: var(--secondary-background-color, rgba(127, 127, 127, 0.12)); } - .next-mode-at-state { - font-size: 11px; - color: var(--secondary-text-color); + .stepper button:disabled { + opacity: 0.4; + cursor: default; } - .error { - color: var(--error-color); - padding: 16px; + .stepper-value { + min-width: 64px; + text-align: center; + font-size: 15px; + font-weight: 500; + } + + .setting-effect { + margin: 8px 0 0; + font-size: 12px; + line-height: 1.4; + color: var(--secondary-text-color, #666); } .cover-times { - position: absolute; - right: 100%; - margin-right: 8px; - top: 4%; display: flex; - flex-direction: column; - gap: 4px; - z-index: 2; - animation: fadeIn 0.3s ease; + gap: 6px; } - .cover-time-row { - display: flex; + .cover-time { + display: inline-flex; align-items: center; gap: 4px; - padding: 2px 6px; - border-radius: 10px; - background: rgba(128, 128, 128, 0.12); - border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.1)); - font-size: 11px; - color: var(--secondary-text-color); - white-space: nowrap; + padding: 4px 8px; + border-radius: 8px; + border: 1px solid var(--divider-color, #ccc); + background: var(--card-background-color); + color: var(--primary-text-color); + font-size: 13px; + font-family: inherit; } - .cover-time-row.actionable { - cursor: pointer; - transition: - border-color 0.2s ease, - background 0.2s ease; + .cover-time ha-icon { + --mdc-icon-size: 16px; + color: var(--secondary-text-color, #666); } - .cover-time-row.actionable:hover { - border-color: var(--primary-color); - background: rgba(128, 128, 128, 0.22); + .cover-time.actionable { + cursor: pointer; } - .cover-time-row ha-icon { - color: var(--secondary-text-color); - --mdi-icon-size: 14px; + .cover-time.actionable:hover { + border-color: var(--primary-color); } - /* Badges sit beside the dial and stack downwards, so heat protection - and covers left open can be raised at the same time without one - covering the other. */ - .badge-stack { - position: absolute; - left: 100%; - margin-left: 8px; - top: 4%; + /* Alerts only exist while something is wrong; nothing is shown otherwise. */ + .alerts { display: flex; - flex-direction: column; - gap: 8px; - z-index: 3; + flex-wrap: wrap; + gap: 6px; + padding-top: 10px; + border-top: 1px solid var(--divider-color, #e0e0e0); } - .badge { - display: flex; + .chip { + display: inline-flex; align-items: center; - justify-content: center; - width: 36px; - height: 36px; - border-radius: 50%; - color: var(--text-primary-color, #fff); + gap: 4px; + padding: 4px 8px; + border-radius: 8px; + font-size: 12px; animation: fadeIn 0.3s ease; } - .badge ha-icon { - color: var(--text-primary-color, #fff); - --mdi-icon-size: 20px; + .chip ha-icon { + --mdc-icon-size: 16px; } - .heat-protection-badge { + .chip--heat { background: var(--error-color, #e7973c); - pointer-events: none; + color: var(--text-primary-color, #fff); } /* Pulses between orange and yellow: a cover left up is something to act - on tonight, not a steady status. Hoverable, so the tooltip can name - the covers. */ - .covers-left-open-badge { + on tonight, not a steady status. */ + .chip--covers { background: var(--warning-color, #ff9800); + color: #412402; cursor: help; animation: fadeIn 0.3s ease, @@ -837,13 +896,34 @@ class HomeShiftCard extends LitElement { } } - /* A blinking badge is exactly what reduced-motion asks us not to do; + /* A blinking chip is exactly what reduced-motion asks us not to do; the colour alone still reads as a warning. */ @media (prefers-reduced-motion: reduce) { - .covers-left-open-badge { + .chip--covers { animation: fadeIn 0.3s ease; } } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(3px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + select { + padding: 6px 8px; + border-radius: 8px; + border: 1px solid var(--divider-color, #ccc); + background: var(--card-background-color); + color: var(--primary-text-color); + font-size: 13px; + font-family: inherit; + } `; } diff --git a/src/localize/en.json b/src/localize/en.json index d7a666d..185f326 100644 --- a/src/localize/en.json +++ b/src/localize/en.json @@ -1,6 +1,5 @@ { "card": { - "title": "Thermostat", "entity_not_found": "Entity not found: {entity}", "off": "Off", "next_scan": "Next scan", @@ -13,9 +12,22 @@ "cover_close_time": "Cover closing time", "cover_open_action": "Open now", "cover_close_action": "Close now", - "covers_left_open": "Covers left open: {covers}" + "covers_left_open": "Not closed: {covers}", + "day_mode": "Day mode", + "next": "Next", + "covers": "Covers", + "duration_off": "No", + "override_label": "Keep my manual choice", + "override_none": "A manual change is replaced at the calendar's next pass.", + "override_effect": "After a change by hand, the calendar waits {duration} — until {time} if you change it now.", + "early_label": "Start events early", + "early_none": "{mode} will start at its scheduled time, {time}.", + "early_effect": "{mode} will start at {time} instead of {scheduled}.", + "early_no_event": "No event scheduled.", + "covers_left_open_count": "Not closed: {count} covers" }, "thermostat": { + "off": "Off", "heating": "Heating", "cooling": "Cooling", "ventilation": "Ventilation" @@ -27,7 +39,6 @@ "cover_open_time_entity": "Cover Open Time Entity", "cover_close_time_entity": "Cover Close Time Entity", "cover_entity": "Cover Entity (manual open/close)", - "covers_left_open_entity": "Covers Left Open Entity", - "show_title": "Show title" + "covers_left_open_entity": "Covers Left Open Entity" } } diff --git a/src/localize/fr.json b/src/localize/fr.json index 8505605..b89cf7b 100644 --- a/src/localize/fr.json +++ b/src/localize/fr.json @@ -1,6 +1,5 @@ { "card": { - "title": "Thermostat", "entity_not_found": "Entité introuvable: {entity}", "off": "Éteint", "next_scan": "Prochain scan", @@ -13,9 +12,22 @@ "cover_close_time": "Heure de fermeture des volets", "cover_open_action": "Ouvrir maintenant", "cover_close_action": "Fermer maintenant", - "covers_left_open": "Volets non fermés : {covers}" + "covers_left_open": "Non fermés : {covers}", + "day_mode": "Mode du jour", + "next": "Prochain", + "covers": "Volets", + "duration_off": "Non", + "override_label": "Garder mon choix manuel", + "override_none": "Un changement manuel est remplacé au prochain passage du calendrier.", + "override_effect": "Après un changement à la main, le calendrier attend {duration} — jusqu'à {time} si tu changes maintenant.", + "early_label": "Anticiper les événements", + "early_none": "{mode} commencera à l'heure prévue, {time}.", + "early_effect": "{mode} commencera à {time} au lieu de {scheduled}.", + "early_no_event": "Aucun événement prévu.", + "covers_left_open_count": "Non fermés : {count} volets" }, "thermostat": { + "off": "Éteint", "heating": "Chauffage", "cooling": "Climatisation", "ventilation": "Ventilation" @@ -27,7 +39,6 @@ "cover_open_time_entity": "Entité heure d'ouverture des volets", "cover_close_time_entity": "Entité heure de fermeture des volets", "cover_entity": "Entité volet (ouverture/fermeture manuelle)", - "covers_left_open_entity": "Entité volets non fermés", - "show_title": "Afficher le titre" + "covers_left_open_entity": "Entité volets non fermés" } } From a2f059418c11df1e724f0ed257fcdad8ec9191e7 Mon Sep 17 00:00:00 2001 From: Gamso Date: Sat, 3 Oct 2026 19:00:12 +0200 Subject: [PATCH 02/25] chore: add MIT LICENSE file package.json already declares "license": "MIT" and the README says MIT, but the repository shipped no licence text, which leaves reuse legally unclear. Same holder and wording as the companion integration. Audit: T-1 (stack note: missing LICENSE) Co-Authored-By: Claude Opus 5.5 --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e535794 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Gamso + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From e71e2220cf03b52de65e1c03c8c776b39bc02352 Mon Sep 17 00:00:00 2001 From: Gamso Date: Sat, 3 Oct 2026 19:00:42 +0200 Subject: [PATCH 03/25] build: drop unused custom-card-helpers and stop emitting sourcemaps custom-card-helpers was declared as a runtime dependency but never imported, so it only added install weight and audit noise. The bundle referenced homeshift-card.js.map while dist/*.map is git-ignored: HACS users got a 404 in devtools for a map that is never shipped. The bundle is minified, reproducible from the tagged sources, and small, so sourcemaps are disabled (rollup and tsconfig) instead of versioning a .map that would double the dist diff on every change. Audit: T-1 Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 178 +--------------------------------------------- package.json | 1 - rollup.config.js | 2 +- tsconfig.json | 2 +- 4 files changed, 4 insertions(+), 179 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8809107..6a9e16c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.2.0", "license": "MIT", "dependencies": { - "custom-card-helpers": "^1.9.0", "lit": "^3.3.2" }, "devDependencies": { @@ -24,65 +23,6 @@ "typescript": "^5.9.3" } }, - "node_modules/@formatjs/ecma402-abstract": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", - "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", - "license": "MIT", - "dependencies": { - "@formatjs/intl-localematcher": "0.2.25", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/fast-memoize": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz", - "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz", - "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/icu-skeleton-parser": "1.3.6", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz", - "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/intl-localematcher": { - "version": "0.2.25", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", - "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/intl-utils": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-3.8.4.tgz", - "integrity": "sha512-j5C6NyfKevIxsfLK8KwO1C0vvP7k1+h4A9cFpc+cr6mEwCc1sPkr17dzh0Ke6k9U5pQccAQoXdcNBl3IYa4+ZQ==", - "deprecated": "the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package", - "license": "MIT", - "dependencies": { - "emojis-list": "^3.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -708,89 +648,6 @@ "dev": true, "license": "MIT" }, - "node_modules/custom-card-helpers": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/custom-card-helpers/-/custom-card-helpers-1.9.0.tgz", - "integrity": "sha512-5IW4OXq3MiiCqDvqeu+MYsM1NmntKW1WfJhyJFsdP2tbzqEI4BOnqRz2qzdp08lE4QLVhYfRLwe0WAqgQVNeFg==", - "license": "MIT", - "dependencies": { - "@formatjs/intl-utils": "^3.8.4", - "home-assistant-js-websocket": "^6.0.1", - "intl-messageformat": "^9.11.1", - "lit": "^2.1.1", - "rollup": "^2.63.0", - "superstruct": "^0.15.3", - "typescript": "^4.5.4" - } - }, - "node_modules/custom-card-helpers/node_modules/@lit/reactive-element": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", - "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.0.0" - } - }, - "node_modules/custom-card-helpers/node_modules/lit": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", - "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^1.6.0", - "lit-element": "^3.3.0", - "lit-html": "^2.8.0" - } - }, - "node_modules/custom-card-helpers/node_modules/lit-element": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", - "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.1.0", - "@lit/reactive-element": "^1.3.0", - "lit-html": "^2.8.0" - } - }, - "node_modules/custom-card-helpers/node_modules/lit-html": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", - "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, - "node_modules/custom-card-helpers/node_modules/rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/custom-card-helpers/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -801,15 +658,6 @@ "node": ">=0.10.0" } }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -839,6 +687,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -872,24 +721,6 @@ "node": ">= 0.4" } }, - "node_modules/home-assistant-js-websocket": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/home-assistant-js-websocket/-/home-assistant-js-websocket-6.1.1.tgz", - "integrity": "sha512-TnZFzF4mn5F/v0XKUTK2GMQXrn/+eQpgaSDSELl6U0HSwSbFwRhGWLz330YT+hiKMspDflamsye//RPL+zwhDw==", - "license": "Apache-2.0" - }, - "node_modules/intl-messageformat": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz", - "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==", - "license": "BSD-3-Clause", - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/fast-memoize": "1.2.1", - "@formatjs/icu-messageformat-parser": "2.1.0", - "tslib": "^2.1.0" - } - }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -1119,12 +950,6 @@ "source-map": "^0.6.0" } }, - "node_modules/superstruct": { - "version": "0.15.5", - "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", - "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==", - "license": "MIT" - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -1161,6 +986,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, "license": "0BSD" }, "node_modules/typescript": { diff --git a/package.json b/package.json index 662e0cb..d31cf2f 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "typescript": "^5.9.3" }, "dependencies": { - "custom-card-helpers": "^1.9.0", "lit": "^3.3.2" } } diff --git a/rollup.config.js b/rollup.config.js index c6e5f48..8cf40d7 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -9,7 +9,7 @@ export default { output: { file: "dist/homeshift-card.js", format: "es", - sourcemap: true, + sourcemap: false, }, plugins: [ resolve(), diff --git a/tsconfig.json b/tsconfig.json index 8ec0c27..fac6930 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "module": "ES2020", "lib": ["ES2020", "DOM", "DOM.Iterable"], "declaration": false, - "sourceMap": true, + "sourceMap": false, "outDir": "dist", "rootDir": "src", "strict": true, From 3da17f6de02fdcdd28d789a4401b56ad2927ccd4 Mon Sep 17 00:00:00 2001 From: Gamso Date: Sat, 3 Oct 2026 19:02:00 +0200 Subject: [PATCH 04/25] test: set up Vitest with happy-dom, ESLint and a typecheck script The repository had no lint, no test and no type check beyond the build, which is how the editor key typo and the dead localize fallbacks went unnoticed. Adds: - vitest + happy-dom (Lit components render in a DOM without a browser), - ESLint flat config with typescript-eslint, - "typecheck" (tsc --noEmit over src and tests), "lint" and "test" scripts, used by the upcoming CI workflow. Audit: T-1 Co-Authored-By: Claude Opus 5.5 --- eslint.config.js | 27 + package-lock.json | 3411 +++++++++++++++++++++++++++++++++++------ package.json | 13 +- test/localize.test.ts | 12 + test/tsconfig.json | 8 + vitest.config.ts | 8 + 6 files changed, 2978 insertions(+), 501 deletions(-) create mode 100644 eslint.config.js create mode 100644 test/localize.test.ts create mode 100644 test/tsconfig.json create mode 100644 vitest.config.ts diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..791c2f1 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,27 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist/", "node_modules/", ".devcontainer/"] }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.ts"], + languageOptions: { + globals: { ...globals.browser }, + }, + rules: { + // The hass object and entity states come untyped from Home Assistant. + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + }, + }, + { + files: ["*.js"], + languageOptions: { globals: { ...globals.node } }, + }, +); diff --git a/package-lock.json b/package-lock.json index 6a9e16c..7e45fac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,15 +12,239 @@ "lit": "^3.3.2" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.3.0", "@types/node": "^25.0.8", + "eslint": "^10.12.0", + "globals": "^17.13.0", + "happy-dom": "^20.14.5", "rollup": "^4.55.1", "tslib": "^2.8.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "typescript-eslint": "^8.71.0", + "vitest": "^5.0.3" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@jridgewell/gen-mapping": { @@ -56,9 +280,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -73,6 +297,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", @@ -88,240 +336,111 @@ "@lit-labs/ssr-dom-shim": "^1.5.0" } }, - "node_modules/@rollup/plugin-commonjs": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.0.tgz", - "integrity": "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==", + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", "dev": true, "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" } }, - "node_modules/@rollup/plugin-json": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.0" - }, + "optional": true, + "os": [ + "android" + ], + "peer": true, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", - "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, + "optional": true, + "os": [ + "android" + ], + "peer": true, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "serialize-javascript": "^6.0.1", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rollup/plugin-typescript": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz", - "integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.0", - "resolve": "^1.22.1" - }, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.14.0||^3.0.0||^4.0.0", - "tslib": "*", - "typescript": ">=3.7.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - }, - "tslib": { - "optional": true - } + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", - "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", - "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", - "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", - "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", - "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", - "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", - "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", "cpu": [ "arm" ], @@ -330,180 +449,178 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", - "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", - "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", - "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", - "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", - "cpu": [ - "loong64" + "libc": [ + "musl" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", - "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", - "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", - "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", - "cpu": [ - "ppc64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", - "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", "cpu": [ - "riscv64" + "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", - "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", "cpu": [ - "riscv64" + "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", - "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", "cpu": [ - "s390x" + "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", - "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", - "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", - "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", "cpu": [ "x64" ], @@ -511,27 +628,185 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", - "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", - "cpu": [ - "arm64" + "win32" ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT", - "optional": true, + "peer": true + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.0.tgz", + "integrity": "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz", + "integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, "os": [ - "openharmony" + "android" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { + "node_modules/@rollup/rollup-android-arm64": { "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", - "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", "cpu": [ "arm64" ], @@ -539,282 +814,1978 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "android" ] }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { + "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", - "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.0.tgz", + "integrity": "sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/type-utils": "8.71.0", + "@typescript-eslint/utils": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.71.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.12.tgz", + "integrity": "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.0.tgz", + "integrity": "sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.0.tgz", + "integrity": "sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.0", + "@typescript-eslint/types": "^8.71.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.0.tgz", + "integrity": "sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz", + "integrity": "sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.0.tgz", + "integrity": "sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/utils": "8.71.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.0.tgz", + "integrity": "sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.0.tgz", + "integrity": "sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.71.0", + "@typescript-eslint/tsconfig-utils": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.0.tgz", + "integrity": "sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.0.tgz", + "integrity": "sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz", + "integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.3", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz", + "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz", + "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/happy-dom": { + "version": "20.14.5", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.5.tgz", + "integrity": "sha512-x/RzkpWO40bTjIoT30iQtt64FLLmH/iRcUCN2X//bLx7H3ifkdfPXyqsro/OYtqzIAhiLMMA7mmiOR9C3NOKjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ - "ia32" + "x64" ], "dev": true, - "license": "MIT", + "libc": [ + "musl" + ], + "license": "MPL-2.0", "optional": true, "os": [ - "win32" - ] + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", - "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ - "x64" + "arm64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "win32" - ] + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", - "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@types/node": { - "version": "25.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", - "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", - "dev": true, - "license": "MIT", + "node_modules/lit": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", + "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", + "license": "BSD-3-Clause", "dependencies": { - "undici-types": "~7.16.0" + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" } }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true, - "license": "MIT" + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "p-locate": "^5.0.0" }, "engines": { - "node": ">=0.4.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "MIT" + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, "engines": { - "node": ">=0.10.0" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=12.20.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.8.0" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "p-limit": "^3.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "*" + "engines": { + "node": ">=8" } }, - "node_modules/lit": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", - "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" - } + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" }, - "node_modules/lit-element": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", - "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/lit-html": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", - "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", - "license": "BSD-3-Clause", + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, "dependencies": { - "@types/trusted-types": "^2.0.2" + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=6" + } }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "hookified": "^2.1.1" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=20" } }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -846,6 +2817,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, "node_modules/rollup": { "version": "4.56.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", @@ -912,6 +2918,19 @@ ], "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -922,6 +2941,29 @@ "randombytes": "^2.1.0" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/smob": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", @@ -939,6 +2981,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -950,6 +3003,13 @@ "source-map": "^0.6.0" } }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -982,6 +3042,56 @@ "node": ">=10" } }, + "node_modules/tinybench": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", + "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -989,6 +3099,19 @@ "dev": true, "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1003,12 +3126,302 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.0.tgz", + "integrity": "sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.71.0", + "@typescript-eslint/parser": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/utils": "8.71.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz", + "integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.3", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.3", + "@vitest/browser-preview": "5.0.3", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.3", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index d31cf2f..afeba97 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "main": "dist/homeshift-card.js", "scripts": { "build": "rollup -c", - "watch": "rollup -c -w" + "watch": "rollup -c -w", + "typecheck": "tsc --noEmit -p test/tsconfig.json", + "lint": "eslint .", + "test": "vitest run" }, "keywords": [ "home-assistant", @@ -17,15 +20,21 @@ "author": "", "license": "MIT", "devDependencies": { + "@eslint/js": "^10.0.1", "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.3.0", "@types/node": "^25.0.8", + "eslint": "^10.12.0", + "globals": "^17.13.0", + "happy-dom": "^20.14.5", "rollup": "^4.55.1", "tslib": "^2.8.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "typescript-eslint": "^8.71.0", + "vitest": "^5.0.3" }, "dependencies": { "lit": "^3.3.2" diff --git a/test/localize.test.ts b/test/localize.test.ts new file mode 100644 index 0000000..558eabc --- /dev/null +++ b/test/localize.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { localize } from "../src/localize/localize"; + +describe("localize", () => { + it("resolves a key in the user's language", () => { + expect(localize({ locale: { language: "fr" } }, "card.off")).toBe("Éteint"); + }); + + it("falls back to English for an unknown language", () => { + expect(localize({ locale: { language: "de" } }, "card.off")).toBe("Off"); + }); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..9698150 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": ".." + }, + "include": ["../src/**/*", "./**/*"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..5b69960 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "happy-dom", + include: ["test/**/*.test.ts"], + }, +}); From dda76110ea9f2a065afb8780b36b26a1d407bfce Mon Sep 17 00:00:00 2001 From: Gamso Date: Sat, 3 Oct 2026 19:03:23 +0200 Subject: [PATCH 05/25] fix(editor): use thermostat_mode_entity and expose every option The visual editor read and wrote "hermostat_mode_tentity" while the card reads "thermostat_mode_entity": picking a thermostat in the editor saved a key the card ignored, and an existing YAML config showed an empty picker. The editor also only covered 8 of the 13 options and kept its own copy of the config interface, which is how the two drifted apart. HomeShiftCardConfig and a key/domain/default table now live in src/types.ts; the card derives its defaults and watched entities from the table and the editor generates one picker per entry, so a new option cannot be forgotten in one of them. Missing editor labels are added in en/fr. Tests assert that every key the editor writes is a key the card reads, that every entity option has a picker and a label. Audit: B-1, A-1 Co-Authored-By: Claude Opus 5.5 --- src/components/homeshift-card-editor.ts | 82 ++++------------------- src/components/homeshift-card.ts | 60 +++-------------- src/localize/en.json | 7 +- src/localize/fr.json | 7 +- src/types.ts | 88 +++++++++++++++++++++++++ test/editor.test.ts | 73 ++++++++++++++++++++ 6 files changed, 196 insertions(+), 121 deletions(-) create mode 100644 src/types.ts create mode 100644 test/editor.test.ts diff --git a/src/components/homeshift-card-editor.ts b/src/components/homeshift-card-editor.ts index 1c5069c..d8c0c4b 100644 --- a/src/components/homeshift-card-editor.ts +++ b/src/components/homeshift-card-editor.ts @@ -1,17 +1,7 @@ import { LitElement, html, css } from "lit"; import { property, state } from "lit/decorators.js"; import { localize } from "../localize/localize"; - -interface HomeShiftCardConfig { - name?: string; - day_mode_entity?: string; - hermostat_mode_tentity?: string; - cover_open_time_entity?: string; - cover_close_time_entity?: string; - cover_entity?: string; - covers_left_open_entity?: string; - show_title?: boolean; -} +import { ENTITY_FIELDS, EntityConfigKey, HomeShiftCardConfig } from "../types"; class HomeShiftCardEditor extends LitElement { @property({ attribute: false }) public hass!: any; @@ -34,11 +24,11 @@ class HomeShiftCardEditor extends LitElement { private _onEntityChanged( ev: CustomEvent, - fieldName: keyof HomeShiftCardConfig, + fieldName: EntityConfigKey, ) { if (!this._config || !this.hass) return; const newValue = ev.detail.value; - if ((this._config as any)[fieldName] === newValue) return; + if (this._config[fieldName] === newValue) return; const newConfig = { ...this._config, [fieldName]: newValue }; this._config = newConfig; @@ -78,61 +68,17 @@ class HomeShiftCardEditor extends LitElement { @input=${this._onNameChanged} > - - this._onEntityChanged(e, "day_mode_entity")} - allow-custom-entity - > - - - this._onEntityChanged(e, "hermostat_mode_tentity")} - allow-custom-entity - > - - - this._onEntityChanged(e, "cover_open_time_entity")} - allow-custom-entity - > - - - this._onEntityChanged(e, "cover_close_time_entity")} - allow-custom-entity - > - - - this._onEntityChanged(e, "cover_entity")} - allow-custom-entity - > - - - this._onEntityChanged(e, "covers_left_open_entity")} - allow-custom-entity - > + ${ENTITY_FIELDS.map( + (field) => html` + this._onEntityChanged(e, field.key)} + allow-custom-entity + >`, + )} this._config?.[field.key], + ).filter(Boolean) as string[]; return watchedEntities.some( (id) => oldHass.states[id] !== this.hass.states[id], ); diff --git a/src/localize/en.json b/src/localize/en.json index d7a666d..c86d42c 100644 --- a/src/localize/en.json +++ b/src/localize/en.json @@ -23,7 +23,12 @@ "editor": { "name": "Name", "day_mode_entity": "Day Mode Entity", - "hermostat_mode_tentity": "Thermostat Mode Entity", + "thermostat_mode_entity": "Thermostat Mode Entity", + "override_duration_entity": "Override Duration Entity", + "early_switch_entity": "Early Switch Entity", + "next_mode_entity": "Next Mode Entity", + "next_mode_at_entity": "Next Mode Time Entity", + "heat_protection_entity": "Heat Protection Entity", "cover_open_time_entity": "Cover Open Time Entity", "cover_close_time_entity": "Cover Close Time Entity", "cover_entity": "Cover Entity (manual open/close)", diff --git a/src/localize/fr.json b/src/localize/fr.json index 8505605..1c13765 100644 --- a/src/localize/fr.json +++ b/src/localize/fr.json @@ -23,7 +23,12 @@ "editor": { "name": "Nom", "day_mode_entity": "Entité mode jour", - "hermostat_mode_tentity": "Entité mode thermostat", + "thermostat_mode_entity": "Entité mode thermostat", + "override_duration_entity": "Entité durée de dérogation", + "early_switch_entity": "Entité anticipation", + "next_mode_entity": "Entité prochain mode", + "next_mode_at_entity": "Entité heure du prochain mode", + "heat_protection_entity": "Entité protection thermique", "cover_open_time_entity": "Entité heure d'ouverture des volets", "cover_close_time_entity": "Entité heure de fermeture des volets", "cover_entity": "Entité volet (ouverture/fermeture manuelle)", diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..76de890 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,88 @@ +/** Card configuration, shared by the card and its visual editor. */ +export interface HomeShiftCardConfig { + name?: string; + day_mode_entity?: string; + thermostat_mode_entity?: string; + override_duration_entity?: string; + early_switch_entity?: string; + next_mode_entity?: string; + next_mode_at_entity?: string; + heat_protection_entity?: string; + cover_open_time_entity?: string; + cover_close_time_entity?: string; + cover_entity?: string; + covers_left_open_entity?: string; + show_title?: boolean; +} + +export type EntityConfigKey = { + [K in keyof HomeShiftCardConfig]-?: K extends `${string}_entity` ? K : never; +}[keyof HomeShiftCardConfig]; + +/** + * Every entity option, in editor order, with the domains its picker offers + * and the entity the HomeShift integration creates by default ("" = none). + * The editor is generated from this table so it cannot drift from the + * config keys the card reads. + */ +export const ENTITY_FIELDS: readonly { + key: EntityConfigKey; + domains: string[]; + default: string; +}[] = [ + { + key: "day_mode_entity", + domains: ["select", "input_select"], + default: "select.homeshift_day_mode", + }, + { + key: "thermostat_mode_entity", + domains: ["select", "input_select"], + default: "select.homeshift_thermostat_mode", + }, + { + key: "override_duration_entity", + domains: ["number", "input_number"], + default: "number.homeshift_override_duration", + }, + { + key: "early_switch_entity", + domains: ["number", "input_number"], + default: "number.homeshift_early_switch", + }, + { + key: "next_mode_entity", + domains: ["sensor"], + default: "sensor.homeshift_next_mode", + }, + { + key: "next_mode_at_entity", + domains: ["sensor"], + default: "sensor.homeshift_next_mode_at", + }, + { + key: "heat_protection_entity", + domains: ["binary_sensor"], + default: "binary_sensor.homeshift_cover_heat_active", + }, + { + key: "cover_open_time_entity", + domains: ["sensor"], + default: "sensor.homeshift_cover_open_time", + }, + { + key: "cover_close_time_entity", + domains: ["sensor"], + default: "sensor.homeshift_cover_close_time", + }, + { + key: "cover_entity", + domains: ["cover"], + default: "", + }, + { + key: "covers_left_open_entity", + domains: ["binary_sensor"], + default: "binary_sensor.homeshift_covers_left_open", + }, +]; diff --git a/test/editor.test.ts b/test/editor.test.ts new file mode 100644 index 0000000..f92f018 --- /dev/null +++ b/test/editor.test.ts @@ -0,0 +1,73 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { HomeShiftCard } from "../src/components/homeshift-card"; +import { HomeShiftCardEditor } from "../src/components/homeshift-card-editor"; +import en from "../src/localize/en.json"; +import fr from "../src/localize/fr.json"; + +const hass = { states: {}, locale: { language: "en" } }; + +async function renderEditor(): Promise { + const editor = document.createElement( + "homeshift-card-editor", + ) as HomeShiftCardEditor; + (editor as any).hass = hass; + editor.setConfig(HomeShiftCard.getStubConfig()); + document.body.appendChild(editor); + await editor.updateComplete; + return editor; +} + +function cardConfigKeys(): string[] { + const card = new HomeShiftCard(); + card.setConfig({}); + return Object.keys((card as any)._config); +} + +afterEach(() => { + document.body.innerHTML = ""; +}); + +describe("homeshift-card-editor", () => { + it("only writes keys the card reads", async () => { + const editor = await renderEditor(); + const written = new Set(); + editor.addEventListener("config-changed", (e: Event) => { + const config = (e as CustomEvent).detail.config; + Object.keys(config).forEach((k) => written.add(k)); + }); + + const pickers = editor.shadowRoot!.querySelectorAll("ha-entity-picker"); + pickers.forEach((picker, i) => + picker.dispatchEvent( + new CustomEvent("value-changed", { detail: { value: `sensor.x${i}` } }), + ), + ); + + expect(written.size).toBeGreaterThan(0); + const cardKeys = cardConfigKeys(); + for (const key of written) expect(cardKeys).toContain(key); + }); + + it("exposes every entity option of the card", async () => { + const editor = await renderEditor(); + const pickers = editor.shadowRoot!.querySelectorAll("ha-entity-picker"); + const entityKeys = cardConfigKeys().filter((k) => k.endsWith("_entity")); + expect(pickers.length).toBe(entityKeys.length); + }); + + it("shows the thermostat entity of an existing YAML config", async () => { + const editor = await renderEditor(); + const values = [ + ...editor.shadowRoot!.querySelectorAll("ha-entity-picker"), + ].map((p) => (p as any).value); + expect(values).toContain("select.homeshift_thermostat_mode"); + }); + + it("has a label in every language for every option", () => { + for (const dict of [en, fr]) { + for (const key of cardConfigKeys()) { + expect(dict.editor, key).toHaveProperty(key); + } + } + }); +}); From 78a8a5a3d4d09ab6fee8bf709880ae21fcb51075 Mon Sep 17 00:00:00 2001 From: Gamso Date: Sat, 3 Oct 2026 19:04:40 +0200 Subject: [PATCH 06/25] fix(i18n): fall back to English, then to a caller fallback, in localize localize() returned the key itself when a translation was missing, so every `localize(...) || "default"` in the card was dead code, a key missing from fr.json showed up raw instead of in English, and the preview stub displayed "preview.day_mode_state" with no option or arc segment highlighted (the preview section existed in no dictionary). Lookup is now: user language, English, optional fallback argument, key. The dead `||` fallbacks are removed; thermostat modes unknown to the card fall back to the integration's display value; the preview stub is built from new preview.* strings and the existing thermostat labels so its state matches an option. Tests cover the lookup chain, parameters and en/fr key parity. Audit: A-3 Co-Authored-By: Claude Opus 5.5 --- src/components/homeshift-card.ts | 73 +++++++++++++++----------------- src/localize/en.json | 6 +++ src/localize/fr.json | 6 +++ src/localize/localize.ts | 20 +++++++-- test/localize.test.ts | 38 ++++++++++++++++- 5 files changed, 98 insertions(+), 45 deletions(-) diff --git a/src/components/homeshift-card.ts b/src/components/homeshift-card.ts index 30fd000..3102135 100644 --- a/src/components/homeshift-card.ts +++ b/src/components/homeshift-card.ts @@ -182,12 +182,12 @@ class HomeShiftCard extends LitElement { if (isNaN(dt.getTime())) return isoString; const diffMs = dt.getTime() - Date.now(); const diffMin = Math.round(diffMs / 60000); - if (diffMin <= 0) return localize(this.hass, "card.past") || "passé"; + if (diffMin <= 0) return localize(this.hass, "card.past"); if (diffMin < 60) - return `${localize(this.hass, "card.in") || "dans"} ${diffMin} min`; + return `${localize(this.hass, "card.in")} ${diffMin} min`; const h = Math.floor(diffMin / 60); const m = diffMin % 60; - return `${localize(this.hass, "card.in") || "dans"} ${h}h${m > 0 ? m.toString().padStart(2, "0") : ""}`; + return `${localize(this.hass, "card.in")} ${h}h${m > 0 ? m.toString().padStart(2, "0") : ""}`; } private _formatAbsoluteTime(isoString?: string): string { @@ -202,10 +202,10 @@ class HomeShiftCard extends LitElement { minute: "2-digit", }); if (dt.toDateString() === now.toDateString()) { - return `${localize(this.hass, "card.today") || "Auj."} ${timeStr}`; + return `${localize(this.hass, "card.today")} ${timeStr}`; } if (dt.toDateString() === tomorrow.toDateString()) { - return `${localize(this.hass, "card.tomorrow") || "Dem."} ${timeStr}`; + return `${localize(this.hass, "card.tomorrow")} ${timeStr}`; } return `${dt.toLocaleDateString([], { month: "short", day: "numeric" })} ${timeStr}`; } @@ -256,16 +256,17 @@ class HomeShiftCard extends LitElement { const THERMOSTAT_ACTIVE_KEYS = ["heating", "cooling", "ventilation"]; const thermoLocalizedLabels: string[] = hasThermoMap ? [ - localize(this.hass, "card.off") || offDisplay, - ...Object.keys(thermoModeMap) - .filter((key) => key !== "off") - .map((key) => localize(this.hass, `thermostat.${key}`) || key), + localize(this.hass, "card.off"), + ...Object.entries(thermoModeMap) + .filter(([key]) => key !== "off") + .map(([key, display]) => + localize(this.hass, `thermostat.${key}`, undefined, display), + ), ] : [ - localize(this.hass, "card.off") || - (thermo.attributes?.options?.[0] ?? ""), - ...THERMOSTAT_ACTIVE_KEYS.map( - (key) => localize(this.hass, `thermostat.${key}`) || key, + localize(this.hass, "card.off"), + ...THERMOSTAT_ACTIVE_KEYS.map((key) => + localize(this.hass, `thermostat.${key}`), ), ]; @@ -293,10 +294,8 @@ class HomeShiftCard extends LitElement { ? html`
this.onCoverAction("open_cover")} > @@ -307,10 +306,8 @@ class HomeShiftCard extends LitElement { ? html`
this.onCoverAction("close_cover")} > @@ -327,7 +324,7 @@ class HomeShiftCard extends LitElement { title="${localize( this.hass, "card.heat_protection_active", - ) || "Heat protection active"}" + )}" >
` @@ -450,31 +447,27 @@ class HomeShiftCard extends LitElement { const thermoRaw = this.getEntityState(this._config.thermostat_mode_entity); const day = dayRaw ?? { - entity_id: this._config.day_mode_entity ?? "select.homeshift_day_mode", - state: localize(this.hass, "preview.day_mode_state") || "Travail", + entity_id: this._config.day_mode_entity, + state: localize(this.hass, "preview.work"), attributes: { - options: ["Maison", "Travail", "Télétravail", "Absence"], - option_map: { - home: "Maison", - work: "Travail", - remote: "Télétravail", - away: "Absence", - }, + option_map: Object.fromEntries( + ["home", "work", "remote", "away"].map((key) => [ + key, + localize(this.hass, `preview.${key}`), + ]), + ), }, }; const thermo = thermoRaw ?? { - entity_id: - this._config.thermostat_mode_entity ?? - "select.homeshift_thermostat_mode", - state: localize(this.hass, "preview.thermostat_state") || "Chauffage", + entity_id: this._config.thermostat_mode_entity, + state: localize(this.hass, "thermostat.heating"), attributes: { - options: ["Eteint", "Chauffage", "Climatisation", "Ventilation"], option_map: { - off: "Eteint", - heating: "Chauffage", - cooling: "Climatisation", - ventilation: "Ventilation", + off: localize(this.hass, "card.off"), + heating: localize(this.hass, "thermostat.heating"), + cooling: localize(this.hass, "thermostat.cooling"), + ventilation: localize(this.hass, "thermostat.ventilation"), }, }, }; diff --git a/src/localize/en.json b/src/localize/en.json index c86d42c..f1e35d8 100644 --- a/src/localize/en.json +++ b/src/localize/en.json @@ -34,5 +34,11 @@ "cover_entity": "Cover Entity (manual open/close)", "covers_left_open_entity": "Covers Left Open Entity", "show_title": "Show title" + }, + "preview": { + "home": "Home", + "work": "Work", + "remote": "Remote", + "away": "Away" } } diff --git a/src/localize/fr.json b/src/localize/fr.json index 1c13765..f901171 100644 --- a/src/localize/fr.json +++ b/src/localize/fr.json @@ -34,5 +34,11 @@ "cover_entity": "Entité volet (ouverture/fermeture manuelle)", "covers_left_open_entity": "Entité volets non fermés", "show_title": "Afficher le titre" + }, + "preview": { + "home": "Maison", + "work": "Travail", + "remote": "Télétravail", + "away": "Absence" } } diff --git a/src/localize/localize.ts b/src/localize/localize.ts index b5c5a32..9ca4d35 100644 --- a/src/localize/localize.ts +++ b/src/localize/localize.ts @@ -18,11 +18,23 @@ function resolveKey(obj: Dict, path: string): any { return path.split('.').reduce((acc: any, k: string) => (acc && acc[k] != null ? acc[k] : undefined), obj); } -export function localize(hass: any, key: string, params?: Record): string { +/** + * Translates `key` into the Home Assistant user's language. + * + * Lookup order: user's language, then English, then `fallback`, then the + * key itself (so a missing translation stays visible during development). + */ +export function localize( + hass: any, + key: string, + params?: Record, + fallback?: string, +): string { const lang = get(hass); - const dict = languages[lang] || languages.en; - let text = resolveKey(dict, key) as string | undefined; - if (!text) return key; + let text = (resolveKey(languages[lang], key) ?? + resolveKey(languages.en, key) ?? + fallback) as string | undefined; + if (text == null) return key; if (params) { Object.entries(params).forEach(([k, v]) => (text = (text as string).replace(`{${k}}`, v))); } diff --git a/test/localize.test.ts b/test/localize.test.ts index 558eabc..e02e110 100644 --- a/test/localize.test.ts +++ b/test/localize.test.ts @@ -1,12 +1,48 @@ import { describe, expect, it } from "vitest"; import { localize } from "../src/localize/localize"; +import en from "../src/localize/en.json"; +import fr from "../src/localize/fr.json"; + +const FR = { locale: { language: "fr" } }; + +function leafKeys(obj: Record, prefix = ""): string[] { + return Object.entries(obj).flatMap(([k, v]) => + typeof v === "object" ? leafKeys(v, `${prefix}${k}.`) : [`${prefix}${k}`], + ); +} describe("localize", () => { it("resolves a key in the user's language", () => { - expect(localize({ locale: { language: "fr" } }, "card.off")).toBe("Éteint"); + expect(localize(FR, "card.off")).toBe("Éteint"); }); it("falls back to English for an unknown language", () => { expect(localize({ locale: { language: "de" } }, "card.off")).toBe("Off"); }); + + it("uses the region-less language code", () => { + expect(localize({ locale: { language: "fr-CA" } }, "card.off")).toBe( + "Éteint", + ); + }); + + it("returns the caller's fallback for a missing key", () => { + expect(localize(FR, "thermostat.dehumidify", undefined, "Déshu")).toBe( + "Déshu", + ); + }); + + it("returns the key when nothing else is available", () => { + expect(localize(FR, "card.does_not_exist")).toBe("card.does_not_exist"); + }); + + it("substitutes parameters", () => { + expect(localize(FR, "card.covers_left_open", { covers: "Salon" })).toBe( + "Volets non fermés : Salon", + ); + }); + + it("has the same keys in every language", () => { + expect(leafKeys(fr).sort()).toEqual(leafKeys(en).sort()); + }); }); From 68127234e067d9a46bab2b9b776c5d8bd4600f97 Mon Sep 17 00:00:00 2001 From: Gamso Date: Sat, 3 Oct 2026 19:08:08 +0200 Subject: [PATCH 07/25] fix(card): handle missing, unknown and unavailable entities - Missing entities (B-3): the stub day/thermostat data was used on every dashboard, so a typo in day_mode_entity produced a card that looked functional. The stub is now reserved for the card picker / editor preview (HA's `preview` property); elsewhere the card shows the existing, previously unused card.entity_not_found message. - Next mode (B-4): unavailable was not filtered and an "unknown" next_mode_at was truthy, giving an invalid Date and "unknown" in the dial centre. A shared isUsable() now filters both sensors and the cover times; an unparsable timestamp renders nothing. - Number dropdowns (B-5): Number("unavailable") gave NaN, highlighted the dropdown and set value "NaN"; a value outside the presets (25 min) had no option. Values are read with Number.isFinite, an ad hoc option is injected for off-preset values, the dropdown is disabled while the entity is unavailable, and both dropdowns share one renderer (the override dropdown is now also hidden when its entity is not set). Audit: B-3, B-4, B-5 Co-Authored-By: Claude Opus 5.5 --- src/components/homeshift-card.ts | 153 +++++++++++++++++-------------- src/state.ts | 26 ++++++ test/helpers.ts | 61 ++++++++++++ test/states.test.ts | 109 ++++++++++++++++++++++ 4 files changed, 280 insertions(+), 69 deletions(-) create mode 100644 src/state.ts create mode 100644 test/helpers.ts create mode 100644 test/states.test.ts diff --git a/src/components/homeshift-card.ts b/src/components/homeshift-card.ts index 3102135..7615063 100644 --- a/src/components/homeshift-card.ts +++ b/src/components/homeshift-card.ts @@ -4,6 +4,7 @@ import { localize } from "../localize/localize"; import "./circular-slider"; import "./homeshift-card-editor"; import { ENTITY_FIELDS, HomeShiftCardConfig } from "../types"; +import { formatMinutes, isUsable, numericState } from "../state"; class HomeShiftCard extends LitElement { // Predefined override duration values in minutes (0 = disabled). @@ -36,6 +37,8 @@ class HomeShiftCard extends LitElement { ]; @property({ attribute: false }) public hass!: any; + /** Set by Home Assistant in the card picker and the editor preview. */ + @property({ type: Boolean }) public preview = false; @state() private _config!: HomeShiftCardConfig; @state() private _tick = 0; private _refreshInterval?: ReturnType; @@ -94,7 +97,7 @@ class HomeShiftCard extends LitElement { } protected shouldUpdate(changedProps: Map): boolean { - if (changedProps.has("_config")) return true; + if (changedProps.has("_config") || changedProps.has("preview")) return true; if (changedProps.has("_tick")) return true; if (changedProps.has("hass")) { const oldHass = changedProps.get("hass") as any; @@ -156,24 +159,48 @@ class HomeShiftCard extends LitElement { this.hass.callService("cover", action, { entity_id: entityId }); } - private onOverrideDurationSelect(ev: Event) { - const entityId = this._config.override_duration_entity; - if (!entityId) return; + private onPresetSelect(entityId: string, ev: Event) { const value = Number((ev.target as HTMLSelectElement).value); + if (!Number.isFinite(value)) return; this.hass.callService("number", "set_value", { entity_id: entityId, value, }); } - private onEarlySwitchSelect(ev: Event) { - const entityId = this._config.early_switch_entity; - if (!entityId) return; - const value = Number((ev.target as HTMLSelectElement).value); - this.hass.callService("number", "set_value", { - entity_id: entityId, - value, - }); + /** + * Dropdown for a minutes number entity (override duration, early switch). + * Highlighted only for a real non-zero value; a value set outside the + * presets (e.g. 25 min from the entity page) gets its own option so the + * dropdown shows what is actually active. Disabled while unavailable. + */ + private _renderPresetSelect( + entityId: string | undefined, + presets: { label: string; value: number }[], + modifierClass: string, + ) { + if (!entityId) return nothing; + const current = numericState(this.getEntityState(entityId)); + const options = + current !== undefined && !presets.some((p) => p.value === current) + ? [...presets, { label: formatMinutes(current), value: current }].sort( + (a, b) => a.value - b.value, + ) + : presets; + const active = current !== undefined && current !== 0; + return html``; } private _formatRelativeTime(isoString?: string): string { @@ -193,7 +220,7 @@ class HomeShiftCard extends LitElement { private _formatAbsoluteTime(isoString?: string): string { if (!isoString) return ""; const dt = new Date(isoString); - if (isNaN(dt.getTime())) return isoString; + if (isNaN(dt.getTime())) return ""; const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); @@ -272,17 +299,14 @@ class HomeShiftCard extends LitElement { const nextMode = this.getEntityState(this._config.next_mode_entity); const nextModeAt = this.getEntityState(this._config.next_mode_at_entity); - const currentEarlySwitch = Number( - this.getEntityState(this._config.early_switch_entity)?.state ?? 0, - ); - - const hasNextMode = - nextMode?.state && nextMode.state !== "unknown" && nextMode.state !== ""; + // The next-mode sensors are unknown when no switch is scheduled. + const hasNextMode = isUsable(nextMode); + const nextModeAtText = isUsable(nextModeAt) + ? this._formatAbsoluteTime(nextModeAt.state) + : ""; - const hasCoverOpenTime = - coverOpenTime && coverOpenTime !== "unknown" && coverOpenTime !== "unavailable"; - const hasCoverCloseTime = - coverCloseTime && coverCloseTime !== "unknown" && coverCloseTime !== "unavailable"; + const hasCoverOpenTime = isUsable(coverOpenTime); + const hasCoverCloseTime = isUsable(coverCloseTime); const canControlCover = Boolean(this._config.cover_entity); @@ -354,39 +378,25 @@ class HomeShiftCard extends LitElement { >
- ${hasNextMode || nextModeAt?.state + ${hasNextMode || nextModeAtText ? html`
${hasNextMode ? html`${nextMode!.state}` : nothing} - ${nextModeAt?.state + ${nextModeAtText ? html`${this._formatAbsoluteTime(nextModeAt.state)}${nextModeAtText}` : nothing}
` : nothing} - ${this._config.early_switch_entity - ? html`` - : nothing} + ${this._renderPresetSelect( + this._config.early_switch_entity, + HomeShiftCard.EARLY_SWITCH_PRESETS, + "list-select--early", + )}
@@ -408,29 +418,11 @@ class HomeShiftCard extends LitElement {
- ${(() => { - const currentOverride = Number( - this.getEntityState(this._config.override_duration_entity) - ?.state ?? 0, - ); - return html``; - })()} + ${this._renderPresetSelect( + this._config.override_duration_entity, + HomeShiftCard.OVERRIDE_PRESETS, + "list-select--override", + )}
@@ -442,10 +434,33 @@ class HomeShiftCard extends LitElement { const title = this._config.name ?? localize(this.hass, "card.title"); - // Use real entities when available, fall back to stub data for the picker preview const dayRaw = this.getEntityState(this._config.day_mode_entity); const thermoRaw = this.getEntityState(this._config.thermostat_mode_entity); + // Stub data is only for the card picker / editor preview: on a real + // dashboard a missing entity (typo, integration not loaded) must say so + // rather than show a card that looks functional. + if (!this.preview) { + const missing = [ + [this._config.day_mode_entity, dayRaw], + [this._config.thermostat_mode_entity, thermoRaw], + ] + .filter(([, stateObj]) => !stateObj) + .map(([entityId]) => entityId || "?"); + if (missing.length > 0) { + return html` + + ${missing.map( + (entity) => + html`
+ ${localize(this.hass, "card.entity_not_found", { entity })} +
`, + )} +
+ `; + } + } + const day = dayRaw ?? { entity_id: this._config.day_mode_entity, state: localize(this.hass, "preview.work"), diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..8c25ce4 --- /dev/null +++ b/src/state.ts @@ -0,0 +1,26 @@ +/** States Home Assistant uses when an entity has no meaningful value. */ +const UNUSABLE_STATES = new Set(["unknown", "unavailable", ""]); + +/** + * True when the entity exists and carries a real value. Accepts a state + * object or a raw state string. + */ +export function isUsable(stateObj: { state?: unknown } | string | undefined | null): boolean { + const state = typeof stateObj === "string" ? stateObj : stateObj?.state; + return typeof state === "string" && !UNUSABLE_STATES.has(state); +} + +/** Numeric value of a number entity, or undefined when it has none. */ +export function numericState(stateObj: { state?: unknown } | undefined): number | undefined { + if (!isUsable(stateObj)) return undefined; + const value = Number(stateObj!.state); + return Number.isFinite(value) ? value : undefined; +} + +/** Same label format as the preset lists: "25min", "1h", "1h30". */ +export function formatMinutes(minutes: number): string { + if (minutes < 60) return `${minutes}min`; + const h = Math.floor(minutes / 60); + const m = Math.round(minutes % 60); + return m > 0 ? `${h}h${String(m).padStart(2, "0")}` : `${h}h`; +} diff --git a/test/helpers.ts b/test/helpers.ts new file mode 100644 index 0000000..c204c19 --- /dev/null +++ b/test/helpers.ts @@ -0,0 +1,61 @@ +import { vi } from "vitest"; +import { HomeShiftCard } from "../src/components/homeshift-card"; +import "../src/components/homeshift-card"; + +export function entity(entity_id: string, state: string, attributes = {}) { + return { entity_id, state, attributes }; +} + +/** States of a fully configured HomeShift integration. */ +export function homeshiftStates(): Record { + const list = [ + entity("select.homeshift_day_mode", "Travail", { + options: ["Maison", "Travail"], + option_map: { home: "Maison", work: "Travail" }, + }), + entity("select.homeshift_thermostat_mode", "Chauffage", { + options: ["Eteint", "Chauffage", "Climatisation"], + option_map: { off: "Eteint", heating: "Chauffage", cooling: "Climatisation" }, + }), + entity("number.homeshift_override_duration", "0"), + entity("number.homeshift_early_switch", "0"), + entity("sensor.homeshift_next_mode", "Maison"), + entity("sensor.homeshift_next_mode_at", new Date().toISOString()), + entity("binary_sensor.homeshift_cover_heat_active", "off"), + entity("sensor.homeshift_cover_open_time", "08:30"), + entity("sensor.homeshift_cover_close_time", "21:15"), + entity("binary_sensor.homeshift_covers_left_open", "off", { covers: [] }), + ]; + return Object.fromEntries(list.map((e) => [e.entity_id, e])); +} + +export function makeHass(states: Record, language = "en") { + return { + states, + locale: { language, time_format: "24" }, + language, + callService: vi.fn().mockResolvedValue(undefined), + }; +} + +export async function renderCard( + hass: any, + config: Record = {}, + preview = false, +): Promise { + const card = document.createElement("homeshift-card") as HomeShiftCard; + card.setConfig(config); + card.preview = preview; + card.hass = hass; + document.body.appendChild(card); + await card.updateComplete; + return card; +} + +export function $(card: HomeShiftCard, selector: string) { + return card.shadowRoot!.querySelector(selector) as HTMLElement | null; +} + +export function $$(card: HomeShiftCard, selector: string) { + return [...card.shadowRoot!.querySelectorAll(selector)] as HTMLElement[]; +} diff --git a/test/states.test.ts b/test/states.test.ts new file mode 100644 index 0000000..0b95fbc --- /dev/null +++ b/test/states.test.ts @@ -0,0 +1,109 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { formatMinutes, isUsable, numericState } from "../src/state"; +import { $, $$, entity, homeshiftStates, makeHass, renderCard } from "./helpers"; + +afterEach(() => { + document.body.innerHTML = ""; +}); + +describe("state helpers", () => { + it("rejects unknown, unavailable, empty and missing states", () => { + expect(isUsable(undefined)).toBe(false); + expect(isUsable("unknown")).toBe(false); + expect(isUsable({ state: "unavailable" })).toBe(false); + expect(isUsable({ state: "" })).toBe(false); + expect(isUsable({ state: "08:30" })).toBe(true); + }); + + it("reads numbers only from usable finite states", () => { + expect(numericState({ state: "unavailable" })).toBeUndefined(); + expect(numericState({ state: "abc" })).toBeUndefined(); + expect(numericState({ state: "25" })).toBe(25); + }); + + it("formats minutes like the preset labels", () => { + expect(formatMinutes(25)).toBe("25min"); + expect(formatMinutes(60)).toBe("1h"); + expect(formatMinutes(90)).toBe("1h30"); + }); +}); + +describe("missing entities (B-3)", () => { + it("reports a missing entity instead of showing stub data", async () => { + const states = homeshiftStates(); + delete states["select.homeshift_day_mode"]; + const card = await renderCard(makeHass(states)); + expect($(card, ".error")?.textContent).toContain( + "Entity not found: select.homeshift_day_mode", + ); + expect($(card, "homeshift-circular-slider")).toBeNull(); + }); + + it("uses localized stub data in the card picker preview", async () => { + const card = await renderCard(makeHass({}, "fr"), {}, true); + expect($(card, ".error")).toBeNull(); + const labels = $$(card, ".day-section option").map((o) => o.textContent!.trim()); + expect(labels).toEqual(["Maison", "Travail", "Télétravail", "Absence"]); + const selected = $(card, ".day-section option[selected]"); + expect(selected?.textContent?.trim()).toBe("Travail"); + const slider = $(card, "homeshift-circular-slider") as any; + expect(slider.options).toContain(slider.currentValue); + }); +}); + +describe("unknown / unavailable sensors (B-4)", () => { + it.each(["unknown", "unavailable"])("hides next mode when %s", async (s) => { + const states = homeshiftStates(); + states["sensor.homeshift_next_mode"] = entity("sensor.homeshift_next_mode", s); + states["sensor.homeshift_next_mode_at"] = entity("sensor.homeshift_next_mode_at", s); + const card = await renderCard(makeHass(states)); + expect($(card, ".next-info")).toBeNull(); + expect(card.shadowRoot!.textContent).not.toContain(s); + }); + + it("hides cover times when unavailable", async () => { + const states = homeshiftStates(); + states["sensor.homeshift_cover_open_time"].state = "unavailable"; + states["sensor.homeshift_cover_close_time"].state = "unknown"; + const card = await renderCard(makeHass(states)); + expect($(card, ".cover-times")).toBeNull(); + }); +}); + +describe("number dropdowns (B-5)", () => { + it("is neither highlighted nor NaN when unavailable", async () => { + const states = homeshiftStates(); + states["number.homeshift_override_duration"].state = "unavailable"; + const card = await renderCard(makeHass(states)); + const select = $(card, ".list-select--override") as HTMLSelectElement; + expect(select.classList.contains("active")).toBe(false); + expect(select.disabled).toBe(true); + expect(card.shadowRoot!.innerHTML).not.toContain("NaN"); + }); + + it("shows a value set outside the presets", async () => { + const states = homeshiftStates(); + states["number.homeshift_early_switch"].state = "25"; + const card = await renderCard(makeHass(states)); + const select = $(card, ".list-select--early") as HTMLSelectElement; + expect(select.classList.contains("active")).toBe(true); + // happy-dom miscounts selectedIndex next to Lit's comment markers, so + // assert on the attribute a browser uses to pick the selected option. + const selected = select.querySelectorAll("option[selected]"); + expect(selected).toHaveLength(1); + expect((selected[0] as HTMLOptionElement).value).toBe("25"); + expect(selected[0].textContent!.trim()).toBe("25min"); + }); + + it("calls number.set_value with the chosen preset", async () => { + const hass = makeHass(homeshiftStates()); + const card = await renderCard(hass); + const select = $(card, ".list-select--override") as HTMLSelectElement; + select.value = "60"; + select.dispatchEvent(new Event("change")); + expect(hass.callService).toHaveBeenCalledWith("number", "set_value", { + entity_id: "number.homeshift_override_duration", + value: 60, + }); + }); +}); From ea0aebfb275183becbf6ee77c67f7b7398f64611 Mon Sep 17 00:00:00 2001 From: Gamso Date: Sat, 3 Oct 2026 19:09:25 +0200 Subject: [PATCH 08/25] refactor: replace the 30 s re-render with a midnight timer, drop dead code The card re-rendered every 30 seconds although entity changes already trigger renders and the only time-dependent output is the Today / Tomorrow prefix, which changes at midnight. A single timeout now fires just after midnight and reschedules itself; it is still cleared in disconnectedCallback. Removed, never called or never rendered: _formatRelativeTime, onOffButtonClick, the .off-row / .center-button / hui-card styles, the slider's _onSvgClick / _getPercentageFromEvent, its angle constants, @query svg and its unused hass / entityId properties, and the card.next_scan / card.in / card.past strings. noUnusedLocals and noUnusedParameters are enabled so dead private members fail the build. Audit: P-1 Co-Authored-By: Claude Opus 5.5 --- src/components/circular-slider.ts | 49 +--------------- src/components/homeshift-card.ts | 93 +++++++------------------------ src/localize/en.json | 3 - src/localize/fr.json | 3 - test/time.test.ts | 29 ++++++++++ tsconfig.json | 2 + 6 files changed, 53 insertions(+), 126 deletions(-) create mode 100644 test/time.test.ts diff --git a/src/components/circular-slider.ts b/src/components/circular-slider.ts index 25f8cb2..01eeb10 100644 --- a/src/components/circular-slider.ts +++ b/src/components/circular-slider.ts @@ -1,5 +1,5 @@ import { LitElement, html, css, svg } from "lit"; -import { property, query } from "lit/decorators.js"; +import { property } from "lit/decorators.js"; /* ======================= CONFIGURATION @@ -11,12 +11,6 @@ const CLICK_AREA_PADDING = 10; // 180° arc (opening at the bottom) const ARC_PATH = "M 30 150 A 85 85 0 1 1 170 150"; -// Angles used for click calculations -const START_ANGLE = 34.2; -const TOTAL_ARC_DEGREES = 180; -const ARC_END_ANGLE = START_ANGLE - TOTAL_ARC_DEGREES; -const ARC_END_ANGLE_POSITIVE = ARC_END_ANGLE + 360; - /* ======================= COMPONENT ======================= */ @@ -30,14 +24,11 @@ const SEGMENT_COLORS = [ ]; export class HomeShiftCircularSlider extends LitElement { - @property({ attribute: false }) public hass!: any; - @property() public entityId!: string; @property() public currentValue!: string; @property({ type: Array }) public options: string[] = []; /** Optional display labels shown on the arc. Falls back to options[i] when not provided. */ @property({ type: Array }) public labels: string[] = []; @property({ type: Number }) private selectedIndex = -1; - @query("svg") private _svg?: SVGSVGElement; /* ======================= UTILS @@ -67,44 +58,6 @@ export class HomeShiftCircularSlider extends LitElement { return [`${length} 10`, `-${start}`]; } - private _getPercentageFromEvent(e: MouseEvent): number { - if (!this._svg) return -1; - - const rect = this._svg.getBoundingClientRect(); - const x = (2 * (e.clientX - rect.left - rect.width / 2)) / rect.width; - const y = (2 * (e.clientY - rect.top - rect.height / 2)) / rect.height; - - const angle = (Math.atan2(y, x) * 180) / Math.PI; - - if (angle >= ARC_END_ANGLE && angle <= START_ANGLE) { - return (START_ANGLE - angle) / TOTAL_ARC_DEGREES; - } - - if (angle >= ARC_END_ANGLE_POSITIVE) { - return (START_ANGLE + (360 - angle)) / TOTAL_ARC_DEGREES; - } - - return -1; - } - - private _onSvgClick(e: MouseEvent) { - const p = this._getPercentageFromEvent(e); - if (p < 0 || this.options.length === 0) return; - - // Map percentage (0 to 1) directly to option index - const index = Math.floor(p * this.options.length); - - const selectedIdx = Math.max(0, Math.min(index, this.options.length - 1)); - - this.dispatchEvent( - new CustomEvent("option-selected", { - detail: { option: this.options[selectedIdx] }, - bubbles: true, - composed: true, - }), - ); - } - /* ======================= RENDER ======================= */ diff --git a/src/components/homeshift-card.ts b/src/components/homeshift-card.ts index 7615063..5e1f740 100644 --- a/src/components/homeshift-card.ts +++ b/src/components/homeshift-card.ts @@ -40,19 +40,33 @@ class HomeShiftCard extends LitElement { /** Set by Home Assistant in the card picker and the editor preview. */ @property({ type: Boolean }) public preview = false; @state() private _config!: HomeShiftCardConfig; - @state() private _tick = 0; - private _refreshInterval?: ReturnType; + /** Bumped at midnight so the "Today" / "Tomorrow" prefixes roll over. */ + @state() private _day = 0; + private _midnightTimer?: ReturnType; connectedCallback() { super.connectedCallback(); - this._refreshInterval = setInterval(() => { - this._tick++; - }, 30000); + this._scheduleMidnightRefresh(); } disconnectedCallback() { super.disconnectedCallback(); - clearInterval(this._refreshInterval); + clearTimeout(this._midnightTimer); + } + + /** + * Entity changes already trigger renders; the only time-dependent output + * is the day prefix of the next-mode time, which changes at midnight. + */ + private _scheduleMidnightRefresh() { + clearTimeout(this._midnightTimer); + const now = new Date(); + const nextMidnight = new Date(now); + nextMidnight.setHours(24, 0, 1, 0); + this._midnightTimer = setTimeout(() => { + this._day++; + this._scheduleMidnightRefresh(); + }, nextMidnight.getTime() - now.getTime()); } public static getStubConfig(): HomeShiftCardConfig { @@ -98,7 +112,7 @@ class HomeShiftCard extends LitElement { protected shouldUpdate(changedProps: Map): boolean { if (changedProps.has("_config") || changedProps.has("preview")) return true; - if (changedProps.has("_tick")) return true; + if (changedProps.has("_day")) return true; if (changedProps.has("hass")) { const oldHass = changedProps.get("hass") as any; if (!oldHass) return true; @@ -145,14 +159,6 @@ class HomeShiftCard extends LitElement { }); } - private onOffButtonClick(entityId: string, offOption: string) { - if (!offOption) return; - this.hass.callService("select", "select_option", { - entity_id: entityId, - option: offOption, - }); - } - private onCoverAction(action: "open_cover" | "close_cover") { const entityId = this._config.cover_entity; if (!entityId) return; @@ -203,20 +209,6 @@ class HomeShiftCard extends LitElement { `; } - private _formatRelativeTime(isoString?: string): string { - if (!isoString) return ""; - const dt = new Date(isoString); - if (isNaN(dt.getTime())) return isoString; - const diffMs = dt.getTime() - Date.now(); - const diffMin = Math.round(diffMs / 60000); - if (diffMin <= 0) return localize(this.hass, "card.past"); - if (diffMin < 60) - return `${localize(this.hass, "card.in")} ${diffMin} min`; - const h = Math.floor(diffMin / 60); - const m = diffMin % 60; - return `${localize(this.hass, "card.in")} ${h}h${m > 0 ? m.toString().padStart(2, "0") : ""}`; - } - private _formatAbsoluteTime(isoString?: string): string { if (!isoString) return ""; const dt = new Date(isoString); @@ -368,8 +360,6 @@ class HomeShiftCard extends LitElement {
` : nothing} { + vi.useRealTimers(); + document.body.innerHTML = ""; +}); + +describe("next mode time", () => { + it("rolls Tomorrow over to Today at midnight, without polling", async () => { + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "setInterval", "Date"] }); + vi.setSystemTime(new Date(2026, 0, 10, 23, 58)); + const states = homeshiftStates(); + states["sensor.homeshift_next_mode_at"] = entity( + "sensor.homeshift_next_mode_at", + new Date(2026, 0, 11, 8, 0).toISOString(), + ); + const card = await renderCard(makeHass(states)); + expect($(card, ".next-mode-at-state")?.textContent).toMatch(/^Tomorrow /); + expect(vi.getTimerCount()).toBe(1); + + await vi.advanceTimersByTimeAsync(3 * 60 * 1000); + await card.updateComplete; + expect($(card, ".next-mode-at-state")?.textContent).toMatch(/^Today /); + + card.remove(); + expect(vi.getTimerCount()).toBe(0); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index fac6930..a9f0fe5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,8 @@ "outDir": "dist", "rootDir": "src", "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, From 32823575c24b9b220c7ec23eb38f46fce47bacf6 Mon Sep 17 00:00:00 2001 From: Gamso Date: Sat, 3 Oct 2026 19:10:14 +0200 Subject: [PATCH 09/25] fix(card): format the next-mode time with the HA profile settings toLocaleTimeString([], ...) used the browser locale and ignored hass.locale.time_format, so a user who chose 24 h in their Home Assistant profile could still see "02:05 PM" (and the date used the browser language). Time and date now follow hass.locale: forced 12/24 h, "system" = browser locale, default = HA language convention. Audit: A-4 Co-Authored-By: Claude Opus 5.5 --- src/components/homeshift-card.ts | 8 +++----- src/time.ts | 32 ++++++++++++++++++++++++++++++++ test/time.test.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 src/time.ts diff --git a/src/components/homeshift-card.ts b/src/components/homeshift-card.ts index 5e1f740..4a27ee1 100644 --- a/src/components/homeshift-card.ts +++ b/src/components/homeshift-card.ts @@ -5,6 +5,7 @@ import "./circular-slider"; import "./homeshift-card-editor"; import { ENTITY_FIELDS, HomeShiftCardConfig } from "../types"; import { formatMinutes, isUsable, numericState } from "../state"; +import { formatShortDate, formatTime } from "../time"; class HomeShiftCard extends LitElement { // Predefined override duration values in minutes (0 = disabled). @@ -216,17 +217,14 @@ class HomeShiftCard extends LitElement { const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); - const timeStr = dt.toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - }); + const timeStr = formatTime(dt, this.hass?.locale); if (dt.toDateString() === now.toDateString()) { return `${localize(this.hass, "card.today")} ${timeStr}`; } if (dt.toDateString() === tomorrow.toDateString()) { return `${localize(this.hass, "card.tomorrow")} ${timeStr}`; } - return `${dt.toLocaleDateString([], { month: "short", day: "numeric" })} ${timeStr}`; + return `${formatShortDate(dt, this.hass?.locale)} ${timeStr}`; } private _renderMain( diff --git a/src/time.ts b/src/time.ts new file mode 100644 index 0000000..7d42108 --- /dev/null +++ b/src/time.ts @@ -0,0 +1,32 @@ +/** + * Locale and 12/24 h choice for Intl, following the user's Home Assistant + * profile (hass.locale) rather than the browser: + * - time_format "12" / "24" forces the hour cycle, + * - "system" uses the browser locale, + * - "language" (default) uses the HA language's own convention. + */ +export function timeFormat(hassLocale: any): { + locale: string | undefined; + hour12: boolean | undefined; +} { + const format = hassLocale?.time_format; + const locale = format === "system" ? undefined : hassLocale?.language; + const hour12 = format === "12" ? true : format === "24" ? false : undefined; + return { locale, hour12 }; +} + +/** "14:05" / "2:05 PM" according to the HA profile. */ +export function formatTime(date: Date, hassLocale: any): string { + const { locale, hour12 } = timeFormat(hassLocale); + return date.toLocaleTimeString(locale, { + hour: "2-digit", + minute: "2-digit", + hour12, + }); +} + +/** "12 Jan" / "Jan 12" according to the HA language. */ +export function formatShortDate(date: Date, hassLocale: any): string { + const { locale } = timeFormat(hassLocale); + return date.toLocaleDateString(locale, { month: "short", day: "numeric" }); +} diff --git a/test/time.test.ts b/test/time.test.ts index dd1cf2e..13cc638 100644 --- a/test/time.test.ts +++ b/test/time.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { formatTime, timeFormat } from "../src/time"; import { $, entity, homeshiftStates, makeHass, renderCard } from "./helpers"; afterEach(() => { @@ -27,3 +28,29 @@ describe("next mode time", () => { expect(vi.getTimerCount()).toBe(0); }); }); + +describe("time format (A-4)", () => { + const afternoon = new Date(2026, 0, 10, 14, 5); + + it("follows a forced 24 h profile", () => { + expect(formatTime(afternoon, { language: "en", time_format: "24" })).toBe( + "14:05", + ); + }); + + it("follows a forced 12 h profile", () => { + expect( + formatTime(afternoon, { language: "fr", time_format: "12" }), + ).toMatch(/02:05\s?PM/i); + }); + + it("uses the HA language, not the browser, by default", () => { + expect(timeFormat({ language: "fr", time_format: "language" })).toEqual({ + locale: "fr", + hour12: undefined, + }); + expect(timeFormat({ language: "fr", time_format: "system" }).locale).toBe( + undefined, + ); + }); +}); From 54d47c52ecc58ef2a0ad1ab09cf98a284a2585df Mon Sep 17 00:00:00 2001 From: Gamso Date: Sat, 3 Oct 2026 19:12:36 +0200 Subject: [PATCH 10/25] fix(a11y): keyboard-operable dial, tappable badges, labelled controls - Dial: the SVG had no role, no tab stop and no keyboard handling. Each mode is now a role="radio" group (accessible name = localized label, aria-checked = active) inside a labelled radiogroup, with a roving tabindex; arrows/Home/End move the focus, Enter/Space switch the mode. Arrows deliberately do not switch, so browsing the modes does not fire a thermostat change per key press. - Badges: the covers-left-open list only lived in a title tooltip (cursor: help), unreachable on touch screens, and the heat protection tooltip was dead because of pointer-events: none (B-6). Both badges are now buttons with an aria-label that toggle the detail text inline. - The three dropdowns get localized aria-labels. The clickable cover-time rows are turned into buttons by the covers feature commit that follows. Audit: A-2, B-6 Co-Authored-By: Claude Opus 5.5 --- src/components/circular-slider.ts | 144 ++++++++++++++++++++---------- src/components/homeshift-card.ts | 94 ++++++++++++++----- src/localize/en.json | 6 +- src/localize/fr.json | 6 +- test/a11y.test.ts | 91 +++++++++++++++++++ 5 files changed, 270 insertions(+), 71 deletions(-) create mode 100644 test/a11y.test.ts diff --git a/src/components/circular-slider.ts b/src/components/circular-slider.ts index 01eeb10..9fb7e29 100644 --- a/src/components/circular-slider.ts +++ b/src/components/circular-slider.ts @@ -28,6 +28,8 @@ export class HomeShiftCircularSlider extends LitElement { @property({ type: Array }) public options: string[] = []; /** Optional display labels shown on the arc. Falls back to options[i] when not provided. */ @property({ type: Array }) public labels: string[] = []; + /** Accessible name of the whole selector (e.g. "Thermostat mode"). */ + @property() public label = ""; @property({ type: Number }) private selectedIndex = -1; /* ======================= @@ -58,6 +60,53 @@ export class HomeShiftCircularSlider extends LitElement { return [`${length} 10`, `-${start}`]; } + private _select(index: number) { + this.dispatchEvent( + new CustomEvent("option-selected", { + detail: { option: this.options[index] }, + bubbles: true, + composed: true, + }), + ); + } + + /** + * Keyboard support: arrows move the focus along the arc (wrapping), + * Enter / Space activate the focused mode. Arrows do not activate, so + * browsing the modes does not switch the thermostat on each key press. + */ + private _onKeyDown(e: KeyboardEvent, index: number) { + const n = this.options.length; + let target: number | undefined; + switch (e.key) { + case "ArrowRight": + case "ArrowUp": + target = (index + 1) % n; + break; + case "ArrowLeft": + case "ArrowDown": + target = (index - 1 + n) % n; + break; + case "Home": + target = 0; + break; + case "End": + target = n - 1; + break; + case "Enter": + case " ": + e.preventDefault(); + this._select(index); + return; + default: + return; + } + e.preventDefault(); + this.renderRoot + .querySelector(`.segment[data-index="${target}"]`) + ?.focus(); + } + /* ======================= RENDER ======================= */ @@ -83,7 +132,7 @@ export class HomeShiftCircularSlider extends LitElement { return html`
- + @@ -150,31 +199,6 @@ export class HomeShiftCircularSlider extends LitElement { })()} ${this.options.map((_mode, i) => { const [dasharray, dashoffset] = this._strokeDashArc(i, i + 1); - - return svg` - { - e.stopPropagation(); - this.dispatchEvent( - new CustomEvent("option-selected", { - detail: { option: this.options[i] }, - bubbles: true, - composed: true, - }), - ); - }} - /> - `; - })} - ${this.options.map((_mode, i) => { // Compute the center position of the segment const segmentStart = this._valueToPercentage(i); const segmentEnd = this._valueToPercentage(i + 1); @@ -182,30 +206,47 @@ export class HomeShiftCircularSlider extends LitElement { const startOffset = segmentCenter * 100; // Prefer the translated label when provided, fall back to the raw option value const displayLabel = this.labels[i] ?? this.options[i]; + // Roving tabindex: one tab stop, on the active mode (or the first). + const tabStop = + this.selectedIndex === -1 ? i === 0 : i === this.selectedIndex; return svg` - { e.stopPropagation(); - this.dispatchEvent( - new CustomEvent("option-selected", { - detail: { option: this.options[i] }, - bubbles: true, - composed: true, - }), - ); + this._select(i); }} + @keydown=${(e: KeyboardEvent) => this._onKeyDown(e, i)} > - - ${displayLabel} - - + + + `; })} @@ -229,7 +270,20 @@ export class HomeShiftCircularSlider extends LitElement { max-width: 240px; aspect-ratio: 1; } + + .segment { + cursor: pointer; + user-select: none; + outline: none; + } + + .segment:focus-visible .hit { + stroke: var(--primary-text-color, #000); + stroke-opacity: 0.25; + } `; } -customElements.define("homeshift-circular-slider", HomeShiftCircularSlider); +if (!customElements.get("homeshift-circular-slider")) { + customElements.define("homeshift-circular-slider", HomeShiftCircularSlider); +} diff --git a/src/components/homeshift-card.ts b/src/components/homeshift-card.ts index 4a27ee1..f929947 100644 --- a/src/components/homeshift-card.ts +++ b/src/components/homeshift-card.ts @@ -41,6 +41,8 @@ class HomeShiftCard extends LitElement { /** Set by Home Assistant in the card picker and the editor preview. */ @property({ type: Boolean }) public preview = false; @state() private _config!: HomeShiftCardConfig; + /** Badge whose detail text is expanded (tap / keyboard), if any. */ + @state() private _openBadge?: "heat" | "covers"; /** Bumped at midnight so the "Today" / "Tomorrow" prefixes roll over. */ @state() private _day = 0; private _midnightTimer?: ReturnType; @@ -113,7 +115,7 @@ class HomeShiftCard extends LitElement { protected shouldUpdate(changedProps: Map): boolean { if (changedProps.has("_config") || changedProps.has("preview")) return true; - if (changedProps.has("_day")) return true; + if (changedProps.has("_day") || changedProps.has("_openBadge")) return true; if (changedProps.has("hass")) { const oldHass = changedProps.get("hass") as any; if (!oldHass) return true; @@ -185,6 +187,7 @@ class HomeShiftCard extends LitElement { entityId: string | undefined, presets: { label: string; value: number }[], modifierClass: string, + label: string, ) { if (!entityId) return nothing; const current = numericState(this.getEntityState(entityId)); @@ -196,6 +199,7 @@ class HomeShiftCard extends LitElement { : presets; const active = current !== undefined && current !== 0; return html`
${text}
` : nothing}`; + } + private _renderMain( thermo: any, day: any, @@ -333,31 +362,27 @@ class HomeShiftCard extends LitElement { ${heatProtectionActive || coversLeftOpen.length > 0 ? html`
${heatProtectionActive - ? html`
- -
` + ? this._renderBadge( + "heat", + "heat-protection-badge", + "mdi:window-shutter", + localize(this.hass, "card.heat_protection_active"), + ) : nothing} ${coversLeftOpen.length > 0 - ? html`
- -
` + ? this._renderBadge( + "covers", + "covers-left-open-badge", + "mdi:window-shutter-alert", + localize(this.hass, "card.covers_left_open", { + covers: coversLeftOpen.join(", "), + }), + ) : nothing}
` : nothing} @@ -391,6 +417,7 @@ class HomeShiftCard extends LitElement {
` with a localized `aria-label`; no hover-only information. +- **Styling:** add styles under `static styles = css\`...\``; prefer HA CSS variables (`--primary-color`, `--divider-color`) and honour `prefers-reduced-motion`. ## Localization Conventions -- **Keys:** Use dot paths like `card.mode_jour`, `card.entity_not_found`. -- **Params:** Pass values via `localize(this.hass, "card.entity_not_found", { entity: id })`. -- **Language fallback:** `localize()` falls back to English; add keys to both `en.json` and `fr.json`. - -## Project Conventions - -- **TypeScript:** Strict settings (see [tsconfig.json](tsconfig.json)); target ES2020; modules are ESM. -- **Dependencies:** `lit` for UI; `custom-card-helpers` is available if needed, but not required by current code. -- **Structure:** Keep source under `src/`, output to `dist/`; do not move files arbitrarily. +- **Keys:** dot paths like `card.entity_not_found`, `editor.day_mode_entity`, `thermostat.heating`. +- **Params:** `localize(this.hass, "card.entity_not_found", { entity: id })`. +- **Both languages:** add every key to `en.json` and `fr.json`; a test enforces identical key sets. ## Safe Changes for Agents -- **Minimal diffs:** Change only what’s needed; preserve public API (`HomeShiftCard`, tag name, config keys). -- **Feature additions:** - - Add new localized labels by extending JSON dictionaries and using `localize()` in templates. - - Extend configuration by updating `HomeShiftCardConfig`, `getStubConfig()`, and `setConfig()` consistently. - - New interactions should call HA services through `hass.callService` and read entity data via `this.hass.states`. -- **Build output:** Ensure changes still produce `dist/homershift-card.js` via Rollup. Do not rename the output file. - -## Practical Examples - -- **Add a localized label:** - - Update [src/localize/en.json](src/localize/en.json) and [src/localize/fr.json](src/localize/fr.json) with `card.new_label`. - - Use `localize(this.hass, "card.new_label")` in [src/components/homeshift-card.ts](src/components/homeshift-card.ts). -- **Add a new config key:** - - Extend the `HomeShiftCardConfig` interface and defaults, then reference it in `render()`. - -## Debug Notes - -- If the card doesn’t appear, confirm HA resource points to `/local/workspaces/homeshift/homeshift-card.js` and that `npm run build` generated `dist/homeshift-card.js`. -- Use `npm run watch` during dev and reload Lovelace to see updates. +- **Minimal diffs:** change only what's needed; preserve the public API (`HomeShiftCard`, tag names, config keys). +- **Tests:** add a Vitest test in `test/` for every bug fix or new option. +- **Build output:** do not rename `dist/homeshift-card.js` (HACS `filename`). diff --git a/README.md b/README.md index 49603b3..2d1489d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ HomeShift Card gives you full control over your HomeShift-managed thermostat and ### Circular thermostat selector -The main element of the card is a 270° arc split into color-coded sections — one per thermostat mode. Tap a section to activate it instantly. +The main element of the card is a ~250° arc, open at the bottom, split into color-coded sections — one per thermostat mode. Tap a section to activate it instantly. With a keyboard, Tab reaches the active mode, the arrow keys move between modes and Enter or Space activates the focused one. | Section | Color | Mode | | ----------- | ----- | -------------- | @@ -47,7 +47,11 @@ If the legacy `cover_entity` option is set, it takes priority over the buttons: ### Next mode display -The center of the arc shows the name of the next scheduled mode and its scheduled time. The time refreshes automatically every 30 seconds and displays as a relative duration (e.g. "in 1h30") or an absolute time (Today / Tomorrow + HH:MM). +The center of the arc shows the name of the next scheduled mode and its scheduled time, as Today / Tomorrow + time (or the date when further away), in the 12 h / 24 h format of your Home Assistant profile. Nothing is shown while no switch is scheduled (sensors `unknown` or `unavailable`). + +### Status badges + +Beside the dial, a badge appears while the heat protection keeps the covers down, and a blinking badge when tonight's close had to leave covers open (window open). Tap a badge (or focus it and press Enter) to read the detail, including the names of the covers left open. --- @@ -65,11 +69,11 @@ This card requires the [HomeShift integration](https://github.com/Gamso/homeshif ### Manual -1. Download `homershift-card.js` from the [latest release](https://github.com/Gamso/homeshift_card/releases/latest) -2. Copy it to `config/www/homeshift_card/homershift-card.js` +1. Download `homeshift-card.js` from the [latest release](https://github.com/Gamso/homeshift_card/releases/latest) +2. Copy it to `config/www/homeshift_card/homeshift-card.js` 3. Add the resource in **Settings → Dashboards → Resources**: ``` - /local/homeshift_card/homershift-card.js + /local/homeshift_card/homeshift-card.js ``` --- @@ -88,7 +92,7 @@ This card requires the [HomeShift integration](https://github.com/Gamso/homeshif ## Configuration ```yaml -type: custom:homershift-card +type: custom:homeshift-card name: Thermostat show_title: true @@ -98,23 +102,49 @@ override_duration_entity: number.homeshift_override_duration early_switch_entity: number.homeshift_early_switch next_mode_entity: sensor.homeshift_next_mode next_mode_at_entity: sensor.homeshift_next_mode_at +heat_protection_entity: binary_sensor.homeshift_cover_heat_active +cover_open_time_entity: sensor.homeshift_cover_open_time +cover_close_time_entity: sensor.homeshift_cover_close_time +open_covers_entity: button.homeshift_open_covers +close_covers_entity: button.homeshift_close_covers +covers_left_open_entity: binary_sensor.homeshift_covers_left_open ``` +Every entity option defaults to the entity the HomeShift integration creates, so a plain `type: custom:homeshift-card` works with a default installation. Set an option to an empty string to hide the corresponding element. If the day mode or thermostat mode entity does not exist, the card shows an "Entity not found" message instead of the controls. + ### Options -| Option | Type | Default | Description | -| -------------------------- | ------- | ------------------------------------ | ------------------------------- | -| `name` | string | `"Thermostat"` | Card title | -| `show_title` | boolean | `true` | Show or hide the title | -| `day_mode_entity` | string | `select.homeshift_day_mode` | Day mode select entity | -| `thermostat_mode_entity` | string | `select.homeshift_thermostat_mode` | Thermostat mode select entity | -| `override_duration_entity` | string | `number.homeshift_override_duration` | Override duration number entity | -| `early_switch_entity` | string | `number.homeshift_early_switch` | Early switch number entity | -| `next_mode_entity` | string | `sensor.homeshift_next_mode` | Next scheduled mode sensor | -| `next_mode_at_entity` | string | `sensor.homeshift_next_mode_at` | Next scheduled time sensor | -| `open_covers_entity` | string | `button.homeshift_open_covers` | Button pressed by the "open now" row | -| `close_covers_entity` | string | `button.homeshift_close_covers` | Button pressed by the "close now" row | -| `cover_entity` | string | _(none)_ | Legacy: cover opened/closed by the rows; overrides the two buttons when set | +| Option | Type | Default | Description | +| -------------------------- | ------- | ------------------------------------------ | ---------------------------------------------------------------------------- | +| `name` | string | `"Thermostat"` | Card title | +| `show_title` | boolean | `true` | Show or hide the title | +| `day_mode_entity` | string | `select.homeshift_day_mode` | Day mode select entity | +| `thermostat_mode_entity` | string | `select.homeshift_thermostat_mode` | Thermostat mode select entity | +| `override_duration_entity` | string | `number.homeshift_override_duration` | Override duration number entity | +| `early_switch_entity` | string | `number.homeshift_early_switch` | Early switch number entity | +| `next_mode_entity` | string | `sensor.homeshift_next_mode` | Next scheduled mode sensor | +| `next_mode_at_entity` | string | `sensor.homeshift_next_mode_at` | Next scheduled time sensor | +| `heat_protection_entity` | string | `binary_sensor.homeshift_cover_heat_active` | Heat protection badge | +| `cover_open_time_entity` | string | `sensor.homeshift_cover_open_time` | Scheduled cover opening time | +| `cover_close_time_entity` | string | `sensor.homeshift_cover_close_time` | Scheduled cover closing time | +| `open_covers_entity` | string | `button.homeshift_open_covers` | Button pressed by the "open now" row | +| `close_covers_entity` | string | `button.homeshift_close_covers` | Button pressed by the "close now" row | +| `cover_entity` | string | _(none)_ | Legacy: cover opened/closed by the rows; overrides the two buttons when set | +| `covers_left_open_entity` | string | `binary_sensor.homeshift_covers_left_open` | "Covers left open" badge | + +All options can also be set from the visual editor. + +## Development + +```bash +npm ci +npm run build # dist/homeshift-card.js (committed, served by HACS) +npm run typecheck +npm run lint +npm test +``` + +CI runs the same commands, checks that the committed `dist/` matches a fresh build, and validates the repository with the HACS action. --- diff --git a/src/components/circular-slider.ts b/src/components/circular-slider.ts index 9fb7e29..9f710a1 100644 --- a/src/components/circular-slider.ts +++ b/src/components/circular-slider.ts @@ -8,7 +8,7 @@ import { property } from "lit/decorators.js"; const STROKE_WIDTH = 25; const CLICK_AREA_PADDING = 10; -// 180° arc (opening at the bottom) +// ~250° arc of radius 85, open at the bottom (chord from x=30 to x=170 at y=150) const ARC_PATH = "M 30 150 A 85 85 0 1 1 170 150"; /* ======================= From 0998845746f656b522e8b697a3a62c7f75cfb931 Mon Sep 17 00:00:00 2001 From: Gamso Date: Sat, 3 Oct 2026 19:18:16 +0200 Subject: [PATCH 14/25] ci: add a validate workflow (build, typecheck, lint, tests, dist, HACS) Nothing guarded the repository: the editor key typo and the dead localize fallbacks would have been caught by a trivial test. On every push to main and every pull request the workflow now runs npm ci, typecheck, lint, the Vitest suite and the build, fails if the committed dist/ differs from the fresh build, and runs hacs/action for a plugin. Audit: T-1 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/validate.yml | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/validate.yml diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..3ea2f70 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,40 @@ +name: Validate + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build, lint and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 24 + cache: npm + - run: npm ci + - run: npm run typecheck + - run: npm run lint + - run: npm test + - run: npm run build + - name: Committed dist/ matches the sources + run: | + if ! git diff --exit-code --stat -- dist/; then + echo "::error::dist/ is out of date: run 'npm run build' and commit the result." + exit 1 + fi + + hacs: + name: HACS validation + runs-on: ubuntu-latest + steps: + - uses: hacs/action@main + with: + category: plugin From c2a1dd0e2189d3d1a83f121832914db71ff96e69 Mon Sep 17 00:00:00 2001 From: Gamso Date: Sat, 3 Oct 2026 19:18:35 +0200 Subject: [PATCH 15/25] chore(dist): rebuild bundle Rebuilt from the sources of this branch (no sourcemap reference any more), so the CI "dist matches the sources" check passes. Co-Authored-By: Claude Opus 5.5 --- dist/homeshift-card.js | 415 +++++++++++++++++++---------------------- 1 file changed, 196 insertions(+), 219 deletions(-) diff --git a/dist/homeshift-card.js b/dist/homeshift-card.js index 4a9b37c..1889649 100644 --- a/dist/homeshift-card.js +++ b/dist/homeshift-card.js @@ -1,51 +1,46 @@ -function t(t,e,i,o){var s,n=arguments.length,r=n<3?e:null===o?o=Object.getOwnPropertyDescriptor(e,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,i,o);else for(var a=t.length-1;a>=0;a--)(s=t[a])&&(r=(n<3?s(r):n>3?s(e,i,r):s(e,i))||r);return n>3&&r&&Object.defineProperty(e,i,r),r}"function"==typeof SuppressedError&&SuppressedError; +function t(t,e,i,o){var s,r=arguments.length,n=r<3?e:null===o?o=Object.getOwnPropertyDescriptor(e,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,i,o);else for(var a=t.length-1;a>=0;a--)(s=t[a])&&(n=(r<3?s(n):r>3?s(e,i,n):s(e,i))||n);return r>3&&n&&Object.defineProperty(e,i,n),n}"function"==typeof SuppressedError&&SuppressedError; /** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const e=globalThis,i=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,o=Symbol(),s=new WeakMap;let n=class{constructor(t,e,i){if(this._$cssResult$=!0,i!==o)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const e=this.t;if(i&&void 0===t){const i=void 0!==e&&1===e.length;i&&(t=s.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),i&&s.set(e,t))}return t}toString(){return this.cssText}};const r=(t,...e)=>{const i=1===t.length?t[0]:e.reduce((e,i,o)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[o+1],t[0]);return new n(i,t,o)},a=i?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new n("string"==typeof t?t:t+"",void 0,o))(e)})(t):t,{is:c,defineProperty:h,getOwnPropertyDescriptor:l,getOwnPropertyNames:d,getOwnPropertySymbols:p,getPrototypeOf:_}=Object,u=globalThis,f=u.trustedTypes,m=f?f.emptyScript:"",v=u.reactiveElementPolyfillSupport,g=(t,e)=>t,y={toAttribute(t,e){switch(e){case Boolean:t=t?m:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},$=(t,e)=>!c(t,e),b={attribute:!0,type:String,converter:y,reflect:!1,useDefault:!1,hasChanged:$}; +const e=globalThis,i=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,o=Symbol(),s=new WeakMap;let r=class{constructor(t,e,i){if(this._$cssResult$=!0,i!==o)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const e=this.t;if(i&&void 0===t){const i=void 0!==e&&1===e.length;i&&(t=s.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),i&&s.set(e,t))}return t}toString(){return this.cssText}};const n=(t,...e)=>{const i=1===t.length?t[0]:e.reduce((e,i,o)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[o+1],t[0]);return new r(i,t,o)},a=i?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new r("string"==typeof t?t:t+"",void 0,o))(e)})(t):t,{is:c,defineProperty:l,getOwnPropertyDescriptor:h,getOwnPropertyNames:d,getOwnPropertySymbols:p,getPrototypeOf:_}=Object,u=globalThis,m=u.trustedTypes,f=m?m.emptyScript:"",v=u.reactiveElementPolyfillSupport,g=(t,e)=>t,y={toAttribute(t,e){switch(e){case Boolean:t=t?f:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},$=(t,e)=>!c(t,e),b={attribute:!0,type:String,converter:y,reflect:!1,useDefault:!1,hasChanged:$}; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */Symbol.metadata??=Symbol("metadata"),u.litPropertyMetadata??=new WeakMap;let x=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=b){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const i=Symbol(),o=this.getPropertyDescriptor(t,i,e);void 0!==o&&h(this.prototype,t,o)}}static getPropertyDescriptor(t,e,i){const{get:o,set:s}=l(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:o,set(e){const n=o?.call(this);s?.call(this,e),this.requestUpdate(t,n,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(g("elementProperties")))return;const t=_(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(g("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(g("properties"))){const t=this.properties,e=[...d(t),...p(t)];for(const i of e)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,e]of this.elementProperties){const i=this._$Eu(t,e);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(a(t))}else void 0!==t&&e.push(a(t));return e}static _$Eu(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.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(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const i of e.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((t,o)=>{if(i)t.adoptedStyleSheets=o.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const i of o){const o=document.createElement("style"),s=e.litNonce;void 0!==s&&o.setAttribute("nonce",s),o.textContent=i.cssText,t.appendChild(o)}})(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ET(t,e){const i=this.constructor.elementProperties.get(t),o=this.constructor._$Eu(t,i);if(void 0!==o&&!0===i.reflect){const s=(void 0!==i.converter?.toAttribute?i.converter:y).toAttribute(e,i.type);this._$Em=t,null==s?this.removeAttribute(o):this.setAttribute(o,s),this._$Em=null}}_$AK(t,e){const i=this.constructor,o=i._$Eh.get(t);if(void 0!==o&&this._$Em!==o){const t=i.getPropertyOptions(o),s="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:y;this._$Em=o;const n=s.fromAttribute(e,t.type);this[o]=n??this._$Ej?.get(o)??n,this._$Em=null}}requestUpdate(t,e,i,o=!1,s){if(void 0!==t){const n=this.constructor;if(!1===o&&(s=this[t]),i??=n.getPropertyOptions(t),!((i.hasChanged??$)(s,e)||i.useDefault&&i.reflect&&s===this._$Ej?.get(t)&&!this.hasAttribute(n._$Eu(t,i))))return;this.C(t,e,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:i,reflect:o,wrapped:s},n){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,n??e??this[t]),!0!==s||void 0!==n)||(this._$AL.has(t)||(this.hasUpdated||i||(e=void 0),this._$AL.set(t,e)),!0===o&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,i]of t){const{wrapped:t}=i,o=this[e];!0!==t||this._$AL.has(e)||void 0===o||this.C(e,void 0,i,o)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}};x.elementStyles=[],x.shadowRootOptions={mode:"open"},x[g("elementProperties")]=new Map,x[g("finalized")]=new Map,v?.({ReactiveElement:x}),(u.reactiveElementVersions??=[]).push("2.1.2"); + */Symbol.metadata??=Symbol("metadata"),u.litPropertyMetadata??=new WeakMap;let x=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=b){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const i=Symbol(),o=this.getPropertyDescriptor(t,i,e);void 0!==o&&l(this.prototype,t,o)}}static getPropertyDescriptor(t,e,i){const{get:o,set:s}=h(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:o,set(e){const r=o?.call(this);s?.call(this,e),this.requestUpdate(t,r,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(g("elementProperties")))return;const t=_(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(g("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(g("properties"))){const t=this.properties,e=[...d(t),...p(t)];for(const i of e)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,e]of this.elementProperties){const i=this._$Eu(t,e);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(a(t))}else void 0!==t&&e.push(a(t));return e}static _$Eu(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.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(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const i of e.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((t,o)=>{if(i)t.adoptedStyleSheets=o.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const i of o){const o=document.createElement("style"),s=e.litNonce;void 0!==s&&o.setAttribute("nonce",s),o.textContent=i.cssText,t.appendChild(o)}})(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ET(t,e){const i=this.constructor.elementProperties.get(t),o=this.constructor._$Eu(t,i);if(void 0!==o&&!0===i.reflect){const s=(void 0!==i.converter?.toAttribute?i.converter:y).toAttribute(e,i.type);this._$Em=t,null==s?this.removeAttribute(o):this.setAttribute(o,s),this._$Em=null}}_$AK(t,e){const i=this.constructor,o=i._$Eh.get(t);if(void 0!==o&&this._$Em!==o){const t=i.getPropertyOptions(o),s="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:y;this._$Em=o;const r=s.fromAttribute(e,t.type);this[o]=r??this._$Ej?.get(o)??r,this._$Em=null}}requestUpdate(t,e,i,o=!1,s){if(void 0!==t){const r=this.constructor;if(!1===o&&(s=this[t]),i??=r.getPropertyOptions(t),!((i.hasChanged??$)(s,e)||i.useDefault&&i.reflect&&s===this._$Ej?.get(t)&&!this.hasAttribute(r._$Eu(t,i))))return;this.C(t,e,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:i,reflect:o,wrapped:s},r){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,r??e??this[t]),!0!==s||void 0!==r)||(this._$AL.has(t)||(this.hasUpdated||i||(e=void 0),this._$AL.set(t,e)),!0===o&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,i]of t){const{wrapped:t}=i,o=this[e];!0!==t||this._$AL.has(e)||void 0===o||this.C(e,void 0,i,o)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}};x.elementStyles=[],x.shadowRootOptions={mode:"open"},x[g("elementProperties")]=new Map,x[g("finalized")]=new Map,v?.({ReactiveElement:x}),(u.reactiveElementVersions??=[]).push("2.1.2"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const w=globalThis,E=t=>t,A=w.trustedTypes,S=A?A.createPolicy("lit-html",{createHTML:t=>t}):void 0,C="$lit$",k=`lit$${Math.random().toFixed(9).slice(2)}$`,P="?"+k,T=`<${P}>`,O=document,M=()=>O.createComment(""),R=t=>null===t||"object"!=typeof t&&"function"!=typeof t,N=Array.isArray,U="[ \t\n\f\r]",H=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,D=/-->/g,I=/>/g,j=RegExp(`>|${U}(?:([^\\s"'>=/]+)(${U}*=${U}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),L=/'/g,z=/"/g,V=/^(?:script|style|textarea|title)$/i,B=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),W=B(1),q=B(2),F=Symbol.for("lit-noChange"),Y=Symbol.for("lit-nothing"),J=new WeakMap,K=O.createTreeWalker(O,129);function X(t,e){if(!N(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==S?S.createHTML(e):e}const Z=(t,e)=>{const i=t.length-1,o=[];let s,n=2===e?"":3===e?"":"",r=H;for(let e=0;e"===c[0]?(r=s??H,h=-1):void 0===c[1]?h=-2:(h=r.lastIndex-c[2].length,a=c[1],r=void 0===c[3]?j:'"'===c[3]?z:L):r===z||r===L?r=j:r===D||r===I?r=H:(r=j,s=void 0);const d=r===j&&t[e+1].startsWith("/>")?" ":"";n+=r===H?i+T:h>=0?(o.push(a),i.slice(0,h)+C+i.slice(h)+k+d):i+k+(-2===h?e:d)}return[X(t,n+(t[i]||"")+(2===e?"":3===e?"":"")),o]};class G{constructor({strings:t,_$litType$:e},i){let o;this.parts=[];let s=0,n=0;const r=t.length-1,a=this.parts,[c,h]=Z(t,e);if(this.el=G.createElement(c,i),K.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(o=K.nextNode())&&a.length0){o.textContent=A?A.emptyScript:"";for(let i=0;iN(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==Y&&R(this._$AH)?this._$AA.nextSibling.data=t:this.T(O.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:i}=t,o="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=G.createElement(X(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===o)this._$AH.p(e);else{const t=new tt(o,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=J.get(t.strings);return void 0===e&&J.set(t.strings,e=new G(t)),e}k(t){N(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,o=0;for(const s of t)o===e.length?e.push(i=new et(this.O(M()),this.O(M()),this,this.options)):i=e[o],i._$AI(s),o++;o2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=Y}_$AI(t,e=this,i,o){const s=this.strings;let n=!1;if(void 0===s)t=Q(this,t,e,0),n=!R(t)||t!==this._$AH&&t!==F,n&&(this._$AH=t);else{const o=t;let r,a;for(t=s[0],r=0;rt,A=w.trustedTypes,S=A?A.createPolicy("lit-html",{createHTML:t=>t}):void 0,C="$lit$",k=`lit$${Math.random().toFixed(9).slice(2)}$`,P="?"+k,T=`<${P}>`,O=document,M=()=>O.createComment(""),R=t=>null===t||"object"!=typeof t&&"function"!=typeof t,D=Array.isArray,U="[ \t\n\f\r]",H=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,N=/-->/g,I=/>/g,j=RegExp(`>|${U}(?:([^\\s"'>=/]+)(${U}*=${U}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),L=/'/g,B=/"/g,z=/^(?:script|style|textarea|title)$/i,V=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),W=V(1),q=V(2),F=Symbol.for("lit-noChange"),K=Symbol.for("lit-nothing"),Y=new WeakMap,J=O.createTreeWalker(O,129);function Z(t,e){if(!D(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==S?S.createHTML(e):e}const X=(t,e)=>{const i=t.length-1,o=[];let s,r=2===e?"":3===e?"":"",n=H;for(let e=0;e"===c[0]?(n=s??H,l=-1):void 0===c[1]?l=-2:(l=n.lastIndex-c[2].length,a=c[1],n=void 0===c[3]?j:'"'===c[3]?B:L):n===B||n===L?n=j:n===N||n===I?n=H:(n=j,s=void 0);const d=n===j&&t[e+1].startsWith("/>")?" ":"";r+=n===H?i+T:l>=0?(o.push(a),i.slice(0,l)+C+i.slice(l)+k+d):i+k+(-2===l?e:d)}return[Z(t,r+(t[i]||"")+(2===e?"":3===e?"":"")),o]};class G{constructor({strings:t,_$litType$:e},i){let o;this.parts=[];let s=0,r=0;const n=t.length-1,a=this.parts,[c,l]=X(t,e);if(this.el=G.createElement(c,i),J.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(o=J.nextNode())&&a.length0){o.textContent=A?A.emptyScript:"";for(let i=0;iD(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==K&&R(this._$AH)?this._$AA.nextSibling.data=t:this.T(O.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:i}=t,o="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=G.createElement(Z(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===o)this._$AH.p(e);else{const t=new tt(o,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=Y.get(t.strings);return void 0===e&&Y.set(t.strings,e=new G(t)),e}k(t){D(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,o=0;for(const s of t)o===e.length?e.push(i=new et(this.O(M()),this.O(M()),this,this.options)):i=e[o],i._$AI(s),o++;o2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=K}_$AI(t,e=this,i,o){const s=this.strings;let r=!1;if(void 0===s)t=Q(this,t,e,0),r=!R(t)||t!==this._$AH&&t!==F,r&&(this._$AH=t);else{const o=t;let n,a;for(t=s[0],n=0;n{const o=i?.renderBefore??e;let s=o._$litPart$;if(void 0===s){const t=i?.renderBefore??null;o._$litPart$=s=new et(e.insertBefore(M(),t),t,void 0,i??{})}return s._$AI(t),s})(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return F}}ht._$litElement$=!0,ht.finalized=!0,ct.litElementHydrateSupport?.({LitElement:ht});const lt=ct.litElementPolyfillSupport;lt?.({LitElement:ht}),(ct.litElementVersions??=[]).push("4.2.2"); + */class lt extends x{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=((t,e,i)=>{const o=i?.renderBefore??e;let s=o._$litPart$;if(void 0===s){const t=i?.renderBefore??null;o._$litPart$=s=new et(e.insertBefore(M(),t),t,void 0,i??{})}return s._$AI(t),s})(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return F}}lt._$litElement$=!0,lt.finalized=!0,ct.litElementHydrateSupport?.({LitElement:lt});const ht=ct.litElementPolyfillSupport;ht?.({LitElement:lt}),(ct.litElementVersions??=[]).push("4.2.2"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const dt={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:$},pt=(t=dt,e,i)=>{const{kind:o,metadata:s}=i;let n=globalThis.litPropertyMetadata.get(s);if(void 0===n&&globalThis.litPropertyMetadata.set(s,n=new Map),"setter"===o&&((t=Object.create(t)).wrapped=!0),n.set(i.name,t),"accessor"===o){const{name:o}=i;return{set(i){const s=e.get.call(this);e.set.call(this,i),this.requestUpdate(o,s,t,!0,i)},init(e){return void 0!==e&&this.C(o,void 0,t,e),e}}}if("setter"===o){const{name:o}=i;return function(i){const s=this[o];e.call(this,i),this.requestUpdate(o,s,t,!0,i)}}throw Error("Unsupported decorator location: "+o)};function _t(t){return(e,i)=>"object"==typeof i?pt(t,e,i):((t,e,i)=>{const o=e.hasOwnProperty(i);return e.constructor.createProperty(i,t),o?Object.getOwnPropertyDescriptor(e,i):void 0})(t,e,i)} +const dt={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:$},pt=(t=dt,e,i)=>{const{kind:o,metadata:s}=i;let r=globalThis.litPropertyMetadata.get(s);if(void 0===r&&globalThis.litPropertyMetadata.set(s,r=new Map),"setter"===o&&((t=Object.create(t)).wrapped=!0),r.set(i.name,t),"accessor"===o){const{name:o}=i;return{set(i){const s=e.get.call(this);e.set.call(this,i),this.requestUpdate(o,s,t,!0,i)},init(e){return void 0!==e&&this.C(o,void 0,t,e),e}}}if("setter"===o){const{name:o}=i;return function(i){const s=this[o];e.call(this,i),this.requestUpdate(o,s,t,!0,i)}}throw Error("Unsupported decorator location: "+o)};function _t(t){return(e,i)=>"object"==typeof i?pt(t,e,i):((t,e,i)=>{const o=e.hasOwnProperty(i);return e.constructor.createProperty(i,t),o?Object.getOwnPropertyDescriptor(e,i):void 0})(t,e,i)} /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */function ut(t){return _t({...t,state:!0,attribute:!1})} -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */const ft={en:{card:{title:"Thermostat",entity_not_found:"Entity not found: {entity}",off:"Off",next_scan:"Next scan",in:"in",today:"Today",tomorrow:"Tomorrow",past:"past",heat_protection_active:"Heat protection active",cover_open_time:"Cover opening time",cover_close_time:"Cover closing time",cover_open_action:"Open now",cover_close_action:"Close now",covers_left_open:"Covers left open: {covers}"},thermostat:{heating:"Heating",cooling:"Cooling",ventilation:"Ventilation"},editor:{name:"Name",day_mode_entity:"Day Mode Entity",hermostat_mode_tentity:"Thermostat Mode Entity",cover_open_time_entity:"Cover Open Time Entity",cover_close_time_entity:"Cover Close Time Entity",cover_entity:"Cover Entity (manual open/close)",covers_left_open_entity:"Covers Left Open Entity",show_title:"Show title"}},fr:{card:{title:"Thermostat",entity_not_found:"Entité introuvable: {entity}",off:"Éteint",next_scan:"Prochain scan",in:"dans",today:"Auj.",tomorrow:"Dem.",past:"passé",heat_protection_active:"Protection thermique active",cover_open_time:"Heure d'ouverture des volets",cover_close_time:"Heure de fermeture des volets",cover_open_action:"Ouvrir maintenant",cover_close_action:"Fermer maintenant",covers_left_open:"Volets non fermés : {covers}"},thermostat:{heating:"Chauffage",cooling:"Climatisation",ventilation:"Ventilation"},editor:{name:"Nom",day_mode_entity:"Entité mode jour",hermostat_mode_tentity:"Entité mode thermostat",cover_open_time_entity:"Entité heure d'ouverture des volets",cover_close_time_entity:"Entité heure de fermeture des volets",cover_entity:"Entité volet (ouverture/fermeture manuelle)",covers_left_open_entity:"Entité volets non fermés",show_title:"Afficher le titre"}}};function mt(t,e,i){const o=function(t){const e=t?.locale?.language||t?.language||"en",i=String(e).split("-")[0];return ft[e]?e:ft[i]?i:"en"}(t);let s=(n=ft[o]||ft.en,e.split(".").reduce((t,e)=>t&&null!=t[e]?t[e]:void 0,n));var n;return s?(i&&Object.entries(i).forEach(([t,e])=>s=s.replace(`{${t}}`,e)),s):e}const vt="M 30 150 A 85 85 0 1 1 170 150",gt=34.2,yt=-145.8,$t=["var(--disabled-text-color, #9e9e9e)","#e74c3c","var(--primary-color, #3b82f6)","#d4a574"];class bt extends ht{constructor(){super(...arguments),this.options=[],this.labels=[],this.selectedIndex=-1}_getColorForIndex(t){return $t[t%$t.length]??"var(--primary-color)"}_valueToPercentage(t){return t/this.options.length}_strokeDashArc(t,e){const i=this._valueToPercentage(t);return[`${this._valueToPercentage(e)-i} 10`,`-${i}`]}_getPercentageFromEvent(t){if(!this._svg)return-1;const e=this._svg.getBoundingClientRect(),i=2*(t.clientX-e.left-e.width/2)/e.width,o=2*(t.clientY-e.top-e.height/2)/e.height,s=180*Math.atan2(o,i)/Math.PI;return s>=yt&&s<=gt?(gt-s)/180:s>=214.2?(gt+(360-s))/180:-1}_onSvgClick(t){const e=this._getPercentageFromEvent(t);if(e<0||0===this.options.length)return;const i=Math.floor(e*this.options.length),o=Math.max(0,Math.min(i,this.options.length-1));this.dispatchEvent(new CustomEvent("option-selected",{detail:{option:this.options[o]},bubbles:!0,composed:!0}))}render(){if(!this.options||0===this.options.length)return W`
`;const t=this.currentValue?this.options.indexOf(this.currentValue):-1;return this.selectedIndex=-1!==t?t:-1,W` + */function ut(t){return _t({...t,state:!0,attribute:!1})}const mt={en:{card:{title:"Thermostat",entity_not_found:"Entity not found: {entity}",off:"Off",today:"Today",tomorrow:"Tomorrow",heat_protection_active:"Heat protection active",cover_open_time:"Cover opening time",cover_close_time:"Cover closing time",cover_open_action:"Open the covers now (scheduled {time})",cover_close_action:"Close the covers now (scheduled {time})",covers_left_open:"Covers left open: {covers}",thermostat_mode:"Thermostat mode",day_mode:"Day mode",override_duration:"Override duration",early_switch:"Early switch",cover_sending:"Sending…",cover_open_failed:"Could not open the covers: {error}",cover_close_failed:"Could not close the covers: {error}"},thermostat:{heating:"Heating",cooling:"Cooling",ventilation:"Ventilation"},editor:{name:"Name",day_mode_entity:"Day Mode Entity",thermostat_mode_entity:"Thermostat Mode Entity",override_duration_entity:"Override Duration Entity",early_switch_entity:"Early Switch Entity",next_mode_entity:"Next Mode Entity",next_mode_at_entity:"Next Mode Time Entity",heat_protection_entity:"Heat Protection Entity",cover_open_time_entity:"Cover Open Time Entity",cover_close_time_entity:"Cover Close Time Entity",open_covers_entity:"Open Covers Button",close_covers_entity:"Close Covers Button",cover_entity:"Cover Entity (legacy, overrides the buttons)",covers_left_open_entity:"Covers Left Open Entity",show_title:"Show title"},preview:{home:"Home",work:"Work",remote:"Remote",away:"Away"}},fr:{card:{title:"Thermostat",entity_not_found:"Entité introuvable: {entity}",off:"Éteint",today:"Auj.",tomorrow:"Dem.",heat_protection_active:"Protection thermique active",cover_open_time:"Heure d'ouverture des volets",cover_close_time:"Heure de fermeture des volets",cover_open_action:"Ouvrir les volets maintenant (prévu à {time})",cover_close_action:"Fermer les volets maintenant (prévu à {time})",covers_left_open:"Volets non fermés : {covers}",thermostat_mode:"Mode thermostat",day_mode:"Mode jour",override_duration:"Durée de dérogation",early_switch:"Anticipation",cover_sending:"Envoi…",cover_open_failed:"Impossible d'ouvrir les volets : {error}",cover_close_failed:"Impossible de fermer les volets : {error}"},thermostat:{heating:"Chauffage",cooling:"Climatisation",ventilation:"Ventilation"},editor:{name:"Nom",day_mode_entity:"Entité mode jour",thermostat_mode_entity:"Entité mode thermostat",override_duration_entity:"Entité durée de dérogation",early_switch_entity:"Entité anticipation",next_mode_entity:"Entité prochain mode",next_mode_at_entity:"Entité heure du prochain mode",heat_protection_entity:"Entité protection thermique",cover_open_time_entity:"Entité heure d'ouverture des volets",cover_close_time_entity:"Entité heure de fermeture des volets",open_covers_entity:"Bouton d'ouverture des volets",close_covers_entity:"Bouton de fermeture des volets",cover_entity:"Entité volet (ancienne option, prioritaire sur les boutons)",covers_left_open_entity:"Entité volets non fermés",show_title:"Afficher le titre"},preview:{home:"Maison",work:"Travail",remote:"Télétravail",away:"Absence"}}};function ft(t,e){return e.split(".").reduce((t,e)=>t&&null!=t[e]?t[e]:void 0,t)}function vt(t,e,i,o){const s=function(t){const e=t?.locale?.language||t?.language||"en",i=String(e).split("-")[0];return mt[e]?e:mt[i]?i:"en"}(t);let r=ft(mt[s],e)??ft(mt.en,e)??o;return null==r?e:(i&&Object.entries(i).forEach(([t,e])=>r=r.replace(`{${t}}`,e)),r)}const gt="M 30 150 A 85 85 0 1 1 170 150",yt=["var(--disabled-text-color, #9e9e9e)","#e74c3c","var(--primary-color, #3b82f6)","#d4a574"];class $t extends lt{constructor(){super(...arguments),this.options=[],this.labels=[],this.label="",this.selectedIndex=-1}_getColorForIndex(t){return yt[t%yt.length]??"var(--primary-color)"}_valueToPercentage(t){return t/this.options.length}_strokeDashArc(t,e){const i=this._valueToPercentage(t);return[`${this._valueToPercentage(e)-i} 10`,`-${i}`]}_select(t){this.dispatchEvent(new CustomEvent("option-selected",{detail:{option:this.options[t]},bubbles:!0,composed:!0}))}_onKeyDown(t,e){const i=this.options.length;let o;switch(t.key){case"ArrowRight":case"ArrowUp":o=(e+1)%i;break;case"ArrowLeft":case"ArrowDown":o=(e-1+i)%i;break;case"Home":o=0;break;case"End":o=i-1;break;case"Enter":case" ":return t.preventDefault(),void this._select(e);default:return}t.preventDefault(),this.renderRoot.querySelector(`.segment[data-index="${o}"]`)?.focus()}render(){if(!this.options||0===this.options.length)return W`
`;const t=this.currentValue?this.options.indexOf(this.currentValue):-1;return this.selectedIndex=-1!==t?t:-1,W`
- + - + ${q` `} ${-1!==this.selectedIndex?(()=>{const[t,e]=this._strokeDashArc(this.selectedIndex,this.selectedIndex+1),i=this._getColorForIndex(this.selectedIndex);return q` {const t=this.options.length;if(t<2)return null;const e=.002,i=1/t,o=[0,i-e];for(let e=0;e `})()} - ${this.options.map((t,e)=>{const[i,o]=this._strokeDashArc(e,e+1);return q` - {t.stopPropagation(),this.dispatchEvent(new CustomEvent("option-selected",{detail:{option:this.options[e]},bubbles:!0,composed:!0}))}} - /> - `})} - ${this.options.map((t,e)=>{const i=100*((this._valueToPercentage(e)+this._valueToPercentage(e+1))/2),o=this.labels[e]??this.options[e];return q` - {t.stopPropagation(),this.dispatchEvent(new CustomEvent("option-selected",{detail:{option:this.options[e]},bubbles:!0,composed:!0}))}} + ${this.options.map((t,e)=>{const[i,o]=this._strokeDashArc(e,e+1),s=100*((this._valueToPercentage(e)+this._valueToPercentage(e+1))/2),r=this.labels[e]??this.options[e],n=-1===this.selectedIndex?0===e:e===this.selectedIndex;return q` + {t.stopPropagation(),this._select(e)}} + @keydown=${t=>this._onKeyDown(t,e)} > - - ${o} - - + + + `})}
- `}}bt.styles=r` + `}}$t.styles=n` :host { display: block; } @@ -123,78 +125,42 @@ const dt={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:$},pt=(t=dt max-width: 240px; aspect-ratio: 1; } - `,t([_t({attribute:!1})],bt.prototype,"hass",void 0),t([_t()],bt.prototype,"entityId",void 0),t([_t()],bt.prototype,"currentValue",void 0),t([_t({type:Array})],bt.prototype,"options",void 0),t([_t({type:Array})],bt.prototype,"labels",void 0),t([_t({type:Number})],bt.prototype,"selectedIndex",void 0),t([ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */ -function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect.decorate&&"object"!=typeof e&&Object.defineProperty(t,e,i),i))(e,i,{get(){return(e=>e.renderRoot?.querySelector(t)??null)(this)}})}("svg")],bt.prototype,"_svg",void 0),customElements.define("homeshift-circular-slider",bt);class xt extends ht{setConfig(t){this._config=t}_onNameChanged(t){if(!this._config||!this.hass)return;const e=t.target.value;if(this._config.name===e)return;const i={...this._config,name:e};this._config=i,this._dispatchConfigChanged(i)}_onEntityChanged(t,e){if(!this._config||!this.hass)return;const i=t.detail.value;if(this._config[e]===i)return;const o={...this._config,[e]:i};this._config=o,this._dispatchConfigChanged(o)}_onShowTitleChanged(t){if(!this._config||!this.hass)return;const e=t.target.checked;if(this._config.show_title===e)return;const i={...this._config,show_title:e};this._config=i,this._dispatchConfigChanged(i)}_dispatchConfigChanged(t){const e=new CustomEvent("config-changed",{detail:{config:t},bubbles:!0,composed:!0});this.dispatchEvent(e)}render(){return this.hass&&this._config?W` + + .segment { + cursor: pointer; + user-select: none; + outline: none; + } + + .segment:focus-visible .hit { + stroke: var(--primary-text-color, #000); + stroke-opacity: 0.25; + } + `,t([_t()],$t.prototype,"currentValue",void 0),t([_t({type:Array})],$t.prototype,"options",void 0),t([_t({type:Array})],$t.prototype,"labels",void 0),t([_t()],$t.prototype,"label",void 0),t([_t({type:Number})],$t.prototype,"selectedIndex",void 0),customElements.get("homeshift-circular-slider")||customElements.define("homeshift-circular-slider",$t);const bt=[{key:"day_mode_entity",domains:["select","input_select"],default:"select.homeshift_day_mode"},{key:"thermostat_mode_entity",domains:["select","input_select"],default:"select.homeshift_thermostat_mode"},{key:"override_duration_entity",domains:["number","input_number"],default:"number.homeshift_override_duration"},{key:"early_switch_entity",domains:["number","input_number"],default:"number.homeshift_early_switch"},{key:"next_mode_entity",domains:["sensor"],default:"sensor.homeshift_next_mode"},{key:"next_mode_at_entity",domains:["sensor"],default:"sensor.homeshift_next_mode_at"},{key:"heat_protection_entity",domains:["binary_sensor"],default:"binary_sensor.homeshift_cover_heat_active"},{key:"cover_open_time_entity",domains:["sensor"],default:"sensor.homeshift_cover_open_time"},{key:"cover_close_time_entity",domains:["sensor"],default:"sensor.homeshift_cover_close_time"},{key:"open_covers_entity",domains:["button"],default:"button.homeshift_open_covers"},{key:"close_covers_entity",domains:["button"],default:"button.homeshift_close_covers"},{key:"cover_entity",domains:["cover"],default:""},{key:"covers_left_open_entity",domains:["binary_sensor"],default:"binary_sensor.homeshift_covers_left_open"}];class xt extends lt{setConfig(t){this._config=t}_onNameChanged(t){if(!this._config||!this.hass)return;const e=t.target.value;if(this._config.name===e)return;const i={...this._config,name:e};this._config=i,this._dispatchConfigChanged(i)}_onEntityChanged(t,e){if(!this._config||!this.hass)return;const i=t.detail.value;if(this._config[e]===i)return;const o={...this._config,[e]:i};this._config=o,this._dispatchConfigChanged(o)}_onShowTitleChanged(t){if(!this._config||!this.hass)return;const e=t.target.checked;if(this._config.show_title===e)return;const i={...this._config,show_title:e};this._config=i,this._dispatchConfigChanged(i)}_dispatchConfigChanged(t){const e=new CustomEvent("config-changed",{detail:{config:t},bubbles:!0,composed:!0});this.dispatchEvent(e)}render(){return this.hass&&this._config?W`
- this._onEntityChanged(t,"day_mode_entity")} - allow-custom-entity - > - - this._onEntityChanged(t,"hermostat_mode_tentity")} - allow-custom-entity - > - - this._onEntityChanged(t,"cover_open_time_entity")} - allow-custom-entity - > - - this._onEntityChanged(t,"cover_close_time_entity")} - allow-custom-entity - > - - this._onEntityChanged(t,"cover_entity")} - allow-custom-entity - > - - this._onEntityChanged(t,"covers_left_open_entity")} - allow-custom-entity - > - - + ${bt.map(t=>W`this._onEntityChanged(e,t.key)} + allow-custom-entity + >`)} + +
- `:W``}}xt.styles=r` + `:W``}}xt.styles=n` .card-config { display: flex; flex-direction: column; @@ -212,43 +178,57 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. align-items: center; padding: 8px 0; } - `,t([_t({attribute:!1})],xt.prototype,"hass",void 0),t([ut()],xt.prototype,"_config",void 0),customElements.get("homeshift-card-editor")||customElements.define("homeshift-card-editor",xt);class wt extends ht{constructor(){super(...arguments),this._tick=0}connectedCallback(){super.connectedCallback(),this._refreshInterval=setInterval(()=>{this._tick++},3e4)}disconnectedCallback(){super.disconnectedCallback(),clearInterval(this._refreshInterval)}static getStubConfig(){return{name:"Thermostat",day_mode_entity:"select.homeshift_day_mode",thermostat_mode_entity:"select.homeshift_thermostat_mode",override_duration_entity:"number.homeshift_override_duration",early_switch_entity:"number.homeshift_early_switch",next_mode_entity:"sensor.homeshift_next_mode",next_mode_at_entity:"sensor.homeshift_next_mode_at",heat_protection_entity:"binary_sensor.homeshift_cover_heat_active",cover_open_time_entity:"sensor.homeshift_cover_open_time",cover_close_time_entity:"sensor.homeshift_cover_close_time",cover_entity:"cover.homeshift_daily_covers",covers_left_open_entity:"binary_sensor.homeshift_covers_left_open",show_title:!0}}static getConfigElement(){return document.createElement("homeshift-card-editor")}setConfig(t){if(!t)throw new Error("Missing configuration");this._config={name:t.name??"Thermostat",day_mode_entity:t.day_mode_entity??"select.homeshift_day_mode",thermostat_mode_entity:t.thermostat_mode_entity??"select.homeshift_thermostat_mode",override_duration_entity:t.override_duration_entity??"number.homeshift_override_duration",early_switch_entity:t.early_switch_entity??"number.homeshift_early_switch",next_mode_entity:t.next_mode_entity??"sensor.homeshift_next_mode",next_mode_at_entity:t.next_mode_at_entity??"sensor.homeshift_next_mode_at",heat_protection_entity:t.heat_protection_entity??"binary_sensor.homeshift_cover_heat_active",cover_open_time_entity:t.cover_open_time_entity??"sensor.homeshift_cover_open_time",cover_close_time_entity:t.cover_close_time_entity??"sensor.homeshift_cover_close_time",cover_entity:t.cover_entity??"",covers_left_open_entity:t.covers_left_open_entity??"binary_sensor.homeshift_covers_left_open",show_title:!1!==t.show_title}}getCardSize(){return 4}shouldUpdate(t){if(t.has("_config"))return!0;if(t.has("_tick"))return!0;if(t.has("hass")){const e=t.get("hass");if(!e)return!0;return[this._config?.day_mode_entity,this._config?.thermostat_mode_entity,this._config?.override_duration_entity,this._config?.early_switch_entity,this._config?.next_mode_entity,this._config?.next_mode_at_entity,this._config?.heat_protection_entity,this._config?.cover_open_time_entity,this._config?.cover_close_time_entity,this._config?.cover_entity,this._config?.covers_left_open_entity].filter(Boolean).some(t=>e.states[t]!==this.hass.states[t])}return!1}getEntityState(t){if(t)return this.hass?.states?.[t]}getCoversLeftOpen(){const t=this.getEntityState(this._config?.covers_left_open_entity)?.attributes?.covers;return Array.isArray(t)?t.map(t=>this.hass?.states?.[t]?.attributes?.friendly_name??t):[]}onSelect(t,e){const i=e.target,o=i?.value;o&&this.hass.callService("select","select_option",{entity_id:t,option:o})}onCircularSliderSelect(t,e){this.hass.callService("select","select_option",{entity_id:t,option:e})}onOffButtonClick(t,e){e&&this.hass.callService("select","select_option",{entity_id:t,option:e})}onCoverAction(t){const e=this._config.cover_entity;e&&this.hass.callService("cover",t,{entity_id:e})}onOverrideDurationSelect(t){const e=this._config.override_duration_entity;if(!e)return;const i=Number(t.target.value);this.hass.callService("number","set_value",{entity_id:e,value:i})}onEarlySwitchSelect(t){const e=this._config.early_switch_entity;if(!e)return;const i=Number(t.target.value);this.hass.callService("number","set_value",{entity_id:e,value:i})}_formatRelativeTime(t){if(!t)return"";const e=new Date(t);if(isNaN(e.getTime()))return t;const i=e.getTime()-Date.now(),o=Math.round(i/6e4);if(o<=0)return mt(this.hass,"card.past")||"passé";if(o<60)return`${mt(this.hass,"card.in")||"dans"} ${o} min`;const s=Math.floor(o/60),n=o%60;return`${mt(this.hass,"card.in")||"dans"} ${s}h${n>0?n.toString().padStart(2,"0"):""}`}_formatAbsoluteTime(t){if(!t)return"";const e=new Date(t);if(isNaN(e.getTime()))return t;const i=new Date,o=new Date(i);o.setDate(o.getDate()+1);const s=e.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});return e.toDateString()===i.toDateString()?`${mt(this.hass,"card.today")||"Auj."} ${s}`:e.toDateString()===o.toDateString()?`${mt(this.hass,"card.tomorrow")||"Dem."} ${s}`:`${e.toLocaleDateString([],{month:"short",day:"numeric"})} ${s}`}_renderMain(t,e,i,o,s,n){const r=e.attributes?.option_map??{},a=Object.keys(r).length>0?Object.entries(r):(e.attributes?.options??[]).map(t=>[t,t]),c=t.attributes?.option_map??{},h=Object.keys(c).length>0,l=h?c.off??t.attributes?.options?.[0]??"":t.attributes?.options?.[0]??"",d=h?[l,...Object.entries(c).filter(([t])=>"off"!==t).map(([,t])=>t)]:t.attributes?.options??[],p=h?[mt(this.hass,"card.off")||l,...Object.keys(c).filter(t=>"off"!==t).map(t=>mt(this.hass,`thermostat.${t}`)||t)]:[mt(this.hass,"card.off")||(t.attributes?.options?.[0]??""),...["heating","cooling","ventilation"].map(t=>mt(this.hass,`thermostat.${t}`)||t)],_=this.getEntityState(this._config.next_mode_entity),u=this.getEntityState(this._config.next_mode_at_entity),f=Number(this.getEntityState(this._config.early_switch_entity)?.state??0),m=_?.state&&"unknown"!==_.state&&""!==_.state,v=s&&"unknown"!==s&&"unavailable"!==s,g=n&&"unknown"!==n&&"unavailable"!==n,y=Boolean(this._config.cover_entity);return W` + `,t([_t({attribute:!1})],xt.prototype,"hass",void 0),t([ut()],xt.prototype,"_config",void 0),customElements.get("homeshift-card-editor")||customElements.define("homeshift-card-editor",xt);const wt=new Set(["unknown","unavailable",""]);function Et(t){const e="string"==typeof t?t:t?.state;return"string"==typeof e&&!wt.has(e)}function At(t){if(t<60)return`${t}min`;const e=Math.floor(t/60),i=Math.round(t%60);return i>0?`${e}h${String(i).padStart(2,"0")}`:`${e}h`}function St(t){const e=t?.time_format;return{locale:"system"===e?void 0:t?.language,hour12:"12"===e||"24"!==e&&void 0}}class Ct extends lt{constructor(){super(...arguments),this.preview=!1,this._day=0}connectedCallback(){super.connectedCallback(),this._scheduleMidnightRefresh()}disconnectedCallback(){super.disconnectedCallback(),clearTimeout(this._midnightTimer)}_scheduleMidnightRefresh(){clearTimeout(this._midnightTimer);const t=new Date,e=new Date(t);e.setHours(24,0,1,0),this._midnightTimer=setTimeout(()=>{this._day++,this._scheduleMidnightRefresh()},e.getTime()-t.getTime())}static getStubConfig(){return{name:"Thermostat",day_mode_entity:"select.homeshift_day_mode",thermostat_mode_entity:"select.homeshift_thermostat_mode",override_duration_entity:"number.homeshift_override_duration",early_switch_entity:"number.homeshift_early_switch",next_mode_entity:"sensor.homeshift_next_mode",next_mode_at_entity:"sensor.homeshift_next_mode_at",heat_protection_entity:"binary_sensor.homeshift_cover_heat_active",cover_open_time_entity:"sensor.homeshift_cover_open_time",cover_close_time_entity:"sensor.homeshift_cover_close_time",covers_left_open_entity:"binary_sensor.homeshift_covers_left_open",show_title:!0}}static getConfigElement(){return document.createElement("homeshift-card-editor")}setConfig(t){if(!t)throw new Error("Missing configuration");const e={name:t.name??"Thermostat",show_title:!1!==t.show_title};for(const i of bt)e[i.key]=t[i.key]??i.default;this._config=e}getCardSize(){return 4}shouldUpdate(t){if(t.has("_config")||t.has("preview"))return!0;if(t.has("_day")||t.has("_openBadge")||t.has("_coverPending"))return!0;if(t.has("hass")){const e=t.get("hass");if(!e)return!0;return bt.map(t=>this._config?.[t.key]).filter(Boolean).some(t=>e.states[t]!==this.hass.states[t])}return!1}getEntityState(t){if(t)return this.hass?.states?.[t]}getCoversLeftOpen(){const t=this.getEntityState(this._config?.covers_left_open_entity)?.attributes?.covers;return Array.isArray(t)?t.map(t=>this.hass?.states?.[t]?.attributes?.friendly_name??t):[]}onSelect(t,e){const i=e.target,o=i?.value;o&&this.hass.callService("select","select_option",{entity_id:t,option:o})}onCircularSliderSelect(t,e){this.hass.callService("select","select_option",{entity_id:t,option:e})}_coverCall(t){const e=this._config.cover_entity;if(e)return Et(this.getEntityState(e))?{domain:"cover",service:`${t}_cover`,entity_id:e}:void 0;const i="open"===t?this._config.open_covers_entity:this._config.close_covers_entity,o=this.getEntityState(i);return o&&"unavailable"!==o.state?{domain:"button",service:"press",entity_id:i}:void 0}async onCoverAction(t){const e=this._coverCall(t);if(e&&!this._coverPending){this._coverPending=t;try{await Promise.all([this.hass.callService(e.domain,e.service,{entity_id:e.entity_id}),new Promise(t=>setTimeout(t,Ct.COVER_FEEDBACK_MS))])}catch(e){this.dispatchEvent(new CustomEvent("hass-notification",{detail:{message:vt(this.hass,`card.cover_${t}_failed`,{error:e?.message??String(e)})},bubbles:!0,composed:!0}))}finally{this._coverPending=void 0}}}_renderCoverRow(t,e){const i="open"===t?"mdi:roller-shade":"mdi:roller-shade-closed";if(!this._coverCall(t))return W`
+ + ${e} +
`;const o=this._coverPending===t,s=vt(this.hass,`card.cover_${t}_action`,{time:e});return W``}onPresetSelect(t,e){const i=Number(e.target.value);Number.isFinite(i)&&this.hass.callService("number","set_value",{entity_id:t,value:i})}_renderPresetSelect(t,e,i,o){if(!t)return K;const s=function(t){if(!Et(t))return;const e=Number(t.state);return Number.isFinite(e)?e:void 0}(this.getEntityState(t)),r=void 0===s||e.some(t=>t.value===s)?e:[...e,{label:At(s),value:s}].sort((t,e)=>t.value-e.value),n=void 0!==s&&0!==s;return W``}_formatAbsoluteTime(t){if(!t)return"";const e=new Date(t);if(isNaN(e.getTime()))return"";const i=new Date,o=new Date(i);o.setDate(o.getDate()+1);const s=function(t,e){const{locale:i,hour12:o}=St(e);return t.toLocaleTimeString(i,{hour:"2-digit",minute:"2-digit",hour12:o})}(e,this.hass?.locale);return e.toDateString()===i.toDateString()?`${vt(this.hass,"card.today")} ${s}`:e.toDateString()===o.toDateString()?`${vt(this.hass,"card.tomorrow")} ${s}`:`${function(t,e){const{locale:i}=St(e);return t.toLocaleDateString(i,{month:"short",day:"numeric"})}(e,this.hass?.locale)} ${s}`}_renderBadge(t,e,i,o){const s=this._openBadge===t;return W` + ${s?W`
${o}
`:K}`}_renderMain(t,e,i,o,s,r){const n=e.attributes?.option_map??{},a=Object.keys(n).length>0?Object.entries(n):(e.attributes?.options??[]).map(t=>[t,t]),c=t.attributes?.option_map??{},l=Object.keys(c).length>0,h=l?c.off??t.attributes?.options?.[0]??"":t.attributes?.options?.[0]??"",d=l?[h,...Object.entries(c).filter(([t])=>"off"!==t).map(([,t])=>t)]:t.attributes?.options??[],p=l?[vt(this.hass,"card.off"),...Object.entries(c).filter(([t])=>"off"!==t).map(([t,e])=>vt(this.hass,`thermostat.${t}`,void 0,e))]:[vt(this.hass,"card.off"),...["heating","cooling","ventilation"].map(t=>vt(this.hass,`thermostat.${t}`))],_=this.getEntityState(this._config.next_mode_entity),u=this.getEntityState(this._config.next_mode_at_entity),m=Et(_),f=Et(u)?this._formatAbsoluteTime(u.state):"",v=Et(s),g=Et(r);return W`
${v||g?W`
- ${v?W`
this.onCoverAction("open_cover")} - > - - ${s} -
`:Y} - ${g?W`
this.onCoverAction("close_cover")} - > - - ${n} -
`:Y} -
`:Y} + ${v?this._renderCoverRow("open",s):K} + ${g?this._renderCoverRow("close",r):K} +
`:K} ${i||o.length>0?W`
- ${i?W`
- -
`:Y} - ${o.length>0?W`
- -
`:Y} -
`:Y} + ${i?this._renderBadge("heat","heat-protection-badge","mdi:window-shutter",vt(this.hass,"card.heat_protection_active")):K} + ${o.length>0?this._renderBadge("covers","covers-left-open-badge","mdi:window-shutter-alert",vt(this.hass,"card.covers_left_open",{covers:o.join(", ")})):K} +
`:K} ((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. >
- ${m||u?.state?W`
+ ${m||f?W`
${m?W`${_.state}`:Y} - ${u?.state?W`${this._formatAbsoluteTime(u.state)}`:Y} -
`:Y} - ${this._config.early_switch_entity?W``:Y} + >`:K} + ${f?W`${f}`:K} +
`:K} + ${this._renderPresetSelect(this._config.early_switch_entity,Ct.EARLY_SWITCH_PRESETS,"list-select--early",vt(this.hass,"card.early_switch"))}
- ${(()=>{const t=Number(this.getEntityState(this._config.override_duration_entity)?.state??0);return W``})()} + ${this._renderPresetSelect(this._config.override_duration_entity,Ct.OVERRIDE_PRESETS,"list-select--override",vt(this.hass,"card.override_duration"))}
- `}render(){if(!this.hass||!this._config)return Y;const t=this._config.name??mt(this.hass,"card.title"),e=this.getEntityState(this._config.day_mode_entity),i=this.getEntityState(this._config.thermostat_mode_entity),o=e??{entity_id:this._config.day_mode_entity??"select.homeshift_day_mode",state:mt(this.hass,"preview.day_mode_state")||"Travail",attributes:{options:["Maison","Travail","Télétravail","Absence"],option_map:{home:"Maison",work:"Travail",remote:"Télétravail",away:"Absence"}}},s=i??{entity_id:this._config.thermostat_mode_entity??"select.homeshift_thermostat_mode",state:mt(this.hass,"preview.thermostat_state")||"Chauffage",attributes:{options:["Eteint","Chauffage","Climatisation","Ventilation"],option_map:{off:"Eteint",heating:"Chauffage",cooling:"Climatisation",ventilation:"Ventilation"}}},n="on"===this.getEntityState(this._config.heat_protection_entity)?.state,r=this.getEntityState(this._config.cover_open_time_entity)?.state,a=this.getEntityState(this._config.cover_close_time_entity)?.state;return W` + `}render(){if(!this.hass||!this._config)return K;const t=this._config.name??vt(this.hass,"card.title"),e=this.getEntityState(this._config.day_mode_entity),i=this.getEntityState(this._config.thermostat_mode_entity);if(!this.preview){const o=[[this._config.day_mode_entity,e],[this._config.thermostat_mode_entity,i]].filter(([,t])=>!t).map(([t])=>t||"?");if(o.length>0)return W` + + ${o.map(t=>W`
+ ${vt(this.hass,"card.entity_not_found",{entity:t})} +
`)} +
+ `}const o=e??{entity_id:this._config.day_mode_entity,state:vt(this.hass,"preview.work"),attributes:{option_map:Object.fromEntries(["home","work","remote","away"].map(t=>[t,vt(this.hass,`preview.${t}`)]))}},s=i??{entity_id:this._config.thermostat_mode_entity,state:vt(this.hass,"thermostat.heating"),attributes:{option_map:{off:vt(this.hass,"card.off"),heating:vt(this.hass,"thermostat.heating"),cooling:vt(this.hass,"thermostat.cooling"),ventilation:vt(this.hass,"thermostat.ventilation")}}},r="on"===this.getEntityState(this._config.heat_protection_entity)?.state,n=this.getEntityState(this._config.cover_open_time_entity)?.state,a=this.getEntityState(this._config.cover_close_time_entity)?.state;return W`
- ${this._renderMain(s,o,n,this.getCoversLeftOpen(),r,a)} + ${this._renderMain(s,o,r,this.getCoversLeftOpen(),n,a)}
- `}}wt.OVERRIDE_PRESETS=[{label:"--",value:0},{label:"15min",value:15},{label:"30min",value:30},{label:"1h",value:60},{label:"2h",value:120},{label:"4h",value:240},{label:"8h",value:480},{label:"12h",value:720}],wt.EARLY_SWITCH_PRESETS=[{label:"--",value:0},{label:"15min",value:15},{label:"30min",value:30},{label:"45min",value:45},{label:"1h",value:60},{label:"1h30",value:90},{label:"2h",value:120},{label:"3h",value:180},{label:"4h",value:240}],wt.styles=r` + `}}Ct.COVER_FEEDBACK_MS=600,Ct.OVERRIDE_PRESETS=[{label:"--",value:0},{label:"15min",value:15},{label:"30min",value:30},{label:"1h",value:60},{label:"2h",value:120},{label:"4h",value:240},{label:"8h",value:480},{label:"12h",value:720}],Ct.EARLY_SWITCH_PRESETS=[{label:"--",value:0},{label:"15min",value:15},{label:"30min",value:30},{label:"45min",value:45},{label:"1h",value:60},{label:"1h30",value:90},{label:"2h",value:120},{label:"3h",value:180},{label:"4h",value:240}],Ct.styles=n` ha-card { padding: 8px; position: relative; @@ -356,13 +321,6 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. width: 90%; } - .off-row { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - } - .bottom-controls { flex: 1; display: flex; @@ -379,40 +337,6 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. width: 100%; } - .center-button { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 50%; - border: 1px solid var(--divider-color, #ccc); - background: var(--card-background-color, #ffffff); - cursor: pointer; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - transition: - background 0.2s, - border-color 0.2s; - } - - .center-button ha-icon { - color: var(--secondary-text-color, #666); - } - - .center-button.active { - background: var(--primary-color); - border-color: var(--primary-color); - } - - .center-button.active ha-icon { - color: var(--text-primary-color, #fff); - } - - hui-card { - --ha-card-box-shadow: none; - --ha-card-border-width: 0; - } - @keyframes fadeIn { from { opacity: 0; @@ -531,18 +455,54 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. white-space: nowrap; } + button.cover-time-row { + font: inherit; + font-size: 11px; + } + .cover-time-row.actionable { cursor: pointer; transition: border-color 0.2s ease, - background 0.2s ease; + background 0.2s ease, + opacity 0.2s ease; } - .cover-time-row.actionable:hover { + .cover-time-row.actionable:hover:not(:disabled) { border-color: var(--primary-color); background: rgba(128, 128, 128, 0.22); } + .cover-time-row.actionable:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 1px; + } + + .cover-time-row.actionable:disabled { + cursor: progress; + } + + .cover-time-row.pending { + border-color: var(--primary-color); + color: var(--primary-color); + animation: cover-pending 0.8s ease-in-out infinite alternate; + } + + @keyframes cover-pending { + from { + opacity: 1; + } + to { + opacity: 0.5; + } + } + + @media (prefers-reduced-motion: reduce) { + .cover-time-row.pending { + animation: none; + } + } + .cover-time-row ha-icon { color: var(--secondary-text-color); --mdi-icon-size: 14px; @@ -568,11 +528,32 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. justify-content: center; width: 36px; height: 36px; + padding: 0; + border: none; border-radius: 50%; color: var(--text-primary-color, #fff); + cursor: pointer; + font: inherit; animation: fadeIn 0.3s ease; } + .badge:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; + } + + .badge-detail { + max-width: 140px; + padding: 4px 8px; + border-radius: 8px; + background: var(--card-background-color, #fff); + border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.1)); + color: var(--primary-text-color); + font-size: 11px; + text-align: left; + animation: fadeIn 0.2s ease; + } + .badge ha-icon { color: var(--text-primary-color, #fff); --mdi-icon-size: 20px; @@ -580,15 +561,12 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. .heat-protection-badge { background: var(--error-color, #e7973c); - pointer-events: none; } /* Pulses between orange and yellow: a cover left up is something to act - on tonight, not a steady status. Hoverable, so the tooltip can name - the covers. */ + on tonight, not a steady status. Tapping it names the covers. */ .covers-left-open-badge { background: var(--warning-color, #ff9800); - cursor: help; animation: fadeIn 0.3s ease, covers-left-open-blink 1.2s ease-in-out infinite; @@ -613,5 +591,4 @@ function(t){return(e,i,o)=>((t,e,i)=>(i.configurable=!0,i.enumerable=!0,Reflect. animation: fadeIn 0.3s ease; } } - `,t([_t({attribute:!1})],wt.prototype,"hass",void 0),t([ut()],wt.prototype,"_config",void 0),t([ut()],wt.prototype,"_tick",void 0),customElements.get("homeshift-card")||customElements.define("homeshift-card",wt),window.customCards=window.customCards||[],window.customCards.push({type:"homeshift-card",name:"HomeShift Card",description:"Card to manage day mode and thermostat mode via the HomeShift integration.",preview:!0}); -//# sourceMappingURL=homeshift-card.js.map + `,t([_t({attribute:!1})],Ct.prototype,"hass",void 0),t([_t({type:Boolean})],Ct.prototype,"preview",void 0),t([ut()],Ct.prototype,"_config",void 0),t([ut()],Ct.prototype,"_coverPending",void 0),t([ut()],Ct.prototype,"_openBadge",void 0),t([ut()],Ct.prototype,"_day",void 0),customElements.get("homeshift-card")||customElements.define("homeshift-card",Ct),window.customCards=window.customCards||[],window.customCards.push({type:"homeshift-card",name:"HomeShift Card",description:"Card to manage day mode and thermostat mode via the HomeShift integration.",preview:!0}); From f99e92622c0df01f4d252efe4b96f6af66b6546c Mon Sep 17 00:00:00 2001 From: Dimitri CONNES Date: Sat, 3 Oct 2026 20:30:24 +0200 Subject: [PATCH 16/25] chore: add MIT LICENSE file HACS validation reads the license from the default branch through the GitHub API, so it must exist on main for pull requests to pass. Co-Authored-By: Claude Opus 5.5 --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e535794 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Gamso + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 196d04ff52ad954f1c5819efa09fa3527d00db4d Mon Sep 17 00:00:00 2001 From: Gamso Date: Sat, 3 Oct 2026 21:00:03 +0200 Subject: [PATCH 17/25] fix(card): step a duration to the adjacent preset from any value The list layout's stepper looked up the first preset >= the current value and moved one index from there. From a value between two presets (25 min set on the entity page) "+" therefore skipped 30 and jumped to 1h. "+" now goes to the first preset above the value and "-" to the last one below it. The presets were also carried as { label, value } pairs whose labels the list never displays; they are plain minute lists now. Audit: B-5 (values outside the presets), P-1 (dead code) Co-Authored-By: Claude Opus 5.5 --- src/components/homeshift-card.ts | 59 +++++++++++--------------------- test/states.test.ts | 37 ++++++++++++++++++++ 2 files changed, 57 insertions(+), 39 deletions(-) diff --git a/src/components/homeshift-card.ts b/src/components/homeshift-card.ts index 2a53ca7..24e74aa 100644 --- a/src/components/homeshift-card.ts +++ b/src/components/homeshift-card.ts @@ -12,33 +12,12 @@ class HomeShiftCard extends LitElement { /** Minimum time the "sending" state of a cover button stays visible. */ static COVER_FEEDBACK_MS = 600; - // Predefined override duration values in minutes (0 = disabled). - private static readonly OVERRIDE_PRESETS: { label: string; value: number }[] = - [ - { label: "--", value: 0 }, - { label: "15min", value: 15 }, - { label: "30min", value: 30 }, - { label: "1h", value: 60 }, - { label: "2h", value: 120 }, - { label: "4h", value: 240 }, - { label: "8h", value: 480 }, - { label: "12h", value: 720 }, - ]; - - // Predefined early-switch anticipation values in minutes (0 = disabled). - private static readonly EARLY_SWITCH_PRESETS: { - label: string; - value: number; - }[] = [ - { label: "--", value: 0 }, - { label: "15min", value: 15 }, - { label: "30min", value: 30 }, - { label: "45min", value: 45 }, - { label: "1h", value: 60 }, - { label: "1h30", value: 90 }, - { label: "2h", value: 120 }, - { label: "3h", value: 180 }, - { label: "4h", value: 240 }, + // Override duration steps in minutes (0 = disabled). + private static readonly OVERRIDE_PRESETS = [0, 15, 30, 60, 120, 240, 480, 720]; + + // Early-switch anticipation steps in minutes (0 = disabled). + private static readonly EARLY_SWITCH_PRESETS = [ + 0, 15, 30, 45, 60, 90, 120, 180, 240, ]; @property({ attribute: false }) public hass!: any; @@ -351,25 +330,27 @@ class HomeShiftCard extends LitElement { private _stepSetting( name: "override" | "early", - presets: { value: number }[], + presets: number[], current: number, - direction: number, + direction: 1 | -1, ) { const entityId = name === "override" ? this._config.override_duration_entity : this._config.early_switch_entity; if (!entityId) return; - const values = presets.map((preset) => preset.value); - // The stored value may sit between two presets; land on the neighbour in - // the direction asked for rather than on a preset index that never matches. - const index = values.findIndex((value) => value >= current); - const from = index === -1 ? values.length - 1 : index; - const next = Math.min(values.length - 1, Math.max(0, from + direction)); + // The stored value may sit between two presets (25 min set from the + // entity page): land on the nearest preset in the direction asked for, + // 30 going up and 15 going down. + const next = + direction > 0 + ? presets.find((value) => value > current) + : [...presets].reverse().find((value) => value < current); + if (next === undefined) return; this._armSettingTimeout(); this.hass.callService("number", "set_value", { entity_id: entityId, - value: values[next], + value: next, }); } @@ -384,7 +365,7 @@ class HomeShiftCard extends LitElement { icon: string, label: string, effect: string, - presets: { value: number }[], + presets: number[], current: number | undefined, ) { const open = current !== undefined && this._openSetting === name; @@ -407,7 +388,7 @@ class HomeShiftCard extends LitElement {
- ${this._settingLabel(current)} + ${this._settingLabel(current)} `}_formatAbsoluteTime(t){if(!t)return"";const e=new Date(t);if(isNaN(e.getTime()))return"";const i=new Date,o=new Date(i);o.setDate(o.getDate()+1);const s=bt(e,this.hass?.locale);return e.toDateString()===i.toDateString()?`${mt(this.hass,"card.today")} ${s}`:e.toDateString()===o.toDateString()?`${mt(this.hass,"card.tomorrow")} ${s}`:`${function(t,e){const{locale:i}=$t(e);return t.toLocaleDateString(i,{month:"short",day:"numeric"})}(e,this.hass?.locale)} ${s}`}_settingLabel(t){return void 0===t?mt(this.hass,"card.unavailable"):t?function(t){if(t<60)return`${t} min`;const e=Math.floor(t/60),i=Math.round(t%60);return i>0?`${e}h${String(i).padStart(2,"0")}`:`${e}h`}(t):mt(this.hass,"card.duration_off")}_clockIn(t){return bt(new Date(Date.now()+6e4*t),this.hass?.locale)}_clockAt(t,e=0){return bt(new Date(new Date(t).getTime()+6e4*e),this.hass?.locale)}_toggleSetting(t){this._openSetting=this._openSetting===t?null:t,this._armSettingTimeout()}_armSettingTimeout(){clearTimeout(this._settingTimeout),this._openSetting&&(this._settingTimeout=setTimeout(()=>{this._openSetting=null},6e3))}_stepSetting(t,e,i,o){const s="override"===t?this._config.override_duration_entity:this._config.early_switch_entity;if(!s)return;const n=o>0?e.find(t=>t>i):[...e].reverse().find(t=>t + `}onPresetSelect(t,e){const i=Number(e.target.value);Number.isFinite(i)&&this.hass.callService("number","set_value",{entity_id:t,value:i})}_renderPresetSelect(t,e,i,o){if(!t)return K;const s=function(t){if(!Et(t))return;const e=Number(t.state);return Number.isFinite(e)?e:void 0}(this.getEntityState(t)),r=void 0===s||e.some(t=>t.value===s)?e:[...e,{label:At(s),value:s}].sort((t,e)=>t.value-e.value),n=void 0!==s&&0!==s;return W``}_formatAbsoluteTime(t){if(!t)return"";const e=new Date(t);if(isNaN(e.getTime()))return"";const i=new Date,o=new Date(i);o.setDate(o.getDate()+1);const s=function(t,e){const{locale:i,hour12:o}=St(e);return t.toLocaleTimeString(i,{hour:"2-digit",minute:"2-digit",hour12:o})}(e,this.hass?.locale);return e.toDateString()===i.toDateString()?`${vt(this.hass,"card.today")} ${s}`:e.toDateString()===o.toDateString()?`${vt(this.hass,"card.tomorrow")} ${s}`:`${function(t,e){const{locale:i}=St(e);return t.toLocaleDateString(i,{month:"short",day:"numeric"})}(e,this.hass?.locale)} ${s}`}_renderBadge(t,e,i,o){const s=this._openBadge===t;return W` - ${s?W`
${o}
`:K}`}_renderMain(t,e,i,o,s,r){const n=e.attributes?.option_map??{},a=Object.keys(n).length>0?Object.entries(n):(e.attributes?.options??[]).map(t=>[t,t]),c=t.attributes?.option_map??{},l=Object.keys(c).length>0,h=l?c.off??t.attributes?.options?.[0]??"":t.attributes?.options?.[0]??"",d=l?[h,...Object.entries(c).filter(([t])=>"off"!==t).map(([,t])=>t)]:t.attributes?.options??[],p=l?[vt(this.hass,"card.off"),...Object.entries(c).filter(([t])=>"off"!==t).map(([t,e])=>vt(this.hass,`thermostat.${t}`,void 0,e))]:[vt(this.hass,"card.off"),...["heating","cooling","ventilation"].map(t=>vt(this.hass,`thermostat.${t}`))],_=this.getEntityState(this._config.next_mode_entity),u=this.getEntityState(this._config.next_mode_at_entity),m=Et(_),f=Et(u)?this._formatAbsoluteTime(u.state):"",v=Et(s),g=Et(r);return W` -
- ${v||g?W`
- ${v?this._renderCoverRow("open",s):K} - ${g?this._renderCoverRow("close",r):K} -
`:K} - ${i||o.length>0?W`
- ${i?this._renderBadge("heat","heat-protection-badge","mdi:window-shutter",vt(this.hass,"card.heat_protection_active")):K} - ${o.length>0?this._renderBadge("covers","covers-left-open-badge","mdi:window-shutter-alert",vt(this.hass,"card.covers_left_open",{covers:o.join(", ")})):K} -
`:K} - this.onCircularSliderSelect(t.entity_id,e.detail.option)} - > - -
- ${m||f?W`
- ${m?W`${_.state}`:K} - ${f?W`${f}`:K} -
`:K} - ${this._renderPresetSelect(this._config.early_switch_entity,Ct.EARLY_SWITCH_PRESETS,"list-select--early",vt(this.hass,"card.early_switch"))} -
- -
-
-
- + + +
+

${o}

+
`:W} +
`}_minutes(t){if(!t)return null;const e=this.getEntityState(t);return e?function(t){if(!yt(t))return;const e=Number(t.state);return Number.isFinite(e)?e:void 0}(e):this.preview?0:null}_renderMain(t,e,i,o,s,n){const r=e.attributes?.option_map??{},a=Object.keys(r).length>0?Object.entries(r):(e.attributes?.options??[]).map(t=>[t,t]),c=t.attributes?.option_map??{},l=Object.keys(c).length>0?Object.entries(c):(t.attributes?.options??[]).map(t=>[t,t]),h=this.getEntityState(this._config.next_mode_entity),d=this.getEntityState(this._config.next_mode_at_entity),p=this._minutes(this._config.early_switch_entity),_=this._minutes(this._config.override_duration_entity),u=yt(h),m=u?h.state:"",f=yt(d)?this._formatAbsoluteTime(d.state):"",v=""!==f,g=_?mt(this.hass,"card.override_effect",{duration:this._settingLabel(_),time:this._clockIn(_)}):mt(this.hass,"card.override_none"),y=v?p?mt(this.hass,"card.early_effect",{mode:m,time:this._clockAt(d.state),scheduled:this._clockAt(d.state,p)}):mt(this.hass,"card.early_none",{mode:m,time:this._clockAt(d.state)}):mt(this.hass,"card.early_no_event"),$=yt(s),b=yt(n),w=mt(this.hass,"card.covers_left_open",{covers:o.join(", ")}),x=o.length>3,E=x?mt(this.hass,"card.covers_left_open_count",{count:String(o.length)}):w,A=t.attributes?.current_key,S=yt(t);return V` +
+
+ ${l.map(([e,i])=>{const o=S&&(A?e===A:i===t.state);return V``})} +
- ${this._renderPresetSelect(this._config.override_duration_entity,Ct.OVERRIDE_PRESETS,"list-select--override",vt(this.hass,"card.override_duration"))} -
+
+ ${mt(this.hass,"card.day_mode")} +
+ + ${u||v?V`
+ ${mt(this.hass,"card.next")} + ${m}${u&&v?" · ":""}${f} +
`:W} + + ${null!==_?this._renderSetting("override","mdi:hand-back-left",mt(this.hass,"card.override_label"),g,wt.OVERRIDE_PRESETS,_):W} + ${null!==p?this._renderSetting("early","mdi:clock-fast",mt(this.hass,"card.early_label"),y,wt.EARLY_SWITCH_PRESETS,p):W} + + ${$||b?V`
+ ${mt(this.hass,"card.covers")} + + ${$?this._renderCoverTime("open",s):W} + ${b?this._renderCoverTime("close",n):W} + +
`:W} + + ${i||o.length>0?V`
+ ${i?V` + + ${mt(this.hass,"card.heat_protection_active")} + `:W} + ${o.length>0&&!x?V` + + ${E} + `:W} + ${x?V``:W} +
+ ${x&&this._coversExpanded?V`

${w}

`:W}`:W}
- `}render(){if(!this.hass||!this._config)return K;const t=this._config.name??vt(this.hass,"card.title"),e=this.getEntityState(this._config.day_mode_entity),i=this.getEntityState(this._config.thermostat_mode_entity);if(!this.preview){const o=[[this._config.day_mode_entity,e],[this._config.thermostat_mode_entity,i]].filter(([,t])=>!t).map(([t])=>t||"?");if(o.length>0)return W` - - ${o.map(t=>W`
- ${vt(this.hass,"card.entity_not_found",{entity:t})} + `}render(){if(!this.hass||!this._config)return W;const t=this.getEntityState(this._config.day_mode_entity),e=this.getEntityState(this._config.thermostat_mode_entity);if(!this.preview){const i=[[this._config.day_mode_entity,t],[this._config.thermostat_mode_entity,e]].filter(([,t])=>!t).map(([t])=>t||"?");if(i.length>0)return V` + + ${i.map(t=>V`
+ ${mt(this.hass,"card.entity_not_found",{entity:t})}
`)}
- `}const o=e??{entity_id:this._config.day_mode_entity,state:vt(this.hass,"preview.work"),attributes:{option_map:Object.fromEntries(["home","work","remote","away"].map(t=>[t,vt(this.hass,`preview.${t}`)]))}},s=i??{entity_id:this._config.thermostat_mode_entity,state:vt(this.hass,"thermostat.heating"),attributes:{option_map:{off:vt(this.hass,"card.off"),heating:vt(this.hass,"thermostat.heating"),cooling:vt(this.hass,"thermostat.cooling"),ventilation:vt(this.hass,"thermostat.ventilation")}}},r="on"===this.getEntityState(this._config.heat_protection_entity)?.state,n=this.getEntityState(this._config.cover_open_time_entity)?.state,a=this.getEntityState(this._config.cover_close_time_entity)?.state;return W` - + `}const i=t??{entity_id:this._config.day_mode_entity,state:mt(this.hass,"preview.work"),attributes:{option_map:Object.fromEntries(["home","work","remote","away"].map(t=>[t,mt(this.hass,`preview.${t}`)]))}},o=e??{entity_id:this._config.thermostat_mode_entity,state:mt(this.hass,"thermostat.heating"),attributes:{option_map:Object.fromEntries(["off","heating","cooling","ventilation"].map(t=>[t,mt(this.hass,`thermostat.${t}`)])),current_key:"heating"}},s="on"===this.getEntityState(this._config.heat_protection_entity)?.state,n=this.getEntityState(this._config.cover_open_time_entity)?.state,r=this.getEntityState(this._config.cover_close_time_entity)?.state;return V` +
- ${this._renderMain(s,o,r,this.getCoversLeftOpen(),n,a)} + ${this._renderMain(o,i,s,this.getCoversLeftOpen(),n,r)}
- `}}Ct.COVER_FEEDBACK_MS=600,Ct.OVERRIDE_PRESETS=[{label:"--",value:0},{label:"15min",value:15},{label:"30min",value:30},{label:"1h",value:60},{label:"2h",value:120},{label:"4h",value:240},{label:"8h",value:480},{label:"12h",value:720}],Ct.EARLY_SWITCH_PRESETS=[{label:"--",value:0},{label:"15min",value:15},{label:"30min",value:30},{label:"45min",value:45},{label:"1h",value:60},{label:"1h30",value:90},{label:"2h",value:120},{label:"3h",value:180},{label:"4h",value:240}],Ct.styles=n` + `}}wt.COVER_FEEDBACK_MS=600,wt.OVERRIDE_PRESETS=[0,15,30,60,120,240,480,720],wt.EARLY_SWITCH_PRESETS=[0,15,30,45,60,90,120,180,240],wt.styles=r` ha-card { - padding: 8px; + padding: 12px; position: relative; - min-height: 240px; - text-align: center; } .container { - display: flex; - justify-content: center; width: 100%; } - /* SLIDER VIEW */ - .thermo-section { - position: relative; - width: 100%; - max-width: 240px; + .rows { display: flex; flex-direction: column; - align-items: center; } - homeshift-circular-slider { - width: 100%; + /* Thermostat presets — one segmented control, one colour per mode. */ + .presets { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 1fr; + border: 1px solid var(--divider-color, #ccc); + border-radius: 8px; + overflow: hidden; + margin-bottom: 4px; + } + + .preset { + border: 0; + border-left: 1px solid var(--divider-color, #ccc); + background: transparent; + color: var(--secondary-text-color, #666); + font-size: 13px; + font-family: inherit; + padding: 8px 4px; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: + background 0.2s ease, + color 0.2s ease; } - .thermo-bottom { - position: absolute; - bottom: 16px; - left: 50%; - transform: translateX(-50%); + .preset:first-child { + border-left: 0; + } + + .preset:hover { + background: var(--secondary-background-color, rgba(127, 127, 127, 0.12)); + } + + .preset:disabled { + cursor: default; + opacity: 0.6; + } + + .preset:focus-visible, + .setting-head:focus-visible, + .stepper button:focus-visible, + .cover-time.actionable:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: -2px; + } + + .preset.on { + background: var(--secondary-text-color, #666); + color: var(--text-primary-color, #fff); + } + + .preset--heating.on { + background: var(--state-climate-heat-color, #e24b4a); + } + + .preset--cooling.on { + background: var(--state-climate-cool-color, #378add); + } + + .preset--ventilation.on { + background: var(--state-fan-active-color, #1d9e75); + } + + .preset--off.on { + background: var(--divider-color, #9e9e9e); + color: var(--primary-text-color); + } + + .row { display: flex; - flex-direction: column; align-items: center; + justify-content: space-between; gap: 8px; - z-index: 2; - width: 90%; + padding: 8px 0; + border-top: 1px solid var(--divider-color, #e0e0e0); + font-size: 13px; } - .bottom-controls { - flex: 1; + .row-key { + color: var(--secondary-text-color, #666); + } + + /* Collapsed by default: the value is readable without opening anything. */ + .setting { + border-top: 1px solid var(--divider-color, #e0e0e0); + } + + .setting-head { display: flex; align-items: center; gap: 8px; + width: 100%; + padding: 8px 0; + border: 0; + background: transparent; + color: var(--primary-text-color); + font-size: 13px; + font-family: inherit; + text-align: left; + cursor: pointer; } - .day-section { - flex: 1; - min-width: 0; + .setting-head:disabled { + cursor: default; } - .day-section select { - width: 100%; + .setting-head:disabled .chevron { + visibility: hidden; } - @keyframes fadeIn { - from { - opacity: 0; - transform: translateY(5px); - } - to { - opacity: 1; - transform: translateY(0); - } + .setting-head ha-icon { + --mdc-icon-size: 18px; + color: var(--secondary-text-color, #666); + flex-shrink: 0; } - /* UI ELEMENTS */ - select { - padding: 8px; - border-radius: 6px; - border: 1px solid var(--divider-color, #ccc); - background: var(--card-background-color); - color: var(--primary-text-color); + .setting-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - .list-select { - flex-shrink: 0; - align-self: stretch; - padding: 0 8px; - border-radius: 6px; - border: 1px solid var(--divider-color, #ccc); - background: var(--card-background-color); - color: var(--primary-text-color); - font-size: 13px; - cursor: pointer; - transition: - background 0.2s ease, - border-color 0.2s ease, - color 0.2s ease; + .setting-value { + color: var(--secondary-text-color, #666); } - .list-select--override.active { - border-color: var(--primary-color); - background: var(--primary-color); - color: var(--text-primary-color, #fff); + .setting-value.set { + color: var(--primary-text-color); + font-weight: 500; } - .list-select--early.active { - border-color: var(--accent-color, var(--primary-color)); - background: var(--accent-color, var(--primary-color)); - color: var(--text-primary-color, #fff); + .setting .chevron { + transition: transform 0.15s ease; } - .list-select--early { - padding: 8px; + .setting.open .chevron { + transform: rotate(180deg); } - .next-info-group { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - z-index: 2; - text-align: center; - background: rgba(128, 128, 128, 0.12); - border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.1)); - border-radius: 10px; - padding: 6px 12px; + .setting-body { + padding: 0 0 10px 26px; + animation: fadeIn 0.15s ease; } - .next-info { + .stepper { display: flex; - flex-direction: column; align-items: center; - gap: 2px; - pointer-events: none; + gap: 10px; } - .next-mode-state { - font-size: 13px; - font-weight: 600; + .stepper button { + width: 34px; + height: 34px; + border-radius: 8px; + border: 1px solid var(--divider-color, #ccc); + background: var(--card-background-color); color: var(--primary-text-color); + font-size: 18px; + line-height: 1; + cursor: pointer; } - .next-mode-at-state { - font-size: 11px; - color: var(--secondary-text-color); + .stepper button:hover:not(:disabled) { + background: var(--secondary-background-color, rgba(127, 127, 127, 0.12)); } - .error { - color: var(--error-color); - padding: 16px; + .stepper button:disabled { + opacity: 0.4; + cursor: default; + } + + .stepper-value { + min-width: 64px; + text-align: center; + font-size: 15px; + font-weight: 500; + } + + .setting-effect { + margin: 8px 0 0; + font-size: 12px; + line-height: 1.4; + color: var(--secondary-text-color, #666); } .cover-times { - position: absolute; - right: 100%; - margin-right: 8px; - top: 4%; display: flex; - flex-direction: column; - gap: 4px; - z-index: 2; - animation: fadeIn 0.3s ease; + gap: 6px; } - .cover-time-row { - display: flex; + .cover-time { + display: inline-flex; align-items: center; gap: 4px; - padding: 2px 6px; - border-radius: 10px; - background: rgba(128, 128, 128, 0.12); - border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.1)); - font-size: 11px; - color: var(--secondary-text-color); - white-space: nowrap; + padding: 4px 8px; + border-radius: 8px; + border: 1px solid var(--divider-color, #ccc); + background: var(--card-background-color); + color: var(--primary-text-color); + font-size: 13px; + font-family: inherit; } - button.cover-time-row { - font: inherit; - font-size: 11px; + .cover-time ha-icon { + --mdc-icon-size: 16px; + color: var(--secondary-text-color, #666); } - .cover-time-row.actionable { + .cover-time.actionable { cursor: pointer; - transition: - border-color 0.2s ease, - background 0.2s ease, - opacity 0.2s ease; } - .cover-time-row.actionable:hover:not(:disabled) { + .cover-time.actionable:hover:not(:disabled) { border-color: var(--primary-color); - background: rgba(128, 128, 128, 0.22); - } - - .cover-time-row.actionable:focus-visible { - outline: 2px solid var(--primary-color); - outline-offset: 1px; } - .cover-time-row.actionable:disabled { + .cover-time.actionable:disabled { cursor: progress; } - .cover-time-row.pending { + .cover-time.pending { border-color: var(--primary-color); color: var(--primary-color); animation: cover-pending 0.8s ease-in-out infinite alternate; @@ -498,75 +454,67 @@ const dt={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:$},pt=(t=dt } @media (prefers-reduced-motion: reduce) { - .cover-time-row.pending { + .cover-time.pending { animation: none; } } - .cover-time-row ha-icon { - color: var(--secondary-text-color); - --mdi-icon-size: 14px; + .error { + color: var(--error-color); + padding: 4px; } - /* Badges sit beside the dial and stack downwards, so heat protection - and covers left open can be raised at the same time without one - covering the other. */ - .badge-stack { - position: absolute; - left: 100%; - margin-left: 8px; - top: 4%; + /* Alerts only exist while something is wrong; nothing is shown otherwise. */ + .alerts { display: flex; - flex-direction: column; - gap: 8px; - z-index: 3; + flex-wrap: wrap; + gap: 6px; + padding-top: 10px; + border-top: 1px solid var(--divider-color, #e0e0e0); } - .badge { - display: flex; + .chip { + display: inline-flex; align-items: center; - justify-content: center; - width: 36px; - height: 36px; - padding: 0; - border: none; - border-radius: 50%; - color: var(--text-primary-color, #fff); - cursor: pointer; - font: inherit; + gap: 4px; + padding: 4px 8px; + border-radius: 8px; + font-size: 12px; animation: fadeIn 0.3s ease; } - .badge:focus-visible { + .chip ha-icon { + --mdc-icon-size: 16px; + } + + button.chip { + border: 0; + font-family: inherit; + cursor: pointer; + } + + button.chip:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; } - .badge-detail { - max-width: 140px; - padding: 4px 8px; - border-radius: 8px; - background: var(--card-background-color, #fff); - border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.1)); + .covers-detail { + margin: 6px 0 0; + font-size: 12px; + line-height: 1.4; color: var(--primary-text-color); - font-size: 11px; - text-align: left; - animation: fadeIn 0.2s ease; - } - - .badge ha-icon { - color: var(--text-primary-color, #fff); - --mdi-icon-size: 20px; } - .heat-protection-badge { + .chip--heat { background: var(--error-color, #e7973c); + color: var(--text-primary-color, #fff); } /* Pulses between orange and yellow: a cover left up is something to act - on tonight, not a steady status. Tapping it names the covers. */ - .covers-left-open-badge { + on tonight, not a steady status. */ + .chip--covers { background: var(--warning-color, #ff9800); + color: #412402; animation: fadeIn 0.3s ease, covers-left-open-blink 1.2s ease-in-out infinite; @@ -584,11 +532,32 @@ const dt={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:$},pt=(t=dt } } - /* A blinking badge is exactly what reduced-motion asks us not to do; + /* A blinking chip is exactly what reduced-motion asks us not to do; the colour alone still reads as a warning. */ @media (prefers-reduced-motion: reduce) { - .covers-left-open-badge { + .chip--covers { animation: fadeIn 0.3s ease; } } - `,t([_t({attribute:!1})],Ct.prototype,"hass",void 0),t([_t({type:Boolean})],Ct.prototype,"preview",void 0),t([ut()],Ct.prototype,"_config",void 0),t([ut()],Ct.prototype,"_coverPending",void 0),t([ut()],Ct.prototype,"_openBadge",void 0),t([ut()],Ct.prototype,"_day",void 0),customElements.get("homeshift-card")||customElements.define("homeshift-card",Ct),window.customCards=window.customCards||[],window.customCards.push({type:"homeshift-card",name:"HomeShift Card",description:"Card to manage day mode and thermostat mode via the HomeShift integration.",preview:!0}); + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(3px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + select { + padding: 6px 8px; + border-radius: 8px; + border: 1px solid var(--divider-color, #ccc); + background: var(--card-background-color); + color: var(--primary-text-color); + font-size: 13px; + font-family: inherit; + } + `,t([dt({attribute:!1})],wt.prototype,"hass",void 0),t([dt({type:Boolean})],wt.prototype,"preview",void 0),t([pt()],wt.prototype,"_config",void 0),t([pt()],wt.prototype,"_openSetting",void 0),t([pt()],wt.prototype,"_coverPending",void 0),t([pt()],wt.prototype,"_coversExpanded",void 0),t([pt()],wt.prototype,"_day",void 0),customElements.get("homeshift-card")||customElements.define("homeshift-card",wt),window.customCards=window.customCards||[],window.customCards.push({type:"homeshift-card",name:"HomeShift Card",description:"Card to manage day mode and thermostat mode via the HomeShift integration.",preview:!0}); From 64652fdda96af6480971418ab3d30e1f3d4e1af2 Mon Sep 17 00:00:00 2001 From: Gamso Date: Thu, 8 Oct 2026 10:05:19 +0000 Subject: [PATCH 21/25] feat(card): pause the automation of chosen covers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A "Cover automation" row lists every cover HomeShift drives (read from sensor.homeshift_covers_inhibited). Pick a duration — 4 h to 7 days, or until resumed — then pause covers one by one; each paused cover shows when it comes back and can be resumed, or all at once. While covers are paused, a calm chip names them and reopens the row. A refused call is shown as Home Assistant's notification toast. Calls homeshift.inhibit_covers / homeshift.resume_covers. The row is hidden when the sensor is missing, so older integrations are unaffected. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016uVhrB9KRnmbFZRPHNHUev --- README.md | 10 +- dist/homeshift-card.js | 304 +++++++++++++++++++++------- src/components/homeshift-card.ts | 331 ++++++++++++++++++++++++++++++- src/localize/en.json | 20 +- src/localize/fr.json | 20 +- src/types.ts | 7 + test/inhibition.test.ts | 118 +++++++++++ 7 files changed, 725 insertions(+), 85 deletions(-) create mode 100644 test/inhibition.test.ts diff --git a/README.md b/README.md index 28c719f..a20350a 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,15 @@ The **Covers** row shows the scheduled opening and closing times of the daily co If the legacy `cover_entity` option is set, it takes priority over the buttons: the times then call `cover.open_cover` / `cover.close_cover` on that entity, as in previous versions. +### Cover automation pause + +Keep one or more covers out of HomeShift's automation for a while — e.g. a bedroom kept dark for a few days. Open the **Cover automation** row, pick a duration (4 h to 7 days, or until resumed), then press **Pause** next to each cover to hold. A paused cover shows when it comes back; **Resume** (or **Resume all**) hands it back right away. A refused call is reported in the usual Home Assistant notification toast. + +Pausing or resuming never moves a cover: it simply skips, or rejoins, the next scheduled open/close (and the "open/close now" buttons skip it too). The row lists the covers of HomeShift's `sensor.homeshift_covers_inhibited` and is hidden when that entity is missing. + ### Alerts -At the bottom of the card, a chip appears while the heat protection keeps the covers down, and a blinking chip names the covers tonight's close had to leave open (window open). Past three covers the chip shows a count; tap it (or focus it and press Enter) to read the names. Nothing is shown while everything is in order. +At the bottom of the card, a chip appears while the heat protection keeps the covers down, and a blinking chip names the covers tonight's close had to leave open (window open). Past three covers the chip shows a count; tap it (or focus it and press Enter) to read the names. A calm, non-blinking chip names the paused covers; tap it to open the pause row. Nothing is shown while everything is in order. --- @@ -108,6 +114,7 @@ cover_close_time_entity: sensor.homeshift_cover_close_time open_covers_entity: button.homeshift_open_covers close_covers_entity: button.homeshift_close_covers covers_left_open_entity: binary_sensor.homeshift_covers_left_open +covers_inhibited_entity: sensor.homeshift_covers_inhibited ``` Every entity option defaults to the entity the HomeShift integration creates, so a plain `type: custom:homeshift-card` works with a default installation. Set an option to an empty string to hide the corresponding element. If the day mode or thermostat mode entity does not exist, the card shows an "Entity not found" message instead of the controls. @@ -130,6 +137,7 @@ Every entity option defaults to the entity the HomeShift integration creates, so | `close_covers_entity` | string | `button.homeshift_close_covers` | Button pressed by the closing time ("close now") | | `cover_entity` | string | _(none)_ | Legacy: cover opened/closed by the times; overrides the two buttons when set | | `covers_left_open_entity` | string | `binary_sensor.homeshift_covers_left_open` | "Not closed" chip | +| `covers_inhibited_entity` | string | `sensor.homeshift_covers_inhibited` | Cover automation pause row and "Paused" chip | All options can also be set from the visual editor. diff --git a/dist/homeshift-card.js b/dist/homeshift-card.js index 178c6b7..dc15402 100644 --- a/dist/homeshift-card.js +++ b/dist/homeshift-card.js @@ -1,54 +1,54 @@ -function t(t,e,i,o){var s,n=arguments.length,r=n<3?e:null===o?o=Object.getOwnPropertyDescriptor(e,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,i,o);else for(var a=t.length-1;a>=0;a--)(s=t[a])&&(r=(n<3?s(r):n>3?s(e,i,r):s(e,i))||r);return n>3&&r&&Object.defineProperty(e,i,r),r}"function"==typeof SuppressedError&&SuppressedError; +function e(e,t,i,s){var o,n=arguments.length,r=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(e,t,i,s);else for(var a=e.length-1;a>=0;a--)(o=e[a])&&(r=(n<3?o(r):n>3?o(t,i,r):o(t,i))||r);return n>3&&r&&Object.defineProperty(t,i,r),r}"function"==typeof SuppressedError&&SuppressedError; /** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const e=globalThis,i=e.ShadowRoot&&(void 0===e.ShadyCSS||e.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,o=Symbol(),s=new WeakMap;let n=class{constructor(t,e,i){if(this._$cssResult$=!0,i!==o)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const e=this.t;if(i&&void 0===t){const i=void 0!==e&&1===e.length;i&&(t=s.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),i&&s.set(e,t))}return t}toString(){return this.cssText}};const r=(t,...e)=>{const i=1===t.length?t[0]:e.reduce((e,i,o)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[o+1],t[0]);return new n(i,t,o)},a=i?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new n("string"==typeof t?t:t+"",void 0,o))(e)})(t):t,{is:c,defineProperty:l,getOwnPropertyDescriptor:h,getOwnPropertyNames:d,getOwnPropertySymbols:p,getPrototypeOf:_}=Object,u=globalThis,m=u.trustedTypes,f=m?m.emptyScript:"",v=u.reactiveElementPolyfillSupport,g=(t,e)=>t,y={toAttribute(t,e){switch(e){case Boolean:t=t?f:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},$=(t,e)=>!c(t,e),b={attribute:!0,type:String,converter:y,reflect:!1,useDefault:!1,hasChanged:$}; +const t=globalThis,i=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s=Symbol(),o=new WeakMap;let n=class{constructor(e,t,i){if(this._$cssResult$=!0,i!==s)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(i&&void 0===e){const i=void 0!==t&&1===t.length;i&&(e=o.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),i&&o.set(t,e))}return e}toString(){return this.cssText}};const r=(e,...t)=>{const i=1===e.length?e[0]:t.reduce((t,i,s)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[s+1],e[0]);return new n(i,e,s)},a=i?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const i of e.cssRules)t+=i.cssText;return(e=>new n("string"==typeof e?e:e+"",void 0,s))(t)})(e):e,{is:c,defineProperty:h,getOwnPropertyDescriptor:l,getOwnPropertyNames:d,getOwnPropertySymbols:p,getPrototypeOf:u}=Object,_=globalThis,m=_.trustedTypes,v=m?m.emptyScript:"",f=_.reactiveElementPolyfillSupport,g=(e,t)=>e,y={toAttribute(e,t){switch(t){case Boolean:e=e?v:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let i=e;switch(t){case Boolean:i=null!==e;break;case Number:i=null===e?null:Number(e);break;case Object:case Array:try{i=JSON.parse(e)}catch(e){i=null}}return i}},b=(e,t)=>!c(e,t),$={attribute:!0,type:String,converter:y,reflect:!1,useDefault:!1,hasChanged:b}; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */Symbol.metadata??=Symbol("metadata"),u.litPropertyMetadata??=new WeakMap;let w=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=b){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const i=Symbol(),o=this.getPropertyDescriptor(t,i,e);void 0!==o&&l(this.prototype,t,o)}}static getPropertyDescriptor(t,e,i){const{get:o,set:s}=h(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:o,set(e){const n=o?.call(this);s?.call(this,e),this.requestUpdate(t,n,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(g("elementProperties")))return;const t=_(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(g("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(g("properties"))){const t=this.properties,e=[...d(t),...p(t)];for(const i of e)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,e]of this.elementProperties){const i=this._$Eu(t,e);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(a(t))}else void 0!==t&&e.push(a(t));return e}static _$Eu(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.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(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const i of e.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((t,o)=>{if(i)t.adoptedStyleSheets=o.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const i of o){const o=document.createElement("style"),s=e.litNonce;void 0!==s&&o.setAttribute("nonce",s),o.textContent=i.cssText,t.appendChild(o)}})(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ET(t,e){const i=this.constructor.elementProperties.get(t),o=this.constructor._$Eu(t,i);if(void 0!==o&&!0===i.reflect){const s=(void 0!==i.converter?.toAttribute?i.converter:y).toAttribute(e,i.type);this._$Em=t,null==s?this.removeAttribute(o):this.setAttribute(o,s),this._$Em=null}}_$AK(t,e){const i=this.constructor,o=i._$Eh.get(t);if(void 0!==o&&this._$Em!==o){const t=i.getPropertyOptions(o),s="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:y;this._$Em=o;const n=s.fromAttribute(e,t.type);this[o]=n??this._$Ej?.get(o)??n,this._$Em=null}}requestUpdate(t,e,i,o=!1,s){if(void 0!==t){const n=this.constructor;if(!1===o&&(s=this[t]),i??=n.getPropertyOptions(t),!((i.hasChanged??$)(s,e)||i.useDefault&&i.reflect&&s===this._$Ej?.get(t)&&!this.hasAttribute(n._$Eu(t,i))))return;this.C(t,e,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:i,reflect:o,wrapped:s},n){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,n??e??this[t]),!0!==s||void 0!==n)||(this._$AL.has(t)||(this.hasUpdated||i||(e=void 0),this._$AL.set(t,e)),!0===o&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,i]of t){const{wrapped:t}=i,o=this[e];!0!==t||this._$AL.has(e)||void 0===o||this.C(e,void 0,i,o)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}};w.elementStyles=[],w.shadowRootOptions={mode:"open"},w[g("elementProperties")]=new Map,w[g("finalized")]=new Map,v?.({ReactiveElement:w}),(u.reactiveElementVersions??=[]).push("2.1.2"); + */Symbol.metadata??=Symbol("metadata"),_.litPropertyMetadata??=new WeakMap;let x=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=$){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){const i=Symbol(),s=this.getPropertyDescriptor(e,i,t);void 0!==s&&h(this.prototype,e,s)}}static getPropertyDescriptor(e,t,i){const{get:s,set:o}=l(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get:s,set(t){const n=s?.call(this);o?.call(this,t),this.requestUpdate(e,n,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??$}static _$Ei(){if(this.hasOwnProperty(g("elementProperties")))return;const e=u(this);e.finalize(),void 0!==e.l&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(g("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(g("properties"))){const e=this.properties,t=[...d(e),...p(e)];for(const i of t)this.createProperty(i,e[i])}const e=this[Symbol.metadata];if(null!==e){const t=litPropertyMetadata.get(e);if(void 0!==t)for(const[e,i]of t)this.elementProperties.set(e,i)}this._$Eh=new Map;for(const[e,t]of this.elementProperties){const i=this._$Eu(e,t);void 0!==i&&this._$Eh.set(i,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const i=new Set(e.flat(1/0).reverse());for(const e of i)t.unshift(a(e))}else void 0!==e&&t.push(a(e));return t}static _$Eu(e,t){const i=t.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof e?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),void 0!==this.renderRoot&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const i of t.keys())this.hasOwnProperty(i)&&(e.set(i,this[i]),delete this[i]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((e,s)=>{if(i)e.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(const i of s){const s=document.createElement("style"),o=t.litNonce;void 0!==o&&s.setAttribute("nonce",o),s.textContent=i.cssText,e.appendChild(s)}})(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,i){this._$AK(e,i)}_$ET(e,t){const i=this.constructor.elementProperties.get(e),s=this.constructor._$Eu(e,i);if(void 0!==s&&!0===i.reflect){const o=(void 0!==i.converter?.toAttribute?i.converter:y).toAttribute(t,i.type);this._$Em=e,null==o?this.removeAttribute(s):this.setAttribute(s,o),this._$Em=null}}_$AK(e,t){const i=this.constructor,s=i._$Eh.get(e);if(void 0!==s&&this._$Em!==s){const e=i.getPropertyOptions(s),o="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==e.converter?.fromAttribute?e.converter:y;this._$Em=s;const n=o.fromAttribute(t,e.type);this[s]=n??this._$Ej?.get(s)??n,this._$Em=null}}requestUpdate(e,t,i,s=!1,o){if(void 0!==e){const n=this.constructor;if(!1===s&&(o=this[e]),i??=n.getPropertyOptions(e),!((i.hasChanged??b)(o,t)||i.useDefault&&i.reflect&&o===this._$Ej?.get(e)&&!this.hasAttribute(n._$Eu(e,i))))return;this.C(e,t,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(e,t,{useDefault:i,reflect:s,wrapped:o},n){i&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,n??t??this[e]),!0!==o||void 0!==n)||(this._$AL.has(e)||(this.hasUpdated||i||(t=void 0),this._$AL.set(e,t)),!0===s&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}const e=this.constructor.elementProperties;if(e.size>0)for(const[t,i]of e){const{wrapped:e}=i,s=this[t];!0!==e||this._$AL.has(t)||void 0===s||this.C(t,void 0,i,s)}}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(e=>e.hostUpdate?.()),this.update(t)):this._$EM()}catch(t){throw e=!1,this._$EM(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(e=>e.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(e=>this._$ET(e,this[e])),this._$EM()}updated(e){}firstUpdated(e){}};x.elementStyles=[],x.shadowRootOptions={mode:"open"},x[g("elementProperties")]=new Map,x[g("finalized")]=new Map,f?.({ReactiveElement:x}),(_.reactiveElementVersions??=[]).push("2.1.2"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const x=globalThis,E=t=>t,A=x.trustedTypes,S=A?A.createPolicy("lit-html",{createHTML:t=>t}):void 0,C="$lit$",k=`lit$${Math.random().toFixed(9).slice(2)}$`,T="?"+k,P=`<${T}>`,O=document,M=()=>O.createComment(""),N=t=>null===t||"object"!=typeof t&&"function"!=typeof t,R=Array.isArray,U="[ \t\n\f\r]",D=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,H=/-->/g,j=/>/g,z=RegExp(`>|${U}(?:([^\\s"'>=/]+)(${U}*=${U}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),I=/'/g,L=/"/g,B=/^(?:script|style|textarea|title)$/i,V=(t=>(e,...i)=>({_$litType$:t,strings:e,values:i}))(1),q=Symbol.for("lit-noChange"),W=Symbol.for("lit-nothing"),F=new WeakMap,K=O.createTreeWalker(O,129);function Y(t,e){if(!R(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==S?S.createHTML(e):e}const J=(t,e)=>{const i=t.length-1,o=[];let s,n=2===e?"":3===e?"":"",r=D;for(let e=0;e"===c[0]?(r=s??D,l=-1):void 0===c[1]?l=-2:(l=r.lastIndex-c[2].length,a=c[1],r=void 0===c[3]?z:'"'===c[3]?L:I):r===L||r===I?r=z:r===H||r===j?r=D:(r=z,s=void 0);const d=r===z&&t[e+1].startsWith("/>")?" ":"";n+=r===D?i+P:l>=0?(o.push(a),i.slice(0,l)+C+i.slice(l)+k+d):i+k+(-2===l?e:d)}return[Y(t,n+(t[i]||"")+(2===e?"":3===e?"":"")),o]};class Z{constructor({strings:t,_$litType$:e},i){let o;this.parts=[];let s=0,n=0;const r=t.length-1,a=this.parts,[c,l]=J(t,e);if(this.el=Z.createElement(c,i),K.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(o=K.nextNode())&&a.length0){o.textContent=A?A.emptyScript:"";for(let i=0;iR(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==W&&N(this._$AH)?this._$AA.nextSibling.data=t:this.T(O.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:i}=t,o="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=Z.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===o)this._$AH.p(e);else{const t=new Q(o,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=F.get(t.strings);return void 0===e&&F.set(t.strings,e=new Z(t)),e}k(t){R(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,o=0;for(const s of t)o===e.length?e.push(i=new X(this.O(M()),this.O(M()),this,this.options)):i=e[o],i._$AI(s),o++;o2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=W}_$AI(t,e=this,i,o){const s=this.strings;let n=!1;if(void 0===s)t=G(this,t,e,0),n=!N(t)||t!==this._$AH&&t!==q,n&&(this._$AH=t);else{const o=t;let r,a;for(t=s[0],r=0;re,S=E.trustedTypes,A=S?S.createPolicy("lit-html",{createHTML:e=>e}):void 0,C="$lit$",k=`lit$${Math.random().toFixed(9).slice(2)}$`,T="?"+k,P=`<${T}>`,I=document,O=()=>I.createComment(""),N=e=>null===e||"object"!=typeof e&&"function"!=typeof e,M=Array.isArray,R="[ \t\n\f\r]",D=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,H=/-->/g,U=/>/g,j=RegExp(`>|${R}(?:([^\\s"'>=/]+)(${R}*=${R}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),z=/'/g,L=/"/g,B=/^(?:script|style|textarea|title)$/i,q=(e=>(t,...i)=>({_$litType$:e,strings:t,values:i}))(1),V=Symbol.for("lit-noChange"),W=Symbol.for("lit-nothing"),F=new WeakMap,K=I.createTreeWalker(I,129);function Y(e,t){if(!M(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==A?A.createHTML(t):t}const J=(e,t)=>{const i=e.length-1,s=[];let o,n=2===t?"":3===t?"":"",r=D;for(let t=0;t"===c[0]?(r=o??D,h=-1):void 0===c[1]?h=-2:(h=r.lastIndex-c[2].length,a=c[1],r=void 0===c[3]?j:'"'===c[3]?L:z):r===L||r===z?r=j:r===H||r===U?r=D:(r=j,o=void 0);const d=r===j&&e[t+1].startsWith("/>")?" ":"";n+=r===D?i+P:h>=0?(s.push(a),i.slice(0,h)+C+i.slice(h)+k+d):i+k+(-2===h?t:d)}return[Y(e,n+(e[i]||"")+(2===t?"":3===t?"":"")),s]};class X{constructor({strings:e,_$litType$:t},i){let s;this.parts=[];let o=0,n=0;const r=e.length-1,a=this.parts,[c,h]=J(e,t);if(this.el=X.createElement(c,i),K.currentNode=this.el.content,2===t||3===t){const e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;null!==(s=K.nextNode())&&a.length0){s.textContent=S?S.emptyScript:"";for(let i=0;iM(e)||"function"==typeof e?.[Symbol.iterator])(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!==W&&N(this._$AH)?this._$AA.nextSibling.data=e:this.T(I.createTextNode(e)),this._$AH=e}$(e){const{values:t,_$litType$:i}=e,s="number"==typeof i?this._$AC(e):(void 0===i.el&&(i.el=X.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(t);else{const e=new G(s,this),i=e.u(this.options);e.p(t),this.T(i),this._$AH=e}}_$AC(e){let t=F.get(e.strings);return void 0===t&&F.set(e.strings,t=new X(e)),t}k(e){M(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let i,s=0;for(const o of e)s===t.length?t.push(i=new Q(this.O(O()),this.O(O()),this,this.options)):i=t[s],i._$AI(o),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=W}_$AI(e,t=this,i,s){const o=this.strings;let n=!1;if(void 0===o)e=Z(this,e,t,0),n=!N(e)||e!==this._$AH&&e!==V,n&&(this._$AH=e);else{const s=e;let r,a;for(e=o[0],r=0;r{const o=i?.renderBefore??e;let s=o._$litPart$;if(void 0===s){const t=i?.renderBefore??null;o._$litPart$=s=new X(e.insertBefore(M(),t),t,void 0,i??{})}return s._$AI(t),s})(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return q}}at._$litElement$=!0,at.finalized=!0,rt.litElementHydrateSupport?.({LitElement:at});const ct=rt.litElementPolyfillSupport;ct?.({LitElement:at}),(rt.litElementVersions??=[]).push("4.2.2"); + */class ae extends x{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){const t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=((e,t,i)=>{const s=i?.renderBefore??t;let o=s._$litPart$;if(void 0===o){const e=i?.renderBefore??null;s._$litPart$=o=new Q(t.insertBefore(O(),e),e,void 0,i??{})}return o._$AI(e),o})(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return V}}ae._$litElement$=!0,ae.finalized=!0,re.litElementHydrateSupport?.({LitElement:ae});const ce=re.litElementPolyfillSupport;ce?.({LitElement:ae}),(re.litElementVersions??=[]).push("4.2.2"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const lt={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:$},ht=(t=lt,e,i)=>{const{kind:o,metadata:s}=i;let n=globalThis.litPropertyMetadata.get(s);if(void 0===n&&globalThis.litPropertyMetadata.set(s,n=new Map),"setter"===o&&((t=Object.create(t)).wrapped=!0),n.set(i.name,t),"accessor"===o){const{name:o}=i;return{set(i){const s=e.get.call(this);e.set.call(this,i),this.requestUpdate(o,s,t,!0,i)},init(e){return void 0!==e&&this.C(o,void 0,t,e),e}}}if("setter"===o){const{name:o}=i;return function(i){const s=this[o];e.call(this,i),this.requestUpdate(o,s,t,!0,i)}}throw Error("Unsupported decorator location: "+o)};function dt(t){return(e,i)=>"object"==typeof i?ht(t,e,i):((t,e,i)=>{const o=e.hasOwnProperty(i);return e.constructor.createProperty(i,t),o?Object.getOwnPropertyDescriptor(e,i):void 0})(t,e,i)} +const he={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:b},le=(e=he,t,i)=>{const{kind:s,metadata:o}=i;let n=globalThis.litPropertyMetadata.get(o);if(void 0===n&&globalThis.litPropertyMetadata.set(o,n=new Map),"setter"===s&&((e=Object.create(e)).wrapped=!0),n.set(i.name,e),"accessor"===s){const{name:s}=i;return{set(i){const o=t.get.call(this);t.set.call(this,i),this.requestUpdate(s,o,e,!0,i)},init(t){return void 0!==t&&this.C(s,void 0,e,t),t}}}if("setter"===s){const{name:s}=i;return function(i){const o=this[s];t.call(this,i),this.requestUpdate(s,o,e,!0,i)}}throw Error("Unsupported decorator location: "+s)};function de(e){return(t,i)=>"object"==typeof i?le(e,t,i):((e,t,i)=>{const s=t.hasOwnProperty(i);return t.constructor.createProperty(i,e),s?Object.getOwnPropertyDescriptor(t,i):void 0})(e,t,i)} /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */function pt(t){return dt({...t,state:!0,attribute:!1})}const _t={en:{card:{entity_not_found:"Entity not found: {entity}",unavailable:"Unavailable",today:"Today",tomorrow:"Tomorrow",heat_protection_active:"Heat protection active",cover_open_time:"Cover opening time",cover_close_time:"Cover closing time",cover_open_action:"Open the covers now (scheduled {time})",cover_close_action:"Close the covers now (scheduled {time})",cover_sending:"Sending…",cover_open_failed:"Could not open the covers: {error}",cover_close_failed:"Could not close the covers: {error}",covers_left_open:"Not closed: {covers}",thermostat_mode:"Thermostat mode",day_mode:"Day mode",next:"Next",covers:"Covers",duration_off:"No",decrease:"Decrease: {setting}",increase:"Increase: {setting}",override_label:"Keep my manual choice",override_none:"A manual change is replaced at the calendar's next pass.",override_effect:"After a change by hand, the calendar waits {duration} — until {time} if you change it now.",early_label:"Start events early",early_none:"{mode} will start at its scheduled time, {time}.",early_effect:"{mode} will start at {time} instead of {scheduled}.",early_no_event:"No event scheduled.",covers_left_open_count:"Not closed: {count} covers"},thermostat:{off:"Off",heating:"Heating",cooling:"Cooling",ventilation:"Ventilation"},editor:{name:"Name",day_mode_entity:"Day Mode Entity",thermostat_mode_entity:"Thermostat Mode Entity",override_duration_entity:"Override Duration Entity",early_switch_entity:"Early Switch Entity",next_mode_entity:"Next Mode Entity",next_mode_at_entity:"Next Mode Time Entity",heat_protection_entity:"Heat Protection Entity",cover_open_time_entity:"Cover Open Time Entity",cover_close_time_entity:"Cover Close Time Entity",open_covers_entity:"Open Covers Button",close_covers_entity:"Close Covers Button",cover_entity:"Cover Entity (legacy, overrides the buttons)",covers_left_open_entity:"Covers Left Open Entity"},preview:{home:"Home",work:"Work",remote:"Remote",away:"Away"}},fr:{card:{entity_not_found:"Entité introuvable: {entity}",unavailable:"Indisponible",today:"Auj.",tomorrow:"Dem.",heat_protection_active:"Protection thermique active",cover_open_time:"Heure d'ouverture des volets",cover_close_time:"Heure de fermeture des volets",cover_open_action:"Ouvrir les volets maintenant (prévu à {time})",cover_close_action:"Fermer les volets maintenant (prévu à {time})",cover_sending:"Envoi…",cover_open_failed:"Impossible d'ouvrir les volets : {error}",cover_close_failed:"Impossible de fermer les volets : {error}",covers_left_open:"Non fermés : {covers}",thermostat_mode:"Mode thermostat",day_mode:"Mode du jour",next:"Prochain",covers:"Volets",duration_off:"Non",decrease:"Diminuer : {setting}",increase:"Augmenter : {setting}",override_label:"Garder mon choix manuel",override_none:"Un changement manuel est remplacé au prochain passage du calendrier.",override_effect:"Après un changement à la main, le calendrier attend {duration} — jusqu'à {time} si tu changes maintenant.",early_label:"Anticiper les événements",early_none:"{mode} commencera à l'heure prévue, {time}.",early_effect:"{mode} commencera à {time} au lieu de {scheduled}.",early_no_event:"Aucun événement prévu.",covers_left_open_count:"Non fermés : {count} volets"},thermostat:{off:"Éteint",heating:"Chauffage",cooling:"Climatisation",ventilation:"Ventilation"},editor:{name:"Nom",day_mode_entity:"Entité mode jour",thermostat_mode_entity:"Entité mode thermostat",override_duration_entity:"Entité durée de dérogation",early_switch_entity:"Entité anticipation",next_mode_entity:"Entité prochain mode",next_mode_at_entity:"Entité heure du prochain mode",heat_protection_entity:"Entité protection thermique",cover_open_time_entity:"Entité heure d'ouverture des volets",cover_close_time_entity:"Entité heure de fermeture des volets",open_covers_entity:"Bouton d'ouverture des volets",close_covers_entity:"Bouton de fermeture des volets",cover_entity:"Entité volet (ancienne option, prioritaire sur les boutons)",covers_left_open_entity:"Entité volets non fermés"},preview:{home:"Maison",work:"Travail",remote:"Télétravail",away:"Absence"}}};function ut(t,e){return e.split(".").reduce((t,e)=>t&&null!=t[e]?t[e]:void 0,t)}function mt(t,e,i,o){const s=function(t){const e=t?.locale?.language||t?.language||"en",i=String(e).split("-")[0];return _t[e]?e:_t[i]?i:"en"}(t);let n=ut(_t[s],e)??ut(_t.en,e)??o;return null==n?e:(i&&Object.entries(i).forEach(([t,e])=>n=n.replace(`{${t}}`,e)),n)}const ft=[{key:"day_mode_entity",domains:["select","input_select"],default:"select.homeshift_day_mode"},{key:"thermostat_mode_entity",domains:["select","input_select"],default:"select.homeshift_thermostat_mode"},{key:"override_duration_entity",domains:["number","input_number"],default:"number.homeshift_override_duration"},{key:"early_switch_entity",domains:["number","input_number"],default:"number.homeshift_early_switch"},{key:"next_mode_entity",domains:["sensor"],default:"sensor.homeshift_next_mode"},{key:"next_mode_at_entity",domains:["sensor"],default:"sensor.homeshift_next_mode_at"},{key:"heat_protection_entity",domains:["binary_sensor"],default:"binary_sensor.homeshift_cover_heat_active"},{key:"cover_open_time_entity",domains:["sensor"],default:"sensor.homeshift_cover_open_time"},{key:"cover_close_time_entity",domains:["sensor"],default:"sensor.homeshift_cover_close_time"},{key:"open_covers_entity",domains:["button"],default:"button.homeshift_open_covers"},{key:"close_covers_entity",domains:["button"],default:"button.homeshift_close_covers"},{key:"cover_entity",domains:["cover"],default:""},{key:"covers_left_open_entity",domains:["binary_sensor"],default:"binary_sensor.homeshift_covers_left_open"}];class vt extends at{setConfig(t){this._config=t}_onNameChanged(t){if(!this._config||!this.hass)return;const e=t.target.value;if(this._config.name===e)return;const i={...this._config,name:e};this._config=i,this._dispatchConfigChanged(i)}_onEntityChanged(t,e){if(!this._config||!this.hass)return;const i=t.detail.value;if(this._config[e]===i)return;const o={...this._config,[e]:i};this._config=o,this._dispatchConfigChanged(o)}_dispatchConfigChanged(t){const e=new CustomEvent("config-changed",{detail:{config:t},bubbles:!0,composed:!0});this.dispatchEvent(e)}render(){return this.hass&&this._config?V` + */function pe(e){return de({...e,state:!0,attribute:!1})}const ue={en:{card:{entity_not_found:"Entity not found: {entity}",unavailable:"Unavailable",today:"Today",tomorrow:"Tomorrow",heat_protection_active:"Heat protection active",cover_open_time:"Cover opening time",cover_close_time:"Cover closing time",cover_open_action:"Open the covers now (scheduled {time})",cover_close_action:"Close the covers now (scheduled {time})",cover_sending:"Sending…",cover_open_failed:"Could not open the covers: {error}",cover_close_failed:"Could not close the covers: {error}",covers_left_open:"Not closed: {covers}",thermostat_mode:"Thermostat mode",day_mode:"Day mode",next:"Next",covers:"Covers",duration_off:"No",decrease:"Decrease: {setting}",increase:"Increase: {setting}",override_label:"Keep my manual choice",override_none:"A manual change is replaced at the calendar's next pass.",override_effect:"After a change by hand, the calendar waits {duration} — until {time} if you change it now.",early_label:"Start events early",early_none:"{mode} will start at its scheduled time, {time}.",early_effect:"{mode} will start at {time} instead of {scheduled}.",early_no_event:"No event scheduled.",covers_left_open_count:"Not closed: {count} covers",inhibit_label:"Cover automation",inhibit_none:"Auto",inhibited_count:"{count} paused",inhibit_forever:"Until resumed",days_short:"d",inhibit_effect:"Pause for {duration} — until {time} if you start it now.",inhibit_effect_forever:"The cover stays paused until you resume it.",inhibited_until:"Paused until {time}",inhibited_forever:"Paused until resumed",pause:"Pause",resume:"Resume",resume_all:"Resume all",covers_paused:"Paused: {covers}",covers_paused_count:"{count} covers paused",inhibit_failed:"Could not pause or resume the cover: {error}"},thermostat:{off:"Off",heating:"Heating",cooling:"Cooling",ventilation:"Ventilation"},editor:{name:"Name",day_mode_entity:"Day Mode Entity",thermostat_mode_entity:"Thermostat Mode Entity",override_duration_entity:"Override Duration Entity",early_switch_entity:"Early Switch Entity",next_mode_entity:"Next Mode Entity",next_mode_at_entity:"Next Mode Time Entity",heat_protection_entity:"Heat Protection Entity",cover_open_time_entity:"Cover Open Time Entity",cover_close_time_entity:"Cover Close Time Entity",open_covers_entity:"Open Covers Button",close_covers_entity:"Close Covers Button",cover_entity:"Cover Entity (legacy, overrides the buttons)",covers_left_open_entity:"Covers Left Open Entity",covers_inhibited_entity:"Paused covers entity"},preview:{home:"Home",work:"Work",remote:"Remote",away:"Away"}},fr:{card:{entity_not_found:"Entité introuvable: {entity}",unavailable:"Indisponible",today:"Auj.",tomorrow:"Dem.",heat_protection_active:"Protection thermique active",cover_open_time:"Heure d'ouverture des volets",cover_close_time:"Heure de fermeture des volets",cover_open_action:"Ouvrir les volets maintenant (prévu à {time})",cover_close_action:"Fermer les volets maintenant (prévu à {time})",cover_sending:"Envoi…",cover_open_failed:"Impossible d'ouvrir les volets : {error}",cover_close_failed:"Impossible de fermer les volets : {error}",covers_left_open:"Non fermés : {covers}",thermostat_mode:"Mode thermostat",day_mode:"Mode du jour",next:"Prochain",covers:"Volets",duration_off:"Non",decrease:"Diminuer : {setting}",increase:"Augmenter : {setting}",override_label:"Garder mon choix manuel",override_none:"Un changement manuel est remplacé au prochain passage du calendrier.",override_effect:"Après un changement à la main, le calendrier attend {duration} — jusqu'à {time} si tu changes maintenant.",early_label:"Anticiper les événements",early_none:"{mode} commencera à l'heure prévue, {time}.",early_effect:"{mode} commencera à {time} au lieu de {scheduled}.",early_no_event:"Aucun événement prévu.",covers_left_open_count:"Non fermés : {count} volets",inhibit_label:"Pilotage auto des volets",inhibit_none:"Auto",inhibited_count:"{count} en pause",inhibit_forever:"Jusqu'à reprise",days_short:"j",inhibit_effect:"Pause de {duration} — jusqu'à {time} si tu l'actives maintenant.",inhibit_effect_forever:"Le volet reste en pause jusqu'à ce que tu le reprennes.",inhibited_until:"En pause jusqu'à {time}",inhibited_forever:"En pause jusqu'à reprise",pause:"Pause",resume:"Reprendre",resume_all:"Tout reprendre",covers_paused:"En pause : {covers}",covers_paused_count:"{count} volets en pause",inhibit_failed:"Échec de la mise en pause : {error}"},thermostat:{off:"Éteint",heating:"Chauffage",cooling:"Climatisation",ventilation:"Ventilation"},editor:{name:"Nom",day_mode_entity:"Entité mode jour",thermostat_mode_entity:"Entité mode thermostat",override_duration_entity:"Entité durée de dérogation",early_switch_entity:"Entité anticipation",next_mode_entity:"Entité prochain mode",next_mode_at_entity:"Entité heure du prochain mode",heat_protection_entity:"Entité protection thermique",cover_open_time_entity:"Entité heure d'ouverture des volets",cover_close_time_entity:"Entité heure de fermeture des volets",open_covers_entity:"Bouton d'ouverture des volets",close_covers_entity:"Bouton de fermeture des volets",cover_entity:"Entité volet (ancienne option, prioritaire sur les boutons)",covers_left_open_entity:"Entité volets non fermés",covers_inhibited_entity:"Entité volets en pause"},preview:{home:"Maison",work:"Travail",remote:"Télétravail",away:"Absence"}}};function _e(e,t){return t.split(".").reduce((e,t)=>e&&null!=e[t]?e[t]:void 0,e)}function me(e,t,i,s){const o=function(e){const t=e?.locale?.language||e?.language||"en",i=String(t).split("-")[0];return ue[t]?t:ue[i]?i:"en"}(e);let n=_e(ue[o],t)??_e(ue.en,t)??s;return null==n?t:(i&&Object.entries(i).forEach(([e,t])=>n=n.replace(`{${e}}`,t)),n)}const ve=[{key:"day_mode_entity",domains:["select","input_select"],default:"select.homeshift_day_mode"},{key:"thermostat_mode_entity",domains:["select","input_select"],default:"select.homeshift_thermostat_mode"},{key:"override_duration_entity",domains:["number","input_number"],default:"number.homeshift_override_duration"},{key:"early_switch_entity",domains:["number","input_number"],default:"number.homeshift_early_switch"},{key:"next_mode_entity",domains:["sensor"],default:"sensor.homeshift_next_mode"},{key:"next_mode_at_entity",domains:["sensor"],default:"sensor.homeshift_next_mode_at"},{key:"heat_protection_entity",domains:["binary_sensor"],default:"binary_sensor.homeshift_cover_heat_active"},{key:"cover_open_time_entity",domains:["sensor"],default:"sensor.homeshift_cover_open_time"},{key:"cover_close_time_entity",domains:["sensor"],default:"sensor.homeshift_cover_close_time"},{key:"open_covers_entity",domains:["button"],default:"button.homeshift_open_covers"},{key:"close_covers_entity",domains:["button"],default:"button.homeshift_close_covers"},{key:"cover_entity",domains:["cover"],default:""},{key:"covers_left_open_entity",domains:["binary_sensor"],default:"binary_sensor.homeshift_covers_left_open"},{key:"covers_inhibited_entity",domains:["sensor"],default:"sensor.homeshift_covers_inhibited"}];class fe extends ae{setConfig(e){this._config=e}_onNameChanged(e){if(!this._config||!this.hass)return;const t=e.target.value;if(this._config.name===t)return;const i={...this._config,name:t};this._config=i,this._dispatchConfigChanged(i)}_onEntityChanged(e,t){if(!this._config||!this.hass)return;const i=e.detail.value;if(this._config[t]===i)return;const s={...this._config,[t]:i};this._config=s,this._dispatchConfigChanged(s)}_dispatchConfigChanged(e){const t=new CustomEvent("config-changed",{detail:{config:e},bubbles:!0,composed:!0});this.dispatchEvent(t)}render(){return this.hass&&this._config?q`
- ${ft.map(t=>V`q`this._onEntityChanged(e,t.key)} + .value=${this._config[e.key]||""} + .includeDomains=${e.domains} + @value-changed=${t=>this._onEntityChanged(t,e.key)} allow-custom-entity >`)}
- `:V``}}vt.styles=r` + `:q``}}fe.styles=r` .card-config { display: flex; flex-direction: column; @@ -60,41 +60,102 @@ const lt={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:$},ht=(t=lt ha-entity-picker { width: 100%; } - `,t([dt({attribute:!1})],vt.prototype,"hass",void 0),t([pt()],vt.prototype,"_config",void 0),customElements.get("homeshift-card-editor")||customElements.define("homeshift-card-editor",vt);const gt=new Set(["unknown","unavailable",""]);function yt(t){const e="string"==typeof t?t:t?.state;return"string"==typeof e&&!gt.has(e)}function $t(t){const e=t?.time_format;return{locale:"system"===e?void 0:t?.language,hour12:"12"===e||"24"!==e&&void 0}}function bt(t,e){const{locale:i,hour12:o}=$t(e);return t.toLocaleTimeString(i,{hour:"2-digit",minute:"2-digit",hour12:o})}class wt extends at{constructor(){super(...arguments),this.preview=!1,this._openSetting=null,this._coversExpanded=!1,this._day=0}connectedCallback(){super.connectedCallback(),this._scheduleMidnightRefresh()}disconnectedCallback(){super.disconnectedCallback(),clearTimeout(this._midnightTimer),clearTimeout(this._settingTimeout)}_scheduleMidnightRefresh(){clearTimeout(this._midnightTimer);const t=new Date,e=new Date(t);e.setHours(24,0,1,0),this._midnightTimer=setTimeout(()=>{this._day++,this._scheduleMidnightRefresh()},e.getTime()-t.getTime())}static getStubConfig(){return{name:"Thermostat",day_mode_entity:"select.homeshift_day_mode",thermostat_mode_entity:"select.homeshift_thermostat_mode",override_duration_entity:"number.homeshift_override_duration",early_switch_entity:"number.homeshift_early_switch",next_mode_entity:"sensor.homeshift_next_mode",next_mode_at_entity:"sensor.homeshift_next_mode_at",heat_protection_entity:"binary_sensor.homeshift_cover_heat_active",cover_open_time_entity:"sensor.homeshift_cover_open_time",cover_close_time_entity:"sensor.homeshift_cover_close_time",covers_left_open_entity:"binary_sensor.homeshift_covers_left_open"}}static getConfigElement(){return document.createElement("homeshift-card-editor")}setConfig(t){if(!t)throw new Error("Missing configuration");const e={name:t.name??"Thermostat"};for(const i of ft)e[i.key]=t[i.key]??i.default;this._config=e}getCardSize(){return 4}shouldUpdate(t){if(t.has("_config")||t.has("preview"))return!0;if(t.has("_day")||t.has("_openSetting")||t.has("_coversExpanded")||t.has("_coverPending"))return!0;if(t.has("hass")){const e=t.get("hass");if(!e)return!0;return ft.map(t=>this._config?.[t.key]).filter(Boolean).some(t=>e.states[t]!==this.hass.states[t])}return!1}_presetClass(t,e){const i=t=>t.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g,"").replace(/[^a-z0-9]+/g,"-");if(["off","heating","cooling","ventilation"].includes(i(t)))return i(t);return{eteint:"off",arret:"off",off:"off",chauffage:"heating",heat:"heating",heating:"heating",climatisation:"cooling",clim:"cooling",cool:"cooling",cooling:"cooling",ventilation:"ventilation",ventilateur:"ventilation",fan:"ventilation"}[i(e)]??i(t)}getEntityState(t){if(t)return this.hass?.states?.[t]}getCoversLeftOpen(){const t=this.getEntityState(this._config?.covers_left_open_entity)?.attributes?.covers;return Array.isArray(t)?t.map(t=>this.hass?.states?.[t]?.attributes?.friendly_name??t):[]}onSelect(t,e){const i=e.target,o=i?.value;o&&this.hass.callService("select","select_option",{entity_id:t,option:o})}onThermostatSelect(t,e){this.hass.callService("select","select_option",{entity_id:t,option:e})}_coverCall(t){const e=this._config.cover_entity;if(e)return yt(this.getEntityState(e))?{domain:"cover",service:`${t}_cover`,entity_id:e}:void 0;const i="open"===t?this._config.open_covers_entity:this._config.close_covers_entity,o=this.getEntityState(i);return o&&"unavailable"!==o.state?{domain:"button",service:"press",entity_id:i}:void 0}async onCoverAction(t){const e=this._coverCall(t);if(e&&!this._coverPending){this._coverPending=t;try{await Promise.all([this.hass.callService(e.domain,e.service,{entity_id:e.entity_id}),new Promise(t=>setTimeout(t,wt.COVER_FEEDBACK_MS))])}catch(e){this.dispatchEvent(new CustomEvent("hass-notification",{detail:{message:mt(this.hass,`card.cover_${t}_failed`,{error:e?.message??String(e)})},bubbles:!0,composed:!0}))}finally{this._coverPending=void 0}}}_renderCoverTime(t,e){const i="open"===t?"mdi:roller-shade":"mdi:roller-shade-closed";if(!this._coverCall(t))return V`0?`${t}h${String(i).padStart(2,"0")}`:`${t}h`}function $e(e){const t=e?.time_format;return{locale:"system"===t?void 0:e?.language,hour12:"12"===t||"24"!==t&&void 0}}function xe(e,t){const{locale:i,hour12:s}=$e(t);return e.toLocaleTimeString(i,{hour:"2-digit",minute:"2-digit",hour12:s})}class Ee extends ae{constructor(){super(...arguments),this.preview=!1,this._openSetting=null,this._inhibitIndex=Ee.INHIBIT_DEFAULT_INDEX,this._coversExpanded=!1,this._day=0}connectedCallback(){super.connectedCallback(),this._scheduleMidnightRefresh()}disconnectedCallback(){super.disconnectedCallback(),clearTimeout(this._midnightTimer),clearTimeout(this._settingTimeout)}_scheduleMidnightRefresh(){clearTimeout(this._midnightTimer);const e=new Date,t=new Date(e);t.setHours(24,0,1,0),this._midnightTimer=setTimeout(()=>{this._day++,this._scheduleMidnightRefresh()},t.getTime()-e.getTime())}static getStubConfig(){return{name:"Thermostat",day_mode_entity:"select.homeshift_day_mode",thermostat_mode_entity:"select.homeshift_thermostat_mode",override_duration_entity:"number.homeshift_override_duration",early_switch_entity:"number.homeshift_early_switch",next_mode_entity:"sensor.homeshift_next_mode",next_mode_at_entity:"sensor.homeshift_next_mode_at",heat_protection_entity:"binary_sensor.homeshift_cover_heat_active",cover_open_time_entity:"sensor.homeshift_cover_open_time",cover_close_time_entity:"sensor.homeshift_cover_close_time",covers_left_open_entity:"binary_sensor.homeshift_covers_left_open",covers_inhibited_entity:"sensor.homeshift_covers_inhibited"}}static getConfigElement(){return document.createElement("homeshift-card-editor")}setConfig(e){if(!e)throw new Error("Missing configuration");const t={name:e.name??"Thermostat"};for(const i of ve)t[i.key]=e[i.key]??i.default;this._config=t}getCardSize(){return 4}shouldUpdate(e){if(e.has("_config")||e.has("preview"))return!0;if(e.has("_day")||e.has("_openSetting")||e.has("_inhibitIndex")||e.has("_coversExpanded")||e.has("_coverPending"))return!0;if(e.has("hass")){const t=e.get("hass");if(!t)return!0;return ve.map(e=>this._config?.[e.key]).filter(Boolean).some(e=>t.states[e]!==this.hass.states[e])}return!1}_presetClass(e,t){const i=e=>e.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g,"").replace(/[^a-z0-9]+/g,"-");if(["off","heating","cooling","ventilation"].includes(i(e)))return i(e);return{eteint:"off",arret:"off",off:"off",chauffage:"heating",heat:"heating",heating:"heating",climatisation:"cooling",clim:"cooling",cool:"cooling",cooling:"cooling",ventilation:"ventilation",ventilateur:"ventilation",fan:"ventilation"}[i(t)]??i(e)}getEntityState(e){if(e)return this.hass?.states?.[e]}getCoversLeftOpen(){const e=this.getEntityState(this._config?.covers_left_open_entity)?.attributes?.covers;return Array.isArray(e)?e.map(e=>this.hass?.states?.[e]?.attributes?.friendly_name??e):[]}_friendlyName(e){return this.hass?.states?.[e]?.attributes?.friendly_name??e}getCoverInhibitions(){const e=this.getEntityState(this._config?.covers_inhibited_entity)?.attributes??{},t=Array.isArray(e.managed_covers)?e.managed_covers:[],i=new Map,s=Date.now();for(const[t,o]of Object.entries(e.covers??{}))"string"==typeof o&&new Date(o).getTime()<=s||i.set(t,"string"==typeof o?o:null);return{managed:t,inhibited:i}}async _callInhibitService(e,t){this._armSettingTimeout();try{await this.hass.callService("homeshift",e,t)}catch(e){this.dispatchEvent(new CustomEvent("hass-notification",{detail:{message:me(this.hass,"card.inhibit_failed",{error:e?.message??String(e)})},bubbles:!0,composed:!0}))}}_inhibitCover(e){const t=Ee.INHIBIT_PRESETS[this._inhibitIndex];this._callInhibitService("inhibit_covers",{entity_id:[e],...t?{duration:{minutes:t}}:{}})}_resumeCovers(e){this._callInhibitService("resume_covers",e?{entity_id:e}:{})}_stepInhibit(e){const t=Ee.INHIBIT_PRESETS.length-1;this._inhibitIndex=Math.min(t,Math.max(0,this._inhibitIndex+e)),this._armSettingTimeout()}_inhibitLabel(e){return e?e<1440?be(e):`${e/1440} ${me(this.hass,"card.days_short")}`:me(this.hass,"card.inhibit_forever")}_inhibitEnd(e){return e?me(this.hass,"card.inhibited_until",{time:this._formatAbsoluteTime(e)}):me(this.hass,"card.inhibited_forever")}_renderCoverInhibition({managed:e,inhibited:t}){const i="covers"===this._openSetting,s=Ee.INHIBIT_PRESETS,o=s[this._inhibitIndex],n=me(this.hass,"card.inhibit_label"),r=o?me(this.hass,"card.inhibit_effect",{duration:this._inhibitLabel(o),time:this._formatAbsoluteTime(new Date(Date.now()+6e4*o).toISOString())}):me(this.hass,"card.inhibit_effect_forever");return q`
+ + ${i?q`
+
+ + ${this._inhibitLabel(o)} + +
+

${r}

+
    + ${e.map(e=>{const i=t.has(e),s=this._friendlyName(e),o=me(this.hass,i?"card.resume":"card.pause");return q`
  • + + ${s} + ${i?this._inhibitEnd(t.get(e)??null):me(this.hass,"card.inhibit_none")} + + +
  • `})} +
+ ${t.size>1?q``:W} +
`:W} +
`}onSelect(e,t){const i=t.target,s=i?.value;s&&this.hass.callService("select","select_option",{entity_id:e,option:s})}onThermostatSelect(e,t){this.hass.callService("select","select_option",{entity_id:e,option:t})}_coverCall(e){const t=this._config.cover_entity;if(t)return ye(this.getEntityState(t))?{domain:"cover",service:`${e}_cover`,entity_id:t}:void 0;const i="open"===e?this._config.open_covers_entity:this._config.close_covers_entity,s=this.getEntityState(i);return s&&"unavailable"!==s.state?{domain:"button",service:"press",entity_id:i}:void 0}async onCoverAction(e){const t=this._coverCall(e);if(t&&!this._coverPending){this._coverPending=e;try{await Promise.all([this.hass.callService(t.domain,t.service,{entity_id:t.entity_id}),new Promise(e=>setTimeout(e,Ee.COVER_FEEDBACK_MS))])}catch(t){this.dispatchEvent(new CustomEvent("hass-notification",{detail:{message:me(this.hass,`card.cover_${e}_failed`,{error:t?.message??String(t)})},bubbles:!0,composed:!0}))}finally{this._coverPending=void 0}}}_renderCoverTime(e,t){const i="open"===e?"mdi:roller-shade":"mdi:roller-shade-closed";if(!this._coverCall(e))return q` - ${e} - `;const o=this._coverPending===t,s=mt(this.hass,`card.cover_${t}_action`,{time:e});return V``}_formatAbsoluteTime(t){if(!t)return"";const e=new Date(t);if(isNaN(e.getTime()))return"";const i=new Date,o=new Date(i);o.setDate(o.getDate()+1);const s=bt(e,this.hass?.locale);return e.toDateString()===i.toDateString()?`${mt(this.hass,"card.today")} ${s}`:e.toDateString()===o.toDateString()?`${mt(this.hass,"card.tomorrow")} ${s}`:`${function(t,e){const{locale:i}=$t(e);return t.toLocaleDateString(i,{month:"short",day:"numeric"})}(e,this.hass?.locale)} ${s}`}_settingLabel(t){return void 0===t?mt(this.hass,"card.unavailable"):t?function(t){if(t<60)return`${t} min`;const e=Math.floor(t/60),i=Math.round(t%60);return i>0?`${e}h${String(i).padStart(2,"0")}`:`${e}h`}(t):mt(this.hass,"card.duration_off")}_clockIn(t){return bt(new Date(Date.now()+6e4*t),this.hass?.locale)}_clockAt(t,e=0){return bt(new Date(new Date(t).getTime()+6e4*e),this.hass?.locale)}_toggleSetting(t){this._openSetting=this._openSetting===t?null:t,this._armSettingTimeout()}_armSettingTimeout(){clearTimeout(this._settingTimeout),this._openSetting&&(this._settingTimeout=setTimeout(()=>{this._openSetting=null},6e3))}_stepSetting(t,e,i,o){const s="override"===t?this._config.override_duration_entity:this._config.early_switch_entity;if(!s)return;const n=o>0?e.find(t=>t>i):[...e].reverse().find(t=>t + ${s?me(this.hass,"card.cover_sending"):t} + `}_formatAbsoluteTime(e){if(!e)return"";const t=new Date(e);if(isNaN(t.getTime()))return"";const i=new Date,s=new Date(i);s.setDate(s.getDate()+1);const o=xe(t,this.hass?.locale);return t.toDateString()===i.toDateString()?`${me(this.hass,"card.today")} ${o}`:t.toDateString()===s.toDateString()?`${me(this.hass,"card.tomorrow")} ${o}`:`${function(e,t){const{locale:i}=$e(t);return e.toLocaleDateString(i,{month:"short",day:"numeric"})}(t,this.hass?.locale)} ${o}`}_settingLabel(e){return void 0===e?me(this.hass,"card.unavailable"):e?be(e):me(this.hass,"card.duration_off")}_clockIn(e){return xe(new Date(Date.now()+6e4*e),this.hass?.locale)}_clockAt(e,t=0){return xe(new Date(new Date(e).getTime()+6e4*t),this.hass?.locale)}_toggleSetting(e){this._openSetting=this._openSetting===e?null:e,this._armSettingTimeout()}_armSettingTimeout(){if(clearTimeout(this._settingTimeout),!this._openSetting)return;const e="covers"===this._openSetting?2e4:6e3;this._settingTimeout=setTimeout(()=>{this._openSetting=null},e)}_stepSetting(e,t,i,s){const o="override"===e?this._config.override_duration_entity:this._config.early_switch_entity;if(!o)return;const n=s>0?t.find(e=>e>i):[...t].reverse().find(e=>e - ${r?V`
+ ${r?q`
@@ -102,103 +163,114 @@ const lt={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:$},ht=(t=lt >${this._settingLabel(n)}
-

${o}

+

${s}

`:W} -
`}_minutes(t){if(!t)return null;const e=this.getEntityState(t);return e?function(t){if(!yt(t))return;const e=Number(t.state);return Number.isFinite(e)?e:void 0}(e):this.preview?0:null}_renderMain(t,e,i,o,s,n){const r=e.attributes?.option_map??{},a=Object.keys(r).length>0?Object.entries(r):(e.attributes?.options??[]).map(t=>[t,t]),c=t.attributes?.option_map??{},l=Object.keys(c).length>0?Object.entries(c):(t.attributes?.options??[]).map(t=>[t,t]),h=this.getEntityState(this._config.next_mode_entity),d=this.getEntityState(this._config.next_mode_at_entity),p=this._minutes(this._config.early_switch_entity),_=this._minutes(this._config.override_duration_entity),u=yt(h),m=u?h.state:"",f=yt(d)?this._formatAbsoluteTime(d.state):"",v=""!==f,g=_?mt(this.hass,"card.override_effect",{duration:this._settingLabel(_),time:this._clockIn(_)}):mt(this.hass,"card.override_none"),y=v?p?mt(this.hass,"card.early_effect",{mode:m,time:this._clockAt(d.state),scheduled:this._clockAt(d.state,p)}):mt(this.hass,"card.early_none",{mode:m,time:this._clockAt(d.state)}):mt(this.hass,"card.early_no_event"),$=yt(s),b=yt(n),w=mt(this.hass,"card.covers_left_open",{covers:o.join(", ")}),x=o.length>3,E=x?mt(this.hass,"card.covers_left_open_count",{count:String(o.length)}):w,A=t.attributes?.current_key,S=yt(t);return V` +
`}_minutes(e){if(!e)return null;const t=this.getEntityState(e);return t?function(e){if(!ye(e))return;const t=Number(e.state);return Number.isFinite(t)?t:void 0}(t):this.preview?0:null}_renderMain(e,t,i,s,o,n,r){const a=t.attributes?.option_map??{},c=Object.keys(a).length>0?Object.entries(a):(t.attributes?.options??[]).map(e=>[e,e]),h=e.attributes?.option_map??{},l=Object.keys(h).length>0?Object.entries(h):(e.attributes?.options??[]).map(e=>[e,e]),d=this.getEntityState(this._config.next_mode_entity),p=this.getEntityState(this._config.next_mode_at_entity),u=this._minutes(this._config.early_switch_entity),_=this._minutes(this._config.override_duration_entity),m=ye(d),v=m?d.state:"",f=ye(p)?this._formatAbsoluteTime(p.state):"",g=""!==f,y=_?me(this.hass,"card.override_effect",{duration:this._settingLabel(_),time:this._clockIn(_)}):me(this.hass,"card.override_none"),b=g?u?me(this.hass,"card.early_effect",{mode:v,time:this._clockAt(p.state),scheduled:this._clockAt(p.state,u)}):me(this.hass,"card.early_none",{mode:v,time:this._clockAt(p.state)}):me(this.hass,"card.early_no_event"),$=ye(n),x=ye(r),E=me(this.hass,"card.covers_left_open",{covers:s.join(", ")}),w=s.length>3,S=w?me(this.hass,"card.covers_left_open_count",{count:String(s.length)}):E,A=[...o.inhibited.keys()].map(e=>this._friendlyName(e)),C=A.length>2?me(this.hass,"card.covers_paused_count",{count:String(A.length)}):me(this.hass,"card.covers_paused",{covers:A.join(", ")}),k=[...o.inhibited.entries()].map(([e,t])=>`${this._friendlyName(e)} — ${this._inhibitEnd(t)}`).join("\n"),T=e.attributes?.current_key,P=ye(e);return q`
- ${l.map(([e,i])=>{const o=S&&(A?e===A:i===t.state);return V``})}
- ${mt(this.hass,"card.day_mode")} + ${me(this.hass,"card.day_mode")}
- ${u||v?V`
- ${mt(this.hass,"card.next")} + ${m||g?q`
+ ${me(this.hass,"card.next")} ${m}${u&&v?" · ":""}${f}${v}${m&&g?" · ":""}${f}
`:W} - ${null!==_?this._renderSetting("override","mdi:hand-back-left",mt(this.hass,"card.override_label"),g,wt.OVERRIDE_PRESETS,_):W} - ${null!==p?this._renderSetting("early","mdi:clock-fast",mt(this.hass,"card.early_label"),y,wt.EARLY_SWITCH_PRESETS,p):W} + ${null!==_?this._renderSetting("override","mdi:hand-back-left",me(this.hass,"card.override_label"),y,Ee.OVERRIDE_PRESETS,_):W} + ${null!==u?this._renderSetting("early","mdi:clock-fast",me(this.hass,"card.early_label"),b,Ee.EARLY_SWITCH_PRESETS,u):W} - ${$||b?V`
- ${mt(this.hass,"card.covers")} + ${$||x?q`
+ ${me(this.hass,"card.covers")} - ${$?this._renderCoverTime("open",s):W} - ${b?this._renderCoverTime("close",n):W} + ${$?this._renderCoverTime("open",n):W} + ${x?this._renderCoverTime("close",r):W}
`:W} - ${i||o.length>0?V`
- ${i?V` + ${o.managed.length>0?this._renderCoverInhibition(o):W} + + ${i||s.length>0||A.length>0?q`
+ ${i?q` - ${mt(this.hass,"card.heat_protection_active")} + ${me(this.hass,"card.heat_protection_active")} `:W} - ${o.length>0&&!x?V` + ${s.length>0&&!w?q` - ${E} + ${S} `:W} - ${x?V``:W} + ${A.length>0?q``:W}
- ${x&&this._coversExpanded?V`

${w}

`:W}`:W} + ${w&&this._coversExpanded?q`

${E}

`:W}`:W}
- `}render(){if(!this.hass||!this._config)return W;const t=this.getEntityState(this._config.day_mode_entity),e=this.getEntityState(this._config.thermostat_mode_entity);if(!this.preview){const i=[[this._config.day_mode_entity,t],[this._config.thermostat_mode_entity,e]].filter(([,t])=>!t).map(([t])=>t||"?");if(i.length>0)return V` + `}render(){if(!this.hass||!this._config)return W;const e=this.getEntityState(this._config.day_mode_entity),t=this.getEntityState(this._config.thermostat_mode_entity);if(!this.preview){const i=[[this._config.day_mode_entity,e],[this._config.thermostat_mode_entity,t]].filter(([,e])=>!e).map(([e])=>e||"?");if(i.length>0)return q` - ${i.map(t=>V`
- ${mt(this.hass,"card.entity_not_found",{entity:t})} + ${i.map(e=>q`
+ ${me(this.hass,"card.entity_not_found",{entity:e})}
`)} - `}const i=t??{entity_id:this._config.day_mode_entity,state:mt(this.hass,"preview.work"),attributes:{option_map:Object.fromEntries(["home","work","remote","away"].map(t=>[t,mt(this.hass,`preview.${t}`)]))}},o=e??{entity_id:this._config.thermostat_mode_entity,state:mt(this.hass,"thermostat.heating"),attributes:{option_map:Object.fromEntries(["off","heating","cooling","ventilation"].map(t=>[t,mt(this.hass,`thermostat.${t}`)])),current_key:"heating"}},s="on"===this.getEntityState(this._config.heat_protection_entity)?.state,n=this.getEntityState(this._config.cover_open_time_entity)?.state,r=this.getEntityState(this._config.cover_close_time_entity)?.state;return V` + `}const i=e??{entity_id:this._config.day_mode_entity,state:me(this.hass,"preview.work"),attributes:{option_map:Object.fromEntries(["home","work","remote","away"].map(e=>[e,me(this.hass,`preview.${e}`)]))}},s=t??{entity_id:this._config.thermostat_mode_entity,state:me(this.hass,"thermostat.heating"),attributes:{option_map:Object.fromEntries(["off","heating","cooling","ventilation"].map(e=>[e,me(this.hass,`thermostat.${e}`)])),current_key:"heating"}},o="on"===this.getEntityState(this._config.heat_protection_entity)?.state,n=this.getEntityState(this._config.cover_open_time_entity)?.state,r=this.getEntityState(this._config.cover_close_time_entity)?.state;return q`
- ${this._renderMain(o,i,s,this.getCoversLeftOpen(),n,r)} + ${this._renderMain(s,i,o,this.getCoversLeftOpen(),this.getCoverInhibitions(),n,r)}
- `}}wt.COVER_FEEDBACK_MS=600,wt.OVERRIDE_PRESETS=[0,15,30,60,120,240,480,720],wt.EARLY_SWITCH_PRESETS=[0,15,30,45,60,90,120,180,240],wt.styles=r` + `}}Ee.COVER_FEEDBACK_MS=600,Ee.OVERRIDE_PRESETS=[0,15,30,60,120,240,480,720],Ee.EARLY_SWITCH_PRESETS=[0,15,30,45,60,90,120,180,240],Ee.INHIBIT_PRESETS=[240,1440,2880,4320,10080,0],Ee.INHIBIT_DEFAULT_INDEX=1,Ee.styles=r` ha-card { padding: 12px; position: relative; @@ -510,6 +582,86 @@ const lt={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:$},ht=(t=lt color: var(--text-primary-color, #fff); } + /* A paused cover is a choice, not a fault: calm colour, no animation. */ + .chip--paused { + border: 0; + font-family: inherit; + background: var(--secondary-background-color, rgba(127, 127, 127, 0.12)); + color: var(--primary-text-color); + cursor: pointer; + } + + .chip--paused ha-icon { + color: var(--secondary-text-color, #666); + } + + .cover-list { + list-style: none; + margin: 8px 0 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 6px; + } + + .cover-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + font-size: 13px; + } + + .cover-name { + display: flex; + flex-direction: column; + min-width: 0; + } + + .cover-status { + font-size: 12px; + color: var(--secondary-text-color, #666); + } + + .cover-item.paused .cover-status { + color: var(--primary-color); + } + + .cover-toggle, + .resume-all { + display: inline-flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + padding: 4px 10px; + border-radius: 8px; + border: 1px solid var(--divider-color, #ccc); + background: var(--card-background-color); + color: var(--primary-text-color); + font-size: 13px; + font-family: inherit; + cursor: pointer; + } + + .cover-toggle ha-icon { + --mdc-icon-size: 16px; + } + + .cover-toggle:hover, + .resume-all:hover { + border-color: var(--primary-color); + } + + .cover-item.paused .cover-toggle { + border-color: var(--primary-color); + color: var(--primary-color); + } + + .resume-all { + align-self: flex-start; + margin-top: 8px; + } + /* Pulses between orange and yellow: a cover left up is something to act on tonight, not a steady status. */ .chip--covers { @@ -560,4 +712,4 @@ const lt={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:$},ht=(t=lt font-size: 13px; font-family: inherit; } - `,t([dt({attribute:!1})],wt.prototype,"hass",void 0),t([dt({type:Boolean})],wt.prototype,"preview",void 0),t([pt()],wt.prototype,"_config",void 0),t([pt()],wt.prototype,"_openSetting",void 0),t([pt()],wt.prototype,"_coverPending",void 0),t([pt()],wt.prototype,"_coversExpanded",void 0),t([pt()],wt.prototype,"_day",void 0),customElements.get("homeshift-card")||customElements.define("homeshift-card",wt),window.customCards=window.customCards||[],window.customCards.push({type:"homeshift-card",name:"HomeShift Card",description:"Card to manage day mode and thermostat mode via the HomeShift integration.",preview:!0}); + `,e([de({attribute:!1})],Ee.prototype,"hass",void 0),e([de({type:Boolean})],Ee.prototype,"preview",void 0),e([pe()],Ee.prototype,"_config",void 0),e([pe()],Ee.prototype,"_openSetting",void 0),e([pe()],Ee.prototype,"_inhibitIndex",void 0),e([pe()],Ee.prototype,"_coverPending",void 0),e([pe()],Ee.prototype,"_coversExpanded",void 0),e([pe()],Ee.prototype,"_day",void 0),customElements.get("homeshift-card")||customElements.define("homeshift-card",Ee),window.customCards=window.customCards||[],window.customCards.push({type:"homeshift-card",name:"HomeShift Card",description:"Card to manage day mode and thermostat mode via the HomeShift integration.",preview:!0}); diff --git a/src/components/homeshift-card.ts b/src/components/homeshift-card.ts index b1e136b..f793976 100644 --- a/src/components/homeshift-card.ts +++ b/src/components/homeshift-card.ts @@ -7,6 +7,13 @@ import { formatMinutes, isUsable, numericState } from "../state"; import { formatShortDate, formatTime } from "../time"; type CoverAction = "open" | "close"; +type SettingName = "override" | "early" | "covers"; + +/** Covers HomeShift drives, and the end of each running inhibition (null: until resumed). */ +interface CoverInhibitions { + managed: string[]; + inhibited: Map; +} class HomeShiftCard extends LitElement { /** Minimum time the "sending" state of a cover button stays visible. */ @@ -20,11 +27,17 @@ class HomeShiftCard extends LitElement { 0, 15, 30, 45, 60, 90, 120, 180, 240, ]; + // How long a cover is paused for, in minutes (0 = until resumed). + private static readonly INHIBIT_PRESETS = [240, 1440, 2880, 4320, 10080, 0]; + private static readonly INHIBIT_DEFAULT_INDEX = 1; + @property({ attribute: false }) public hass!: any; /** Set by Home Assistant in the card picker and the editor preview. */ @property({ type: Boolean }) public preview = false; @state() private _config!: HomeShiftCardConfig; - @state() private _openSetting: "override" | "early" | null = null; + @state() private _openSetting: SettingName | null = null; + /** Index in INHIBIT_PRESETS of the pause duration picked in the covers row. */ + @state() private _inhibitIndex = HomeShiftCard.INHIBIT_DEFAULT_INDEX; /** Cover button whose service call is in flight. */ @state() private _coverPending?: CoverAction; /** Whether the full list behind a "Not closed: N covers" chip is shown. */ @@ -75,6 +88,7 @@ class HomeShiftCard extends LitElement { cover_open_time_entity: "sensor.homeshift_cover_open_time", cover_close_time_entity: "sensor.homeshift_cover_close_time", covers_left_open_entity: "binary_sensor.homeshift_covers_left_open", + covers_inhibited_entity: "sensor.homeshift_covers_inhibited", }; } @@ -104,6 +118,7 @@ class HomeShiftCard extends LitElement { if ( changedProps.has("_day") || changedProps.has("_openSetting") || + changedProps.has("_inhibitIndex") || changedProps.has("_coversExpanded") || changedProps.has("_coverPending") ) @@ -172,6 +187,195 @@ class HomeShiftCard extends LitElement { ); } + private _friendlyName(entityId: string): string { + return this.hass?.states?.[entityId]?.attributes?.friendly_name ?? entityId; + } + + /** + * Every cover HomeShift drives, and the running inhibitions. An end + * already past is dropped here rather than waiting for the integration's + * next poll to forget it. + */ + private getCoverInhibitions(): CoverInhibitions { + const attributes = + this.getEntityState(this._config?.covers_inhibited_entity)?.attributes ?? + {}; + const managed: string[] = Array.isArray(attributes.managed_covers) + ? attributes.managed_covers + : []; + const inhibited = new Map(); + const now = Date.now(); + for (const [cover, until] of Object.entries(attributes.covers ?? {})) { + if (typeof until === "string" && new Date(until).getTime() <= now) { + continue; + } + inhibited.set(cover, typeof until === "string" ? until : null); + } + return { managed, inhibited }; + } + + /** Call a HomeShift cover service; show a refusal as Home Assistant's toast. */ + private async _callInhibitService(service: string, data: object) { + this._armSettingTimeout(); + try { + await this.hass.callService("homeshift", service, data); + } catch (err: any) { + this.dispatchEvent( + new CustomEvent("hass-notification", { + detail: { + message: localize(this.hass, "card.inhibit_failed", { + error: err?.message ?? String(err), + }), + }, + bubbles: true, + composed: true, + }), + ); + } + } + + private _inhibitCover(entityId: string) { + const minutes = HomeShiftCard.INHIBIT_PRESETS[this._inhibitIndex]; + this._callInhibitService("inhibit_covers", { + entity_id: [entityId], + ...(minutes ? { duration: { minutes } } : {}), + }); + } + + private _resumeCovers(entityIds?: string[]) { + this._callInhibitService( + "resume_covers", + entityIds ? { entity_id: entityIds } : {}, + ); + } + + private _stepInhibit(direction: 1 | -1) { + const last = HomeShiftCard.INHIBIT_PRESETS.length - 1; + this._inhibitIndex = Math.min( + last, + Math.max(0, this._inhibitIndex + direction), + ); + this._armSettingTimeout(); + } + + /** "4h", "3 d" / "3 j", or "Until resumed". */ + private _inhibitLabel(minutes: number): string { + if (!minutes) return localize(this.hass, "card.inhibit_forever"); + if (minutes < 1440) return formatMinutes(minutes); + return `${minutes / 1440} ${localize(this.hass, "card.days_short")}`; + } + + private _inhibitEnd(until: string | null): string { + return until + ? localize(this.hass, "card.inhibited_until", { + time: this._formatAbsoluteTime(until), + }) + : localize(this.hass, "card.inhibited_forever"); + } + + /** Collapsible row pausing the automation of chosen covers. */ + private _renderCoverInhibition({ managed, inhibited }: CoverInhibitions) { + const open = this._openSetting === "covers"; + const presets = HomeShiftCard.INHIBIT_PRESETS; + const minutes = presets[this._inhibitIndex]; + const label = localize(this.hass, "card.inhibit_label"); + const effect = minutes + ? localize(this.hass, "card.inhibit_effect", { + duration: this._inhibitLabel(minutes), + time: this._formatAbsoluteTime( + new Date(Date.now() + minutes * 60000).toISOString(), + ), + }) + : localize(this.hass, "card.inhibit_effect_forever"); + return html`
+ + ${open + ? html`
+
+ + ${this._inhibitLabel(minutes)} + +
+

${effect}

+
    + ${managed.map((cover) => { + const paused = inhibited.has(cover); + const name = this._friendlyName(cover); + const action = localize( + this.hass, + paused ? "card.resume" : "card.pause", + ); + return html`
  • + + ${name} + ${paused + ? this._inhibitEnd(inhibited.get(cover) ?? null) + : localize(this.hass, "card.inhibit_none")} + + +
  • `; + })} +
+ ${inhibited.size > 1 + ? html`` + : nothing} +
` + : nothing} +
`; + } + private onSelect(entityId: string, ev: Event) { const target = ev.target as HTMLSelectElement; const option = target?.value; @@ -315,7 +519,7 @@ class HomeShiftCard extends LitElement { } /** Open one setting at a time, and fold it back once it is left alone. */ - private _toggleSetting(name: "override" | "early") { + private _toggleSetting(name: SettingName) { this._openSetting = this._openSetting === name ? null : name; this._armSettingTimeout(); } @@ -323,9 +527,11 @@ class HomeShiftCard extends LitElement { private _armSettingTimeout() { clearTimeout(this._settingTimeout); if (!this._openSetting) return; + // Picking covers one by one takes longer than nudging a stepper. + const delay = this._openSetting === "covers" ? 20000 : 6000; this._settingTimeout = setTimeout(() => { this._openSetting = null; - }, 6000); + }, delay); } private _stepSetting( @@ -432,6 +638,7 @@ class HomeShiftCard extends LitElement { day: any, heatProtectionActive: boolean, coversLeftOpen: string[], + coverInhibitions: CoverInhibitions, coverOpenTime?: string, coverCloseTime?: string, ) { @@ -501,6 +708,21 @@ class HomeShiftCard extends LitElement { }) : coversFull; + const pausedCovers = [...coverInhibitions.inhibited.keys()].map((id) => + this._friendlyName(id), + ); + const pausedSummary = + pausedCovers.length > 2 + ? localize(this.hass, "card.covers_paused_count", { + count: String(pausedCovers.length), + }) + : localize(this.hass, "card.covers_paused", { + covers: pausedCovers.join(", "), + }); + const pausedDetail = [...coverInhibitions.inhibited.entries()] + .map(([id, until]) => `${this._friendlyName(id)} — ${this._inhibitEnd(until)}`) + .join("\n"); + const activeKey: string | undefined = thermo.attributes?.current_key; const thermoUsable = isUsable(thermo); @@ -593,7 +815,13 @@ class HomeShiftCard extends LitElement {
` : nothing} - ${heatProtectionActive || coversLeftOpen.length > 0 + ${coverInhibitions.managed.length > 0 + ? this._renderCoverInhibition(coverInhibitions) + : nothing} + + ${heatProtectionActive || + coversLeftOpen.length > 0 || + pausedCovers.length > 0 ? html`
${heatProtectionActive ? html` @@ -621,6 +849,20 @@ class HomeShiftCard extends LitElement { ${coversSummary} ` : nothing} + ${pausedCovers.length > 0 + ? html`` + : nothing}
${coversTruncated && this._coversExpanded ? html`

${coversFull}

` @@ -704,6 +946,7 @@ class HomeShiftCard extends LitElement { day, heatProtectionActive, this.getCoversLeftOpen(), + this.getCoverInhibitions(), coverOpenTime, coverCloseTime, )} @@ -1024,6 +1267,86 @@ class HomeShiftCard extends LitElement { color: var(--text-primary-color, #fff); } + /* A paused cover is a choice, not a fault: calm colour, no animation. */ + .chip--paused { + border: 0; + font-family: inherit; + background: var(--secondary-background-color, rgba(127, 127, 127, 0.12)); + color: var(--primary-text-color); + cursor: pointer; + } + + .chip--paused ha-icon { + color: var(--secondary-text-color, #666); + } + + .cover-list { + list-style: none; + margin: 8px 0 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 6px; + } + + .cover-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + font-size: 13px; + } + + .cover-name { + display: flex; + flex-direction: column; + min-width: 0; + } + + .cover-status { + font-size: 12px; + color: var(--secondary-text-color, #666); + } + + .cover-item.paused .cover-status { + color: var(--primary-color); + } + + .cover-toggle, + .resume-all { + display: inline-flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + padding: 4px 10px; + border-radius: 8px; + border: 1px solid var(--divider-color, #ccc); + background: var(--card-background-color); + color: var(--primary-text-color); + font-size: 13px; + font-family: inherit; + cursor: pointer; + } + + .cover-toggle ha-icon { + --mdc-icon-size: 16px; + } + + .cover-toggle:hover, + .resume-all:hover { + border-color: var(--primary-color); + } + + .cover-item.paused .cover-toggle { + border-color: var(--primary-color); + color: var(--primary-color); + } + + .resume-all { + align-self: flex-start; + margin-top: 8px; + } + /* Pulses between orange and yellow: a cover left up is something to act on tonight, not a steady status. */ .chip--covers { diff --git a/src/localize/en.json b/src/localize/en.json index 42404b3..0b1a32e 100644 --- a/src/localize/en.json +++ b/src/localize/en.json @@ -27,7 +27,22 @@ "early_none": "{mode} will start at its scheduled time, {time}.", "early_effect": "{mode} will start at {time} instead of {scheduled}.", "early_no_event": "No event scheduled.", - "covers_left_open_count": "Not closed: {count} covers" + "covers_left_open_count": "Not closed: {count} covers", + "inhibit_label": "Cover automation", + "inhibit_none": "Auto", + "inhibited_count": "{count} paused", + "inhibit_forever": "Until resumed", + "days_short": "d", + "inhibit_effect": "Pause for {duration} — until {time} if you start it now.", + "inhibit_effect_forever": "The cover stays paused until you resume it.", + "inhibited_until": "Paused until {time}", + "inhibited_forever": "Paused until resumed", + "pause": "Pause", + "resume": "Resume", + "resume_all": "Resume all", + "covers_paused": "Paused: {covers}", + "covers_paused_count": "{count} covers paused", + "inhibit_failed": "Could not pause or resume the cover: {error}" }, "thermostat": { "off": "Off", @@ -49,7 +64,8 @@ "open_covers_entity": "Open Covers Button", "close_covers_entity": "Close Covers Button", "cover_entity": "Cover Entity (legacy, overrides the buttons)", - "covers_left_open_entity": "Covers Left Open Entity" + "covers_left_open_entity": "Covers Left Open Entity", + "covers_inhibited_entity": "Paused covers entity" }, "preview": { "home": "Home", diff --git a/src/localize/fr.json b/src/localize/fr.json index 79d178e..477c326 100644 --- a/src/localize/fr.json +++ b/src/localize/fr.json @@ -27,7 +27,22 @@ "early_none": "{mode} commencera à l'heure prévue, {time}.", "early_effect": "{mode} commencera à {time} au lieu de {scheduled}.", "early_no_event": "Aucun événement prévu.", - "covers_left_open_count": "Non fermés : {count} volets" + "covers_left_open_count": "Non fermés : {count} volets", + "inhibit_label": "Pilotage auto des volets", + "inhibit_none": "Auto", + "inhibited_count": "{count} en pause", + "inhibit_forever": "Jusqu'à reprise", + "days_short": "j", + "inhibit_effect": "Pause de {duration} — jusqu'à {time} si tu l'actives maintenant.", + "inhibit_effect_forever": "Le volet reste en pause jusqu'à ce que tu le reprennes.", + "inhibited_until": "En pause jusqu'à {time}", + "inhibited_forever": "En pause jusqu'à reprise", + "pause": "Pause", + "resume": "Reprendre", + "resume_all": "Tout reprendre", + "covers_paused": "En pause : {covers}", + "covers_paused_count": "{count} volets en pause", + "inhibit_failed": "Échec de la mise en pause : {error}" }, "thermostat": { "off": "Éteint", @@ -49,7 +64,8 @@ "open_covers_entity": "Bouton d'ouverture des volets", "close_covers_entity": "Bouton de fermeture des volets", "cover_entity": "Entité volet (ancienne option, prioritaire sur les boutons)", - "covers_left_open_entity": "Entité volets non fermés" + "covers_left_open_entity": "Entité volets non fermés", + "covers_inhibited_entity": "Entité volets en pause" }, "preview": { "home": "Maison", diff --git a/src/types.ts b/src/types.ts index 4c3f38f..dceaed7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,8 @@ export interface HomeShiftCardConfig { /** Legacy: when set, the rows call cover.open/close_cover on it instead. */ cover_entity?: string; covers_left_open_entity?: string; + /** HomeShift's covers_inhibited sensor: the cover automation pause row. */ + covers_inhibited_entity?: string; } export type EntityConfigKey = { @@ -99,4 +101,9 @@ export const ENTITY_FIELDS: readonly { domains: ["binary_sensor"], default: "binary_sensor.homeshift_covers_left_open", }, + { + key: "covers_inhibited_entity", + domains: ["sensor"], + default: "sensor.homeshift_covers_inhibited", + }, ]; diff --git a/test/inhibition.test.ts b/test/inhibition.test.ts new file mode 100644 index 0000000..58f47a9 --- /dev/null +++ b/test/inhibition.test.ts @@ -0,0 +1,118 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { $, $$, entity, homeshiftStates, makeHass, renderCard } from "./helpers"; + +afterEach(() => { + document.body.innerHTML = ""; +}); + +const IN_THREE_DAYS = new Date(Date.now() + 3 * 86400e3).toISOString(); + +function withInhibitions(covers: Record = {}) { + const states = homeshiftStates(); + states["sensor.homeshift_covers_inhibited"] = entity( + "sensor.homeshift_covers_inhibited", + String(Object.keys(covers).length), + { covers, managed_covers: ["cover.living", "cover.bedroom"] }, + ); + states["cover.living"] = entity("cover.living", "open", { friendly_name: "Living" }); + states["cover.bedroom"] = entity("cover.bedroom", "closed", { friendly_name: "Bedroom" }); + return states; +} + +async function openPanel(card: any) { + const head = $$(card, ".setting-head").find((el) => + el.textContent?.includes("Cover automation"), + )!; + head.click(); + await card.updateComplete; +} + +describe("cover automation pause", () => { + it("is hidden without the covers_inhibited sensor", async () => { + const card = await renderCard(makeHass(homeshiftStates())); + expect(card.shadowRoot!.textContent).not.toContain("Cover automation"); + }); + + it("reads Auto while nothing is paused", async () => { + const card = await renderCard(makeHass(withInhibitions())); + const head = $$(card, ".setting-head").find((el) => + el.textContent?.includes("Cover automation"), + )!; + expect(head.querySelector(".setting-value")!.textContent).toBe("Auto"); + expect($(card, ".chip--paused")).toBeNull(); + }); + + it("pauses one cover for the picked duration", async () => { + const hass = makeHass(withInhibitions()); + const card = await renderCard(hass); + await openPanel(card); + + // Default is one day; one step up gives two. + $$(card, ".stepper button")[1].click(); + await card.updateComplete; + expect($(card, ".stepper-value")!.textContent).toBe("2 d"); + + $$(card, ".cover-toggle")[1].click(); + expect(hass.callService).toHaveBeenCalledWith("homeshift", "inhibit_covers", { + entity_id: ["cover.bedroom"], + duration: { minutes: 2880 }, + }); + }); + + it("pauses until resumed at the last step", async () => { + const hass = makeHass(withInhibitions()); + const card = await renderCard(hass); + await openPanel(card); + for (let i = 0; i < 6; i++) { + $$(card, ".stepper button")[1].click(); + await card.updateComplete; + } + expect($(card, ".stepper-value")!.textContent).toBe("Until resumed"); + + $$(card, ".cover-toggle")[0].click(); + expect(hass.callService).toHaveBeenCalledWith("homeshift", "inhibit_covers", { + entity_id: ["cover.living"], + }); + }); + + it("lists paused covers and resumes them", async () => { + const hass = makeHass( + withInhibitions({ "cover.bedroom": IN_THREE_DAYS, "cover.living": null }), + ); + const card = await renderCard(hass); + expect($(card, ".chip--paused")!.textContent).toContain("Paused: Bedroom, Living"); + + await openPanel(card); + const statuses = $$(card, ".cover-status").map((el) => el.textContent); + expect(statuses[0]).toBe("Paused until resumed"); + expect(statuses[1]).toMatch(/^Paused until /); + + $$(card, ".cover-toggle")[1].click(); + expect(hass.callService).toHaveBeenCalledWith("homeshift", "resume_covers", { + entity_id: ["cover.bedroom"], + }); + + $(card, ".resume-all")!.click(); + expect(hass.callService).toHaveBeenCalledWith("homeshift", "resume_covers", {}); + }); + + it("ignores an inhibition already over", async () => { + const past = new Date(Date.now() - 60000).toISOString(); + const card = await renderCard(makeHass(withInhibitions({ "cover.bedroom": past }))); + expect($(card, ".chip--paused")).toBeNull(); + }); + + it("shows a refused call as a notification", async () => { + const hass = makeHass(withInhibitions()); + hass.callService.mockRejectedValueOnce(new Error("nope")); + const card = await renderCard(hass); + const messages: string[] = []; + card.addEventListener("hass-notification", (ev: any) => + messages.push(ev.detail.message), + ); + await openPanel(card); + $$(card, ".cover-toggle")[0].click(); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(messages).toEqual(["Could not pause or resume the cover: nope"]); + }); +}); From 6575e7cf1ff5004ed8c3b7a8d1ae08b8cb433c3d Mon Sep 17 00:00:00 2001 From: Gamso Date: Sun, 11 Oct 2026 09:30:35 +0200 Subject: [PATCH 22/25] feat(card): schedules panel with day mode tabs and the scheduler editor A Schedules bar at the foot of the card slides a panel up over it: one tab per day mode with the 24 h timelines of the schedules tagged with it, grouped by room, and a pencil that swaps them for the scheduler-card list of the tab. The panel is as tall as its content, anchored to the foot of the card, and scrolls when the schedules are taller than it. The bar appears once the Scheduler integration holds a schedule tagged with a day mode; show_schedules: false hides it and schedule_entities narrows it. Also aligns the gap under the thermostat modes with the card padding. Co-Authored-By: Claude Opus 5.5 --- src/components/homeshift-card-editor.ts | 31 +- src/components/homeshift-card.ts | 220 ++++++++++++- src/localize/en.json | 15 +- src/localize/fr.json | 15 +- src/schedule/schedule-panel.ts | 276 ++++++++++++++++ src/schedule/schedule-timeline.ts | 401 ++++++++++++++++++++++++ src/schedule/timeline.ts | 211 +++++++++++++ src/types.ts | 7 + test/localize.test.ts | 12 +- test/schedules.test.ts | 260 +++++++++++++++ 10 files changed, 1438 insertions(+), 10 deletions(-) create mode 100644 src/schedule/schedule-panel.ts create mode 100644 src/schedule/schedule-timeline.ts create mode 100644 src/schedule/timeline.ts create mode 100644 test/schedules.test.ts diff --git a/src/components/homeshift-card-editor.ts b/src/components/homeshift-card-editor.ts index 9f54530..6c3f29f 100644 --- a/src/components/homeshift-card-editor.ts +++ b/src/components/homeshift-card-editor.ts @@ -35,6 +35,17 @@ class HomeShiftCardEditor extends LitElement { this._dispatchConfigChanged(newConfig); } + /** Schedules options: `show_schedules` and `schedule_entities`. */ + private _onScheduleChanged( + ev: CustomEvent, + key: "show_schedules" | "schedule_entities", + ) { + if (!this._config || !this.hass) return; + const newConfig = { ...this._config, [key]: ev.detail.value }; + this._config = newConfig; + this._dispatchConfigChanged(newConfig); + } + private _dispatchConfigChanged(newConfig: HomeShiftCardConfig) { const event = new CustomEvent("config-changed", { detail: { config: newConfig }, @@ -68,6 +79,23 @@ class HomeShiftCardEditor extends LitElement { allow-custom-entity >`, )} + + + this._onScheduleChanged(e, "show_schedules")} + > + + this._onScheduleChanged(e, "schedule_entities")} + >
`; } @@ -81,7 +109,8 @@ class HomeShiftCardEditor extends LitElement { } ha-textfield, - ha-entity-picker { + ha-entity-picker, + ha-selector { width: 100%; } `; diff --git a/src/components/homeshift-card.ts b/src/components/homeshift-card.ts index f793976..753be55 100644 --- a/src/components/homeshift-card.ts +++ b/src/components/homeshift-card.ts @@ -5,6 +5,8 @@ import "./homeshift-card-editor"; import { ENTITY_FIELDS, HomeShiftCardConfig } from "../types"; import { formatMinutes, isUsable, numericState } from "../state"; import { formatShortDate, formatTime } from "../time"; +import "../schedule/schedule-panel"; +import { Schedule } from "../schedule/timeline"; type CoverAction = "open" | "close"; type SettingName = "override" | "early" | "covers"; @@ -44,6 +46,12 @@ class HomeShiftCard extends LitElement { @state() private _coversExpanded = false; /** Bumped at midnight so the "Today" / "Tomorrow" prefixes roll over. */ @state() private _day = 0; + /** Every schedule of the Scheduler integration, once read (schedules panel). */ + @state() private _schedules?: Schedule[]; + /** Whether the schedules panel covers the card. */ + @state() private _schedulesOpen = false; + /** Fingerprint of the schedule switches, to read the schedules again when they change. */ + private _schedulesKey?: string; private _midnightTimer?: ReturnType; private _settingTimeout?: ReturnType; @@ -106,6 +114,10 @@ class HomeShiftCard extends LitElement { for (const field of ENTITY_FIELDS) { normalized[field.key] = config[field.key] ?? field.default; } + normalized.show_schedules = config.show_schedules !== false; + normalized.schedule_entities = Array.isArray(config.schedule_entities) + ? config.schedule_entities + : []; this._config = normalized; } @@ -114,18 +126,24 @@ class HomeShiftCard extends LitElement { } protected shouldUpdate(changedProps: Map): boolean { + // Here rather than in willUpdate: most hass updates stop at this method. + if (changedProps.has("hass")) this._refreshSchedules(); if (changedProps.has("_config") || changedProps.has("preview")) return true; if ( changedProps.has("_day") || changedProps.has("_openSetting") || changedProps.has("_inhibitIndex") || changedProps.has("_coversExpanded") || - changedProps.has("_coverPending") + changedProps.has("_coverPending") || + changedProps.has("_schedules") || + changedProps.has("_schedulesOpen") ) return true; if (changedProps.has("hass")) { const oldHass = changedProps.get("hass") as any; if (!oldHass) return true; + // The open panel shows the schedule switches and the room temperatures. + if (this._schedulesOpen) return true; const watchedEntities = ENTITY_FIELDS.map( (field) => this._config?.[field.key], ).filter(Boolean) as string[]; @@ -376,6 +394,98 @@ class HomeShiftCard extends LitElement {
`; } + /** + * Reads the schedules again when a schedule switch changes: the Scheduler + * integration updates them whenever a schedule is edited, toggled or run. + * Without the integration the call fails and the panel stays hidden. + */ + private _refreshSchedules() { + if (!this._config?.show_schedules || this.preview || !this.hass?.callWS) { + return; + } + const key = Object.values(this.hass.states ?? {}) + .filter((s: any) => s.entity_id.startsWith("switch.schedule")) + .map((s: any) => `${s.entity_id}:${s.last_updated}`) + .join("|"); + if (key === this._schedulesKey) return; + this._schedulesKey = key; + this.hass + .callWS({ type: "scheduler" }) + .then((schedules: Schedule[]) => { + this._schedules = Array.isArray(schedules) ? schedules : undefined; + }) + .catch(() => { + this._schedules = undefined; + }); + } + + /** Whether a schedule is tagged with one of the day modes. */ + private _hasDaySchedules(dayModes: string[]): boolean { + return (this._schedules ?? []).some((schedule) => + schedule.tags?.some((tag) => dayModes.includes(tag)), + ); + } + + private _toggleSchedules(open: boolean) { + this._schedulesOpen = open; + // Keep the focus on the control that opens or closes the panel. + this.updateComplete.then(() => + this.shadowRoot + ?.querySelector(open ? ".schedules-close" : ".schedules-open") + ?.focus(), + ); + } + + /** + * Bar at the foot of the card, and the panel it slides up over the card: + * a tab per day mode with the 24 h timelines of its schedules, and the + * scheduler card to edit them. The card keeps its height, so the + * dashboard around it does not move. + */ + private _renderSchedules(dayMode: string, dayModes: string[]) { + const title = localize(this.hass, "schedule.title"); + const bar = (open: boolean) => html``; + return html`${bar(false)} + ${this._schedulesOpen + ? html`
{ + if (ev.key !== "Escape") return; + ev.stopPropagation(); + this._toggleSchedules(false); + }} + > + ${bar(true)} +
+ +
+
` + : nothing}`; + } + private onSelect(entityId: string, ev: Event) { const target = ev.target as HTMLSelectElement; const option = target?.value; @@ -938,9 +1048,12 @@ class HomeShiftCard extends LitElement { this._config.cover_close_time_entity, )?.state; + const dayModes: string[] = day.attributes?.options ?? []; + const showSchedules = isUsable(day) && this._hasDaySchedules(dayModes); + return html` -
+
${this._renderMain( thermo, day, @@ -951,6 +1064,7 @@ class HomeShiftCard extends LitElement { coverCloseTime, )}
+ ${showSchedules ? this._renderSchedules(day.state, dayModes) : nothing} `; } @@ -959,6 +1073,8 @@ class HomeShiftCard extends LitElement { ha-card { padding: 12px; position: relative; + /* Clips the schedules panel while it slides up. */ + overflow: hidden; } .container { @@ -978,7 +1094,8 @@ class HomeShiftCard extends LitElement { border: 1px solid var(--divider-color, #ccc); border-radius: 8px; overflow: hidden; - margin-bottom: 4px; + /* Same gap above the first divider as the card's padding above. */ + margin-bottom: 12px; } .preset { @@ -1388,6 +1505,103 @@ class HomeShiftCard extends LitElement { } } + /* Schedules: a bar at the foot of the card, and the panel it slides up. */ + .schedules-bar { + display: flex; + align-items: center; + gap: 8px; + width: calc(100% + 24px); + margin: 10px -12px -12px; + padding: 10px 12px; + border: 0; + border-top: 1px solid var(--divider-color, #e0e0e0); + background: transparent; + color: var(--secondary-text-color, #666); + font-family: inherit; + font-size: 12px; + font-weight: 500; + letter-spacing: 0.05em; + text-transform: uppercase; + text-align: left; + cursor: pointer; + } + + .schedules-bar:hover { + background: var(--secondary-background-color, rgba(127, 127, 127, 0.08)); + } + + .schedules-bar:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: -2px; + } + + .schedules-bar ha-icon { + --mdc-icon-size: 18px; + flex-shrink: 0; + } + + .schedules-title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .schedules-mode { + color: var(--primary-text-color); + text-transform: none; + letter-spacing: normal; + } + + /* Anchored to the foot of the card and as tall as its content: the top + of the card stays in sight while the schedules are shorter than it; + longer ones scroll inside the panel. */ + .schedules-panel { + position: absolute; + left: 0; + right: 0; + bottom: 0; + max-height: 100%; + z-index: 1; + border-top: 1px solid var(--divider-color, #e0e0e0); + box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.25); + display: flex; + flex-direction: column; + background: var(--ha-card-background, var(--card-background-color, #fff)); + animation: schedules-slide-up 0.2s ease; + } + + .schedules-panel .schedules-bar { + width: 100%; + margin: 0; + border-top: 0; + border-bottom: 1px solid var(--divider-color, #e0e0e0); + } + + .schedules-body { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + padding: 10px 12px 12px; + } + + @keyframes schedules-slide-up { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } + } + + @media (prefers-reduced-motion: reduce) { + .schedules-panel { + animation: none; + } + } + select { padding: 6px 8px; border-radius: 8px; diff --git a/src/localize/en.json b/src/localize/en.json index 0b1a32e..0bf4214 100644 --- a/src/localize/en.json +++ b/src/localize/en.json @@ -50,6 +50,17 @@ "cooling": "Cooling", "ventilation": "Ventilation" }, + "schedule": { + "title": "Schedules", + "empty": "No schedule for {mode}", + "on": "On", + "off": "Off", + "edit": "Edit schedules", + "back": "Back to timelines", + "active_mode": "Current day mode", + "next_run": "Next run", + "days": "Days" + }, "editor": { "name": "Name", "day_mode_entity": "Day Mode Entity", @@ -65,7 +76,9 @@ "close_covers_entity": "Close Covers Button", "cover_entity": "Cover Entity (legacy, overrides the buttons)", "covers_left_open_entity": "Covers Left Open Entity", - "covers_inhibited_entity": "Paused covers entity" + "covers_inhibited_entity": "Paused covers entity", + "show_schedules": "Schedules panel (Scheduler integration)", + "schedule_entities": "Schedules panel: only these entities (all when empty)" }, "preview": { "home": "Home", diff --git a/src/localize/fr.json b/src/localize/fr.json index 477c326..1aeb2a6 100644 --- a/src/localize/fr.json +++ b/src/localize/fr.json @@ -50,6 +50,17 @@ "cooling": "Climatisation", "ventilation": "Ventilation" }, + "schedule": { + "title": "Plannings", + "empty": "Aucune planification pour {mode}", + "on": "Allumé", + "off": "Éteint", + "edit": "Modifier les planifications", + "back": "Retour aux frises", + "active_mode": "Mode du jour actuel", + "next_run": "Prochain lancement", + "days": "Jours" + }, "editor": { "name": "Nom", "day_mode_entity": "Entité mode jour", @@ -65,7 +76,9 @@ "close_covers_entity": "Bouton de fermeture des volets", "cover_entity": "Entité volet (ancienne option, prioritaire sur les boutons)", "covers_left_open_entity": "Entité volets non fermés", - "covers_inhibited_entity": "Entité volets en pause" + "covers_inhibited_entity": "Entité volets en pause", + "show_schedules": "Panneau des plannings (intégration Scheduler)", + "schedule_entities": "Panneau des plannings : seulement ces entités (toutes si vide)" }, "preview": { "home": "Maison", diff --git a/src/schedule/schedule-panel.ts b/src/schedule/schedule-panel.ts new file mode 100644 index 0000000..d113413 --- /dev/null +++ b/src/schedule/schedule-panel.ts @@ -0,0 +1,276 @@ +import { LitElement, html, css, nothing, PropertyValues } from "lit"; +import { property, state } from "lit/decorators.js"; +import { localize } from "../localize/localize"; +import { Schedule } from "./timeline"; +import "./schedule-timeline"; + +/** + * Content of the card's schedules panel: one tab per day mode, the 24 h + * timelines of the shown mode, and a pencil that swaps them for the + * scheduler-card list of that mode, to add and edit its schedules. + */ +export class HomeShiftSchedulePanel extends LitElement { + @property({ attribute: false }) public hass!: any; + @property({ attribute: false }) public schedules: Schedule[] = []; + /** Day modes, in the order of the day mode entity: one tab each. */ + @property({ attribute: false }) public modes: string[] = []; + /** Current day mode: marked with a dot, and shown whenever it changes. */ + @property() public activeMode = ""; + /** Only the schedules acting on these entities, when there are some. */ + @property({ attribute: false }) public entities: string[] = []; + /** Day mode whose tab is shown. */ + @state() private _selected?: string; + /** Whether the scheduler card replaces the timelines. */ + @state() private _editing = false; + /** Bumped when a scheduler card is built, to render it. */ + @state() private _cardsVersion = 0; + /** One scheduler card per mode, built once and kept while switching tabs. */ + private _cards = new Map(); + private _building = new Set(); + private _helpers?: Promise; + + protected willUpdate(changed: PropertyValues): void { + if (changed.has("activeMode") && this.modes.includes(this.activeMode)) { + this._selected = this.activeMode; + } else if (!this._selected || !this.modes.includes(this._selected)) { + this._selected = this.modes[0]; + } + if (changed.has("entities") && changed.get("entities") !== undefined) { + this._cards = new Map(); + this._building.clear(); + } + } + + protected updated(): void { + const card = this._selected ? this._cards.get(this._selected) : undefined; + if (card && (card as any).hass !== this.hass) (card as any).hass = this.hass; + } + + /** scheduler-card configuration of one tab: its tag and entities. */ + private _schedulerConfig(mode: string): Record { + return { + type: "custom:scheduler-card", + title: false, + discover_existing: false, + show_header_toggle: false, + time_step: 5, + sort_by: ["title"], + display_options: { + primary_info: ["{name}"], + secondary_info: [ + `${localize(this.hass, "schedule.next_run")} {relative-time}`, + `${localize(this.hass, "schedule.days")} {days}`, + "additional-tasks", + ], + icon: "entity", + }, + tags: [mode], + ...(this.entities.length ? { include: this.entities } : {}), + }; + } + + /** Scheduler card of a mode, built on first display. */ + private _card(mode: string): HTMLElement | undefined { + const card = this._cards.get(mode); + if (card) return card; + if (!this._building.has(mode)) { + this._building.add(mode); + this._buildCard(mode).finally(() => this._building.delete(mode)); + } + return undefined; + } + + private async _buildCard(mode: string): Promise { + const config = this._schedulerConfig(mode); + const generation = this._cards; + this._helpers ??= + (window as any).loadCardHelpers?.() ?? Promise.resolve(undefined); + const helpers = await this._helpers; + let card: HTMLElement; + if (helpers) { + card = helpers.createCardElement(config); + } else { + await customElements.whenDefined("scheduler-card"); + card = document.createElement("scheduler-card"); + (card as any).setConfig(config); + } + // The entities changed while the card was being built. + if (generation !== this._cards) return; + // Home Assistant asks for a rebuild once a card that loaded late is defined. + card.addEventListener("ll-rebuild", (ev) => { + ev.stopPropagation(); + this._cards.delete(mode); + this._cardsVersion++; + }); + (card as any).hass = this.hass; + this._cards.set(mode, card); + this._cardsVersion++; + } + + private _select(mode: string) { + this._selected = mode; + } + + private _onTabKeydown(ev: KeyboardEvent) { + const step = ({ ArrowRight: 1, ArrowLeft: -1 } as Record)[ + ev.key + ]; + if (!step) return; + const index = this.modes.indexOf(this._selected ?? ""); + const next = this.modes[(index + step + this.modes.length) % this.modes.length]; + this._select(next); + ev.preventDefault(); + this.updateComplete.then(() => + this.shadowRoot + ?.querySelector('[aria-selected="true"]') + ?.focus(), + ); + } + + protected render() { + if (!this.hass || !this._selected) return nothing; + const selected = this._selected; + const editLabel = localize( + this.hass, + this._editing ? "schedule.back" : "schedule.edit", + ); + return html` +
+
+ ${this.modes.map((mode) => { + const isSelected = mode === selected; + return html``; + })} +
+ +
+
+ ${this._editing + ? (this._card(selected) ?? nothing) + : html``} +
+ `; + } + + static styles = css` + :host { + display: flex; + flex-direction: column; + gap: 10px; + } + .header { + display: flex; + align-items: center; + gap: 6px; + } + /* Segmented control: one button per mode, the shown one filled. */ + .tabs { + flex: 1; + min-width: 0; + display: flex; + gap: 2px; + padding: 3px; + border-radius: 10px; + background: color-mix(in srgb, var(--primary-text-color) 7%, transparent); + overflow-x: auto; + scrollbar-width: none; + } + .tab { + flex: 1 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + padding: 5px 8px; + border: none; + border-radius: 8px; + background: none; + cursor: pointer; + font: inherit; + font-size: 13px; + font-weight: 500; + color: var(--secondary-text-color); + } + .tab:hover:not(.selected) { + background: color-mix(in srgb, var(--primary-text-color) 6%, transparent); + } + .tab:focus-visible, + .edit:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 1px; + } + .tab.selected { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + } + .active-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--state-active-color, var(--success-color, #43a047)); + flex: none; + } + .edit { + flex: none; + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + padding: 0; + border: none; + border-radius: 10px; + background: color-mix(in srgb, var(--primary-text-color) 7%, transparent); + color: var(--primary-color); + cursor: pointer; + --mdc-icon-size: 18px; + } + .edit:hover { + background: color-mix(in srgb, var(--primary-text-color) 12%, transparent); + } + .edit[aria-pressed="true"] { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + } + /* The scheduler card blends into the panel. */ + .content { + --ha-card-border-width: 0; + --ha-card-box-shadow: none; + --ha-card-background: transparent; + } + `; +} + +if (!customElements.get("homeshift-schedule-panel")) { + customElements.define("homeshift-schedule-panel", HomeShiftSchedulePanel); +} diff --git a/src/schedule/schedule-timeline.ts b/src/schedule/schedule-timeline.ts new file mode 100644 index 0000000..71758c6 --- /dev/null +++ b/src/schedule/schedule-timeline.ts @@ -0,0 +1,401 @@ +import { LitElement, html, css, nothing } from "lit"; +import { property } from "lit/decorators.js"; +import { localize } from "../localize/localize"; +import { + DAY_MINUTES, + Schedule, + Segment, + SlotValue, + daySegments, + formatMinutes, + scheduleEntities, + schedulesFor, + segmentColors, +} from "./timeline"; + +const KIND_ICONS: Record = { + heat: "mdi:radiator", + cool: "mdi:snowflake", + // A preset alone does not say heating or cooling: heating, as for the exclusion. + preset: "mdi:radiator", + on: "mdi:toggle-switch-outline", + off: "mdi:toggle-switch-off-outline", + other: "mdi:calendar-clock", +}; + +/** Schedules sharing a heading: those driving the same room's thermostat. */ +interface Room { + name: string; + /** Entity the schedules drive, when they drive a single one. */ + entity?: string; + schedules: Schedule[]; +} + +/** Whether a timeline heats or cools, for the heating / cooling exclusion. */ +function climateSide(kind: string): "heat" | "cool" | undefined { + if (kind === "cool") return "cool"; + if (kind === "heat" || kind === "preset") return "heat"; + return undefined; +} + +/** Order of a room's timelines: heating first, then cooling. */ +const KIND_ORDER = ["heat", "preset", "cool", "on", "off", "other"]; + +/** + * Kind of a timeline, for its icon: cooling wins, then heating, then + * presets; turning off alone does not tell what the schedule is for. + */ +function rowKind(segments: Segment[]): string { + const kinds = new Set(segments.map((s) => s.value.kind)); + return ( + ["cool", "heat", "preset", "on", "off"].find((kind) => kinds.has(kind as any)) ?? + "other" + ); +} + +/** + * 24 h timelines of the schedules tagged with one day mode, grouped by + * room, each with the switch that enables it. The schedules are handed + * over by the card, which fetches them once for the whole dashboard view. + */ +export class HomeShiftScheduleTimeline extends LitElement { + @property({ attribute: false }) public hass!: any; + @property({ attribute: false }) public schedules: Schedule[] = []; + /** Scheduler tag of the schedules shown: the day mode. */ + @property() public tag = ""; + /** Only the schedules acting on these entities, when there are some. */ + @property({ attribute: false }) public entities: string[] = []; + private _clock?: ReturnType; + + connectedCallback() { + super.connectedCallback(); + // Moves the "now" marker along the timelines. + this._clock = setInterval(() => this.requestUpdate(), 60_000); + } + + disconnectedCallback() { + super.disconnectedCallback(); + clearInterval(this._clock); + } + + protected render() { + if (!this.hass) return nothing; + const schedules = schedulesFor(this.schedules, this.tag, this.entities); + if (!schedules.length) { + return html`
+ ${localize(this.hass, "schedule.empty", { mode: this.tag })} +
`; + } + const now = new Date(); + const nowMinutes = now.getHours() * 60 + now.getMinutes(); + return html` + ${this._rooms(schedules).map((room) => this._renderRoom(room, nowMinutes))} + + `; + } + + /** + * Schedules grouped by room: those driving the same single entity (e.g. + * the heating and the cooling of the living room) share a heading; any + * other schedule is a room of its own, named after itself. + */ + private _rooms(schedules: Schedule[]): Room[] { + const rooms = new Map(); + for (const schedule of schedules) { + const entities = scheduleEntities(schedule, true); + const entity = entities.length === 1 ? entities[0] : undefined; + const key = entity ?? schedule.entity_id; + if (!rooms.has(key)) { + const name = entity + ? (this.hass.states[entity]?.attributes?.friendly_name ?? entity) + : schedule.name; + rooms.set(key, { name, entity, schedules: [] }); + } + rooms.get(key)!.schedules.push(schedule); + } + for (const room of rooms.values()) { + room.schedules.sort( + (a, b) => + KIND_ORDER.indexOf(rowKind(daySegments(a))) - + KIND_ORDER.indexOf(rowKind(daySegments(b))), + ); + } + return [...rooms.values()]; + } + + private _renderRoom(room: Room, nowMinutes: number) { + const measured = room.entity?.startsWith("climate.") + ? this.hass.states[room.entity]?.attributes?.current_temperature + : undefined; + const allDisabled = room.schedules.every((s) => !this._enabled(s)); + return html` +
+
+ ${room.name} + ${typeof measured === "number" + ? html`${this._number(measured)}°` + : nothing} +
+ ${room.schedules.map((s) => this._renderRow(s, room, nowMinutes))} +
+ `; + } + + private _enabled(schedule: Schedule): boolean { + const state = this.hass.states[schedule.entity_id]; + return state ? state.state !== "off" : schedule.enabled; + } + + /** One schedule: its kind, its timeline and its switch. */ + private _renderRow(schedule: Schedule, room: Room, nowMinutes: number) { + const segments = daySegments(schedule); + const kind = rowKind(segments); + const enabled = this._enabled(schedule); + + return html` +
+ +
+ ${segments.map((s) => this._renderSegment(s))} +
+
+ +
+ `; + } + + private _renderSegment(segment: Segment) { + const { background, color } = segmentColors(segment.value); + const width = ((segment.end - segment.start) / DAY_MINUTES) * 100; + const range = `${formatMinutes(segment.start)} – ${formatMinutes(segment.end)}`; + const label = this._label(segment.value); + // The card is narrow: a label is written only where it fits, about 2 % + // of the bar per character ("18°" fits in 10 %, "Confort" needs 16 %). + const fits = width >= Math.max(10, label.length * 2.2); + return html`
+ ${fits ? label : nothing} +
`; + } + + /** A number in the user's format (20.5 / 20,5). */ + private _number(value: number): string { + const language = this.hass.locale?.language ?? this.hass.language ?? "en"; + return value.toLocaleString(language, { maximumFractionDigits: 1 }); + } + + /** Text of a value, with the temperature in the user's number format (20.5° / 20,5°). */ + private _label(value: SlotValue): string { + const stateObj = value.entity ? this.hass.states[value.entity] : undefined; + if (value.temperature != null) return `${this._number(value.temperature)}°`; + // Presets and HVAC modes as Home Assistant names them ("Confort", "Éteint"...). + if (value.preset != null) { + return stateObj && this.hass.formatEntityAttributeValue + ? this.hass.formatEntityAttributeValue(stateObj, "preset_mode", value.preset) + : value.preset; + } + if (value.hvacMode != null) { + return stateObj && this.hass.formatEntityState + ? this.hass.formatEntityState(stateObj, value.hvacMode) + : value.hvacMode; + } + if (value.kind === "on" || value.kind === "off") { + return localize(this.hass, `schedule.${value.kind}`); + } + return value.label; + } + + /** + * Turns a schedule on or off. Heating and cooling the same room at once + * makes no sense: turning one on first turns off the room's enabled + * schedules of the other side. + */ + private async _toggle(schedule: Schedule, room: Room, enabled: boolean) { + if (enabled) { + await this.hass.callService("switch", "turn_off", { + entity_id: schedule.entity_id, + }); + return; + } + const side = climateSide(rowKind(daySegments(schedule))); + const opposite = side + ? room.schedules.filter((s) => { + const other = climateSide(rowKind(daySegments(s))); + return s !== schedule && other && other !== side && this._enabled(s); + }) + : []; + if (opposite.length) { + await this.hass.callService("switch", "turn_off", { + entity_id: opposite.map((s) => s.entity_id), + }); + } + await this.hass.callService("switch", "turn_on", { + entity_id: schedule.entity_id, + }); + } + + static styles = css` + :host { + display: flex; + flex-direction: column; + gap: 10px; + } + .room { + display: flex; + flex-direction: column; + gap: 4px; + } + .room-head { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + } + .room-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 500; + } + .room-name.disabled { + color: var(--secondary-text-color); + } + .row { + display: flex; + align-items: center; + gap: 8px; + } + .row .bar { + flex: 1; + } + /* A disabled schedule keeps its colors, hatched, so it stays readable. */ + .row.disabled .segment { + background-image: repeating-linear-gradient( + 135deg, + rgba(0, 0, 0, 0.3) 0 4px, + transparent 4px 8px + ); + } + .kind { + flex: none; + --mdc-icon-size: 18px; + color: var(--secondary-text-color); + } + .kind.heat, + .kind.preset { + color: #ef9f27; + } + .kind.cool { + color: #378add; + } + .measured { + flex: none; + padding: 1px 8px; + border-radius: 10px; + background: color-mix(in srgb, var(--primary-text-color) 10%, transparent); + font-size: 12px; + } + .switch { + position: relative; + flex: none; + width: 34px; + height: 18px; + border: none; + border-radius: 9px; + background: var(--disabled-color, #888); + cursor: pointer; + padding: 0; + } + .switch::after { + content: ""; + position: absolute; + top: 3px; + left: 3px; + width: 12px; + height: 12px; + border-radius: 50%; + background: #fff; + transition: left 0.15s; + } + .switch.on { + background: var(--switch-checked-track-color, var(--primary-color)); + } + .switch.on::after { + left: 19px; + } + .switch:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; + } + .bar { + position: relative; + height: 20px; + border-radius: 6px; + background: color-mix(in srgb, var(--primary-text-color) 6%, transparent); + overflow: hidden; + } + .segment { + position: absolute; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 600; + overflow: hidden; + white-space: nowrap; + box-shadow: inset -1px 0 0 var(--ha-card-background, var(--card-background-color)); + } + .now { + position: absolute; + top: 0; + bottom: 0; + width: 2px; + margin-left: -1px; + background: var(--primary-text-color); + } + .axis { + position: relative; + height: 14px; + /* Under the bars only: past the kind icon (18 + 8 px) and before the switch (34 + 8 px). */ + margin: -4px 42px 0 26px; + font-size: 11px; + color: var(--secondary-text-color); + } + .axis span { + position: absolute; + transform: translateX(-50%); + } + .axis span:first-child { + transform: none; + } + .axis span:last-child { + transform: translateX(-100%); + } + .empty { + padding: 16px 0; + text-align: center; + font-size: 13px; + color: var(--secondary-text-color); + } + `; +} + +if (!customElements.get("homeshift-schedule-timeline")) { + customElements.define("homeshift-schedule-timeline", HomeShiftScheduleTimeline); +} diff --git a/src/schedule/timeline.ts b/src/schedule/timeline.ts new file mode 100644 index 0000000..422890f --- /dev/null +++ b/src/schedule/timeline.ts @@ -0,0 +1,211 @@ +/** A schedule as returned by the scheduler component's `scheduler` websocket command. */ +export interface Schedule { + schedule_id: string; + entity_id: string; + name: string; + enabled: boolean; + weekdays: string[]; + tags?: string[]; + timeslots: Timeslot[]; +} + +export interface Timeslot { + start: string; + stop: string | null; + actions: { service: string; entity_id?: string; service_data?: Record }[]; +} + +export type ValueKind = "heat" | "cool" | "preset" | "off" | "on" | "other"; + +/** What a timeslot sets its entity to. */ +export interface SlotValue { + kind: ValueKind; + /** Entity of the action the value comes from. */ + entity?: string; + temperature?: number; + /** Preset set by `climate.set_preset_mode` ("comfort", "eco"...). */ + preset?: string; + /** HVAC mode set without a temperature ("off", "heat"...). */ + hvacMode?: string; + /** + * Raw text of the value ("20°", "comfort", "off"...); the card shows + * temperatures, presets and modes in the user's language instead. + */ + label: string; +} + +/** Part of the day, in minutes since midnight, during which a value applies. */ +export interface Segment { + start: number; + end: number; + value: SlotValue; +} + +export const DAY_MINUTES = 24 * 60; + +/** "07:30:00" → 450. */ +export function parseTime(time: string): number { + const [h, m] = time.split(":").map(Number); + return h * 60 + (m || 0); +} + +/** "HH:MM" of a number of minutes since midnight. */ +export function formatMinutes(minutes: number): string { + const m = minutes % DAY_MINUTES; + return `${String(Math.floor(m / 60)).padStart(2, "0")}:${String(m % 60).padStart(2, "0")}`; +} + +const isClimate = (action: Timeslot["actions"][number]) => action.service.startsWith("climate."); + +/** Whether a schedule drives a thermostat: its timeline then shows only the thermostat. */ +function drivesClimate(schedule: Schedule): boolean { + return schedule.timeslots.some((slot) => slot.actions.some(isClimate)); +} + +/** Value set by a timeslot, from its first thermostat action, else its first action. */ +export function slotValue(slot: Timeslot): SlotValue { + const action = slot.actions?.find(isClimate) ?? slot.actions?.[0]; + if (!action) return { kind: "other", label: "" }; + const [, service] = action.service.split("."); + const data = action.service_data ?? {}; + const entity = action.entity_id; + const mode = data.hvac_mode; + if (mode === "off") return { kind: "off", entity, hvacMode: mode, label: mode }; + if (service === "turn_off") return { kind: "off", entity, label: "off" }; + if (typeof data.temperature === "number") { + return { + kind: mode === "cool" ? "cool" : "heat", + entity, + temperature: data.temperature, + label: `${data.temperature}°`, + }; + } + if (data.preset_mode != null) { + const preset = String(data.preset_mode); + return { kind: "preset", entity, preset, label: preset }; + } + if (mode) return { kind: mode === "cool" ? "cool" : "heat", entity, hvacMode: mode, label: mode }; + if (service === "turn_on") return { kind: "on", entity, label: "on" }; + return { kind: "other", entity, label: service ?? action.service }; +} + +/** + * Segments of the day a schedule covers. A timeslot with a stop time spans + * start → stop (across midnight if stop is earlier); a timeslot without one + * is a single action whose value holds until the next timeslot, wrapping + * around midnight. + */ +export function daySegments(schedule: Schedule): Segment[] { + const climate = drivesClimate(schedule); + const slots = schedule.timeslots + .filter((slot) => !climate || slot.actions.some(isClimate)) + .map((slot) => ({ slot, start: parseTime(slot.start) })) + .sort((a, b) => a.start - b.start); + const segments: Segment[] = []; + + slots.forEach(({ slot, start }, i) => { + const value = slotValue(slot); + let end: number; + if (slot.stop != null) { + end = parseTime(slot.stop); + if (end <= start) end += DAY_MINUTES; + } else { + const next = slots[(i + 1) % slots.length].start; + end = next > start ? next : next + DAY_MINUTES; + } + if (end > DAY_MINUTES) { + segments.push({ start, end: DAY_MINUTES, value }); + if (end - DAY_MINUTES > 0) segments.push({ start: 0, end: end - DAY_MINUTES, value }); + } else { + segments.push({ start, end, value }); + } + }); + + return mergeSegments( + segments.filter((s) => s.end > s.start).sort((a, b) => a.start - b.start), + ); +} + +/** Whether two values set the same thing. */ +export function sameValue(a: SlotValue, b: SlotValue): boolean { + return ( + a.kind === b.kind && + a.temperature === b.temperature && + a.preset === b.preset && + a.hvacMode === b.hvacMode && + a.label === b.label + ); +} + +/** Joins back-to-back segments of the same value ("Eco | Eco" → "Eco"). */ +function mergeSegments(segments: Segment[]): Segment[] { + const merged: Segment[] = []; + for (const segment of segments) { + const last = merged[merged.length - 1]; + if (last && last.end === segment.start && sameValue(last.value, segment.value)) { + last.end = segment.end; + } else { + merged.push({ ...segment }); + } + } + return merged; +} + +/** + * Entities a schedule acts on. With `shown`, only those of its timeline: + * its thermostats when it drives some. + */ +export function scheduleEntities(schedule: Schedule, shown = false): string[] { + const climate = shown && drivesClimate(schedule); + const ids = schedule.timeslots.flatMap((slot) => + slot.actions + .filter((a) => !climate || isClimate(a)) + .map((a) => a.entity_id) + .filter((id): id is string => !!id), + ); + return [...new Set(ids)]; +} + +/** Schedules of a tab: tagged with its tag, and acting on its entities when it has some. */ +export function schedulesFor(schedules: Schedule[], tag: string, entities: string[]): Schedule[] { + return schedules + .filter((s) => s.tags?.includes(tag)) + .filter((s) => !entities.length || scheduleEntities(s).some((e) => entities.includes(e))) + .sort((a, b) => a.name.localeCompare(b.name)); +} + +/** Segment background and text colors: warmer for higher setpoints. */ +export function segmentColors(value: SlotValue): { background: string; color: string } { + switch (value.kind) { + case "cool": + return { background: "#378ADD", color: "#fff" }; + case "off": + return { background: "var(--divider-color, #5F5E5A)", color: "var(--secondary-text-color)" }; + case "on": + return { background: "#1D9E75", color: "#fff" }; + case "other": + return { background: "#888780", color: "#fff" }; + case "preset": + return presetColors(value.preset ?? ""); + } + const t = value.temperature ?? 19; + if (t < 15) return { background: "#85B7EB", color: "#042C53" }; + if (t < 17.5) return { background: "#FAC775", color: "#412402" }; + if (t < 19.5) return { background: "#EF9F27", color: "#412402" }; + if (t < 21) return { background: "#D85A30", color: "#fff" }; + return { background: "#A32D2D", color: "#fff" }; +} + +/** + * Preset colors on the same scale as the setpoints: frost and away are + * cold, eco mild, comfort warm, boost hot (Versatile Thermostat and the + * presets Home Assistant defines). + */ +function presetColors(preset: string): { background: string; color: string } { + const p = preset.toLowerCase(); + if (/frost|hors|away|absen|vacation/.test(p)) return { background: "#85B7EB", color: "#042C53" }; + if (/eco|sleep|night|nuit/.test(p)) return { background: "#FAC775", color: "#412402" }; + if (/boost/.test(p)) return { background: "#A32D2D", color: "#fff" }; + if (/comfort|confort|home|activity/.test(p)) return { background: "#EF9F27", color: "#412402" }; + return { background: "#B4B2A9", color: "#2C2C2A" }; +} diff --git a/src/types.ts b/src/types.ts index dceaed7..4107d2e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,13 @@ export interface HomeShiftCardConfig { covers_left_open_entity?: string; /** HomeShift's covers_inhibited sensor: the cover automation pause row. */ covers_inhibited_entity?: string; + /** + * Schedules panel: false hides it. Otherwise it appears on its own once + * the Scheduler integration holds schedules tagged with a day mode. + */ + show_schedules?: boolean; + /** Only the schedules acting on these entities, in the schedules panel. */ + schedule_entities?: string[]; } export type EntityConfigKey = { diff --git a/test/localize.test.ts b/test/localize.test.ts index 4ab9db8..392fbb8 100644 --- a/test/localize.test.ts +++ b/test/localize.test.ts @@ -50,17 +50,21 @@ describe("localize", () => { it("has no key the sources never use", () => { // Vitest runs from the repository root. - const dir = join(process.cwd(), "src/components"); - const source = readdirSync(dir) - .map((f) => readFileSync(join(dir, f), "utf-8")) + const source = ["src/components", "src/schedule"] + .map((sub) => join(process.cwd(), sub)) + .flatMap((dir) => + readdirSync(dir).map((f) => readFileSync(join(dir, f), "utf-8")), + ) .join("\n"); // Keys built at runtime: `thermostat.${key}`, `preview.${key}`, - // `editor.${field.key}` and `card.cover_${action}_...`. + // `editor.${field.key}`, `card.cover_${action}_...` and + // `schedule.${value.kind}`. const dynamic = [ /^thermostat\./, /^preview\./, /^editor\./, /^card\.cover_(open|close)_(time|action|failed)$/, + /^schedule\.(on|off)$/, ]; const unused = leafKeys(en).filter( (key) => diff --git a/test/schedules.test.ts b/test/schedules.test.ts new file mode 100644 index 0000000..896cb9b --- /dev/null +++ b/test/schedules.test.ts @@ -0,0 +1,260 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { HomeShiftCard } from "../src/components/homeshift-card"; +import { Schedule } from "../src/schedule/timeline"; +import { $, entity, homeshiftStates, makeHass, renderCard } from "./helpers"; + +afterEach(() => { + document.body.innerHTML = ""; +}); + +const heat = (entity_id: string, temperature: number) => ({ + service: "climate.set_temperature", + entity_id, + service_data: { temperature, hvac_mode: "heat" }, +}); + +const cool = (entity_id: string, temperature: number) => ({ + service: "climate.set_temperature", + entity_id, + service_data: { temperature, hvac_mode: "cool" }, +}); + +function schedules(): Schedule[] { + return [ + { + schedule_id: "a1", + entity_id: "switch.schedule_travail_salon", + name: "Travail salon", + enabled: true, + weekdays: ["daily"], + tags: ["Travail"], + timeslots: [ + { start: "00:00:00", stop: "07:00:00", actions: [heat("climate.salon", 18)] }, + { start: "07:00:00", stop: "00:00:00", actions: [heat("climate.salon", 20.5)] }, + ], + }, + { + schedule_id: "a2", + entity_id: "switch.schedule_travail_salon_clim", + name: "Travail salon clim", + enabled: false, + weekdays: ["daily"], + tags: ["Travail"], + timeslots: [{ start: "14:00:00", stop: "18:00:00", actions: [cool("climate.salon", 26)] }], + }, + { + schedule_id: "b1", + entity_id: "switch.schedule_maison_bureau", + name: "Maison bureau", + enabled: true, + weekdays: ["daily"], + tags: ["Maison"], + timeslots: [{ start: "08:00:00", stop: "18:00:00", actions: [heat("climate.bureau", 20)] }], + }, + ]; +} + +function schedulerHass(list: Schedule[] = schedules()) { + const states = homeshiftStates(); + states["climate.salon"] = entity("climate.salon", "heat", { + friendly_name: "Salon", + current_temperature: 19.2, + }); + states["switch.schedule_travail_salon"] = entity("switch.schedule_travail_salon", "on"); + states["switch.schedule_travail_salon_clim"] = entity( + "switch.schedule_travail_salon_clim", + "off", + ); + return { ...makeHass(states), callWS: vi.fn().mockResolvedValue(list) }; +} + +async function settle(card: HomeShiftCard) { + for (let i = 0; i < 3; i++) { + await card.updateComplete; + await new Promise((r) => setTimeout(r, 0)); + } +} + +async function render(hass: any, config: Record = {}) { + const card = await renderCard(hass, config); + await settle(card); + return card; +} + +type Lit = HTMLElement & { updateComplete: Promise }; + +/** Stands in for scheduler-card, recording the configuration it receives. */ +class FakeScheduler extends HTMLElement { + config: any; + hass: any; + setConfig(config: any) { + this.config = config; + } +} +if (!customElements.get("scheduler-card")) customElements.define("scheduler-card", FakeScheduler); + +/** Shadow root of the panel: the tabs, the edit button, the content. */ +async function panel(card: HomeShiftCard) { + await settle(card); + const element = $(card, "homeshift-schedule-panel") as Lit; + await element.updateComplete; + return element.shadowRoot!; +} + +/** Shadow root of the timelines of the shown tab. */ +async function timelines(card: HomeShiftCard) { + const element = (await panel(card)).querySelector("homeshift-schedule-timeline") as Lit; + await element.updateComplete; + return element.shadowRoot!; +} + +async function open(card: HomeShiftCard) { + $(card, ".schedules-open")!.click(); + return timelines(card); +} + +describe("schedules panel", () => { + it("shows a bar naming the day mode, panel closed", async () => { + const card = await render(schedulerHass()); + const bar = $(card, ".schedules-open")!; + expect(bar.textContent).toContain("Schedules"); + expect(bar.textContent).toContain("Travail"); + expect(bar.getAttribute("aria-expanded")).toBe("false"); + expect($(card, ".schedules-panel")).toBeNull(); + }); + + it("stays hidden without the Scheduler integration", async () => { + const hass = { ...makeHass(homeshiftStates()), callWS: vi.fn().mockRejectedValue(new Error("unknown command")) }; + const card = await render(hass); + expect($(card, ".schedules-bar")).toBeNull(); + }); + + it("stays hidden when no schedule is tagged with a day mode", async () => { + const list = schedules().map((s) => ({ ...s, tags: ["Other"] })); + const card = await render(schedulerHass(list)); + expect($(card, ".schedules-bar")).toBeNull(); + }); + + it("stays hidden and reads nothing with show_schedules: false", async () => { + const hass = schedulerHass(); + const card = await render(hass, { show_schedules: false }); + expect($(card, ".schedules-bar")).toBeNull(); + expect(hass.callWS).not.toHaveBeenCalled(); + }); + + it("opens over the card with the day mode's rooms", async () => { + const card = await render(schedulerHass()); + const root = await open(card); + expect($(card, ".schedules-close")!.getAttribute("aria-expanded")).toBe("true"); + expect($(card, ".container")!.hasAttribute("inert")).toBe(true); + const rooms = [...root.querySelectorAll(".room-name")].map((r) => r.textContent); + expect(rooms).toEqual(["Salon"]); + expect(root.querySelector(".measured")!.textContent).toBe("19.2°"); + // Heating first, then cooling; the disabled one hatched. + expect([...root.querySelectorAll(".kind")].map((k) => k.getAttribute("icon"))).toEqual([ + "mdi:radiator", + "mdi:snowflake", + ]); + expect(root.querySelectorAll(".row.disabled")).toHaveLength(1); + }); + + it("follows the day mode", async () => { + const hass = schedulerHass(); + hass.states["select.homeshift_day_mode"] = entity("select.homeshift_day_mode", "Maison", { + options: ["Maison", "Travail"], + }); + const card = await render(hass); + const root = await open(card); + expect([...root.querySelectorAll(".row")].map((r) => r.getAttribute("title"))).toEqual([ + "Maison bureau", + ]); + }); + + it("has a tab per day mode, the current one marked and shown", async () => { + const card = await render(schedulerHass()); + await open(card); + const root = await panel(card); + const tabs = [...root.querySelectorAll(".tab")]; + expect(tabs.map((t) => t.textContent!.trim())).toEqual(["Maison", "Travail"]); + expect(tabs[1].getAttribute("aria-selected")).toBe("true"); + expect(tabs[1].querySelector(".active-dot")).not.toBeNull(); + expect(tabs[0].querySelector(".active-dot")).toBeNull(); + + tabs[0].click(); + const rows = [...(await timelines(card)).querySelectorAll(".row")]; + expect(rows.map((r) => r.getAttribute("title"))).toEqual(["Maison bureau"]); + }); + + it("swaps the timelines for the scheduler card of the tab", async () => { + const card = await render(schedulerHass(), { schedule_entities: ["climate.salon"] }); + await open(card); + const root = await panel(card); + const edit = root.querySelector(".edit")!; + expect(edit.getAttribute("aria-label")).toBe("Edit schedules"); + edit.click(); + await panel(card); + await panel(card); + const scheduler = root.querySelector("scheduler-card") as FakeScheduler; + expect(scheduler.config).toMatchObject({ + type: "custom:scheduler-card", + tags: ["Travail"], + include: ["climate.salon"], + }); + expect(root.querySelector("homeshift-schedule-timeline")).toBeNull(); + + edit.click(); + await panel(card); + expect(root.querySelector("scheduler-card")).toBeNull(); + expect(root.querySelector("homeshift-schedule-timeline")).not.toBeNull(); + }); + + it("keeps only the schedules of schedule_entities", async () => { + const card = await render(schedulerHass(), { schedule_entities: ["climate.bureau"] }); + const root = await open(card); + expect(root.querySelector(".empty")!.textContent).toContain("No schedule for Travail"); + }); + + it("closes with the bar or Escape, focus back on the bar", async () => { + const card = await render(schedulerHass()); + await open(card); + $(card, ".schedules-panel")!.dispatchEvent( + new KeyboardEvent("keydown", { key: "Escape", bubbles: true }), + ); + await settle(card); + expect($(card, ".schedules-panel")).toBeNull(); + expect(card.shadowRoot!.activeElement).toBe($(card, ".schedules-open")); + + await open(card); + $(card, ".schedules-close")!.click(); + await settle(card); + expect($(card, ".schedules-panel")).toBeNull(); + }); + + it("turns the cooling on after turning the heating of the room off", async () => { + const hass = schedulerHass(); + const card = await render(hass); + const root = await open(card); + const [, coolSwitch] = root.querySelectorAll(".switch"); + coolSwitch.click(); + await settle(card); + expect(hass.callService.mock.calls).toEqual([ + ["switch", "turn_off", { entity_id: ["switch.schedule_travail_salon"] }], + ["switch", "turn_on", { entity_id: "switch.schedule_travail_salon_clim" }], + ]); + }); + + it("reads the schedules again when a schedule switch changes", async () => { + const hass = schedulerHass(); + const card = await render(hass); + expect(hass.callWS).toHaveBeenCalledTimes(1); + const states = { ...hass.states }; + states["switch.schedule_travail_salon"] = { + ...states["switch.schedule_travail_salon"], + state: "off", + last_updated: "2026-10-10T12:00:00Z", + }; + card.hass = { ...hass, states }; + await settle(card); + expect(hass.callWS).toHaveBeenCalledTimes(2); + }); +}); From 151321296a20d9357960430529c49fa72f3c9119 Mon Sep 17 00:00:00 2001 From: Gamso Date: Sun, 11 Oct 2026 09:30:35 +0200 Subject: [PATCH 23/25] chore(devcontainer): run Home Assistant as the main process, with test data Home Assistant was started in the background from postCreateCommand, which VS Code kills: start.sh is now the container's main process and restarts it. onboard.py creates the admin / admin account on first launch; setup.sh installs Scheduler and scheduler-card v4.0.19 (v4.0.11 froze the browser when opening a schedule), seeds sample schedules tagged with the day modes, simulated thermostats and a HomeShift dashboard. Covers use the modern template syntax, *.sh keep LF endings. Co-Authored-By: Claude Opus 5.5 --- .devcontainer/Dockerfile | 15 +- .devcontainer/README.md | 138 +- .devcontainer/config/configuration.yaml | 194 ++- .devcontainer/config/ui-lovelace.yaml | 50 - .devcontainer/dashboard.yaml | 56 + .devcontainer/devcontainer.json | 9 +- .devcontainer/onboard.py | 69 + .devcontainer/scheduler.storage | 1722 +++++------------------ .devcontainer/setup.sh | 305 ++-- .devcontainer/start.sh | 27 + .gitattributes | 1 + .gitignore | 5 + 12 files changed, 867 insertions(+), 1724 deletions(-) delete mode 100644 .devcontainer/config/ui-lovelace.yaml create mode 100644 .devcontainer/dashboard.yaml create mode 100644 .devcontainer/onboard.py create mode 100644 .devcontainer/start.sh create mode 100644 .gitattributes diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d6936e8..e02684c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,12 +1,11 @@ -FROM ghcr.io/home-assistant/home-assistant:latest +FROM ghcr.io/home-assistant/home-assistant:stable -# Install Node.js and npm -RUN apk add --no-cache nodejs npm +# Node.js to build the card, wget/unzip to fetch the scheduler component and card +RUN apk add --no-cache nodejs npm wget unzip -# Install additional tools if needed -# RUN apk add --no-cache bash - -# The Home Assistant image already has an entrypoint that starts Home Assistant -# We'll let it run in the background +# Home Assistant is run by start.sh, not by the image's s6 init: it waits for +# the seeds of setup.sh (scheduler config entry, schedules, dashboard) to be in place. +ENTRYPOINT [] +CMD ["bash", "/workspaces/homeshift_card/.devcontainer/start.sh"] WORKDIR /workspaces/homeshift_card diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 2f8134a..5375385 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -1,126 +1,58 @@ -# DevContainer Development Environment +# DevContainer Home Assistant -Ce projet inclut une configuration DevContainer pour développer et tester une carte Home Assistant dans un environnement isolé. +Home Assistant avec les entités HomeShift simulées, Scheduler, des thermostats simulés et des planifications d'exemple pour voir la carte en situation, panneau des plannings compris. ## Prérequis -- Visual Studio Code +- Visual Studio Code + extension Dev Containers - Docker Desktop -- Extension VS Code: Dev Containers -## Démarrage rapide +## Démarrage -1. Ouvrir le dossier dans VS Code -2. F1 → "Dev Containers: Reopen in Container" -3. Attendre l’initialisation de Home Assistant (1–2 min la première fois) -4. Construire la - ```bash - npm install - npm run build - ``` -5. Accéder à Home Assistant: http://localhost:8123 +1. Ouvrir le dossier dans VS Code → F1 → **Dev Containers: Reopen in Container** (ou **Rebuild Container** après une modification du `Dockerfile` ou de `devcontainer.json`) +2. `setup.sh` (une fois) : `npm install` + build, scheduler-component v3.3.8, scheduler-card v4.0.19, planifications d'exemple, intégration Scheduler, ressources Lovelace, dashboard **HomeShift**. +3. Home Assistant est le processus principal du conteneur (`start.sh`) : il démarre dès la fin de `setup.sh`, puis à chaque démarrage du conteneur → http://localhost:8123 (1-2 min au premier lancement) +4. Au premier lancement, `onboard.py` termine l'onboarding tout seul : se connecter avec le compte de test **`admin` / `admin`** +5. Ouvrir le dashboard **HomeShift** dans la barre latérale -## Ressource de la carte +L'intégration Scheduler est préconfigurée : inutile de l'ajouter depuis l'UI. -Le dossier `dist/` est monté sur `/config/www/homeshift_card` et `setup.sh` enregistre la ressource dans `.storage/lovelace_resources` (dashboards en mode `storage`) : +## Itérer sur la carte -- URL: `/local/homeshift_card/homeshift-card.js` -- Type: Module +`dist/` est monté sur `/config/www/homeshift_card` : -Après `npm run build` (ou pendant `npm run watch`), un rafraîchissement du navigateur suffit. +```bash +npm run watch +``` -## Dashboard Lovelace +puis Ctrl+F5 dans le navigateur. -La configuration utilise un **dashboard YAML dédié** (`ui-lovelace.yaml`) tout en laissant le dashboard principal en mode `storage` : +## Données d'exemple -| Dashboard | Mode | Accès | Editeur graphique | -| ---------------------- | ------------------------- | ---------------------------- | ----------------- | -| **Overview** (sidebar) | YAML — `ui-lovelace.yaml` | icône dans la barre latérale | ❌ | -| **Accueil** (défaut) | Storage | icône maison | ✅ | +| Entité | Rôle | +| ---------------------------------------------------------- | ---------------------------------------------------------------------- | +| `select.homeshift_day_mode` / `homeshift_thermostat_mode` | Modes du jour et du thermostat (template select sur `input_select`) | +| `number.homeshift_*`, `sensor.homeshift_*`, `button.*` | Les autres entités de l'intégration HomeShift, simulées | +| `input_boolean.homeshift_window_*_backing` | Fenêtres ouvertes : font apparaître l'alerte « Non fermés » | +| `climate.bureau` / `chambre` / `salon` | `generic_thermostat` sur radiateurs `input_boolean` | +| `input_number.temperature_*_backing` | Températures mesurées, réglables | +| `switch.salon_enable_auto_start_stop` | Template switch piloté par les planifications | +| `input_boolean.force_presence` | Entité propre au mode Travail | -- La carte `homeshift-card` est définie dans `ui-lovelace.yaml` et apparaît automatiquement dans l'onglet **Overview**. -- Le dashboard **Accueil** reste entièrement éditable via l'interface graphique de HA. +`scheduler.storage` contient 13 planifications taguées `Maison`, `Télétravail`, `Travail` et `Absence`, les options du mode du jour : le panneau des plannings de la carte les affiche. Il n'est copié que s'il n'existe pas encore dans `config/.storage/`. Pour revenir aux exemples : supprimer `config/.storage/scheduler.storage` puis reconstruire le conteneur. -> Toute modification de `ui-lovelace.yaml` est prise en compte en rafraîchissant le navigateur (F5). +Le dashboard **HomeShift** est éditable dans l'UI. Il est créé au premier setup à partir de `dashboard.yaml` : la carte, une section de simulation et une scheduler-card pour modifier les planifications. Les modifications faites dans l'UI sont conservées ; pour revenir au modèle, supprimer `config/.storage/lovelace.dashboard_homeshift` puis reconstruire le conteneur. -## Entités +## Repartir de zéro -En production, les entités proviennent de l'intégration **HomeShift** (https://github.com/Gamso/homeshift): +Tout l'état de Home Assistant (base, `.storage`, comptes, logs) vit dans `config/`, ignoré par git sauf `configuration.yaml`. Pour une instance neuve (compte `admin` / `admin` recréé) : supprimer tout `config/` sauf `configuration.yaml`, puis **Rebuild Container**. -- `select.homeshift_day_mode`: options configurables par l'utilisateur (défaut: Maison, Travail, Télétravail, Absence) -- `select.homeshift_thermostat_mode`: options configurables par l'utilisateur (défaut: Eteint, Chauffage, Climatisation, Ventilation) -- `number.homeshift_override_duration` / `number.homeshift_early_switch` : dérogation et anticipation, en minutes -- `sensor.homeshift_next_mode` / `sensor.homeshift_next_mode_at` : prochain mode et son heure -- `sensor.homeshift_cover_open_time` / `sensor.homeshift_cover_close_time`, `binary_sensor.homeshift_cover_heat_active`, `binary_sensor.homeshift_covers_left_open` : volets -- `button.homeshift_open_covers` / `button.homeshift_close_covers` : ouverture / fermeture immédiate des volets +## Fichiers -> **Devcontainer** : en l'absence de l'intégration HomeShift, ces entités sont simulées dans `configuration.yaml` par des entités `template` (mêmes identifiants) adossées à des `input_*` `..._backing` ; les boutons créent une notification persistante. - -Des entités `input_boolean` sont incluses pour tester le scheduler: - -- `input_boolean.test_switch`: Interrupteur de test -- `input_boolean.test_light`: Lumière de test - -## Scheduler Integration - -L'environnement de test inclut automatiquement: - -- **scheduler-component** (v3.3.8): Composant permettant de créer des planifications -- **scheduler-card** (v4.0.11): Interface UI pour gérer les planifications - -Ces composants sont automatiquement téléchargés et installés lors de la première initialisation du conteneur. - -### ⚠️ IMPORTANT: Configuration Requise - -**L'intégration Scheduler doit être ajoutée manuellement via l'interface Home Assistant:** - -1. Attendre que Home Assistant soit complètement démarré (1-2 minutes) -2. Compléter l'onboarding (créer un compte admin/admin si nécessaire) -3. Aller dans **Paramètres** → **Appareils et Services** -4. Cliquer sur **+ Ajouter une intégration** -5. Rechercher **"Scheduler"** et cliquer pour l'ajouter -6. L'intégration sera alors configurée et fonctionnelle - -### Utilisation du Scheduler - -Une fois l'intégration ajoutée: - -1. Accéder à l'onglet "Scheduler" dans le dashboard -2. Utiliser la carte scheduler-card pour créer des planifications -3. Sélectionner les entités `input_boolean.test_switch` ou `input_boolean.test_light` pour tester - -### Dépannage - -Si l'intégration "Scheduler" n'apparaît pas dans la liste: - -**⚠️ Pour les utilisateurs ayant une installation précédente:** -Si vous avez utilisé une version antérieure du DevContainer, les fichiers peuvent avoir été installés incorrectement. Solution: - -1. Fermer VS Code et arrêter le DevContainer -2. Supprimer le volume Docker associé au conteneur -3. Rouvrir dans le DevContainer (reconstruction complète) -4. Les fichiers seront installés correctement dans `/config/custom_components/scheduler/` - -**Autres vérifications:** - -- Vérifier que Home Assistant est complètement démarré (voir les logs) -- Vérifier que les fichiers sont dans `/config/custom_components/scheduler/` avec: - ```bash - ls -la /config/custom_components/scheduler/ - # Doit afficher: manifest.json, __init__.py, etc. - ``` -- Redémarrer Home Assistant: **Paramètres** → **Système** → **Redémarrer** -- Rafraîchir le navigateur (Ctrl+F5) -- Vider le cache du navigateur et recharger - -**Si les fichiers sont au mauvais endroit:** -Si vous voyez des fichiers Python directement dans `/config/custom_components/` (au lieu de `/config/custom_components/scheduler/`), c'est une installation incorrecte. Le script de setup les nettoiera automatiquement lors du prochain redémarrage du conteneur. +- `config/configuration.yaml`, `dashboard.yaml`, `scheduler.storage` : versionnés +- le reste de `config/` : état local ignoré par git ## Debug -- Logs HA: `/config/home-assistant.log` -- Console navigateur: F12 - -## Arrêt - -- Fermer VS Code ou F1 → "Dev Containers: Reopen Folder Locally" +- Logs HA : `tail -f /config/home-assistant.log` +- Redémarrer HA : depuis l'UI (Paramètres → Système → Redémarrer) ou `pkill -f "^python3 -m homeassistant"` ; `start.sh` le relance automatiquement diff --git a/.devcontainer/config/configuration.yaml b/.devcontainer/config/configuration.yaml index e6b0d15..fe0f21a 100644 --- a/.devcontainer/config/configuration.yaml +++ b/.devcontainer/config/configuration.yaml @@ -19,8 +19,9 @@ logger: logs: homeassistant.components.lovelace: debug -# User interface -# Resources are registered via .storage/lovelace_resources (storage mode ignores configuration.yaml resources) +# User interface: every dashboard is editable in the UI. setup.sh creates the +# resources and the "HomeShift" dashboard in .storage +# (dashboard seed: .devcontainer/dashboard.yaml). lovelace: mode: storage @@ -129,6 +130,22 @@ template: unique_id: homeshift_next_mode_at state: "{{ (now() + timedelta(hours=2, minutes=15)).isoformat() }}" device_class: timestamp + # Measured room temperatures, set with the input_number helpers. + - name: Température bureau + unique_id: temperature_bureau + device_class: temperature + unit_of_measurement: "°C" + state: "{{ states('input_number.temperature_bureau_backing') }}" + - name: Température chambre + unique_id: temperature_chambre + device_class: temperature + unit_of_measurement: "°C" + state: "{{ states('input_number.temperature_chambre_backing') }}" + - name: Température salon + unique_id: temperature_salon + device_class: temperature + unit_of_measurement: "°C" + state: "{{ states('input_number.temperature_salon_backing') }}" - name: "homeshift_cover_open_time" unique_id: homeshift_cover_open_time state: "{{ states('input_text.homeshift_cover_open_time_backing') }}" @@ -137,6 +154,54 @@ template: unique_id: homeshift_cover_close_time state: "{{ states('input_text.homeshift_cover_close_time_backing') }}" icon: mdi:roller-shade-closed + # Test cover simulating the HomeShift daily cover group (the legacy + # cover_entity option calls cover.open_cover / close_cover on it). + - cover: + - name: HomeShift Daily Covers + unique_id: homeshift_daily_covers + device_class: shutter + state: "{{ 'open' if is_state('input_boolean.homeshift_cover_state_backing', 'on') else 'closed' }}" + open_cover: + action: input_boolean.turn_on + target: + entity_id: input_boolean.homeshift_cover_state_backing + close_cover: + action: input_boolean.turn_off + target: + entity_id: input_boolean.homeshift_cover_state_backing + # The covers the simulated windows belong to. They exist only so the + # card can resolve their friendly names; up while their window is open. + - name: Volet chambre + unique_id: volet_chambre + device_class: shutter + state: "{{ 'open' if is_state('input_boolean.homeshift_window_chambre_backing', 'on') else 'closed' }}" + open_cover: + - delay: 0 + close_cover: + - delay: 0 + - name: Volet salon + unique_id: volet_salon + device_class: shutter + state: "{{ 'open' if is_state('input_boolean.homeshift_window_salon_backing', 'on') else 'closed' }}" + open_cover: + - delay: 0 + close_cover: + - delay: 0 + # Simulates the auto start/stop switch of the living room air conditioner + # (driven by the sample schedules). + - switch: + - name: Salon enable auto start stop + unique_id: salon_enable_auto_start_stop + icon: mdi:air-conditioner + state: "{{ is_state('input_boolean.salon_auto_start_stop_backing', 'on') }}" + turn_on: + action: input_boolean.turn_on + target: + entity_id: input_boolean.salon_auto_start_stop_backing + turn_off: + action: input_boolean.turn_off + target: + entity_id: input_boolean.salon_auto_start_stop_backing - binary_sensor: - name: "homeshift_cover_heat_active" unique_id: homeshift_cover_heat_active @@ -194,6 +259,27 @@ input_number: step: 5 unit_of_measurement: min icon: mdi:clock-fast + temperature_bureau_backing: + name: Température bureau (simulation) + min: 10 + max: 30 + step: 0.5 + initial: 19 + unit_of_measurement: "°C" + temperature_chambre_backing: + name: Température chambre (simulation) + min: 10 + max: 30 + step: 0.5 + initial: 18 + unit_of_measurement: "°C" + temperature_salon_backing: + name: Température salon (simulation) + min: 10 + max: 30 + step: 0.5 + initial: 20.5 + unit_of_measurement: "°C" # Simulates sensor.homeshift_next_mode (text) from the HomeShift integration. input_text: @@ -209,23 +295,24 @@ input_text: initial: "21:15" icon: mdi:roller-shade-closed -# Scheduler integration -# NOTE: The scheduler integration must be added via the Home Assistant UI first! -# Steps to add: -# 1. Go to Settings → Devices & Services -# 2. Click "+ Add Integration" -# 3. Search for "Scheduler" and add it -# -# After adding the integration, you can customize it by uncommenting and editing below: -# scheduler: -# customize: -# light: -# icon: mdi:lightbulb -# switch: -# icon: mdi:power-plug +# The Scheduler integration is set up by setup.sh (config entry in .storage), +# with the sample schedules of .devcontainer/scheduler.storage. -# Test entities for scheduler input_boolean: + force_presence: + name: Forcer la présence + icon: mdi:account-lock + salon_auto_start_stop_backing: + name: Auto start/stop salon (backing) + radiateur_bureau: + name: Radiateur bureau + icon: mdi:radiator + radiateur_chambre: + name: Radiateur chambre + icon: mdi:radiator + radiateur_salon: + name: Radiateur salon + icon: mdi:radiator test_switch: name: Test Switch icon: mdi:power-plug @@ -247,38 +334,41 @@ input_boolean: name: Fenêtre salon ouverte (simulation) icon: mdi:window-open-variant -# Test cover entity simulating the HomeShift daily cover group -# (select.cover_entity in the card triggers cover.open_cover/close_cover on this entity). -cover: - - platform: template - covers: - homeshift_daily_covers: - friendly_name: "HomeShift Daily Covers" - device_class: shutter - value_template: "{{ 'open' if is_state('input_boolean.homeshift_cover_state_backing', 'on') else 'closed' }}" - open_cover: - service: input_boolean.turn_on - target: - entity_id: input_boolean.homeshift_cover_state_backing - close_cover: - service: input_boolean.turn_off - target: - entity_id: input_boolean.homeshift_cover_state_backing - # The covers the simulated windows belong to. They exist only so the - # card can resolve their friendly names; up while their window is open. - volet_chambre: - friendly_name: "Volet chambre" - device_class: shutter - value_template: "{{ 'open' if is_state('input_boolean.homeshift_window_chambre_backing', 'on') else 'closed' }}" - open_cover: - - delay: 0 - close_cover: - - delay: 0 - volet_salon: - friendly_name: "Volet salon" - device_class: shutter - value_template: "{{ 'open' if is_state('input_boolean.homeshift_window_salon_backing', 'on') else 'closed' }}" - open_cover: - - delay: 0 - close_cover: - - delay: 0 +# Thermostats driven by the sample schedules: climate.bureau, climate.chambre, climate.salon +climate: + - platform: generic_thermostat + name: Bureau + unique_id: climate_bureau + heater: input_boolean.radiateur_bureau + target_sensor: sensor.temperature_bureau + min_temp: 7 + max_temp: 28 + target_temp: 19 + away_temp: 12 + eco_temp: 17 + comfort_temp: 20 + precision: 0.5 + - platform: generic_thermostat + name: Chambre + unique_id: climate_chambre + heater: input_boolean.radiateur_chambre + target_sensor: sensor.temperature_chambre + min_temp: 7 + max_temp: 28 + target_temp: 18 + away_temp: 12 + eco_temp: 17 + comfort_temp: 20 + precision: 0.5 + - platform: generic_thermostat + name: Salon + unique_id: climate_salon + heater: input_boolean.radiateur_salon + target_sensor: sensor.temperature_salon + min_temp: 7 + max_temp: 28 + target_temp: 20 + away_temp: 12 + eco_temp: 17 + comfort_temp: 20 + precision: 0.5 diff --git a/.devcontainer/config/ui-lovelace.yaml b/.devcontainer/config/ui-lovelace.yaml deleted file mode 100644 index 2dea6c0..0000000 --- a/.devcontainer/config/ui-lovelace.yaml +++ /dev/null @@ -1,50 +0,0 @@ -title: HomeShift Dashboard -# NOTE: Ce fichier est une référence documentaire uniquement. -# En mode "storage", Home Assistant utilise .storage/lovelace comme configuration active. -# Pour modifier le tableau de bord, utiliser l'éditeur HA ou éditer .storage/lovelace directement. -views: - - title: Home - path: home - icon: mdi:home - cards: - - type: custom:homeshift-card - name: Thermostat - day_mode_entity: select.homeshift_day_mode - thermostat_mode_entity: select.homeshift_thermostat_mode - override_duration_entity: number.homeshift_override_duration - early_switch_entity: number.homeshift_early_switch - next_mode_entity: sensor.homeshift_next_mode - next_mode_at_entity: sensor.homeshift_next_mode_at - heat_protection_entity: binary_sensor.homeshift_cover_heat_active - cover_open_time_entity: sensor.homeshift_cover_open_time - cover_close_time_entity: sensor.homeshift_cover_close_time - open_covers_entity: button.homeshift_open_covers - close_covers_entity: button.homeshift_close_covers - covers_left_open_entity: binary_sensor.homeshift_covers_left_open - - - type: entities - title: Entités HomeShift - entities: - - entity: select.homeshift_day_mode - - entity: select.homeshift_thermostat_mode - - entity: number.homeshift_override_duration - - entity: number.homeshift_early_switch - - entity: sensor.homeshift_next_mode - - entity: sensor.homeshift_next_mode_at - - entity: binary_sensor.homeshift_cover_heat_active - - entity: input_boolean.homeshift_window_chambre_backing - - entity: input_boolean.homeshift_window_salon_backing - - entity: binary_sensor.homeshift_covers_left_open - - - title: Scheduler - path: scheduler - icon: mdi:calendar-clock - cards: - - type: custom:scheduler-card - title: Schedules - - - type: entities - title: Test Entities - entities: - - entity: input_boolean.test_switch - - entity: input_boolean.test_light diff --git a/.devcontainer/dashboard.yaml b/.devcontainer/dashboard.yaml new file mode 100644 index 0000000..224ad76 --- /dev/null +++ b/.devcontainer/dashboard.yaml @@ -0,0 +1,56 @@ +# Seed of the "HomeShift" dashboard (storage mode, editable in the UI). +# setup.sh copies it into .storage/lovelace.dashboard_homeshift when that file does not exist yet. +title: HomeShift +views: + - title: HomeShift + path: homeshift + icon: mdi:home-clock + type: sections + max_columns: 3 + sections: + - type: grid + cards: + - type: heading + heading: Carte + icon: mdi:home-clock + # Every entity option defaults to the simulated HomeShift entities. + - type: custom:homeshift-card + + - type: grid + cards: + - type: heading + heading: Simulation + icon: mdi:tune + - type: entities + entities: + - entity: select.homeshift_day_mode + name: Mode du jour + - entity: select.homeshift_thermostat_mode + name: Mode thermostat + - number.homeshift_override_duration + - number.homeshift_early_switch + - input_text.homeshift_next_mode_backing + - input_boolean.homeshift_heat_protection_backing + - input_boolean.homeshift_window_chambre_backing + - input_boolean.homeshift_window_salon_backing + - input_number.temperature_bureau_backing + - input_number.temperature_chambre_backing + - input_number.temperature_salon_backing + - type: thermostat + entity: climate.salon + + - type: grid + cards: + - type: heading + heading: Planifications + icon: mdi:calendar-clock + # Edits the schedules shown in the card's schedules panel. + - type: custom:scheduler-card + title: false + discover_existing: false + include: + - climate.bureau + - climate.chambre + - climate.salon + - switch.salon_enable_auto_start_stop + - input_boolean.force_presence diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5afddef..f076269 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "name": "Home Assistant Dev Environment", + "name": "HomeShift Card - Home Assistant", "build": { "dockerfile": "Dockerfile" }, @@ -13,7 +13,14 @@ "onAutoForward": "notify" } }, + // Home Assistant est le processus principal du conteneur (CMD du Dockerfile : start.sh) ; + // VS Code ne doit pas le remplacer par sa boucle "sleep". + "overrideCommand": false, + // Installe les dépendances et prépare /config ; start.sh attend la fin de ce script. "postCreateCommand": "bash /workspaces/homeshift_card/.devcontainer/setup.sh", + // Chemin fixe dans le conteneur, quel que soit le nom du dossier sur la machine. + "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/homeshift_card,type=bind", + "workspaceFolder": "/workspaces/homeshift_card", "mounts": [ "source=${localWorkspaceFolder}/.devcontainer/config,target=/config,type=bind", "source=${localWorkspaceFolder}/dist,target=/config/www/homeshift_card,type=bind" diff --git a/.devcontainer/onboard.py b/.devcontainer/onboard.py new file mode 100644 index 0000000..1fa3b0a --- /dev/null +++ b/.devcontainer/onboard.py @@ -0,0 +1,69 @@ +"""Completes the onboarding of the devcontainer's Home Assistant on its own. + +Creates the test account admin / admin and goes through the remaining +onboarding steps, so http://localhost:8123 opens on the login page. Does +nothing once the onboarding is done (an existing instance keeps its users). +""" + +import json +import time +import urllib.error +import urllib.parse +import urllib.request + +BASE = "http://127.0.0.1:8123" +CLIENT_ID = f"{BASE}/" +USER = {"name": "Admin", "username": "admin", "password": "admin", "language": "fr"} + + +def request(path, data=None, token=None, form=False): + headers = {} + body = None + if token: + headers["Authorization"] = f"Bearer {token}" + if data is not None: + if form: + body = urllib.parse.urlencode(data).encode() + headers["Content-Type"] = "application/x-www-form-urlencoded" + else: + body = json.dumps(data).encode() + headers["Content-Type"] = "application/json" + req = urllib.request.Request(BASE + path, data=body, headers=headers) + with urllib.request.urlopen(req, timeout=10) as response: + text = response.read().decode() + return json.loads(text) if text else None + + +def wait_for_home_assistant(timeout=900): + """Onboarding steps, once the HTTP server answers.""" + deadline = time.time() + timeout + while time.time() < deadline: + try: + return request("/api/onboarding") + except (urllib.error.URLError, ConnectionError, TimeoutError): + time.sleep(5) + raise SystemExit("onboard: Home Assistant did not answer, onboarding skipped") + + +def main(): + steps = {step["step"]: step["done"] for step in wait_for_home_assistant()} + if steps.get("user"): + return + code = request("/api/onboarding/users", {**USER, "client_id": CLIENT_ID})["auth_code"] + token = request( + "/auth/token", + {"grant_type": "authorization_code", "code": code, "client_id": CLIENT_ID}, + form=True, + )["access_token"] + request("/api/onboarding/core_config", {}, token) + request("/api/onboarding/analytics", {}, token) + request( + "/api/onboarding/integration", + {"client_id": CLIENT_ID, "redirect_uri": f"{BASE}/?auth_callback=1"}, + token, + ) + print("onboard: account admin / admin created") + + +if __name__ == "__main__": + main() diff --git a/.devcontainer/scheduler.storage b/.devcontainer/scheduler.storage index df0d78b..7870415 100644 --- a/.devcontainer/scheduler.storage +++ b/.devcontainer/scheduler.storage @@ -5,46 +5,53 @@ "data": { "schedules": [ { - "schedule_id": "ea9150", + "schedule_id": "1a2b30", "timeslots": [ { - "start": "08:30:00", - "stop": null, + "start": "00:00:00", + "stop": "07:00:00", "conditions": [], "condition_type": null, "track_conditions": false, "actions": [ { - "service": "cover.open_cover", - "entity_id": "cover.volets", - "service_data": {} + "service": "climate.set_preset_mode", + "entity_id": "climate.bureau", + "service_data": { + "preset_mode": "eco" + } } ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Ouverture volets maison", - "enabled": true - }, - { - "schedule_id": "f9cd26", - "timeslots": [ + }, { - "start": "07:52", - "stop": null, + "start": "07:00:00", + "stop": "22:00:00", "conditions": [], "condition_type": null, "track_conditions": false, "actions": [ { - "service": "cover.open_cover", - "entity_id": "cover.volets", - "service_data": {} + "service": "climate.set_preset_mode", + "entity_id": "climate.bureau", + "service_data": { + "preset_mode": "comfort" + } + } + ] + }, + { + "start": "22:00:00", + "stop": "00:00:00", + "conditions": [], + "condition_type": null, + "track_conditions": false, + "actions": [ + { + "service": "climate.set_preset_mode", + "entity_id": "climate.bureau", + "service_data": { + "preset_mode": "eco" + } } ] } @@ -55,53 +62,39 @@ "start_date": null, "end_date": null, "repeat_type": "repeat", - "name": "Ouverture volets travail", + "name": "Chauffage maison bureau", "enabled": false }, { - "schedule_id": "011e2a", + "schedule_id": "1a2b31", "timeslots": [ { "start": "00:00:00", - "stop": "05:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", + "stop": "06:30:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { "service": "climate.set_temperature", - "entity_id": "climate.salon", + "entity_id": "climate.chambre", "service_data": { - "temperature": 17.5, + "temperature": 17, "hvac_mode": "heat" } } ] }, { - "start": "05:00:00", - "stop": "07:10:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", + "start": "06:30:00", + "stop": "21:00:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { "service": "climate.set_temperature", - "entity_id": "climate.salon", + "entity_id": "climate.chambre", "service_data": { "temperature": 18, "hvac_mode": "heat" @@ -110,48 +103,65 @@ ] }, { - "start": "07:10:00", - "stop": "15:30:00", - "conditions": [ + "start": "21:00:00", + "stop": "00:00:00", + "conditions": [], + "condition_type": null, + "track_conditions": false, + "actions": [ { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" + "service": "climate.set_temperature", + "entity_id": "climate.chambre", + "service_data": { + "temperature": 17.5, + "hvac_mode": "heat" + } } - ], - "condition_type": "or", + ] + } + ], + "weekdays": [ + "daily" + ], + "start_date": null, + "end_date": null, + "repeat_type": "repeat", + "name": "Chauffage maison chambre", + "enabled": false + }, + { + "schedule_id": "1a2b32", + "timeslots": [ + { + "start": "00:00:00", + "stop": "07:00:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { "service": "climate.set_temperature", "entity_id": "climate.salon", "service_data": { - "temperature": 17.5, + "temperature": 18, "hvac_mode": "heat" } } ] }, { - "start": "15:30:00", + "start": "07:00:00", "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "climate.set_preset_mode", + "service": "climate.set_temperature", "entity_id": "climate.salon", "service_data": { - "preset_mode": "comfort" + "temperature": 20.5, + "hvac_mode": "heat" } } ] @@ -163,177 +173,194 @@ "start_date": null, "end_date": null, "repeat_type": "repeat", - "name": "Chauffage travail salon", + "name": "Chauffage maison salon", "enabled": false }, { - "schedule_id": "60b115", + "schedule_id": "1a2b33", "timeslots": [ { - "start": "00:00:00", - "stop": "06:30:00", - "conditions": [ + "start": "10:00:00", + "stop": null, + "conditions": [], + "condition_type": null, + "track_conditions": false, + "actions": [ { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" + "service": "switch.turn_on", + "entity_id": "switch.salon_enable_auto_start_stop", + "service_data": {} } - ], - "condition_type": "or", + ] + }, + { + "start": "11:00:00", + "stop": "15:00:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "climate.set_preset_mode", - "entity_id": "climate.bureau", + "service": "climate.set_temperature", + "entity_id": "climate.salon", "service_data": { - "preset_mode": "eco" + "temperature": 26, + "hvac_mode": "cool" } } ] }, { - "start": "06:30:00", - "stop": "07:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", + "start": "15:00:00", + "stop": "20:00:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { "service": "climate.set_temperature", - "entity_id": "climate.bureau", + "entity_id": "climate.salon", "service_data": { - "temperature": 17, - "hvac_mode": "heat" + "temperature": 25, + "hvac_mode": "cool" } } ] }, { - "start": "07:00:00", - "stop": "17:00:00", - "conditions": [ + "start": "23:00:00", + "stop": null, + "conditions": [], + "condition_type": null, + "track_conditions": false, + "actions": [ { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" + "service": "switch.turn_off", + "entity_id": "switch.salon_enable_auto_start_stop", + "service_data": {} } - ], - "condition_type": "or", + ] + } + ], + "weekdays": [ + "daily" + ], + "start_date": null, + "end_date": null, + "repeat_type": "repeat", + "name": "Climatisation maison salon", + "enabled": true + }, + { + "schedule_id": "1a2b34", + "timeslots": [ + { + "start": "00:00:00", + "stop": "08:00:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "climate.set_preset_mode", + "service": "climate.set_temperature", "entity_id": "climate.bureau", "service_data": { - "preset_mode": "eco" + "temperature": 17, + "hvac_mode": "heat" } } ] }, { - "start": "17:00:00", - "stop": "22:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", + "start": "08:00:00", + "stop": "18:30:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { "service": "climate.set_temperature", "entity_id": "climate.bureau", "service_data": { - "temperature": 19.5, + "temperature": 20.5, "hvac_mode": "heat" } } ] }, { - "start": "22:00:00", + "start": "18:30:00", "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "climate.set_preset_mode", + "service": "climate.set_temperature", "entity_id": "climate.bureau", "service_data": { - "preset_mode": "eco" + "temperature": 17, + "hvac_mode": "heat" } } ] } ], "weekdays": [ - "daily" + "mon", + "tue", + "wed", + "thu", + "fri" ], "start_date": null, "end_date": null, "repeat_type": "repeat", - "name": "Chauffage travail bureau", - "enabled": false + "name": "Chauffage télétravail bureau", + "enabled": true }, { - "schedule_id": "7ceade", + "schedule_id": "1a2b35", "timeslots": [ { "start": "00:00:00", "stop": "06:30:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "climate.set_preset_mode", + "service": "climate.set_temperature", "entity_id": "climate.chambre", "service_data": { - "preset_mode": "eco" + "temperature": 17, + "hvac_mode": "heat" } } ] }, { "start": "06:30:00", - "stop": "07:00:00", - "conditions": [ + "stop": "08:00:00", + "conditions": [], + "condition_type": null, + "track_conditions": false, + "actions": [ { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" + "service": "climate.set_temperature", + "entity_id": "climate.chambre", + "service_data": { + "temperature": 19, + "hvac_mode": "heat" + } } - ], - "condition_type": "or", + ] + }, + { + "start": "08:00:00", + "stop": "21:00:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { @@ -347,1204 +374,352 @@ ] }, { - "start": "07:00:00", + "start": "21:00:00", "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "climate.set_preset_mode", + "service": "climate.set_temperature", "entity_id": "climate.chambre", "service_data": { - "preset_mode": "eco" + "temperature": 18, + "hvac_mode": "heat" } } ] } ], "weekdays": [ - "daily" + "mon", + "tue", + "wed", + "thu", + "fri" ], "start_date": null, "end_date": null, "repeat_type": "repeat", - "name": "Chauffage travail chambre", - "enabled": false + "name": "Chauffage télétravail chambre", + "enabled": true }, { - "schedule_id": "636997", + "schedule_id": "1a2b36", "timeslots": [ { "start": "00:00:00", - "stop": "12:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", + "stop": "07:00:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "climate.set_preset_mode", - "entity_id": "climate.bureau", + "service": "climate.set_temperature", + "entity_id": "climate.salon", "service_data": { - "preset_mode": "eco" + "temperature": 18, + "hvac_mode": "heat" } } ] }, { - "start": "12:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", + "start": "07:00:00", + "stop": "23:00:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "climate.set_preset_mode", - "entity_id": "climate.bureau", - "service_data": { - "preset_mode": "eco" - } - } - ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Chauffage absence bureau", - "enabled": false - }, - { - "schedule_id": "268dc6", - "timeslots": [ - { - "start": "00:00:00", - "stop": "12:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.chambre", - "service_data": { - "preset_mode": "eco" - } - } - ] - }, - { - "start": "12:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.chambre", - "service_data": { - "preset_mode": "eco" - } - } - ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Chauffage absence chambre", - "enabled": false - }, - { - "schedule_id": "6779e2", - "timeslots": [ - { - "start": "00:00:00", - "stop": "12:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_temperature", - "entity_id": "climate.salon", - "service_data": { - "hvac_mode": "heat", - "temperature": 17.5 - } - } - ] - }, - { - "start": "12:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_temperature", - "entity_id": "climate.salon", - "service_data": { - "hvac_mode": "heat", - "temperature": 17.5 - } - } - ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Chauffage absence salon", - "enabled": false - }, - { - "schedule_id": "2e86ad", - "timeslots": [ - { - "start": "00:00:00", - "stop": "05:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_temperature", - "entity_id": "climate.salon", - "service_data": { - "temperature": 17.5, - "hvac_mode": "heat" - } - } - ] - }, - { - "start": "05:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.salon", - "service_data": { - "preset_mode": "comfort" - } - } - ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Chauffage télétravail salon", - "enabled": false - }, - { - "schedule_id": "18230d", - "timeslots": [ - { - "start": "00:00:00", - "stop": "06:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.bureau", - "service_data": { - "preset_mode": "eco" - } - } - ] - }, - { - "start": "06:00:00", - "stop": "22:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_temperature", - "entity_id": "climate.bureau", - "service_data": { - "temperature": 19.5, - "hvac_mode": "heat" - } - } - ] - }, - { - "start": "22:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.bureau", - "service_data": { - "preset_mode": "eco" - } - } - ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Chauffage télétravail bureau", - "enabled": false - }, - { - "schedule_id": "2e669b", - "timeslots": [ - { - "start": "00:00:00", - "stop": "06:30:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.chambre", - "service_data": { - "preset_mode": "eco" - } - } - ] - }, - { - "start": "06:30:00", - "stop": "07:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_temperature", - "entity_id": "climate.chambre", - "service_data": { - "temperature": 17, - "hvac_mode": "heat" - } - } - ] - }, - { - "start": "07:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.chambre", - "service_data": { - "preset_mode": "eco" - } - } - ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Chauffage télétravail chambre", - "enabled": false - }, - { - "schedule_id": "621663", - "timeslots": [ - { - "start": "00:00:00", - "stop": "07:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.bureau", - "service_data": { - "preset_mode": "eco" - } - } - ] - }, - { - "start": "07:00:00", - "stop": "22:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_temperature", - "entity_id": "climate.bureau", - "service_data": { - "temperature": 19.5, - "hvac_mode": "heat" - } - } - ] - }, - { - "start": "22:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.bureau", - "service_data": { - "preset_mode": "eco" - } - } - ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Chauffage maison bureau", - "enabled": true - }, - { - "schedule_id": "f3a989", - "timeslots": [ - { - "start": "00:00:00", - "stop": "06:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_temperature", - "entity_id": "climate.salon", - "service_data": { - "temperature": 17.5, - "hvac_mode": "heat" - } - } - ] - }, - { - "start": "06:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.salon", - "service_data": { - "preset_mode": "comfort" - } - } - ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Chauffage maison salon", - "enabled": true - }, - { - "schedule_id": "2ba6d8", - "timeslots": [ - { - "start": "00:00:00", - "stop": "07:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.chambre", - "service_data": { - "preset_mode": "eco" - } - } - ] - }, - { - "start": "07:00:00", - "stop": "08:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_temperature", - "entity_id": "climate.chambre", - "service_data": { - "temperature": 17, - "hvac_mode": "heat" - } - } - ] - }, - { - "start": "08:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.chambre", - "service_data": { - "preset_mode": "eco" - } - } - ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Chauffage maison chambre", - "enabled": true - }, - { - "schedule_id": "8388cc", - "timeslots": [ - { - "start": "00:00:00", - "stop": "15:00:00", - "conditions": [], - "condition_type": null, - "track_conditions": false, - "actions": [ - { - "service": "input_boolean.turn_off", - "entity_id": "input_boolean.force_presence", - "service_data": {} - } - ] - }, - { - "start": "15:00:00", - "stop": "22:00:00", - "conditions": [], - "condition_type": null, - "track_conditions": false, - "actions": [ - { - "service": "input_boolean.turn_on", - "entity_id": "input_boolean.force_presence", - "service_data": {} - } - ] - }, - { - "start": "22:00:00", - "stop": "00:00:00", - "conditions": [], - "condition_type": null, - "track_conditions": false, - "actions": [ - { - "service": "input_boolean.turn_off", - "entity_id": "input_boolean.force_presence", - "service_data": {} - } - ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Simuler présence travail", - "enabled": false - }, - { - "schedule_id": "741dd1", - "timeslots": [ - { - "start": "00:00:00", - "stop": "22:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Climatisation", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.salon", - "service_data": { - "preset_mode": "comfort" - } - } - ] - }, - { - "start": "22:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Climatisation", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.salon", - "service_data": { - "preset_mode": "eco" - } - } - ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Climatisation maison salon", - "enabled": false - }, - { - "schedule_id": "c91e10", - "timeslots": [ - { - "start": "00:00:00", - "stop": "07:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Climatisation", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.salon", - "service_data": { - "preset_mode": "comfort" - } - } - ] - }, - { - "start": "07:00:00", - "stop": "22:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Climatisation", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.salon", - "service_data": { - "preset_mode": "comfort" - } - } - ] - }, - { - "start": "22:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Climatisation", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.set_preset_mode", - "entity_id": "climate.salon", - "service_data": { - "preset_mode": "eco" - } - } - ] - } - ], - "weekdays": [ - "daily" - ], - "start_date": null, - "end_date": null, - "repeat_type": "repeat", - "name": "Climatisation télétravail salon", - "enabled": false - }, - { - "schedule_id": "4852df", - "timeslots": [ - { - "start": "00:00:00", - "stop": "12:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Climatisation", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.turn_off", - "entity_id": "climate.salon", - "service_data": {} - } - ] - }, - { - "start": "12:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Climatisation", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "climate.turn_off", + "service": "climate.set_temperature", "entity_id": "climate.salon", - "service_data": {} + "service_data": { + "temperature": 20, + "hvac_mode": "heat" + } } ] } ], "weekdays": [ - "daily" + "mon", + "tue", + "wed", + "thu", + "fri" ], "start_date": null, "end_date": null, "repeat_type": "repeat", - "name": "Climatisation absence salon", - "enabled": false + "name": "Chauffage télétravail salon", + "enabled": true }, { - "schedule_id": "7e8c60", + "schedule_id": "1a2b37", "timeslots": [ { - "start": "sunset+00:10:00", - "stop": null, + "start": "12:00:00", + "stop": "19:00:00", "conditions": [], "condition_type": null, "track_conditions": false, "actions": [ { - "service": "cover.close_cover", - "entity_id": "cover.volets", - "service_data": {} + "service": "climate.set_temperature", + "entity_id": "climate.salon", + "service_data": { + "temperature": 26, + "hvac_mode": "cool" + } } ] } ], "weekdays": [ - "daily" + "mon", + "tue", + "wed", + "thu", + "fri" ], "start_date": null, "end_date": null, "repeat_type": "repeat", - "name": "Fermeture volets soir", - "enabled": true + "name": "Climatisation télétravail salon", + "enabled": false }, { - "schedule_id": "61574f", + "schedule_id": "1a2b38", "timeslots": [ { "start": "00:00:00", - "stop": "07:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Climatisation", - "match_type": "is" - } - ], - "condition_type": "or", + "stop": "06:00:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "climate.set_preset_mode", - "entity_id": "climate.salon", + "service": "climate.set_temperature", + "entity_id": "climate.chambre", "service_data": { - "preset_mode": "comfort" + "temperature": 17, + "hvac_mode": "heat" } } ] }, { - "start": "07:00:00", - "stop": "16:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Climatisation", - "match_type": "is" - } - ], - "condition_type": "or", + "start": "06:00:00", + "stop": "07:30:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "climate.set_preset_mode", - "entity_id": "climate.salon", + "service": "climate.set_temperature", + "entity_id": "climate.chambre", "service_data": { - "preset_mode": "eco" + "temperature": 19, + "hvac_mode": "heat" } } ] }, { - "start": "16:00:00", - "stop": "22:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Climatisation", - "match_type": "is" - } - ], - "condition_type": "or", + "start": "07:30:00", + "stop": "21:00:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "climate.set_preset_mode", - "entity_id": "climate.salon", + "service": "climate.set_temperature", + "entity_id": "climate.chambre", "service_data": { - "preset_mode": "comfort" + "temperature": 16, + "hvac_mode": "heat" } } ] }, { - "start": "22:00:00", + "start": "21:00:00", "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Climatisation", - "match_type": "is" - } - ], - "condition_type": "or", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "climate.set_preset_mode", - "entity_id": "climate.salon", + "service": "climate.set_temperature", + "entity_id": "climate.chambre", "service_data": { - "preset_mode": "eco" + "temperature": 18, + "hvac_mode": "heat" } } ] } ], "weekdays": [ - "daily" + "mon", + "tue", + "wed", + "thu", + "fri" ], "start_date": null, "end_date": null, "repeat_type": "repeat", - "name": "Climatisation travail salon", - "enabled": false + "name": "Chauffage travail chambre", + "enabled": true }, { - "schedule_id": "890bac", + "schedule_id": "1a2b39", "timeslots": [ { - "start": "00:00:00", - "stop": "17:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", + "start": "06:00:00", + "stop": "08:00:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "switch.turn_off", - "entity_id": "switch.salon_enable_auto_start_stop", - "service_data": {} + "service": "climate.set_temperature", + "entity_id": "climate.salon", + "service_data": { + "temperature": 20, + "hvac_mode": "heat" + } } ] }, { - "start": "17:00:00", - "stop": "00:00:00", - "conditions": [ + "start": "08:00:00", + "stop": "17:30:00", + "conditions": [], + "condition_type": null, + "track_conditions": false, + "actions": [ { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" + "service": "climate.set_temperature", + "entity_id": "climate.salon", + "service_data": { + "temperature": 17, + "hvac_mode": "heat" + } } - ], - "condition_type": "or", + ] + }, + { + "start": "17:30:00", + "stop": "22:30:00", + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "switch.turn_off", - "entity_id": "switch.salon_enable_auto_start_stop", - "service_data": {} + "service": "climate.set_temperature", + "entity_id": "climate.salon", + "service_data": { + "temperature": 20, + "hvac_mode": "heat" + } } ] } ], "weekdays": [ - "daily" + "mon", + "tue", + "wed", + "thu", + "fri" ], "start_date": null, "end_date": null, "repeat_type": "repeat", - "name": "Chauffage travail auto start/stop salon", - "enabled": false + "name": "Chauffage travail salon", + "enabled": true }, { - "schedule_id": "c8c0f3", + "schedule_id": "1a2b3a", "timeslots": [ { - "start": "00:00:00", - "stop": "08:00:00", - "conditions": [ - { - "entity_id": "climate.salon", - "attribute": "state", - "value": "heat", - "match_type": "is" - } - ], - "condition_type": "or", + "start": "17:00:00", + "stop": null, + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "switch.turn_off", - "entity_id": "switch.salon_enable_auto_start_stop", + "service": "input_boolean.turn_on", + "entity_id": "input_boolean.force_presence", "service_data": {} } ] }, { - "start": "08:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "climate.salon", - "attribute": "state", - "value": "heat", - "match_type": "is" - } - ], - "condition_type": "or", + "start": "22:30:00", + "stop": null, + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "switch.turn_off", - "entity_id": "switch.salon_enable_auto_start_stop", + "service": "input_boolean.turn_off", + "entity_id": "input_boolean.force_presence", "service_data": {} } ] } ], "weekdays": [ - "daily" + "mon", + "tue", + "wed", + "thu", + "fri" ], "start_date": null, "end_date": null, "repeat_type": "repeat", - "name": "Chauffage maison auto start/stop salon", + "name": "Présence forcée travail", "enabled": true }, { - "schedule_id": "56538f", + "schedule_id": "1a2b3b", "timeslots": [ { "start": "00:00:00", - "stop": "12:00:00", - "conditions": [ - { - "entity_id": "climate.salon", - "attribute": "state", - "value": "heat", - "match_type": "is" - } - ], - "condition_type": "or", + "stop": null, + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { - "service": "switch.turn_on", - "entity_id": "switch.salon_enable_auto_start_stop", - "service_data": {} - } - ] - }, - { - "start": "12:00:00", - "stop": "00:00:00", - "conditions": [ + "service": "climate.set_temperature", + "entity_id": "climate.bureau", + "service_data": { + "temperature": 12, + "hvac_mode": "heat" + } + }, { - "entity_id": "climate.salon", - "attribute": "state", - "value": "heat", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ + "service": "climate.set_temperature", + "entity_id": "climate.chambre", + "service_data": { + "temperature": 12, + "hvac_mode": "heat" + } + }, { - "service": "switch.turn_on", - "entity_id": "switch.salon_enable_auto_start_stop", - "service_data": {} + "service": "climate.set_temperature", + "entity_id": "climate.salon", + "service_data": { + "temperature": 12, + "hvac_mode": "heat" + } } ] } @@ -1555,45 +730,17 @@ "start_date": null, "end_date": null, "repeat_type": "repeat", - "name": "Chauffage absence auto start/stop salon", - "enabled": false + "name": "Hors-gel absence", + "enabled": true }, { - "schedule_id": "67f8a7", + "schedule_id": "1a2b3c", "timeslots": [ { - "start": "00:00:00", - "stop": "07:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", - "track_conditions": false, - "actions": [ - { - "service": "switch.turn_off", - "entity_id": "switch.salon_enable_auto_start_stop", - "service_data": {} - } - ] - }, - { - "start": "07:00:00", - "stop": "00:00:00", - "conditions": [ - { - "entity_id": "input_select.mode_thermostat", - "attribute": "state", - "value": "Chauffage", - "match_type": "is" - } - ], - "condition_type": "or", + "start": "08:00:00", + "stop": null, + "conditions": [], + "condition_type": null, "track_conditions": false, "actions": [ { @@ -1610,137 +757,44 @@ "start_date": null, "end_date": null, "repeat_type": "repeat", - "name": "Chauffage télétravail auto start/stop salon", - "enabled": false + "name": "Arrêt clim absence", + "enabled": true } ], "tags": [ { - "name": "Chauffage", - "schedules": [ - "011e2a", - "60b115", - "7ceade", - "636997", - "268dc6", - "6779e2", - "2e86ad", - "18230d", - "2e669b", - "621663", - "f3a989", - "2ba6d8", - "8388cc", - "890bac", - "c8c0f3", - "56538f", - "67f8a7" - ] - }, - { - "name": "Salon", - "schedules": [ - "011e2a", - "6779e2", - "2e86ad", - "f3a989", - "741dd1", - "c91e10", - "4852df", - "61574f", - "8388cc", - "890bac", - "56538f", - "67f8a7" - ] - }, - { - "name": "Bureau", - "schedules": [ - "60b115", - "636997", - "18230d", - "621663" - ] - }, - { - "name": "Vacances", - "schedules": [ - "ea9150" - ] - }, - { - "name": "Chambre", - "schedules": [ - "7ceade", - "268dc6", - "2e669b", - "2ba6d8" - ] - }, - { - "name": "Absence", + "name": "Maison", "schedules": [ - "636997", - "268dc6", - "6779e2", - "4852df", - "ea9150", - "56538f" + "1a2b30", + "1a2b31", + "1a2b32", + "1a2b33" ] }, { "name": "Télétravail", "schedules": [ - "2e86ad", - "18230d", - "2e669b", - "c91e10", - "f9cd26", - "67f8a7" - ] - }, - { - "name": "Maison", - "schedules": [ - "621663", - "f3a989", - "2ba6d8", - "ea9150", - "741dd1", - "c8c0f3" + "1a2b34", + "1a2b35", + "1a2b36", + "1a2b37" ] }, { "name": "Travail", "schedules": [ - "7ceade", - "60b115", - "011e2a", - "f9cd26", - "8388cc", - "61574f", - "890bac" - ] - }, - { - "name": "Volet", - "schedules": [ - "7e8c60", - "ea9150", - "f9cd26" + "1a2b38", + "1a2b39", + "1a2b3a" ] }, { - "name": "Climatisation", + "name": "Absence", "schedules": [ - "741dd1", - "c91e10", - "4852df", - "61574f", - "8388cc" + "1a2b3b", + "1a2b3c" ] } ] } -} \ No newline at end of file +} diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index 67febe4..fc25a09 100644 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -1,51 +1,94 @@ #!/bin/bash +# Prepares /config once, when the devcontainer is created: card build, +# scheduler component and card, sample schedules, Lovelace resources and +# the HomeShift dashboard. start.sh then runs Home Assistant. +set -u + +WORKSPACE=/workspaces/homeshift_card +SEEDS=$WORKSPACE/.devcontainer +SCHEDULER_COMPONENT_VERSION="v3.3.8" +SCHEDULER_CARD_VERSION="v4.0.19" echo "Setting up Home Assistant development environment..." -echo "" +mkdir -p /config/.storage /config/custom_components /config/www/scheduler-card -# Create config directories if they don't exist -mkdir -p /config/www/homeshift_card -mkdir -p /config/.storage -mkdir -p /config/custom_components +echo "" +echo "📦 Installing npm dependencies and building the card..." +cd "$WORKSPACE" && npm install --no-audit --no-fund && npm run build -# Copy configuration.yaml from repo if not exists or is older -if [ ! -f /config/configuration.yaml ] || [ /workspaces/homeshift_card/.devcontainer/config/configuration.yaml -nt /config/configuration.yaml ]; then - echo "📝 Copying configuration.yaml from repository..." - cp /workspaces/homeshift_card/.devcontainer/config/configuration.yaml /config/configuration.yaml - echo " ✅ Configuration file updated!" +echo "" +echo "📦 Installing scheduler-component ${SCHEDULER_COMPONENT_VERSION}..." +if [ ! -f /config/custom_components/scheduler/manifest.json ]; then + rm -rf /config/custom_components/scheduler /tmp/scheduler + if wget -q "https://github.com/nielsfaber/scheduler-component/releases/download/${SCHEDULER_COMPONENT_VERSION}/scheduler.zip" -O /tmp/scheduler.zip; then + unzip -q -o /tmp/scheduler.zip -d /tmp/scheduler + # The archive may or may not wrap the files in a scheduler/ folder. + cp -r "$(dirname "$(find /tmp/scheduler -name manifest.json | head -n 1)")" /config/custom_components/scheduler + rm -rf /tmp/scheduler /tmp/scheduler.zip + echo " ✅ Installed in /config/custom_components/scheduler/" + else + echo " ⚠️ Download failed" + fi +else + echo " ✅ Already installed" fi -# Copy scheduler storage file -if [ -f /workspaces/homeshift_card/.devcontainer/scheduler.storage ]; then - echo "📋 Copying scheduler storage file..." - mkdir -p /config/.storage - cp /workspaces/homeshift_card/.devcontainer/scheduler.storage /config/.storage/scheduler.storage - echo " ✅ scheduler.storage file copied!" +echo "" +echo "📦 Installing scheduler-card ${SCHEDULER_CARD_VERSION}..." +if [ ! -f /config/www/scheduler-card/scheduler-card.js ]; then + if wget -q "https://github.com/nielsfaber/scheduler-card/releases/download/${SCHEDULER_CARD_VERSION}/scheduler-card.js" -O /config/www/scheduler-card/scheduler-card.js; then + echo " ✅ Installed" + else + echo " ⚠️ Download failed" + fi +else + echo " ✅ Already installed" fi -# Install scheduler component -echo "📦 Installing scheduler component..." -if [ ! -d /config/custom_components/scheduler ]; then - cd /tmp - rm -rf scheduler-component-* scheduler.zip - wget -q -O scheduler.zip https://github.com/nielsfaber/scheduler-component/archive/refs/heads/master.zip - unzip -o -q scheduler.zip - cp -r scheduler-component-*/custom_components/scheduler /config/custom_components/ - rm -rf scheduler-component-* scheduler.zip - echo " ✅ Scheduler component installed!" +echo "" +echo "📋 Sample schedules..." +# Kept when it exists: it holds the schedules edited in the UI. +# Delete .devcontainer/config/.storage/scheduler.storage to get the samples back. +if [ ! -f /config/.storage/scheduler.storage ]; then + cp "$SEEDS/scheduler.storage" /config/.storage/scheduler.storage + echo " ✅ scheduler.storage copied" else - echo " ℹ️ Scheduler component already installed" + echo " ℹ️ scheduler.storage already present, kept" fi -# Check if the dist folder has the card file -if [ -f /workspaces/homeshift_card/dist/homeshift-card.js ]; then - # dist/ is bind-mounted on /config/www/homeshift_card (devcontainer.json), - # so every rebuild is served without copying. - echo "✅ Card file found in dist/ (mounted as /local/homeshift_card/)" - - # Pre-configure the Lovelace resource - echo "✅ Pre-configuring Lovelace resources..." - cat > /config/.storage/lovelace_resources << 'EOF' +echo "" +echo "🔌 Scheduler integration..." +# Adds the Scheduler config entry so the integration is set up without the UI +# (version 2: the component cannot migrate a version 1 entry on recent HA). +python3 - <<'PY' +import json, os, uuid +from datetime import datetime, timezone + +path = "/config/.storage/core.config_entries" +store = {"version": 1, "minor_version": 5, "key": "core.config_entries", "data": {"entries": []}} +if os.path.exists(path): + with open(path, encoding="utf-8") as f: + store = json.load(f) +entries = store["data"]["entries"] +if any(e["domain"] == "scheduler" for e in entries): + print(" ℹ️ Already configured") +else: + now = datetime.now(timezone.utc).isoformat() + entries.append({ + "created_at": now, "data": {}, "disabled_by": None, "discovery_keys": {}, + "domain": "scheduler", "entry_id": uuid.uuid4().hex, "minor_version": 1, + "modified_at": now, "options": {}, "pref_disable_new_entities": False, + "pref_disable_polling": False, "source": "user", "subentries": [], + "title": "Scheduler", "unique_id": None, "version": 2, + }) + with open(path, "w", encoding="utf-8") as f: + json.dump(store, f, indent=2) + print(" ✅ Config entry added") +PY + +echo "" +echo "🧩 Lovelace resources..." +cat > /config/.storage/lovelace_resources << 'EOF' { "version": 1, "minor_version": 1, @@ -66,147 +109,57 @@ if [ -f /workspaces/homeshift_card/dist/homeshift-card.js ]; then } } EOF - echo " Lovelace resources pre-configured!" -else - echo "⚠️ WARNING: Card file not found in dist/ folder" - echo "" - echo " You need to build the card first!" - echo " On your HOST machine (Windows), run:" - echo " 1. Open a terminal in the project folder" - echo " 2. npm install" - echo " 3. npm run build" - echo "" - echo " The dist/ folder will be automatically mounted in Home Assistant." -fi - -echo "" -echo "📦 Installing scheduler-component..." -SCHEDULER_COMPONENT_VERSION="v3.3.8" -SCHEDULER_COMPONENT_URL="https://github.com/nielsfaber/scheduler-component/releases/download/${SCHEDULER_COMPONENT_VERSION}/scheduler.zip" - -if [ ! -d /config/custom_components/scheduler ]; then - # Install wget and unzip if not available - if ! command -v wget &> /dev/null || ! command -v unzip &> /dev/null; then - echo " Installing wget and unzip..." - apk add --no-cache wget unzip > /dev/null 2>&1 - fi - - echo " Downloading scheduler-component ${SCHEDULER_COMPONENT_VERSION}..." - if wget -q ${SCHEDULER_COMPONENT_URL} -O /tmp/scheduler.zip; then - echo " Extracting scheduler component..." - unzip -q /tmp/scheduler.zip -d /config/custom_components/ - rm /tmp/scheduler.zip - - # Verify installation - if [ -f /config/custom_components/scheduler/manifest.json ]; then - echo " ✅ Scheduler component installed successfully!" - echo " 📁 Location: /config/custom_components/scheduler/" - else - echo " ⚠️ Warning: scheduler component extracted but manifest.json not found" - echo " Checking what was extracted..." - ls -la /config/custom_components/ || true - fi - else - echo " ⚠️ Failed to download scheduler component" - fi -else - echo " ✅ Scheduler component already installed" - echo " 📁 Location: /config/custom_components/scheduler/" - # Verify it's properly installed - if [ -f /config/custom_components/scheduler/manifest.json ]; then - echo " ✓ manifest.json found" - else - echo " ⚠️ Warning: manifest.json not found in scheduler directory" - fi -fi - -echo "" -echo "📦 Installing scheduler-card..." -SCHEDULER_CARD_VERSION="v4.0.11" -SCHEDULER_CARD_URL="https://github.com/nielsfaber/scheduler-card/releases/download/${SCHEDULER_CARD_VERSION}/scheduler-card.js" - -# Create scheduler-card directory -mkdir -p /config/www/scheduler-card - -if [ ! -f /config/www/scheduler-card/scheduler-card.js ]; then - # Install wget if not available - if ! command -v wget &> /dev/null; then - echo " Installing wget..." - apk add --no-cache wget > /dev/null 2>&1 - fi - - echo " Downloading scheduler-card ${SCHEDULER_CARD_VERSION}..." - if wget -q ${SCHEDULER_CARD_URL} -O /config/www/scheduler-card/scheduler-card.js; then - echo " ✅ Scheduler card installed!" - else - echo " ⚠️ Failed to download scheduler card" - fi -else - echo " ✅ Scheduler card already installed" -fi +echo " ✅ Registered" echo "" -echo "🚀 Starting Home Assistant in the background..." - -# Start Home Assistant in the background -nohup python3 -m homeassistant --config /config > /config/home-assistant.log 2>&1 & +echo "📊 HomeShift dashboard..." +# Storage mode, so it can be edited in the UI. Kept when it exists: it holds the UI edits. +# Delete .devcontainer/config/.storage/lovelace.dashboard_homeshift to get the seed back. +python3 - "$SEEDS/dashboard.yaml" <<'PY' +import json, os, sys +import yaml + +def load(path, default): + if not os.path.exists(path): + return default + with open(path, encoding="utf-8") as f: + return json.load(f) + +def save(path, store): + with open(path, "w", encoding="utf-8") as f: + json.dump(store, f, indent=2, ensure_ascii=False) + +dashboards_path = "/config/.storage/lovelace_dashboards" +dashboards = load(dashboards_path, { + "version": 1, "minor_version": 1, "key": "lovelace_dashboards", "data": {"items": []}, +}) +items = dashboards["data"]["items"] +if not any(d["url_path"] == "dashboard-homeshift" for d in items): + items.append({ + "id": "dashboard_homeshift", "url_path": "dashboard-homeshift", + "title": "HomeShift", "icon": "mdi:home-clock", + "show_in_sidebar": True, "require_admin": False, "mode": "storage", + }) + save(dashboards_path, dashboards) + print(" ✅ Dashboard registered") + +config_path = "/config/.storage/lovelace.dashboard_homeshift" +if os.path.exists(config_path): + print(" ℹ️ Dashboard content already present, kept") +else: + with open(sys.argv[1], encoding="utf-8") as f: + config = yaml.safe_load(f) + save(config_path, { + "version": 1, "minor_version": 1, "key": "lovelace.dashboard_homeshift", + "data": {"config": config}, + }) + print(" ✅ Dashboard content seeded") +PY + +# start.sh (the container's main process) waits for this file to start Home Assistant. +touch /var/lib/devcontainer-setup-done -HASS_PID=$! -echo " Home Assistant starting with PID: $HASS_PID" -echo "" -echo "✅ Setup complete!" -echo "" -echo "⏳ IMPORTANT: Home Assistant is starting up..." -echo " This takes 1-2 minutes on first launch." -echo " Please wait before accessing: http://localhost:8123" -echo "" -echo "💡 How to check if Home Assistant is ready:" -echo " - Watch this terminal for Home Assistant logs" -echo " - Look for: 'Home Assistant initialized in XXs'" -echo " - Or check: http://localhost:8123 (refresh after 1-2 minutes)" -echo " - Monitor logs: tail -f /config/home-assistant.log" -echo "" -echo "🔧 If http://localhost:8123 doesn't work after 5 minutes:" -echo " 1. Check process: ps aux | grep homeassistant" -echo " 2. Check logs: tail -f /config/home-assistant.log" -echo " 3. Verify port forwarding in VS Code (PORTS tab)" -echo "" -echo "📦 Card Resource Configuration:" -echo " The card resource will be automatically registered in .storage/lovelace_resources" echo "" -echo "📋 To view the card:" -echo " 1. Wait for Home Assistant to fully start (1-2 minutes)" -echo " 2. Open http://localhost:8123" -echo " 3. Complete onboarding (create account: admin / admin)" -echo " 4. Enable Advanced Mode: Profile (bottom left) → Enable Advanced Mode" -echo " 5. Go to: Overview dashboard" -echo " 6. The card is pre-configured in the dashboard!" -echo "" -echo "📅 Scheduler Integration:" -echo " IMPORTANT: The Scheduler integration must be added after Home Assistant starts:" -echo " 1. Wait for Home Assistant to fully start and complete onboarding" -echo " 2. Go to: Settings → Devices & Services → Add Integration" -echo " 3. Search for 'Scheduler' and click to add it" -echo " 4. Once added, the scheduler-card in the 'Scheduler' tab will work" -echo " " -echo " If 'Scheduler' doesn't appear in integrations list:" -echo " - Make sure Home Assistant has fully started (check logs)" -echo " - Try refreshing the browser (Ctrl+F5)" -echo " - Clear browser cache and reload" -echo " - Restart Home Assistant: Settings → System → Restart" -echo "" -echo " Alternative: Manually add via UI:" -echo " - Click the 3 dots (top right) → Edit Dashboard → Add Card" -echo " - Search for 'HomeShift Card' and select it" -echo " - The HomeShift entities are pre-filled" -echo "" - -# Give a moment for Home Assistant to start writing logs -sleep 3 - -# Show the last few lines of the log to confirm it's starting -if [ -f /config/home-assistant.log ]; then - echo "📋 Home Assistant startup logs (last 20 lines):" - echo "==============================================" - tail -n 20 /config/home-assistant.log -fi +echo "✅ Setup complete! Home Assistant starts now: http://localhost:8123 (1-2 min)" +echo " Login: admin / admin — dashboard HomeShift in the sidebar" +echo " Logs: tail -f /config/home-assistant.log" diff --git a/.devcontainer/start.sh b/.devcontainer/start.sh new file mode 100644 index 0000000..5617adf --- /dev/null +++ b/.devcontainer/start.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Main process of the devcontainer (Dockerfile CMD): runs Home Assistant and +# starts it again whenever it exits, as a restart from the UI does. +# Lifecycle commands (postStartCommand) cannot do this: VS Code kills what +# they leave running in the background. + +# In the container layer, not in /config: a rebuilt container waits for its own setup. +MARKER=/var/lib/devcontainer-setup-done + +until [ -f "$MARKER" ]; do + echo "⏳ Waiting for setup.sh to finish..." + sleep 5 +done + +# Creates the admin / admin account once Home Assistant answers (first launch only). +python3 /workspaces/homeshift_card/.devcontainer/onboard.py & + +trap 'kill "$HASS_PID" 2>/dev/null; wait "$HASS_PID"; exit 0' TERM INT + +while true; do + echo "🚀 Starting Home Assistant: http://localhost:8123 (logs: /config/home-assistant.log)" + python3 -m homeassistant --config /config & + HASS_PID=$! + wait "$HASS_PID" + echo "Home Assistant exited ($?), starting it again in 3 s..." + sleep 3 +done diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/.gitignore b/.gitignore index 76786de..cb0ef8d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,11 @@ node_modules/ # Build artifacts dist/*.map +# Home Assistant runtime state of the devcontainer (only the seeds are versioned) +.devcontainer/config/* +!.devcontainer/config/configuration.yaml + # Temp files .DS_Store *.log +.devcontainer/config.old/ From 9c9d30a88a7b0463d51723eea85d6997b0add71d Mon Sep 17 00:00:00 2001 From: Gamso Date: Sun, 11 Oct 2026 09:30:35 +0200 Subject: [PATCH 24/25] docs: document the schedules panel and refresh the screenshots Co-Authored-By: Claude Opus 5.5 --- README.md | 32 +++++++++++++++++++++++++++++--- assets/card_preview.png | Bin 20803 -> 72193 bytes assets/schedules_preview.png | Bin 0 -> 96959 bytes 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 assets/schedules_preview.png diff --git a/README.md b/README.md index a20350a..92c237b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A clean, compact Lovelace card for Home Assistant to control the **HomeShift** i ## Overview -HomeShift Card gives you full control over your HomeShift-managed thermostat and daily schedule in one place: a compact list of labelled rows, with the less frequent settings folded away until you need them. +HomeShift Card gives you full control over your HomeShift-managed thermostat and daily schedule in one place: a compact list of labelled rows, with the less frequent settings folded away until you need them. With the Scheduler integration, a panel that slides up over the card shows the schedules of each day mode as 24 h timelines, and edits them. --- @@ -23,7 +23,7 @@ The first row is a segmented control with one button per thermostat mode; the ac | Mode | Colour of the active button | | ----------- | --------------------------- | | Off | Gray | -| Heating | Red (HA heating colour) | +| Heating | Orange (HA heating colour) | | Cooling | Blue (HA cooling colour) | | Ventilation | Green (HA fan colour) | @@ -60,7 +60,17 @@ Pausing or resuming never moves a cover: it simply skips, or rejoins, the next s ### Alerts -At the bottom of the card, a chip appears while the heat protection keeps the covers down, and a blinking chip names the covers tonight's close had to leave open (window open). Past three covers the chip shows a count; tap it (or focus it and press Enter) to read the names. A calm, non-blinking chip names the paused covers; tap it to open the pause row. Nothing is shown while everything is in order. +At the bottom of the card (above the Schedules bar), a chip appears while the heat protection keeps the covers down, and a blinking chip names the covers tonight's close had to leave open (window open). Past three covers the chip shows a count; tap it (or focus it and press Enter) to read the names. A calm, non-blinking chip names the paused covers; tap it to open the pause row. Nothing is shown while everything is in order. + +### Schedules + +![Schedules panel](assets/schedules_preview.png) + +With the [Scheduler](https://github.com/nielsfaber/scheduler-component) integration, a **Schedules** bar at the foot of the card names the current day mode. Tap it to slide a panel up over the card. It has one tab per day mode (a dot marks the current one, shown on opening and whenever the mode changes) and the 24 h timelines of the schedules tagged with the tab's mode, grouped by room with the temperature it measures, heating first, then cooling. A vertical line marks the current time; a disabled schedule stays readable, hatched. The switch of each row enables or disables the schedule, and turning a room's cooling on turns its heating off (and the other way round). The pencil next to the tabs shows the [scheduler-card](https://github.com/nielsfaber/scheduler-card) list of the tab instead, with **Add** and the full editor, filtered on its mode and on `schedule_entities`; press it again to come back to the timelines. Tap the bar again, or press Escape, to fold the panel back. + +The panel is as tall as its content and anchored to the foot of the card, so the top of the card stays in sight; when the schedules are taller than the card, they scroll inside the panel. The card keeps its height, so the dashboard does not move. + +Tag each schedule with the name of its day mode (in the scheduler-card editor, **Options → Tags**). The bar only appears once a schedule carries such a tag; without the Scheduler integration the card looks as before. --- @@ -68,6 +78,11 @@ At the bottom of the card, a chip appears while the heat protection keeps the co This card requires the [HomeShift integration](https://github.com/Gamso/homeshift) to be installed and configured. The card reads directly from the entities exposed by the integration. +The schedules panel is optional and needs: + +- [scheduler-component](https://github.com/nielsfaber/scheduler-component) (the Scheduler integration), for the timelines +- [scheduler-card](https://github.com/nielsfaber/scheduler-card), for the pencil that adds and edits schedules + --- ## Installation @@ -115,6 +130,11 @@ open_covers_entity: button.homeshift_open_covers close_covers_entity: button.homeshift_close_covers covers_left_open_entity: binary_sensor.homeshift_covers_left_open covers_inhibited_entity: sensor.homeshift_covers_inhibited +# Schedules panel (optional) +show_schedules: true +schedule_entities: + - climate.living_room + - climate.bedroom ``` Every entity option defaults to the entity the HomeShift integration creates, so a plain `type: custom:homeshift-card` works with a default installation. Set an option to an empty string to hide the corresponding element. If the day mode or thermostat mode entity does not exist, the card shows an "Entity not found" message instead of the controls. @@ -138,9 +158,13 @@ Every entity option defaults to the entity the HomeShift integration creates, so | `cover_entity` | string | _(none)_ | Legacy: cover opened/closed by the times; overrides the two buttons when set | | `covers_left_open_entity` | string | `binary_sensor.homeshift_covers_left_open` | "Not closed" chip | | `covers_inhibited_entity` | string | `sensor.homeshift_covers_inhibited` | Cover automation pause row and "Paused" chip | +| `show_schedules` | boolean | `true` | Schedules panel; `false` hides it | +| `schedule_entities` | list | _(all)_ | Schedules panel: only the schedules acting on these entities | All options can also be set from the visual editor. +--- + ## Development ```bash @@ -153,6 +177,8 @@ npm test CI runs the same commands, checks that the committed `dist/` matches a fresh build, and validates the repository with the HACS action. +The devcontainer (`.devcontainer/`, VS Code → **Dev Containers: Reopen in Container**) runs a Home Assistant with the HomeShift entities simulated, the Scheduler integration, sample schedules and a **HomeShift** dashboard, at http://localhost:8123 (login `admin` / `admin`). See [.devcontainer/README.md](.devcontainer/README.md). + --- ## License diff --git a/assets/card_preview.png b/assets/card_preview.png index 6e328cdeedf89fe29ebe16ece6c03f325f250d12..b98da2e28bcaead65f6d0147be7d659b3d9c5837 100644 GIT binary patch literal 72193 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 diff --git a/assets/schedules_preview.png b/assets/schedules_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..0a6ed778df49094c2ae9f288d12c76e95200738f GIT binary patch literal 96959 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%0DDGcb1{(PDHN4gayKvLly&o7=8gX{{L(uWPsT1 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�`zdZ^ooD(HFPK{LAC4f4lw zklR#}+^xz7=+uH9`i1HiG)9)WE@4#y+2txsn&+$R=0w*^o#GK5TP*GPmbNx1AR2IZ zw+x=GnWd+pje8{0&n(|})@Emg;~$7x{7!^`%oiQlX`Bf|bntz;SiURhe5iVCVj`Fz zFYS+w^m#Qx{lqrd?&{-VmmEY2HaGr`;o##dVPHNI;ypzL{h=`O`f(dQ z`w{NR$yDyBn|unQx3pj+2Yn1Xj}yuaU@un%7|0A;*S!u?)V;qGn)T`#kWwK1LCi39 zN0chS)qUUK9GDdt%G6PKERd{vr>wBZ0Y(9STcq zo*yWJ=x2dxX{7ys18I9cMq#ql7Z!X*PLZ?8Z;Cive3vJ_#ki#B_o|9r>eVT>!5`#1 zM{b#}6nz2w2eejgVg*RD1KRUT(UR3?kf<~{yr2FVfO!5w_P=`?ELRJ5|4!-2J~)-G z;f%>^@EJel!0&7OM-BLVwT80yzssOF?U^t)vb!XHY~gefzf?khmnBxfju&P&N3I7H z9QPkPZmAHyEKH1ozxy^ruXLXkVSVxICGXhZoE4dd{{wPE+n@Hu-L*YCc0w*50o;|E z$^+$>uTWE^jD&Fun#r8}AG;jda+>upH;Fr=ubDMY9I}1kIHE%jhs_T6BwTsYha&$G zu=Cu9xhr}cv~JwC562Hwjxw}gouPY-w%o8pnTBQ}#LMXa>jOWB6%{WYBO?UKWTB}E z{f-#p=kGipF5ABxf^=aIstWz;hXdUallGup`(N+oFY^glg;zBkXkFCIwUvakJ+Nfn z=n*1L!UK$p_NOWhEB2KY6srHb7J59dHvp+S-B&P;n^w+%l^-h#vFa4jyPmyuTV0Q* za}2>^@2KShZ2xPnY{Ms5j&Hu>`^j=N7|SG;T%-Z^yRV>Wf%Ka@^z;eqf&X=()S1r$ zw?pEZ@2q0iI$87$JL=&b%ZL$q*Ccmo@c(C&E&< z%!Yr`Hd(*_U;9RLt>X_e1knyMZUW$8VBX(hx9i(rU|^8```3bpS@tRhK6WFSLC?U* NN-BM;6gLU}{{YwJ@^}CM literal 0 HcmV?d00001 From 6904865049c7f6e4cfae932f4b6b6673bc7e0f87 Mon Sep 17 00:00:00 2001 From: Gamso Date: Wed, 14 Oct 2026 08:42:46 +0200 Subject: [PATCH 25/25] chore(release): bump version to 2.0.0 and rebuild bundle Co-Authored-By: Claude Opus 5.5 --- dist/homeshift-card.js | 543 +++++++++++++++++++++++++++++++++++++---- package-lock.json | 4 +- package.json | 2 +- 3 files changed, 502 insertions(+), 47 deletions(-) diff --git a/dist/homeshift-card.js b/dist/homeshift-card.js index dc15402..46715f7 100644 --- a/dist/homeshift-card.js +++ b/dist/homeshift-card.js @@ -4,18 +4,18 @@ function e(e,t,i,s){var o,n=arguments.length,r=n<3?t:null===s?s=Object.getOwnPro * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const t=globalThis,i=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s=Symbol(),o=new WeakMap;let n=class{constructor(e,t,i){if(this._$cssResult$=!0,i!==s)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(i&&void 0===e){const i=void 0!==t&&1===t.length;i&&(e=o.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),i&&o.set(t,e))}return e}toString(){return this.cssText}};const r=(e,...t)=>{const i=1===e.length?e[0]:t.reduce((t,i,s)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[s+1],e[0]);return new n(i,e,s)},a=i?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const i of e.cssRules)t+=i.cssText;return(e=>new n("string"==typeof e?e:e+"",void 0,s))(t)})(e):e,{is:c,defineProperty:h,getOwnPropertyDescriptor:l,getOwnPropertyNames:d,getOwnPropertySymbols:p,getPrototypeOf:u}=Object,_=globalThis,m=_.trustedTypes,v=m?m.emptyScript:"",f=_.reactiveElementPolyfillSupport,g=(e,t)=>e,y={toAttribute(e,t){switch(t){case Boolean:e=e?v:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let i=e;switch(t){case Boolean:i=null!==e;break;case Number:i=null===e?null:Number(e);break;case Object:case Array:try{i=JSON.parse(e)}catch(e){i=null}}return i}},b=(e,t)=>!c(e,t),$={attribute:!0,type:String,converter:y,reflect:!1,useDefault:!1,hasChanged:b}; +const t=globalThis,i=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s=Symbol(),o=new WeakMap;let n=class{constructor(e,t,i){if(this._$cssResult$=!0,i!==s)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(i&&void 0===e){const i=void 0!==t&&1===t.length;i&&(e=o.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),i&&o.set(t,e))}return e}toString(){return this.cssText}};const r=(e,...t)=>{const i=1===e.length?e[0]:t.reduce((t,i,s)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[s+1],e[0]);return new n(i,e,s)},a=i?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const i of e.cssRules)t+=i.cssText;return(e=>new n("string"==typeof e?e:e+"",void 0,s))(t)})(e):e,{is:c,defineProperty:l,getOwnPropertyDescriptor:d,getOwnPropertyNames:h,getOwnPropertySymbols:u,getPrototypeOf:p}=Object,_=globalThis,m=_.trustedTypes,f=m?m.emptyScript:"",v=_.reactiveElementPolyfillSupport,g=(e,t)=>e,y={toAttribute(e,t){switch(t){case Boolean:e=e?f:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let i=e;switch(t){case Boolean:i=null!==e;break;case Number:i=null===e?null:Number(e);break;case Object:case Array:try{i=JSON.parse(e)}catch(e){i=null}}return i}},b=(e,t)=>!c(e,t),$={attribute:!0,type:String,converter:y,reflect:!1,useDefault:!1,hasChanged:b}; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */Symbol.metadata??=Symbol("metadata"),_.litPropertyMetadata??=new WeakMap;let x=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=$){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){const i=Symbol(),s=this.getPropertyDescriptor(e,i,t);void 0!==s&&h(this.prototype,e,s)}}static getPropertyDescriptor(e,t,i){const{get:s,set:o}=l(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get:s,set(t){const n=s?.call(this);o?.call(this,t),this.requestUpdate(e,n,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??$}static _$Ei(){if(this.hasOwnProperty(g("elementProperties")))return;const e=u(this);e.finalize(),void 0!==e.l&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(g("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(g("properties"))){const e=this.properties,t=[...d(e),...p(e)];for(const i of t)this.createProperty(i,e[i])}const e=this[Symbol.metadata];if(null!==e){const t=litPropertyMetadata.get(e);if(void 0!==t)for(const[e,i]of t)this.elementProperties.set(e,i)}this._$Eh=new Map;for(const[e,t]of this.elementProperties){const i=this._$Eu(e,t);void 0!==i&&this._$Eh.set(i,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const i=new Set(e.flat(1/0).reverse());for(const e of i)t.unshift(a(e))}else void 0!==e&&t.push(a(e));return t}static _$Eu(e,t){const i=t.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof e?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),void 0!==this.renderRoot&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const i of t.keys())this.hasOwnProperty(i)&&(e.set(i,this[i]),delete this[i]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((e,s)=>{if(i)e.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(const i of s){const s=document.createElement("style"),o=t.litNonce;void 0!==o&&s.setAttribute("nonce",o),s.textContent=i.cssText,e.appendChild(s)}})(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,i){this._$AK(e,i)}_$ET(e,t){const i=this.constructor.elementProperties.get(e),s=this.constructor._$Eu(e,i);if(void 0!==s&&!0===i.reflect){const o=(void 0!==i.converter?.toAttribute?i.converter:y).toAttribute(t,i.type);this._$Em=e,null==o?this.removeAttribute(s):this.setAttribute(s,o),this._$Em=null}}_$AK(e,t){const i=this.constructor,s=i._$Eh.get(e);if(void 0!==s&&this._$Em!==s){const e=i.getPropertyOptions(s),o="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==e.converter?.fromAttribute?e.converter:y;this._$Em=s;const n=o.fromAttribute(t,e.type);this[s]=n??this._$Ej?.get(s)??n,this._$Em=null}}requestUpdate(e,t,i,s=!1,o){if(void 0!==e){const n=this.constructor;if(!1===s&&(o=this[e]),i??=n.getPropertyOptions(e),!((i.hasChanged??b)(o,t)||i.useDefault&&i.reflect&&o===this._$Ej?.get(e)&&!this.hasAttribute(n._$Eu(e,i))))return;this.C(e,t,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(e,t,{useDefault:i,reflect:s,wrapped:o},n){i&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,n??t??this[e]),!0!==o||void 0!==n)||(this._$AL.has(e)||(this.hasUpdated||i||(t=void 0),this._$AL.set(e,t)),!0===s&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}const e=this.constructor.elementProperties;if(e.size>0)for(const[t,i]of e){const{wrapped:e}=i,s=this[t];!0!==e||this._$AL.has(t)||void 0===s||this.C(t,void 0,i,s)}}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(e=>e.hostUpdate?.()),this.update(t)):this._$EM()}catch(t){throw e=!1,this._$EM(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(e=>e.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(e=>this._$ET(e,this[e])),this._$EM()}updated(e){}firstUpdated(e){}};x.elementStyles=[],x.shadowRootOptions={mode:"open"},x[g("elementProperties")]=new Map,x[g("finalized")]=new Map,f?.({ReactiveElement:x}),(_.reactiveElementVersions??=[]).push("2.1.2"); + */Symbol.metadata??=Symbol("metadata"),_.litPropertyMetadata??=new WeakMap;let x=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=$){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){const i=Symbol(),s=this.getPropertyDescriptor(e,i,t);void 0!==s&&l(this.prototype,e,s)}}static getPropertyDescriptor(e,t,i){const{get:s,set:o}=d(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get:s,set(t){const n=s?.call(this);o?.call(this,t),this.requestUpdate(e,n,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??$}static _$Ei(){if(this.hasOwnProperty(g("elementProperties")))return;const e=p(this);e.finalize(),void 0!==e.l&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(g("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(g("properties"))){const e=this.properties,t=[...h(e),...u(e)];for(const i of t)this.createProperty(i,e[i])}const e=this[Symbol.metadata];if(null!==e){const t=litPropertyMetadata.get(e);if(void 0!==t)for(const[e,i]of t)this.elementProperties.set(e,i)}this._$Eh=new Map;for(const[e,t]of this.elementProperties){const i=this._$Eu(e,t);void 0!==i&&this._$Eh.set(i,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const i=new Set(e.flat(1/0).reverse());for(const e of i)t.unshift(a(e))}else void 0!==e&&t.push(a(e));return t}static _$Eu(e,t){const i=t.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof e?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),void 0!==this.renderRoot&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const i of t.keys())this.hasOwnProperty(i)&&(e.set(i,this[i]),delete this[i]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((e,s)=>{if(i)e.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(const i of s){const s=document.createElement("style"),o=t.litNonce;void 0!==o&&s.setAttribute("nonce",o),s.textContent=i.cssText,e.appendChild(s)}})(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,i){this._$AK(e,i)}_$ET(e,t){const i=this.constructor.elementProperties.get(e),s=this.constructor._$Eu(e,i);if(void 0!==s&&!0===i.reflect){const o=(void 0!==i.converter?.toAttribute?i.converter:y).toAttribute(t,i.type);this._$Em=e,null==o?this.removeAttribute(s):this.setAttribute(s,o),this._$Em=null}}_$AK(e,t){const i=this.constructor,s=i._$Eh.get(e);if(void 0!==s&&this._$Em!==s){const e=i.getPropertyOptions(s),o="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==e.converter?.fromAttribute?e.converter:y;this._$Em=s;const n=o.fromAttribute(t,e.type);this[s]=n??this._$Ej?.get(s)??n,this._$Em=null}}requestUpdate(e,t,i,s=!1,o){if(void 0!==e){const n=this.constructor;if(!1===s&&(o=this[e]),i??=n.getPropertyOptions(e),!((i.hasChanged??b)(o,t)||i.useDefault&&i.reflect&&o===this._$Ej?.get(e)&&!this.hasAttribute(n._$Eu(e,i))))return;this.C(e,t,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(e,t,{useDefault:i,reflect:s,wrapped:o},n){i&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,n??t??this[e]),!0!==o||void 0!==n)||(this._$AL.has(e)||(this.hasUpdated||i||(t=void 0),this._$AL.set(e,t)),!0===s&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}const e=this.constructor.elementProperties;if(e.size>0)for(const[t,i]of e){const{wrapped:e}=i,s=this[t];!0!==e||this._$AL.has(t)||void 0===s||this.C(t,void 0,i,s)}}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(e=>e.hostUpdate?.()),this.update(t)):this._$EM()}catch(t){throw e=!1,this._$EM(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(e=>e.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(e=>this._$ET(e,this[e])),this._$EM()}updated(e){}firstUpdated(e){}};x.elementStyles=[],x.shadowRootOptions={mode:"open"},x[g("elementProperties")]=new Map,x[g("finalized")]=new Map,v?.({ReactiveElement:x}),(_.reactiveElementVersions??=[]).push("2.1.2"); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const E=globalThis,w=e=>e,S=E.trustedTypes,A=S?S.createPolicy("lit-html",{createHTML:e=>e}):void 0,C="$lit$",k=`lit$${Math.random().toFixed(9).slice(2)}$`,T="?"+k,P=`<${T}>`,I=document,O=()=>I.createComment(""),N=e=>null===e||"object"!=typeof e&&"function"!=typeof e,M=Array.isArray,R="[ \t\n\f\r]",D=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,H=/-->/g,U=/>/g,j=RegExp(`>|${R}(?:([^\\s"'>=/]+)(${R}*=${R}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),z=/'/g,L=/"/g,B=/^(?:script|style|textarea|title)$/i,q=(e=>(t,...i)=>({_$litType$:e,strings:t,values:i}))(1),V=Symbol.for("lit-noChange"),W=Symbol.for("lit-nothing"),F=new WeakMap,K=I.createTreeWalker(I,129);function Y(e,t){if(!M(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==A?A.createHTML(t):t}const J=(e,t)=>{const i=e.length-1,s=[];let o,n=2===t?"":3===t?"":"",r=D;for(let t=0;t"===c[0]?(r=o??D,h=-1):void 0===c[1]?h=-2:(h=r.lastIndex-c[2].length,a=c[1],r=void 0===c[3]?j:'"'===c[3]?L:z):r===L||r===z?r=j:r===H||r===U?r=D:(r=j,o=void 0);const d=r===j&&e[t+1].startsWith("/>")?" ":"";n+=r===D?i+P:h>=0?(s.push(a),i.slice(0,h)+C+i.slice(h)+k+d):i+k+(-2===h?t:d)}return[Y(e,n+(e[i]||"")+(2===t?"":3===t?"":"")),s]};class X{constructor({strings:e,_$litType$:t},i){let s;this.parts=[];let o=0,n=0;const r=e.length-1,a=this.parts,[c,h]=J(e,t);if(this.el=X.createElement(c,i),K.currentNode=this.el.content,2===t||3===t){const e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;null!==(s=K.nextNode())&&a.length0){s.textContent=S?S.emptyScript:"";for(let i=0;iM(e)||"function"==typeof e?.[Symbol.iterator])(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!==W&&N(this._$AH)?this._$AA.nextSibling.data=e:this.T(I.createTextNode(e)),this._$AH=e}$(e){const{values:t,_$litType$:i}=e,s="number"==typeof i?this._$AC(e):(void 0===i.el&&(i.el=X.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(t);else{const e=new G(s,this),i=e.u(this.options);e.p(t),this.T(i),this._$AH=e}}_$AC(e){let t=F.get(e.strings);return void 0===t&&F.set(e.strings,t=new X(e)),t}k(e){M(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let i,s=0;for(const o of e)s===t.length?t.push(i=new Q(this.O(O()),this.O(O()),this,this.options)):i=t[s],i._$AI(o),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=W}_$AI(e,t=this,i,s){const o=this.strings;let n=!1;if(void 0===o)e=Z(this,e,t,0),n=!N(e)||e!==this._$AH&&e!==V,n&&(this._$AH=e);else{const s=e;let r,a;for(e=o[0],r=0;re,E=w.trustedTypes,k=E?E.createPolicy("lit-html",{createHTML:e=>e}):void 0,A="$lit$",C=`lit$${Math.random().toFixed(9).slice(2)}$`,T="?"+C,P=`<${T}>`,M=document,O=()=>M.createComment(""),D=e=>null===e||"object"!=typeof e&&"function"!=typeof e,I=Array.isArray,N="[ \t\n\f\r]",R=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,H=/-->/g,U=/>/g,z=RegExp(`>|${N}(?:([^\\s"'>=/]+)(${N}*=${N}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),j=/'/g,L=/"/g,B=/^(?:script|style|textarea|title)$/i,q=(e=>(t,...i)=>({_$litType$:e,strings:t,values:i}))(1),V=Symbol.for("lit-noChange"),F=Symbol.for("lit-nothing"),W=new WeakMap,K=M.createTreeWalker(M,129);function Y(e,t){if(!I(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==k?k.createHTML(t):t}const J=(e,t)=>{const i=e.length-1,s=[];let o,n=2===t?"":3===t?"":"",r=R;for(let t=0;t"===c[0]?(r=o??R,l=-1):void 0===c[1]?l=-2:(l=r.lastIndex-c[2].length,a=c[1],r=void 0===c[3]?z:'"'===c[3]?L:j):r===L||r===j?r=z:r===H||r===U?r=R:(r=z,o=void 0);const h=r===z&&e[t+1].startsWith("/>")?" ":"";n+=r===R?i+P:l>=0?(s.push(a),i.slice(0,l)+A+i.slice(l)+C+h):i+C+(-2===l?t:h)}return[Y(e,n+(e[i]||"")+(2===t?"":3===t?"":"")),s]};class X{constructor({strings:e,_$litType$:t},i){let s;this.parts=[];let o=0,n=0;const r=e.length-1,a=this.parts,[c,l]=J(e,t);if(this.el=X.createElement(c,i),K.currentNode=this.el.content,2===t||3===t){const e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;null!==(s=K.nextNode())&&a.length0){s.textContent=E?E.emptyScript:"";for(let i=0;iI(e)||"function"==typeof e?.[Symbol.iterator])(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&&D(this._$AH)?this._$AA.nextSibling.data=e:this.T(M.createTextNode(e)),this._$AH=e}$(e){const{values:t,_$litType$:i}=e,s="number"==typeof i?this._$AC(e):(void 0===i.el&&(i.el=X.createElement(Y(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===s)this._$AH.p(t);else{const e=new G(s,this),i=e.u(this.options);e.p(t),this.T(i),this._$AH=e}}_$AC(e){let t=W.get(e.strings);return void 0===t&&W.set(e.strings,t=new X(e)),t}k(e){I(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let i,s=0;for(const o of e)s===t.length?t.push(i=new Q(this.O(O()),this.O(O()),this,this.options)):i=t[s],i._$AI(o),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=F}_$AI(e,t=this,i,s){const o=this.strings;let n=!1;if(void 0===o)e=Z(this,e,t,0),n=!D(e)||e!==this._$AH&&e!==V,n&&(this._$AH=e);else{const s=e;let r,a;for(e=o[0],r=0;re,S=E.trustedTypes,A=S?S.createPolicy("lit-html",{create * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -const he={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:b},le=(e=he,t,i)=>{const{kind:s,metadata:o}=i;let n=globalThis.litPropertyMetadata.get(o);if(void 0===n&&globalThis.litPropertyMetadata.set(o,n=new Map),"setter"===s&&((e=Object.create(e)).wrapped=!0),n.set(i.name,e),"accessor"===s){const{name:s}=i;return{set(i){const o=t.get.call(this);t.set.call(this,i),this.requestUpdate(s,o,e,!0,i)},init(t){return void 0!==t&&this.C(s,void 0,e,t),t}}}if("setter"===s){const{name:s}=i;return function(i){const o=this[s];t.call(this,i),this.requestUpdate(s,o,e,!0,i)}}throw Error("Unsupported decorator location: "+s)};function de(e){return(t,i)=>"object"==typeof i?le(e,t,i):((e,t,i)=>{const s=t.hasOwnProperty(i);return t.constructor.createProperty(i,e),s?Object.getOwnPropertyDescriptor(t,i):void 0})(e,t,i)} +const le={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:b},de=(e=le,t,i)=>{const{kind:s,metadata:o}=i;let n=globalThis.litPropertyMetadata.get(o);if(void 0===n&&globalThis.litPropertyMetadata.set(o,n=new Map),"setter"===s&&((e=Object.create(e)).wrapped=!0),n.set(i.name,e),"accessor"===s){const{name:s}=i;return{set(i){const o=t.get.call(this);t.set.call(this,i),this.requestUpdate(s,o,e,!0,i)},init(t){return void 0!==t&&this.C(s,void 0,e,t),t}}}if("setter"===s){const{name:s}=i;return function(i){const o=this[s];t.call(this,i),this.requestUpdate(s,o,e,!0,i)}}throw Error("Unsupported decorator location: "+s)};function he(e){return(t,i)=>"object"==typeof i?de(e,t,i):((e,t,i)=>{const s=t.hasOwnProperty(i);return t.constructor.createProperty(i,e),s?Object.getOwnPropertyDescriptor(t,i):void 0})(e,t,i)} /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause - */function pe(e){return de({...e,state:!0,attribute:!1})}const ue={en:{card:{entity_not_found:"Entity not found: {entity}",unavailable:"Unavailable",today:"Today",tomorrow:"Tomorrow",heat_protection_active:"Heat protection active",cover_open_time:"Cover opening time",cover_close_time:"Cover closing time",cover_open_action:"Open the covers now (scheduled {time})",cover_close_action:"Close the covers now (scheduled {time})",cover_sending:"Sending…",cover_open_failed:"Could not open the covers: {error}",cover_close_failed:"Could not close the covers: {error}",covers_left_open:"Not closed: {covers}",thermostat_mode:"Thermostat mode",day_mode:"Day mode",next:"Next",covers:"Covers",duration_off:"No",decrease:"Decrease: {setting}",increase:"Increase: {setting}",override_label:"Keep my manual choice",override_none:"A manual change is replaced at the calendar's next pass.",override_effect:"After a change by hand, the calendar waits {duration} — until {time} if you change it now.",early_label:"Start events early",early_none:"{mode} will start at its scheduled time, {time}.",early_effect:"{mode} will start at {time} instead of {scheduled}.",early_no_event:"No event scheduled.",covers_left_open_count:"Not closed: {count} covers",inhibit_label:"Cover automation",inhibit_none:"Auto",inhibited_count:"{count} paused",inhibit_forever:"Until resumed",days_short:"d",inhibit_effect:"Pause for {duration} — until {time} if you start it now.",inhibit_effect_forever:"The cover stays paused until you resume it.",inhibited_until:"Paused until {time}",inhibited_forever:"Paused until resumed",pause:"Pause",resume:"Resume",resume_all:"Resume all",covers_paused:"Paused: {covers}",covers_paused_count:"{count} covers paused",inhibit_failed:"Could not pause or resume the cover: {error}"},thermostat:{off:"Off",heating:"Heating",cooling:"Cooling",ventilation:"Ventilation"},editor:{name:"Name",day_mode_entity:"Day Mode Entity",thermostat_mode_entity:"Thermostat Mode Entity",override_duration_entity:"Override Duration Entity",early_switch_entity:"Early Switch Entity",next_mode_entity:"Next Mode Entity",next_mode_at_entity:"Next Mode Time Entity",heat_protection_entity:"Heat Protection Entity",cover_open_time_entity:"Cover Open Time Entity",cover_close_time_entity:"Cover Close Time Entity",open_covers_entity:"Open Covers Button",close_covers_entity:"Close Covers Button",cover_entity:"Cover Entity (legacy, overrides the buttons)",covers_left_open_entity:"Covers Left Open Entity",covers_inhibited_entity:"Paused covers entity"},preview:{home:"Home",work:"Work",remote:"Remote",away:"Away"}},fr:{card:{entity_not_found:"Entité introuvable: {entity}",unavailable:"Indisponible",today:"Auj.",tomorrow:"Dem.",heat_protection_active:"Protection thermique active",cover_open_time:"Heure d'ouverture des volets",cover_close_time:"Heure de fermeture des volets",cover_open_action:"Ouvrir les volets maintenant (prévu à {time})",cover_close_action:"Fermer les volets maintenant (prévu à {time})",cover_sending:"Envoi…",cover_open_failed:"Impossible d'ouvrir les volets : {error}",cover_close_failed:"Impossible de fermer les volets : {error}",covers_left_open:"Non fermés : {covers}",thermostat_mode:"Mode thermostat",day_mode:"Mode du jour",next:"Prochain",covers:"Volets",duration_off:"Non",decrease:"Diminuer : {setting}",increase:"Augmenter : {setting}",override_label:"Garder mon choix manuel",override_none:"Un changement manuel est remplacé au prochain passage du calendrier.",override_effect:"Après un changement à la main, le calendrier attend {duration} — jusqu'à {time} si tu changes maintenant.",early_label:"Anticiper les événements",early_none:"{mode} commencera à l'heure prévue, {time}.",early_effect:"{mode} commencera à {time} au lieu de {scheduled}.",early_no_event:"Aucun événement prévu.",covers_left_open_count:"Non fermés : {count} volets",inhibit_label:"Pilotage auto des volets",inhibit_none:"Auto",inhibited_count:"{count} en pause",inhibit_forever:"Jusqu'à reprise",days_short:"j",inhibit_effect:"Pause de {duration} — jusqu'à {time} si tu l'actives maintenant.",inhibit_effect_forever:"Le volet reste en pause jusqu'à ce que tu le reprennes.",inhibited_until:"En pause jusqu'à {time}",inhibited_forever:"En pause jusqu'à reprise",pause:"Pause",resume:"Reprendre",resume_all:"Tout reprendre",covers_paused:"En pause : {covers}",covers_paused_count:"{count} volets en pause",inhibit_failed:"Échec de la mise en pause : {error}"},thermostat:{off:"Éteint",heating:"Chauffage",cooling:"Climatisation",ventilation:"Ventilation"},editor:{name:"Nom",day_mode_entity:"Entité mode jour",thermostat_mode_entity:"Entité mode thermostat",override_duration_entity:"Entité durée de dérogation",early_switch_entity:"Entité anticipation",next_mode_entity:"Entité prochain mode",next_mode_at_entity:"Entité heure du prochain mode",heat_protection_entity:"Entité protection thermique",cover_open_time_entity:"Entité heure d'ouverture des volets",cover_close_time_entity:"Entité heure de fermeture des volets",open_covers_entity:"Bouton d'ouverture des volets",close_covers_entity:"Bouton de fermeture des volets",cover_entity:"Entité volet (ancienne option, prioritaire sur les boutons)",covers_left_open_entity:"Entité volets non fermés",covers_inhibited_entity:"Entité volets en pause"},preview:{home:"Maison",work:"Travail",remote:"Télétravail",away:"Absence"}}};function _e(e,t){return t.split(".").reduce((e,t)=>e&&null!=e[t]?e[t]:void 0,e)}function me(e,t,i,s){const o=function(e){const t=e?.locale?.language||e?.language||"en",i=String(t).split("-")[0];return ue[t]?t:ue[i]?i:"en"}(e);let n=_e(ue[o],t)??_e(ue.en,t)??s;return null==n?t:(i&&Object.entries(i).forEach(([e,t])=>n=n.replace(`{${e}}`,t)),n)}const ve=[{key:"day_mode_entity",domains:["select","input_select"],default:"select.homeshift_day_mode"},{key:"thermostat_mode_entity",domains:["select","input_select"],default:"select.homeshift_thermostat_mode"},{key:"override_duration_entity",domains:["number","input_number"],default:"number.homeshift_override_duration"},{key:"early_switch_entity",domains:["number","input_number"],default:"number.homeshift_early_switch"},{key:"next_mode_entity",domains:["sensor"],default:"sensor.homeshift_next_mode"},{key:"next_mode_at_entity",domains:["sensor"],default:"sensor.homeshift_next_mode_at"},{key:"heat_protection_entity",domains:["binary_sensor"],default:"binary_sensor.homeshift_cover_heat_active"},{key:"cover_open_time_entity",domains:["sensor"],default:"sensor.homeshift_cover_open_time"},{key:"cover_close_time_entity",domains:["sensor"],default:"sensor.homeshift_cover_close_time"},{key:"open_covers_entity",domains:["button"],default:"button.homeshift_open_covers"},{key:"close_covers_entity",domains:["button"],default:"button.homeshift_close_covers"},{key:"cover_entity",domains:["cover"],default:""},{key:"covers_left_open_entity",domains:["binary_sensor"],default:"binary_sensor.homeshift_covers_left_open"},{key:"covers_inhibited_entity",domains:["sensor"],default:"sensor.homeshift_covers_inhibited"}];class fe extends ae{setConfig(e){this._config=e}_onNameChanged(e){if(!this._config||!this.hass)return;const t=e.target.value;if(this._config.name===t)return;const i={...this._config,name:t};this._config=i,this._dispatchConfigChanged(i)}_onEntityChanged(e,t){if(!this._config||!this.hass)return;const i=e.detail.value;if(this._config[t]===i)return;const s={...this._config,[t]:i};this._config=s,this._dispatchConfigChanged(s)}_dispatchConfigChanged(e){const t=new CustomEvent("config-changed",{detail:{config:e},bubbles:!0,composed:!0});this.dispatchEvent(t)}render(){return this.hass&&this._config?q` + */function ue(e){return he({...e,state:!0,attribute:!1})}const pe={en:{card:{entity_not_found:"Entity not found: {entity}",unavailable:"Unavailable",today:"Today",tomorrow:"Tomorrow",heat_protection_active:"Heat protection active",cover_open_time:"Cover opening time",cover_close_time:"Cover closing time",cover_open_action:"Open the covers now (scheduled {time})",cover_close_action:"Close the covers now (scheduled {time})",cover_sending:"Sending…",cover_open_failed:"Could not open the covers: {error}",cover_close_failed:"Could not close the covers: {error}",covers_left_open:"Not closed: {covers}",thermostat_mode:"Thermostat mode",day_mode:"Day mode",next:"Next",covers:"Covers",duration_off:"No",decrease:"Decrease: {setting}",increase:"Increase: {setting}",override_label:"Keep my manual choice",override_none:"A manual change is replaced at the calendar's next pass.",override_effect:"After a change by hand, the calendar waits {duration} — until {time} if you change it now.",early_label:"Start events early",early_none:"{mode} will start at its scheduled time, {time}.",early_effect:"{mode} will start at {time} instead of {scheduled}.",early_no_event:"No event scheduled.",covers_left_open_count:"Not closed: {count} covers",inhibit_label:"Cover automation",inhibit_none:"Auto",inhibited_count:"{count} paused",inhibit_forever:"Until resumed",days_short:"d",inhibit_effect:"Pause for {duration} — until {time} if you start it now.",inhibit_effect_forever:"The cover stays paused until you resume it.",inhibited_until:"Paused until {time}",inhibited_forever:"Paused until resumed",pause:"Pause",resume:"Resume",resume_all:"Resume all",covers_paused:"Paused: {covers}",covers_paused_count:"{count} covers paused",inhibit_failed:"Could not pause or resume the cover: {error}"},thermostat:{off:"Off",heating:"Heating",cooling:"Cooling",ventilation:"Ventilation"},schedule:{title:"Schedules",empty:"No schedule for {mode}",on:"On",off:"Off",edit:"Edit schedules",back:"Back to timelines",active_mode:"Current day mode",next_run:"Next run",days:"Days"},editor:{name:"Name",day_mode_entity:"Day Mode Entity",thermostat_mode_entity:"Thermostat Mode Entity",override_duration_entity:"Override Duration Entity",early_switch_entity:"Early Switch Entity",next_mode_entity:"Next Mode Entity",next_mode_at_entity:"Next Mode Time Entity",heat_protection_entity:"Heat Protection Entity",cover_open_time_entity:"Cover Open Time Entity",cover_close_time_entity:"Cover Close Time Entity",open_covers_entity:"Open Covers Button",close_covers_entity:"Close Covers Button",cover_entity:"Cover Entity (legacy, overrides the buttons)",covers_left_open_entity:"Covers Left Open Entity",covers_inhibited_entity:"Paused covers entity",show_schedules:"Schedules panel (Scheduler integration)",schedule_entities:"Schedules panel: only these entities (all when empty)"},preview:{home:"Home",work:"Work",remote:"Remote",away:"Away"}},fr:{card:{entity_not_found:"Entité introuvable: {entity}",unavailable:"Indisponible",today:"Auj.",tomorrow:"Dem.",heat_protection_active:"Protection thermique active",cover_open_time:"Heure d'ouverture des volets",cover_close_time:"Heure de fermeture des volets",cover_open_action:"Ouvrir les volets maintenant (prévu à {time})",cover_close_action:"Fermer les volets maintenant (prévu à {time})",cover_sending:"Envoi…",cover_open_failed:"Impossible d'ouvrir les volets : {error}",cover_close_failed:"Impossible de fermer les volets : {error}",covers_left_open:"Non fermés : {covers}",thermostat_mode:"Mode thermostat",day_mode:"Mode du jour",next:"Prochain",covers:"Volets",duration_off:"Non",decrease:"Diminuer : {setting}",increase:"Augmenter : {setting}",override_label:"Garder mon choix manuel",override_none:"Un changement manuel est remplacé au prochain passage du calendrier.",override_effect:"Après un changement à la main, le calendrier attend {duration} — jusqu'à {time} si tu changes maintenant.",early_label:"Anticiper les événements",early_none:"{mode} commencera à l'heure prévue, {time}.",early_effect:"{mode} commencera à {time} au lieu de {scheduled}.",early_no_event:"Aucun événement prévu.",covers_left_open_count:"Non fermés : {count} volets",inhibit_label:"Pilotage auto des volets",inhibit_none:"Auto",inhibited_count:"{count} en pause",inhibit_forever:"Jusqu'à reprise",days_short:"j",inhibit_effect:"Pause de {duration} — jusqu'à {time} si tu l'actives maintenant.",inhibit_effect_forever:"Le volet reste en pause jusqu'à ce que tu le reprennes.",inhibited_until:"En pause jusqu'à {time}",inhibited_forever:"En pause jusqu'à reprise",pause:"Pause",resume:"Reprendre",resume_all:"Tout reprendre",covers_paused:"En pause : {covers}",covers_paused_count:"{count} volets en pause",inhibit_failed:"Échec de la mise en pause : {error}"},thermostat:{off:"Éteint",heating:"Chauffage",cooling:"Climatisation",ventilation:"Ventilation"},schedule:{title:"Plannings",empty:"Aucune planification pour {mode}",on:"Allumé",off:"Éteint",edit:"Modifier les planifications",back:"Retour aux frises",active_mode:"Mode du jour actuel",next_run:"Prochain lancement",days:"Jours"},editor:{name:"Nom",day_mode_entity:"Entité mode jour",thermostat_mode_entity:"Entité mode thermostat",override_duration_entity:"Entité durée de dérogation",early_switch_entity:"Entité anticipation",next_mode_entity:"Entité prochain mode",next_mode_at_entity:"Entité heure du prochain mode",heat_protection_entity:"Entité protection thermique",cover_open_time_entity:"Entité heure d'ouverture des volets",cover_close_time_entity:"Entité heure de fermeture des volets",open_covers_entity:"Bouton d'ouverture des volets",close_covers_entity:"Bouton de fermeture des volets",cover_entity:"Entité volet (ancienne option, prioritaire sur les boutons)",covers_left_open_entity:"Entité volets non fermés",covers_inhibited_entity:"Entité volets en pause",show_schedules:"Panneau des plannings (intégration Scheduler)",schedule_entities:"Panneau des plannings : seulement ces entités (toutes si vide)"},preview:{home:"Maison",work:"Travail",remote:"Télétravail",away:"Absence"}}};function _e(e,t){return t.split(".").reduce((e,t)=>e&&null!=e[t]?e[t]:void 0,e)}function me(e,t,i,s){const o=function(e){const t=e?.locale?.language||e?.language||"en",i=String(t).split("-")[0];return pe[t]?t:pe[i]?i:"en"}(e);let n=_e(pe[o],t)??_e(pe.en,t)??s;return null==n?t:(i&&Object.entries(i).forEach(([e,t])=>n=n.replace(`{${e}}`,t)),n)}const fe=[{key:"day_mode_entity",domains:["select","input_select"],default:"select.homeshift_day_mode"},{key:"thermostat_mode_entity",domains:["select","input_select"],default:"select.homeshift_thermostat_mode"},{key:"override_duration_entity",domains:["number","input_number"],default:"number.homeshift_override_duration"},{key:"early_switch_entity",domains:["number","input_number"],default:"number.homeshift_early_switch"},{key:"next_mode_entity",domains:["sensor"],default:"sensor.homeshift_next_mode"},{key:"next_mode_at_entity",domains:["sensor"],default:"sensor.homeshift_next_mode_at"},{key:"heat_protection_entity",domains:["binary_sensor"],default:"binary_sensor.homeshift_cover_heat_active"},{key:"cover_open_time_entity",domains:["sensor"],default:"sensor.homeshift_cover_open_time"},{key:"cover_close_time_entity",domains:["sensor"],default:"sensor.homeshift_cover_close_time"},{key:"open_covers_entity",domains:["button"],default:"button.homeshift_open_covers"},{key:"close_covers_entity",domains:["button"],default:"button.homeshift_close_covers"},{key:"cover_entity",domains:["cover"],default:""},{key:"covers_left_open_entity",domains:["binary_sensor"],default:"binary_sensor.homeshift_covers_left_open"},{key:"covers_inhibited_entity",domains:["sensor"],default:"sensor.homeshift_covers_inhibited"}];class ve extends ae{setConfig(e){this._config=e}_onNameChanged(e){if(!this._config||!this.hass)return;const t=e.target.value;if(this._config.name===t)return;const i={...this._config,name:t};this._config=i,this._dispatchConfigChanged(i)}_onEntityChanged(e,t){if(!this._config||!this.hass)return;const i=e.detail.value;if(this._config[t]===i)return;const s={...this._config,[t]:i};this._config=s,this._dispatchConfigChanged(s)}_onScheduleChanged(e,t){if(!this._config||!this.hass)return;const i={...this._config,[t]:e.detail.value};this._config=i,this._dispatchConfigChanged(i)}_dispatchConfigChanged(e){const t=new CustomEvent("config-changed",{detail:{config:e},bubbles:!0,composed:!0});this.dispatchEvent(t)}render(){return this.hass&&this._config?q`

- ${ve.map(e=>q`q`this._onEntityChanged(t,e.key)} allow-custom-entity >`)} + + this._onScheduleChanged(e,"show_schedules")} + > + this._onScheduleChanged(e,"schedule_entities")} + >
- `:q``}}fe.styles=r` + `:q``}}ve.styles=r` .card-config { display: flex; flex-direction: column; @@ -57,10 +72,317 @@ const he={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:b},le=(e=he } ha-textfield, - ha-entity-picker { + ha-entity-picker, + ha-selector { width: 100%; } - `,e([de({attribute:!1})],fe.prototype,"hass",void 0),e([pe()],fe.prototype,"_config",void 0),customElements.get("homeshift-card-editor")||customElements.define("homeshift-card-editor",fe);const ge=new Set(["unknown","unavailable",""]);function ye(e){const t="string"==typeof e?e:e?.state;return"string"==typeof t&&!ge.has(t)}function be(e){if(e<60)return`${e} min`;const t=Math.floor(e/60),i=Math.round(e%60);return i>0?`${t}h${String(i).padStart(2,"0")}`:`${t}h`}function $e(e){const t=e?.time_format;return{locale:"system"===t?void 0:e?.language,hour12:"12"===t||"24"!==t&&void 0}}function xe(e,t){const{locale:i,hour12:s}=$e(t);return e.toLocaleTimeString(i,{hour:"2-digit",minute:"2-digit",hour12:s})}class Ee extends ae{constructor(){super(...arguments),this.preview=!1,this._openSetting=null,this._inhibitIndex=Ee.INHIBIT_DEFAULT_INDEX,this._coversExpanded=!1,this._day=0}connectedCallback(){super.connectedCallback(),this._scheduleMidnightRefresh()}disconnectedCallback(){super.disconnectedCallback(),clearTimeout(this._midnightTimer),clearTimeout(this._settingTimeout)}_scheduleMidnightRefresh(){clearTimeout(this._midnightTimer);const e=new Date,t=new Date(e);t.setHours(24,0,1,0),this._midnightTimer=setTimeout(()=>{this._day++,this._scheduleMidnightRefresh()},t.getTime()-e.getTime())}static getStubConfig(){return{name:"Thermostat",day_mode_entity:"select.homeshift_day_mode",thermostat_mode_entity:"select.homeshift_thermostat_mode",override_duration_entity:"number.homeshift_override_duration",early_switch_entity:"number.homeshift_early_switch",next_mode_entity:"sensor.homeshift_next_mode",next_mode_at_entity:"sensor.homeshift_next_mode_at",heat_protection_entity:"binary_sensor.homeshift_cover_heat_active",cover_open_time_entity:"sensor.homeshift_cover_open_time",cover_close_time_entity:"sensor.homeshift_cover_close_time",covers_left_open_entity:"binary_sensor.homeshift_covers_left_open",covers_inhibited_entity:"sensor.homeshift_covers_inhibited"}}static getConfigElement(){return document.createElement("homeshift-card-editor")}setConfig(e){if(!e)throw new Error("Missing configuration");const t={name:e.name??"Thermostat"};for(const i of ve)t[i.key]=e[i.key]??i.default;this._config=t}getCardSize(){return 4}shouldUpdate(e){if(e.has("_config")||e.has("preview"))return!0;if(e.has("_day")||e.has("_openSetting")||e.has("_inhibitIndex")||e.has("_coversExpanded")||e.has("_coverPending"))return!0;if(e.has("hass")){const t=e.get("hass");if(!t)return!0;return ve.map(e=>this._config?.[e.key]).filter(Boolean).some(e=>t.states[e]!==this.hass.states[e])}return!1}_presetClass(e,t){const i=e=>e.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g,"").replace(/[^a-z0-9]+/g,"-");if(["off","heating","cooling","ventilation"].includes(i(e)))return i(e);return{eteint:"off",arret:"off",off:"off",chauffage:"heating",heat:"heating",heating:"heating",climatisation:"cooling",clim:"cooling",cool:"cooling",cooling:"cooling",ventilation:"ventilation",ventilateur:"ventilation",fan:"ventilation"}[i(t)]??i(e)}getEntityState(e){if(e)return this.hass?.states?.[e]}getCoversLeftOpen(){const e=this.getEntityState(this._config?.covers_left_open_entity)?.attributes?.covers;return Array.isArray(e)?e.map(e=>this.hass?.states?.[e]?.attributes?.friendly_name??e):[]}_friendlyName(e){return this.hass?.states?.[e]?.attributes?.friendly_name??e}getCoverInhibitions(){const e=this.getEntityState(this._config?.covers_inhibited_entity)?.attributes??{},t=Array.isArray(e.managed_covers)?e.managed_covers:[],i=new Map,s=Date.now();for(const[t,o]of Object.entries(e.covers??{}))"string"==typeof o&&new Date(o).getTime()<=s||i.set(t,"string"==typeof o?o:null);return{managed:t,inhibited:i}}async _callInhibitService(e,t){this._armSettingTimeout();try{await this.hass.callService("homeshift",e,t)}catch(e){this.dispatchEvent(new CustomEvent("hass-notification",{detail:{message:me(this.hass,"card.inhibit_failed",{error:e?.message??String(e)})},bubbles:!0,composed:!0}))}}_inhibitCover(e){const t=Ee.INHIBIT_PRESETS[this._inhibitIndex];this._callInhibitService("inhibit_covers",{entity_id:[e],...t?{duration:{minutes:t}}:{}})}_resumeCovers(e){this._callInhibitService("resume_covers",e?{entity_id:e}:{})}_stepInhibit(e){const t=Ee.INHIBIT_PRESETS.length-1;this._inhibitIndex=Math.min(t,Math.max(0,this._inhibitIndex+e)),this._armSettingTimeout()}_inhibitLabel(e){return e?e<1440?be(e):`${e/1440} ${me(this.hass,"card.days_short")}`:me(this.hass,"card.inhibit_forever")}_inhibitEnd(e){return e?me(this.hass,"card.inhibited_until",{time:this._formatAbsoluteTime(e)}):me(this.hass,"card.inhibited_forever")}_renderCoverInhibition({managed:e,inhibited:t}){const i="covers"===this._openSetting,s=Ee.INHIBIT_PRESETS,o=s[this._inhibitIndex],n=me(this.hass,"card.inhibit_label"),r=o?me(this.hass,"card.inhibit_effect",{duration:this._inhibitLabel(o),time:this._formatAbsoluteTime(new Date(Date.now()+6e4*o).toISOString())}):me(this.hass,"card.inhibit_effect_forever");return q`
+ `,e([he({attribute:!1})],ve.prototype,"hass",void 0),e([ue()],ve.prototype,"_config",void 0),customElements.get("homeshift-card-editor")||customElements.define("homeshift-card-editor",ve);const ge=new Set(["unknown","unavailable",""]);function ye(e){const t="string"==typeof e?e:e?.state;return"string"==typeof t&&!ge.has(t)}function be(e){if(e<60)return`${e} min`;const t=Math.floor(e/60),i=Math.round(e%60);return i>0?`${t}h${String(i).padStart(2,"0")}`:`${t}h`}function $e(e){const t=e?.time_format;return{locale:"system"===t?void 0:e?.language,hour12:"12"===t||"24"!==t&&void 0}}function xe(e,t){const{locale:i,hour12:s}=$e(t);return e.toLocaleTimeString(i,{hour:"2-digit",minute:"2-digit",hour12:s})}const we=1440;function Se(e){const[t,i]=e.split(":").map(Number);return 60*t+(i||0)}function Ee(e){const t=e%we;return`${String(Math.floor(t/60)).padStart(2,"0")}:${String(t%60).padStart(2,"0")}`}const ke=e=>e.service.startsWith("climate.");function Ae(e){return e.timeslots.some(e=>e.actions.some(ke))}function Ce(e){const t=Ae(e),i=e.timeslots.filter(e=>!t||e.actions.some(ke)).map(e=>({slot:e,start:Se(e.start)})).sort((e,t)=>e.start-t.start),s=[];return i.forEach(({slot:e,start:t},o)=>{const n=function(e){const t=e.actions?.find(ke)??e.actions?.[0];if(!t)return{kind:"other",label:""};const[,i]=t.service.split("."),s=t.service_data??{},o=t.entity_id,n=s.hvac_mode;if("off"===n)return{kind:"off",entity:o,hvacMode:n,label:n};if("turn_off"===i)return{kind:"off",entity:o,label:"off"};if("number"==typeof s.temperature)return{kind:"cool"===n?"cool":"heat",entity:o,temperature:s.temperature,label:`${s.temperature}°`};if(null!=s.preset_mode){const e=String(s.preset_mode);return{kind:"preset",entity:o,preset:e,label:e}}return n?{kind:"cool"===n?"cool":"heat",entity:o,hvacMode:n,label:n}:"turn_on"===i?{kind:"on",entity:o,label:"on"}:{kind:"other",entity:o,label:i??t.service}}(e);let r;if(null!=e.stop)r=Se(e.stop),r<=t&&(r+=we);else{const e=i[(o+1)%i.length].start;r=e>t?e:e+we}r>we?(s.push({start:t,end:we,value:n}),r-we>0&&s.push({start:0,end:r-we,value:n})):s.push({start:t,end:r,value:n})}),function(e){const t=[];for(const i of e){const e=t[t.length-1];e&&e.end===i.start&&Te(e.value,i.value)?e.end=i.end:t.push({...i})}return t}(s.filter(e=>e.end>e.start).sort((e,t)=>e.start-t.start))}function Te(e,t){return e.kind===t.kind&&e.temperature===t.temperature&&e.preset===t.preset&&e.hvacMode===t.hvacMode&&e.label===t.label}function Pe(e,t=!1){const i=t&&Ae(e),s=e.timeslots.flatMap(e=>e.actions.filter(e=>!i||ke(e)).map(e=>e.entity_id).filter(e=>!!e));return[...new Set(s)]}const Me={heat:"mdi:radiator",cool:"mdi:snowflake",preset:"mdi:radiator",on:"mdi:toggle-switch-outline",off:"mdi:toggle-switch-off-outline",other:"mdi:calendar-clock"};function Oe(e){return"cool"===e?"cool":"heat"===e||"preset"===e?"heat":void 0}const De=["heat","preset","cool","on","off","other"];function Ie(e){const t=new Set(e.map(e=>e.value.kind));return["cool","heat","preset","on","off"].find(e=>t.has(e))??"other"}class Ne extends ae{constructor(){super(...arguments),this.schedules=[],this.tag="",this.entities=[]}connectedCallback(){super.connectedCallback(),this._clock=setInterval(()=>this.requestUpdate(),6e4)}disconnectedCallback(){super.disconnectedCallback(),clearInterval(this._clock)}render(){if(!this.hass)return F;const e=function(e,t,i){return e.filter(e=>e.tags?.includes(t)).filter(e=>!i.length||Pe(e).some(e=>i.includes(e))).sort((e,t)=>e.name.localeCompare(t.name))}(this.schedules,this.tag,this.entities);if(!e.length)return q`
+ ${me(this.hass,"schedule.empty",{mode:this.tag})} +
`;const t=new Date,i=60*t.getHours()+t.getMinutes();return q` + ${this._rooms(e).map(e=>this._renderRoom(e,i))} + + `}_rooms(e){const t=new Map;for(const i of e){const e=Pe(i,!0),s=1===e.length?e[0]:void 0,o=s??i.entity_id;if(!t.has(o)){const e=s?this.hass.states[s]?.attributes?.friendly_name??s:i.name;t.set(o,{name:e,entity:s,schedules:[]})}t.get(o).schedules.push(i)}for(const e of t.values())e.schedules.sort((e,t)=>De.indexOf(Ie(Ce(e)))-De.indexOf(Ie(Ce(t))));return[...t.values()]}_renderRoom(e,t){const i=e.entity?.startsWith("climate.")?this.hass.states[e.entity]?.attributes?.current_temperature:void 0,s=e.schedules.every(e=>!this._enabled(e));return q` +
+
+ ${e.name} + ${"number"==typeof i?q`${this._number(i)}°`:F} +
+ ${e.schedules.map(i=>this._renderRow(i,e,t))} +
+ `}_enabled(e){const t=this.hass.states[e.entity_id];return t?"off"!==t.state:e.enabled}_renderRow(e,t,i){const s=Ce(e),o=Ie(s),n=this._enabled(e);return q` +
+ +
+ ${s.map(e=>this._renderSegment(e))} +
+
+ +
+ `}_renderSegment(e){const{background:t,color:i}=function(e){switch(e.kind){case"cool":return{background:"#378ADD",color:"#fff"};case"off":return{background:"var(--divider-color, #5F5E5A)",color:"var(--secondary-text-color)"};case"on":return{background:"#1D9E75",color:"#fff"};case"other":return{background:"#888780",color:"#fff"};case"preset":return function(e){const t=e.toLowerCase();return/frost|hors|away|absen|vacation/.test(t)?{background:"#85B7EB",color:"#042C53"}:/eco|sleep|night|nuit/.test(t)?{background:"#FAC775",color:"#412402"}:/boost/.test(t)?{background:"#A32D2D",color:"#fff"}:/comfort|confort|home|activity/.test(t)?{background:"#EF9F27",color:"#412402"}:{background:"#B4B2A9",color:"#2C2C2A"}}(e.preset??"")}const t=e.temperature??19;return t<15?{background:"#85B7EB",color:"#042C53"}:t<17.5?{background:"#FAC775",color:"#412402"}:t<19.5?{background:"#EF9F27",color:"#412402"}:t<21?{background:"#D85A30",color:"#fff"}:{background:"#A32D2D",color:"#fff"}}(e.value),s=(e.end-e.start)/we*100,o=`${Ee(e.start)} – ${Ee(e.end)}`,n=this._label(e.value),r=s>=Math.max(10,2.2*n.length);return q`
+ ${r?n:F} +
`}_number(e){const t=this.hass.locale?.language??this.hass.language??"en";return e.toLocaleString(t,{maximumFractionDigits:1})}_label(e){const t=e.entity?this.hass.states[e.entity]:void 0;return null!=e.temperature?`${this._number(e.temperature)}°`:null!=e.preset?t&&this.hass.formatEntityAttributeValue?this.hass.formatEntityAttributeValue(t,"preset_mode",e.preset):e.preset:null!=e.hvacMode?t&&this.hass.formatEntityState?this.hass.formatEntityState(t,e.hvacMode):e.hvacMode:"on"===e.kind||"off"===e.kind?me(this.hass,`schedule.${e.kind}`):e.label}async _toggle(e,t,i){if(i)return void await this.hass.callService("switch","turn_off",{entity_id:e.entity_id});const s=Oe(Ie(Ce(e))),o=s?t.schedules.filter(t=>{const i=Oe(Ie(Ce(t)));return t!==e&&i&&i!==s&&this._enabled(t)}):[];o.length&&await this.hass.callService("switch","turn_off",{entity_id:o.map(e=>e.entity_id)}),await this.hass.callService("switch","turn_on",{entity_id:e.entity_id})}}Ne.styles=r` + :host { + display: flex; + flex-direction: column; + gap: 10px; + } + .room { + display: flex; + flex-direction: column; + gap: 4px; + } + .room-head { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + } + .room-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 500; + } + .room-name.disabled { + color: var(--secondary-text-color); + } + .row { + display: flex; + align-items: center; + gap: 8px; + } + .row .bar { + flex: 1; + } + /* A disabled schedule keeps its colors, hatched, so it stays readable. */ + .row.disabled .segment { + background-image: repeating-linear-gradient( + 135deg, + rgba(0, 0, 0, 0.3) 0 4px, + transparent 4px 8px + ); + } + .kind { + flex: none; + --mdc-icon-size: 18px; + color: var(--secondary-text-color); + } + .kind.heat, + .kind.preset { + color: #ef9f27; + } + .kind.cool { + color: #378add; + } + .measured { + flex: none; + padding: 1px 8px; + border-radius: 10px; + background: color-mix(in srgb, var(--primary-text-color) 10%, transparent); + font-size: 12px; + } + .switch { + position: relative; + flex: none; + width: 34px; + height: 18px; + border: none; + border-radius: 9px; + background: var(--disabled-color, #888); + cursor: pointer; + padding: 0; + } + .switch::after { + content: ""; + position: absolute; + top: 3px; + left: 3px; + width: 12px; + height: 12px; + border-radius: 50%; + background: #fff; + transition: left 0.15s; + } + .switch.on { + background: var(--switch-checked-track-color, var(--primary-color)); + } + .switch.on::after { + left: 19px; + } + .switch:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; + } + .bar { + position: relative; + height: 20px; + border-radius: 6px; + background: color-mix(in srgb, var(--primary-text-color) 6%, transparent); + overflow: hidden; + } + .segment { + position: absolute; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 600; + overflow: hidden; + white-space: nowrap; + box-shadow: inset -1px 0 0 var(--ha-card-background, var(--card-background-color)); + } + .now { + position: absolute; + top: 0; + bottom: 0; + width: 2px; + margin-left: -1px; + background: var(--primary-text-color); + } + .axis { + position: relative; + height: 14px; + /* Under the bars only: past the kind icon (18 + 8 px) and before the switch (34 + 8 px). */ + margin: -4px 42px 0 26px; + font-size: 11px; + color: var(--secondary-text-color); + } + .axis span { + position: absolute; + transform: translateX(-50%); + } + .axis span:first-child { + transform: none; + } + .axis span:last-child { + transform: translateX(-100%); + } + .empty { + padding: 16px 0; + text-align: center; + font-size: 13px; + color: var(--secondary-text-color); + } + `,e([he({attribute:!1})],Ne.prototype,"hass",void 0),e([he({attribute:!1})],Ne.prototype,"schedules",void 0),e([he()],Ne.prototype,"tag",void 0),e([he({attribute:!1})],Ne.prototype,"entities",void 0),customElements.get("homeshift-schedule-timeline")||customElements.define("homeshift-schedule-timeline",Ne);class Re extends ae{constructor(){super(...arguments),this.schedules=[],this.modes=[],this.activeMode="",this.entities=[],this._editing=!1,this._cardsVersion=0,this._cards=new Map,this._building=new Set}willUpdate(e){e.has("activeMode")&&this.modes.includes(this.activeMode)?this._selected=this.activeMode:this._selected&&this.modes.includes(this._selected)||(this._selected=this.modes[0]),e.has("entities")&&void 0!==e.get("entities")&&(this._cards=new Map,this._building.clear())}updated(){const e=this._selected?this._cards.get(this._selected):void 0;e&&e.hass!==this.hass&&(e.hass=this.hass)}_schedulerConfig(e){return{type:"custom:scheduler-card",title:!1,discover_existing:!1,show_header_toggle:!1,time_step:5,sort_by:["title"],display_options:{primary_info:["{name}"],secondary_info:[`${me(this.hass,"schedule.next_run")} {relative-time}`,`${me(this.hass,"schedule.days")} {days}`,"additional-tasks"],icon:"entity"},tags:[e],...this.entities.length?{include:this.entities}:{}}}_card(e){const t=this._cards.get(e);if(t)return t;this._building.has(e)||(this._building.add(e),this._buildCard(e).finally(()=>this._building.delete(e)))}async _buildCard(e){const t=this._schedulerConfig(e),i=this._cards;this._helpers??(this._helpers=window.loadCardHelpers?.()??Promise.resolve(void 0));const s=await this._helpers;let o;s?o=s.createCardElement(t):(await customElements.whenDefined("scheduler-card"),o=document.createElement("scheduler-card"),o.setConfig(t)),i===this._cards&&(o.addEventListener("ll-rebuild",t=>{t.stopPropagation(),this._cards.delete(e),this._cardsVersion++}),o.hass=this.hass,this._cards.set(e,o),this._cardsVersion++)}_select(e){this._selected=e}_onTabKeydown(e){const t={ArrowRight:1,ArrowLeft:-1}[e.key];if(!t)return;const i=this.modes.indexOf(this._selected??""),s=this.modes[(i+t+this.modes.length)%this.modes.length];this._select(s),e.preventDefault(),this.updateComplete.then(()=>this.shadowRoot?.querySelector('[aria-selected="true"]')?.focus())}render(){if(!this.hass||!this._selected)return F;const e=this._selected,t=me(this.hass,this._editing?"schedule.back":"schedule.edit");return q` +
+
+ ${this.modes.map(t=>{const i=t===e;return q``})} +
+ +
+
+ ${this._editing?this._card(e)??F:q``} +
+ `}}Re.styles=r` + :host { + display: flex; + flex-direction: column; + gap: 10px; + } + .header { + display: flex; + align-items: center; + gap: 6px; + } + /* Segmented control: one button per mode, the shown one filled. */ + .tabs { + flex: 1; + min-width: 0; + display: flex; + gap: 2px; + padding: 3px; + border-radius: 10px; + background: color-mix(in srgb, var(--primary-text-color) 7%, transparent); + overflow-x: auto; + scrollbar-width: none; + } + .tab { + flex: 1 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + padding: 5px 8px; + border: none; + border-radius: 8px; + background: none; + cursor: pointer; + font: inherit; + font-size: 13px; + font-weight: 500; + color: var(--secondary-text-color); + } + .tab:hover:not(.selected) { + background: color-mix(in srgb, var(--primary-text-color) 6%, transparent); + } + .tab:focus-visible, + .edit:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 1px; + } + .tab.selected { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + } + .active-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--state-active-color, var(--success-color, #43a047)); + flex: none; + } + .edit { + flex: none; + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + padding: 0; + border: none; + border-radius: 10px; + background: color-mix(in srgb, var(--primary-text-color) 7%, transparent); + color: var(--primary-color); + cursor: pointer; + --mdc-icon-size: 18px; + } + .edit:hover { + background: color-mix(in srgb, var(--primary-text-color) 12%, transparent); + } + .edit[aria-pressed="true"] { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + } + /* The scheduler card blends into the panel. */ + .content { + --ha-card-border-width: 0; + --ha-card-box-shadow: none; + --ha-card-background: transparent; + } + `,e([he({attribute:!1})],Re.prototype,"hass",void 0),e([he({attribute:!1})],Re.prototype,"schedules",void 0),e([he({attribute:!1})],Re.prototype,"modes",void 0),e([he()],Re.prototype,"activeMode",void 0),e([he({attribute:!1})],Re.prototype,"entities",void 0),e([ue()],Re.prototype,"_selected",void 0),e([ue()],Re.prototype,"_editing",void 0),e([ue()],Re.prototype,"_cardsVersion",void 0),customElements.get("homeshift-schedule-panel")||customElements.define("homeshift-schedule-panel",Re);class He extends ae{constructor(){super(...arguments),this.preview=!1,this._openSetting=null,this._inhibitIndex=He.INHIBIT_DEFAULT_INDEX,this._coversExpanded=!1,this._day=0,this._schedulesOpen=!1}connectedCallback(){super.connectedCallback(),this._scheduleMidnightRefresh()}disconnectedCallback(){super.disconnectedCallback(),clearTimeout(this._midnightTimer),clearTimeout(this._settingTimeout)}_scheduleMidnightRefresh(){clearTimeout(this._midnightTimer);const e=new Date,t=new Date(e);t.setHours(24,0,1,0),this._midnightTimer=setTimeout(()=>{this._day++,this._scheduleMidnightRefresh()},t.getTime()-e.getTime())}static getStubConfig(){return{name:"Thermostat",day_mode_entity:"select.homeshift_day_mode",thermostat_mode_entity:"select.homeshift_thermostat_mode",override_duration_entity:"number.homeshift_override_duration",early_switch_entity:"number.homeshift_early_switch",next_mode_entity:"sensor.homeshift_next_mode",next_mode_at_entity:"sensor.homeshift_next_mode_at",heat_protection_entity:"binary_sensor.homeshift_cover_heat_active",cover_open_time_entity:"sensor.homeshift_cover_open_time",cover_close_time_entity:"sensor.homeshift_cover_close_time",covers_left_open_entity:"binary_sensor.homeshift_covers_left_open",covers_inhibited_entity:"sensor.homeshift_covers_inhibited"}}static getConfigElement(){return document.createElement("homeshift-card-editor")}setConfig(e){if(!e)throw new Error("Missing configuration");const t={name:e.name??"Thermostat"};for(const i of fe)t[i.key]=e[i.key]??i.default;t.show_schedules=!1!==e.show_schedules,t.schedule_entities=Array.isArray(e.schedule_entities)?e.schedule_entities:[],this._config=t}getCardSize(){return 4}shouldUpdate(e){if(e.has("hass")&&this._refreshSchedules(),e.has("_config")||e.has("preview"))return!0;if(e.has("_day")||e.has("_openSetting")||e.has("_inhibitIndex")||e.has("_coversExpanded")||e.has("_coverPending")||e.has("_schedules")||e.has("_schedulesOpen"))return!0;if(e.has("hass")){const t=e.get("hass");if(!t)return!0;if(this._schedulesOpen)return!0;return fe.map(e=>this._config?.[e.key]).filter(Boolean).some(e=>t.states[e]!==this.hass.states[e])}return!1}_presetClass(e,t){const i=e=>e.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g,"").replace(/[^a-z0-9]+/g,"-");if(["off","heating","cooling","ventilation"].includes(i(e)))return i(e);return{eteint:"off",arret:"off",off:"off",chauffage:"heating",heat:"heating",heating:"heating",climatisation:"cooling",clim:"cooling",cool:"cooling",cooling:"cooling",ventilation:"ventilation",ventilateur:"ventilation",fan:"ventilation"}[i(t)]??i(e)}getEntityState(e){if(e)return this.hass?.states?.[e]}getCoversLeftOpen(){const e=this.getEntityState(this._config?.covers_left_open_entity)?.attributes?.covers;return Array.isArray(e)?e.map(e=>this.hass?.states?.[e]?.attributes?.friendly_name??e):[]}_friendlyName(e){return this.hass?.states?.[e]?.attributes?.friendly_name??e}getCoverInhibitions(){const e=this.getEntityState(this._config?.covers_inhibited_entity)?.attributes??{},t=Array.isArray(e.managed_covers)?e.managed_covers:[],i=new Map,s=Date.now();for(const[t,o]of Object.entries(e.covers??{}))"string"==typeof o&&new Date(o).getTime()<=s||i.set(t,"string"==typeof o?o:null);return{managed:t,inhibited:i}}async _callInhibitService(e,t){this._armSettingTimeout();try{await this.hass.callService("homeshift",e,t)}catch(e){this.dispatchEvent(new CustomEvent("hass-notification",{detail:{message:me(this.hass,"card.inhibit_failed",{error:e?.message??String(e)})},bubbles:!0,composed:!0}))}}_inhibitCover(e){const t=He.INHIBIT_PRESETS[this._inhibitIndex];this._callInhibitService("inhibit_covers",{entity_id:[e],...t?{duration:{minutes:t}}:{}})}_resumeCovers(e){this._callInhibitService("resume_covers",e?{entity_id:e}:{})}_stepInhibit(e){const t=He.INHIBIT_PRESETS.length-1;this._inhibitIndex=Math.min(t,Math.max(0,this._inhibitIndex+e)),this._armSettingTimeout()}_inhibitLabel(e){return e?e<1440?be(e):`${e/1440} ${me(this.hass,"card.days_short")}`:me(this.hass,"card.inhibit_forever")}_inhibitEnd(e){return e?me(this.hass,"card.inhibited_until",{time:this._formatAbsoluteTime(e)}):me(this.hass,"card.inhibited_forever")}_renderCoverInhibition({managed:e,inhibited:t}){const i="covers"===this._openSetting,s=He.INHIBIT_PRESETS,o=s[this._inhibitIndex],n=me(this.hass,"card.inhibit_label"),r=o?me(this.hass,"card.inhibit_effect",{duration:this._inhibitLabel(o),time:this._formatAbsoluteTime(new Date(Date.now()+6e4*o).toISOString())}):me(this.hass,"card.inhibit_effect_forever");return q`
`:W} -
`:W} -
`}onSelect(e,t){const i=t.target,s=i?.value;s&&this.hass.callService("select","select_option",{entity_id:e,option:s})}onThermostatSelect(e,t){this.hass.callService("select","select_option",{entity_id:e,option:t})}_coverCall(e){const t=this._config.cover_entity;if(t)return ye(this.getEntityState(t))?{domain:"cover",service:`${e}_cover`,entity_id:t}:void 0;const i="open"===e?this._config.open_covers_entity:this._config.close_covers_entity,s=this.getEntityState(i);return s&&"unavailable"!==s.state?{domain:"button",service:"press",entity_id:i}:void 0}async onCoverAction(e){const t=this._coverCall(e);if(t&&!this._coverPending){this._coverPending=e;try{await Promise.all([this.hass.callService(t.domain,t.service,{entity_id:t.entity_id}),new Promise(e=>setTimeout(e,Ee.COVER_FEEDBACK_MS))])}catch(t){this.dispatchEvent(new CustomEvent("hass-notification",{detail:{message:me(this.hass,`card.cover_${e}_failed`,{error:t?.message??String(t)})},bubbles:!0,composed:!0}))}finally{this._coverPending=void 0}}}_renderCoverTime(e,t){const i="open"===e?"mdi:roller-shade":"mdi:roller-shade-closed";if(!this._coverCall(e))return q``:F} +
`:F} +
`}_refreshSchedules(){if(!this._config?.show_schedules||this.preview||!this.hass?.callWS)return;const e=Object.values(this.hass.states??{}).filter(e=>e.entity_id.startsWith("switch.schedule")).map(e=>`${e.entity_id}:${e.last_updated}`).join("|");e!==this._schedulesKey&&(this._schedulesKey=e,this.hass.callWS({type:"scheduler"}).then(e=>{this._schedules=Array.isArray(e)?e:void 0}).catch(()=>{this._schedules=void 0}))}_hasDaySchedules(e){return(this._schedules??[]).some(t=>t.tags?.some(t=>e.includes(t)))}_toggleSchedules(e){this._schedulesOpen=e,this.updateComplete.then(()=>this.shadowRoot?.querySelector(e?".schedules-close":".schedules-open")?.focus())}_renderSchedules(e,t){const i=me(this.hass,"schedule.title"),s=t=>q``;return q`${s(!1)} + ${this._schedulesOpen?q`
{"Escape"===e.key&&(e.stopPropagation(),this._toggleSchedules(!1))}} + > + ${s(!0)} +
+ +
+
`:F}`}onSelect(e,t){const i=t.target,s=i?.value;s&&this.hass.callService("select","select_option",{entity_id:e,option:s})}onThermostatSelect(e,t){this.hass.callService("select","select_option",{entity_id:e,option:t})}_coverCall(e){const t=this._config.cover_entity;if(t)return ye(this.getEntityState(t))?{domain:"cover",service:`${e}_cover`,entity_id:t}:void 0;const i="open"===e?this._config.open_covers_entity:this._config.close_covers_entity,s=this.getEntityState(i);return s&&"unavailable"!==s.state?{domain:"button",service:"press",entity_id:i}:void 0}async onCoverAction(e){const t=this._coverCall(e);if(t&&!this._coverPending){this._coverPending=e;try{await Promise.all([this.hass.callService(t.domain,t.service,{entity_id:t.entity_id}),new Promise(e=>setTimeout(e,He.COVER_FEEDBACK_MS))])}catch(t){this.dispatchEvent(new CustomEvent("hass-notification",{detail:{message:me(this.hass,`card.cover_${e}_failed`,{error:t?.message??String(t)})},bubbles:!0,composed:!0}))}finally{this._coverPending=void 0}}}_renderCoverTime(e,t){const i="open"===e?"mdi:roller-shade":"mdi:roller-shade-closed";if(!this._coverCall(e))return q` @@ -171,15 +525,15 @@ const he={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:b},le=(e=he

${s}

-
`:W} -
`}_minutes(e){if(!e)return null;const t=this.getEntityState(e);return t?function(e){if(!ye(e))return;const t=Number(e.state);return Number.isFinite(t)?t:void 0}(t):this.preview?0:null}_renderMain(e,t,i,s,o,n,r){const a=t.attributes?.option_map??{},c=Object.keys(a).length>0?Object.entries(a):(t.attributes?.options??[]).map(e=>[e,e]),h=e.attributes?.option_map??{},l=Object.keys(h).length>0?Object.entries(h):(e.attributes?.options??[]).map(e=>[e,e]),d=this.getEntityState(this._config.next_mode_entity),p=this.getEntityState(this._config.next_mode_at_entity),u=this._minutes(this._config.early_switch_entity),_=this._minutes(this._config.override_duration_entity),m=ye(d),v=m?d.state:"",f=ye(p)?this._formatAbsoluteTime(p.state):"",g=""!==f,y=_?me(this.hass,"card.override_effect",{duration:this._settingLabel(_),time:this._clockIn(_)}):me(this.hass,"card.override_none"),b=g?u?me(this.hass,"card.early_effect",{mode:v,time:this._clockAt(p.state),scheduled:this._clockAt(p.state,u)}):me(this.hass,"card.early_none",{mode:v,time:this._clockAt(p.state)}):me(this.hass,"card.early_no_event"),$=ye(n),x=ye(r),E=me(this.hass,"card.covers_left_open",{covers:s.join(", ")}),w=s.length>3,S=w?me(this.hass,"card.covers_left_open_count",{count:String(s.length)}):E,A=[...o.inhibited.keys()].map(e=>this._friendlyName(e)),C=A.length>2?me(this.hass,"card.covers_paused_count",{count:String(A.length)}):me(this.hass,"card.covers_paused",{covers:A.join(", ")}),k=[...o.inhibited.entries()].map(([e,t])=>`${this._friendlyName(e)} — ${this._inhibitEnd(t)}`).join("\n"),T=e.attributes?.current_key,P=ye(e);return q` + `:F} + `}_minutes(e){if(!e)return null;const t=this.getEntityState(e);return t?function(e){if(!ye(e))return;const t=Number(e.state);return Number.isFinite(t)?t:void 0}(t):this.preview?0:null}_renderMain(e,t,i,s,o,n,r){const a=t.attributes?.option_map??{},c=Object.keys(a).length>0?Object.entries(a):(t.attributes?.options??[]).map(e=>[e,e]),l=e.attributes?.option_map??{},d=Object.keys(l).length>0?Object.entries(l):(e.attributes?.options??[]).map(e=>[e,e]),h=this.getEntityState(this._config.next_mode_entity),u=this.getEntityState(this._config.next_mode_at_entity),p=this._minutes(this._config.early_switch_entity),_=this._minutes(this._config.override_duration_entity),m=ye(h),f=m?h.state:"",v=ye(u)?this._formatAbsoluteTime(u.state):"",g=""!==v,y=_?me(this.hass,"card.override_effect",{duration:this._settingLabel(_),time:this._clockIn(_)}):me(this.hass,"card.override_none"),b=g?p?me(this.hass,"card.early_effect",{mode:f,time:this._clockAt(u.state),scheduled:this._clockAt(u.state,p)}):me(this.hass,"card.early_none",{mode:f,time:this._clockAt(u.state)}):me(this.hass,"card.early_no_event"),$=ye(n),x=ye(r),w=me(this.hass,"card.covers_left_open",{covers:s.join(", ")}),S=s.length>3,E=S?me(this.hass,"card.covers_left_open_count",{count:String(s.length)}):w,k=[...o.inhibited.keys()].map(e=>this._friendlyName(e)),A=k.length>2?me(this.hass,"card.covers_paused_count",{count:String(k.length)}):me(this.hass,"card.covers_paused",{covers:k.join(", ")}),C=[...o.inhibited.entries()].map(([e,t])=>`${this._friendlyName(e)} — ${this._inhibitEnd(t)}`).join("\n"),T=e.attributes?.current_key,P=ye(e);return q`
- ${l.map(([t,i])=>{const s=P&&(T?t===T:i===e.state);return q`
`:W} +
`:F} - ${null!==_?this._renderSetting("override","mdi:hand-back-left",me(this.hass,"card.override_label"),y,Ee.OVERRIDE_PRESETS,_):W} - ${null!==u?this._renderSetting("early","mdi:clock-fast",me(this.hass,"card.early_label"),b,Ee.EARLY_SWITCH_PRESETS,u):W} + ${null!==_?this._renderSetting("override","mdi:hand-back-left",me(this.hass,"card.override_label"),y,He.OVERRIDE_PRESETS,_):F} + ${null!==p?this._renderSetting("early","mdi:clock-fast",me(this.hass,"card.early_label"),b,He.EARLY_SWITCH_PRESETS,p):F} ${$||x?q`
${me(this.hass,"card.covers")} - ${$?this._renderCoverTime("open",n):W} - ${x?this._renderCoverTime("close",r):W} + ${$?this._renderCoverTime("open",n):F} + ${x?this._renderCoverTime("close",r):F} -
`:W} + `:F} - ${o.managed.length>0?this._renderCoverInhibition(o):W} + ${o.managed.length>0?this._renderCoverInhibition(o):F} - ${i||s.length>0||A.length>0?q`
+ ${i||s.length>0||k.length>0?q`
${i?q` ${me(this.hass,"card.heat_protection_active")} - `:W} - ${s.length>0&&!w?q` + `:F} + ${s.length>0&&!S?q` - ${S} - `:W} - ${w?q``:W} - ${A.length>0?q``:W} + ${A} + `:F}
- ${w&&this._coversExpanded?q`

${E}

`:W}`:W} + ${S&&this._coversExpanded?q`

${w}

`:F}`:F}
- `}render(){if(!this.hass||!this._config)return W;const e=this.getEntityState(this._config.day_mode_entity),t=this.getEntityState(this._config.thermostat_mode_entity);if(!this.preview){const i=[[this._config.day_mode_entity,e],[this._config.thermostat_mode_entity,t]].filter(([,e])=>!e).map(([e])=>e||"?");if(i.length>0)return q` + `}render(){if(!this.hass||!this._config)return F;const e=this.getEntityState(this._config.day_mode_entity),t=this.getEntityState(this._config.thermostat_mode_entity);if(!this.preview){const i=[[this._config.day_mode_entity,e],[this._config.thermostat_mode_entity,t]].filter(([,e])=>!e).map(([e])=>e||"?");if(i.length>0)return q` ${i.map(e=>q`
${me(this.hass,"card.entity_not_found",{entity:e})}
`)}
- `}const i=e??{entity_id:this._config.day_mode_entity,state:me(this.hass,"preview.work"),attributes:{option_map:Object.fromEntries(["home","work","remote","away"].map(e=>[e,me(this.hass,`preview.${e}`)]))}},s=t??{entity_id:this._config.thermostat_mode_entity,state:me(this.hass,"thermostat.heating"),attributes:{option_map:Object.fromEntries(["off","heating","cooling","ventilation"].map(e=>[e,me(this.hass,`thermostat.${e}`)])),current_key:"heating"}},o="on"===this.getEntityState(this._config.heat_protection_entity)?.state,n=this.getEntityState(this._config.cover_open_time_entity)?.state,r=this.getEntityState(this._config.cover_close_time_entity)?.state;return q` + `}const i=e??{entity_id:this._config.day_mode_entity,state:me(this.hass,"preview.work"),attributes:{option_map:Object.fromEntries(["home","work","remote","away"].map(e=>[e,me(this.hass,`preview.${e}`)]))}},s=t??{entity_id:this._config.thermostat_mode_entity,state:me(this.hass,"thermostat.heating"),attributes:{option_map:Object.fromEntries(["off","heating","cooling","ventilation"].map(e=>[e,me(this.hass,`thermostat.${e}`)])),current_key:"heating"}},o="on"===this.getEntityState(this._config.heat_protection_entity)?.state,n=this.getEntityState(this._config.cover_open_time_entity)?.state,r=this.getEntityState(this._config.cover_close_time_entity)?.state,a=i.attributes?.options??[],c=ye(i)&&this._hasDaySchedules(a);return q` -
+
${this._renderMain(s,i,o,this.getCoversLeftOpen(),this.getCoverInhibitions(),n,r)}
+ ${c?this._renderSchedules(i.state,a):F} - `}}Ee.COVER_FEEDBACK_MS=600,Ee.OVERRIDE_PRESETS=[0,15,30,60,120,240,480,720],Ee.EARLY_SWITCH_PRESETS=[0,15,30,45,60,90,120,180,240],Ee.INHIBIT_PRESETS=[240,1440,2880,4320,10080,0],Ee.INHIBIT_DEFAULT_INDEX=1,Ee.styles=r` + `}}He.COVER_FEEDBACK_MS=600,He.OVERRIDE_PRESETS=[0,15,30,60,120,240,480,720],He.EARLY_SWITCH_PRESETS=[0,15,30,45,60,90,120,180,240],He.INHIBIT_PRESETS=[240,1440,2880,4320,10080,0],He.INHIBIT_DEFAULT_INDEX=1,He.styles=r` ha-card { padding: 12px; position: relative; + /* Clips the schedules panel while it slides up. */ + overflow: hidden; } .container { @@ -293,7 +650,8 @@ const he={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:b},le=(e=he border: 1px solid var(--divider-color, #ccc); border-radius: 8px; overflow: hidden; - margin-bottom: 4px; + /* Same gap above the first divider as the card's padding above. */ + margin-bottom: 12px; } .preset { @@ -703,6 +1061,103 @@ const he={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:b},le=(e=he } } + /* Schedules: a bar at the foot of the card, and the panel it slides up. */ + .schedules-bar { + display: flex; + align-items: center; + gap: 8px; + width: calc(100% + 24px); + margin: 10px -12px -12px; + padding: 10px 12px; + border: 0; + border-top: 1px solid var(--divider-color, #e0e0e0); + background: transparent; + color: var(--secondary-text-color, #666); + font-family: inherit; + font-size: 12px; + font-weight: 500; + letter-spacing: 0.05em; + text-transform: uppercase; + text-align: left; + cursor: pointer; + } + + .schedules-bar:hover { + background: var(--secondary-background-color, rgba(127, 127, 127, 0.08)); + } + + .schedules-bar:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: -2px; + } + + .schedules-bar ha-icon { + --mdc-icon-size: 18px; + flex-shrink: 0; + } + + .schedules-title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .schedules-mode { + color: var(--primary-text-color); + text-transform: none; + letter-spacing: normal; + } + + /* Anchored to the foot of the card and as tall as its content: the top + of the card stays in sight while the schedules are shorter than it; + longer ones scroll inside the panel. */ + .schedules-panel { + position: absolute; + left: 0; + right: 0; + bottom: 0; + max-height: 100%; + z-index: 1; + border-top: 1px solid var(--divider-color, #e0e0e0); + box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.25); + display: flex; + flex-direction: column; + background: var(--ha-card-background, var(--card-background-color, #fff)); + animation: schedules-slide-up 0.2s ease; + } + + .schedules-panel .schedules-bar { + width: 100%; + margin: 0; + border-top: 0; + border-bottom: 1px solid var(--divider-color, #e0e0e0); + } + + .schedules-body { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + padding: 10px 12px 12px; + } + + @keyframes schedules-slide-up { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } + } + + @media (prefers-reduced-motion: reduce) { + .schedules-panel { + animation: none; + } + } + select { padding: 6px 8px; border-radius: 8px; @@ -712,4 +1167,4 @@ const he={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:b},le=(e=he font-size: 13px; font-family: inherit; } - `,e([de({attribute:!1})],Ee.prototype,"hass",void 0),e([de({type:Boolean})],Ee.prototype,"preview",void 0),e([pe()],Ee.prototype,"_config",void 0),e([pe()],Ee.prototype,"_openSetting",void 0),e([pe()],Ee.prototype,"_inhibitIndex",void 0),e([pe()],Ee.prototype,"_coverPending",void 0),e([pe()],Ee.prototype,"_coversExpanded",void 0),e([pe()],Ee.prototype,"_day",void 0),customElements.get("homeshift-card")||customElements.define("homeshift-card",Ee),window.customCards=window.customCards||[],window.customCards.push({type:"homeshift-card",name:"HomeShift Card",description:"Card to manage day mode and thermostat mode via the HomeShift integration.",preview:!0}); + `,e([he({attribute:!1})],He.prototype,"hass",void 0),e([he({type:Boolean})],He.prototype,"preview",void 0),e([ue()],He.prototype,"_config",void 0),e([ue()],He.prototype,"_openSetting",void 0),e([ue()],He.prototype,"_inhibitIndex",void 0),e([ue()],He.prototype,"_coverPending",void 0),e([ue()],He.prototype,"_coversExpanded",void 0),e([ue()],He.prototype,"_day",void 0),e([ue()],He.prototype,"_schedules",void 0),e([ue()],He.prototype,"_schedulesOpen",void 0),customElements.get("homeshift-card")||customElements.define("homeshift-card",He),window.customCards=window.customCards||[],window.customCards.push({type:"homeshift-card",name:"HomeShift Card",description:"Card to manage day mode and thermostat mode via the HomeShift integration.",preview:!0}); diff --git a/package-lock.json b/package-lock.json index 7e45fac..a8fcc3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homeshift-card", - "version": "1.2.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homeshift-card", - "version": "1.2.0", + "version": "2.0.0", "license": "MIT", "dependencies": { "lit": "^3.3.2" diff --git a/package.json b/package.json index afeba97..4bdd524 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homeshift-card", - "version": "1.2.0", + "version": "2.0.0", "private": true, "description": "Custom card for Day Mode in Home Assistant", "type": "module",