Initial version of more advanced scan mode control.

This commit is contained in:
Dermot Duffy
2024-01-17 22:07:30 -08:00
parent ad1c323cb0
commit 278f4f372d
24 changed files with 648 additions and 327 deletions
+30 -4
View File
@@ -1,11 +1,13 @@
import {
Actions,
ActionsConfig,
ActionType,
FrigateCardCustomAction,
FRIGATE_CARD_VIEW_DEFAULT,
} from '../config/types.js';
import {
convertActionToFrigateCardCustomAction,
frigateCardHandleAction,
frigateCardHandleActionConfig,
getActionConfigGivenAction,
} from '../utils/action.js';
@@ -81,16 +83,36 @@ export class ActionsManager {
const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail);
if (frigateCardAction) {
this.executeAction(frigateCardAction);
this.executeFrigateAction(frigateCardAction);
}
};
/**
* Small convenience method to call frigateCardHandleAction without the caller
* needing hass or the element.
*/
public executeActions(actions: ActionType | ActionType[]): void {
const hass = this._api.getHASSManager().getHASS();
if (!hass) {
return;
}
frigateCardHandleAction(
this._api.getCardElementManager().getElement(),
hass,
{},
actions,
);
}
/**
* Execute a card action.
* @param frigateCardAction
* @returns `true` if an action is executed.
*/
public async executeAction(frigateCardAction: FrigateCardCustomAction): Promise<void> {
public async executeFrigateAction(
frigateCardAction: FrigateCardCustomAction,
): Promise<void> {
const config = this._api.getConfigManager().getConfig();
const mediaLoadedInfoManager = this._api.getMediaLoadedInfoManager();
@@ -146,8 +168,12 @@ export class ActionsManager {
this._api.getCardElementManager().toggleMenu();
break;
case 'camera_select':
const selectCameraID = frigateCardAction.camera;
if (view) {
const selectCameraID =
frigateCardAction.camera ??
(frigateCardAction.triggered
? this._api.getTriggersManager().getMostRecentlyTriggeredCameraID()
: null);
if (selectCameraID && view) {
const viewOnCameraSelect = config?.view.camera_select ?? 'current';
const targetViewName =
viewOnCameraSelect === 'current' ? view.view : viewOnCameraSelect;
+1 -1
View File
@@ -74,7 +74,7 @@ export class HASSManager {
this._api.getCardElementManager().update();
}
this._api.getTriggersManager().updateTriggeredCameras(oldHass);
this._api.getTriggersManager().updateTriggerHAState(oldHass);
if (this._api.getConditionsManager().hasHAStateConditions()) {
this._api.getConditionsManager().setState({ state: this._hass.states });
@@ -27,9 +27,6 @@ export class InteractionManager {
protected _reportInteraction(): void {
this._timer.stop();
// Interactions reset the trigger state.
this._api.getTriggersManager().untrigger();
const timeoutSeconds = this._api.getConfigManager().getConfig()
?.view.timeout_seconds;
+5 -39
View File
@@ -1,5 +1,8 @@
import { FrigateCardCustomAction, FrigateCardViewAction } from '../config/types';
import { createFrigateCardCameraAction, createFrigateCardSimpleAction } from '../utils/action.js';
import {
createFrigateCardCameraAction,
createFrigateCardSimpleAction
} from '../utils/action.js';
import { CardQueryStringAPI } from './types';
import { ViewManagerSetViewParameters } from './view-manager';
@@ -35,43 +38,6 @@ export class QueryStringManager {
this._executeNonViewRelated(intent);
};
public generateQueryString(action: FrigateCardCustomAction): string | null {
const baseKey =
'frigate-card-action.' + (action.card_id ? `${action.card_id}.` : '');
switch (action.frigate_card_action) {
case 'camera_select':
case 'live_substream_select':
return new URLSearchParams([
[baseKey + action.frigate_card_action, action.camera],
]).toString();
case 'camera_ui':
case 'clip':
case 'clips':
case 'default':
case 'diagnostics':
case 'download':
case 'expand':
case 'image':
case 'live':
case 'menu_toggle':
case 'recording':
case 'recordings':
case 'snapshot':
case 'snapshots':
case 'timeline':
return new URLSearchParams([
[baseKey + action.frigate_card_action, ''],
]).toString();
default:
console.warn(
`Frigate card cannot convert unsupported action to query string:`,
action,
);
}
return null;
}
protected _executeViewRelated(intent: QueryStringViewIntent): void {
if (intent.view) {
if (intent.view.default) {
@@ -96,7 +62,7 @@ export class QueryStringManager {
}
intent.other?.forEach((action) =>
this._api.getActionsManager().executeAction(action),
this._api.getActionsManager().executeFrigateAction(action),
);
}
+98 -64
View File
@@ -2,30 +2,44 @@ import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
import orderBy from 'lodash-es/orderBy';
import { getHassDifferences, isTriggeredState } from '../utils/ha';
import { Timer } from '../utils/timer';
import { View } from '../view/view';
import { CardTriggersAPI } from './types';
export class TriggersManager {
protected _api: CardTriggersAPI;
protected _triggers: Map<string, Date> = new Map();
protected _untriggerTimer = new Timer();
protected _triggeredCameras: Map<string, Date> = new Map();
protected _triggeredCameraTimers: Map<string, Timer> = new Map();
protected _triggeredState: Set<string> = new Set();
constructor(api: CardTriggersAPI) {
this._api = api;
}
public isTriggered(): boolean {
return !!this._triggers.size || this._untriggerTimer.isRunning();
public getTriggeredCameraIDs(): Set<string> {
return new Set(this._triggeredCameras.keys());
}
public updateTriggeredCameras(oldHass?: HomeAssistant | null): boolean {
if (!this._shouldTrackTriggers()) {
return false;
public isTriggered(): boolean {
return !!this._triggeredCameras.size;
}
public getMostRecentlyTriggeredCameraID(): string | null {
const sorted = orderBy(
[...this._triggeredCameras.entries()],
(entry) => entry[1].getTime(),
'desc',
);
return sorted.length ? sorted[0][0] : null;
}
public updateTriggerHAState(oldHass?: HomeAssistant | null): void {
const scanConfig = this._api.getConfigManager().getConfig()?.view.scan;
if (!scanConfig || !scanConfig.enabled) {
return;
}
const hass = this._api.getHASSManager().getHASS();
const now = new Date();
let triggerChanges = false;
const visibleCameraIDs = this._api
@@ -45,88 +59,108 @@ export class TriggersManager {
(entity) => !isTriggeredState(hass?.states[entity]),
);
if (shouldTrigger) {
this._triggers.set(cameraID, now);
this._triggeredState.add(cameraID);
triggerChanges = true;
} else if (shouldUntrigger && this._triggers.has(cameraID)) {
this._triggers.delete(cameraID);
} else if (shouldUntrigger && this._triggeredState.has(cameraID)) {
this._triggeredState.delete(cameraID);
triggerChanges = true;
}
}
if (triggerChanges) {
const targetCameraID = this._getMostRecentTrigger();
if (targetCameraID) {
this._triggerAction(targetCameraID);
return true;
} else {
this._startUntriggerTimer();
this._evaluateTriggers();
}
}
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`).
this._evaluateTriggers();
}
}
protected _evaluateTriggers(): void {
const scanConfig = this._api.getConfigManager().getConfig()?.view.scan;
if (!scanConfig) {
return;
}
const now = new Date();
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))
) {
this._triggeredCameras.set(cameraID, now);
this._triggerAction();
}
}
return false;
}
public untrigger(): void {
const wasTriggered = this.isTriggered();
this._triggers.clear();
this._untriggerTimer.stop();
if (wasTriggered) {
this._untriggerAction();
for (const cameraID of this._triggeredCameras.keys()) {
if (!this._triggeredState.has(cameraID)) {
this._startUntriggerTimer(cameraID);
}
}
}
protected _triggerAction(cameraID: string): void {
const view = this._api.getViewManager().getView();
if (
this._isAutomatedViewUpdateAllowed() &&
(view?.camera !== cameraID || !view?.is('live'))
) {
this._api.getViewManager().setViewByParameters({
viewName: 'live',
cameraID: cameraID,
});
}
}
protected _hasAllowableInteractionState(): boolean {
const scanConfig = this._api.getConfigManager().getConfig()?.view.scan;
const hasInteraction = this._api.getInteractionManager().hasInteraction();
protected _untriggerAction(): void {
if (
!this.isTriggered() &&
this._isAutomatedViewUpdateAllowed() &&
this._api.getConfigManager().getConfig()?.view.scan.untrigger_reset
) {
this._api.getViewManager().setViewDefault();
}
}
protected _isAutomatedViewUpdateAllowed(): boolean {
return (
this._api.getConfigManager().getConfig()?.view.update_force ||
!this._api.getInteractionManager().hasInteraction()
!!scanConfig &&
(scanConfig.interaction_mode === 'all' ||
(scanConfig.interaction_mode === 'active' && hasInteraction) ||
(scanConfig.interaction_mode === 'inactive' && !hasInteraction))
);
}
protected _shouldTrackTriggers(): boolean {
return !!this._api.getConfigManager().getConfig()?.view.scan.enabled;
protected _triggerAction(): void {
const action = this._api.getConfigManager().getConfig()?.view.scan.trigger_action;
if (action && this._hasAllowableInteractionState()) {
this._api.getActionsManager().executeActions(action);
}
// Must update master element to add border pulsing.
this._api.getCardElementManager().update();
}
protected _startUntriggerTimer(): void {
this._untriggerTimer.start(
protected _untriggerAction(cameraID: string): void {
const action = this._api.getConfigManager().getConfig()?.view.scan.untrigger_action;
if (action && this._hasAllowableInteractionState()) {
this._api.getActionsManager().executeActions(action);
}
this._triggeredCameras.delete(cameraID);
this._deleteTimer(cameraID);
// Must update master element to remove border pulsing.
this._api.getCardElementManager().update();
}
protected _startUntriggerTimer(cameraID: string): void {
this._deleteTimer(cameraID);
const timer = new Timer();
this._triggeredCameraTimers.set(cameraID, timer);
timer.start(
/* istanbul ignore next: the case of config being null here cannot be
reached, as there's no way to have the untrigger call happen without
a config. -- @preserve */
this._api.getConfigManager().getConfig()?.view.scan.untrigger_seconds ?? 0,
() => {
this._untriggerAction();
this._untriggerAction(cameraID);
},
);
}
protected _getMostRecentTrigger(): string | null {
const sorted = orderBy(
[...this._triggers.entries()],
(entry) => entry[1].getTime(),
'desc',
);
return sorted.length ? sorted[0][0] : null;
protected _deleteTimer(cameraID: string): void {
this._triggeredCameraTimers.get(cameraID)?.stop();
this._triggeredCameraTimers.delete(cameraID);
}
}
+17 -13
View File
@@ -41,8 +41,9 @@ export interface CardActionsManagerAPI {
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
getMediaPlayerManager(): MediaPlayerManager;
getMessageManager(): MessageManager;
getViewManager(): ViewManager;
getMicrophoneManager(): MicrophoneManager;
getTriggersManager(): TriggersManager;
getViewManager(): ViewManager;
}
export interface CardAutomationsAPI {
@@ -55,17 +56,17 @@ export interface CardAutomationsAPI {
export interface CardAutoRefreshAPI {
getConfigManager(): ConfigManager;
getViewManager(): ViewManager;
getTriggersManager(): TriggersManager;
getInteractionManager(): InteractionManager;
getTriggersManager(): TriggersManager;
getViewManager(): ViewManager;
}
export interface CardCameraAPI {
getConfigManager(): ConfigManager;
getEntityRegistryManager(): EntityRegistryManager;
getResolvedMediaCache(): ResolvedMediaCache;
getHASSManager(): HASSManager;
getMessageManager(): MessageManager;
getResolvedMediaCache(): ResolvedMediaCache;
}
export interface CardCameraURLAPI {
@@ -106,15 +107,15 @@ export interface CardElementAPI {
}
export interface CardExpandAPI {
getFullscreenManager(): FullscreenManager;
getConditionsManager(): ConditionsManager;
getCardElementManager(): CardElementManager;
getConditionsManager(): ConditionsManager;
getFullscreenManager(): FullscreenManager;
}
export interface CardFullscreenAPI {
getCardElementManager(): CardElementManager;
getExpandManager(): ExpandManager;
getConditionsManager(): ConditionsManager;
getExpandManager(): ExpandManager;
getMediaPlayerManager(): MediaPlayerManager;
}
@@ -153,23 +154,23 @@ export interface CardInteractionAPI {
}
export interface CardMediaLoadedAPI {
getCardElementManager(): CardElementManager;
getConditionsManager(): ConditionsManager;
getConfigManager(): ConfigManager;
getCardElementManager(): CardElementManager;
getStyleManager(): StyleManager;
}
export interface CardMediaPlayerAPI {
getHASSManager(): HASSManager;
getCameraManager(): CameraManager;
getEntityRegistryManager(): EntityRegistryManager;
getHASSManager(): HASSManager;
getMessageManager(): MessageManager;
getQueryStringManager(): QueryStringManager;
}
export interface CardMessageAPI {
getConditionsManager(): ConditionsManager;
getCardElementManager(): CardElementManager;
getConditionsManager(): ConditionsManager;
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
}
@@ -179,9 +180,9 @@ export interface CardMicrophoneAPI {
}
export interface CardQueryStringAPI {
getActionsManager(): ActionsManager;
getCardElementManager(): CardElementManager;
getViewManager(): ViewManager;
getActionsManager(): ActionsManager;
}
export interface CardStyleAPI {
@@ -196,7 +197,9 @@ export interface CardStyleAPI {
}
export interface CardTriggersAPI {
getActionsManager(): ActionsManager;
getCameraManager(): CameraManager;
getCardElementManager(): CardElementManager;
getConfigManager(): ConfigManager;
getHASSManager(): HASSManager;
getInteractionManager(): InteractionManager;
@@ -206,11 +209,12 @@ export interface CardTriggersAPI {
export interface CardViewAPI {
getAutoUpdateManager(): AutoUpdateManager;
getCameraManager(): CameraManager;
getCardElementManager(): CardElementManager;
getConditionsManager(): ConditionsManager;
getConfigManager(): ConfigManager;
getHASSManager(): HASSManager;
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
getMessageManager(): MessageManager;
getStyleManager(): StyleManager;
getConditionsManager(): ConditionsManager;
getCardElementManager(): CardElementManager;
getTriggersManager(): TriggersManager;
}
+2
View File
@@ -244,6 +244,8 @@ export class ViewManager {
camera: view.camera,
displayMode: view.displayMode ?? undefined,
});
this._api.getTriggersManager().updateView(oldView);
this._api.getCardElementManager().update();
}