Add out-of-the-box Frigate PTZ support.

This commit is contained in:
Dermot Duffy
2023-10-29 14:10:41 -07:00
parent fa43d2e439
commit e12912fa85
74 changed files with 3981 additions and 1514 deletions
+77 -56
View File
@@ -7,10 +7,10 @@ import {
import {
Actions,
ActionType,
FrigateCardAction,
FrigateCardCustomAction,
frigateCardCustomActionSchema,
ViewDisplayMode,
FrigateCardGeneralAction,
FrigateCardUserSpecifiedView,
} from '../config/types.js';
/**
@@ -30,59 +30,75 @@ export function convertActionToFrigateCardCustomAction(
return parseResult.success ? parseResult.data : null;
}
/**
* Create a Frigate card custom action.
* @param action The Frigate card action string (e.g. 'fullscreen')
* @returns A FrigateCardCustomAction for that action string or null.
*/
export function createFrigateCardCustomAction(
action: FrigateCardAction,
args?: {
export function createFrigateCardSimpleAction(
action: FrigateCardGeneralAction | FrigateCardUserSpecifiedView,
options?: {
cardID?: string;
camera?: string;
media_player?: string;
media_player_action?: 'play' | 'stop';
display_mode?: ViewDisplayMode;
},
): FrigateCardCustomAction | null {
if (action === 'camera_select' || action === 'live_substream_select') {
if (!args?.camera) {
return null;
}
return {
action: 'fire-dom-event',
frigate_card_action: action,
camera: args.camera as string,
...(args.cardID && { card_id: args.cardID }),
};
}
if (action === 'media_player') {
if (!args?.media_player || !args.media_player_action) {
return null;
}
return {
action: 'fire-dom-event',
frigate_card_action: action,
media_player: args.media_player,
media_player_action: args.media_player_action,
...(args.cardID && { card_id: args.cardID }),
};
}
if (action === 'display_mode_select') {
if (!args?.display_mode) {
return null;
}
return {
action: 'fire-dom-event',
frigate_card_action: action,
display_mode: args?.display_mode,
...(args.cardID && { card_id: args.cardID }),
};
}
return {
action: 'fire-dom-event',
frigate_card_action: action,
...(args?.cardID && { card_id: args.cardID }),
...(options?.cardID && { card_id: options.cardID }),
};
}
export function createFrigateCardCameraAction(
action: 'camera_select' | 'live_substream_select',
camera: string,
options?: {
cardID?: string;
},
): FrigateCardCustomAction {
return {
action: 'fire-dom-event',
frigate_card_action: action,
camera: camera,
...(options?.cardID && { card_id: options.cardID }),
};
}
export function createFrigateCardMediaPlayerAction(
mediaPlayer: string,
mediaPlayerAction: 'play' | 'stop',
options?: {
cardID?: string;
},
): FrigateCardCustomAction {
return {
action: 'fire-dom-event',
frigate_card_action: 'media_player',
media_player: mediaPlayer,
media_player_action: mediaPlayerAction,
...(options?.cardID && { card_id: options.cardID }),
};
}
export function createFrigateCardDisplayModeAction(
displayMode: 'single' | 'grid',
options?: {
cardID?: string;
},
): FrigateCardCustomAction {
return {
action: 'fire-dom-event',
frigate_card_action: 'display_mode_select',
display_mode: displayMode,
...(options?.cardID && { card_id: options.cardID }),
};
}
export function createFrigateCardShowPTZAction(
showPTZ: boolean,
options?: {
cardID?: string;
},
): FrigateCardCustomAction {
return {
action: 'fire-dom-event',
frigate_card_action: 'show_ptz',
show_ptz: showPTZ,
...(options?.cardID && { card_id: options.cardID }),
};
}
@@ -94,10 +110,10 @@ export function createFrigateCardCustomAction(
*/
export function getActionConfigGivenAction(
interaction?: string,
config?: Actions,
): ActionType | ActionType[] | undefined {
config?: Actions | null,
): ActionType | ActionType[] | null {
if (!interaction || !config) {
return undefined;
return null;
}
if (interaction == 'tap' && config.tap_action) {
return config.tap_action;
@@ -110,7 +126,7 @@ export function getActionConfigGivenAction(
} else if (interaction == 'start_tap' && config.start_tap_action) {
return config.start_tap_action;
}
return undefined;
return null;
}
/**
@@ -132,7 +148,7 @@ export const frigateCardHandleActionConfig = (
entity?: string;
},
action: string,
actionConfig?: ActionType | ActionType[],
actionConfig?: ActionType | ActionType[] | null,
): boolean => {
// Only allow a tap action to use a default non-config (the more-info config).
if (actionConfig || action == 'tap') {
@@ -149,7 +165,7 @@ export const frigateCardHandleAction = (
camera_image?: string;
entity?: string;
},
actionConfig: ActionType | ActionType[] | undefined,
actionConfig?: ActionType | ActionType[] | null,
): void => {
// ActionConfig vs ActionType:
// * There is a slight typing (but not functional) difference between
@@ -161,7 +177,12 @@ export const frigateCardHandleAction = (
handleActionConfig(node, hass, config, action as ActionConfig | undefined),
);
} else {
handleActionConfig(node, hass, config, actionConfig as ActionConfig | undefined);
handleActionConfig(
node,
hass,
config,
(actionConfig ?? undefined) as ActionConfig | undefined,
);
}
};
-46
View File
@@ -1,4 +1,3 @@
import { CameraManager } from '../camera-manager/manager.js';
import { CameraConfig, RawFrigateCardConfig } from '../config/types.js';
/**
@@ -30,48 +29,3 @@ export function getCameraID(
''
);
}
/**
* Get all cameras that depend on a given camera.
* @param cameraManager The camera manager.
* @param cameraID ID of the target camera.
* @returns A set of dependent cameraIDs or null (since JS sets guarantee order,
* the first item in the set is guaranteed to be the cameraID itself).
*/
export function getAllDependentCameras(
cameraManager: CameraManager,
cameraID: string,
): Set<string>;
export function getAllDependentCameras(
cameraManager?: CameraManager,
cameraID?: string,
): Set<string> | null;
export function getAllDependentCameras(
cameraManager?: CameraManager,
cameraID?: string,
): Set<string> | null {
if (!cameraManager || !cameraID) {
return null;
}
const cameras = cameraManager.getStore().getCameras();
const cameraIDs: Set<string> = new Set();
const getDependentCameras = (cameraID: string): void => {
const cameraConfig = cameras.get(cameraID);
if (cameraConfig) {
cameraIDs.add(cameraID);
const dependentCameras: Set<string> = new Set();
cameraConfig.dependencies.cameras.forEach((item) => dependentCameras.add(item));
if (cameraConfig.dependencies.all_cameras) {
cameras.forEach((_, key) => dependentCameras.add(key));
}
for (const eventCameraID of dependentCameras) {
if (!cameraIDs.has(eventCameraID)) {
getDependentCameras(eventCameraID);
}
}
}
};
getDependentCameras(cameraID);
return cameraIDs;
}
+6 -3
View File
@@ -5,7 +5,7 @@ enum InitializationState {
INITIALIZED = 'initialized',
}
type InitializationCallback = () => Promise<unknown>;
type InitializationCallback = () => Promise<boolean>;
/**
* Manages initialization state & calling initializers.
@@ -43,8 +43,11 @@ export class Initializer {
if (state !== InitializationState.INITIALIZING) {
if (initializer) {
this._state.set(aspect, InitializationState.INITIALIZING);
await initializer();
this._state.set(aspect, InitializationState.INITIALIZED);
if (await initializer()) {
this._state.set(aspect, InitializationState.INITIALIZED);
} else {
this.uninitialize(aspect);
}
} else {
this._state.set(aspect, InitializationState.INITIALIZED);
}
+2 -3
View File
@@ -14,7 +14,6 @@ import {
import { MediaQueriesResults } from '../view/media-queries-results';
import { View } from '../view/view';
import { errorToConsole } from './basic';
import { getAllDependentCameras } from './camera.js';
type ResultSelectType = 'latest' | 'time' | 'none';
@@ -32,7 +31,7 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
): Promise<void> => {
const cameraIDs = options?.allCameras
? cameraManager.getStore().getVisibleCameraIDs()
: getAllDependentCameras(cameraManager, view.camera);
: cameraManager.getStore().getAllDependentCameras(view.camera);
if (!cameraIDs.size) {
return;
}
@@ -98,7 +97,7 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
): Promise<void> => {
const cameraIDs = options?.allCameras
? cameraManager.getStore().getVisibleCameraIDs()
: getAllDependentCameras(cameraManager, view.camera);
: cameraManager.getStore().getAllDependentCameras(view.camera);
if (!cameraIDs.size) {
return;
}
+14
View File
@@ -0,0 +1,14 @@
import { CameraManagerCameraCapabilities } from '../camera-manager/types';
import { FrigateCardPTZConfig, PTZ_CONTROL_ACTIONS } from '../config/types';
export const hasUsablePTZ = (
capabilities: CameraManagerCameraCapabilities | null,
config: FrigateCardPTZConfig,
): boolean => {
for (const actionName of PTZ_CONTROL_ACTIONS) {
if ('actions_' + actionName in config) {
return true;
}
}
return !!capabilities?.ptz;
};