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:
@@ -569,6 +569,7 @@ export class FrigateCamera extends Camera {
|
||||
|
||||
this._eventCallback?.({
|
||||
cameraID,
|
||||
id: review.after.id,
|
||||
fidelity: 'high',
|
||||
type: review.type,
|
||||
review: true,
|
||||
|
||||
@@ -13,8 +13,17 @@ interface CameraTriggerState {
|
||||
// IDs).
|
||||
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.
|
||||
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 {
|
||||
@@ -55,6 +64,7 @@ export class TriggersManager {
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
let triggered = false;
|
||||
let startupActionEvent: CameraEvent | null = null;
|
||||
this._states.clear();
|
||||
|
||||
for (const [cameraID, camera] of this._api
|
||||
.getCameraManager()
|
||||
@@ -97,12 +107,13 @@ export class TriggersManager {
|
||||
): Promise<boolean> {
|
||||
const skipAction = options?.skipAction ?? false;
|
||||
if (ev.type === 'end') {
|
||||
const state = this._states.get(ev.cameraID);
|
||||
state?.sources.delete(ev.id);
|
||||
if (!state?.sources.size) {
|
||||
await this._startUntrigger(ev.cameraID);
|
||||
}
|
||||
return true;
|
||||
return this._handleEndEvent(ev);
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -122,20 +133,15 @@ export class TriggersManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
let state = this._states.get(ev.cameraID);
|
||||
if (!state) {
|
||||
state = {
|
||||
lastTriggerTime: new Date(),
|
||||
sources: new Set(),
|
||||
};
|
||||
this._states.set(ev.cameraID, state);
|
||||
} else {
|
||||
state.lastTriggerTime = new Date();
|
||||
}
|
||||
|
||||
const state = this._getOrCreateState(ev.cameraID);
|
||||
state.lastTriggerTime = new Date();
|
||||
state.sources.add(ev.id);
|
||||
|
||||
this._deleteUntriggerDelayTimer(ev.cameraID);
|
||||
this._startForceUntriggerTimerIfNecessary(
|
||||
ev.cameraID,
|
||||
triggersConfig.untrigger_force_seconds,
|
||||
);
|
||||
this._setConditionStateIfNecessary();
|
||||
if (!skipAction) {
|
||||
await this._throttledTriggerAction(ev);
|
||||
@@ -143,6 +149,24 @@ export class TriggersManager {
|
||||
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 {
|
||||
const triggersConfig = this._api.getConfigManager().getConfig()?.view.triggers;
|
||||
const hasInteraction = this._api.getInteractionManager().hasInteraction();
|
||||
@@ -247,9 +271,10 @@ export class TriggersManager {
|
||||
|
||||
protected async _untriggerAction(cameraID: string): Promise<void> {
|
||||
this._deleteUntriggerDelayTimer(cameraID);
|
||||
this._deleteForceUntriggerTimer(cameraID);
|
||||
|
||||
await this._executeUntriggerAction();
|
||||
this._states.delete(cameraID);
|
||||
this._deleteStateIfIdle(cameraID);
|
||||
|
||||
this._setConditionStateIfNecessary();
|
||||
|
||||
@@ -259,6 +284,7 @@ export class TriggersManager {
|
||||
|
||||
protected async _startUntrigger(cameraID: string): Promise<void> {
|
||||
this._deleteUntriggerDelayTimer(cameraID);
|
||||
this._deleteForceUntriggerTimer(cameraID);
|
||||
|
||||
const state = this._states.get(cameraID);
|
||||
if (!state) {
|
||||
@@ -266,11 +292,11 @@ export class TriggersManager {
|
||||
}
|
||||
|
||||
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.start(untriggerSeconds, async () => {
|
||||
state.untriggerDelayTimer.start(untriggerDelaySeconds, async () => {
|
||||
await this._untriggerAction(cameraID);
|
||||
});
|
||||
} 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 {
|
||||
const state = this._states.get(cameraID);
|
||||
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 {
|
||||
return !!(state.sources.size || state.untriggerDelayTimer);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
|
||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||
CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS,
|
||||
} from '../const';
|
||||
import { arrayify } from '../utils/basic';
|
||||
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,
|
||||
untrigger: 'none' as const,
|
||||
},
|
||||
untrigger_seconds: 0,
|
||||
untrigger_delay_seconds: 0,
|
||||
untrigger_force_seconds: 0,
|
||||
},
|
||||
keyboard_shortcuts: keyboardShortcutsDefault,
|
||||
};
|
||||
@@ -93,7 +94,12 @@ export const triggersSchema = z.object({
|
||||
show_trigger_status: z
|
||||
.boolean()
|
||||
.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>;
|
||||
|
||||
|
||||
+4
-2
@@ -180,8 +180,10 @@ export const CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS =
|
||||
`${CONF_VIEW_TRIGGERS}.show_trigger_status` as const;
|
||||
export const CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA =
|
||||
`${CONF_VIEW_TRIGGERS}.filter_selected_camera` as const;
|
||||
export const CONF_VIEW_TRIGGERS_UNTRIGGER_SECONDS =
|
||||
`${CONF_VIEW_TRIGGERS}.untrigger_seconds` as const;
|
||||
export const CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS =
|
||||
`${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_TRIGGER =
|
||||
`${CONF_VIEW_TRIGGERS_ACTIONS}.trigger` as const;
|
||||
|
||||
+7
-3
@@ -255,7 +255,8 @@ import {
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
|
||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||
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,
|
||||
MEDIA_CHUNK_SIZE_MAX,
|
||||
} from './const.js';
|
||||
@@ -1390,8 +1391,11 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
label: localize(`config.${CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS}`),
|
||||
},
|
||||
)}
|
||||
${this._renderNumberInput(CONF_VIEW_TRIGGERS_UNTRIGGER_SECONDS, {
|
||||
default: this._defaults.view.triggers.untrigger_seconds,
|
||||
${this._renderNumberInput(CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_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(
|
||||
MENU_VIEW_TRIGGERS_ACTIONS,
|
||||
|
||||
@@ -395,7 +395,7 @@
|
||||
"editor_label": "Comportament quan s'activa una càmera",
|
||||
"filter_selected_camera": "Activa només a la càmera seleccionada",
|
||||
"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": {
|
||||
"clip": "Clip més recent",
|
||||
|
||||
@@ -625,7 +625,8 @@
|
||||
"editor_label": "Trigger behavior",
|
||||
"filter_selected_camera": "Only trigger on selected camera",
|
||||
"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": {
|
||||
"auto": "Automatic",
|
||||
|
||||
@@ -542,7 +542,7 @@
|
||||
"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",
|
||||
"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": {
|
||||
"clip": "Clip le plus récent",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
},
|
||||
"triggers": {
|
||||
"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": {
|
||||
"clip": "Clip più recente",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"editor_label": "Zachowanie wyzwalaczy",
|
||||
"filter_selected_camera": "Wyzwalaj tylko na wybranej kamerze",
|
||||
"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": {
|
||||
"clip": "Ostatni klip",
|
||||
|
||||
@@ -286,7 +286,7 @@
|
||||
},
|
||||
"triggers": {
|
||||
"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": {
|
||||
"clip": "Clipe mais recente",
|
||||
|
||||
@@ -283,7 +283,7 @@
|
||||
},
|
||||
"triggers": {
|
||||
"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": {
|
||||
"clip": "Clipe mais recente",
|
||||
|
||||
Reference in New Issue
Block a user