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

feat(templates): Support for one time evaluation of js templates in triggers_update and entity #741

Merged
merged 4 commits into from
Jul 30, 2023
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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,32 @@ Those are the configuration fields which support templating:
- all the `card` parameters in a `custom_field`
- all the `variables`

Special fields which do support templating but are **only evaluated once**, when the configuration is loaded. Error in those templates will only be visible in the javascript console and the card will not display in that case:

- `entity`: You can use JS templates for the `entity` of the card configuration. It is mainly useful when you define your entity in as an entry in `variables`. This is evaluated once only when the configuration is loaded.

```yaml
type: custom:button-card
variables:
my_entity: switch.skylight
entity: '[[[ return variables.my_entity; ]]]'
```

- `triggers_update`: Useful when you define multiple entities in `variables` to use throughout the card. Eg:

```yaml
type: custom:button-card
variables:
my_entity: switch.skylight
my_other_entity: light.bedroom
entity: '[[[ return variables.my_entity; ]]]'
label: '[[[ return localize(variables.my_other_entity) ]]]'
show_label: true
triggers_update:
- '[[[ return variables.my_entity; ]]]'
- '[[[ return variables.my_other_entity; ]]]'
```

Inside the javascript code, you'll have access to those variables:

- `this`: The button-card element itself (advanced stuff, don't mess with it)
Expand Down
145 changes: 94 additions & 51 deletions src/button-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,20 @@ class ButtonCard extends LitElement {

private _entities: string[] = [];

private _initial_setup_complete = false;
private _initialSetupComplete = false;

private get _doIHaveEverything(): boolean {
return !!this._hass && !!this._config && this.isConnected;
}

public set hass(hass: HomeAssistant) {
this._hass = hass;
Object.keys(this._cards).forEach((element) => {
const el = this._cards[element];
el.hass = this._hass;
});
if (!this._initial_setup_complete) {
this._initConnected();
if (!this._initialSetupComplete) {
this._finishSetup();
}
}

Expand All @@ -148,19 +152,92 @@ class ButtonCard extends LitElement {

public connectedCallback(): void {
super.connectedCallback();
if (!this._initial_setup_complete) {
this._initConnected();
if (!this._initialSetupComplete) {
this._finishSetup();
} else {
this._startTimerCountdown();
}
}

private _initConnected(): void {
if (this._hass === undefined) return;
if (this._config === undefined) return;
if (!this.isConnected) return;
this._initial_setup_complete = true;
this._startTimerCountdown();
private _evaluateVariablesSkipError(stateObj?: HassEntity | undefined) {
this._evaledVariables = {};
if (this._config?.variables) {
const variablesNameOrdered = Object.keys(this._config.variables).sort();
variablesNameOrdered.forEach((variable) => {
try {
this._evaledVariables[variable] = this._objectEvalTemplate(stateObj, this._config!.variables![variable]);
} catch (e) {}
});
}
}

private _finishSetup(): void {
if (!this._initialSetupComplete && this._doIHaveEverything) {
this._evaluateVariablesSkipError();

if (this._config!.entity) {
const entityEvaled = this._getTemplateOrValue(undefined, this._config!.entity);
this._config!.entity = entityEvaled;
this._stateObj = this._hass!.states[entityEvaled];
}

this._evaluateVariablesSkipError(this._stateObj);

if (this._config!.entity && DOMAINS_TOGGLE.has(computeDomain(this._config!.entity))) {
this._config = {
tap_action: { action: 'toggle' },
...this._config!,
};
} else if (this._config!.entity) {
this._config = {
tap_action: { action: 'more-info' },
...this._config!,
};
} else {
this._config = {
tap_action: { action: 'none' },
...this._config!,
};
}

const jsonConfig = JSON.stringify(this._config);
this._entities = [];
if (Array.isArray(this._config!.triggers_update)) {
this._config!.triggers_update.forEach((entry) => {
try {
const evaluatedEntry = this._getTemplateOrValue(this._stateObj, entry);
if (evaluatedEntry !== undefined && evaluatedEntry !== null && !this._entities.includes(evaluatedEntry)) {
this._entities.push(evaluatedEntry);
}
} catch (e) {}
});
} else if (typeof this._config!.triggers_update === 'string') {
const result = this._getTemplateOrValue(this._stateObj, this._config!.triggers_update);
if (result && result !== 'all') {
this._entities.push(result);
} else {
this._config.triggers_update = result;
}
}
if (this._config!.triggers_update !== 'all') {
const entitiesRxp = new RegExp(/states\[\s*('|\\")([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+)\1\s*\]/, 'gm');
const entitiesRxp2 = new RegExp(/states\[\s*('|\\")([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+)\1\s*\]/, 'm');
const matched = jsonConfig.match(entitiesRxp);
matched?.forEach((match) => {
const res = match.match(entitiesRxp2);
if (res && !this._entities.includes(res[2])) this._entities.push(res[2]);
});
}
if (this._config!.entity && !this._entities.includes(this._config!.entity))
this._entities.push(this._config!.entity);
this._expandTriggerGroups();

const rxp = new RegExp('\\[\\[\\[.*\\]\\]\\]', 'm');
this._hasTemplate = this._config!.triggers_update === 'all' && jsonConfig.match(rxp) ? true : false;

this._startTimerCountdown();
this._initialSetupComplete = true;
}
}

private _startTimerCountdown(): void {
Expand Down Expand Up @@ -1153,6 +1230,9 @@ class ButtonCard extends LitElement {
if (!config) {
throw new Error('Invalid configuration');
}
if (this._initialSetupComplete) {
this._initialSetupComplete = false;
}

this._cards = {};
this._cardsConfig = {};
Expand Down Expand Up @@ -1183,46 +1263,9 @@ class ButtonCard extends LitElement {
...template.lock,
},
};
if (this._config!.entity && DOMAINS_TOGGLE.has(computeDomain(this._config!.entity))) {
this._config = {
tap_action: { action: 'toggle' },
...this._config,
};
} else if (this._config!.entity) {
this._config = {
tap_action: { action: 'more-info' },
...this._config,
};
} else {
this._config = {
tap_action: { action: 'none' },
...this._config,
};
}

const jsonConfig = JSON.stringify(this._config);
this._entities = [];
if (Array.isArray(this._config.triggers_update)) {
this._entities = [...this._config.triggers_update];
} else if (typeof this._config.triggers_update === 'string' && this._config.triggers_update !== 'all') {
this._entities.push(this._config.triggers_update);
}
if (this._config.triggers_update !== 'all') {
const entitiesRxp = new RegExp(/states\[\s*('|\\")([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+)\1\s*\]/, 'gm');
const entitiesRxp2 = new RegExp(/states\[\s*('|\\")([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+)\1\s*\]/, 'm');
const matched = jsonConfig.match(entitiesRxp);
matched?.forEach((match) => {
const res = match.match(entitiesRxp2);
if (res && !this._entities.includes(res[2])) this._entities.push(res[2]);
});
}
if (this._config.entity && !this._entities.includes(this._config.entity)) this._entities.push(this._config.entity);
this._expandTriggerGroups();

const rxp = new RegExp('\\[\\[\\[.*\\]\\]\\]', 'm');
this._hasTemplate = this._config.triggers_update === 'all' && jsonConfig.match(rxp) ? true : false;
if (!this._initial_setup_complete) {
this._initConnected();
if (!this._initialSetupComplete) {
this._finishSetup();
}
}

Expand All @@ -1245,7 +1288,7 @@ class ButtonCard extends LitElement {
private _expandTriggerGroups(): void {
if (this._hass && this._config?.group_expand && this._entities) {
this._entities.forEach((entity) => {
if (this._hass?.states[entity].attributes?.entity_id) {
if (this._hass?.states[entity]?.attributes?.entity_id) {
this._loopGroup(this._hass?.states[entity].attributes?.entity_id);
}
});
Expand Down