- `:N``}_renderHelp(){return N`
+ `:I``}_renderHelp(){return I`
Auto entities
@@ -31,15 +31,15 @@ function t(t,e,i,n){var s,o=arguments.length,r=o<3?e:null===n?n=Object.getOwnPro
Not all options are available in the GUI editor.
- `}_renderFilterEditor(){var t;return(null===(t=this._config.filter)||void 0===t?void 0:t.template)||this._config.entities?N`
+ `}_renderFilterEditor(){var t;return(null===(t=this._config.filter)||void 0===t?void 0:t.template)||this._config.entities?I`
Your filter method is not handled by the GUI editor.
Please switch to the CODE EDITOR to access all options.
- `:N`
- ${this._config.filter.include.map(((t,e)=>N`
+ `:I`
+ ${this._config.filter.include.map(((t,e)=>I`
- ${void 0===t.type?N`
+ ${void 0===t.type?I`
{const e=Object.assign({},t);return delete e.options,[...Object.entries(e).map(It),Object.assign(Object.assign({},Nt),{name:"key_new",label:"Select property"})]})(t)}
- .data=${(t=>{const e=Object.assign({},t);return delete e.options,Object.assign({},...Object.entries(e).map((([t,e],i)=>({[`key_${i}`]:t,[`value_${i}`]:e}))))})(t)}
+ .schema=${(t=>{const e={...t};return delete e.options,[...Object.entries(e).map(Ft),{...jt,name:"key_new",label:"Select property"}]})(t)}
+ .data=${(t=>{const e={...t};return delete e.options,Object.assign({},...Object.entries(e).map((([t,e],i)=>({[`key_${i}`]:t,[`value_${i}`]:e}))))})(t)}
.computeLabel=${t=>{var e;return null!==(e=t.label)&&void 0!==e?e:t.name}}
@value-changed=${t=>this._changeFilter(e,t)}
>
Options:
this._changeGroupOptions(e,t)}
>
- `:N`
+ `:I`
this._changeSpecialEntry(e,t)}
>
@@ -91,26 +91,26 @@ function t(t,e,i,n){var s,o=arguments.length,r=o<3?e:null===n?n=Object.getOwnPro
Add non-filter entry
- `}_renderSortEditor(){var t;const e=null!==(t=this._config.sort)&&void 0!==t?t:{method:"none"};return N`
+ `}_renderSortEditor(){var t;const e=null!==(t=this._config.sort)&&void 0!==t?t:{method:"none"};return I`
{var e;return null!==(e=t.label)&&void 0!==e?e:t.name}}
@value-changed=${this._changeSortOptions}
>
- `}_renderCardEditor(){var t;const e=Object.assign({},this._config);return e.show_empty=null===(t=e.show_empty)||void 0===t||t,N`
+ `}_renderCardEditor(){var t;const e={...this._config};return e.show_empty=null===(t=e.show_empty)||void 0===t||t,I`
{var e;return null!==(e=t.label)&&void 0!==e?e:t.name}}
.data=${e}
@value-changed=${this._changeCardOptions}
>
- ${this._config.card?N`
+ ${this._config.card?I`
- `:N`
+ `:I`
(i,n)=>{var s;if(void 0===n){const n=null!==(s=i.originalKey)&&void 0!==s?s:i.key,o=null!=e?{kind:"method",placement:"prototype",key:n,descriptor:e(i.key)}:{...i,key:n};return null!=t&&(o.finisher=function(e){t(e,n)}),o}{const s=i.constructor;void 0!==e&&Object.defineProperty(i,n,e(n)),null==t||t(s,n)}})({descriptor:i=>{const n={get(){var e,i;return null!==(i=null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t))&&void 0!==i?i:null},enumerable:!0,configurable:!0};if(e){const e="symbol"==typeof i?Symbol():"__"+i;n.get=function(){var i,n;return void 0===this[e]&&(this[e]=null!==(n=null===(i=this.renderRoot)||void 0===i?void 0:i.querySelector(t))&&void 0!==n?n:null),this[e]}}return n}})}("hui-card-element-editor")],Lt.prototype,"_cardEditorEl",void 0),customElements.define("auto-entities-editor",Lt),window.customCards=window.customCards||[],window.customCards.push({type:"auto-entities",name:"Auto Entities",preview:!1,description:"Entity Filter on Steroids. Auto Entities allows you to fill other cards with entities automatically, based on a number of attributes."}),window.queueMicrotask=window.queueMicrotask||(t=>window.setTimeout(t,1));const Wt=["section","divider"];class Gt extends tt{constructor(){super(...arguments),this.empty=!1,this._updateCooldown={timer:void 0,rerun:!1},this._renderer=t=>{this._template="string"==typeof t?t.split(/[\s,]+/):t}}static getConfigElement(){return document.createElement("auto-entities-editor")}static getStubConfig(){return{card:{type:"entities"},filter:{include:[],exclude:[]}}}setConfig(t){var e,i;if(!t)throw new Error("No configuration.");if(!(null===(e=t.card)||void 0===e?void 0:e.type))throw new Error("No card type specified.");if(!t.filter&&!t.entities)throw new Error("No filters specified.");t=JSON.parse(JSON.stringify(t)),this._config=t,(null===(i=this._config.filter)||void 0===i?void 0:i.template)&&ut(this._config.filter.template)&&ft(this._renderer,this._config.filter.template,{config:t}),this._cardBuilt=new Promise((t=>this._cardBuiltResolve=t)),queueMicrotask((()=>this.build_else())),queueMicrotask((()=>this.update_all()))}connectedCallback(){var t,e;super.connectedCallback(),(null===(e=null===(t=this._config)||void 0===t?void 0:t.filter)||void 0===e?void 0:e.template)&&ut(this._config.filter.template)&&ft(this._renderer,this._config.filter.template,{config:this._config})}disconnectedCallback(){super.disconnectedCallback(),pt(this._renderer)}async update_all(){if(this.card&&(this.card.hass=this.hass),this.else&&(this.else.hass=this.hass),this._updateCooldown.timer)return void(this._updateCooldown.rerun=!0);this._updateCooldown.rerun=!1,this._updateCooldown.timer=window.setTimeout((()=>{this._updateCooldown.timer=void 0,this._updateCooldown.rerun&&this.update_all()}),500);const t=await this.update_entities();this.update_card(t)}async build_else(){if(void 0===this._config.else)return;const t=await window.loadCardHelpers();this.else=await t.createCardElement(this._config.else),this.else.hass=this.hass}async update_card(t){var e,i,n,s,o,r,a,l,d,c,h,u;if(this._entities&&_t(t,this._entities)&&_t(this._cardConfig,this._config.card))return;const v=(null===(e=this._cardConfig)||void 0===e?void 0:e.type)!==this._config.card.type;this._entities=t,this._cardConfig=JSON.parse(JSON.stringify(this._config.card));const f=Object.assign({[this._config.card_param||"entities"]:t},this._config.card);if(!this.card||v){const t=await window.loadCardHelpers();console.oldError=console.oldError||[];const e=console.error;console.oldError.push(e),console.error=(...t)=>{var i,n,s,o,r,a;3===t.length&&t[2].message&&((null===(n=(i=t[2].message).startsWith)||void 0===n?void 0:n.call(i,"Entities"))||(null===(o=(s=t[2].message).startsWith)||void 0===o?void 0:o.call(s,"Either entities"))||(null===(a=(r=t[2].message).endsWith)||void 0===a?void 0:a.call(r,"entity")))||e(...t)};try{if(this.card=await t.createCardElement(f),"hui-error-card"===this.card.localName){const t=this.card;await customElements.whenDefined("hui-error-card");let e=10;for(;!t._config&&e;)await new Promise((t=>window.setTimeout(t,100))),e--;if((null===(s=null===(n=null===(i=t._config)||void 0===i?void 0:i.error)||void 0===n?void 0:n.startsWith)||void 0===s?void 0:s.call(n,"Entities"))||(null===(a=null===(r=null===(o=t._config)||void 0===o?void 0:o.error)||void 0===r?void 0:r.startsWith)||void 0===a?void 0:a.call(r,"Either entities"))||(null===(c=null===(d=null===(l=t._config)||void 0===l?void 0:l.error)||void 0===d?void 0:d.endsWith)||void 0===c?void 0:c.call(d,"entity")))return this.card=void 0,this._entities=void 0,this._cardConfig=void 0,void(null===(h=this._cardBuiltResolve)||void 0===h||h.call(this))}}finally{console.error=console.oldError.pop()}}else this.card.setConfig(f);null===(u=this._cardBuiltResolve)||void 0===u||u.call(this),this.card.hass=this.hass,this.empty=0===t.length||t.every((t=>Wt.includes(t.type)));const p=this.empty&&!1===this._config.show_empty&&void 0===this._config.else;this.style.display=p?"none":null,this.style.margin=p?"0":null,this.card.requestUpdate&&(await this.updateComplete,this.card.requestUpdate())}async update_entities(){var t,e,i,n,s,o,r,a;const l=t=>t?"string"==typeof t?{entity:t.trim()}:t:null;let d=[...(null===(e=null===(t=this._config)||void 0===t?void 0:t.entities)||void 0===e?void 0:e.map(l))||[]];if(!this.hass)return d;if(this._template&&(d=d.concat(this._template.map(l))),d=d.filter(Boolean),null===(i=this._config.filter)||void 0===i?void 0:i.include){const t=Object.keys(this.hass.states).map(l);for(const e of this._config.filter.include){if(e.type){d.push(e);continue}let i=[];for(const n of t)await kt(this.hass,e,n.entity)&&i.push(JSON.parse(JSON.stringify(Object.assign(Object.assign({},n),e.options)).replace(/this.entity_id/g,n.entity)));if(e.sort&&(await Et(this.hass),await $t(this.hass),await yt(this.hass),i=i.sort(Ut(this.hass,e.sort)),null!==(n=e.sort.count)&&void 0!==n?n:e.sort.first)){const t=null!==(s=e.sort.first)&&void 0!==s?s:0;i=i.slice(t,t+(null!==(o=e.sort.count)&&void 0!==o?o:1/0))}d=d.concat(i)}}if(null===(r=this._config.filter)||void 0===r?void 0:r.exclude)for(const t of this._config.filter.exclude){const e=[];for(const i of d)void 0!==i.entity&&await kt(this.hass,t,i.entity)||e.push(i);d=e}if(this._config.sort&&(d=d.sort(Ut(this.hass,this._config.sort)),this._config.sort.count)){const t=null!==(a=this._config.sort.first)&&void 0!==a?a:0;d=d.slice(t,t+this._config.sort.count)}if(this._config.unique){let t=[];for(const e of d)"entity"===this._config.unique&&e.entity&&t.some((t=>t.entity===e.entity))||t.some((t=>_t(t,e)))||t.push(e);d=t}return d}async updated(t){(t.has("_template")||t.has("hass")&&this.hass)&&queueMicrotask((()=>this.update_all()))}createRenderRoot(){return this}render(){return N`${this.empty&&(!1===this._config.show_empty||this._config.else)?this.else:this.card}`}async getCardSize(){var t,e;let i=0;return await this._cardBuilt,this.card&&this.card.getCardSize&&(i=await this.card.getCardSize()),1===i&&(null===(t=this._entities)||void 0===t?void 0:t.length)&&(i=this._entities.length),0===i&&(null===(e=this._config.filter)||void 0===e?void 0:e.include)&&(i=Object.keys(this._config.filter.include).length),i||5}}t([nt()],Gt.prototype,"_config",void 0),t([nt()],Gt.prototype,"hass",void 0),t([nt()],Gt.prototype,"card",void 0),t([nt()],Gt.prototype,"else",void 0),t([nt()],Gt.prototype,"_template",void 0),t([st()],Gt.prototype,"empty",void 0),customElements.get("auto-entities")||(customElements.define("auto-entities",Gt),console.groupCollapsed(`%cAUTO-ENTITIES ${Pt} IS INSTALLED`,"color: green; font-weight: bold"),console.log("Readme:","https://github.com/thomasloven/lovelace-auto-entities"),console.groupEnd());
+ `]}}t([st()],Bt.prototype,"_config",void 0),t([nt()],Bt.prototype,"lovelace",void 0),t([nt()],Bt.prototype,"hass",void 0),t([st()],Bt.prototype,"_selectedTab",void 0),t([st()],Bt.prototype,"_cardGUIMode",void 0),t([st()],Bt.prototype,"_cardGUIModeAvailable",void 0),t([function(t,e){return(({finisher:t,descriptor:e})=>(i,n)=>{var s;if(void 0===n){const n=null!==(s=i.originalKey)&&void 0!==s?s:i.key,o=null!=e?{kind:"method",placement:"prototype",key:n,descriptor:e(i.key)}:{...i,key:n};return null!=t&&(o.finisher=function(e){t(e,n)}),o}{const s=i.constructor;void 0!==e&&Object.defineProperty(i,n,e(n)),null==t||t(s,n)}})({descriptor:i=>{const n={get(){var e,i;return null!==(i=null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t))&&void 0!==i?i:null},enumerable:!0,configurable:!0};if(e){const e="symbol"==typeof i?Symbol():"__"+i;n.get=function(){var i,n;return void 0===this[e]&&(this[e]=null!==(n=null===(i=this.renderRoot)||void 0===i?void 0:i.querySelector(t))&&void 0!==n?n:null),this[e]}}return n}})}("hui-card-element-editor")],Bt.prototype,"_cardEditorEl",void 0),customElements.define("auto-entities-editor",Bt),window.customCards=window.customCards||[],window.customCards.push({type:"auto-entities",name:"Auto Entities",preview:!1,description:"Entity Filter on Steroids. Auto Entities allows you to fill other cards with entities automatically, based on a number of attributes."}),window.queueMicrotask=window.queueMicrotask||(t=>window.setTimeout(t,1));const qt=["section","divider"];class Vt extends tt{constructor(){super(...arguments),this.empty=!1,this._updateCooldown={timer:void 0,rerun:!1},this._renderer=t=>{this._template="string"==typeof t?t.split(/[\s,]+/):t}}static getConfigElement(){return document.createElement("auto-entities-editor")}static getStubConfig(){return{card:{type:"entities"},filter:{include:[],exclude:[]}}}setConfig(t){var e,i;if(!t)throw new Error("No configuration.");if(!(null===(e=t.card)||void 0===e?void 0:e.type))throw new Error("No card type specified.");if(!t.filter&&!t.entities)throw new Error("No filters specified.");t=structuredClone(t),this._config=t,(null===(i=this._config.filter)||void 0===i?void 0:i.template)&&ut(this._config.filter.template)&&vt(this._renderer,this._config.filter.template,{config:t}),this._cardBuilt=new Promise((t=>this._cardBuiltResolve=t)),queueMicrotask((()=>this.build_else())),queueMicrotask((()=>this.update_all()))}connectedCallback(){var t,e;super.connectedCallback(),(null===(e=null===(t=this._config)||void 0===t?void 0:t.filter)||void 0===e?void 0:e.template)&&ut(this._config.filter.template)&&vt(this._renderer,this._config.filter.template,{config:this._config})}disconnectedCallback(){super.disconnectedCallback(),gt(this._renderer)}async update_all(){if(this.card&&(this.card.hass=this.hass),this.else&&(this.else.hass=this.hass),this._updateCooldown.timer)return void(this._updateCooldown.rerun=!0);this._updateCooldown.rerun=!1,this._updateCooldown.timer=window.setTimeout((()=>{this._updateCooldown.timer=void 0,this._updateCooldown.rerun&&this.update_all()}),500);const t=await this.update_entities();this.update_card(t)}async build_else(){if(void 0===this._config.else)return;const t=await window.loadCardHelpers();this.else=await t.createCardElement(this._config.else),this.else.hass=this.hass}async update_card(t){var e,i,n,s,o,r,a,l,c,d,h,u;if(this._entities&&_t(t,this._entities)&&_t(this._cardConfig,this._config.card))return;const p=(null===(e=this._cardConfig)||void 0===e?void 0:e.type)!==this._config.card.type;this._entities=t,this._cardConfig=structuredClone(this._config.card);const v={[this._config.card_param||"entities"]:t,...this._config.card};if(!this.card||p){const t=await window.loadCardHelpers();console.oldError=console.oldError||[];const e=console.error;console.oldError.push(e),console.error=(...t)=>{var i,n,s,o,r,a;3===t.length&&t[2].message&&((null===(n=(i=t[2].message).startsWith)||void 0===n?void 0:n.call(i,"Entities"))||(null===(o=(s=t[2].message).startsWith)||void 0===o?void 0:o.call(s,"Either entities"))||(null===(a=(r=t[2].message).endsWith)||void 0===a?void 0:a.call(r,"entity")))||e(...t)};try{if(this.card=await t.createCardElement(v),"hui-error-card"===this.card.localName){const t=this.card;await customElements.whenDefined("hui-error-card");let e=10;for(;!t._config&&e;)await new Promise((t=>window.setTimeout(t,100))),e--;if((null===(s=null===(n=null===(i=t._config)||void 0===i?void 0:i.error)||void 0===n?void 0:n.startsWith)||void 0===s?void 0:s.call(n,"Entities"))||(null===(a=null===(r=null===(o=t._config)||void 0===o?void 0:o.error)||void 0===r?void 0:r.startsWith)||void 0===a?void 0:a.call(r,"Either entities"))||(null===(d=null===(c=null===(l=t._config)||void 0===l?void 0:l.error)||void 0===c?void 0:c.endsWith)||void 0===d?void 0:d.call(c,"entity")))return this.card=void 0,this._entities=void 0,this._cardConfig=void 0,void(null===(h=this._cardBuiltResolve)||void 0===h||h.call(this))}}finally{console.error=console.oldError.pop()}}else this.card.setConfig(v);null===(u=this._cardBuiltResolve)||void 0===u||u.call(this),this.card.hass=this.hass,this.empty=0===t.length||t.every((t=>qt.includes(t.type)));const g=this.empty&&!1===this._config.show_empty&&void 0===this._config.else;this.style.display=g?"none":null,this.style.margin=g?"0":null,this.card.requestUpdate&&(await this.updateComplete,this.card.requestUpdate())}async update_entities(){var t,e,i,n,s,o,r,a,l,c;const d=t=>t?"string"==typeof t?{entity:t.trim()}:t:null;let[h]=await Promise.all([this._config.sort?Rt(this.hass,this._config.sort):Promise.resolve((t=>Promise.resolve(t)))]);const u=(await Promise.all((null!==(e=null===(t=this._config.filter)||void 0===t?void 0:t.include)&&void 0!==e?e:[]).map((async t=>{if(t.type)return async()=>[t];const e=await Ut(this.hass,t),i=t.sort?await Rt(this.hass,t.sort):t=>t;return async n=>{var s,o,r,a,l;let c=n.filter((t=>e(t.entity)));if(c=await i(c),null!==(o=null===(s=t.sort)||void 0===s?void 0:s.count)&&void 0!==o?o:null===(r=t.sort)||void 0===r?void 0:r.first){const e=null!==(a=t.sort.first)&&void 0!==a?a:0;c=c.slice(e,e+(null!==(l=t.sort.count)&&void 0!==l?l:1/0))}const d=t.options?JSON.stringify(t.options):"{}";return c=c.map((t=>structuredClone({...t,...JSON.parse(d.replace(/this.entity_id/g,t.entity))}))),c}})))).flat(),p=await Promise.all((null!==(n=null===(i=this._config.filter)||void 0===i?void 0:i.exclude)&&void 0!==n?n:[]).map((async t=>{const e=await Ut(this.hass,t);return t=>e(t)})));let v=[...(null===(o=null===(s=this._config)||void 0===s?void 0:s.entities)||void 0===o?void 0:o.map(d))||[]];if(!this.hass)return v;v=v.concat(null!==(l=null===(a=null===(r=this._template)||void 0===r?void 0:r.map)||void 0===a?void 0:a.call(r,d))&&void 0!==l?l:[]),v=v.filter(Boolean);const g=Object.keys(this.hass.states).map(d);if(v=v.concat((await Promise.all(u.map((t=>t(g))))).flat()),v=v.filter((t=>void 0===t.entity||!(t=>p.some((e=>e(t))))(t.entity))),this._config.sort&&(v=await h(v),this._config.sort.count)){const t=null!==(c=this._config.sort.first)&&void 0!==c?c:0;v=v.slice(t,t+this._config.sort.count)}if(this._config.unique){let t=[];for(const e of v)"entity"===this._config.unique&&e.entity&&t.some((t=>t.entity===e.entity))||t.some((t=>_t(t,e)))||t.push(e);v=t}return v}async updated(t){(t.has("_template")||t.has("hass")&&this.hass)&&queueMicrotask((()=>this.update_all()))}createRenderRoot(){return this}render(){return I`${this.empty&&(!1===this._config.show_empty||this._config.else)?this.else:this.card}`}async getCardSize(){var t,e;let i=0;return await this._cardBuilt,this.card&&this.card.getCardSize&&(i=await this.card.getCardSize()),1===i&&(null===(t=this._entities)||void 0===t?void 0:t.length)&&(i=this._entities.length),0===i&&(null===(e=this._config.filter)||void 0===e?void 0:e.include)&&(i=Object.keys(this._config.filter.include).length),i||5}}t([nt()],Vt.prototype,"_config",void 0),t([nt()],Vt.prototype,"hass",void 0),t([nt()],Vt.prototype,"card",void 0),t([nt()],Vt.prototype,"else",void 0),t([nt()],Vt.prototype,"_template",void 0),t([st()],Vt.prototype,"empty",void 0),customElements.get("auto-entities")||(customElements.define("auto-entities",Vt),console.groupCollapsed(`%cAUTO-ENTITIES ${Ht} IS INSTALLED`,"color: green; font-weight: bold"),console.log("Readme:","https://github.com/thomasloven/lovelace-auto-entities"),console.groupEnd());
diff --git a/package-lock.json b/package-lock.json
index e647605..2a355ec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-terser": "^0.2.1",
+ "prettier": "3.4.2",
"rollup": "^3.8.1",
"rollup-plugin-typescript2": "^0.34.1",
"tslib": "^2.4.1",
@@ -1183,6 +1184,22 @@
"node": ">=8"
}
},
+ "node_modules/prettier": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
+ "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
diff --git a/package.json b/package.json
index 652cfec..96cfb3a 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-terser": "^0.2.1",
+ "prettier": "3.4.2",
"rollup": "^3.8.1",
"rollup-plugin-typescript2": "^0.34.1",
"tslib": "^2.4.1",
diff --git a/rollup.config.js b/rollup.config.js
index b067bae..84b9855 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -15,9 +15,12 @@ export default {
plugins: [
nodeResolve(),
json(),
- typescript(),
+ typescript({
+ clean: true,
+ }),
babel({
exclude: "node_modules/**",
+ babelHelpers: "bundled",
}),
!dev && terser({ format: { comments: false } }),
],
diff --git a/src/editor/auto-entities-editor.ts b/src/editor/auto-entities-editor.ts
index 0eea2df..5a0c6ae 100644
--- a/src/editor/auto-entities-editor.ts
+++ b/src/editor/auto-entities-editor.ts
@@ -1,28 +1,35 @@
-import { LitElement, html, CSSResultArray, css } from "lit";
-import { property, state, query } from "lit/decorators.js";
+import { css, CSSResultArray, html, LitElement } from "lit";
+import { property, query, state } from "lit/decorators.js";
import { AutoEntitiesConfig } from "../types";
import { loadHaForm } from "../helpers";
import {
- filterGroupSchema,
- filterGroupOptionsSchema,
+ cardOptionsSchema,
filter2form,
+ filterGroupOptionsSchema,
+ filterGroupSchema,
form2filter,
- specialGroupSchema,
- cardOptionsSchema,
sortSchema,
+ specialGroupSchema,
} from "./schema";
class AutoEntitiesEditor extends LitElement {
- @state() _config: AutoEntitiesConfig;
+ @state()
+ _config: AutoEntitiesConfig;
- @property() lovelace;
- @property() hass;
+ @property()
+ lovelace;
+ @property()
+ hass;
- @state() _selectedTab = 0;
- @state() _cardGUIMode = true;
- @state() _cardGUIModeAvailable = true;
+ @state()
+ _selectedTab = 0;
+ @state()
+ _cardGUIMode = true;
+ @state()
+ _cardGUIModeAvailable = true;
- @query("hui-card-element-editor") private _cardEditorEl?;
+ @query("hui-card-element-editor")
+ private _cardEditorEl?;
setConfig(config) {
this._config = config;
@@ -258,7 +265,7 @@ class AutoEntitiesEditor extends LitElement {
}
_renderFilterEditor() {
- if (this._config.filter?.template || this._config.entities)
+ if (this._config.filter?.template || this._config.entities) {
return html`
@@ -267,6 +274,7 @@ class AutoEntitiesEditor extends LitElement {
Please switch to the CODE EDITOR to access all options.
`;
+ }
return html`
${this._config.filter.include.map(
diff --git a/src/editor/schema.ts b/src/editor/schema.ts
index 0015e99..ef42cec 100644
--- a/src/editor/schema.ts
+++ b/src/editor/schema.ts
@@ -47,12 +47,13 @@ const filterSchema = ([key, value], idx) => {
attributes: { object: {} },
};
- if (!GUI_EDITOR_FILTERS.includes(key))
+ if (!GUI_EDITOR_FILTERS.includes(key)) {
return {
type: "Constant",
name: "Some filters are not shown",
value: "Please switch to the CODE EDITOR to access all options.",
};
+ }
return {
type: "grid",
@@ -100,8 +101,9 @@ export const filter2form = (group) => {
export const form2filter = (config, filter): Object => {
const data = {};
for (let i = 0; i <= config.filter.include.length + 1; i++) {
- if (filter[`key_${i}`] !== undefined)
+ if (filter[`key_${i}`] !== undefined) {
data[filter[`key_${i}`]] = filter[`value_${i}`] ?? "";
+ }
}
if (filter.key_new !== undefined) {
data[filter.key_new] = "";
diff --git a/src/filter.ts b/src/filter.ts
index d7c357f..b246e4c 100644
--- a/src/filter.ts
+++ b/src/filter.ts
@@ -1,15 +1,30 @@
-import { HAState, HassObject } from "./types";
-import { getAreas, getDevices, getEntities, getLabels } from "./helpers";
+import { HassObject, HAState } from "./types";
+import {
+ getAreas,
+ getDevices,
+ getEntities,
+ getEntityAreas,
+ getLabels,
+} from "./helpers";
const ago_suffix_regex = /([mhd])\s+ago\s*$/i;
const default_ago_suffix = "m ago";
-function match(pattern: any, value: any) {
- if (typeof pattern === "string" && pattern.startsWith("$$")) {
- pattern = pattern.substring(2);
- value = JSON.stringify(value);
+async function match(pattern: any): Promise<(value: any) => boolean> {
+ let valueTransformers: Array<(x: any) => unknown> = [];
+ function transformValue(value) {
+ return valueTransformers.reduce((a, x) => x(a), value);
}
- if (typeof value === "string" && typeof pattern === "string") {
+
+ let predicates: Array<[(value: any) => boolean, (value: any) => boolean]> =
+ [];
+
+ if (typeof pattern === "string") {
+ if (pattern.startsWith("$$")) {
+ pattern = pattern.substring(2);
+ valueTransformers.push(JSON.stringify);
+ }
+
if (
(pattern.startsWith("/") && pattern.endsWith("/")) ||
pattern.indexOf("*") !== -1
@@ -19,208 +34,283 @@ function match(pattern: any, value: any) {
pattern = pattern.replace(/\./g, ".").replace(/\*/g, ".*");
pattern = `/^${pattern}$/`;
}
- let regex = new RegExp(pattern.slice(1, -1));
- return regex.test(value);
+ const regex = new RegExp(pattern.slice(1, -1));
+ predicates.push([
+ (value) => typeof value === "string",
+ (value) => regex.test(value),
+ ]);
}
- }
- if (typeof pattern === "string") {
const match = ago_suffix_regex.exec(pattern);
if (match) {
pattern = pattern.replace(match[0], "");
const now = new Date().getTime();
- const updated = new Date(value).getTime();
- value = (now - updated) / 60000;
- const period = match[1];
- if (period === "h") {
- value = value / 60;
- } else if (period === "d") {
- value = value / 60 / 24;
- }
+ valueTransformers.push((value) => {
+ const updated = new Date(value).getTime();
+ value = (now - updated) / 60000;
+ const period = match[1];
+ if (period === "h") {
+ value = value / 60;
+ } else if (period === "d") {
+ value = value / 60 / 24;
+ }
+ });
}
- }
- if (typeof pattern === "string") {
// Comparisons assume numerical values
- if (pattern.startsWith("<="))
- return parseFloat(value) <= parseFloat(pattern.substring(2));
- if (pattern.startsWith(">="))
- return parseFloat(value) >= parseFloat(pattern.substring(2));
- if (pattern.startsWith("<"))
- return parseFloat(value) < parseFloat(pattern.substring(1));
- if (pattern.startsWith(">"))
- return parseFloat(value) > parseFloat(pattern.substring(1));
- if (pattern.startsWith("!"))
- return parseFloat(value) != parseFloat(pattern.substring(1));
- if (pattern.startsWith("="))
- return parseFloat(value) == parseFloat(pattern.substring(1));
+ if (pattern.startsWith("<=")) {
+ const parameter = parseFloat(pattern.substring(2));
+ predicates.push([() => true, (value) => parseFloat(value) <= parameter]);
+ }
+ if (pattern.startsWith(">=")) {
+ const parameter = parseFloat(pattern.substring(2));
+ predicates.push([() => true, (value) => parseFloat(value) >= parameter]);
+ }
+ if (pattern.startsWith("<")) {
+ const parameter = parseFloat(pattern.substring(1));
+ predicates.push([() => true, (value) => parseFloat(value) < parameter]);
+ }
+ if (pattern.startsWith(">")) {
+ const parameter = parseFloat(pattern.substring(1));
+ predicates.push([() => true, (value) => parseFloat(value) > parameter]);
+ }
+ if (pattern.startsWith("!")) {
+ const parameter = parseFloat(pattern.substring(1));
+ predicates.push([() => true, (value) => parseFloat(value) != parameter]);
+ }
+ if (pattern.startsWith("=")) {
+ const parameter = parseFloat(pattern.substring(1));
+ predicates.push([() => true, (value) => parseFloat(value) == parameter]);
+ }
}
- return pattern === value;
+ predicates.push([() => true, (value) => pattern === value]);
+
+ return (value) => {
+ const transformedValue = transformValue(value);
+ const predicate = predicates.find(([valid, predicate]) =>
+ valid(transformedValue)
+ )[1];
+ return predicate(transformedValue);
+ };
}
const FILTERS: Record<
string,
- (hass: HassObject, value: any, entity: HAState) => Promise
+ (hass: HassObject, value: any) => Promise<(entity: HAState) => boolean>
> = {
- options: async () => true,
- sort: async () => true,
- domain: async (hass, value, entity) => {
- return match(value, entity.entity_id.split(".")[0]);
+ options: async (hass, value) => (entity) => true,
+ sort: async (hass, value) => (entity) => true,
+ domain: async (hass, value) => {
+ const [matcher] = await Promise.all([match(value)]);
+ return (entity) => matcher(entity.entity_id.split(".")[0]);
},
- entity_id: async (hass, value, entity) => {
- return match(value, entity.entity_id);
+ entity_id: async (hass, value) => {
+ const [matcher] = await Promise.all([match(value)]);
+ return (entity) => matcher(entity.entity_id);
},
- state: async (hass, value, entity) => {
- return match(value, entity.state);
+ state: async (hass, value) => {
+ const [matcher] = await Promise.all([match(value)]);
+ return (entity) => matcher(entity.state);
},
- name: async (hass, value, entity) => {
- return match(value, entity.attributes?.friendly_name);
+ name: async (hass, value) => {
+ const [matcher] = await Promise.all([match(value)]);
+ return (entity) => matcher(entity.attributes?.friendly_name);
},
- group: async (hass, value, entity) => {
+ group: async (hass, value) => (entity) => {
return hass.states[value]?.attributes?.entity_id?.includes(
entity.entity_id
);
},
- attributes: async (hass, value, entity) => {
- for (const [k, v] of Object.entries(value as Record)) {
- let attr = k.split(" ")[0]; // Remove any suffixes
- let obj = entity.attributes;
- for (const step of attr.split(":")) {
- obj = obj ? obj[step] : undefined;
- }
- if (obj === undefined || !match(v, obj)) return false;
- }
- return true;
+ attributes: async (hass, value: Record) => {
+ let filters = (
+ await Promise.all(
+ Object.entries(value).map(async ([k, v]) => {
+ const attr = k.split(" ")[0]; // Remove any suffixes
+ const steps = attr.split(":");
+ const stepper = (obj) => steps.reduce((a, x) => a?.[x], obj);
+ const matcher = await match(v);
+ return { stepper, matcher };
+ })
+ )
+ ).map(({ stepper, matcher }) => (entity) => {
+ const obj = stepper(entity.attributes);
+ return obj !== undefined && matcher(obj);
+ });
+ return (entity) => !filters.map((filter) => filter(entity)).includes(false);
},
- not: async (hass, value, entity) => {
- return !(await filter_entity(hass, value, entity.entity_id));
+ not: async (hass, value) => {
+ const [filter] = await Promise.all([filter_entity(hass, value)]);
+ return (entity) => !filter(entity.entity_id);
},
- and: async (hass, value, entity) => {
- for (const v of value) {
- if (!(await filter_entity(hass, v, entity.entity_id))) return false;
- }
- return true;
+ and: async (hass, value) => {
+ const filters = await Promise.all(value.map((v) => filter_entity(hass, v)));
+ return (entity) => !filters.some((filter) => !filter(entity));
},
- or: async (hass, value, entity) => {
- for (const v of value) {
- if (await filter_entity(hass, v, entity.entity_id)) return true;
- }
- return false;
+ or: async (hass, value) => {
+ const filters = await Promise.all(value.map((v) => filter_entity(hass, v)));
+ return (entity) => filters.some((filter) => filter(entity));
},
- device: async (hass, value, entity) => {
- const ent = (await getEntities(hass)).find(
- (e) => e.entity_id === entity.entity_id
- );
- if (!ent) return false;
- const device = (await getDevices(hass)).find((d) => d.id === ent.device_id);
- if (!device) return false;
- return match(value, device.name_by_user) || match(value, device.name);
- },
- device_manufacturer: async (hass, value, entity) => {
- const ent = (await getEntities(hass)).find(
- (e) => e.entity_id === entity.entity_id
- );
- if (!ent) return false;
- const device = (await getDevices(hass)).find((d) => d.id === ent.device_id);
- if (!device) return false;
- return match(value, device.manufacturer);
- },
- device_model: async (hass, value, entity) => {
- const ent = (await getEntities(hass)).find(
- (e) => e.entity_id === entity.entity_id
- );
- if (!ent) return false;
- const device = (await getDevices(hass)).find((d) => d.id === ent.device_id);
- if (!device) return false;
- return match(value, device.model);
- },
- area: async (hass, value, entity) => {
- const ent = (await getEntities(hass)).find(
- (e) => e.entity_id === entity.entity_id
- );
- if (!ent) return false;
- let area = (await getAreas(hass)).find((a) => a.area_id === ent.area_id);
- if (area) return match(value, area.name) || match(value, area.area_id);
- const device = (await getDevices(hass)).find((d) => d.id === ent.device_id);
- if (!device) return false;
- area = (await getAreas(hass)).find((a) => a.area_id === device.area_id);
- if (!area) return false;
- return match(value, area.name) || match(value, area.area_id);
- },
- entity_category: async (hass, value, entity) => {
- const ent = (await getEntities(hass)).find(
- (e) => e.entity_id === entity.entity_id
- );
- if (!ent) return false;
- return match(value, ent.entity_category);
+ device: async (hass, value) => {
+ const [entities, devices, matcher] = await Promise.all([
+ getEntities(hass),
+ getDevices(hass),
+ match(value),
+ ]);
+ return (entity) => {
+ const ent = entities.get(entity.entity_id);
+ if (!ent) return false;
+ const device = devices.get(ent.device_id);
+ if (!device) return false;
+ return matcher(device.name_by_user) || matcher(device.name);
+ };
+ },
+ device_manufacturer: async (hass, value) => {
+ const [entities, devices, matcher] = await Promise.all([
+ getEntities(hass),
+ getDevices(hass),
+ match(value),
+ ]);
+ return (entity) => {
+ const ent = entities.get(entity.entity_id);
+ if (!ent) return false;
+ const device = devices.get(ent.device_id);
+ if (!device) return false;
+ return matcher(device.manufacturer);
+ };
+ },
+ device_model: async (hass, value) => {
+ const [entities, devices, matcher] = await Promise.all([
+ getEntities(hass),
+ getDevices(hass),
+ match(value),
+ ]);
+ return (entity) => {
+ const ent = entities.get(entity.entity_id);
+ if (!ent) return false;
+ const device = devices.get(ent.device_id);
+ if (!device) return false;
+ return matcher(device.model);
+ };
+ },
+ area: async (hass, value) => {
+ const [entityAreas, areas, matcher] = await Promise.all([
+ getEntityAreas(hass),
+ getAreas(hass),
+ match(value),
+ ]);
+ return (entity) => {
+ const area = areas.get(entityAreas.get(entity.entity_id));
+ if (!area) return false;
+ return (
+ (area.name !== undefined && matcher(area.name)) ||
+ (area.area_id !== undefined && matcher(area.area_id))
+ );
+ };
+ },
+ entity_category: async (hass, value) => {
+ const [entities, matcher] = await Promise.all([
+ getEntities(hass),
+ match(value),
+ ]);
+ return (entity) => {
+ const ent = entities.get(entity.entity_id);
+ if (!ent) return false;
+ return matcher(ent.entity_category);
+ };
},
- last_changed: async (hass, value, entity) => {
- if (!ago_suffix_regex.test(value)) value = value + default_ago_suffix;
+ last_changed: async (hass, value) => {
+ const [matcher] = await Promise.all([match(value)]);
+ return (entity) => {
+ if (!ago_suffix_regex.test(value)) value = value + default_ago_suffix;
- return match(value, entity.last_changed);
+ return matcher(entity.last_changed);
+ };
},
- last_updated: async (hass, value, entity) => {
- if (!ago_suffix_regex.test(value)) value = value + default_ago_suffix;
+ last_updated: async (hass, value) => {
+ const [matcher] = await Promise.all([match(value)]);
+ return (entity) => {
+ if (!ago_suffix_regex.test(value)) value = value + default_ago_suffix;
- return match(value, entity.last_updated);
+ return matcher(entity.last_updated);
+ };
},
- last_triggered: async (hass, value, entity) => {
- if (entity.attributes.last_triggered == null) return false;
- if (!ago_suffix_regex.test(value)) value = value + default_ago_suffix;
+ last_triggered: async (hass, value) => {
+ const [matcher] = await Promise.all([match(value)]);
+ return (entity) => {
+ if (entity.attributes.last_triggered == null) return false;
+ if (!ago_suffix_regex.test(value)) value = value + default_ago_suffix;
- return match(value, entity.attributes.last_triggered);
+ return matcher(entity.attributes.last_triggered);
+ };
},
- integration: async (hass, value, entity) => {
- const ent = (await getEntities(hass)).find(
- (e) => e.entity_id === entity.entity_id
- );
- if (!ent) return false;
- return match(value, ent.platform);
+ integration: async (hass, value) => {
+ const [entities, matcher] = await Promise.all([
+ getEntities(hass),
+ match(value),
+ ]);
+ return (entity) => {
+ const ent = entities.get(entity.entity_id);
+ if (!ent) return false;
+ return matcher(ent.platform);
+ };
},
- hidden_by: async (hass, value, entity) => {
- const ent = (await getEntities(hass)).find(
- (e) => e.entity_id === entity.entity_id
- );
- if (!ent) return false;
- return match(value, ent.hidden_by);
+ hidden_by: async (hass, value) => {
+ const [entities, matcher] = await Promise.all([
+ getEntities(hass),
+ match(value),
+ ]);
+ return (entity) => {
+ const ent = entities.get(entity.entity_id);
+ if (!ent) return false;
+ return matcher(ent.hidden_by);
+ };
},
- label: async (hass, value, entity) => {
- const ent = (await getEntities(hass)).find(
- (e) => e.entity_id === entity.entity_id
- );
- const labels = await getLabels(hass);
-
- const match_label = (value, lbl) => {
- if (match(value, lbl)) return true;
- const label = labels.find((l) => l.label_id === lbl);
+ label: async (hass, value) => {
+ const [entities, devices, labels, matcher] = await Promise.all([
+ getEntities(hass),
+ getDevices(hass),
+ getLabels(hass),
+ match(value),
+ ]);
+ const match_label = (lbl) => {
+ if (matcher(lbl)) return true;
+ const label = labels.get(lbl);
if (!label) return false;
- const result = match(value, label.name);
+ const result = matcher(label.name);
return result;
};
+ return (entity) => {
+ const ent = entities.get(entity.entity_id);
- if (!ent) return false;
- if (!ent.labels) return false;
- const entity_match = ent.labels.some((lbl) => match_label(value, lbl));
- if (entity_match) return entity_match;
+ if (!ent) return false;
+ if (!ent.labels) return false;
+ const entity_match = ent.labels.some((lbl) => match_label(lbl));
+ if (entity_match) return entity_match;
- const device = (await getDevices(hass)).find((d) => d.id === ent.device_id);
- if (!device) return false;
- const device_match = device.labels.some((lbl) => match_label(value, lbl));
- return device_match;
+ const device = devices.get(ent.device_id);
+ if (!device) return false;
+ const device_match = device.labels.some((lbl) => match_label(lbl));
+ return device_match;
+ };
},
};
export async function filter_entity(
hass: HassObject,
- filter: Record,
- entity_id: string
-): Promise {
- if (!hass.states[entity_id]) return false;
- for (let [k, v] of Object.entries(filter)) {
- k = k.trim().split(" ")[0].trim();
- if (!(await FILTERS[k]?.(hass, v, hass.states[entity_id]))) return false;
- }
- return true;
+ filter: Record
+): Promise<(entity_id: string) => boolean> {
+ const filters = (
+ await Promise.all(
+ Object.entries(filter).map(([k, v]) =>
+ FILTERS[k.trim().split(" ")[0].trim()]?.(hass, v)
+ )
+ )
+ ).filter(Boolean);
+ return (entity_id: string) => {
+ const entity = hass?.states?.[entity_id];
+ if (!entity) return false;
+ return !filters.some((filter) => !filter(entity));
+ };
}
diff --git a/src/helpers.ts b/src/helpers.ts
index b93c91e..3517af0 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -1,3 +1,5 @@
+import type { HassObject } from "./types";
+
export const loadHaForm = async () => {
if (customElements.get("ha-form")) return;
@@ -26,43 +28,136 @@ export const compare_deep = (a: any, b: any) => {
return true;
};
-(window as any).autoEntities_cache = (window as any).autoEntities_cache ?? {};
-const cache = (window as any).autoEntities_cache;
-export async function getAreas(hass) {
- cache.areas =
- cache.areas ?? (await hass.callWS({ type: "config/area_registry/list" }));
- return cache.areas;
-}
-export function cached_areas() {
- return cache.areas;
-}
-export async function getDevices(hass) {
- cache.devices =
- cache.devices ??
- (await hass.callWS({ type: "config/device_registry/list" }));
- return cache.devices;
-}
-export function cached_devices() {
- return cache.devices;
-}
-export async function getEntities(hass) {
- cache.entities =
- cache.entities ??
- (await hass.callWS({ type: "config/entity_registry/list" }));
- return cache.entities;
-}
-export function cached_entities() {
- return cache.entities;
+function makeCache Promise>(
+ name: string,
+ loader: T
+): T extends (...args: infer R) => Promise
+ ? {
+ get_cached: () => Promise;
+ get: (...args: R) => Promise;
+ }
+ : never;
+function makeCache Promise>(
+ name: string,
+ loader: T
+): {
+ get_cached: () => Promise>>;
+ get: (...args: Parameters) => Promise>>;
+} {
+ function refresh() {
+ let loading = false;
+ let resolve, reject;
+ const promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ const load = (loader: () => Promise) => {
+ if (!loading) {
+ loading = true;
+ try {
+ loader().then(resolve, reject);
+ } catch (e) {
+ loading = false;
+ if (e !== undefined) throw e;
+ }
+ }
+ return promise;
+ };
+ return { promise, resolve, reject, load };
+ }
+ let cache = refresh>>();
+ async function get_cached(): Promise>> {
+ return await cache.promise;
+ }
+ async function get(...args: Parameters): Promise>> {
+ try {
+ return await cache.load(
+ () => loader(...args) as Promise>>
+ );
+ } catch (e) {
+ if (e !== undefined) console.warn(`${name} failed:`, e);
+ cache = refresh();
+ throw e;
+ }
+ }
+ return {
+ get_cached,
+ get,
+ };
}
-export async function getLabels(hass) {
- cache.labels =
- cache.labels ?? (await hass.callWS({ type: "config/label_registry/list" }));
- return cache.labels;
+
+function hassListLoader>(
+ key: keyof T,
+ type: string
+): (hass: HassObject) => Promise