From 7fb710d45d011db7a322f0d5241c2984c4b48c29 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Fri, 14 Feb 2025 07:23:19 -0800 Subject: [PATCH] feat: Add support for templates in actions (#1891) - Closes #1878 --- docs/configuration/actions/_sidebar.md | 1 + docs/configuration/actions/custom/_sidebar.md | 1 + docs/configuration/actions/stock/README.md | 6 +- docs/configuration/actions/stock/_sidebar.md | 1 + docs/configuration/actions/templates.md | 57 +++++++++++ docs/configuration/view.md | 2 +- docs/examples.md | 55 +++++++++++ package.json | 1 + rollup.config.js | 12 +++ .../actions/actions-manager.ts | 34 +++++-- src/card-controller/automations-manager.ts | 6 +- src/card-controller/controller.ts | 3 +- src/card-controller/templates/index.ts | 80 ++++++++++++++++ src/conditions/types.ts | 11 +-- .../actions/actions-manager.test.ts | 32 +++++++ tests/templates/index.test.ts | 93 ++++++++++++++++++ vite.config.ts | 7 ++ yarn.lock | 96 ++++++++++++++++++- 18 files changed, 474 insertions(+), 24 deletions(-) create mode 100644 docs/configuration/actions/templates.md create mode 100644 src/card-controller/templates/index.ts create mode 100644 tests/templates/index.test.ts diff --git a/docs/configuration/actions/_sidebar.md b/docs/configuration/actions/_sidebar.md index 0da30b6e..c97d52cf 100644 --- a/docs/configuration/actions/_sidebar.md +++ b/docs/configuration/actions/_sidebar.md @@ -3,6 +3,7 @@ - [`actions`](README.md) - [Custom Actions](./custom/README.md) - [Stock Actions](./stock/README.md) + - [Templates](./templates.md) - [`automations`](../automations.md) - [`cameras`](../cameras/README.md) - [`conditions`](../conditions.md) diff --git a/docs/configuration/actions/custom/_sidebar.md b/docs/configuration/actions/custom/_sidebar.md index 2a59a918..13896495 100644 --- a/docs/configuration/actions/custom/_sidebar.md +++ b/docs/configuration/actions/custom/_sidebar.md @@ -3,6 +3,7 @@ - [`actions`](../README.md) - [Custom Actions](README.md) - [Stock Actions](../stock/README.md) + - [Templates](../templates.md) - [`automations`](../../automations.md) - [`cameras`](../../cameras/README.md) - [`conditions`](../../conditions.md) diff --git a/docs/configuration/actions/stock/README.md b/docs/configuration/actions/stock/README.md index 805d7219..b2daa30c 100644 --- a/docs/configuration/actions/stock/README.md +++ b/docs/configuration/actions/stock/README.md @@ -73,13 +73,13 @@ elements: action: toggle - type: icon icon: mdi:numeric-3-box - title: Call Service action + title: Perform Action / Call Service action style: left: 200px top: 150px tap_action: - action: call-service - service: homeassistant.toggle + action: perform-action + perform_action: homeassistant.toggle data: entity_id: light.office_main_lights - type: icon diff --git a/docs/configuration/actions/stock/_sidebar.md b/docs/configuration/actions/stock/_sidebar.md index 302cfa94..1bdc196b 100644 --- a/docs/configuration/actions/stock/_sidebar.md +++ b/docs/configuration/actions/stock/_sidebar.md @@ -3,6 +3,7 @@ - [`actions`](../README.md) - [Custom Actions](../custom/README.md) - [Stock Actions](README.md) + - [Templates](../templates.md) - [`automations`](../../automations.md) - [`cameras`](../../cameras/README.md) - [`conditions`](../../conditions.md) diff --git a/docs/configuration/actions/templates.md b/docs/configuration/actions/templates.md new file mode 100644 index 00000000..a0af4476 --- /dev/null +++ b/docs/configuration/actions/templates.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. diff --git a/docs/configuration/view.md b/docs/configuration/view.md index 6ef695ef..2ff68403 100644 --- a/docs/configuration/view.md +++ b/docs/configuration/view.md @@ -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. | | `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`). | -| `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. | | `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. | diff --git a/docs/examples.md b/docs/examples.md index c8d97aae..9f75622e 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -947,6 +947,61 @@ menu: 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 You can control the card itself with the `custom:advanced-camera-card-action` action. diff --git a/package.json b/package.json index 1befd109..a113025b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "date-fns-tz": "^3.1.3", "embla-carousel": "^8.3.0", "embla-carousel-wheel-gestures": "^8.0.1", + "ha-nunjucks": "^1.3.0", "home-assistant-js-websocket": "^9.4.0", "js-yaml": "^4.1.0", "keycharm": "^0.4.0", diff --git a/rollup.config.js b/rollup.config.js index 134d04df..08b86957 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -89,6 +89,8 @@ const outputEntryTemplate = { sourcemap: dev, }; +const CIRCULAR_DEPENDENCY_IGNORE_REGEXP = /(ha-nunjucks|ts-py-datetime)/; + /** * @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/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; diff --git a/src/card-controller/actions/actions-manager.ts b/src/card-controller/actions/actions-manager.ts index 65807af7..db052e93 100644 --- a/src/card-controller/actions/actions-manager.ts +++ b/src/card-controller/actions/actions-manager.ts @@ -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, ): Promise => { - 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 { - 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, }); diff --git a/src/card-controller/automations-manager.ts b/src/card-controller/automations-manager.ts index cf62a228..c051c3fb 100644 --- a/src/card-controller/automations-manager.ts +++ b/src/card-controller/automations-manager.ts @@ -64,6 +64,7 @@ export class AutomationsManager { const runActions = async (actions: AutomationActions): Promise => { ++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); diff --git a/src/card-controller/controller.ts b/src/card-controller/controller.ts index 5d7466bb..df19b1ec 100644 --- a/src/card-controller/controller.ts +++ b/src/card-controller/controller.ts @@ -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); diff --git a/src/card-controller/templates/index.ts b/src/card-controller/templates/index.ts new file mode 100644 index 00000000..94e18297 --- /dev/null +++ b/src/card-controller/templates/index.ts @@ -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; + } +} diff --git a/src/conditions/types.ts b/src/conditions/types.ts index 088fe0a2..7a9c942c 100644 --- a/src/conditions/types.ts +++ b/src/conditions/types.ts @@ -48,17 +48,10 @@ export interface ConditionsEvaluationData { view?: ConditionsEvaluationDataFromTo; state?: ConditionsEvaluationDataState; } -interface ConditionsEvaluationResultTrue { - result: true; +export interface ConditionsEvaluationResult { + result: boolean; data?: ConditionsEvaluationData; } -interface ConditionsEvaluationResultFalse { - result: false; -} - -export type ConditionsEvaluationResult = - | ConditionsEvaluationResultTrue - | ConditionsEvaluationResultFalse; export type ConditionsListener = (result: ConditionsEvaluationResult) => void; diff --git a/tests/card-controller/actions/actions-manager.test.ts b/tests/card-controller/actions/actions-manager.test.ts index ba8f127f..352febfa 100644 --- a/tests/card-controller/actions/actions-manager.test.ts +++ b/tests/card-controller/actions/actions-manager.test.ts @@ -1,9 +1,11 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { mock } from 'vitest-mock-extended'; import { ActionsManager, Interaction, InteractionName, } from '../../../src/card-controller/actions/actions-manager'; +import { TemplateRenderer } from '../../../src/card-controller/templates'; import { AdvancedCameraCardView } from '../../../src/config/types'; import { createLogAction } from '../../../src/utils/action'; import { @@ -286,4 +288,34 @@ describe('ActionsManager', () => { expect(consoleSpy).not.toBeCalled(); }); }); + + it('should render templates', async () => { + const action = createLogAction('{{ acc.camera }}'); + + const templateRenderer = mock(); + 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, + }); + }); }); diff --git a/tests/templates/index.test.ts b/tests/templates/index.test.ts new file mode 100644 index 00000000..29c9800d --- /dev/null +++ b/tests/templates/index.test.ts @@ -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', + }, + }, + }, + }); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index ea40a38f..07b6d2fc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -72,6 +72,13 @@ const calculateFullCoverageThresholds = (): Record => { // ts-prune-ignore-next export default defineConfig({ 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'], coverage: { exclude: ['docs/**', 'tests/**', '.eslintrc.cjs'], diff --git a/yarn.lock b/yarn.lock index 9ce38cc9..4b528376 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2394,6 +2394,13 @@ __metadata: languageName: node 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": version: 2.0.6 resolution: "abab@npm:2.0.6" @@ -2504,6 +2511,7 @@ __metadata: eslint-config-airbnb-base: "npm:^15.0.0" eslint-config-prettier: "npm:^9.1.0" eslint-plugin-import: "npm:^2.29.1" + ha-nunjucks: "npm:^1.3.0" home-assistant-js-websocket: "npm:^9.4.0" js-yaml: "npm:^4.1.0" jsdom: "npm:^21.1.2" @@ -2835,6 +2843,13 @@ __metadata: languageName: node 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": version: 1.1.0 resolution: "assertion-error@npm:1.1.0" @@ -3485,6 +3500,13 @@ __metadata: languageName: node 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": version: 6.2.1 resolution: "commander@npm:6.2.1" @@ -3949,6 +3971,33 @@ __metadata: languageName: node 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": version: 6.0.2 resolution: "data-uri-to-buffer@npm:6.0.2" @@ -5714,6 +5763,17 @@ __metadata: languageName: node 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": version: 4.7.8 resolution: "handlebars@npm:4.7.8" @@ -5817,7 +5877,7 @@ __metadata: languageName: node 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 resolution: "home-assistant-js-websocket@npm:9.4.0" checksum: 10c0/9c8a5ff903648ebe7856f62a58af53e3d89dd24a30c600a2145ed518cd5f5f2218345048cfc42bdc3ee5d6f2994da3948765c77b6012ed56cf4546173a1a1de1 @@ -6145,6 +6205,13 @@ __metadata: languageName: node 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": version: 9.13.0 resolution: "intl-messageformat@npm:9.13.0" @@ -8239,6 +8306,24 @@ __metadata: languageName: node 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": version: 2.2.7 resolution: "nwsapi@npm:2.2.7" @@ -11198,6 +11283,15 @@ __metadata: languageName: node 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": version: 3.15.0 resolution: "tsconfig-paths@npm:3.15.0"