From 64a909ead09670c5839787f865bb8a2e7529a333 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sun, 21 Jan 2024 11:30:23 -0800 Subject: [PATCH] Add editor and migration support. --- README.md | 38 ++--- src/card-controller/triggers-manager.ts | 36 +++-- src/card-controller/types.ts | 1 - src/card.ts | 2 +- src/config-mgmt.ts | 9 ++ src/config/types.ts | 94 +++-------- src/const.ts | 13 +- src/editor.ts | 148 +++++++++++++----- src/localize/languages/en.json | 25 ++- src/localize/languages/it.json | 25 ++- src/localize/languages/pt-BR.json | 25 ++- src/localize/languages/pt-PT.json | 25 ++- .../card-controller/triggers-manager.test.ts | 98 +++++++----- tests/config-mgmt.test.ts | 45 ++++++ tests/config/types.test.ts | 23 +-- vite.config.ts | 8 +- 16 files changed, 385 insertions(+), 230 deletions(-) diff --git a/README.md b/README.md index 218f38ec..8891b1d3 100644 --- a/README.md +++ b/README.md @@ -432,32 +432,16 @@ Scan mode tracks Home Assistant state *changes* -- when the card is first starte | Option | Default | Overridable | Description | | - | - | - | - | | `enabled` | `false` | :white_check_mark: | Whether to enable scan mode. | -| `interaction_mode` | `inactive` | :white_check_mark: | Whether actions should be taken when the card is being interacted with. If `all`, actions will always be taken regardless. If `inactive` actions will only be taken if the card has *not* had human interaction recently (as defined by `view.timeout_seconds`). If `active` actions will only be taken if the card *has* had human interaction recently. This does not stop triggering itself (i.e. border will still pulse if `trigger_show_status` is true) but rather just prevents the actions being performed.| -| `trigger_filter_camera` | `all` | :white_check_mark: | If set to `all` the camera will be triggered regardless of which camera is currently selected, if set to `selected` the camera will only trigger if that camera is already selected.| -| `trigger_show_status` | `true` | :white_check_mark: | Whether or not the card should show a visual indication that it is triggered (a pulsing border around the card edge). | -| `trigger_action` | Selects triggered camera in `live` view (see below) | :white_check_mark: | An action or list of actions that are executed when a camera is triggered. May be set to `null` for no action.| -| `untrigger_reset` | `true` | :white_check_mark: | Whether or not to reset the view to the default after untriggering. | +| `filter_selected_camera` | `false` | :white_check_mark: | If set to `true` will only trigger on the currently selected camera.| +| `show_trigger_status` | `true` | :white_check_mark: | Whether or not the card should show a visual indication that it is triggered (a pulsing border around the card edge). | | `untrigger_seconds` | `0` | :white_check_mark: | The number of seconds to wait after all entities are inactive before untriggering. | -| `untrigger_action` | Selects default view and camera (see below) | :white_check_mark: | An action or list of actions that are executed when a camera is triggered. May bet to `null` for no action.| +| `actions` | | :white_check_mark: | The actions to take when scan mode triggers (see below). | -##### View: Scan Mode default actions +#### View: Scan Mode Actions configuration -The default `trigger_action` is: - -```yaml - - action: custom:frigate-card-action - frigate_card_action: camera_select - triggered: true - - action: custom:frigate-card-action - frigate_card_action: live -``` - -The default `untrigger_action` is: - -```yaml - - action: custom:frigate-card-action - frigate_card_action: default -``` +| `trigger` | `live` | :white_check_mark | When `live` the trigger will select the triggered camera in `live` view, when `none` will take no action. | +| `untrigger` | `default` | :white_check_mark | When `default` the untrigger will return to the default view and camera, when `none` will take no action. | +| `interaction_mode` | `inactive` | :white_check_mark: | Whether actions should be taken when the card is being interacted with. If `all`, actions will always be taken regardless. If `inactive` actions will only be taken if the card has *not* had human interaction recently (as defined by `view.timeout_seconds`). If `active` actions will only be taken if the card *has* had human interaction recently. This does not stop triggering itself (i.e. border will still pulse if `show_trigger_status` is true) but rather just prevents the actions being performed.| ### Menu Options @@ -1935,9 +1919,13 @@ view: dark_mode: 'off' scan: enabled: false - trigger_show_status: true - untrigger_reset: true + show_trigger_status: true + filter_selected_camera: false untrigger_seconds: 0 + actions: + interaction_mode: 'inactive' as const, + trigger: 'live' as const, + untrigger: 'default' as const, actions: entity: light.office_main_lights tap_action: diff --git a/src/card-controller/triggers-manager.ts b/src/card-controller/triggers-manager.ts index 78ff1295..dbd17869 100644 --- a/src/card-controller/triggers-manager.ts +++ b/src/card-controller/triggers-manager.ts @@ -75,8 +75,8 @@ export class TriggersManager { public updateView(oldView?: View | null): void { if (oldView?.camera !== this._api.getViewManager().getView()?.camera) { // If the view changes, a new camera may have been selected, which may - // mean a trigger is required (in the case that `trigger_filter_camera` - // has been set to `selected`). + // mean a trigger is required (in the case that `filter_selected_camera` + // is true). this._evaluateTriggers(); } } @@ -91,12 +91,11 @@ export class TriggersManager { for (const cameraID of this._triggeredState.keys()) { if ( !this._triggeredCameras.has(cameraID) && - (scanConfig.trigger_filter_camera === 'all' || - (scanConfig.trigger_filter_camera === 'selected' && - cameraID === this._api.getViewManager().getView()?.camera)) + (!scanConfig.filter_selected_camera || + cameraID === this._api.getViewManager().getView()?.camera) ) { this._triggeredCameras.set(cameraID, now); - this._triggerAction(); + this._triggerAction(cameraID); } } @@ -107,23 +106,26 @@ export class TriggersManager { } } - protected _hasAllowableInteractionState(): boolean { + protected _hasAllowableInteractionStateForAction(): boolean { const scanConfig = this._api.getConfigManager().getConfig()?.view.scan; const hasInteraction = this._api.getInteractionManager().hasInteraction(); return ( !!scanConfig && - (scanConfig.interaction_mode === 'all' || - (scanConfig.interaction_mode === 'active' && hasInteraction) || - (scanConfig.interaction_mode === 'inactive' && !hasInteraction)) + (scanConfig.actions.interaction_mode === 'all' || + (scanConfig.actions.interaction_mode === 'active' && hasInteraction) || + (scanConfig.actions.interaction_mode === 'inactive' && !hasInteraction)) ); } - protected _triggerAction(): void { - const action = this._api.getConfigManager().getConfig()?.view.scan.trigger_action; + protected _triggerAction(cameraID: string): void { + const action = this._api.getConfigManager().getConfig()?.view.scan.actions.trigger; - if (action && this._hasAllowableInteractionState()) { - this._api.getActionsManager().executeActions(action); + if (action === 'live' && this._hasAllowableInteractionStateForAction()) { + this._api.getViewManager().setViewByParameters({ + viewName: 'live', + cameraID: cameraID, + }); } // Must update master element to add border pulsing. @@ -131,10 +133,10 @@ export class TriggersManager { } protected _untriggerAction(cameraID: string): void { - const action = this._api.getConfigManager().getConfig()?.view.scan.untrigger_action; + const action = this._api.getConfigManager().getConfig()?.view.scan.actions.untrigger; - if (action && this._hasAllowableInteractionState()) { - this._api.getActionsManager().executeActions(action); + if (action === 'default' && this._hasAllowableInteractionStateForAction()) { + this._api.getViewManager().setViewDefault(); } this._triggeredCameras.delete(cameraID); this._deleteTimer(cameraID); diff --git a/src/card-controller/types.ts b/src/card-controller/types.ts index a962d08a..068439ee 100644 --- a/src/card-controller/types.ts +++ b/src/card-controller/types.ts @@ -197,7 +197,6 @@ export interface CardStyleAPI { } export interface CardTriggersAPI { - getActionsManager(): ActionsManager; getCameraManager(): CameraManager; getCardElementManager(): CardElementManager; getConfigManager(): ConfigManager; diff --git a/src/card.ts b/src/card.ts index 1f50e2b4..527b710a 100644 --- a/src/card.ts +++ b/src/card.ts @@ -241,7 +241,7 @@ class FrigateCard extends LitElement { const cardClasses = { triggered: !!this._controller.getTriggersManager().isTriggered() && - !!this._config?.view.scan.trigger_show_status, + !!this._config?.view.scan.show_trigger_status, }; const mainClasses = { main: true, diff --git a/src/config-mgmt.ts b/src/config-mgmt.ts index fc81696a..8a9a8234 100644 --- a/src/config-mgmt.ts +++ b/src/config-mgmt.ts @@ -13,6 +13,7 @@ import { CONF_MEDIA_GALLERY, CONF_MENU_BUTTONS_CAMERA_UI, CONF_OVERRIDES, + CONF_VIEW_SCAN_ACTIONS_UNTRIGGER, } from './const'; import { arrayify } from './utils/basic'; @@ -474,4 +475,12 @@ const UPGRADES = [ ); }, upgradePTZElementsToLive(), + upgradeMoveToWithOverrides( + 'view.scan.untrigger_reset', + CONF_VIEW_SCAN_ACTIONS_UNTRIGGER, + { + // Delete the value if it's set to the default. + transform: (val) => (val ? 'default' : null), + }, + ), ]; diff --git a/src/config/types.ts b/src/config/types.ts index f618ea74..3c70cf38 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,42 +1,3 @@ -// TODO: Config migration -// TODO: Editor support for scan mode changes -// TODO: Consider this: - -/* -// NOW ===> -view: - scan: - enabled: true - trigger_show_status: true, - trigger_action: - - action: custom:frigate-card-action - frigate_card_action: camera_select - triggered: true - - action: custom:frigate-card-action - frigate_card_action: live - untrigger_action: - - action: custom:frigate-card-action - interaction_mode: inactive - trigger_filter_camera: all - untrigger_seconds: 0 - -view: - scan: - enabled: true - -// TO - -view: - scan: - enabled: true - trigger_show_status: true - actions: - live | live-reset | none - interaction: inactive | active | all - trigger_filter_camera: all - untrigger_seconds: 0 -automations: -*/ import { CallServiceActionConfig, @@ -1160,47 +1121,36 @@ const viewConfigDefault = { dark_mode: 'off' as const, scan: { enabled: false, - trigger_show_status: true, - trigger_action: [ - { - action: 'custom:frigate-card-action' as const, - frigate_card_action: 'camera_select' as const, - triggered: true, - }, - { - action: 'custom:frigate-card-action' as const, - frigate_card_action: 'live' as const, - }, - ], - untrigger_action: { - action: 'custom:frigate-card-action' as const, - frigate_card_action: 'default' as const, + show_trigger_status: true, + filter_selected_camera: false, + actions: { + interaction_mode: 'inactive' as const, + trigger: 'live' as const, + untrigger: 'default' as const, }, - interaction_mode: 'inactive' as const, - trigger_filter_camera: 'all' as const, untrigger_seconds: 0, }, }; -const scanSchema = z.object({ +export const scanSchema = z.object({ enabled: z.boolean().default(viewConfigDefault.scan.enabled), - interaction_mode: z - .enum(['all', 'inactive', 'active']) - .default(viewConfigDefault.scan.interaction_mode), - trigger_filter_camera: z - .enum(['all', 'selected']) - .default(viewConfigDefault.scan.trigger_filter_camera), - trigger_show_status: z.boolean().default(viewConfigDefault.scan.trigger_show_status), - trigger_action: actionSchema - .or(actionSchema.array()) - .nullable() - .default(viewConfigDefault.scan.trigger_action), + filter_selected_camera: z + .boolean() + .default(viewConfigDefault.scan.filter_selected_camera), + show_trigger_status: z.boolean().default(viewConfigDefault.scan.show_trigger_status), - untrigger_action: actionSchema - .or(actionSchema.array()) - .nullable() - .default(viewConfigDefault.scan.untrigger_action), + actions: z + .object({ + interaction_mode: z + .enum(['all', 'inactive', 'active']) + .default(viewConfigDefault.scan.actions.interaction_mode), + trigger: z.enum(['live', 'none']).default(viewConfigDefault.scan.actions.trigger), + untrigger: z + .enum(['default', 'none']) + .default(viewConfigDefault.scan.actions.untrigger), + }) + .default(viewConfigDefault.scan.actions), untrigger_seconds: z.number().default(viewConfigDefault.scan.untrigger_seconds), }); export type ScanOptions = z.infer; diff --git a/src/const.ts b/src/const.ts index 1d6c4c62..16f16147 100644 --- a/src/const.ts +++ b/src/const.ts @@ -77,10 +77,19 @@ export const CONF_VIEW_UPDATE_FORCE = `${CONF_VIEW}.update_force` as const; export const CONF_VIEW_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const; export const CONF_VIEW_SCAN = `${CONF_VIEW}.scan` as const; export const CONF_VIEW_SCAN_ENABLED = `${CONF_VIEW_SCAN}.enabled` as const; -export const CONF_VIEW_SCAN_TRIGGER_SHOW_STATUS = - `${CONF_VIEW_SCAN}.trigger_show_status` as const; +export const CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS = + `${CONF_VIEW_SCAN}.show_trigger_status` as const; +export const CONF_VIEW_SCAN_FILTER_SELECTED_CAMERA = + `${CONF_VIEW_SCAN}.filter_selected_camera` as const; export const CONF_VIEW_SCAN_UNTRIGGER_SECONDS = `${CONF_VIEW_SCAN}.untrigger_seconds` as const; +export const CONF_VIEW_SCAN_ACTIONS = `${CONF_VIEW_SCAN}.actions` as const; +export const CONF_VIEW_SCAN_ACTIONS_TRIGGER = + `${CONF_VIEW_SCAN_ACTIONS}.trigger` as const; +export const CONF_VIEW_SCAN_ACTIONS_UNTRIGGER = + `${CONF_VIEW_SCAN_ACTIONS}.untrigger` as const; +export const CONF_VIEW_SCAN_ACTIONS_INTERACTION_MODE = + `${CONF_VIEW_SCAN_ACTIONS}.interaction_mode` as const; export const CONF_MEDIA_GALLERY = 'media_gallery' as const; export const CONF_MEDIA_GALLERY_CONTROLS_FILTER_MODE = diff --git a/src/editor.ts b/src/editor.ts index 64ab9401..d3be3939 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -181,8 +181,13 @@ import { CONF_VIEW_DARK_MODE, CONF_VIEW_DEFAULT, CONF_VIEW_SCAN, + CONF_VIEW_SCAN_ACTIONS, + CONF_VIEW_SCAN_ACTIONS_INTERACTION_MODE, + CONF_VIEW_SCAN_ACTIONS_TRIGGER, + CONF_VIEW_SCAN_ACTIONS_UNTRIGGER, CONF_VIEW_SCAN_ENABLED, - CONF_VIEW_SCAN_TRIGGER_SHOW_STATUS, + CONF_VIEW_SCAN_FILTER_SELECTED_CAMERA, + CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS, CONF_VIEW_SCAN_UNTRIGGER_SECONDS, CONF_VIEW_TIMEOUT_SECONDS, CONF_VIEW_UPDATE_CYCLE_CAMERA, @@ -237,6 +242,7 @@ const MENU_PERFORMANCE_FEATURES = 'performance.features'; const MENU_PERFORMANCE_STYLE = 'performance.style'; const MENU_TIMELINE_CONTROLS_THUMBNAILS = 'timeline.controls.thumbnails'; const MENU_VIEW_SCAN = 'scan'; +const MENU_VIEW_SCAN_ACTIONS = 'scan.actions'; interface EditorOptionsSet { icon: string; @@ -620,6 +626,46 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor }, ]; + protected _scanActionsInteractionModes: EditorSelectOption[] = [ + { value: '', label: '' }, + { + value: 'all', + label: localize('config.view.scan.actions.interaction_modes.all'), + }, + { + value: 'inactive', + label: localize('config.view.scan.actions.interaction_modes.inactive'), + }, + { + value: 'active', + label: localize('config.view.scan.actions.interaction_modes.active'), + }, + ]; + + protected _scanActionsTrigger: EditorSelectOption[] = [ + { value: '', label: '' }, + { + value: 'live', + label: localize('config.view.scan.actions.triggers.live'), + }, + { + value: 'none', + label: localize('config.view.scan.actions.triggers.none'), + }, + ]; + + protected _scanActionsUntrigger: EditorSelectOption[] = [ + { value: '', label: '' }, + { + value: 'default', + label: localize('config.view.scan.actions.untriggers.default'), + }, + { + value: 'none', + label: localize('config.view.scan.actions.untriggers.none'), + }, + ]; + public setConfig(config: RawFrigateCardConfig): void { // Note: This does not use Zod to parse the configuration, so it may be // partially or completely invalid. It's more useful to have a partially @@ -876,44 +922,68 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor } protected _renderViewScanMenu(): TemplateResult { - const submenuClasses = { - submenu: true, - selected: !!this._expandedMenus[MENU_VIEW_SCAN], - }; - return html` -
- - ${this._expandedMenus[MENU_VIEW_SCAN] - ? html`
- ${this._renderSwitch( - CONF_VIEW_SCAN_ENABLED, - this._defaults.view.scan.enabled, - { - label: localize(`config.${CONF_VIEW_SCAN_ENABLED}`), - }, - )} - ${this._renderSwitch( - CONF_VIEW_SCAN_TRIGGER_SHOW_STATUS, - this._defaults.view.scan.trigger_show_status, - { - label: localize(`config.${CONF_VIEW_SCAN_TRIGGER_SHOW_STATUS}`), - }, - )} - ${this._renderNumberInput(CONF_VIEW_SCAN_UNTRIGGER_SECONDS, { - default: this._defaults.view.scan.untrigger_seconds, - })} -
` - : ''} -
- `; + return this._putInSubmenu( + MENU_VIEW_SCAN, + true, + `config.${CONF_VIEW_SCAN}.editor_label`, + { name: 'mdi:target-account' }, + html` + ${this._renderSwitch(CONF_VIEW_SCAN_ENABLED, this._defaults.view.scan.enabled, { + label: localize(`config.${CONF_VIEW_SCAN_ENABLED}`), + })} + ${this._renderSwitch( + CONF_VIEW_SCAN_FILTER_SELECTED_CAMERA, + this._defaults.view.scan.filter_selected_camera, + { + label: localize(`config.${CONF_VIEW_SCAN_FILTER_SELECTED_CAMERA}`), + }, + )} + ${this._renderSwitch( + CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS, + this._defaults.view.scan.show_trigger_status, + { + label: localize(`config.${CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS}`), + }, + )} + ${this._renderNumberInput(CONF_VIEW_SCAN_UNTRIGGER_SECONDS, { + default: this._defaults.view.scan.untrigger_seconds, + })} + ${this._renderOptionSelector( + CONF_VIEW_SCAN_ACTIONS_TRIGGER, + this._scanActionsTrigger, + { + label: localize('config.view.scan.actions.trigger'), + }, + )} + ${this._putInSubmenu( + MENU_VIEW_SCAN_ACTIONS, + true, + `config.${CONF_VIEW_SCAN_ACTIONS}.editor_label`, + { name: 'mdi:cogs' }, + html` ${this._renderOptionSelector( + CONF_VIEW_SCAN_ACTIONS_TRIGGER, + this._scanActionsTrigger, + { + label: localize('config.view.scan.actions.trigger'), + }, + )} + ${this._renderOptionSelector( + CONF_VIEW_SCAN_ACTIONS_UNTRIGGER, + this._scanActionsUntrigger, + { + label: localize('config.view.scan.actions.untrigger'), + }, + )} + ${this._renderOptionSelector( + CONF_VIEW_SCAN_ACTIONS_INTERACTION_MODE, + this._scanActionsInteractionModes, + { + label: localize('config.view.scan.actions.interaction_mode'), + }, + )}`, + )} + `, + ); } /** diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 6d93d174..d4f7fd10 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -385,10 +385,29 @@ }, "default": "Default view", "scan": { + "actions": { + "editor_label": "Scan mode actions", + "interaction_mode": "How to handle actions when the card has human interaction", + "interaction_modes": { + "active": "Only trigger actions when card has human interaction", + "all": "Trigger actions regardless of human interaction", + "inactive": "Only trigger actions when card has no human interaction" + }, + "trigger": "Trigger action", + "triggers": { + "live": "Change to live", + "none": "Take no action" + }, + "untrigger": "Untrigger action", + "untriggers": { + "default": "Change to default view/camera", + "none": "Take no action" + } + }, + "editor_label": "Scan mode", + "filter_selected_camera": "Only trigger on selected camera", "enabled": "Scan mode enabled", - "scan_mode": "Scan mode", - "trigger_show_status": "Show pulsing border when triggered", - "untrigger_reset": "Reset the view to default after untrigger", + "show_trigger_status": "Show pulsing border when triggered", "untrigger_seconds": "Seconds after inactive state change to untrigger" }, "timeout_seconds": "Reset to default view X seconds after user action (0=never)", diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json index 363c00c4..b5940789 100644 --- a/src/localize/languages/it.json +++ b/src/localize/languages/it.json @@ -381,10 +381,29 @@ }, "default": "Visualizzazione predefinita", "scan": { + "actions": { + "editor_label": "", + "interaction_mode": "", + "interaction_modes": { + "active": "", + "all": "", + "inactive": "" + }, + "trigger": "", + "triggers": { + "live": "", + "none": "" + }, + "untrigger": "", + "untriggers": { + "default": "", + "none": "" + } + }, + "editor_label": "Modalità di scansione", "enabled": "Modalità di scansione abilitata", - "scan_mode": "Modalità di scansione", - "trigger_show_status": "Mostra bordo pulsante quando attivato", - "untrigger_reset": "Reset the view to default after untrigger", + "filter_selected_camera": "", + "show_trigger_status": "Mostra bordo pulsante quando attivato", "untrigger_seconds": "Reimposta la vista ai valori predefiniti dopo aver annullato l'attivazione" }, "timeout_seconds": "Ripristina la vista predefinita x secondi dopo l'azione dell'utente (0 = mai)", diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json index 58c5c7ad..8c6db881 100644 --- a/src/localize/languages/pt-BR.json +++ b/src/localize/languages/pt-BR.json @@ -384,10 +384,29 @@ }, "default": "Visualização padrão", "scan": { + "actions": { + "editor_label": "", + "interaction_mode": "", + "interaction_modes": { + "active": "", + "all": "", + "inactive": "" + }, + "trigger": "", + "triggers": { + "live": "", + "none": "" + }, + "untrigger": "", + "untriggers": { + "default": "", + "none": "" + } + }, + "editor_label": "Modo scan", "enabled": "Modo scan ativado", - "scan_mode": "Modo scan", - "trigger_show_status": "Pulsar borda quando acionado", - "untrigger_reset": "Redefinir a visualização para o padrão após desacionar", + "filter_selected_camera": "", + "show_trigger_status": "Pulsar borda quando acionado", "untrigger_seconds": "Segundos após a mudar para o estado inativo para desacionar" }, "timeout_seconds": "Redefinir para a visualização padrão X segundos após a ação do usuário (0 = nunca)", diff --git a/src/localize/languages/pt-PT.json b/src/localize/languages/pt-PT.json index 702913ce..b164abc4 100644 --- a/src/localize/languages/pt-PT.json +++ b/src/localize/languages/pt-PT.json @@ -374,10 +374,29 @@ }, "default": "Visualização padrão", "scan": { + "actions": { + "editor_label": "", + "interaction_mode": "", + "interaction_modes": { + "active": "", + "all": "", + "inactive": "" + }, + "trigger": "", + "triggers": { + "live": "", + "none": "" + }, + "untrigger": "", + "untriggers": { + "default": "", + "none": "" + } + }, + "editor_label": "Modo scan", "enabled": "Modo scan ativado", - "scan_mode": "Modo scan", - "trigger_show_status": "Exibir estado do gatilho", - "untrigger_reset": "Redefinir a visualização para o padrão após desacionar", + "filter_selected_camera": "", + "show_trigger_status": "Exibir estado do gatilho", "untrigger_seconds": "Segundos após a mudar para o estado inativo para desacionar" }, "timeout_seconds": "Redefinir para a visualização padrão X segundos após a ação do usuário (0 = nunca)", diff --git a/tests/card-controller/triggers-manager.test.ts b/tests/card-controller/triggers-manager.test.ts index 7fc1688a..d6ae6b4d 100644 --- a/tests/card-controller/triggers-manager.test.ts +++ b/tests/card-controller/triggers-manager.test.ts @@ -3,7 +3,7 @@ import { HassEntities } from 'home-assistant-js-websocket'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { CardController } from '../../src/card-controller/controller'; import { TriggersManager } from '../../src/card-controller/triggers-manager'; -import { ScanOptions } from '../../src/config/types'; +import { ScanOptions, scanSchema } from '../../src/config/types'; import { createCameraConfig, createCameraManager, @@ -18,22 +18,11 @@ import { const baseScanConfig: Partial = { enabled: true, untrigger_seconds: 10, - interaction_mode: 'inactive' as const, - trigger_filter_camera: 'all' as const, - trigger_action: [ - { - action: 'fire-dom-event' as const, - frigate_card_action: 'camera_select' as const, - triggered: true, - }, - { - action: 'fire-dom-event' as const, - frigate_card_action: 'live' as const, - }, - ], - untrigger_action: { - action: 'fire-dom-event' as const, - frigate_card_action: 'default' as const, + filter_selected_camera: false, + actions: { + trigger: 'live' as const, + untrigger: 'default' as const, + interaction_mode: 'inactive' as const, }, }; @@ -48,7 +37,7 @@ const createTriggerAPI = (options?: { vi.mocked(api.getConfigManager().getConfig).mockReturnValue( createConfig({ view: { - scan: options?.config ?? baseScanConfig, + scan: options?.config ? scanSchema.parse(options.config) : baseScanConfig, }, }), ); @@ -134,17 +123,10 @@ describe('TriggersManager', () => { manager.updateTriggerHAState(createHASS(hassInactiveState)); expect(manager.isTriggered()).toBeTruthy(); - expect(api.getActionsManager().executeActions).toBeCalledWith([ - { - action: 'fire-dom-event' as const, - frigate_card_action: 'camera_select' as const, - triggered: true, - }, - { - action: 'fire-dom-event' as const, - frigate_card_action: 'live' as const, - }, - ]); + expect(api.getViewManager().setViewByParameters).toBeCalledWith({ + viewName: 'live' as const, + cameraID: 'camera_1' as const, + }); vi.mocked(api.getHASSManager().getHASS).mockReturnValue( createHASS(hassInactiveState), @@ -163,10 +145,7 @@ describe('TriggersManager', () => { expect(manager.isTriggered()).toBeFalsy(); - expect(api.getActionsManager().executeActions).toBeCalledWith({ - action: 'fire-dom-event' as const, - frigate_card_action: 'default' as const, - }); + expect(api.getViewManager().setViewDefault).toBeCalled(); }); it('should trigger when entity state is active on startup', () => { @@ -191,7 +170,7 @@ describe('TriggersManager', () => { manager.updateTriggerHAState(createHASS(hassInactiveState)); expect(manager.isTriggered()).toBeTruthy(); - expect(api.getActionsManager().executeActions).not.toBeCalled(); + expect(api.getViewManager().setViewByParameters).not.toBeCalled(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue( createHASS(hassInactiveState), @@ -203,7 +182,39 @@ describe('TriggersManager', () => { expect(manager.isTriggered()).toBeFalsy(); - expect(api.getActionsManager().executeActions).not.toBeCalled(); + expect(api.getViewManager().setViewDefault).not.toBeCalled(); + }); + + it('should take no actions when actions are set to none', () => { + const start = new Date('2023-10-01T17:14'); + const api = createTriggerAPI({ + hassStates: hassActiveState, + config: { + enabled: true, + actions: { + interaction_mode: 'all', + trigger: 'none', + untrigger: 'none', + }, + }, + }); + const manager = new TriggersManager(api); + manager.updateTriggerHAState(createHASS(hassInactiveState)); + expect(manager.isTriggered()).toBeTruthy(); + + expect(api.getViewManager().setViewByParameters).not.toBeCalled(); + + vi.mocked(api.getHASSManager().getHASS).mockReturnValue( + createHASS(hassInactiveState), + ); + manager.updateTriggerHAState(createHASS(hassActiveState)); + + vi.setSystemTime(add(start, { seconds: 10 })); + vi.runOnlyPendingTimers(); + + expect(manager.isTriggered()).toBeFalsy(); + + expect(api.getViewManager().setViewDefault).not.toBeCalled(); }); it('should take actions with human interactions when interaction mode is active', () => { @@ -214,14 +225,21 @@ describe('TriggersManager', () => { interaction: true, config: { ...baseScanConfig, - interaction_mode: 'active', + actions: { + trigger: 'live' as const, + untrigger: 'default' as const, + interaction_mode: 'active', + }, }, }); const manager = new TriggersManager(api); manager.updateTriggerHAState(createHASS(hassInactiveState)); expect(manager.isTriggered()).toBeTruthy(); - expect(api.getActionsManager().executeActions).toBeCalledTimes(1); + expect(api.getViewManager().setViewByParameters).toBeCalledWith({ + viewName: 'live' as const, + cameraID: 'camera_1' as const, + }); vi.mocked(api.getHASSManager().getHASS).mockReturnValue( createHASS(hassInactiveState), @@ -233,7 +251,7 @@ describe('TriggersManager', () => { expect(manager.isTriggered()).toBeFalsy(); - expect(api.getActionsManager().executeActions).toBeCalledTimes(2); + expect(api.getViewManager().setViewDefault).toBeCalled(); }); it('should report multiple triggered cameras', () => { @@ -303,7 +321,7 @@ describe('TriggersManager', () => { config: { ...baseScanConfig, // Filter triggers to selected camera only. - trigger_filter_camera: 'selected' as const, + filter_selected_camera: true, }, hassStates: hassActiveState, }); @@ -339,7 +357,7 @@ describe('TriggersManager', () => { config: { ...baseScanConfig, // Filter triggers to selected camera only. - trigger_filter_camera: 'selected' as const, + filter_selected_camera: true, }, hassStates: hassActiveState, }); diff --git a/tests/config-mgmt.test.ts b/tests/config-mgmt.test.ts index cb51a12c..d90c099f 100644 --- a/tests/config-mgmt.test.ts +++ b/tests/config-mgmt.test.ts @@ -1166,6 +1166,51 @@ describe('should handle version specific upgrades', () => { type: 'custom:frigate-card', }); }); + }); + describe('should move and transform untrigger_reset', () => { + it('when true', () => { + const config = { + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + view: { + scan: { + untrigger_reset: true, + }, + }, + }; + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + view: { + scan: { + actions: { + untrigger: 'default', + }, + }, + }, + }); + }); + + it('when false', () => { + const config = { + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + view: { + scan: { + untrigger_reset: false, + }, + }, + }; + expect(upgradeConfig(config)).toBeTruthy(); + expect(config).toEqual({ + type: 'custom:frigate-card', + cameras: [{ camera_entity: 'camera.office' }], + view: { + scan: {}, + }, + }); + }); }); }); diff --git a/tests/config/types.test.ts b/tests/config/types.test.ts index 7758c2bf..9bd1fe41 100644 --- a/tests/config/types.test.ts +++ b/tests/config/types.test.ts @@ -279,25 +279,14 @@ describe('config defaults', () => { default: 'live', scan: { enabled: false, - interaction_mode: 'inactive', - trigger_show_status: true, + show_trigger_status: true, untrigger_seconds: 0, - trigger_action: [ - { - action: 'fire-dom-event', - frigate_card_action: 'camera_select', - triggered: true, - }, - { - action: 'fire-dom-event', - frigate_card_action: 'live', - }, - ], - trigger_filter_camera: 'all', - untrigger_action: { - action: 'fire-dom-event', - frigate_card_action: 'default', + actions: { + trigger: 'live', + untrigger: 'default', + interaction_mode: 'inactive', }, + filter_selected_camera: false, }, timeout_seconds: 300, update_cycle_camera: false, diff --git a/vite.config.ts b/vite.config.ts index ff62e3a5..699b52b7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,10 +10,10 @@ export default defineConfig({ // Thresholds will automatically be updated as coverage improves to avoid // back-sliding. thresholdAutoUpdate: true, - statements: 71.74, - branches: 60.62, - functions: 72.84, - lines: 71.63, + statements: 71.82, + branches: 60.67, + functions: 72.86, + lines: 71.71, }, }, });