Skip to content
Merged
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
14 changes: 0 additions & 14 deletions .eslintrc.js

This file was deleted.

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
23 changes: 23 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @ts-check

import js from '@eslint/js';
import ts from 'typescript-eslint';
import globals from 'globals';

export default ts.config(
js.configs.recommended,
ts.configs.recommended,
{
rules: {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
},
languageOptions: {
sourceType: 'module', // Allows for the use of imports
parserOptions: {
tsconfigRootDir: import.meta.dirname,
projectService: true, // TypeScript type checking service
},
},
}
);
35 changes: 10 additions & 25 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,21 @@
"author": "Ian Richardson <iantrich@gmail.com>",
"license": "MIT",
"dependencies": {
"custom-card-helpers": "^1.9.0",
"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"
"lit": "^2.8.0"
},
"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",
"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"
"rollup": "^4.24.3",
"@rollup/plugin-node-resolve": "^15.3.0",
"rollup-plugin-typescript2": "^0.36.0",
"typescript": "~5.7.3",
"rollup-plugin-serve": "^3.0.0",
"eslint": "^9.20.1",
"typescript-eslint": "^8.24.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
105 changes: 48 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 (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 this._evaluateTemplate(config);
}

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,23 @@ 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;`);
}

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));
}

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

return eval(varDef + template.substring(2, template.length - 1));
return template;
}
}
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"dom.iterable"
],
"noEmit": true,
"sourceMap": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
Expand All @@ -18,4 +19,4 @@
"resolveJsonModule": true,
"experimentalDecorators": true
}
}
}
Loading