Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow async evaluation in special mode #149

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@ resources:

## Options

| 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 |
| variables | list | **Optional** | List of variables, which can be templates, that can be used in your `config` and indexed using `vars` or by name |
| card | object | **Optional** | Card configuration. (A card, row, or element configuaration must be provided) |
| row | object | **Optional** | Row configuration. (A card, row, or element configuaration must be provided) |
| element | object | **Optional** | Element configuration. (A card, row, or element configuaration must be provided) |
| style | object | **Optional** | Style configuration. |
| 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 |
| variables | list | **Optional** | List of variables, which can be templates, that can be used in your `config` and indexed using `vars` or by name |
| async | boolean | **Optional** | Whether to use asynchronous evaluation. See [async](#asynchronous-mode) below. |
| card | object | **Optional** | Card configuration. (A card, row, or element configuaration must be provided) |
| row | object | **Optional** | Row configuration. (A card, row, or element configuaration must be provided) |
| element | object | **Optional** | Element configuration. (A card, row, or element configuaration must be provided) |
| style | object | **Optional** | Style configuration. |

### Available variables for templating

Expand All @@ -57,6 +58,18 @@ resources:
| `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. |

### Asynchronous mode

Setting the `async` option to `true` will enable asynchronous evaluation in your template. This allows you to `await` async functions or promises.

There are a two things to note here:

- In async mode, your template **must** explicitly `return` a value. Implicit completion value templates will simply eval to `undefined`.
- Example: instead of using `${1 + 1}`, use `${return 1 + 1}` instead.
- Async evaluation does **not** work in the entities list
- The card may flicker while rendering if your template takes too long to evaluate.

## Examples

```yaml
Expand Down Expand Up @@ -112,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
Expand Down Expand Up @@ -160,7 +174,7 @@ type: 'custom:config-template-card'
entities:
- entity: climate.ecobee
name: '${ setTempMessage(currentTemp) }'
````
```

## Dashboard wide variables

Expand Down
109 changes: 83 additions & 26 deletions src/config-template-card.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { LitElement, html, customElement, property, TemplateResult, PropertyValues, state } from 'lit-element';
import { LitElement, html, TemplateResult, PropertyValues } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { until } from 'lit/directives/until.js';
import deepClone from 'deep-clone-simple';
import { computeCardSize, HomeAssistant, LovelaceCard } from 'custom-card-helpers';

Expand Down Expand Up @@ -46,33 +48,37 @@ export class ConfigTemplateCard extends LitElement {
throw new Error('No entities defined');
}

if (config.async !== undefined && !(typeof config.async === 'boolean')) {
throw new Error('Config value "async" is not a boolean');
}

this._config = config;

this.loadCardHelpers();
}

private getLovelacePanel() {
const ha = document.querySelector("home-assistant");
const ha = document.querySelector('home-assistant');

if (ha && ha.shadowRoot) {
const haMain = ha.shadowRoot.querySelector("home-assistant-main");
const haMain = ha.shadowRoot.querySelector('home-assistant-main');

if (haMain && haMain.shadowRoot) {
return haMain.shadowRoot.querySelector('ha-panel-lovelace');
}
}

return null
return null;
}

private getLovelaceConfig() {
const panel = this.getLovelacePanel() as any;

if (panel && panel.lovelace && panel.lovelace.config && panel.lovelace.config.config_template_card_vars) {
return panel.lovelace.config.config_template_card_vars
return panel.lovelace.config.config_template_card_vars;
}

return {}
return {};
}

protected shouldUpdate(changedProps: PropertyValues): boolean {
Expand All @@ -89,7 +95,7 @@ export class ConfigTemplateCard extends LitElement {

if (oldHass) {
for (const entity of this._config.entities) {
const evaluatedTemplate = this._evaluateTemplate(entity);
const evaluatedTemplate = this._evaluateTemplate(entity, false);
if (Boolean(this.hass && oldHass.states[evaluatedTemplate] !== this.hass.states[evaluatedTemplate])) {
return true;
}
Expand All @@ -114,6 +120,10 @@ export class ConfigTemplateCard extends LitElement {
}

protected render(): TemplateResult | void {
return html`${until(this.getCardElement())}`;
}

private async getCardElement(): Promise<TemplateResult> {
if (
!this._config ||
!this.hass ||
Expand All @@ -126,41 +136,46 @@ export class ConfigTemplateCard extends LitElement {
let config = this._config.card
? deepClone(this._config.card)
: this._config.row
? deepClone(this._config.row)
: deepClone(this._config.element);
? deepClone(this._config.row)
: deepClone(this._config.element);

let style = this._config.style ? deepClone(this._config.style) : {};

config = this._evaluateConfig(config);
if (style) {
style = this._evaluateConfig(style);
if (this._config.async ?? false) {
// Eval in async mode
config = await this._evaluateConfigAsync(config);
if (style) {
style = await this._evaluateConfigAsync(style);
}
} else {
// Eval in non-async (default) mode
config = this._evaluateConfig(config);
if (style) {
style = this._evaluateConfig(style);
}
}

const element = this._config.card
? this._helpers.createCardElement(config)
: this._config.row
? this._helpers.createRowElement(config)
: this._helpers.createHuiElement(config);
? this._helpers.createRowElement(config)
: this._helpers.createHuiElement(config);
element.hass = this.hass;

if (this._config.element) {
if (style) {
Object.keys(style).forEach(prop => {
Object.keys(style).forEach((prop) => {
this.style.setProperty(prop, style[prop]);
});
}
if (config.style) {
Object.keys(config.style).forEach(prop => {
Object.keys(config.style).forEach((prop) => {
element.style.setProperty(prop, config.style[prop]);
});
}
}

return html`
<div id="card">
${element}
</div>
`;
return html` <div id="card">${element}</div> `;
}

private _initialize(): void {
Expand All @@ -176,7 +191,7 @@ export class ConfigTemplateCard extends LitElement {

/* eslint-disable @typescript-eslint/no-explicit-any */
private _evaluateConfig(config: any): any {
Object.entries(config).forEach(entry => {
Object.entries(config).forEach((entry) => {
const key = entry[0];
const value = entry[1];

Expand All @@ -186,14 +201,31 @@ export class ConfigTemplateCard extends LitElement {
} else if (typeof value === 'object') {
config[key] = this._evaluateConfig(value);
} else if (typeof value === 'string' && value.includes('${')) {
config[key] = this._evaluateTemplate(value);
config[key] = this._evaluateTemplate(value, false);
}
}
});

return config;
}

/* eslint-disable @typescript-eslint/no-explicit-any */
private async _evaluateConfigAsync(config: any): Promise<any> {
for (const [key, value] of Object.entries(config)) {
if (value !== null) {
if (value instanceof Array) {
config[key] = await this._evaluateArrayAsync(value);
} else if (typeof value === 'object') {
config[key] = await this._evaluateConfigAsync(value);
} else if (typeof value === 'string' && value.includes('${')) {
config[key] = await this._evaluateTemplate(value, true);
}
}
}

return config;
}

/* eslint-disable @typescript-eslint/no-explicit-any */
private _evaluateArray(array: any): any {
for (let i = 0; i < array.length; ++i) {
Expand All @@ -203,15 +235,36 @@ export class ConfigTemplateCard extends LitElement {
} else if (typeof value === 'object') {
array[i] = this._evaluateConfig(value);
} else if (typeof value === 'string' && value.includes('${')) {
array[i] = this._evaluateTemplate(value);
array[i] = this._evaluateTemplate(value, false);
}
}

return array;
}

private _evaluateTemplate(template: string): string {
/* eslint-disable @typescript-eslint/no-explicit-any */
private async _evaluateArrayAsync(array: any): Promise<any> {
for (let i = 0; i < array.length; ++i) {
const value = array[i];
if (value instanceof Array) {
array[i] = await this._evaluateArrayAsync(value);
} else if (typeof value === 'object') {
array[i] = await this._evaluateConfigAsync(value);
} else if (typeof value === 'string' && value.includes('${')) {
array[i] = await this._evaluateTemplate(value, true);
}
}

return array;
}

private _evaluateTemplate(template: string, useAsync: false): string;
private _evaluateTemplate(template: string, useAsync: true): Promise<string>;
private _evaluateTemplate(template: string, useAsync: boolean) {
if (!template.includes('${')) {
if (useAsync) {
return new Promise((resolve) => resolve(template));
}
return template;
}

Expand Down Expand Up @@ -253,6 +306,10 @@ export class ConfigTemplateCard extends LitElement {
varDef = varDef + `var ${varName} = vars['${varName}'];\n`;
}

return eval(varDef + template.substring(2, template.length - 1));
let evalBody = varDef + template.substring(2, template.length - 1);
if (useAsync) {
evalBody = `(async () => { ${evalBody} })()`;
}
return eval(evalBody) as string | Promise<string>;
}
}
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface ConfigTemplateConfig {
type: string;
entities: string[];
variables?: string[] | { [key: string]: string };
async?: boolean;
card?: LovelaceCardConfig;
row?: EntitiesCardEntityConfig;
element?: LovelaceElementConfigBase;
Expand Down