Add editor and migration support.

This commit is contained in:
Dermot Duffy
2024-01-21 11:30:23 -08:00
parent 278f4f372d
commit 64a909ead0
16 changed files with 385 additions and 230 deletions
+13 -25
View File
@@ -432,32 +432,16 @@ Scan mode tracks Home Assistant state *changes* -- when the card is first starte
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `enabled` | `false` | :white_check_mark: | Whether to enable scan mode. | | `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.| | `filter_selected_camera` | `false` | :white_check_mark: | If set to `true` will only trigger on the currently selected camera.|
| `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.| | `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). |
| `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. |
| `untrigger_seconds` | `0` | :white_check_mark: | The number of seconds to wait after all entities are inactive before untriggering. | | `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: | `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. |
```yaml | `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.|
- 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
```
### Menu Options ### Menu Options
@@ -1935,9 +1919,13 @@ view:
dark_mode: 'off' dark_mode: 'off'
scan: scan:
enabled: false enabled: false
trigger_show_status: true show_trigger_status: true
untrigger_reset: true filter_selected_camera: false
untrigger_seconds: 0 untrigger_seconds: 0
actions:
interaction_mode: 'inactive' as const,
trigger: 'live' as const,
untrigger: 'default' as const,
actions: actions:
entity: light.office_main_lights entity: light.office_main_lights
tap_action: tap_action:
+19 -17
View File
@@ -75,8 +75,8 @@ export class TriggersManager {
public updateView(oldView?: View | null): void { public updateView(oldView?: View | null): void {
if (oldView?.camera !== this._api.getViewManager().getView()?.camera) { if (oldView?.camera !== this._api.getViewManager().getView()?.camera) {
// If the view changes, a new camera may have been selected, which may // 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` // mean a trigger is required (in the case that `filter_selected_camera`
// has been set to `selected`). // is true).
this._evaluateTriggers(); this._evaluateTriggers();
} }
} }
@@ -91,12 +91,11 @@ export class TriggersManager {
for (const cameraID of this._triggeredState.keys()) { for (const cameraID of this._triggeredState.keys()) {
if ( if (
!this._triggeredCameras.has(cameraID) && !this._triggeredCameras.has(cameraID) &&
(scanConfig.trigger_filter_camera === 'all' || (!scanConfig.filter_selected_camera ||
(scanConfig.trigger_filter_camera === 'selected' && cameraID === this._api.getViewManager().getView()?.camera)
cameraID === this._api.getViewManager().getView()?.camera))
) { ) {
this._triggeredCameras.set(cameraID, now); 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 scanConfig = this._api.getConfigManager().getConfig()?.view.scan;
const hasInteraction = this._api.getInteractionManager().hasInteraction(); const hasInteraction = this._api.getInteractionManager().hasInteraction();
return ( return (
!!scanConfig && !!scanConfig &&
(scanConfig.interaction_mode === 'all' || (scanConfig.actions.interaction_mode === 'all' ||
(scanConfig.interaction_mode === 'active' && hasInteraction) || (scanConfig.actions.interaction_mode === 'active' && hasInteraction) ||
(scanConfig.interaction_mode === 'inactive' && !hasInteraction)) (scanConfig.actions.interaction_mode === 'inactive' && !hasInteraction))
); );
} }
protected _triggerAction(): void { protected _triggerAction(cameraID: string): void {
const action = this._api.getConfigManager().getConfig()?.view.scan.trigger_action; const action = this._api.getConfigManager().getConfig()?.view.scan.actions.trigger;
if (action && this._hasAllowableInteractionState()) { if (action === 'live' && this._hasAllowableInteractionStateForAction()) {
this._api.getActionsManager().executeActions(action); this._api.getViewManager().setViewByParameters({
viewName: 'live',
cameraID: cameraID,
});
} }
// Must update master element to add border pulsing. // Must update master element to add border pulsing.
@@ -131,10 +133,10 @@ export class TriggersManager {
} }
protected _untriggerAction(cameraID: string): void { 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()) { if (action === 'default' && this._hasAllowableInteractionStateForAction()) {
this._api.getActionsManager().executeActions(action); this._api.getViewManager().setViewDefault();
} }
this._triggeredCameras.delete(cameraID); this._triggeredCameras.delete(cameraID);
this._deleteTimer(cameraID); this._deleteTimer(cameraID);
-1
View File
@@ -197,7 +197,6 @@ export interface CardStyleAPI {
} }
export interface CardTriggersAPI { export interface CardTriggersAPI {
getActionsManager(): ActionsManager;
getCameraManager(): CameraManager; getCameraManager(): CameraManager;
getCardElementManager(): CardElementManager; getCardElementManager(): CardElementManager;
getConfigManager(): ConfigManager; getConfigManager(): ConfigManager;
+1 -1
View File
@@ -241,7 +241,7 @@ class FrigateCard extends LitElement {
const cardClasses = { const cardClasses = {
triggered: triggered:
!!this._controller.getTriggersManager().isTriggered() && !!this._controller.getTriggersManager().isTriggered() &&
!!this._config?.view.scan.trigger_show_status, !!this._config?.view.scan.show_trigger_status,
}; };
const mainClasses = { const mainClasses = {
main: true, main: true,
+9
View File
@@ -13,6 +13,7 @@ import {
CONF_MEDIA_GALLERY, CONF_MEDIA_GALLERY,
CONF_MENU_BUTTONS_CAMERA_UI, CONF_MENU_BUTTONS_CAMERA_UI,
CONF_OVERRIDES, CONF_OVERRIDES,
CONF_VIEW_SCAN_ACTIONS_UNTRIGGER,
} from './const'; } from './const';
import { arrayify } from './utils/basic'; import { arrayify } from './utils/basic';
@@ -474,4 +475,12 @@ const UPGRADES = [
); );
}, },
upgradePTZElementsToLive(), 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),
},
),
]; ];
+21 -71
View File
@@ -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 { import {
CallServiceActionConfig, CallServiceActionConfig,
@@ -1160,47 +1121,36 @@ const viewConfigDefault = {
dark_mode: 'off' as const, dark_mode: 'off' as const,
scan: { scan: {
enabled: false, enabled: false,
trigger_show_status: true, show_trigger_status: true,
trigger_action: [ filter_selected_camera: false,
{ actions: {
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,
},
interaction_mode: 'inactive' as const, interaction_mode: 'inactive' as const,
trigger_filter_camera: 'all' as const, trigger: 'live' as const,
untrigger: 'default' as const,
},
untrigger_seconds: 0, untrigger_seconds: 0,
}, },
}; };
const scanSchema = z.object({ export const scanSchema = z.object({
enabled: z.boolean().default(viewConfigDefault.scan.enabled), enabled: z.boolean().default(viewConfigDefault.scan.enabled),
filter_selected_camera: z
.boolean()
.default(viewConfigDefault.scan.filter_selected_camera),
show_trigger_status: z.boolean().default(viewConfigDefault.scan.show_trigger_status),
actions: z
.object({
interaction_mode: z interaction_mode: z
.enum(['all', 'inactive', 'active']) .enum(['all', 'inactive', 'active'])
.default(viewConfigDefault.scan.interaction_mode), .default(viewConfigDefault.scan.actions.interaction_mode),
trigger_filter_camera: z trigger: z.enum(['live', 'none']).default(viewConfigDefault.scan.actions.trigger),
.enum(['all', 'selected']) untrigger: z
.default(viewConfigDefault.scan.trigger_filter_camera), .enum(['default', 'none'])
trigger_show_status: z.boolean().default(viewConfigDefault.scan.trigger_show_status), .default(viewConfigDefault.scan.actions.untrigger),
trigger_action: actionSchema })
.or(actionSchema.array()) .default(viewConfigDefault.scan.actions),
.nullable()
.default(viewConfigDefault.scan.trigger_action),
untrigger_action: actionSchema
.or(actionSchema.array())
.nullable()
.default(viewConfigDefault.scan.untrigger_action),
untrigger_seconds: z.number().default(viewConfigDefault.scan.untrigger_seconds), untrigger_seconds: z.number().default(viewConfigDefault.scan.untrigger_seconds),
}); });
export type ScanOptions = z.infer<typeof scanSchema>; export type ScanOptions = z.infer<typeof scanSchema>;
+11 -2
View File
@@ -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_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const;
export const CONF_VIEW_SCAN = `${CONF_VIEW}.scan` 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_ENABLED = `${CONF_VIEW_SCAN}.enabled` as const;
export const CONF_VIEW_SCAN_TRIGGER_SHOW_STATUS = export const CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS =
`${CONF_VIEW_SCAN}.trigger_show_status` as const; `${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 = export const CONF_VIEW_SCAN_UNTRIGGER_SECONDS =
`${CONF_VIEW_SCAN}.untrigger_seconds` as const; `${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 = 'media_gallery' as const;
export const CONF_MEDIA_GALLERY_CONTROLS_FILTER_MODE = export const CONF_MEDIA_GALLERY_CONTROLS_FILTER_MODE =
+99 -29
View File
@@ -181,8 +181,13 @@ import {
CONF_VIEW_DARK_MODE, CONF_VIEW_DARK_MODE,
CONF_VIEW_DEFAULT, CONF_VIEW_DEFAULT,
CONF_VIEW_SCAN, 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_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_SCAN_UNTRIGGER_SECONDS,
CONF_VIEW_TIMEOUT_SECONDS, CONF_VIEW_TIMEOUT_SECONDS,
CONF_VIEW_UPDATE_CYCLE_CAMERA, CONF_VIEW_UPDATE_CYCLE_CAMERA,
@@ -237,6 +242,7 @@ const MENU_PERFORMANCE_FEATURES = 'performance.features';
const MENU_PERFORMANCE_STYLE = 'performance.style'; const MENU_PERFORMANCE_STYLE = 'performance.style';
const MENU_TIMELINE_CONTROLS_THUMBNAILS = 'timeline.controls.thumbnails'; const MENU_TIMELINE_CONTROLS_THUMBNAILS = 'timeline.controls.thumbnails';
const MENU_VIEW_SCAN = 'scan'; const MENU_VIEW_SCAN = 'scan';
const MENU_VIEW_SCAN_ACTIONS = 'scan.actions';
interface EditorOptionsSet { interface EditorOptionsSet {
icon: string; 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 { public setConfig(config: RawFrigateCardConfig): void {
// Note: This does not use Zod to parse the configuration, so it may be // 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 // 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 { protected _renderViewScanMenu(): TemplateResult {
const submenuClasses = { return this._putInSubmenu(
submenu: true, MENU_VIEW_SCAN,
selected: !!this._expandedMenus[MENU_VIEW_SCAN], true,
}; `config.${CONF_VIEW_SCAN}.editor_label`,
return html` { name: 'mdi:target-account' },
<div class="${classMap(submenuClasses)}"> html`
<div ${this._renderSwitch(CONF_VIEW_SCAN_ENABLED, this._defaults.view.scan.enabled, {
class="submenu-header"
@click=${this._toggleMenu}
.domain=${MENU_VIEW_SCAN}
.key=${true}
>
<ha-icon .icon=${'mdi:target-account'}></ha-icon>
<span>${localize(`config.${CONF_VIEW_SCAN}.scan_mode`)}</span>
</div>
${this._expandedMenus[MENU_VIEW_SCAN]
? html` <div class="values">
${this._renderSwitch(
CONF_VIEW_SCAN_ENABLED,
this._defaults.view.scan.enabled,
{
label: localize(`config.${CONF_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( ${this._renderSwitch(
CONF_VIEW_SCAN_TRIGGER_SHOW_STATUS, CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS,
this._defaults.view.scan.trigger_show_status, this._defaults.view.scan.show_trigger_status,
{ {
label: localize(`config.${CONF_VIEW_SCAN_TRIGGER_SHOW_STATUS}`), label: localize(`config.${CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS}`),
}, },
)} )}
${this._renderNumberInput(CONF_VIEW_SCAN_UNTRIGGER_SECONDS, { ${this._renderNumberInput(CONF_VIEW_SCAN_UNTRIGGER_SECONDS, {
default: this._defaults.view.scan.untrigger_seconds, default: this._defaults.view.scan.untrigger_seconds,
})} })}
</div>` ${this._renderOptionSelector(
: ''} CONF_VIEW_SCAN_ACTIONS_TRIGGER,
</div> 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'),
},
)}`,
)}
`,
);
} }
/** /**
+22 -3
View File
@@ -385,10 +385,29 @@
}, },
"default": "Default view", "default": "Default view",
"scan": { "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", "enabled": "Scan mode enabled",
"scan_mode": "Scan mode", "show_trigger_status": "Show pulsing border when triggered",
"trigger_show_status": "Show pulsing border when triggered",
"untrigger_reset": "Reset the view to default after untrigger",
"untrigger_seconds": "Seconds after inactive state change to untrigger" "untrigger_seconds": "Seconds after inactive state change to untrigger"
}, },
"timeout_seconds": "Reset to default view X seconds after user action (0=never)", "timeout_seconds": "Reset to default view X seconds after user action (0=never)",
+22 -3
View File
@@ -381,10 +381,29 @@
}, },
"default": "Visualizzazione predefinita", "default": "Visualizzazione predefinita",
"scan": { "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", "enabled": "Modalità di scansione abilitata",
"scan_mode": "Modalità di scansione", "filter_selected_camera": "",
"trigger_show_status": "Mostra bordo pulsante quando attivato", "show_trigger_status": "Mostra bordo pulsante quando attivato",
"untrigger_reset": "Reset the view to default after untrigger",
"untrigger_seconds": "Reimposta la vista ai valori predefiniti dopo aver annullato l'attivazione" "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)", "timeout_seconds": "Ripristina la vista predefinita x secondi dopo l'azione dell'utente (0 = mai)",
+22 -3
View File
@@ -384,10 +384,29 @@
}, },
"default": "Visualização padrão", "default": "Visualização padrão",
"scan": { "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", "enabled": "Modo scan ativado",
"scan_mode": "Modo scan", "filter_selected_camera": "",
"trigger_show_status": "Pulsar borda quando acionado", "show_trigger_status": "Pulsar borda quando acionado",
"untrigger_reset": "Redefinir a visualização para o padrão após desacionar",
"untrigger_seconds": "Segundos após a mudar para o estado inativo para desacionar" "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)", "timeout_seconds": "Redefinir para a visualização padrão X segundos após a ação do usuário (0 = nunca)",
+22 -3
View File
@@ -374,10 +374,29 @@
}, },
"default": "Visualização padrão", "default": "Visualização padrão",
"scan": { "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", "enabled": "Modo scan ativado",
"scan_mode": "Modo scan", "filter_selected_camera": "",
"trigger_show_status": "Exibir estado do gatilho", "show_trigger_status": "Exibir estado do gatilho",
"untrigger_reset": "Redefinir a visualização para o padrão após desacionar",
"untrigger_seconds": "Segundos após a mudar para o estado inativo para desacionar" "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)", "timeout_seconds": "Redefinir para a visualização padrão X segundos após a ação do usuário (0 = nunca)",
+56 -38
View File
@@ -3,7 +3,7 @@ import { HassEntities } from 'home-assistant-js-websocket';
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CardController } from '../../src/card-controller/controller'; import { CardController } from '../../src/card-controller/controller';
import { TriggersManager } from '../../src/card-controller/triggers-manager'; import { TriggersManager } from '../../src/card-controller/triggers-manager';
import { ScanOptions } from '../../src/config/types'; import { ScanOptions, scanSchema } from '../../src/config/types';
import { import {
createCameraConfig, createCameraConfig,
createCameraManager, createCameraManager,
@@ -18,22 +18,11 @@ import {
const baseScanConfig: Partial<ScanOptions> = { const baseScanConfig: Partial<ScanOptions> = {
enabled: true, enabled: true,
untrigger_seconds: 10, untrigger_seconds: 10,
filter_selected_camera: false,
actions: {
trigger: 'live' as const,
untrigger: 'default' as const,
interaction_mode: 'inactive' as const, 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,
}, },
}; };
@@ -48,7 +37,7 @@ const createTriggerAPI = (options?: {
vi.mocked(api.getConfigManager().getConfig).mockReturnValue( vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({ createConfig({
view: { view: {
scan: options?.config ?? baseScanConfig, scan: options?.config ? scanSchema.parse(options.config) : baseScanConfig,
}, },
}), }),
); );
@@ -134,17 +123,10 @@ describe('TriggersManager', () => {
manager.updateTriggerHAState(createHASS(hassInactiveState)); manager.updateTriggerHAState(createHASS(hassInactiveState));
expect(manager.isTriggered()).toBeTruthy(); expect(manager.isTriggered()).toBeTruthy();
expect(api.getActionsManager().executeActions).toBeCalledWith([ expect(api.getViewManager().setViewByParameters).toBeCalledWith({
{ viewName: 'live' as const,
action: 'fire-dom-event' as const, cameraID: 'camera_1' as const,
frigate_card_action: 'camera_select' as const, });
triggered: true,
},
{
action: 'fire-dom-event' as const,
frigate_card_action: 'live' as const,
},
]);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue( vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
createHASS(hassInactiveState), createHASS(hassInactiveState),
@@ -163,10 +145,7 @@ describe('TriggersManager', () => {
expect(manager.isTriggered()).toBeFalsy(); expect(manager.isTriggered()).toBeFalsy();
expect(api.getActionsManager().executeActions).toBeCalledWith({ expect(api.getViewManager().setViewDefault).toBeCalled();
action: 'fire-dom-event' as const,
frigate_card_action: 'default' as const,
});
}); });
it('should trigger when entity state is active on startup', () => { it('should trigger when entity state is active on startup', () => {
@@ -191,7 +170,7 @@ describe('TriggersManager', () => {
manager.updateTriggerHAState(createHASS(hassInactiveState)); manager.updateTriggerHAState(createHASS(hassInactiveState));
expect(manager.isTriggered()).toBeTruthy(); expect(manager.isTriggered()).toBeTruthy();
expect(api.getActionsManager().executeActions).not.toBeCalled(); expect(api.getViewManager().setViewByParameters).not.toBeCalled();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue( vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
createHASS(hassInactiveState), createHASS(hassInactiveState),
@@ -203,7 +182,39 @@ describe('TriggersManager', () => {
expect(manager.isTriggered()).toBeFalsy(); 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', () => { it('should take actions with human interactions when interaction mode is active', () => {
@@ -214,14 +225,21 @@ describe('TriggersManager', () => {
interaction: true, interaction: true,
config: { config: {
...baseScanConfig, ...baseScanConfig,
actions: {
trigger: 'live' as const,
untrigger: 'default' as const,
interaction_mode: 'active', interaction_mode: 'active',
}, },
},
}); });
const manager = new TriggersManager(api); const manager = new TriggersManager(api);
manager.updateTriggerHAState(createHASS(hassInactiveState)); manager.updateTriggerHAState(createHASS(hassInactiveState));
expect(manager.isTriggered()).toBeTruthy(); 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( vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
createHASS(hassInactiveState), createHASS(hassInactiveState),
@@ -233,7 +251,7 @@ describe('TriggersManager', () => {
expect(manager.isTriggered()).toBeFalsy(); expect(manager.isTriggered()).toBeFalsy();
expect(api.getActionsManager().executeActions).toBeCalledTimes(2); expect(api.getViewManager().setViewDefault).toBeCalled();
}); });
it('should report multiple triggered cameras', () => { it('should report multiple triggered cameras', () => {
@@ -303,7 +321,7 @@ describe('TriggersManager', () => {
config: { config: {
...baseScanConfig, ...baseScanConfig,
// Filter triggers to selected camera only. // Filter triggers to selected camera only.
trigger_filter_camera: 'selected' as const, filter_selected_camera: true,
}, },
hassStates: hassActiveState, hassStates: hassActiveState,
}); });
@@ -339,7 +357,7 @@ describe('TriggersManager', () => {
config: { config: {
...baseScanConfig, ...baseScanConfig,
// Filter triggers to selected camera only. // Filter triggers to selected camera only.
trigger_filter_camera: 'selected' as const, filter_selected_camera: true,
}, },
hassStates: hassActiveState, hassStates: hassActiveState,
}); });
+45
View File
@@ -1166,6 +1166,51 @@ describe('should handle version specific upgrades', () => {
type: 'custom:frigate-card', 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: {},
},
});
});
}); });
}); });
+6 -17
View File
@@ -279,25 +279,14 @@ describe('config defaults', () => {
default: 'live', default: 'live',
scan: { scan: {
enabled: false, enabled: false,
interaction_mode: 'inactive', show_trigger_status: true,
trigger_show_status: true,
untrigger_seconds: 0, untrigger_seconds: 0,
trigger_action: [ actions: {
{ trigger: 'live',
action: 'fire-dom-event', untrigger: 'default',
frigate_card_action: 'camera_select', interaction_mode: 'inactive',
triggered: true,
},
{
action: 'fire-dom-event',
frigate_card_action: 'live',
},
],
trigger_filter_camera: 'all',
untrigger_action: {
action: 'fire-dom-event',
frigate_card_action: 'default',
}, },
filter_selected_camera: false,
}, },
timeout_seconds: 300, timeout_seconds: 300,
update_cycle_camera: false, update_cycle_camera: false,
+4 -4
View File
@@ -10,10 +10,10 @@ export default defineConfig({
// Thresholds will automatically be updated as coverage improves to avoid // Thresholds will automatically be updated as coverage improves to avoid
// back-sliding. // back-sliding.
thresholdAutoUpdate: true, thresholdAutoUpdate: true,
statements: 71.74, statements: 71.82,
branches: 60.62, branches: 60.67,
functions: 72.84, functions: 72.86,
lines: 71.63, lines: 71.71,
}, },
}, },
}); });