feat: Add ability to untrigger after fixed number of seconds (#2350)
- Related: #2342 BREAKING CHANGE: Renames: `untrigger_seconds` to `untrigger_delay_seconds`
This commit is contained in:
+29
-14
@@ -99,7 +99,11 @@ Allows overriding of any CSS value, can be used to tweak theming parameters.
|
|||||||
|
|
||||||
## `triggers`
|
## `triggers`
|
||||||
|
|
||||||
The `triggers` block controls how the card reacts when a camera is triggered (note that _what_ triggers the camera is controlled by the [`triggers`](cameras/README.md?id=triggers) block within the config for a given camera). This can be used for a variety of purposes, such as allowing the card to automatically change to `live` for a camera that triggers.
|
The `triggers` block controls how the card reacts when a camera is triggered
|
||||||
|
(note that _what_ triggers the camera is controlled by the
|
||||||
|
[`triggers`](cameras/README.md?id=triggers) block within the config for a given
|
||||||
|
camera). This can be used for a variety of purposes, such as allowing the card
|
||||||
|
to automatically change to `live` for a camera that triggers.
|
||||||
|
|
||||||
All configuration is under:
|
All configuration is under:
|
||||||
|
|
||||||
@@ -109,23 +113,33 @@ view:
|
|||||||
# [...]
|
# [...]
|
||||||
```
|
```
|
||||||
|
|
||||||
When a camera untriggers (e.g. an entity state returning to something other than
|
When all trigger sources for a camera end (e.g. an entity state returns to
|
||||||
`on` or `open`), an action can also be taken with an optional number of seconds
|
something other than `on` or `open`), an untrigger action can be taken.
|
||||||
to wait prior to the acting (see `untrigger_seconds`). By default, triggering is
|
|
||||||
only allowed when there is no ongoing human interaction with the card. This
|
|
||||||
behavior can be controlled by the `interaction_mode` parameter.
|
|
||||||
|
|
||||||
If the card starts when a trigger entity is already in a triggered state, the
|
The triggered state can be extended by a number of seconds after the source ends
|
||||||
action will be taken on card startup. If multiple cameras are triggered at
|
(see `untrigger_delay_seconds`). Alternatively, a camera can be forcibly
|
||||||
startup, all are marked as triggered, but the startup action is taken for the
|
untriggered after a fixed duration regardless of the state of the trigger
|
||||||
first triggered camera only.
|
sources (see `untrigger_force_seconds`).
|
||||||
|
|
||||||
|
By default, trigger/untrigger actions are only taken when there is no ongoing
|
||||||
|
human interaction with the card; this behavior can be configured via the
|
||||||
|
`interaction_mode` parameter.
|
||||||
|
|
||||||
|
> [!TIP] If a camera is already in a triggered state when the card starts, the trigger
|
||||||
|
> action is taken immediately. If multiple cameras are triggered at startup, they
|
||||||
|
> are all marked as triggered, but the action is only taken for the first one.
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| ------------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `actions` | | The actions to take when a camera is triggered. See below. |
|
| `actions` | | The actions to take when a camera is triggered. See below. |
|
||||||
| `filter_selected_camera` | `false` | If set to `true` will only trigger on the currently selected camera. |
|
| `filter_selected_camera` | `true` | If set to `true` will only trigger on the currently selected camera. |
|
||||||
| `show_trigger_status` | `false` | Whether or not the `live` view should show a visual indication that it is triggered (a pulsing border around the camera edge). |
|
| `show_trigger_status` | `false` | Whether or not the `live` view should show a visual indication that it is triggered (a pulsing border around the camera edge). |
|
||||||
| `untrigger_seconds` | `0` | The number of seconds to wait after a camera untriggers before considering the card untriggered and taking the `untrigger` action. |
|
| `untrigger_delay_seconds` | `0` | The number of seconds to continue to consider the camera triggered after the entity/event/state has reset, before considering the camera untriggered and taking the configured `untrigger` action. |
|
||||||
|
| `untrigger_force_seconds` | `0` | The number of seconds after a camera first triggers before force untriggering that camera. Set to `0` to disable. |
|
||||||
|
|
||||||
|
> [!WARNING] If `untrigger_force_seconds` is used to untrigger a camera, the
|
||||||
|
> state will need to 'reset' (e.g. an entity would need to change state to
|
||||||
|
> `off`) before it will trigger again.
|
||||||
|
|
||||||
### Trigger action configuration
|
### Trigger action configuration
|
||||||
|
|
||||||
@@ -181,7 +195,8 @@ view:
|
|||||||
triggers:
|
triggers:
|
||||||
show_trigger_status: false
|
show_trigger_status: false
|
||||||
filter_selected_camera: true
|
filter_selected_camera: true
|
||||||
untrigger_seconds: 0
|
untrigger_delay_seconds: 0
|
||||||
|
untrigger_force_seconds: 0
|
||||||
actions:
|
actions:
|
||||||
interaction_mode: inactive
|
interaction_mode: inactive
|
||||||
trigger: update
|
trigger: update
|
||||||
|
|||||||
@@ -569,6 +569,7 @@ export class FrigateCamera extends Camera {
|
|||||||
|
|
||||||
this._eventCallback?.({
|
this._eventCallback?.({
|
||||||
cameraID,
|
cameraID,
|
||||||
|
id: review.after.id,
|
||||||
fidelity: 'high',
|
fidelity: 'high',
|
||||||
type: review.type,
|
type: review.type,
|
||||||
review: true,
|
review: true,
|
||||||
|
|||||||
@@ -13,8 +13,17 @@ interface CameraTriggerState {
|
|||||||
// IDs).
|
// IDs).
|
||||||
sources: Set<string>;
|
sources: Set<string>;
|
||||||
|
|
||||||
|
// The set of ignored event IDs (e.g. events that have been forcibly
|
||||||
|
// untriggered).
|
||||||
|
ignoredSources: Set<string>;
|
||||||
|
|
||||||
// A timer used to delay the untrigger action.
|
// A timer used to delay the untrigger action.
|
||||||
untriggerDelayTimer?: Timer;
|
untriggerDelayTimer?: Timer;
|
||||||
|
|
||||||
|
// A one-shot timer used to force untriggering a camera if no end event is
|
||||||
|
// seen within a configured duration. This timer starts when the camera first
|
||||||
|
// triggers and is not reset by subsequent trigger events.
|
||||||
|
untriggerForceTimer?: Timer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TriggersManager {
|
export class TriggersManager {
|
||||||
@@ -55,6 +64,7 @@ export class TriggersManager {
|
|||||||
const hass = this._api.getHASSManager().getHASS();
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
let triggered = false;
|
let triggered = false;
|
||||||
let startupActionEvent: CameraEvent | null = null;
|
let startupActionEvent: CameraEvent | null = null;
|
||||||
|
this._states.clear();
|
||||||
|
|
||||||
for (const [cameraID, camera] of this._api
|
for (const [cameraID, camera] of this._api
|
||||||
.getCameraManager()
|
.getCameraManager()
|
||||||
@@ -97,12 +107,13 @@ export class TriggersManager {
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const skipAction = options?.skipAction ?? false;
|
const skipAction = options?.skipAction ?? false;
|
||||||
if (ev.type === 'end') {
|
if (ev.type === 'end') {
|
||||||
const state = this._states.get(ev.cameraID);
|
return this._handleEndEvent(ev);
|
||||||
state?.sources.delete(ev.id);
|
|
||||||
if (!state?.sources.size) {
|
|
||||||
await this._startUntrigger(ev.cameraID);
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
|
// Ignore stale updates for force-untriggered IDs before doing any further
|
||||||
|
// processing to avoid re-activating muted IDs.
|
||||||
|
if (this._isIgnoredUpdateEvent(ev)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
const config = this._api.getConfigManager().getConfig();
|
||||||
@@ -122,20 +133,15 @@ export class TriggersManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = this._states.get(ev.cameraID);
|
const state = this._getOrCreateState(ev.cameraID);
|
||||||
if (!state) {
|
|
||||||
state = {
|
|
||||||
lastTriggerTime: new Date(),
|
|
||||||
sources: new Set(),
|
|
||||||
};
|
|
||||||
this._states.set(ev.cameraID, state);
|
|
||||||
} else {
|
|
||||||
state.lastTriggerTime = new Date();
|
state.lastTriggerTime = new Date();
|
||||||
}
|
|
||||||
|
|
||||||
state.sources.add(ev.id);
|
state.sources.add(ev.id);
|
||||||
|
|
||||||
this._deleteUntriggerDelayTimer(ev.cameraID);
|
this._deleteUntriggerDelayTimer(ev.cameraID);
|
||||||
|
this._startForceUntriggerTimerIfNecessary(
|
||||||
|
ev.cameraID,
|
||||||
|
triggersConfig.untrigger_force_seconds,
|
||||||
|
);
|
||||||
this._setConditionStateIfNecessary();
|
this._setConditionStateIfNecessary();
|
||||||
if (!skipAction) {
|
if (!skipAction) {
|
||||||
await this._throttledTriggerAction(ev);
|
await this._throttledTriggerAction(ev);
|
||||||
@@ -143,6 +149,24 @@ export class TriggersManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async _handleEndEvent(ev: CameraEvent): Promise<boolean> {
|
||||||
|
this._deleteIgnoredEventID(ev.cameraID, ev.id);
|
||||||
|
|
||||||
|
const state = this._states.get(ev.cameraID);
|
||||||
|
state?.sources.delete(ev.id);
|
||||||
|
if (!state?.sources.size) {
|
||||||
|
await this._startUntrigger(ev.cameraID);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _isIgnoredUpdateEvent(ev: CameraEvent): boolean {
|
||||||
|
return (
|
||||||
|
(ev.type === 'update' || ev.type === 'genai') &&
|
||||||
|
this._hasIgnoredEventID(ev.cameraID, ev.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected _hasAllowableInteractionStateForAction(): boolean {
|
protected _hasAllowableInteractionStateForAction(): boolean {
|
||||||
const triggersConfig = this._api.getConfigManager().getConfig()?.view.triggers;
|
const triggersConfig = this._api.getConfigManager().getConfig()?.view.triggers;
|
||||||
const hasInteraction = this._api.getInteractionManager().hasInteraction();
|
const hasInteraction = this._api.getInteractionManager().hasInteraction();
|
||||||
@@ -247,9 +271,10 @@ export class TriggersManager {
|
|||||||
|
|
||||||
protected async _untriggerAction(cameraID: string): Promise<void> {
|
protected async _untriggerAction(cameraID: string): Promise<void> {
|
||||||
this._deleteUntriggerDelayTimer(cameraID);
|
this._deleteUntriggerDelayTimer(cameraID);
|
||||||
|
this._deleteForceUntriggerTimer(cameraID);
|
||||||
|
|
||||||
await this._executeUntriggerAction();
|
await this._executeUntriggerAction();
|
||||||
this._states.delete(cameraID);
|
this._deleteStateIfIdle(cameraID);
|
||||||
|
|
||||||
this._setConditionStateIfNecessary();
|
this._setConditionStateIfNecessary();
|
||||||
|
|
||||||
@@ -259,6 +284,7 @@ export class TriggersManager {
|
|||||||
|
|
||||||
protected async _startUntrigger(cameraID: string): Promise<void> {
|
protected async _startUntrigger(cameraID: string): Promise<void> {
|
||||||
this._deleteUntriggerDelayTimer(cameraID);
|
this._deleteUntriggerDelayTimer(cameraID);
|
||||||
|
this._deleteForceUntriggerTimer(cameraID);
|
||||||
|
|
||||||
const state = this._states.get(cameraID);
|
const state = this._states.get(cameraID);
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@@ -266,11 +292,11 @@ export class TriggersManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
const config = this._api.getConfigManager().getConfig();
|
||||||
const untriggerSeconds = config?.view?.triggers.untrigger_seconds ?? 0;
|
const untriggerDelaySeconds = config?.view?.triggers.untrigger_delay_seconds ?? 0;
|
||||||
|
|
||||||
if (untriggerSeconds > 0) {
|
if (untriggerDelaySeconds > 0) {
|
||||||
state.untriggerDelayTimer = new Timer();
|
state.untriggerDelayTimer = new Timer();
|
||||||
state.untriggerDelayTimer.start(untriggerSeconds, async () => {
|
state.untriggerDelayTimer.start(untriggerDelaySeconds, async () => {
|
||||||
await this._untriggerAction(cameraID);
|
await this._untriggerAction(cameraID);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -278,6 +304,81 @@ export class TriggersManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _startForceUntriggerTimerIfNecessary(
|
||||||
|
cameraID: string,
|
||||||
|
forceUntriggerSeconds: number,
|
||||||
|
): void {
|
||||||
|
if (forceUntriggerSeconds <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = this._states.get(cameraID);
|
||||||
|
if (!state || state.untriggerForceTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = new Timer();
|
||||||
|
state.untriggerForceTimer = timer;
|
||||||
|
timer.start(forceUntriggerSeconds, async () => {
|
||||||
|
await this._forceUntrigger(state, cameraID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _forceUntrigger(
|
||||||
|
state: CameraTriggerState,
|
||||||
|
cameraID: string,
|
||||||
|
): Promise<void> {
|
||||||
|
state.sources.forEach((id) => this._addIgnoredEventID(cameraID, id));
|
||||||
|
state.sources.clear();
|
||||||
|
this._deleteForceUntriggerTimer(cameraID);
|
||||||
|
await this._startUntrigger(cameraID);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _addIgnoredEventID(cameraID: string, eventID: string): void {
|
||||||
|
const state = this._getOrCreateState(cameraID);
|
||||||
|
state.ignoredSources.add(eventID);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _deleteIgnoredEventID(cameraID: string, eventID: string): void {
|
||||||
|
const state = this._states.get(cameraID);
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.ignoredSources.delete(eventID);
|
||||||
|
this._deleteStateIfIdle(cameraID);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _hasIgnoredEventID(cameraID: string, eventID: string): boolean {
|
||||||
|
return !!this._states.get(cameraID)?.ignoredSources.has(eventID);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getOrCreateState(cameraID: string): CameraTriggerState {
|
||||||
|
let state = this._states.get(cameraID);
|
||||||
|
if (!state) {
|
||||||
|
state = {
|
||||||
|
lastTriggerTime: new Date(),
|
||||||
|
sources: new Set(),
|
||||||
|
ignoredSources: new Set(),
|
||||||
|
};
|
||||||
|
this._states.set(cameraID, state);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _deleteStateIfIdle(cameraID: string): void {
|
||||||
|
const state = this._states.get(cameraID);
|
||||||
|
if (
|
||||||
|
state &&
|
||||||
|
!state.sources.size &&
|
||||||
|
!state.ignoredSources.size &&
|
||||||
|
!state.untriggerDelayTimer &&
|
||||||
|
!state.untriggerForceTimer
|
||||||
|
) {
|
||||||
|
this._states.delete(cameraID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected _deleteUntriggerDelayTimer(cameraID: string): void {
|
protected _deleteUntriggerDelayTimer(cameraID: string): void {
|
||||||
const state = this._states.get(cameraID);
|
const state = this._states.get(cameraID);
|
||||||
if (state?.untriggerDelayTimer) {
|
if (state?.untriggerDelayTimer) {
|
||||||
@@ -286,6 +387,14 @@ export class TriggersManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _deleteForceUntriggerTimer(cameraID: string): void {
|
||||||
|
const state = this._states.get(cameraID);
|
||||||
|
if (state?.untriggerForceTimer) {
|
||||||
|
state.untriggerForceTimer.stop();
|
||||||
|
delete state.untriggerForceTimer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected _isStateTriggered(state: CameraTriggerState): boolean {
|
protected _isStateTriggered(state: CameraTriggerState): boolean {
|
||||||
return !!(state.sources.size || state.untriggerDelayTimer);
|
return !!(state.sources.size || state.untriggerDelayTimer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
|
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
|
||||||
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
|
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
|
||||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||||
|
CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS,
|
||||||
} from '../const';
|
} from '../const';
|
||||||
import { arrayify } from '../utils/basic';
|
import { arrayify } from '../utils/basic';
|
||||||
import { AdvancedCameraCardCondition } from './schema/conditions/types';
|
import { AdvancedCameraCardCondition } from './schema/conditions/types';
|
||||||
@@ -960,4 +961,8 @@ const UPGRADES = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
upgradeMoveToWithOverrides(
|
||||||
|
'view.triggers.untrigger_seconds',
|
||||||
|
CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ export const viewConfigDefault = {
|
|||||||
trigger: 'update' as const,
|
trigger: 'update' as const,
|
||||||
untrigger: 'none' as const,
|
untrigger: 'none' as const,
|
||||||
},
|
},
|
||||||
untrigger_seconds: 0,
|
untrigger_delay_seconds: 0,
|
||||||
|
untrigger_force_seconds: 0,
|
||||||
},
|
},
|
||||||
keyboard_shortcuts: keyboardShortcutsDefault,
|
keyboard_shortcuts: keyboardShortcutsDefault,
|
||||||
};
|
};
|
||||||
@@ -93,7 +94,12 @@ export const triggersSchema = z.object({
|
|||||||
show_trigger_status: z
|
show_trigger_status: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.default(viewConfigDefault.triggers.show_trigger_status),
|
.default(viewConfigDefault.triggers.show_trigger_status),
|
||||||
untrigger_seconds: z.number().default(viewConfigDefault.triggers.untrigger_seconds),
|
untrigger_delay_seconds: z
|
||||||
|
.number()
|
||||||
|
.default(viewConfigDefault.triggers.untrigger_delay_seconds),
|
||||||
|
untrigger_force_seconds: z
|
||||||
|
.number()
|
||||||
|
.default(viewConfigDefault.triggers.untrigger_force_seconds),
|
||||||
});
|
});
|
||||||
export type TriggersOptions = z.infer<typeof triggersSchema>;
|
export type TriggersOptions = z.infer<typeof triggersSchema>;
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -180,8 +180,10 @@ export const CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS =
|
|||||||
`${CONF_VIEW_TRIGGERS}.show_trigger_status` as const;
|
`${CONF_VIEW_TRIGGERS}.show_trigger_status` as const;
|
||||||
export const CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA =
|
export const CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA =
|
||||||
`${CONF_VIEW_TRIGGERS}.filter_selected_camera` as const;
|
`${CONF_VIEW_TRIGGERS}.filter_selected_camera` as const;
|
||||||
export const CONF_VIEW_TRIGGERS_UNTRIGGER_SECONDS =
|
export const CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS =
|
||||||
`${CONF_VIEW_TRIGGERS}.untrigger_seconds` as const;
|
`${CONF_VIEW_TRIGGERS}.untrigger_delay_seconds` as const;
|
||||||
|
export const CONF_VIEW_TRIGGERS_UNTRIGGER_FORCE_SECONDS =
|
||||||
|
`${CONF_VIEW_TRIGGERS}.untrigger_force_seconds` as const;
|
||||||
export const CONF_VIEW_TRIGGERS_ACTIONS = `${CONF_VIEW_TRIGGERS}.actions` as const;
|
export const CONF_VIEW_TRIGGERS_ACTIONS = `${CONF_VIEW_TRIGGERS}.actions` as const;
|
||||||
export const CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER =
|
export const CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER =
|
||||||
`${CONF_VIEW_TRIGGERS_ACTIONS}.trigger` as const;
|
`${CONF_VIEW_TRIGGERS_ACTIONS}.trigger` as const;
|
||||||
|
|||||||
+7
-3
@@ -255,7 +255,8 @@ import {
|
|||||||
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
|
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
|
||||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||||
CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS,
|
CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS,
|
||||||
CONF_VIEW_TRIGGERS_UNTRIGGER_SECONDS,
|
CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS,
|
||||||
|
CONF_VIEW_TRIGGERS_UNTRIGGER_FORCE_SECONDS,
|
||||||
FOLDERS_CONFIGURATION_URL,
|
FOLDERS_CONFIGURATION_URL,
|
||||||
MEDIA_CHUNK_SIZE_MAX,
|
MEDIA_CHUNK_SIZE_MAX,
|
||||||
} from './const.js';
|
} from './const.js';
|
||||||
@@ -1390,8 +1391,11 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
|||||||
label: localize(`config.${CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS}`),
|
label: localize(`config.${CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS}`),
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
${this._renderNumberInput(CONF_VIEW_TRIGGERS_UNTRIGGER_SECONDS, {
|
${this._renderNumberInput(CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS, {
|
||||||
default: this._defaults.view.triggers.untrigger_seconds,
|
default: this._defaults.view.triggers.untrigger_delay_seconds,
|
||||||
|
})}
|
||||||
|
${this._renderNumberInput(CONF_VIEW_TRIGGERS_UNTRIGGER_FORCE_SECONDS, {
|
||||||
|
default: this._defaults.view.triggers.untrigger_force_seconds,
|
||||||
})}
|
})}
|
||||||
${this._putInSubmenu(
|
${this._putInSubmenu(
|
||||||
MENU_VIEW_TRIGGERS_ACTIONS,
|
MENU_VIEW_TRIGGERS_ACTIONS,
|
||||||
|
|||||||
@@ -395,7 +395,7 @@
|
|||||||
"editor_label": "Comportament quan s'activa una càmera",
|
"editor_label": "Comportament quan s'activa una càmera",
|
||||||
"filter_selected_camera": "Activa només a la càmera seleccionada",
|
"filter_selected_camera": "Activa només a la càmera seleccionada",
|
||||||
"show_trigger_status": "Mostra la vora intermitent quan s'activa",
|
"show_trigger_status": "Mostra la vora intermitent quan s'activa",
|
||||||
"untrigger_seconds": "Segons després del canvi d'estat inactiu a desactivat"
|
"untrigger_delay_seconds": "Segons després del canvi d'estat inactiu a desactivat"
|
||||||
},
|
},
|
||||||
"views": {
|
"views": {
|
||||||
"clip": "Clip més recent",
|
"clip": "Clip més recent",
|
||||||
|
|||||||
@@ -625,7 +625,8 @@
|
|||||||
"editor_label": "Trigger behavior",
|
"editor_label": "Trigger behavior",
|
||||||
"filter_selected_camera": "Only trigger on selected camera",
|
"filter_selected_camera": "Only trigger on selected camera",
|
||||||
"show_trigger_status": "Show pulsing border when triggered",
|
"show_trigger_status": "Show pulsing border when triggered",
|
||||||
"untrigger_seconds": "Seconds after inactive state change to untrigger"
|
"untrigger_delay_seconds": "Seconds delay after trigger state change before untrigger",
|
||||||
|
"untrigger_force_seconds": "Seconds before forced untrigger"
|
||||||
},
|
},
|
||||||
"views": {
|
"views": {
|
||||||
"auto": "Automatic",
|
"auto": "Automatic",
|
||||||
|
|||||||
@@ -542,7 +542,7 @@
|
|||||||
"editor_label": "Comportement en cas de déclenchement d'une caméra",
|
"editor_label": "Comportement en cas de déclenchement d'une caméra",
|
||||||
"filter_selected_camera": "Déclenchement uniquement sur la caméra sélectionnée",
|
"filter_selected_camera": "Déclenchement uniquement sur la caméra sélectionnée",
|
||||||
"show_trigger_status": "Afficher la bordure clignotante lors du déclenchement",
|
"show_trigger_status": "Afficher la bordure clignotante lors du déclenchement",
|
||||||
"untrigger_seconds": "Quelques secondes après le changement d'état inactif pour débloquer"
|
"untrigger_delay_seconds": "Quelques secondes après le changement d'état inactif pour débloquer"
|
||||||
},
|
},
|
||||||
"views": {
|
"views": {
|
||||||
"clip": "Clip le plus récent",
|
"clip": "Clip le plus récent",
|
||||||
|
|||||||
@@ -283,7 +283,7 @@
|
|||||||
},
|
},
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"show_trigger_status": "Mostra bordo pulsante quando attivato",
|
"show_trigger_status": "Mostra bordo pulsante quando attivato",
|
||||||
"untrigger_seconds": "Reimposta la vista ai valori predefiniti dopo aver annullato l'attivazione"
|
"untrigger_delay_seconds": "Reimposta la vista ai valori predefiniti dopo aver annullato l'attivazione"
|
||||||
},
|
},
|
||||||
"views": {
|
"views": {
|
||||||
"clip": "Clip più recente",
|
"clip": "Clip più recente",
|
||||||
|
|||||||
@@ -592,7 +592,7 @@
|
|||||||
"editor_label": "Zachowanie wyzwalaczy",
|
"editor_label": "Zachowanie wyzwalaczy",
|
||||||
"filter_selected_camera": "Wyzwalaj tylko na wybranej kamerze",
|
"filter_selected_camera": "Wyzwalaj tylko na wybranej kamerze",
|
||||||
"show_trigger_status": "Pokaż pulsującą ramkę przy wyzwoleniu",
|
"show_trigger_status": "Pokaż pulsującą ramkę przy wyzwoleniu",
|
||||||
"untrigger_seconds": "Sekundy po ustaniu stanu, aby dezaktywować"
|
"untrigger_delay_seconds": "Sekundy po ustaniu stanu, aby dezaktywować"
|
||||||
},
|
},
|
||||||
"views": {
|
"views": {
|
||||||
"clip": "Ostatni klip",
|
"clip": "Ostatni klip",
|
||||||
|
|||||||
@@ -286,7 +286,7 @@
|
|||||||
},
|
},
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"show_trigger_status": "Pulsar borda quando acionado",
|
"show_trigger_status": "Pulsar borda quando acionado",
|
||||||
"untrigger_seconds": "Segundos após a mudar para o estado inativo para desacionar"
|
"untrigger_delay_seconds": "Segundos após a mudar para o estado inativo para desacionar"
|
||||||
},
|
},
|
||||||
"views": {
|
"views": {
|
||||||
"clip": "Clipe mais recente",
|
"clip": "Clipe mais recente",
|
||||||
|
|||||||
@@ -283,7 +283,7 @@
|
|||||||
},
|
},
|
||||||
"triggers": {
|
"triggers": {
|
||||||
"show_trigger_status": "Exibir estado do gatilho",
|
"show_trigger_status": "Exibir estado do gatilho",
|
||||||
"untrigger_seconds": "Segundos após a mudar para o estado inativo para desacionar"
|
"untrigger_delay_seconds": "Segundos após a mudar para o estado inativo para desacionar"
|
||||||
},
|
},
|
||||||
"views": {
|
"views": {
|
||||||
"clip": "Clipe mais recente",
|
"clip": "Clipe mais recente",
|
||||||
|
|||||||
@@ -1280,6 +1280,7 @@ describe('FrigateCamera', () => {
|
|||||||
expect(eventCallback).toBeCalledWith({
|
expect(eventCallback).toBeCalledWith({
|
||||||
type: 'new',
|
type: 'new',
|
||||||
cameraID: 'CAMERA_1',
|
cameraID: 'CAMERA_1',
|
||||||
|
id: '123',
|
||||||
fidelity: 'high',
|
fidelity: 'high',
|
||||||
review: true,
|
review: true,
|
||||||
});
|
});
|
||||||
@@ -1351,6 +1352,7 @@ describe('FrigateCamera', () => {
|
|||||||
expect(eventCallback).toBeCalledWith({
|
expect(eventCallback).toBeCalledWith({
|
||||||
type: 'update',
|
type: 'update',
|
||||||
cameraID: 'CAMERA_1',
|
cameraID: 'CAMERA_1',
|
||||||
|
id: '123',
|
||||||
fidelity: 'high',
|
fidelity: 'high',
|
||||||
review: true,
|
review: true,
|
||||||
});
|
});
|
||||||
@@ -1422,6 +1424,7 @@ describe('FrigateCamera', () => {
|
|||||||
expect(eventCallback).toBeCalledWith({
|
expect(eventCallback).toBeCalledWith({
|
||||||
type: 'update',
|
type: 'update',
|
||||||
cameraID: 'CAMERA_1',
|
cameraID: 'CAMERA_1',
|
||||||
|
id: '123',
|
||||||
fidelity: 'high',
|
fidelity: 'high',
|
||||||
review: true,
|
review: true,
|
||||||
});
|
});
|
||||||
@@ -1785,6 +1788,7 @@ describe('FrigateCamera', () => {
|
|||||||
expect(eventCallback).toBeCalledWith({
|
expect(eventCallback).toBeCalledWith({
|
||||||
type: 'end',
|
type: 'end',
|
||||||
cameraID: 'CAMERA_1',
|
cameraID: 'CAMERA_1',
|
||||||
|
id: '123',
|
||||||
fidelity: 'high',
|
fidelity: 'high',
|
||||||
review: true,
|
review: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ vi.mock('lodash-es', async () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const baseTriggersConfig: TriggersOptions = {
|
const baseTriggersConfig: TriggersOptions = {
|
||||||
untrigger_seconds: 10,
|
untrigger_delay_seconds: 10,
|
||||||
|
untrigger_force_seconds: 0,
|
||||||
filter_selected_camera: false,
|
filter_selected_camera: false,
|
||||||
show_trigger_status: false,
|
show_trigger_status: false,
|
||||||
actions: {
|
actions: {
|
||||||
@@ -515,10 +516,36 @@ describe('TriggersManager', () => {
|
|||||||
expect(manager.getTriggeredCameraIDs()).toEqual(new Set());
|
expect(manager.getTriggeredCameraIDs()).toEqual(new Set());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should untrigger immediately when untrigger_seconds is 0', async () => {
|
it('should stay triggered during the untrigger delay', async () => {
|
||||||
const api = createTriggerAPI({
|
const api = createTriggerAPI({
|
||||||
config: {
|
config: {
|
||||||
untrigger_seconds: 0,
|
untrigger_delay_seconds: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
|
||||||
|
// 1. Trigger the camera.
|
||||||
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'new' });
|
||||||
|
expect(manager.isTriggered()).toBe(true);
|
||||||
|
|
||||||
|
// 2. End the event.
|
||||||
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'end' });
|
||||||
|
|
||||||
|
// 3. Verify it's STILL triggered during the delay.
|
||||||
|
expect(manager.isTriggered()).toBe(true);
|
||||||
|
|
||||||
|
// 4. Fast forward and verify it untriggers.
|
||||||
|
vi.setSystemTime(add(start, { seconds: 15 }));
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(manager.isTriggered()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should untrigger immediately when untrigger_delay_seconds is 0', async () => {
|
||||||
|
const api = createTriggerAPI({
|
||||||
|
config: {
|
||||||
|
untrigger_delay_seconds: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const manager = new TriggersManager(api);
|
const manager = new TriggersManager(api);
|
||||||
@@ -537,6 +564,119 @@ describe('TriggersManager', () => {
|
|||||||
expect(manager.isTriggered()).toBeFalsy();
|
expect(manager.isTriggered()).toBeFalsy();
|
||||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should force untrigger when untrigger_force_seconds expires', async () => {
|
||||||
|
const api = createTriggerAPI({
|
||||||
|
config: {
|
||||||
|
untrigger_delay_seconds: 0,
|
||||||
|
untrigger_force_seconds: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
|
||||||
|
await manager.handleCameraEvent({
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
id: 'event-1',
|
||||||
|
type: 'new',
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(5000);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(manager.isTriggered()).toBeFalsy();
|
||||||
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not reset force timer on source update', async () => {
|
||||||
|
const api = createTriggerAPI({
|
||||||
|
config: {
|
||||||
|
untrigger_delay_seconds: 0,
|
||||||
|
untrigger_force_seconds: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
|
||||||
|
await manager.handleCameraEvent({
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
id: 'event-1',
|
||||||
|
type: 'new',
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(5000);
|
||||||
|
await manager.handleCameraEvent({
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
id: 'event-1',
|
||||||
|
type: 'update',
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(4900);
|
||||||
|
await flushPromises();
|
||||||
|
expect(manager.isTriggered()).toBeTruthy();
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(100);
|
||||||
|
await flushPromises();
|
||||||
|
expect(manager.isTriggered()).toBeFalsy();
|
||||||
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should force untrigger all sources when one force timer expires', async () => {
|
||||||
|
const api = createTriggerAPI({
|
||||||
|
config: {
|
||||||
|
untrigger_delay_seconds: 0,
|
||||||
|
untrigger_force_seconds: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
|
||||||
|
await manager.handleCameraEvent({
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
id: 'source-1',
|
||||||
|
type: 'new',
|
||||||
|
});
|
||||||
|
await manager.handleCameraEvent({
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
id: 'source-2',
|
||||||
|
type: 'new',
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(5000);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(manager.isTriggered()).toBeFalsy();
|
||||||
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not extend force timer when a second source triggers later', async () => {
|
||||||
|
const api = createTriggerAPI({
|
||||||
|
config: {
|
||||||
|
untrigger_delay_seconds: 0,
|
||||||
|
untrigger_force_seconds: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
|
||||||
|
await manager.handleCameraEvent({
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
id: 'source-1',
|
||||||
|
type: 'new',
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(5000);
|
||||||
|
await manager.handleCameraEvent({
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
id: 'source-2',
|
||||||
|
type: 'new',
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(4900);
|
||||||
|
await flushPromises();
|
||||||
|
expect(manager.isTriggered()).toBeTruthy();
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(100);
|
||||||
|
await flushPromises();
|
||||||
|
expect(manager.isTriggered()).toBeFalsy();
|
||||||
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('condition state management', () => {
|
describe('condition state management', () => {
|
||||||
@@ -1178,7 +1318,7 @@ describe('TriggersManager', () => {
|
|||||||
it('should not include cameras with no active sources in triggered IDs even before untrigger action completes', async () => {
|
it('should not include cameras with no active sources in triggered IDs even before untrigger action completes', async () => {
|
||||||
const api = createTriggerAPI({
|
const api = createTriggerAPI({
|
||||||
config: {
|
config: {
|
||||||
untrigger_seconds: 0,
|
untrigger_delay_seconds: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const manager = new TriggersManager(api);
|
const manager = new TriggersManager(api);
|
||||||
@@ -1218,4 +1358,168 @@ describe('TriggersManager', () => {
|
|||||||
// But the camera should be untriggered.
|
// But the camera should be untriggered.
|
||||||
expect(manager.isTriggered()).toBe(false);
|
expect(manager.isTriggered()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ignore list behavior', () => {
|
||||||
|
it('should ignore updates for event IDs that have been forcibly untriggered', async () => {
|
||||||
|
const api = createTriggerAPI({
|
||||||
|
config: {
|
||||||
|
untrigger_delay_seconds: 0,
|
||||||
|
untrigger_force_seconds: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
|
||||||
|
// 1. Initial trigger.
|
||||||
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'new' });
|
||||||
|
|
||||||
|
// 2. Fast forward to force untrigger.
|
||||||
|
vi.setSystemTime(add(start, { seconds: 15 }));
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(manager.isTriggered()).toBe(false);
|
||||||
|
|
||||||
|
// 3. Send an update for the same ID. It should be ignored.
|
||||||
|
const result = await manager.handleCameraEvent({
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
id: 'e1',
|
||||||
|
type: 'update',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(manager.isTriggered()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should un-ignore an event ID once an end event arrives', async () => {
|
||||||
|
const api = createTriggerAPI({
|
||||||
|
config: {
|
||||||
|
untrigger_force_seconds: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
|
||||||
|
// 1. Trigger and force untrigger.
|
||||||
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'new' });
|
||||||
|
vi.setSystemTime(add(start, { seconds: 15 }));
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// 2. Send 'end'. This should clear from ignore list even if state was already deleted.
|
||||||
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'end' });
|
||||||
|
|
||||||
|
// 3. Send a new trigger for the same ID. It should be accepted.
|
||||||
|
const result = await manager.handleCameraEvent({
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
id: 'e1',
|
||||||
|
type: 'new',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(manager.isTriggered()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process updates for event IDs that were never ignored', async () => {
|
||||||
|
const api = createTriggerAPI({
|
||||||
|
config: {
|
||||||
|
actions: {
|
||||||
|
trigger: 'update',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
|
||||||
|
// Simulate an update for a brand new ID (e.g. from an engine we just switched to).
|
||||||
|
// Even without a 'new' event, if it's not in our ignore list, it should be processed.
|
||||||
|
const result = await manager.handleCameraEvent({
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
id: 'new-engine-event',
|
||||||
|
type: 'update',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(manager.isTriggered()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not suppress same event ID on a different camera', async () => {
|
||||||
|
const api = createTriggerAPI({
|
||||||
|
config: {
|
||||||
|
untrigger_delay_seconds: 0,
|
||||||
|
untrigger_force_seconds: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
|
||||||
|
// Force-ignore e1 on camera_1.
|
||||||
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'new' });
|
||||||
|
vi.setSystemTime(add(start, { seconds: 15 }));
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// Same ID on a different camera should not be ignored.
|
||||||
|
const result = await manager.handleCameraEvent({
|
||||||
|
cameraID: 'camera_2',
|
||||||
|
id: 'e1',
|
||||||
|
type: 'update',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(manager.getTriggeredCameraIDs()).toContain('camera_2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly prune ignore list for a camera with multiple ignored IDs', async () => {
|
||||||
|
const api = createTriggerAPI({
|
||||||
|
config: {
|
||||||
|
untrigger_delay_seconds: 0,
|
||||||
|
untrigger_force_seconds: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
|
||||||
|
// Force-ignore e1 and e2 on camera_1.
|
||||||
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'new' });
|
||||||
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e2', type: 'new' });
|
||||||
|
vi.setSystemTime(add(start, { seconds: 15 }));
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// End e1. The set should still exist because e2 is still ignored.
|
||||||
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'end' });
|
||||||
|
|
||||||
|
// Verify e2 is still ignored.
|
||||||
|
const result = await manager.handleCameraEvent({
|
||||||
|
cameraID: 'camera_1',
|
||||||
|
id: 'e2',
|
||||||
|
type: 'update',
|
||||||
|
});
|
||||||
|
expect(result).toBe(false);
|
||||||
|
|
||||||
|
// End e2. The set should now be deleted.
|
||||||
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e2', type: 'end' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should safely handle events with unknown camera IDs', async () => {
|
||||||
|
const api = createTriggerAPI({
|
||||||
|
config: {
|
||||||
|
untrigger_delay_seconds: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const manager = new TriggersManager(api);
|
||||||
|
|
||||||
|
// Call handleCameraEvent with an unknown camera ID to ensure guard behavior.
|
||||||
|
const result = await manager.handleCameraEvent({
|
||||||
|
cameraID: 'unknown',
|
||||||
|
id: 'e1',
|
||||||
|
type: 'new',
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(manager.getTriggeredCameraIDs()).toContain('unknown');
|
||||||
|
|
||||||
|
await manager.handleCameraEvent({
|
||||||
|
cameraID: 'unknown',
|
||||||
|
id: 'e1',
|
||||||
|
type: 'end',
|
||||||
|
});
|
||||||
|
expect(manager.getTriggeredCameraIDs()).not.toContain('unknown');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3690,5 +3690,62 @@ describe('should handle version specific upgrades', () => {
|
|||||||
postUpgradeChecks(config);
|
postUpgradeChecks(config);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('view.triggers.untrigger_seconds -> view.triggers.untrigger_delay_seconds', () => {
|
||||||
|
const config = {
|
||||||
|
type: 'custom:advanced-camera-card',
|
||||||
|
cameras: [{}],
|
||||||
|
view: {
|
||||||
|
triggers: {
|
||||||
|
untrigger_seconds: 42,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'media_loaded' as const,
|
||||||
|
media_loaded: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
merge: {
|
||||||
|
view: {
|
||||||
|
triggers: {
|
||||||
|
untrigger_seconds: 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(upgradeConfig(config)).toBeTruthy();
|
||||||
|
expect(config).toEqual({
|
||||||
|
type: 'custom:advanced-camera-card',
|
||||||
|
cameras: [{}],
|
||||||
|
view: {
|
||||||
|
triggers: {
|
||||||
|
untrigger_delay_seconds: 42,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
condition: 'media_loaded' as const,
|
||||||
|
media_loaded: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
merge: {
|
||||||
|
view: {
|
||||||
|
triggers: {
|
||||||
|
untrigger_delay_seconds: 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
postUpgradeChecks(config);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -408,7 +408,8 @@ describe('config defaults', () => {
|
|||||||
},
|
},
|
||||||
triggers: {
|
triggers: {
|
||||||
show_trigger_status: false,
|
show_trigger_status: false,
|
||||||
untrigger_seconds: 0,
|
untrigger_delay_seconds: 0,
|
||||||
|
untrigger_force_seconds: 0,
|
||||||
actions: {
|
actions: {
|
||||||
trigger: 'update',
|
trigger: 'update',
|
||||||
untrigger: 'none',
|
untrigger: 'none',
|
||||||
|
|||||||
Reference in New Issue
Block a user