From 37361ea1b5d96f354e589919a19943594c22751b Mon Sep 17 00:00:00 2001 From: Paul Donohue Date: Sat, 22 Feb 2025 12:10:58 -0500 Subject: [PATCH] Cleanup, document initialization, and prep for async changes --- README.md | 85 ++++++----- eslint.config.mjs | 1 + src/config-template-card.ts | 285 +++++++++++++++++++++--------------- src/types.ts | 22 ++- src/util.ts | 18 ++- tsconfig.json | 2 +- 6 files changed, 252 insertions(+), 161 deletions(-) diff --git a/README.md b/README.md index 3776c99..0b388ce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Config Template Card Card +# Config Template Card -📝 Templatable Configuration Card +📝 Template Based Configuration for Lovelace Dashboard Cards [![GitHub Release][releases-shield]][releases] [![License][license-shield]](LICENSE.md) @@ -15,13 +15,23 @@ [![Twitter][twitter]][twitter] [![Github][github]][github] -This card is for [Lovelace](https://www.home-assistant.io/lovelace) on [Home Assistant](https://www.home-assistant.io/) that allows you to use pretty much any valid Javascript on the hass object in your configuration +## Overview -## Minimum Home Assistant Version +This [Home Assistant](https://www.home-assistant.io/) [Lovelace Dashboard](https://www.home-assistant.io/dashboards) Card supports the use of Javascript templates to dynamically configure other nested Dashboard Cards. -Home Assistant version 0.110.0 or higher is required as of release 1.2.0 of config-template-card +### Template Language -## Support +Note that this Card uses **Javascript** templates. It does NOT support the **Jinja2** templates that are used elsewhere in Home Assistant. + +This is because Dashboard Cards are normally rendered entirely in the web browser using Javascript, while Jinja2 templates must be rendered by Python on the Home Assistant server. The use of Javascript for templates enables this Card to render templates in the browser without deviating from the expected design pattern of Dashboard Cards. + +For an alternative Card that does support Jinja2 templates, see [lovelace-card-templater](https://github.com/gadgetchnnel/lovelace-card-templater). That Card works by making API calls from the browser to the Home Assistant server to render each template. + +### Minimum Home Assistant Version + +Home Assistant version 0.110.0 or higher is required as of release 1.2.0 of config-template-card. + +### Support Hey dude! Help me out for a couple of :beers: or a :coffee:! @@ -41,22 +51,22 @@ resources: | Name | Type | Requirement | Description | | --------------- | ------ | ------------ | ------------------------------------------------------ | -| type | string | **Required** | `custom:config-template-card` | -| entities | list | **Required** | List of entity strings that should be watched for updates. Templates can be used here | +| type | string | **Required** | `custom:config-template-card` | +| entities | list | **Required** | List of entity strings that should be watched for updates. Templates can be used here. | | variables | list | **Optional** | List of variables, which can be templates, that can be used in your `config` and indexed using `vars` or by name. These are evaluated on each update/render. | | staticVariables | list | **Optional** | List of variables, which can be templates, that can be used in your `config` and indexed using `svars` or by name. These are evaluated only on the first update/render and are preserved without re-evaluation for subsequent updates. | -| card | object | **Optional** | Card configuration. (A card, row, or element configuration must be provided) | -| row | object | **Optional** | Row configuration. (A card, row, or element configuration must be provided) | -| element | object | **Optional** | Element configuration. (A card, row, or element configuration must be provided) | -| style | object | **Optional** | Style configuration. | +| card | object | **Optional** | Card configuration. (A card, row, or element configuration must be provided.) | +| row | object | **Optional** | Row configuration. (A card, row, or element configuration must be provided.) | +| element | object | **Optional** | Element configuration. (A card, row, or element configuration must be provided.) | +| style | object | **Optional** | Style configuration. Used only for element. | ### Available variables for templating | Variable | Description | | ----------- | ---------------------------------------------------------------------------------- | -| `hass` | The [hass](https://developers.home-assistant.io/docs/frontend/data/) object | -| `states` | The [states](https://developers.home-assistant.io/docs/frontend/data/#hassstates) object | -| `user` | The [user](https://developers.home-assistant.io/docs/frontend/data/#hassuser) object | +| `hass` | The [hass](https://developers.home-assistant.io/docs/frontend/data/) object | +| `states` | The [states](https://developers.home-assistant.io/docs/frontend/data/#hassstates) object | +| `user` | The [user](https://developers.home-assistant.io/docs/frontend/data/#hassuser) object | | `vars` | Defined by `variables` configuration and accessible in your templates to help clean them up. If `variables` in the configuration is a yaml list, then `vars` is an array starting at the 0th index as your firstly defined variable. If `variables` is an object in the configuration, then `vars` is a string-indexed map and you can also access the variables by name without using `vars` at all. | | `svars` | Defined by `staticVariables` configuration and accessible in your templates to avoid redundant expensive operations. If `staticVariables` in the configuration is a yaml list, then `svars` is an array starting at the 0th index as your firstly defined variable. If `staticVariables` is an object in the configuration, then `svars` is a string-indexed map and you can also access the variables by name without using `svars` at all. | @@ -120,7 +130,7 @@ elements: 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`. +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 @@ -161,31 +171,31 @@ variables: # {{ states('sensor.time') }} ``` -### Defining global functions in variables +### Defining functions in variables -If you find yourself having to rewrite the same logic in multiple locations, you can define global methods inside Config Template Card's variables, which can be called anywhere within the scope of the card: +If you find yourself having to rewrite the same logic in multiple locations, you can define methods inside Config Template Card's variables, which can be called anywhere within the scope of the card: ```yaml type: 'custom:config-template-card' - variables: - setTempMessage: | - (prefix, temp) => { - if (temp <= 19) { - return prefix + 'Quick, get a blanket!'; - } - else if (temp >= 20 && temp <= 22) { - return prefix + 'Cozy!'; - } - return prefix + 'It's getting hot in here...'; +variables: + setTempMessage: | + (prefix, temp) => { + if (temp <= 19) { + return prefix + 'Quick, get a blanket!'; } - currentTemp: states['climate.ecobee'].attributes.current_temperature + else if (temp >= 20 && temp <= 22) { + return prefix + 'Cozy!'; + } + return prefix + 'It's getting hot in here...'; + } + currentTemp: states['climate.ecobee'].attributes.current_temperature +entities: + - climate.ecobee +card: + type: entities entities: - - climate.ecobee - card: - type: entities - entities: - - entity: climate.ecobee - name: '${ setTempMessage("House: ", currentTemp) }' + - entity: climate.ecobee + name: '${ setTempMessage("House: ", currentTemp) }' ```` ### Dashboard wide variables @@ -203,7 +213,9 @@ config_template_card_staticVars: views: ``` -Both arrays and objects are supported, just like in card's local variables. It is allowed to mix the two types, i.e. use an array in dashboard variables and an object in card variables, or the other way around. If both definitions are arrays, then dashboard variables are put first in `vars`. In the mixed mode, `vars` have array indices and as well as variable names. +Both arrays and objects are supported, just like in card's local variables. It is allowed to mix the two types, i.e. use an array in dashboard variables and an object in card variables, or the other way around. If both definitions are arrays, then dashboard variables are put first in `vars`/`svars`. In the mixed mode, `vars`/`svars` have array indices and as well as variable names. + +Changes to these variable definitions are not automatically propagated to cards that are already rendered on the dashboard, so you must reload your browser after making changes. ### Note: All templates must be enclosed by `${}`, except when defining variables. @@ -225,6 +237,7 @@ Fork and then clone the repo to your local machine. From the cloned directory ru `npm install && npm run build` + [commits-shield]: https://img.shields.io/github/commit-activity/y/custom-cards/config-template-card.svg?style=for-the-badge [commits]: https://github.com/custom-cards/config-template-card/commits/master [discord]: https://discord.gg/Qa5fW2R diff --git a/eslint.config.mjs b/eslint.config.mjs index 6465cd3..e7b8cb3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,6 +14,7 @@ export default ts.config( '@typescript-eslint/no-unused-vars': ['warn', {'argsIgnorePattern': '^_'}], '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/no-unsafe-call': 'off', }, languageOptions: { diff --git a/src/config-template-card.ts b/src/config-template-card.ts index 34d2735..ca54fe0 100644 --- a/src/config-template-card.ts +++ b/src/config-template-card.ts @@ -2,9 +2,9 @@ import { LitElement, html, TemplateResult, PropertyValues } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { computeCardSize, HomeAssistant, LovelaceCard } from 'custom-card-helpers'; -import { Config as ConfigType, VarMgr as VarMgrType, Vars as VarsType } from './types'; +import { Config, SVarMgr, VarMgr, Vars, ObjMap } from './types'; import { VERSION } from './version'; -import { isString } from './util'; +import { assertNotNull, isString } from './util'; console.info( `%c CONFIG-TEMPLATE-CARD \n%c Version ${VERSION} `, @@ -14,42 +14,78 @@ console.info( @customElement('config-template-card') export class ConfigTemplateCard extends LitElement { + + // External interactions: + // + // Lit updates are triggered by changing any "property" or "state" variables, or by explicitly + // calling `this.requestUpdate()`. + // + // After a Lit update has been triggered, Lit will call `shouldUpdate(changedProps)`, and if that + // returns `true` then Lit will call `render()`. + // When performing async Lit rendering using `until()`, Lit should not begin a new update until + // the prior async update has completed. However, this code is designed to be able to handle + // parallel updates anyway. + // + // When HA state changes, HA will set `hass`. + // When HA config changes, HA will call `setConfig(config)`. + // When the global (dashboard wide) 'config_template_card_*' config changes, nothing happens; + // Users should reload their browser after changing the global config. + // + // After construction, the following will be triggered in an unspecified order: + // * Lit will trigger an update + // * HA will call `setConfig(config)` (which will trigger another update) + // * HA will set `hass` (which will trigger another update) + // + // It is not clear whether the global config is available at construction time, and it is only + // used here in combination with the local config, so we don't retrieve it until `setConfig()` is + // called. + @property({ attribute: false }) public hass?: HomeAssistant; - @state() private _config?: ConfigType; - private _varMgr: VarMgrType = {}; + @state() private _config?: Config; @state() private _helpers?: any; + + private _globalConfig: { svars: any, vars: any } = { svars: undefined, vars: undefined }; + private _svarMgr?: SVarMgr; private _initialized = false; + private _tmpVarMgr?: VarMgr; - public setConfig(config?: ConfigType): void { + constructor() { + super(); + void this.loadCardHelpers(); + } + + public setConfig(config?: Config): void { if (!config) { throw new Error('Invalid configuration'); } - if (!config.card && !config.row && !config.element) { throw new Error('No card or row or element defined'); } - + if ([config.card, config.row, config.element].filter(v => v).length > 1) { + throw new Error('Only one of card/row/element can be defined'); + } if (config.card && !config.card.type) { throw new Error('No card type defined'); } - if (config.card && config.card.type === 'picture-elements') { console.warn( 'WARNING: config-template-card should not be used with the picture-elements card itself. Instead use it as one of the elements. Check the README for details', ); } - if (config.element && !config.element.type) { throw new Error('No element type defined'); } - - if (!config.entities) { - throw new Error('No entities defined'); + if (!config.element && config.style) { + throw new Error('style can only be used with element'); } - this._config = config; - void this.loadCardHelpers(); + this._globalConfig = this.getLovelaceConfig(); + + // Force re-evaluation of staticVariables + this._svarMgr = undefined; + this._initialized = false; + this._initialize(); } private async loadCardHelpers(): Promise { @@ -70,14 +106,14 @@ export class ConfigTemplateCard extends LitElement { private getLovelaceConfig(): any { const panel = this.getLovelacePanel(); return { - vars: panel?.lovelace?.config?.config_template_card_vars, svars: panel?.lovelace?.config?.config_template_card_staticVars, + vars: panel?.lovelace?.config?.config_template_card_vars, }; } public getCardSize(): number | Promise { if (this.shadowRoot) { - // eslint detects this assertion as unnecessary, but typescript requires it. + // eslint improperly parses this assertion, but typescript handles it properly // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const element = this.shadowRoot.querySelector('#card > *') as LovelaceCard | null; if (element) { @@ -91,26 +127,35 @@ export class ConfigTemplateCard extends LitElement { } private _initialize(): boolean { - if (!this.hass || !this._config || !this._helpers) { return false; } + // _initSVars() requires hass and _config + if (!this.hass || !this._config) { return false; } + + // shouldUpdate() requires _svarMgr + if (!this._svarMgr) { + this._svarMgr = this._evaluateVars(true); + } + + // render() requires hass, _config, and _helpers + if (!this._helpers) { return false; } + this._initialized = true; return true; } protected shouldUpdate(changedProps: PropertyValues): boolean { - if (!this._initialized && !this._initialize()) { - return true; - } - // TypeScript doesn't detect the check in _initialize() - if (!this._config) { return true; } + if (!this._initialized) { return this._initialize(); } + assertNotNull(this._config); // TypeScript can't detect the gate in _initialize() - if (changedProps.has('_config')) { - return true; - } + if (changedProps.has('_config')) { return true; } const oldHass = changedProps.get('hass') as HomeAssistant | undefined; if (oldHass) { - this._evaluateVars(); - for (const entity of this._evaluateStructure(structuredClone(this._config.entities))) { + if (!this._config.entities) { return false; } + 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); + for (const entity of entities) { if (this.hass && oldHass.states[entity] !== this.hass.states[entity]) { return true; } @@ -118,34 +163,21 @@ export class ConfigTemplateCard extends LitElement { return false; } + // If anything else changed then re-render return true; } protected render(): TemplateResult { - if (!this._initialized && !this._initialize()) { - return html``; - } - // TypeScript doesn't detect the check in _initialize() - if (!this._config) { return html``; } - - let configSection = this._config.card - ? structuredClone(this._config.card) - : this._config.row - ? structuredClone(this._config.row) - : structuredClone(this._config.element); - - let style = this._config.style ? structuredClone(this._config.style) : {}; + if (!this._initialized) { return html``; } // Shouldn't happen - // render() is usually called shortly after shouldUpdate(), in which case we probably don't need - // to re-evaluate variables. - if (!this._varMgr.vars) { this._evaluateVars(); } + let varMgr = this._tmpVarMgr; + this._tmpVarMgr = undefined; + if (!varMgr) { varMgr = this._evaluateVars(false); } // Shouldn't happen - configSection = this._evaluateStructure(configSection); - style = this._evaluateStructure(style); + assertNotNull(this._config); // TypeScript can't detect the gate in _initialize() - // In case the next call to render() is not preceded by a call to shouldUpdate(), force the next - // render() call to re-evaluate variables. - this._varMgr.vars = undefined; + let configSection = (this._config.card ?? this._config.row ?? this._config.element); + configSection = this._evaluateStructure(varMgr, configSection); const element = this._config.card ? this._helpers.createCardElement(configSection) @@ -155,14 +187,17 @@ export class ConfigTemplateCard extends LitElement { element.hass = this.hass; if (this._config.element) { - Object.keys(style).forEach((prop) => { - this.style.setProperty(prop, style[prop]); - }); + if (this._config.style) { + let style = this._config.style; + style = this._evaluateStructure(varMgr, style); + Object.keys(style).forEach((prop) => { + this.style.setProperty(prop, style[prop]); + }); + } if (configSection?.style) { Object.keys(configSection.style).forEach((prop) => { - if (configSection.style) { // TypeScript requires a redundant check here, not sure why - element.style.setProperty(prop, configSection.style[prop]); - } + assertNotNull(configSection.style); // TypeScript can't detect the enclosing if() + element.style.setProperty(prop, configSection.style[prop]); }); } } @@ -170,33 +205,24 @@ export class ConfigTemplateCard extends LitElement { return html`
${element}
`; } - private _evaluateVars(doStatic = false, globalConfig: any = undefined): void { - const vars: VarsType = []; - let namedVars: Record = {}; - let arrayVars: any[] = []; - let init = '', initRef: string; - - let globalVars: VarsType | undefined; - let localVars: VarsType | undefined; - if (!globalConfig) { globalConfig = this.getLovelaceConfig(); } - if (!doStatic) { - Object.assign(this._varMgr, { - hass: this.hass, states: this.hass?.states, user: this.hass?.user, vars: vars, - }); - if (!this._varMgr.svars) { - this._evaluateVars(true, globalConfig); - } - globalVars = globalConfig.vars; - localVars = this._config?.variables; - initRef = 'vars'; + private _evaluateVars(doStatic: false): VarMgr; + private _evaluateVars(doStatic: true): SVarMgr; + private _evaluateVars(doStatic) { + assertNotNull(this.hass); // TypeScript can't detect the gate in _initialize() + assertNotNull(this._config); // TypeScript can't detect the gate in _initialize() + + let globalVars: Vars | undefined; + let localVars: Vars | undefined; + if (doStatic) { + globalVars = this._globalConfig.svars; + localVars = this._config.staticVariables; } else { - // This assumes that _evaluateVars(true) is only called by _evaluateVars(false), so we can - // assume that _varMgr is already initialized. - globalVars = globalConfig.svars; - localVars = this._config?.staticVariables; - initRef = 'svars'; + globalVars = this._globalConfig.vars; + localVars = this._config.variables; } + const arrayVars: any[] = []; + const namedVars: ObjMap = {}; if (globalVars) { if (Array.isArray(globalVars)) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument @@ -205,7 +231,6 @@ export class ConfigTemplateCard extends LitElement { Object.assign(namedVars, globalVars); } } - if (localVars) { if (Array.isArray(localVars)) { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument @@ -215,45 +240,77 @@ export class ConfigTemplateCard extends LitElement { } } - arrayVars = structuredClone(arrayVars); + const varMgr: VarMgr = { + hass: this.hass, states: this.hass.states, user: this.hass.user, + svars: this._svarMgr?.svars ?? [], _evalInitSVars: this._svarMgr?._evalInitSVars ?? '', + vars: [], _evalInitVars: '', + }; + const vars: Vars = (doStatic ? varMgr.svars : varMgr.vars); + const initKey = (doStatic ? '_evalInitSVars' : '_evalInitVars'); + const initRef = (doStatic ? 'svars' : 'vars'); + for (let v of arrayVars) { - if (isString(v)) { v = this._evaluateTemplate(v, true); } - else { v = this._evaluateStructure(v); } - vars.push(v); + if (isString(v)) { + v = this._evaluateTemplate(varMgr, v, true); + vars.push(v); + } else { + v = this._evaluateStructure(varMgr, v); + vars.push(v); + } } - - namedVars = structuredClone(namedVars); for (const varName in namedVars) { let v = namedVars[varName]; - if (isString(v)) { v = this._evaluateTemplate(v, true); } - else { v = this._evaluateStructure(v); } - vars[varName] = v; - init += `var ${varName} = ${initRef}['${varName}'];\n`; - if (!doStatic) { this._varMgr._evalInitVars = init; } - else { this._varMgr._evalInitSVars = init; } + if (isString(v)) { + v = this._evaluateTemplate(varMgr, v, true); + vars[varName] = v; + } + else { + v = this._evaluateStructure(varMgr, v); + vars[varName] = v; + } + // 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 + // shouldn't be a problem, since the second definition will simply override the first. + // However, if browsers/JavaScript are changed so that re-defining a variable causes a warning + // or error then we may need to explicitly remove duplicates from `_evalInitSVars`. + varMgr[initKey] += `var ${varName} = ${initRef}['${varName}'];\n`; + } + + if (doStatic) { + const svarMgr: SVarMgr = { + svars: vars, _evalInitSVars: varMgr._evalInitSVars, + }; + return svarMgr; + } else { + return varMgr; } } - private _evaluateStructure(struct: any): any { + private _evaluateStructure(varMgr: VarMgr, struct: any): any { + let ret; + if (struct instanceof Array) { - for (let i = 0; i < struct.length; ++i) { - const value = struct[i]; - struct[i] = this._evaluateStructure(value); - } + ret = struct.map((v, _i) => + this._evaluateStructure(varMgr, v) + ); + } else if (typeof struct === 'object') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - Object.entries(struct).forEach(entry => { - const key = entry[0]; - const value = entry[1]; - struct[key] = this._evaluateStructure(value); - }); + const tmp = Object.entries(struct as ObjMap).map(([k, v], _i) => + [k, this._evaluateStructure(varMgr, v)] + ); + ret = Object.fromEntries(tmp); + } else if (isString(struct)) { - return this._evaluateTemplate(struct); + ret = this._evaluateTemplate(varMgr, struct); + + } else { + ret = structuredClone(struct); } - return struct; + + return ret; } - private _evaluateTemplate(template: string, withoutDelim = false): any { + private _evaluateTemplate(varMgr: VarMgr, template: string, withoutDelim = false): any { if (template.startsWith('$!')) { return template.substring(2, template.length); } @@ -261,20 +318,20 @@ export class ConfigTemplateCard extends LitElement { if (template.startsWith('${') && template.endsWith('}')) { // The entire property is a template, return eval's result directly // to preserve types other than string (eg. numbers) - return this._evalWithVars(template.substring(2, template.length - 1)); + return this._evalWithVars(varMgr, template.substring(2, template.length - 1)); } const matches = /\${[^}]+}/.exec(template); if (matches) { - matches.forEach(m => { - const repl = this._evalWithVars(m.substring(2, m.length - 1), '').toString() as string; - template = template.replace(m, repl); + 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 (withoutDelim) { - return this._evalWithVars(template); + return this._evalWithVars(varMgr, template); } return template; @@ -289,7 +346,7 @@ export class ConfigTemplateCard extends LitElement { 'var vars = globalThis._varMgr.vars;\n' + ''); - private _evalWithVars(template: string, exceptRet: any = null): any { + private _evalWithVars(varMgr: VarMgr, template: string, exceptRet: any = null): any { // "direct" eval() is considered insecure and generates warnings, so use "indirect" eval(). // // "indirect" eval() sets `this` to `globalThis`/`window`, and does not support changing `this` @@ -314,11 +371,11 @@ export class ConfigTemplateCard extends LitElement { const origHass = globalThis.hass; try { - globalThis._varMgr = this._varMgr; + globalThis._varMgr = varMgr; globalThis.hass = this.hass; const initBase = this._evalInitBase; - const initSVars = (this._varMgr._evalInitSVars ?? ''); - const initVars = (this._varMgr._evalInitVars ?? ''); + const initSVars = varMgr._evalInitSVars; + const initVars = varMgr._evalInitVars; const indirectEval = eval; const ret = indirectEval(initBase + initSVars + initVars + template); diff --git a/src/types.ts b/src/types.ts index 0ff159a..bb78bec 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,8 @@ import { LovelaceCardConfig, EntitiesCardEntityConfig, LovelaceElementConfigBase import { HomeAssistant, CurrentUser } from 'custom-card-helpers'; import { HassEntities } from 'home-assistant-js-websocket'; -export type Vars = Record | any[]; +export type ObjMap = Record; +export type Vars = ObjMap | any[]; interface StyleMixin { style?: Record; @@ -19,12 +20,17 @@ export interface Config { style?: Record; } +export interface SVarMgr { + svars: Vars; + _evalInitSVars: string; +} + export interface VarMgr { - hass?: HomeAssistant; - states?: HassEntities; - user?: CurrentUser; - vars?: Vars; - _evalInitVars?: string; - svars?: Vars; - _evalInitSVars?: string; + hass: HomeAssistant; + states: HassEntities; + user: CurrentUser; + svars: Vars; + _evalInitSVars: string; + vars: Vars; + _evalInitVars: string; } diff --git a/src/util.ts b/src/util.ts index e8c32c4..1dd34a2 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,17 @@ -export function isString(testObj: any): testObj is string { - return (typeof testObj === 'string' || testObj instanceof String); +export function assertNotNull(value: T | null | undefined): asserts value is T { + if (value == null) { + throw new Error('Unexpected null or undefined value'); + } +} + +export function isString(value: any): value is string { + return (typeof value === 'string' || value instanceof String); +} + +export function isPromise(value: any): value is Promise { + return Boolean(value && typeof value.then === 'function'); +} + +export function somePromise(arr: any[]): boolean { + return arr.some((v) => isPromise(v)); } diff --git a/tsconfig.json b/tsconfig.json index 159a683..b8c6bdd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "module": "esnext", "moduleResolution": "bundler", "lib": [ - "es2017", + "es2019", "dom", "dom.iterable" ],