@@ -3,6 +3,7 @@
|
|||||||
- [`actions`](README.md)
|
- [`actions`](README.md)
|
||||||
- [Custom Actions](./custom/README.md)
|
- [Custom Actions](./custom/README.md)
|
||||||
- [Stock Actions](./stock/README.md)
|
- [Stock Actions](./stock/README.md)
|
||||||
|
- [Templates](./templates.md)
|
||||||
- [`automations`](../automations.md)
|
- [`automations`](../automations.md)
|
||||||
- [`cameras`](../cameras/README.md)
|
- [`cameras`](../cameras/README.md)
|
||||||
- [`conditions`](../conditions.md)
|
- [`conditions`](../conditions.md)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
- [`actions`](../README.md)
|
- [`actions`](../README.md)
|
||||||
- [Custom Actions](README.md)
|
- [Custom Actions](README.md)
|
||||||
- [Stock Actions](../stock/README.md)
|
- [Stock Actions](../stock/README.md)
|
||||||
|
- [Templates](../templates.md)
|
||||||
- [`automations`](../../automations.md)
|
- [`automations`](../../automations.md)
|
||||||
- [`cameras`](../../cameras/README.md)
|
- [`cameras`](../../cameras/README.md)
|
||||||
- [`conditions`](../../conditions.md)
|
- [`conditions`](../../conditions.md)
|
||||||
|
|||||||
@@ -73,13 +73,13 @@ elements:
|
|||||||
action: toggle
|
action: toggle
|
||||||
- type: icon
|
- type: icon
|
||||||
icon: mdi:numeric-3-box
|
icon: mdi:numeric-3-box
|
||||||
title: Call Service action
|
title: Perform Action / Call Service action
|
||||||
style:
|
style:
|
||||||
left: 200px
|
left: 200px
|
||||||
top: 150px
|
top: 150px
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: perform-action
|
||||||
service: homeassistant.toggle
|
perform_action: homeassistant.toggle
|
||||||
data:
|
data:
|
||||||
entity_id: light.office_main_lights
|
entity_id: light.office_main_lights
|
||||||
- type: icon
|
- type: icon
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
- [`actions`](../README.md)
|
- [`actions`](../README.md)
|
||||||
- [Custom Actions](../custom/README.md)
|
- [Custom Actions](../custom/README.md)
|
||||||
- [Stock Actions](README.md)
|
- [Stock Actions](README.md)
|
||||||
|
- [Templates](../templates.md)
|
||||||
- [`automations`](../../automations.md)
|
- [`automations`](../../automations.md)
|
||||||
- [`cameras`](../../cameras/README.md)
|
- [`cameras`](../../cameras/README.md)
|
||||||
- [`conditions`](../../conditions.md)
|
- [`conditions`](../../conditions.md)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Templates
|
||||||
|
|
||||||
|
Before actions are executed, template values (if present) are replaced within
|
||||||
|
each action. This allows a variety of Home Assistant data to be automatically
|
||||||
|
populated in your actions, as well as some Advanced Camera Card data.
|
||||||
|
|
||||||
|
## Stock Templates
|
||||||
|
|
||||||
|
The Advanced Camera Card uses
|
||||||
|
[ha-nunjucks](https://github.com/Nerwyn/ha-nunjucks) to process templates.
|
||||||
|
Consult its documentation for the wide variety of different template values
|
||||||
|
supported.
|
||||||
|
|
||||||
|
See [an example](../../examples.md?id=accessing-home-assistant-state) that
|
||||||
|
accesses Home Assistant state.
|
||||||
|
|
||||||
|
## Custom Templates
|
||||||
|
|
||||||
|
Custom template values must be proceeded by `advanced_camera_card` (or `acc` for
|
||||||
|
short).
|
||||||
|
|
||||||
|
| Template | Replaced with |
|
||||||
|
| -------- | -------------------------------------------------- |
|
||||||
|
| `camera` | The currently selected camera. |
|
||||||
|
| `view` | The current [view](../view.md?id=supported-views). |
|
||||||
|
|
||||||
|
See [an example](../../examples.md?id=accessing-advanced-camera-card-state) that
|
||||||
|
accesses Advanced Camera Card state.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
|
||||||
|
If the action is called by an [Advanced Camera Card
|
||||||
|
Automation](../automations.md), additional data is available representing the
|
||||||
|
current and prior state of whatever triggered the action.
|
||||||
|
|
||||||
|
Trigger template values must be proceeded by `advanced_camera_card.trigger` (or
|
||||||
|
`acc.trigger` for short).
|
||||||
|
|
||||||
|
| Template | Replaced with |
|
||||||
|
| -------------- | ------------------------------------------------------------------------------------------------- |
|
||||||
|
| `camera.to` | For [camera conditions](../conditions.md?id=camera), the currently selected camera. |
|
||||||
|
| `camera.from` | For [camera conditions](../conditions.md?id=camera), the previously selected camera. |
|
||||||
|
| `view.to` | For [view conditions](../conditions.md?id=view), the currently selected view. |
|
||||||
|
| `view.from` | For [view conditions](../conditions.md?id=view), the previously selected view. |
|
||||||
|
| `state.entity` | For [state conditions](../conditions.md?id=state), the entity state that triggered the condition. |
|
||||||
|
| `state.to` | For [state conditions](../conditions.md?id=state), the current state of the entity. |
|
||||||
|
| `state.from` | For [state conditions](../conditions.md?id=state), the previous state of the entity. |
|
||||||
|
|
||||||
|
!> If an action is triggered with multiple [state
|
||||||
|
conditions](../conditions.md?id=state), only data from the last listed state
|
||||||
|
condition is available.
|
||||||
|
|
||||||
|
Please [request](https://github.com/dermotduffy/advanced-camera-card/issues) if
|
||||||
|
you need data from additional conditions.
|
||||||
|
|
||||||
|
See [an example](../../examples.md?id=accessing-trigger-state) that accesses
|
||||||
|
trigger state.
|
||||||
@@ -143,7 +143,7 @@ This card supports several different views.
|
|||||||
| `clip` | Shows a viewer for the most recent clip for this camera. Can also be accessed by holding down the `clips` menu icon. |
|
| `clip` | Shows a viewer for the most recent clip for this camera. Can also be accessed by holding down the `clips` menu icon. |
|
||||||
| `clips` | Shows a gallery of clips for this camera. |
|
| `clips` | Shows a gallery of clips for this camera. |
|
||||||
| `image` | Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view.interaction_seconds`). |
|
| `image` | Shows a static image specified by the `image` parameter, can be used as a discrete default view or a screensaver (via `view.interaction_seconds`). |
|
||||||
| `live` | Shows the live camera view with the configured [live provider](). |
|
| `live` | Shows the live camera view with the configured [live provider](./cameras/live-provider.md). |
|
||||||
| `recording` | Shows a viewer for the most recent recording for this camera. Can also be accessed by holding down the `recordings` menu icon. |
|
| `recording` | Shows a viewer for the most recent recording for this camera. Can also be accessed by holding down the `recordings` menu icon. |
|
||||||
| `recordings` | Shows a gallery of recent (last day) recordings for this camera and its dependents. |
|
| `recordings` | Shows a gallery of recent (last day) recordings for this camera and its dependents. |
|
||||||
| `snapshot` | Shows a viewer for the most recent snapshot for this camera. Can also be accessed by holding down the `snapshots` menu icon. |
|
| `snapshot` | Shows a viewer for the most recent snapshot for this camera. Can also be accessed by holding down the `snapshots` menu icon. |
|
||||||
|
|||||||
@@ -947,6 +947,61 @@ menu:
|
|||||||
icon: mdi:high-definition
|
icon: mdi:high-definition
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Templates in actions
|
||||||
|
|
||||||
|
### Accessing Home Assistant state
|
||||||
|
|
||||||
|
Perhaps the most common usage of templates is to access Home Assistant state
|
||||||
|
values. In the below example a fictitious service `homeassistant.service` is
|
||||||
|
called with data that refers to the current state of the `light.sunroom_ceiling`
|
||||||
|
entity.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tap_action:
|
||||||
|
action: perform-action
|
||||||
|
perform_action: homeassistant.service
|
||||||
|
data:
|
||||||
|
key: '{{ hass.states["light.sunroom_ceiling"].state }}'
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Stock Templates](./configuration/actions/templates.md?id=stock-templates).
|
||||||
|
|
||||||
|
### Accessing Advanced Camera Card state
|
||||||
|
|
||||||
|
In this example, the currently selected camera and
|
||||||
|
[view](./configuration/view.md) are passed as data to a fictitious service
|
||||||
|
`homeassistant.service`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tap_action:
|
||||||
|
action: perform-action
|
||||||
|
perform_action: homeassistant.service
|
||||||
|
data:
|
||||||
|
camera: '{{ advanced_camera_card.camera }}'
|
||||||
|
view: '{{ advanced_camera_card.view }}'
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Custom Templates](./configuration/actions/templates.md?id=custom-templates).
|
||||||
|
|
||||||
|
### Accessing Trigger state
|
||||||
|
|
||||||
|
In this example, an [automation](./configuration/automations.md) is triggered,
|
||||||
|
and values associated with the triggering are included in the action.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
automations:
|
||||||
|
- conditions:
|
||||||
|
- condition: camera
|
||||||
|
actions:
|
||||||
|
- action: perform-action
|
||||||
|
perform_action: homeassistant.service
|
||||||
|
data:
|
||||||
|
from_camera: '{{ acc.trigger.camera.from }}'
|
||||||
|
to_camera: '{{ acc.trigger.camera.to }}'
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Trigger Templates](./configuration/actions/templates.md?id=triggers).
|
||||||
|
|
||||||
## Trigger actions
|
## Trigger actions
|
||||||
|
|
||||||
You can control the card itself with the `custom:advanced-camera-card-action` action.
|
You can control the card itself with the `custom:advanced-camera-card-action` action.
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"date-fns-tz": "^3.1.3",
|
"date-fns-tz": "^3.1.3",
|
||||||
"embla-carousel": "^8.3.0",
|
"embla-carousel": "^8.3.0",
|
||||||
"embla-carousel-wheel-gestures": "^8.0.1",
|
"embla-carousel-wheel-gestures": "^8.0.1",
|
||||||
|
"ha-nunjucks": "^1.3.0",
|
||||||
"home-assistant-js-websocket": "^9.4.0",
|
"home-assistant-js-websocket": "^9.4.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"keycharm": "^0.4.0",
|
"keycharm": "^0.4.0",
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ const outputEntryTemplate = {
|
|||||||
sourcemap: dev,
|
sourcemap: dev,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CIRCULAR_DEPENDENCY_IGNORE_REGEXP = /(ha-nunjucks|ts-py-datetime)/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import('rollup').RollupOptions}
|
* @type {import('rollup').RollupOptions}
|
||||||
*/
|
*/
|
||||||
@@ -116,6 +118,16 @@ const config = {
|
|||||||
'./node_modules/@formatjs/intl-utils/lib/src/diff.js': 'window',
|
'./node_modules/@formatjs/intl-utils/lib/src/diff.js': 'window',
|
||||||
'./node_modules/@formatjs/intl-utils/lib/src/resolve-locale.js': 'window',
|
'./node_modules/@formatjs/intl-utils/lib/src/resolve-locale.js': 'window',
|
||||||
},
|
},
|
||||||
|
// Ignore circular dependencies from underlying libraries.
|
||||||
|
onwarn: (warning, defaultHandler) => {
|
||||||
|
if (
|
||||||
|
warning.code === 'CIRCULAR_DEPENDENCY' &&
|
||||||
|
warning.ids.some((id) => id.match(CIRCULAR_DEPENDENCY_IGNORE_REGEXP))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
defaultHandler(warning);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { ActionContext } from 'action';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { ConditionsEvaluationData } from '../../conditions/types.js';
|
||||||
import { Actions, ActionsConfig, ActionType } from '../../config/types.js';
|
import { Actions, ActionsConfig, ActionType } from '../../config/types.js';
|
||||||
import { getActionConfigGivenAction } from '../../utils/action.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 { CardActionsManagerAPI } from '../types.js';
|
||||||
|
import { ActionSet } from './actions/set.js';
|
||||||
import { ActionExecutionRequest, AuxillaryActionConfig } from './types.js';
|
import { ActionExecutionRequest, AuxillaryActionConfig } from './types.js';
|
||||||
import { ActionContext } from 'action';
|
|
||||||
|
|
||||||
const INTERACTIONS = ['tap', 'double_tap', 'hold', 'start_tap', 'end_tap'] as const;
|
const INTERACTIONS = ['tap', 'double_tap', 'hold', 'start_tap', 'end_tap'] as const;
|
||||||
export type InteractionName = (typeof INTERACTIONS)[number];
|
export type InteractionName = (typeof INTERACTIONS)[number];
|
||||||
@@ -22,9 +24,11 @@ export class ActionsManager {
|
|||||||
protected _api: CardActionsManagerAPI;
|
protected _api: CardActionsManagerAPI;
|
||||||
protected _actionsInFlight: ActionSet[] = [];
|
protected _actionsInFlight: ActionSet[] = [];
|
||||||
protected _actionContext: ActionContext = {};
|
protected _actionContext: ActionContext = {};
|
||||||
|
protected _templateRenderer: TemplateRenderer | null;
|
||||||
|
|
||||||
constructor(api: CardActionsManagerAPI) {
|
constructor(api: CardActionsManagerAPI, templateRenderer?: TemplateRenderer) {
|
||||||
this._api = api;
|
this._api = api;
|
||||||
|
this._templateRenderer = templateRenderer ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,7 +76,7 @@ export class ActionsManager {
|
|||||||
// actions).
|
// actions).
|
||||||
actionConfig
|
actionConfig
|
||||||
) {
|
) {
|
||||||
this.executeActions(actionConfig, config);
|
this.executeActions(actionConfig, { config });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,7 +101,9 @@ export class ActionsManager {
|
|||||||
public handleActionExecutionRequestEvent = async (
|
public handleActionExecutionRequestEvent = async (
|
||||||
ev: CustomEvent<ActionExecutionRequest>,
|
ev: CustomEvent<ActionExecutionRequest>,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await this.executeActions(ev.detail.action, ev.detail.config);
|
await this.executeActions(ev.detail.action, {
|
||||||
|
config: ev.detail.config,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public uninitialize(): void {
|
public uninitialize(): void {
|
||||||
@@ -107,10 +113,22 @@ export class ActionsManager {
|
|||||||
|
|
||||||
public async executeActions(
|
public async executeActions(
|
||||||
action: ActionType | ActionType[],
|
action: ActionType | ActionType[],
|
||||||
config?: AuxillaryActionConfig,
|
options?: {
|
||||||
|
config?: AuxillaryActionConfig;
|
||||||
|
triggerData?: ConditionsEvaluationData;
|
||||||
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const actionSet = new ActionSet(this._actionContext, action, {
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
config: config,
|
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,
|
cardID: this._api.getConfigManager().getConfig()?.card_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export class AutomationsManager {
|
|||||||
|
|
||||||
const runActions = async (actions: AutomationActions): Promise<void> => {
|
const runActions = async (actions: AutomationActions): Promise<void> => {
|
||||||
++this._nestedAutomationExecutions;
|
++this._nestedAutomationExecutions;
|
||||||
|
|
||||||
if (this._nestedAutomationExecutions > MAX_NESTED_AUTOMATION_EXECUTIONS) {
|
if (this._nestedAutomationExecutions > MAX_NESTED_AUTOMATION_EXECUTIONS) {
|
||||||
this._api.getMessageManager().setMessageIfHigherPriority({
|
this._api.getMessageManager().setMessageIfHigherPriority({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@@ -72,7 +73,10 @@ export class AutomationsManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._api.getActionsManager().executeActions(actions);
|
await this._api
|
||||||
|
.getActionsManager()
|
||||||
|
.executeActions(actions, { triggerData: result.data });
|
||||||
|
|
||||||
--this._nestedAutomationExecutions;
|
--this._nestedAutomationExecutions;
|
||||||
};
|
};
|
||||||
runActions(actions);
|
runActions(actions);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { MicrophoneManager } from './microphone-manager';
|
|||||||
import { QueryStringManager } from './query-string-manager';
|
import { QueryStringManager } from './query-string-manager';
|
||||||
import { StatusBarItemManager } from './status-bar-item-manager';
|
import { StatusBarItemManager } from './status-bar-item-manager';
|
||||||
import { StyleManager } from './style-manager';
|
import { StyleManager } from './style-manager';
|
||||||
|
import { TemplateRenderer } from './templates';
|
||||||
import { TriggersManager } from './triggers-manager';
|
import { TriggersManager } from './triggers-manager';
|
||||||
import {
|
import {
|
||||||
CardActionsManagerAPI,
|
CardActionsManagerAPI,
|
||||||
@@ -104,7 +105,7 @@ export class CardController
|
|||||||
);
|
);
|
||||||
protected _resolvedMediaCache = new ResolvedMediaCache();
|
protected _resolvedMediaCache = new ResolvedMediaCache();
|
||||||
|
|
||||||
protected _actionsManager = new ActionsManager(this);
|
protected _actionsManager = new ActionsManager(this, new TemplateRenderer());
|
||||||
protected _automationsManager = new AutomationsManager(this);
|
protected _automationsManager = new AutomationsManager(this);
|
||||||
protected _cameraManager = new CameraManager(this);
|
protected _cameraManager = new CameraManager(this);
|
||||||
protected _cameraURLManager = new CameraURLManager(this);
|
protected _cameraURLManager = new CameraURLManager(this);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,17 +48,10 @@ export interface ConditionsEvaluationData {
|
|||||||
view?: ConditionsEvaluationDataFromTo;
|
view?: ConditionsEvaluationDataFromTo;
|
||||||
state?: ConditionsEvaluationDataState;
|
state?: ConditionsEvaluationDataState;
|
||||||
}
|
}
|
||||||
interface ConditionsEvaluationResultTrue {
|
export interface ConditionsEvaluationResult {
|
||||||
result: true;
|
result: boolean;
|
||||||
data?: ConditionsEvaluationData;
|
data?: ConditionsEvaluationData;
|
||||||
}
|
}
|
||||||
interface ConditionsEvaluationResultFalse {
|
|
||||||
result: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ConditionsEvaluationResult =
|
|
||||||
| ConditionsEvaluationResultTrue
|
|
||||||
| ConditionsEvaluationResultFalse;
|
|
||||||
|
|
||||||
export type ConditionsListener = (result: ConditionsEvaluationResult) => void;
|
export type ConditionsListener = (result: ConditionsEvaluationResult) => void;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
import {
|
import {
|
||||||
ActionsManager,
|
ActionsManager,
|
||||||
Interaction,
|
Interaction,
|
||||||
InteractionName,
|
InteractionName,
|
||||||
} from '../../../src/card-controller/actions/actions-manager';
|
} from '../../../src/card-controller/actions/actions-manager';
|
||||||
|
import { TemplateRenderer } from '../../../src/card-controller/templates';
|
||||||
import { AdvancedCameraCardView } from '../../../src/config/types';
|
import { AdvancedCameraCardView } from '../../../src/config/types';
|
||||||
import { createLogAction } from '../../../src/utils/action';
|
import { createLogAction } from '../../../src/utils/action';
|
||||||
import {
|
import {
|
||||||
@@ -286,4 +288,34 @@ describe('ActionsManager', () => {
|
|||||||
expect(consoleSpy).not.toBeCalled();
|
expect(consoleSpy).not.toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render templates', async () => {
|
||||||
|
const action = createLogAction('{{ acc.camera }}');
|
||||||
|
|
||||||
|
const templateRenderer = mock<TemplateRenderer>();
|
||||||
|
templateRenderer.renderRecursively.mockReturnValue(action);
|
||||||
|
|
||||||
|
const api = createCardAPI();
|
||||||
|
const hass = createHASS();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
|
|
||||||
|
const conditionState = {
|
||||||
|
camera: 'camera',
|
||||||
|
};
|
||||||
|
vi.mocked(api.getConditionStateManager().getState).mockReturnValue(conditionState);
|
||||||
|
|
||||||
|
const manager = new ActionsManager(api, templateRenderer);
|
||||||
|
const config = { camera_image: 'camera-image' };
|
||||||
|
const triggerData = { view: { from: 'previous-view', to: 'view' } };
|
||||||
|
|
||||||
|
await manager.executeActions(action, {
|
||||||
|
config,
|
||||||
|
triggerData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(templateRenderer.renderRecursively).toBeCalledWith(hass, action, {
|
||||||
|
conditionState,
|
||||||
|
triggerData,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { renderTemplate } from 'ha-nunjucks';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { TemplateRenderer } from '../../src/card-controller/templates';
|
||||||
|
import { ConditionsEvaluationData, ConditionState } from '../../src/conditions/types';
|
||||||
|
import { createHASS } from '../test-utils';
|
||||||
|
|
||||||
|
// ha-nunjucks attempts to make websocket calls initially so mock it out.
|
||||||
|
vi.mock('ha-nunjucks');
|
||||||
|
|
||||||
|
describe('TemplateRenderer', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render data', () => {
|
||||||
|
const data = {
|
||||||
|
camera: '{{ acc.camera }}',
|
||||||
|
array: ['{{ acc.camera }}', '{{ acc.view }}'],
|
||||||
|
nested: {
|
||||||
|
camera: '{{ acc.camera }}',
|
||||||
|
},
|
||||||
|
number: 42,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(renderTemplate)
|
||||||
|
.mockReturnValueOnce('one')
|
||||||
|
.mockReturnValueOnce('two')
|
||||||
|
.mockReturnValueOnce('three')
|
||||||
|
.mockReturnValueOnce('four');
|
||||||
|
|
||||||
|
const renderer = new TemplateRenderer();
|
||||||
|
const hass = createHASS();
|
||||||
|
|
||||||
|
expect(renderer.renderRecursively(hass, data)).toEqual({
|
||||||
|
camera: 'one',
|
||||||
|
array: ['two', 'three'],
|
||||||
|
nested: {
|
||||||
|
camera: 'four',
|
||||||
|
},
|
||||||
|
number: 42,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include triggers', () => {
|
||||||
|
const conditionState: ConditionState = {
|
||||||
|
camera: 'camera',
|
||||||
|
view: 'view',
|
||||||
|
};
|
||||||
|
const triggerData: ConditionsEvaluationData = {
|
||||||
|
camera: {
|
||||||
|
to: 'camera',
|
||||||
|
from: 'previous-camera',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = new TemplateRenderer();
|
||||||
|
const hass = createHASS();
|
||||||
|
|
||||||
|
renderer.renderRecursively(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
key: 'value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditionState,
|
||||||
|
triggerData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(renderTemplate).toHaveBeenCalledWith(hass, 'value', {
|
||||||
|
acc: {
|
||||||
|
camera: 'camera',
|
||||||
|
view: 'view',
|
||||||
|
trigger: {
|
||||||
|
camera: {
|
||||||
|
to: 'camera',
|
||||||
|
from: 'previous-camera',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
advanced_camera_card: {
|
||||||
|
camera: 'camera',
|
||||||
|
view: 'view',
|
||||||
|
trigger: {
|
||||||
|
camera: {
|
||||||
|
to: 'camera',
|
||||||
|
from: 'previous-camera',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -72,6 +72,13 @@ const calculateFullCoverageThresholds = (): Record<string, Threshold> => {
|
|||||||
// ts-prune-ignore-next
|
// ts-prune-ignore-next
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
|
server: {
|
||||||
|
deps: {
|
||||||
|
// These dependencies import without extensions.
|
||||||
|
// Related: https://github.com/vitest-dev/vitest/issues/2313
|
||||||
|
inline: ['ha-nunjucks', 'ts-py-datetime'],
|
||||||
|
},
|
||||||
|
},
|
||||||
include: ['tests/**/*.test.ts'],
|
include: ['tests/**/*.test.ts'],
|
||||||
coverage: {
|
coverage: {
|
||||||
exclude: ['docs/**', 'tests/**', '.eslintrc.cjs'],
|
exclude: ['docs/**', 'tests/**', '.eslintrc.cjs'],
|
||||||
|
|||||||
@@ -2394,6 +2394,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"a-sync-waterfall@npm:^1.0.0":
|
||||||
|
version: 1.0.1
|
||||||
|
resolution: "a-sync-waterfall@npm:1.0.1"
|
||||||
|
checksum: 10c0/1c7b258da2c77eb1447dcc683afb10ca3dc8880de990562ccbb7b282538aba01e910345ce9e8500c1458272c7866b85fcfa5ca8159e33550b011ab5c586ec5a4
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"abab@npm:^2.0.6":
|
"abab@npm:^2.0.6":
|
||||||
version: 2.0.6
|
version: 2.0.6
|
||||||
resolution: "abab@npm:2.0.6"
|
resolution: "abab@npm:2.0.6"
|
||||||
@@ -2504,6 +2511,7 @@ __metadata:
|
|||||||
eslint-config-airbnb-base: "npm:^15.0.0"
|
eslint-config-airbnb-base: "npm:^15.0.0"
|
||||||
eslint-config-prettier: "npm:^9.1.0"
|
eslint-config-prettier: "npm:^9.1.0"
|
||||||
eslint-plugin-import: "npm:^2.29.1"
|
eslint-plugin-import: "npm:^2.29.1"
|
||||||
|
ha-nunjucks: "npm:^1.3.0"
|
||||||
home-assistant-js-websocket: "npm:^9.4.0"
|
home-assistant-js-websocket: "npm:^9.4.0"
|
||||||
js-yaml: "npm:^4.1.0"
|
js-yaml: "npm:^4.1.0"
|
||||||
jsdom: "npm:^21.1.2"
|
jsdom: "npm:^21.1.2"
|
||||||
@@ -2835,6 +2843,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"asap@npm:^2.0.3":
|
||||||
|
version: 2.0.6
|
||||||
|
resolution: "asap@npm:2.0.6"
|
||||||
|
checksum: 10c0/c6d5e39fe1f15e4b87677460bd66b66050cd14c772269cee6688824c1410a08ab20254bb6784f9afb75af9144a9f9a7692d49547f4d19d715aeb7c0318f3136d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"assertion-error@npm:^1.1.0":
|
"assertion-error@npm:^1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "assertion-error@npm:1.1.0"
|
resolution: "assertion-error@npm:1.1.0"
|
||||||
@@ -3485,6 +3500,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"commander@npm:^5.1.0":
|
||||||
|
version: 5.1.0
|
||||||
|
resolution: "commander@npm:5.1.0"
|
||||||
|
checksum: 10c0/da9d71dbe4ce039faf1fe9eac3771dca8c11d66963341f62602f7b66e36d2a3f8883407af4f9a37b1db1a55c59c0c1325f186425764c2e963dc1d67aec2a4b6d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"commander@npm:^6.2.1":
|
"commander@npm:^6.2.1":
|
||||||
version: 6.2.1
|
version: 6.2.1
|
||||||
resolution: "commander@npm:6.2.1"
|
resolution: "commander@npm:6.2.1"
|
||||||
@@ -3949,6 +3971,33 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"d3-array@npm:2 - 3":
|
||||||
|
version: 3.2.4
|
||||||
|
resolution: "d3-array@npm:3.2.4"
|
||||||
|
dependencies:
|
||||||
|
internmap: "npm:1 - 2"
|
||||||
|
checksum: 10c0/08b95e91130f98c1375db0e0af718f4371ccacef7d5d257727fe74f79a24383e79aba280b9ffae655483ffbbad4fd1dec4ade0119d88c4749f388641c8bf8c50
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"d3-time-format@npm:^4.1.0":
|
||||||
|
version: 4.1.0
|
||||||
|
resolution: "d3-time-format@npm:4.1.0"
|
||||||
|
dependencies:
|
||||||
|
d3-time: "npm:1 - 3"
|
||||||
|
checksum: 10c0/735e00fb25a7fd5d418fac350018713ae394eefddb0d745fab12bbff0517f9cdb5f807c7bbe87bb6eeb06249662f8ea84fec075f7d0cd68609735b2ceb29d206
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"d3-time@npm:1 - 3":
|
||||||
|
version: 3.1.0
|
||||||
|
resolution: "d3-time@npm:3.1.0"
|
||||||
|
dependencies:
|
||||||
|
d3-array: "npm:2 - 3"
|
||||||
|
checksum: 10c0/a984f77e1aaeaa182679b46fbf57eceb6ebdb5f67d7578d6f68ef933f8eeb63737c0949991618a8d29472dbf43736c7d7f17c452b2770f8c1271191cba724ca1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"data-uri-to-buffer@npm:^6.0.2":
|
"data-uri-to-buffer@npm:^6.0.2":
|
||||||
version: 6.0.2
|
version: 6.0.2
|
||||||
resolution: "data-uri-to-buffer@npm:6.0.2"
|
resolution: "data-uri-to-buffer@npm:6.0.2"
|
||||||
@@ -5714,6 +5763,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ha-nunjucks@npm:^1.3.0":
|
||||||
|
version: 1.3.0
|
||||||
|
resolution: "ha-nunjucks@npm:1.3.0"
|
||||||
|
dependencies:
|
||||||
|
home-assistant-js-websocket: "npm:latest"
|
||||||
|
nunjucks: "npm:latest"
|
||||||
|
ts-py-datetime: "npm:latest"
|
||||||
|
checksum: 10c0/9c346ee9866f748b41d98dbefefd46ddb0a43f7d563e4456e000047e273e5441e5a15321ba8684325a81d746a9e9c32c5c0f4f1a58637291babebcae45329442
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"handlebars@npm:^4.7.7":
|
"handlebars@npm:^4.7.7":
|
||||||
version: 4.7.8
|
version: 4.7.8
|
||||||
resolution: "handlebars@npm:4.7.8"
|
resolution: "handlebars@npm:4.7.8"
|
||||||
@@ -5817,7 +5877,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"home-assistant-js-websocket@npm:^9.4.0":
|
"home-assistant-js-websocket@npm:^9.4.0, home-assistant-js-websocket@npm:latest":
|
||||||
version: 9.4.0
|
version: 9.4.0
|
||||||
resolution: "home-assistant-js-websocket@npm:9.4.0"
|
resolution: "home-assistant-js-websocket@npm:9.4.0"
|
||||||
checksum: 10c0/9c8a5ff903648ebe7856f62a58af53e3d89dd24a30c600a2145ed518cd5f5f2218345048cfc42bdc3ee5d6f2994da3948765c77b6012ed56cf4546173a1a1de1
|
checksum: 10c0/9c8a5ff903648ebe7856f62a58af53e3d89dd24a30c600a2145ed518cd5f5f2218345048cfc42bdc3ee5d6f2994da3948765c77b6012ed56cf4546173a1a1de1
|
||||||
@@ -6145,6 +6205,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"internmap@npm:1 - 2":
|
||||||
|
version: 2.0.3
|
||||||
|
resolution: "internmap@npm:2.0.3"
|
||||||
|
checksum: 10c0/8cedd57f07bbc22501516fbfc70447f0c6812871d471096fad9ea603516eacc2137b633633daf432c029712df0baefd793686388ddf5737e3ea15074b877f7ed
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"intl-messageformat@npm:^9.11.1":
|
"intl-messageformat@npm:^9.11.1":
|
||||||
version: 9.13.0
|
version: 9.13.0
|
||||||
resolution: "intl-messageformat@npm:9.13.0"
|
resolution: "intl-messageformat@npm:9.13.0"
|
||||||
@@ -8239,6 +8306,24 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"nunjucks@npm:latest":
|
||||||
|
version: 3.2.4
|
||||||
|
resolution: "nunjucks@npm:3.2.4"
|
||||||
|
dependencies:
|
||||||
|
a-sync-waterfall: "npm:^1.0.0"
|
||||||
|
asap: "npm:^2.0.3"
|
||||||
|
commander: "npm:^5.1.0"
|
||||||
|
peerDependencies:
|
||||||
|
chokidar: ^3.3.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
chokidar:
|
||||||
|
optional: true
|
||||||
|
bin:
|
||||||
|
nunjucks-precompile: bin/precompile
|
||||||
|
checksum: 10c0/7fe5197559b7c09972c79e2a86f9c093459b9075bc9b41134cd2bc599ae93567b53bd09d472a748edc736192d9ccd2998aa8c20cfcbe6a3fffd281f91897c888
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"nwsapi@npm:^2.2.4":
|
"nwsapi@npm:^2.2.4":
|
||||||
version: 2.2.7
|
version: 2.2.7
|
||||||
resolution: "nwsapi@npm:2.2.7"
|
resolution: "nwsapi@npm:2.2.7"
|
||||||
@@ -11198,6 +11283,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ts-py-datetime@npm:latest":
|
||||||
|
version: 0.10.0
|
||||||
|
resolution: "ts-py-datetime@npm:0.10.0"
|
||||||
|
dependencies:
|
||||||
|
d3-time-format: "npm:^4.1.0"
|
||||||
|
checksum: 10c0/2e82ba1b02edd566600052a6c90ea06d1eeeff7557e75cfba6c80bc3898b8ecbd825b7b51e9690c68ab97dc8c12d362e3e509896c617c6a556f4d6cfe9cf3579
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"tsconfig-paths@npm:^3.15.0":
|
"tsconfig-paths@npm:^3.15.0":
|
||||||
version: 3.15.0
|
version: 3.15.0
|
||||||
resolution: "tsconfig-paths@npm:3.15.0"
|
resolution: "tsconfig-paths@npm:3.15.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user