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

Fix various issues with templates and variables #153

Open
wants to merge 6 commits 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
33 changes: 28 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,29 @@ entities:
- entity: light.bed_light
```

### Markdown card example

```yaml
type: custom:config-template-card
entities:
- sensor.outside_temperature
- sensor.time
- weather.home
variables:
weather: |
() => {
let hass = document.querySelector("home-assistant").hass;
let w = states['weather.home'].state;
let key = 'component.weather.state._.' + w;
return hass.resources[hass.language][key];
}
card:
type: markdown
content: |
### {{ states('sensor.outside_temperature') }} °C - ${weather()}
# {{ states('sensor.time') }}
```

## Defining global 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:
Expand All @@ -143,14 +166,14 @@ If you find yourself having to rewrite the same logic in multiple locations, you
type: 'custom:config-template-card'
variables:
setTempMessage: |
temp => {
(prefix, temp) => {
if (temp <= 19) {
return 'Quick, get a blanket!';
return prefix + 'Quick, get a blanket!';
}
else if (temp >= 20 && temp <= 22) {
return 'Cozy!';
return prefix + 'Cozy!';
}
return 'It's getting hot in here...';
return prefix + 'It's getting hot in here...';
}
currentTemp: states['climate.ecobee'].attributes.current_temperature
entities:
Expand All @@ -159,7 +182,7 @@ type: 'custom:config-template-card'
type: entities
entities:
- entity: climate.ecobee
name: '${ setTempMessage(currentTemp) }'
name: '${ setTempMessage("House: ", currentTemp) }'
````

## Dashboard wide variables
Expand Down
35 changes: 16 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,33 @@
"author": "Ian Richardson <[email protected]>",
"license": "MIT",
"dependencies": {
"deep-clone-simple": "^1.1.1",
"custom-card-helpers": "^1.7.2",
"home-assistant-js-websocket": "^5.11.1",
"lit": "^2.0.0-rc.2"
"deep-clone-simple": "^1.1.1",
"lit": "^3.2.1"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-decorators": "^7.14.5",
"@rollup/plugin-json": "^4.1.0",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^28.0.0",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@typescript-eslint/eslint-plugin": "^8.8.1",
"@typescript-eslint/parser": "^8.8.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.24.0",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.4.1",
"rollup": "^2.58.0",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-serve": "^1.1.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.30.0",
"rollup-plugin-uglify": "^6.0.4",
"typescript": "^4.4.3"
"prettier": "^3.3.3",
"rollup": "^4.24.0",
"rollup-plugin-serve": "^1.1.1",
"rollup-plugin-typescript2": "^0.36.0",
"typescript": "~5.5.0"
},
"scripts": {
"start": "rollup -c rollup.config.dev.js --watch",
"start": "rollup -c rollup.config.dev.mjs --watch",
"build": "npm run lint && npm run rollup",
"lint": "eslint src/*.ts",
"rollup": "rollup -c"
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.dev.js → rollup.config.dev.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import resolve from 'rollup-plugin-node-resolve';
import resolve from '@rollup/plugin-node-resolve';
import typescript from 'rollup-plugin-typescript2';
import serve from 'rollup-plugin-serve';

Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js → rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import resolve from "rollup-plugin-node-resolve";
import resolve from "@rollup/plugin-node-resolve";
import typescript from "rollup-plugin-typescript2";

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

Expand All @@ -16,6 +17,7 @@ console.info(
export class ConfigTemplateCard extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: ConfigTemplateConfig;
/* eslint-disable @typescript-eslint/no-explicit-any */
@state() private _helpers?: any;
private _initialized = false;

Expand Down Expand Up @@ -52,27 +54,28 @@ export class ConfigTemplateCard extends LitElement {
}

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() {
/* eslint-disable @typescript-eslint/no-explicit-any */
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 @@ -88,9 +91,8 @@ export class ConfigTemplateCard extends LitElement {
const oldHass = changedProps.get('hass') as HomeAssistant | undefined;

if (oldHass) {
for (const entity of this._config.entities) {
const evaluatedTemplate = this._evaluateTemplate(entity);
if (Boolean(this.hass && oldHass.states[evaluatedTemplate] !== this.hass.states[evaluatedTemplate])) {
for (const entity of this._evaluateConfig(this._config.entities)) {
if (Boolean(this.hass && oldHass.states[entity] !== this.hass.states[entity])) {
return true;
}
}
Expand Down Expand Up @@ -126,8 +128,8 @@ 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) : {};

Expand All @@ -139,28 +141,24 @@ export class ConfigTemplateCard extends LitElement {
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 @@ -171,57 +169,37 @@ export class ConfigTemplateCard extends LitElement {
}

private async loadCardHelpers(): Promise<void> {
/* eslint-disable @typescript-eslint/no-explicit-any */
this._helpers = await (window as any).loadCardHelpers();
}

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

if (value !== null) {
if (value instanceof Array) {
config[key] = this._evaluateArray(value);
} else if (typeof value === 'object') {
config[key] = this._evaluateConfig(value);
} else if (typeof value === 'string' && value.includes('${')) {
config[key] = this._evaluateTemplate(value);
}
}
});

return config;
}

/* eslint-disable @typescript-eslint/no-explicit-any */
private _evaluateArray(array: any): any {
for (let i = 0; i < array.length; ++i) {
const value = array[i];
if (value instanceof Array) {
array[i] = this._evaluateArray(value);
} else if (typeof value === 'object') {
array[i] = this._evaluateConfig(value);
} else if (typeof value === 'string' && value.includes('${')) {
array[i] = this._evaluateTemplate(value);
if (config instanceof Array) {
for (let i = 0; i < config.length; ++i) {
const value = config[i];
config[i] = this._evaluateConfig(value);
}
} else if (typeof config === 'object') {
Object.entries(config).forEach(entry => {
const key = entry[0];
const value = entry[1];
config[key] = this._evaluateConfig(value);
});
} else if (typeof config === 'string' && config.includes('${')) {
return _evaluateTemplate(config);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you mean this._evaluateTemplate? I'm getting a compile error here.

}

return array;
return config;
}

private _evaluateTemplate(template: string): string {
if (!template.includes('${')) {
return template;
}

/* eslint-disable @typescript-eslint/no-unused-vars */
const user = this.hass ? this.hass.user : undefined;
const states = this.hass ? this.hass.states : undefined;
const vars: any[] = [];
const namedVars: { [key: string]: any } = {};
const arrayVars: string[] = [];
let varDef = '';

if (this._config) {
if (Array.isArray(this._config.variables)) {
Expand Down Expand Up @@ -249,10 +227,19 @@ export class ConfigTemplateCard extends LitElement {
for (const varName in namedVars) {
const newV = eval(namedVars[varName]);
vars[varName] = newV;
// create variable definitions to be injected:
varDef = varDef + `var ${varName} = vars['${varName}'];\n`;
eval(`var ${varName} = newV;`);
}

return eval(varDef + template.substring(2, template.length - 1));
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 eval(template.substring(2, template.length - 1));
}

template.match(/\${[^}]+}/g).forEach(m => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the match may be null giving another compile error. can we maybe do this?

const matches = template.match(/\${[^}]+}/g);
if (matches) {
  matches.forEach(m => {
    const repl = eval(m.substring(2, m.length - 1));
    template = template.replace(m, repl);
  });
}

const repl = eval(m.substring(2, m.length - 1));
template = template.replace(m, repl);
});
return template;
}
}
Loading