diff --git a/README.md b/README.md index 0b388ce..c31f0d0 100644 --- a/README.md +++ b/README.md @@ -125,11 +125,12 @@ elements: type: icon icon: "${vars[0] === 'on' ? 'mdi:home' : 'mdi:circle'}" style: - '--paper-item-icon-color': '${ states[''sensor.light_icon_color''].state }' + '--paper-item-icon-color': "${ states['sensor.light_icon_color'].state }" style: top: 47% left: 75% ``` + The `style` object on the element configuration is applied to the element itself, the `style` object on the `config-template-card` is applied to the surrounding card. Both can contain templated values. For example, in order to place the card properly, the `top` and `left` attributes must always be configured on the `config-template-card`. ### Entities card example @@ -196,7 +197,35 @@ card: entities: - entity: climate.ecobee name: '${ setTempMessage("House: ", currentTemp) }' -```` +``` + +### Asynchronous functions + +Asynchronous functions can be used in most templates. + +```yaml +type: 'custom:config-template-card' +entities: + - light.bed_light + - light.porch_light +card: + type: entities + entities: + - entity: light.bed_light + name: "${(async () => states['light.bed_light'].state === 'on' ? 'Bed Light On' : 'Bed Light Off' )();}" + - entity: light.porch_light + name: "${(async () => { return states['light.bed_light'].state === 'on' ? 'Porch Light On' : 'Porch Light Off'; })();}" +``` + +Card rendering will be delayed until all asynchronous functions (in all templates) have completed, so long-running asynchronous functions may prevent the card from rendering on page load or delay updates to the card. + +When defining `staticVariables` that reference other (previously defined) `svars`, any referenced `svars` that use asynchronous functions will be `null`. + +Similarly, when defining `variables` that reference other (previously defined) `vars`, any referenced `vars` that use asynchronous functions will be `null`. However, any referenced `svars` that use asynchronous functions will have complete/settled values. + +When defining `entities` that use templates, any `entities` templates that use asynchronous functions will be ignored, and any referenced `vars` that use asynchronous functions will be `null`. However, any referenced `svars` that use asynchronous functions will have complete/settled values. + +(The reason that asynchronous functions cannot be used for `entities` is that Lit does not support asynchronous functions in `shouldUpdate()` where `entities` is evaluated and used. The alternative [lovelace-card-templater](https://github.com/gadgetchnnel/lovelace-card-templater) cannot support templates in its `entities` for the same reason; It uses API calls to render templates, and API calls require the use of asynchronous functions.) ### Dashboard wide variables diff --git a/src/config-template-card.ts b/src/config-template-card.ts index ca54fe0..f789216 100644 --- a/src/config-template-card.ts +++ b/src/config-template-card.ts @@ -1,10 +1,11 @@ import { LitElement, html, TemplateResult, PropertyValues } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; +import { until } from 'lit/directives/until.js'; import { computeCardSize, HomeAssistant, LovelaceCard } from 'custom-card-helpers'; import { Config, SVarMgr, VarMgr, Vars, ObjMap } from './types'; import { VERSION } from './version'; -import { assertNotNull, isString } from './util'; +import { assertNotNull, isString, isPromise, somePromise } from './util'; console.info( `%c CONFIG-TEMPLATE-CARD \n%c Version ${VERSION} `, @@ -130,9 +131,19 @@ export class ConfigTemplateCard extends LitElement { // _initSVars() requires hass and _config if (!this.hass || !this._config) { return false; } - // shouldUpdate() requires _svarMgr + // shouldUpdate() requires _svarMgr to be settled if (!this._svarMgr) { this._svarMgr = this._evaluateVars(true); + if (this._svarMgr._svarsPromise) { + void this._svarMgr._svarsPromise.then((v) => { + // Explicitly trigger an update after svars has settled + this.requestUpdate(); + return v; + }); + return false; + } + } else { + if (this._svarMgr._svarsPromise) { return false; } } // render() requires hass, _config, and _helpers @@ -154,7 +165,7 @@ export class ConfigTemplateCard extends LitElement { const varMgr = this._evaluateVars(false); // Cache the evaluated variables to avoid requiring render() to evaluate them again this._tmpVarMgr = varMgr; - const entities = this._evaluateStructure(varMgr, this._config.entities); + const entities = this._evaluateStructure(varMgr, this._config.entities, 'immediate'); for (const entity of entities) { if (this.hass && oldHass.states[entity] !== this.hass.states[entity]) { return true; @@ -174,10 +185,16 @@ export class ConfigTemplateCard extends LitElement { this._tmpVarMgr = undefined; if (!varMgr) { varMgr = this._evaluateVars(false); } // Shouldn't happen + return html`${until(this._getCardElement(varMgr))}`; + } + + private async _getCardElement(varMgr: VarMgr): Promise { assertNotNull(this._config); // TypeScript can't detect the gate in _initialize() + if (varMgr._varsPromise) { await varMgr._varsPromise; } + let configSection = (this._config.card ?? this._config.row ?? this._config.element); - configSection = this._evaluateStructure(varMgr, configSection); + configSection = await this._evaluateStructure(varMgr, configSection); const element = this._config.card ? this._helpers.createCardElement(configSection) @@ -189,7 +206,7 @@ export class ConfigTemplateCard extends LitElement { if (this._config.element) { if (this._config.style) { let style = this._config.style; - style = this._evaluateStructure(varMgr, style); + style = await this._evaluateStructure(varMgr, style); Object.keys(style).forEach((prop) => { this.style.setProperty(prop, style[prop]); }); @@ -245,28 +262,34 @@ export class ConfigTemplateCard extends LitElement { svars: this._svarMgr?.svars ?? [], _evalInitSVars: this._svarMgr?._evalInitSVars ?? '', vars: [], _evalInitVars: '', }; - const vars: Vars = (doStatic ? varMgr.svars : varMgr.vars); + const immediateVars: Vars = (doStatic ? varMgr.svars : varMgr.vars); const initKey = (doStatic ? '_evalInitSVars' : '_evalInitVars'); const initRef = (doStatic ? 'svars' : 'vars'); + const arrayVars2: any[] = []; for (let v of arrayVars) { if (isString(v)) { v = this._evaluateTemplate(varMgr, v, true); - vars.push(v); + immediateVars.push(isPromise(v) ? null : v); + arrayVars2.push(v); } else { - v = this._evaluateStructure(varMgr, v); - vars.push(v); + v = this._evaluateStructure(varMgr, v, 'both'); + immediateVars.push(v.immediate); + arrayVars2.push(v.promise); } } + const namedVars2: ObjMap = {}; for (const varName in namedVars) { let v = namedVars[varName]; if (isString(v)) { v = this._evaluateTemplate(varMgr, v, true); - vars[varName] = v; + immediateVars[varName] = (isPromise(v) ? null : v); + namedVars2[varName] = v; } else { - v = this._evaluateStructure(varMgr, v); - vars[varName] = v; + v = this._evaluateStructure(varMgr, v, 'both'); + immediateVars[varName] = v.immediate; + namedVars2[varName] = v.promise; } // Note that if `staticVariables` and `variables` both contain a variable with the same name // then `_evalInitSVars + _evalInitVars` will end up defining the variable twice. This @@ -276,35 +299,108 @@ export class ConfigTemplateCard extends LitElement { varMgr[initKey] += `var ${varName} = ${initRef}['${varName}'];\n`; } + let promiseArrayVars: Promise | undefined; + if (somePromise(arrayVars2)) { + promiseArrayVars = Promise.all(arrayVars2.map((v) => Promise.resolve(v))); + } + let promiseNamedVars: Promise | undefined; + if (somePromise(Object.entries(namedVars2).map(([_k, v], _i) => v))) { + promiseNamedVars = Promise.all(Object.entries(namedVars2).map(([k, v], _i) => + Promise.resolve(v).then((v) => [k, v]) + )).then((a) => Object.fromEntries(a)); + } + let promise: Promise | undefined; + if (isPromise(promiseArrayVars) || isPromise(promiseNamedVars)) { + promise = Promise.all([promiseArrayVars, promiseNamedVars].map((v) => Promise.resolve(v))) + .then(([pav, pnv]) => Object.assign(pav as any[], pnv as ObjMap)); + } + if (doStatic) { const svarMgr: SVarMgr = { - svars: vars, _evalInitSVars: varMgr._evalInitSVars, + svars: immediateVars, _evalInitSVars: varMgr._evalInitSVars, }; + if (promise) { + svarMgr._svarsPromise = promise.then((v) => { + svarMgr.svars = v; + svarMgr._svarsPromise = undefined; + return v; + }); + } return svarMgr; } else { + if (promise) { + varMgr._varsPromise = promise.then((v) => { + varMgr.vars = v; + varMgr._varsPromise = undefined; + return v; + }); + } return varMgr; } } - private _evaluateStructure(varMgr: VarMgr, struct: any): any { + // Return value is based on `mode`: + // * 'promise' will return either a Promise (that will settle when all Promises returned by + // templates have settled), or a non-Promise (if no nested templates return a Promise). + // * 'immediate' will return a non-Promise with all Promise values returned by templates replaced + // with `null`. + // * 'both' will return both of the above as `{ promise: p, immediate: i }`. + private _evaluateStructure(varMgr: VarMgr, struct: any, mode = 'promise'): any { let ret; if (struct instanceof Array) { ret = struct.map((v, _i) => - this._evaluateStructure(varMgr, v) + this._evaluateStructure(varMgr, v, mode) ); + if (mode != 'immediate') { + let promiseTmp: any[], immediate: any[] = [], promise: any[] | Promise; + if (mode == 'both') { immediate = ret.map((v) => v.immediate); } + promise = promiseTmp = (mode == 'both' ? ret.map((v) => v.promise) : ret); + if (somePromise(promiseTmp)) { + promise = Promise.all(promiseTmp.map((v) => Promise.resolve(v))); + } + ret = (mode == 'both' ? { promise: promise, immediate: immediate } : promise); + } } else if (typeof struct === 'object') { const tmp = Object.entries(struct as ObjMap).map(([k, v], _i) => - [k, this._evaluateStructure(varMgr, v)] + [k, this._evaluateStructure(varMgr, v, mode)] ); - ret = Object.fromEntries(tmp); + let immediateTmp: any[], promiseTmp: any[]; + let immediate: ObjMap = {}, promise: ObjMap | Promise = {}; + if (mode != 'promise') { + immediateTmp = (mode == 'both' ? tmp.map(([k, v], _i) => [k, v.immediate]) : tmp); + immediate = Object.fromEntries(immediateTmp); + ret = immediate; + } + if (mode != 'immediate') { + promiseTmp = (mode == 'both' ? tmp.map(([k, v], _i) => [k, v.promise]) : tmp); + if (somePromise(promiseTmp.map(([_k, v], _i) => v))) { + promise = Promise.all(promiseTmp.map(([k, v], _i) => + Promise.resolve(v).then((v) => [k, v]) + )).then((a) => Object.fromEntries(a)); + } else { + promise = Object.fromEntries(promiseTmp); + } + ret = promise; + } + if (mode == 'both') { ret = { promise: promise, immediate: immediate }; } } else if (isString(struct)) { ret = this._evaluateTemplate(varMgr, struct); + if (isPromise(ret)) { + if (mode == 'immediate') { + console.warn("Ignoring asynchronous function in 'entities'. Asynchronous functions are not permitted in 'entities'."); + ret = null; + } + if (mode == 'both') { ret = { promise: ret, immediate: null }; } + } else { + if (mode == 'both') { ret = { promise: ret, immediate: ret }; } + } } else { ret = structuredClone(struct); + if (mode == 'both') { ret = { promise: ret, immediate: ret }; }; } return ret; @@ -326,8 +422,18 @@ export class ConfigTemplateCard extends LitElement { const repls = matches.map((m, _i) => { return [m, this._evalWithVars(varMgr, m.substring(2, m.length - 1), '')] }); - repls.forEach(([m, r]) => template = template.replace(m as string, r.toString() as string)); - return template; + if (somePromise(repls.map(([_m, r], _i) => r))) { + return Promise.all(repls.map(([m, p]) => + Promise.resolve(p).then((r) => [m, r]) + )).then((a) => { + let t = template; + a.forEach(([m, r]) => t = t.replace(m as string, r.toString() as string)); + return t; + }); + } else { + repls.forEach(([m, r]) => template = template.replace(m as string, r.toString() as string)); + return template; + } } if (withoutDelim) { @@ -380,6 +486,12 @@ export class ConfigTemplateCard extends LitElement { const ret = indirectEval(initBase + initSVars + initVars + template); + if (isPromise(ret)) { + return ret.catch((e: unknown) => { + console.error('Template error:', e); + return exceptRet; + }); + } return ret; } catch(e) { console.error('Template error:', e); diff --git a/src/types.ts b/src/types.ts index bb78bec..b20b6dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,7 @@ export interface Config { export interface SVarMgr { svars: Vars; _evalInitSVars: string; + _svarsPromise?: Promise; } export interface VarMgr { @@ -33,4 +34,5 @@ export interface VarMgr { _evalInitSVars: string; vars: Vars; _evalInitVars: string; + _varsPromise?: Promise; }