feat: Add support for templates in actions (#1891)

- Closes #1878
This commit is contained in:
Dermot Duffy
2025-02-14 07:23:19 -08:00
committed by GitHub
parent 8d3cf07b43
commit 7fb710d45d
18 changed files with 474 additions and 24 deletions
+26 -8
View File
@@ -1,10 +1,12 @@
import { ActionContext } from 'action';
import { z } from 'zod';
import { ConditionsEvaluationData } from '../../conditions/types.js';
import { Actions, ActionsConfig, ActionType } from '../../config/types.js';
import { getActionConfigGivenAction } from '../../utils/action.js';
import { ActionSet } from './actions/set.js';
import { TemplateRenderer } from '../templates/index.js';
import { CardActionsManagerAPI } from '../types.js';
import { ActionSet } from './actions/set.js';
import { ActionExecutionRequest, AuxillaryActionConfig } from './types.js';
import { ActionContext } from 'action';
const INTERACTIONS = ['tap', 'double_tap', 'hold', 'start_tap', 'end_tap'] as const;
export type InteractionName = (typeof INTERACTIONS)[number];
@@ -22,9 +24,11 @@ export class ActionsManager {
protected _api: CardActionsManagerAPI;
protected _actionsInFlight: ActionSet[] = [];
protected _actionContext: ActionContext = {};
protected _templateRenderer: TemplateRenderer | null;
constructor(api: CardActionsManagerAPI) {
constructor(api: CardActionsManagerAPI, templateRenderer?: TemplateRenderer) {
this._api = api;
this._templateRenderer = templateRenderer ?? null;
}
/**
@@ -72,7 +76,7 @@ export class ActionsManager {
// actions).
actionConfig
) {
this.executeActions(actionConfig, config);
this.executeActions(actionConfig, { config });
}
};
@@ -97,7 +101,9 @@ export class ActionsManager {
public handleActionExecutionRequestEvent = async (
ev: CustomEvent<ActionExecutionRequest>,
): Promise<void> => {
await this.executeActions(ev.detail.action, ev.detail.config);
await this.executeActions(ev.detail.action, {
config: ev.detail.config,
});
};
public uninitialize(): void {
@@ -107,10 +113,22 @@ export class ActionsManager {
public async executeActions(
action: ActionType | ActionType[],
config?: AuxillaryActionConfig,
options?: {
config?: AuxillaryActionConfig;
triggerData?: ConditionsEvaluationData;
},
): Promise<void> {
const actionSet = new ActionSet(this._actionContext, action, {
config: config,
const hass = this._api.getHASSManager().getHASS();
const renderedAction =
hass && this._templateRenderer
? this._templateRenderer.renderRecursively(hass, action, {
conditionState: this._api.getConditionStateManager().getState(),
triggerData: options?.triggerData,
})
: action;
const actionSet = new ActionSet(this._actionContext, renderedAction, {
config: options?.config,
cardID: this._api.getConfigManager().getConfig()?.card_id,
});
+5 -1
View File
@@ -64,6 +64,7 @@ export class AutomationsManager {
const runActions = async (actions: AutomationActions): Promise<void> => {
++this._nestedAutomationExecutions;
if (this._nestedAutomationExecutions > MAX_NESTED_AUTOMATION_EXECUTIONS) {
this._api.getMessageManager().setMessageIfHigherPriority({
type: 'error',
@@ -72,7 +73,10 @@ export class AutomationsManager {
return;
}
await this._api.getActionsManager().executeActions(actions);
await this._api
.getActionsManager()
.executeActions(actions, { triggerData: result.data });
--this._nestedAutomationExecutions;
};
runActions(actions);
+2 -1
View File
@@ -37,6 +37,7 @@ import { MicrophoneManager } from './microphone-manager';
import { QueryStringManager } from './query-string-manager';
import { StatusBarItemManager } from './status-bar-item-manager';
import { StyleManager } from './style-manager';
import { TemplateRenderer } from './templates';
import { TriggersManager } from './triggers-manager';
import {
CardActionsManagerAPI,
@@ -104,7 +105,7 @@ export class CardController
);
protected _resolvedMediaCache = new ResolvedMediaCache();
protected _actionsManager = new ActionsManager(this);
protected _actionsManager = new ActionsManager(this, new TemplateRenderer());
protected _automationsManager = new AutomationsManager(this);
protected _cameraManager = new CameraManager(this);
protected _cameraURLManager = new CameraURLManager(this);
+80
View File
@@ -0,0 +1,80 @@
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
import { HASS, renderTemplate } from 'ha-nunjucks/dist';
import { ConditionsEvaluationData, ConditionState } from '../../conditions/types';
import { ActionType } from '../../config/types';
interface TemplateContextInternal {
camera?: string;
view?: string;
trigger?: ConditionsEvaluationData;
}
interface TemplateContext {
advanced_camera_card: TemplateContextInternal;
// Convenient alias.
acc: TemplateContextInternal;
}
export class TemplateRenderer {
public renderRecursively = (
hass: HomeAssistant,
data: unknown,
options?: {
conditionState?: ConditionState;
triggerData?: ConditionsEvaluationData;
},
): ActionType => {
return this._renderTemplateRecursively(
hass,
data,
this._conditionStateToTemplateContext(
options?.conditionState,
options?.triggerData,
),
);
};
protected _conditionStateToTemplateContext(
conditionState?: ConditionState,
triggerData?: ConditionsEvaluationData,
): TemplateContext | undefined {
if (!conditionState?.camera && !conditionState?.view && !triggerData) {
return;
}
const advancedCameraCardContext: TemplateContextInternal = {
...(conditionState?.camera && { camera: conditionState.camera }),
...(conditionState?.view && { view: conditionState.view }),
...(triggerData && { trigger: triggerData }),
};
return {
acc: advancedCameraCardContext,
advanced_camera_card: advancedCameraCardContext,
};
}
protected _renderTemplateRecursively(
hass: HomeAssistant,
data: unknown,
templateContext?: TemplateContext,
): ActionType {
if (typeof data === 'string') {
// ha-nunjucks has a more complete model of the Home Assistant object, but
// does not export it as a type.
return renderTemplate(hass as unknown as typeof HASS, data, templateContext);
} else if (Array.isArray(data)) {
return data.map((item) =>
this._renderTemplateRecursively(hass, item, templateContext),
);
} else if (typeof data === 'object' && data !== null) {
const result = {};
for (const key in data) {
result[key] = this._renderTemplateRecursively(hass, data[key], templateContext);
}
return result;
}
return data;
}
}