Files
advanced-camera-card/src/card-controller/automations-manager.ts
T

112 lines
4.2 KiB
TypeScript

import { ConditionEvaluator } from '../condition-trigger/conditions/conditions/types.js';
import { createConditionEvaluator } from '../condition-trigger/conditions/factory.js';
import { TriggersManager } from '../condition-trigger/triggers/manager.js';
import { TriggerData } from '../condition-trigger/triggers/types.js';
import { Automation, AutomationActions } from '../config/schema/automations.js';
import { localize } from '../localize/localize.js';
import { TemplateRenderer } from './templates/index.js';
import { CardAutomationsAPI, TaggedAutomation } from './types.js';
const MAX_NESTED_AUTOMATION_EXECUTIONS = 10;
export class AutomationsManager {
private _api: CardAutomationsAPI;
private _automations = new Map<TaggedAutomation, TriggersManager>();
// A counter to avoid infinite loops, increases every time actions are run,
// decreases every time actions are complete.
private _nestedAutomationExecutions = 0;
constructor(api: CardAutomationsAPI) {
this._api = api;
}
public deleteAutomations(tag?: unknown) {
for (const [automation, triggers] of this._automations) {
if (automation.tag === tag) {
this._automations.delete(automation);
triggers.destroy();
}
}
}
public addAutomations(automations: TaggedAutomation[]): void {
const context = { templateRenderer: new TemplateRenderer() };
for (const automation of automations) {
const triggers = new TriggersManager(
automation.triggers,
this._api.getConditionStateManager(),
this._api.getHASSManager(),
);
// The ongoing `conditions:` block is pull-evaluated at trigger time, so
// its evaluators are never subscribed and hold no resources to tear down.
// They live in the trigger callback and are released when `triggers` is
// destroyed.
const conditions = (automation.conditions ?? []).map((condition) =>
createConditionEvaluator(condition, context),
);
triggers.addListener((data) => this._execute(automation, conditions, data));
this._automations.set(automation, triggers);
}
}
private _execute(
automation: Automation,
conditions: ConditionEvaluator[],
triggerData: TriggerData,
): void {
if (
!this._api.getHASSManager().hasHASS() ||
// Never execute automations if the card hasn't finished initializing, as
// it could cause a view change when camera loads are not finished.
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1407
!this._api.getInitializationManager().isInitializedMandatory() ||
// Never execute automations if there's an error (as our automation loop
// avoidance -- which shows as an error -- would not work!).
this._api.getIssueManager().getStateManager().hasFullCardIssue()
) {
return;
}
// Evaluate the ongoing conditions against the current state at the instant
// the automation is triggered. The state manager updates its stored state
// before dispatching to listeners, so this already reflects the triggering
// change.
const state = this._api.getConditionStateManager().getState();
const ongoingConditionsHold = conditions.every(
(evaluator) => evaluator.evaluate(state).result,
);
if (!ongoingConditionsHold || !automation.actions.length) {
return;
}
const runActions = async (actions: AutomationActions): Promise<void> => {
// Check the limit *before* incrementing, so the overflow path holds no
// increment to leak; the `finally` then guarantees the decrement even if
// executing the actions throws. Either leak would permanently inflate the
// counter and eventually block all automations.
if (this._nestedAutomationExecutions >= MAX_NESTED_AUTOMATION_EXECUTIONS) {
this._api.getNotificationManager().setNotification({
heading: {
text: localize('error.too_many_automations'),
icon: 'mdi:alert',
severity: 'high',
},
});
return;
}
++this._nestedAutomationExecutions;
try {
await this._api.getActionsManager().executeActions({ actions, triggerData });
} finally {
--this._nestedAutomationExecutions;
}
};
runActions(automation.actions);
}
}