Refactor camera initialization into the camera engines.
This commit is contained in:
@@ -1,27 +1,30 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { CameraConfig, CardWideConfig } from '../types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||
import { RecordingSegmentsCache, RequestCache } from './cache';
|
||||
import { CameraManagerEngine } from './engine';
|
||||
import { FrigateCameraManagerEngine } from './frigate/engine-frigate';
|
||||
import { GenericCameraManagerEngine } from './generic/engine-generic';
|
||||
import { Engine } from './types';
|
||||
|
||||
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
|
||||
|
||||
export class CameraManagerEngineFactory {
|
||||
protected _engines: Map<Engine, CameraManagerEngine> = new Map();
|
||||
protected _entityRegistryManager: EntityRegistryManager;
|
||||
protected _cardWideConfig: CardWideConfig;
|
||||
|
||||
constructor(cardWideConfig: CardWideConfig) {
|
||||
constructor(
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
cardWideConfig: CardWideConfig,
|
||||
) {
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
this._cardWideConfig = cardWideConfig;
|
||||
}
|
||||
|
||||
public getEngine(engine: Engine): CameraManagerEngine | null {
|
||||
const cachedEngine = this._engines.get(engine);
|
||||
if (cachedEngine) {
|
||||
return cachedEngine;
|
||||
}
|
||||
public async createEngine(engine: Engine): Promise<CameraManagerEngine | null> {
|
||||
let cameraManagerEngine: CameraManagerEngine | null = null;
|
||||
switch (engine) {
|
||||
case Engine.Generic:
|
||||
cameraManagerEngine = new GenericCameraManagerEngine();
|
||||
break;
|
||||
case Engine.Frigate:
|
||||
cameraManagerEngine = new FrigateCameraManagerEngine(
|
||||
this._cardWideConfig,
|
||||
@@ -30,64 +33,39 @@ export class CameraManagerEngineFactory {
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (cameraManagerEngine) {
|
||||
this._engines.set(engine, cameraManagerEngine);
|
||||
}
|
||||
return cameraManagerEngine;
|
||||
}
|
||||
|
||||
public getEngineForCamera(cameraConfig?: CameraConfig): CameraManagerEngine | null {
|
||||
public async getEngineForCamera(
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<Engine | null> {
|
||||
if (!cameraConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let engine: Engine | null = null;
|
||||
if (cameraConfig.frigate.camera_name) {
|
||||
if (cameraConfig.engine === 'frigate') {
|
||||
engine = Engine.Frigate;
|
||||
}
|
||||
return engine ? this.getEngine(engine) : null;
|
||||
}
|
||||
} else if (cameraConfig.engine === 'auto') {
|
||||
const cameraEntity = cameraConfig.camera_entity;
|
||||
|
||||
public getEnginesForCameraIDs(
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameraIDs: Set<string>,
|
||||
): CameraManagerEngineCameraIDMap | null {
|
||||
const output: CameraManagerEngineCameraIDMap = new Map();
|
||||
|
||||
for (const cameraID of cameraIDs) {
|
||||
const cameraConfig = cameras.get(cameraID);
|
||||
if (!cameraConfig) {
|
||||
continue;
|
||||
if (cameraEntity) {
|
||||
const entity = await this._entityRegistryManager.getEntity(hass, cameraEntity);
|
||||
switch (entity?.platform) {
|
||||
case 'frigate':
|
||||
engine = Engine.Frigate;
|
||||
break;
|
||||
default:
|
||||
engine = Engine.Generic;
|
||||
}
|
||||
} else if (cameraConfig.frigate.camera_name) {
|
||||
// Frigate technically does not need an entity, if the camera name is
|
||||
// manually set the camera is assumed to be Frigate.
|
||||
engine = Engine.Frigate;
|
||||
}
|
||||
|
||||
const engine = this.getEngineForCamera(cameraConfig);
|
||||
if (!engine) {
|
||||
continue;
|
||||
}
|
||||
if (!output.has(engine)) {
|
||||
output.set(engine, new Set());
|
||||
}
|
||||
output.get(engine)?.add(cameraID);
|
||||
}
|
||||
return output.size ? output : null;
|
||||
}
|
||||
|
||||
public getEngineForMedia(
|
||||
cameras: Map<string, CameraConfig>,
|
||||
media: ViewMedia,
|
||||
): CameraManagerEngine | null {
|
||||
const cameraID = media.getCameraID();
|
||||
if (!cameraID) {
|
||||
return null;
|
||||
}
|
||||
const engines = this.getEnginesForCameraIDs(cameras, new Set([cameraID]));
|
||||
return engines ? ([...engines.keys()][0] ?? null) : null;
|
||||
}
|
||||
|
||||
public getAllEngines(
|
||||
cameras: Map<string, CameraConfig>,
|
||||
): CameraManagerEngine[] | null {
|
||||
const engines = this.getEnginesForCameraIDs(cameras, new Set(cameras.keys()));
|
||||
return engines ? [...engines.keys()] : null;
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { CameraConfig } from '../types';
|
||||
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import {
|
||||
DataQuery,
|
||||
@@ -17,57 +18,68 @@ import {
|
||||
CameraManagerCameraCapabilities,
|
||||
CameraManagerMediaCapabilities,
|
||||
CameraManagerCameraMetadata,
|
||||
CameraURLContext,
|
||||
CameraConfigs,
|
||||
Engine,
|
||||
} from './types';
|
||||
|
||||
export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000;
|
||||
|
||||
export interface CameraManagerEngine {
|
||||
getEngineType(): Engine;
|
||||
|
||||
initializeCamera(
|
||||
hass: HomeAssistant,
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<CameraConfig>;
|
||||
|
||||
generateDefaultEventQuery(
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
cameraIDs: Set<string>,
|
||||
query: PartialEventQuery,
|
||||
): EventQuery[] | null;
|
||||
|
||||
generateDefaultRecordingQuery(
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
cameraIDs: Set<string>,
|
||||
query: PartialRecordingQuery,
|
||||
): RecordingQuery[] | null;
|
||||
|
||||
generateDefaultRecordingSegmentsQuery(
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
cameraIDs: Set<string>,
|
||||
query: PartialRecordingSegmentsQuery,
|
||||
): RecordingSegmentsQuery[] | null;
|
||||
|
||||
getEvents(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
query: EventQuery,
|
||||
): Promise<EventQueryResultsMap | null>;
|
||||
|
||||
getRecordings(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
query: RecordingQuery,
|
||||
): Promise<RecordingQueryResultsMap | null>;
|
||||
|
||||
getRecordingSegments(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
query: RecordingSegmentsQuery,
|
||||
): Promise<RecordingSegmentsQueryResultsMap | null>;
|
||||
|
||||
generateMediaFromEvents(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
query: EventQuery,
|
||||
results: QueryReturnType<EventQuery>,
|
||||
): ViewMedia[] | null;
|
||||
|
||||
generateMediaFromRecordings(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
query: RecordingQuery,
|
||||
results: QueryReturnType<RecordingQuery>,
|
||||
): ViewMedia[] | null;
|
||||
@@ -85,14 +97,14 @@ export interface CameraManagerEngine {
|
||||
|
||||
getMediaSeekTime(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
media: ViewMedia,
|
||||
target: Date,
|
||||
): Promise<number | null>;
|
||||
|
||||
getMediaMetadata(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
): Promise<MediaMetadata | null>;
|
||||
|
||||
getCameraMetadata(
|
||||
@@ -105,4 +117,9 @@ export interface CameraManagerEngine {
|
||||
): CameraManagerCameraCapabilities | null;
|
||||
|
||||
getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null;
|
||||
|
||||
getCameraURL(
|
||||
cameraConfig: CameraConfig,
|
||||
context?: CameraURLContext,
|
||||
): string | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { FrigateCardError } from '../types.js';
|
||||
|
||||
export class CameraInitializationError extends FrigateCardError {}
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
RecordingSegment,
|
||||
RecordingSegmentsQuery,
|
||||
RecordingSegmentsQueryResultsMap,
|
||||
CameraURLContext,
|
||||
CameraConfigs,
|
||||
} from '../types';
|
||||
import { FrigateRecording } from './types';
|
||||
import {
|
||||
@@ -62,7 +64,14 @@ import { FrigateViewMediaClassifier } from './media-classifier';
|
||||
import { ViewMediaClassifier } from '../../view/media-classifier';
|
||||
import { FrigateViewMediaFactory } from './media';
|
||||
import { log } from '../../utils/debug';
|
||||
import { getEntityIcon, getEntityTitle } from '../../utils/ha';
|
||||
import { getEntityTitle } from '../../utils/ha';
|
||||
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
||||
import { ExtendedEntity } from '../../utils/ha/entity-registry/types';
|
||||
import { CameraInitializationError } from '../error';
|
||||
import { localize } from '../../localize/localize';
|
||||
import uniq from 'lodash-es/uniq';
|
||||
import format from 'date-fns/format';
|
||||
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||
|
||||
const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
||||
const RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
||||
@@ -92,7 +101,10 @@ class FrigateQueryResultsClassifier {
|
||||
}
|
||||
}
|
||||
|
||||
export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
export class FrigateCameraManagerEngine
|
||||
extends GenericCameraManagerEngine
|
||||
implements CameraManagerEngine
|
||||
{
|
||||
protected _recordingSegmentsCache: RecordingSegmentsCache;
|
||||
protected _requestCache: RequestCache;
|
||||
protected _cardWideConfig: CardWideConfig;
|
||||
@@ -109,11 +121,162 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
recordingSegmentsCache: RecordingSegmentsCache,
|
||||
requestCache: RequestCache,
|
||||
) {
|
||||
super();
|
||||
this._cardWideConfig = cardWideConfig;
|
||||
this._recordingSegmentsCache = recordingSegmentsCache;
|
||||
this._requestCache = requestCache;
|
||||
}
|
||||
|
||||
public getEngineType(): Engine {
|
||||
return Engine.Frigate;
|
||||
}
|
||||
|
||||
public async initializeCamera(
|
||||
hass: HomeAssistant,
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<CameraConfig> {
|
||||
const hasCameraName = !!cameraConfig.frigate?.camera_name;
|
||||
const hasAutoTriggers =
|
||||
cameraConfig.triggers.motion || cameraConfig.triggers.occupancy;
|
||||
|
||||
let entity: ExtendedEntity | null = null;
|
||||
|
||||
// Extended entity information is required if the Frigate camera name is
|
||||
// missing, or if the entity requires automatic resolution of
|
||||
// motion/occupancy sensors.
|
||||
if (cameraConfig.camera_entity && (!hasCameraName || hasAutoTriggers)) {
|
||||
try {
|
||||
entity = await entityRegistryManager.getExtendedEntity(
|
||||
hass,
|
||||
cameraConfig.camera_entity,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new CameraInitializationError(
|
||||
localize('error.no_camera_entity'),
|
||||
cameraConfig,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity && !hasCameraName) {
|
||||
const resolvedName = this._getFrigateCameraNameFromEntity(entity);
|
||||
if (resolvedName) {
|
||||
cameraConfig.frigate.camera_name = resolvedName;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAutoTriggers) {
|
||||
// Try to find the correct entities for the motion & occupancy sensors.
|
||||
// We know they are binary_sensors, and that they'll have the same
|
||||
// config entry ID as the camera. Searching via unique_id ensures this
|
||||
// search still works if the user renames the entity_id.
|
||||
const binarySensorEntities = await entityRegistryManager.getMatchingEntities(
|
||||
hass,
|
||||
(ent) =>
|
||||
ent.config_entry_id === entity?.config_entry_id &&
|
||||
!ent.disabled_by &&
|
||||
ent.entity_id.startsWith('binary_sensor.'),
|
||||
);
|
||||
|
||||
const extendedEntities = await entityRegistryManager.getExtendedEntities(
|
||||
hass,
|
||||
binarySensorEntities.map((entity) => entity.entity_id),
|
||||
);
|
||||
|
||||
if (cameraConfig.triggers.motion) {
|
||||
const motionEntity = this._getMotionSensor(cameraConfig, [
|
||||
...extendedEntities.values(),
|
||||
]);
|
||||
if (motionEntity) {
|
||||
cameraConfig.triggers.entities.push(motionEntity);
|
||||
}
|
||||
}
|
||||
|
||||
if (cameraConfig.triggers.occupancy) {
|
||||
const occupancyEntity = this._getOccupancySensor(cameraConfig, [
|
||||
...extendedEntities.values(),
|
||||
]);
|
||||
if (occupancyEntity) {
|
||||
cameraConfig.triggers.entities.push(occupancyEntity);
|
||||
}
|
||||
}
|
||||
|
||||
// De-duplicate triggering entities.
|
||||
cameraConfig.triggers.entities = uniq(cameraConfig.triggers.entities);
|
||||
}
|
||||
|
||||
return cameraConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Frigate camera name from an entity.
|
||||
* @returns The Frigate camera name or null if unavailable.
|
||||
*/
|
||||
protected _getFrigateCameraNameFromEntity(entity: ExtendedEntity): string | null {
|
||||
if (entity.unique_id && entity.platform === 'frigate') {
|
||||
const match = entity.unique_id.match(/:camera:(?<camera>[^:]+)$/);
|
||||
if (match && match.groups) {
|
||||
return match.groups['camera'];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the motion sensor entity for a given camera.
|
||||
* @param cache The ExtendedEntityCache of entity registry information.
|
||||
* @param cameraConfig The camera config in question.
|
||||
* @returns The entity id of the motion sensor or null.
|
||||
*/
|
||||
protected _getMotionSensor(
|
||||
cameraConfig: CameraConfig,
|
||||
extendedEntities: ExtendedEntity[],
|
||||
): string | null {
|
||||
if (cameraConfig.frigate.camera_name) {
|
||||
return (
|
||||
extendedEntities.find(
|
||||
(ent) =>
|
||||
!!ent.unique_id?.match(
|
||||
new RegExp(
|
||||
`:motion_sensor:${
|
||||
cameraConfig.frigate.zone || cameraConfig.frigate.camera_name
|
||||
}`,
|
||||
),
|
||||
),
|
||||
)?.entity_id ?? null
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the occupancy sensor entity for a given camera.
|
||||
* @param cache The ExtendedEntityCache of entity registry information.
|
||||
* @param cameraConfig The camera config in question.
|
||||
* @returns The entity id of the occupancy sensor or null.
|
||||
*/
|
||||
protected _getOccupancySensor(
|
||||
cameraConfig: CameraConfig,
|
||||
extendedEntities: ExtendedEntity[],
|
||||
): string | null {
|
||||
if (cameraConfig.frigate.camera_name) {
|
||||
return (
|
||||
extendedEntities.find(
|
||||
(ent) =>
|
||||
!!ent.unique_id?.match(
|
||||
new RegExp(
|
||||
`:occupancy_sensor:${
|
||||
cameraConfig.frigate.zone || cameraConfig.frigate.camera_name
|
||||
}_${cameraConfig.frigate.label || 'all'}`,
|
||||
),
|
||||
),
|
||||
)?.entity_id ?? null
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getMediaDownloadPath(
|
||||
cameraConfig: CameraConfig,
|
||||
media: ViewMedia,
|
||||
@@ -137,7 +300,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
}
|
||||
|
||||
public generateDefaultEventQuery(
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
cameraIDs: Set<string>,
|
||||
query?: PartialEventQuery,
|
||||
): EventQuery[] | null {
|
||||
@@ -182,7 +345,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
}
|
||||
|
||||
public generateDefaultRecordingQuery(
|
||||
_cameras: Map<string, CameraConfig>,
|
||||
_cameras: CameraConfigs,
|
||||
cameraIDs: Set<string>,
|
||||
query?: PartialRecordingQuery,
|
||||
): RecordingQuery[] {
|
||||
@@ -196,7 +359,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
}
|
||||
|
||||
public generateDefaultRecordingSegmentsQuery(
|
||||
_cameras: Map<string, CameraConfig>,
|
||||
_cameras: CameraConfigs,
|
||||
cameraIDs: Set<string>,
|
||||
query: PartialRecordingSegmentsQuery,
|
||||
): RecordingSegmentsQuery[] | null {
|
||||
@@ -229,7 +392,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
}
|
||||
|
||||
protected _buildInstanceToCameraIDMapFromQuery(
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
cameraIDs: Set<string>,
|
||||
): Map<string, Set<string>> {
|
||||
const output: Map<string, Set<string>> = new Map();
|
||||
@@ -247,7 +410,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
}
|
||||
|
||||
protected _getFrigateCameraNamesForCameraIDs(
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
cameraIDs: Set<string>,
|
||||
): Set<string> {
|
||||
const output = new Set<string>();
|
||||
@@ -262,7 +425,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public async getEvents(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
query: EventQuery,
|
||||
): Promise<EventQueryResultsMap | null> {
|
||||
const output: EventQueryResultsMap = new Map();
|
||||
@@ -326,7 +489,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public async getRecordings(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
query: RecordingQuery,
|
||||
): Promise<RecordingQueryResultsMap | null> {
|
||||
const output: RecordingQueryResultsMap = new Map();
|
||||
@@ -408,7 +571,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public async getRecordingSegments(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
query: RecordingSegmentsQuery,
|
||||
): Promise<RecordingSegmentsQueryResultsMap | null> {
|
||||
const output: RecordingSegmentsQueryResultsMap = new Map();
|
||||
@@ -474,7 +637,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
}
|
||||
|
||||
protected _getCameraIDMatch(
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
query: DataQuery,
|
||||
instanceID: string,
|
||||
cameraName: string,
|
||||
@@ -499,7 +662,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public generateMediaFromEvents(
|
||||
_hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
query: EventQuery,
|
||||
results: QueryReturnType<EventQuery>,
|
||||
): ViewMedia[] | null {
|
||||
@@ -552,7 +715,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public generateMediaFromRecordings(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
_query: RecordingQuery,
|
||||
results: QueryReturnType<RecordingQuery>,
|
||||
): ViewMedia[] | null {
|
||||
@@ -590,7 +753,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public async getMediaSeekTime(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
media: ViewMedia,
|
||||
target: Date,
|
||||
): Promise<number | null> {
|
||||
@@ -623,7 +786,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
}
|
||||
|
||||
protected _getQueryableCameraConfig(
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
cameraID: string,
|
||||
): CameraConfig | null {
|
||||
const cameraConfig = cameras.get(cameraID);
|
||||
@@ -635,7 +798,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public async getMediaMetadata(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
): Promise<MediaMetadata | null> {
|
||||
const what: Set<string> = new Set();
|
||||
const where: Set<string> = new Set();
|
||||
@@ -715,7 +878,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
*/
|
||||
protected async _garbageCollectSegments(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameras: CameraConfigs,
|
||||
): Promise<void> {
|
||||
const cameraIDs = this._recordingSegmentsCache.getCameraIDs();
|
||||
const recordingQuery: RecordingQuery = {
|
||||
@@ -812,6 +975,9 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
return {
|
||||
canFavoriteEvents: !isBirdseye,
|
||||
canFavoriteRecordings: !isBirdseye,
|
||||
supportsClips: !isBirdseye,
|
||||
supportsSnapshots: !isBirdseye,
|
||||
supportsRecordings: !isBirdseye,
|
||||
supportsTimeline: !isBirdseye,
|
||||
};
|
||||
}
|
||||
@@ -826,6 +992,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
): CameraManagerCameraMetadata {
|
||||
const metadata = super.getCameraMetadata(hass, cameraConfig);
|
||||
return {
|
||||
title:
|
||||
cameraConfig.title ??
|
||||
@@ -834,10 +1001,51 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||
prettifyTitle(cameraConfig.frigate?.camera_name) ??
|
||||
cameraConfig.id ??
|
||||
'',
|
||||
icon:
|
||||
cameraConfig?.icon ??
|
||||
getEntityIcon(hass, cameraConfig.camera_entity) ??
|
||||
'mdi:video',
|
||||
icon: metadata.icon,
|
||||
};
|
||||
}
|
||||
|
||||
public getCameraURL(
|
||||
cameraConfig: CameraConfig,
|
||||
context?: CameraURLContext,
|
||||
): string | null {
|
||||
if (!cameraConfig.frigate.url) {
|
||||
return null;
|
||||
}
|
||||
if (!cameraConfig.frigate.camera_name) {
|
||||
return cameraConfig.frigate.url;
|
||||
}
|
||||
|
||||
const eventsURL =
|
||||
`${cameraConfig.frigate.url}/events?camera=` + cameraConfig.frigate.camera_name;
|
||||
const recordingsURL =
|
||||
`${cameraConfig.frigate.url}/recording/` + cameraConfig.frigate.camera_name;
|
||||
|
||||
// If media is available, use it since it may result in a more precisely
|
||||
// correct URL.
|
||||
switch (context?.media?.getMediaType()) {
|
||||
case 'clip':
|
||||
case 'snapshot':
|
||||
return eventsURL;
|
||||
case 'recording':
|
||||
const startTime = context.media.getStartTime();
|
||||
if (startTime) {
|
||||
return recordingsURL + format(startTime, 'yyyy-MM-dd/HH');
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, fall back to just using the view if we have that.
|
||||
switch (context?.view) {
|
||||
case 'clip':
|
||||
case 'clips':
|
||||
case 'snapshots':
|
||||
case 'snapshot':
|
||||
return eventsURL;
|
||||
case 'recording':
|
||||
case 'recordings':
|
||||
return recordingsURL;
|
||||
}
|
||||
|
||||
return `${cameraConfig.frigate.url}/cameras/${cameraConfig.frigate.camera_name}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { CameraConfig } from '../../types';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import {
|
||||
CameraManagerCameraMetadata,
|
||||
CameraManagerMediaCapabilities,
|
||||
DataQuery,
|
||||
EventQuery,
|
||||
EventQueryResultsMap,
|
||||
MediaMetadata,
|
||||
PartialEventQuery,
|
||||
PartialRecordingQuery,
|
||||
PartialRecordingSegmentsQuery,
|
||||
RecordingQueryResultsMap,
|
||||
RecordingSegmentsQuery,
|
||||
RecordingSegmentsQueryResultsMap,
|
||||
CameraURLContext,
|
||||
CameraConfigs,
|
||||
RecordingQuery,
|
||||
QueryReturnType,
|
||||
CameraManagerCameraCapabilities,
|
||||
Engine,
|
||||
} from '../types';
|
||||
import { getEntityIcon, getEntityTitle } from '../../utils/ha';
|
||||
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
||||
import { CameraManagerEngine } from '../engine';
|
||||
|
||||
export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
public getEngineType(): Engine {
|
||||
return Engine.Generic;
|
||||
}
|
||||
|
||||
public async initializeCamera(
|
||||
_hass: HomeAssistant,
|
||||
_entityRegistryManager: EntityRegistryManager,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<CameraConfig> {
|
||||
return cameraConfig;
|
||||
}
|
||||
|
||||
public generateDefaultEventQuery(
|
||||
_cameras: CameraConfigs,
|
||||
_cameraIDs: Set<string>,
|
||||
_query: PartialEventQuery,
|
||||
): EventQuery[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public generateDefaultRecordingQuery(
|
||||
_cameras: CameraConfigs,
|
||||
_cameraIDs: Set<string>,
|
||||
_query: PartialRecordingQuery,
|
||||
): RecordingQuery[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public generateDefaultRecordingSegmentsQuery(
|
||||
_cameras: CameraConfigs,
|
||||
_cameraIDs: Set<string>,
|
||||
_query: PartialRecordingSegmentsQuery,
|
||||
): RecordingSegmentsQuery[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getEvents(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_query: EventQuery,
|
||||
): Promise<EventQueryResultsMap | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getRecordings(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_query: RecordingQuery,
|
||||
): Promise<RecordingQueryResultsMap | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getRecordingSegments(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_query: RecordingSegmentsQuery,
|
||||
): Promise<RecordingSegmentsQueryResultsMap | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
public generateMediaFromEvents(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_query: EventQuery,
|
||||
_results: QueryReturnType<EventQuery>,
|
||||
): ViewMedia[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public generateMediaFromRecordings(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_query: RecordingQuery,
|
||||
_results: QueryReturnType<RecordingQuery>,
|
||||
): ViewMedia[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getMediaDownloadPath(
|
||||
_cameraConfig: CameraConfig,
|
||||
_media: ViewMedia,
|
||||
): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async favoriteMedia(
|
||||
_hass: HomeAssistant,
|
||||
_cameraConfig: CameraConfig,
|
||||
_media: ViewMedia,
|
||||
_favorite: boolean,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
public getQueryResultMaxAge(_query: DataQuery): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getMediaSeekTime(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_media: ViewMedia,
|
||||
_target: Date,
|
||||
): Promise<number | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getMediaMetadata(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
): Promise<MediaMetadata | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getCameraMetadata(
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
): CameraManagerCameraMetadata {
|
||||
return {
|
||||
title:
|
||||
cameraConfig.title ??
|
||||
getEntityTitle(hass, cameraConfig.camera_entity) ??
|
||||
getEntityTitle(hass, cameraConfig.webrtc_card?.entity) ??
|
||||
cameraConfig.id ??
|
||||
'',
|
||||
icon:
|
||||
cameraConfig?.icon ??
|
||||
getEntityIcon(hass, cameraConfig.camera_entity) ??
|
||||
'mdi:video',
|
||||
};
|
||||
}
|
||||
|
||||
public getCameraCapabilities(
|
||||
_cameraConfig: CameraConfig,
|
||||
): CameraManagerCameraCapabilities | null {
|
||||
return {
|
||||
canFavoriteEvents: false,
|
||||
canFavoriteRecordings:false,
|
||||
supportsClips: false,
|
||||
supportsRecordings: false,
|
||||
supportsSnapshots: false,
|
||||
supportsTimeline: false,
|
||||
}
|
||||
}
|
||||
|
||||
public getMediaCapabilities(_media: ViewMedia): CameraManagerMediaCapabilities | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getCameraURL(
|
||||
_cameraConfig: CameraConfig,
|
||||
_context?: CameraURLContext,
|
||||
): string | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+206
-76
@@ -1,11 +1,12 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { CameraConfig, CardWideConfig } from '../types.js';
|
||||
import { CameraConfig, CamerasConfig, CardWideConfig } from '../types.js';
|
||||
import { allPromises, arrayify, setify } from '../utils/basic.js';
|
||||
import {
|
||||
CameraManagerCameraCapabilities,
|
||||
CameraManagerCameraMetadata,
|
||||
CameraManagerCapabilities,
|
||||
CameraManagerMediaCapabilities,
|
||||
CameraURLContext,
|
||||
DataQuery,
|
||||
EventQuery,
|
||||
EventQueryResults,
|
||||
@@ -37,6 +38,12 @@ import { CameraManagerEngine } from './engine.js';
|
||||
import sum from 'lodash-es/sum';
|
||||
import add from 'date-fns/add';
|
||||
import { log } from '../utils/debug.js';
|
||||
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
||||
import { getCameraID } from '../utils/camera.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import { CameraInitializationError } from './error.js';
|
||||
import { CameraManagerStore } from './store.js';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
class QueryClassifier {
|
||||
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
|
||||
@@ -77,19 +84,132 @@ export interface ExtendedMediaQueryResult<T extends MediaQuery> {
|
||||
results: ViewMedia[];
|
||||
}
|
||||
|
||||
interface InitializedCamera {
|
||||
inputConfig: CameraConfig;
|
||||
initializedConfig: CameraConfig;
|
||||
engine: CameraManagerEngine;
|
||||
}
|
||||
|
||||
export class CameraManager {
|
||||
protected _engineFactory: CameraManagerEngineFactory;
|
||||
protected _cameras: Map<string, CameraConfig>;
|
||||
protected _cardWideConfig?: CardWideConfig;
|
||||
protected _store: CameraManagerStore;
|
||||
|
||||
constructor(
|
||||
engineFactory: CameraManagerEngineFactory,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cardWideConfig?: CardWideConfig,
|
||||
) {
|
||||
this._engineFactory = engineFactory;
|
||||
this._cameras = cameras;
|
||||
this._cardWideConfig = cardWideConfig;
|
||||
this._store = new CameraManagerStore();
|
||||
}
|
||||
|
||||
protected async _initializeCamera(
|
||||
hass: HomeAssistant,
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
inputCameraConfig: CameraConfig,
|
||||
): Promise<InitializedCamera> {
|
||||
const engineType = await this._engineFactory.getEngineForCamera(
|
||||
hass,
|
||||
inputCameraConfig,
|
||||
);
|
||||
const engine = engineType
|
||||
? this._store.getEngineOfType(engineType) ??
|
||||
(await this._engineFactory.createEngine(engineType))
|
||||
: null;
|
||||
if (!engine) {
|
||||
throw new CameraInitializationError(
|
||||
localize('error.no_camera_engine'),
|
||||
inputCameraConfig,
|
||||
);
|
||||
}
|
||||
|
||||
const initializedConfig = await engine.initializeCamera(
|
||||
hass,
|
||||
entityRegistryManager,
|
||||
// Camera initialization may modify the configuration. Keep the original
|
||||
// for display in error messages to avoid user confusion.
|
||||
cloneDeep(inputCameraConfig),
|
||||
);
|
||||
|
||||
return {
|
||||
inputConfig: inputCameraConfig,
|
||||
initializedConfig: initializedConfig,
|
||||
engine: engine,
|
||||
};
|
||||
}
|
||||
|
||||
public async initializeCameras(
|
||||
hass: HomeAssistant,
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
camerasConfig: CamerasConfig,
|
||||
): Promise<void> {
|
||||
const hasAutoTriggers = (config: CameraConfig): boolean => {
|
||||
return config.triggers.motion || config.triggers.occupancy;
|
||||
};
|
||||
|
||||
if (
|
||||
// If any camera requires automatic trigger detection ...
|
||||
camerasConfig.some((config) => hasAutoTriggers(config))
|
||||
) {
|
||||
// ... then we need to populate the entity cache by fetching all entities
|
||||
// from Home Assistant.
|
||||
await entityRegistryManager.fetchEntityList(hass);
|
||||
}
|
||||
|
||||
const results = await allPromises(
|
||||
camerasConfig,
|
||||
async (cameraConfig) =>
|
||||
await this._initializeCamera(hass, entityRegistryManager, cameraConfig),
|
||||
);
|
||||
|
||||
// Do the additions based off the result-order, to ensure the map order is
|
||||
// preserved.
|
||||
results.forEach((result) => {
|
||||
const id = getCameraID(result.initializedConfig);
|
||||
|
||||
if (!id) {
|
||||
throw new CameraInitializationError(
|
||||
localize('error.no_camera_id'),
|
||||
result.inputConfig,
|
||||
);
|
||||
}
|
||||
|
||||
if (this._store.hasCameraID(id)) {
|
||||
throw new CameraInitializationError(
|
||||
localize('error.duplicate_camera_id'),
|
||||
result.inputConfig,
|
||||
);
|
||||
}
|
||||
|
||||
this._store.addCamera(id, result.initializedConfig, result.engine);
|
||||
});
|
||||
|
||||
if (!this._store.getCameraCount()) {
|
||||
throw new CameraInitializationError(localize('error.no_cameras'));
|
||||
}
|
||||
}
|
||||
|
||||
public isInitialized(): boolean {
|
||||
return this._store.getCameraCount() > 0;
|
||||
}
|
||||
|
||||
public getCameras(): Map<string, CameraConfig> | null {
|
||||
return this._store.getCameras();
|
||||
}
|
||||
|
||||
public getCameraConfig(cameraID: string): CameraConfig | null {
|
||||
return this._store.getCameraConfig(cameraID);
|
||||
}
|
||||
|
||||
public getCameraIDs(): Set<string> | null {
|
||||
return this._store.getCameraCount()
|
||||
? new Set(this._store.getCameras().keys())
|
||||
: null;
|
||||
}
|
||||
|
||||
public hasCameraID(cameraID: string): boolean {
|
||||
return this._store.hasCameraID(cameraID);
|
||||
}
|
||||
|
||||
public generateDefaultEventQueries(
|
||||
@@ -127,13 +247,13 @@ export class CameraManager {
|
||||
const where: Set<string> = new Set();
|
||||
const days: Set<string> = new Set();
|
||||
|
||||
const engines = this._engineFactory.getAllEngines(this._cameras);
|
||||
if (!engines) {
|
||||
return null;
|
||||
}
|
||||
const engines = this._store.getAllEngines();
|
||||
|
||||
const processMetadata = async (engine: CameraManagerEngine): Promise<void> => {
|
||||
const engineMetadata = await engine.getMediaMetadata(hass, this._cameras);
|
||||
const engineMetadata = await engine.getMediaMetadata(
|
||||
hass,
|
||||
this._store.getCameras(),
|
||||
);
|
||||
if (engineMetadata) {
|
||||
if (engineMetadata.what) {
|
||||
engineMetadata.what.forEach(what.add, what);
|
||||
@@ -147,7 +267,7 @@ export class CameraManager {
|
||||
}
|
||||
};
|
||||
|
||||
await allPromises(engines, (engine) => processMetadata(engine));
|
||||
await allPromises(engines, processMetadata);
|
||||
|
||||
if (!what.size && !where.size && !days.size) {
|
||||
return null;
|
||||
@@ -165,11 +285,7 @@ export class CameraManager {
|
||||
): PartialQueryConcreteType<PQT>[] | null {
|
||||
const concreteQueries: PartialQueryConcreteType<PQT>[] = [];
|
||||
const _cameraIDs = setify(cameraIDs);
|
||||
|
||||
const engines = this._engineFactory.getEnginesForCameraIDs(
|
||||
this._cameras,
|
||||
_cameraIDs,
|
||||
);
|
||||
const engines = this._store.getEnginesForCameraIDs(_cameraIDs);
|
||||
|
||||
if (!engines) {
|
||||
return null;
|
||||
@@ -179,19 +295,19 @@ export class CameraManager {
|
||||
let queries: DataQuery[] | null = null;
|
||||
if (QueryClassifier.isEventQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultEventQuery(
|
||||
this._cameras,
|
||||
this._store.getCameras(),
|
||||
cameraIDs,
|
||||
partialQuery,
|
||||
);
|
||||
} else if (QueryClassifier.isRecordingQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultRecordingQuery(
|
||||
this._cameras,
|
||||
this._store.getCameras(),
|
||||
cameraIDs,
|
||||
partialQuery,
|
||||
);
|
||||
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultRecordingSegmentsQuery(
|
||||
this._cameras,
|
||||
this._store.getCameras(),
|
||||
cameraIDs,
|
||||
partialQuery,
|
||||
);
|
||||
@@ -303,10 +419,9 @@ export class CameraManager {
|
||||
}
|
||||
|
||||
public getMediaDownloadPath(media: ViewMedia): string | null {
|
||||
const cameraConfig = this._cameras.get(media.getCameraID());
|
||||
const engine = cameraConfig
|
||||
? this._engineFactory.getEngineForCamera(cameraConfig)
|
||||
: null;
|
||||
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
||||
const engine = this._store.getEngineForMedia(media);
|
||||
|
||||
if (!cameraConfig || !engine) {
|
||||
return null;
|
||||
}
|
||||
@@ -314,7 +429,7 @@ export class CameraManager {
|
||||
}
|
||||
|
||||
public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null {
|
||||
const engine = this._engineFactory.getEngineForMedia(this._cameras, media);
|
||||
const engine = this._store.getEngineForMedia(media);
|
||||
if (!engine) {
|
||||
return null;
|
||||
}
|
||||
@@ -326,26 +441,26 @@ export class CameraManager {
|
||||
media: ViewMedia,
|
||||
favorite: boolean,
|
||||
): Promise<void> {
|
||||
const cameraConfig = this._cameras.get(media.getCameraID());
|
||||
if (!cameraConfig) {
|
||||
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
||||
const engine = this._store.getEngineForMedia(media);
|
||||
|
||||
if (!cameraConfig || !engine) {
|
||||
return;
|
||||
}
|
||||
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
||||
if (engine) {
|
||||
const queryStartTime = new Date();
|
||||
await engine.favoriteMedia(hass, cameraConfig, media, favorite);
|
||||
|
||||
log(
|
||||
this._cardWideConfig,
|
||||
'Frigate Card CameraManager favorite request (',
|
||||
`Duration: ${(new Date().getTime() - queryStartTime.getTime()) / 1000}s,`,
|
||||
'Media:',
|
||||
media.getID(),
|
||||
', Favorite:',
|
||||
favorite,
|
||||
')',
|
||||
);
|
||||
}
|
||||
const queryStartTime = new Date();
|
||||
await engine.favoriteMedia(hass, cameraConfig, media, favorite);
|
||||
|
||||
log(
|
||||
this._cardWideConfig,
|
||||
'Frigate Card CameraManager favorite request (',
|
||||
`Duration: ${(new Date().getTime() - queryStartTime.getTime()) / 1000}s,`,
|
||||
'Media:',
|
||||
media.getID(),
|
||||
', Favorite:',
|
||||
favorite,
|
||||
')',
|
||||
);
|
||||
}
|
||||
|
||||
public areMediaQueriesResultsFresh<T extends MediaQuery>(
|
||||
@@ -355,10 +470,7 @@ export class CameraManager {
|
||||
const now = new Date();
|
||||
|
||||
for (const query of queries) {
|
||||
const engines = this._engineFactory.getEnginesForCameraIDs(
|
||||
this._cameras,
|
||||
query.cameraIDs,
|
||||
);
|
||||
const engines = this._store.getEnginesForCameraIDs(query.cameraIDs);
|
||||
for (const [engine, cameraIDs] of engines ?? []) {
|
||||
const maxAgeSeconds = engine.getQueryResultMaxAge({
|
||||
...query,
|
||||
@@ -382,9 +494,11 @@ export class CameraManager {
|
||||
): Promise<number | null> {
|
||||
const startTime = media.getStartTime();
|
||||
const endTime = media.getEndTime();
|
||||
const cameraConfig = this._cameras.get(media.getCameraID());
|
||||
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
||||
const engine = this._store.getEngineForMedia(media);
|
||||
if (
|
||||
!cameraConfig ||
|
||||
!engine ||
|
||||
!startTime ||
|
||||
!endTime ||
|
||||
target < startTime ||
|
||||
@@ -393,8 +507,7 @@ export class CameraManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
||||
return (await engine?.getMediaSeekTime(hass, this._cameras, media, target)) ?? null;
|
||||
return await engine.getMediaSeekTime(hass, this._store.getCameras(), media, target);
|
||||
}
|
||||
|
||||
protected async _handleQuery<QT extends DataQuery>(
|
||||
@@ -415,19 +528,21 @@ export class CameraManager {
|
||||
|
||||
let engineResult: Map<QT, QueryReturnType<QT>> | null = null;
|
||||
if (QueryClassifier.isEventQuery(query)) {
|
||||
engineResult = (await engine.getEvents(hass, this._cameras, query)) as Map<
|
||||
QT,
|
||||
QueryReturnType<QT>
|
||||
> | null;
|
||||
engineResult = (await engine.getEvents(
|
||||
hass,
|
||||
this._store.getCameras(),
|
||||
query,
|
||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||
} else if (QueryClassifier.isRecordingQuery(query)) {
|
||||
engineResult = (await engine.getRecordings(hass, this._cameras, query)) as Map<
|
||||
QT,
|
||||
QueryReturnType<QT>
|
||||
> | null;
|
||||
engineResult = (await engine.getRecordings(
|
||||
hass,
|
||||
this._store.getCameras(),
|
||||
query,
|
||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||
} else if (QueryClassifier.isRecordingSegmentsQuery(query)) {
|
||||
engineResult = (await engine.getRecordingSegments(
|
||||
hass,
|
||||
this._cameras,
|
||||
this._store.getCameras(),
|
||||
query,
|
||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||
}
|
||||
@@ -436,10 +551,7 @@ export class CameraManager {
|
||||
};
|
||||
|
||||
const processQuery = async (query: QT): Promise<void> => {
|
||||
const engines = this._engineFactory.getEnginesForCameraIDs(
|
||||
this._cameras,
|
||||
query.cameraIDs,
|
||||
);
|
||||
const engines = this._store.getEnginesForCameraIDs(query.cameraIDs);
|
||||
if (!engines) {
|
||||
return;
|
||||
}
|
||||
@@ -481,7 +593,7 @@ export class CameraManager {
|
||||
): ViewMedia[] {
|
||||
const mediaArray: ViewMedia[] = [];
|
||||
for (const [query, result] of results.entries()) {
|
||||
const engine = this._engineFactory.getEngine(result.engine);
|
||||
const engine = this._store.getEngineOfType(result.engine);
|
||||
|
||||
if (engine) {
|
||||
let media: ViewMedia[] | null = null;
|
||||
@@ -489,12 +601,22 @@ export class CameraManager {
|
||||
QueryClassifier.isEventQuery(query) &&
|
||||
QueryResultClassifier.isEventQueryResult(result)
|
||||
) {
|
||||
media = engine.generateMediaFromEvents(hass, this._cameras, query, result);
|
||||
media = engine.generateMediaFromEvents(
|
||||
hass,
|
||||
this._store.getCameras(),
|
||||
query,
|
||||
result,
|
||||
);
|
||||
} else if (
|
||||
QueryClassifier.isRecordingQuery(query) &&
|
||||
QueryResultClassifier.isRecordingQuery(result)
|
||||
) {
|
||||
media = engine.generateMediaFromRecordings(hass, this._cameras, query, result);
|
||||
media = engine.generateMediaFromRecordings(
|
||||
hass,
|
||||
this._store.getCameras(),
|
||||
query,
|
||||
result,
|
||||
);
|
||||
}
|
||||
if (media) {
|
||||
mediaArray.push(...media);
|
||||
@@ -517,13 +639,22 @@ export class CameraManager {
|
||||
);
|
||||
}
|
||||
|
||||
public getCameraURL(cameraID: string, context?: CameraURLContext): string | null {
|
||||
const cameraConfig = this._store.getCameraConfig(cameraID);
|
||||
const engine = this._store.getEngineForCameraID(cameraID);
|
||||
if (!cameraConfig || !engine) {
|
||||
return null;
|
||||
}
|
||||
return engine.getCameraURL(cameraConfig, context);
|
||||
}
|
||||
|
||||
public getCameraMetadata(
|
||||
hass: HomeAssistant,
|
||||
cameraID: string,
|
||||
): CameraManagerCameraMetadata | null {
|
||||
const cameraConfig = this._cameras.get(cameraID);
|
||||
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
||||
if (!engine || !cameraConfig) {
|
||||
const cameraConfig = this._store.getCameraConfig(cameraID);
|
||||
const engine = this._store.getEngineForCameraID(cameraID);
|
||||
if (!cameraConfig || !engine) {
|
||||
return null;
|
||||
}
|
||||
return engine.getCameraMetadata(hass, cameraConfig);
|
||||
@@ -532,23 +663,18 @@ export class CameraManager {
|
||||
public getCameraCapabilities(
|
||||
cameraID: string,
|
||||
): CameraManagerCameraCapabilities | null {
|
||||
const cameraConfig = this._cameras.get(cameraID);
|
||||
if (!cameraConfig) {
|
||||
const cameraConfig = this._store.getCameraConfig(cameraID);
|
||||
const engine = this._store.getEngineForCameraID(cameraID);
|
||||
if (!cameraConfig || !engine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
||||
if (!engine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return engine.getCameraCapabilities(cameraConfig);
|
||||
}
|
||||
|
||||
public getAggregateCameraCapabilities(
|
||||
cameraIDs?: Set<string>,
|
||||
): CameraManagerCapabilities {
|
||||
const perCameraCapabilities = [...(cameraIDs ?? this._cameras.keys())].map(
|
||||
): CameraManagerCapabilities | null {
|
||||
const perCameraCapabilities = [...(cameraIDs ?? this._store.getCameraIDs())].map(
|
||||
(cameraID) => this.getCameraCapabilities(cameraID),
|
||||
);
|
||||
|
||||
@@ -557,6 +683,10 @@ export class CameraManager {
|
||||
canFavoriteRecordings: perCameraCapabilities.some(
|
||||
(cap) => cap?.canFavoriteRecordings,
|
||||
),
|
||||
|
||||
supportsClips: perCameraCapabilities.some((cap) => cap?.supportsClips),
|
||||
supportsRecordings: perCameraCapabilities.some((cap) => cap?.supportsRecordings),
|
||||
supportsSnapshots: perCameraCapabilities.some((cap) => cap?.supportsSnapshots),
|
||||
supportsTimeline: perCameraCapabilities.some((cap) => cap?.supportsTimeline),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import uniq from 'lodash-es/uniq';
|
||||
import { CameraConfig } from '../types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { CameraManagerEngine } from './engine';
|
||||
import { CameraConfigs, Engine } from './types';
|
||||
|
||||
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
|
||||
|
||||
export class CameraManagerStore {
|
||||
protected _configs: Map<string, CameraConfig> = new Map();
|
||||
protected _engines: Map<string, CameraManagerEngine> = new Map();
|
||||
protected _enginesByType: Map<Engine, CameraManagerEngine> = new Map();
|
||||
|
||||
public addCamera(
|
||||
cameraID: string,
|
||||
cameraConfig: CameraConfig,
|
||||
engine: CameraManagerEngine,
|
||||
): void {
|
||||
this._configs.set(cameraID, cameraConfig);
|
||||
this._engines.set(cameraID, engine);
|
||||
this._enginesByType.set(engine.getEngineType(), engine);
|
||||
}
|
||||
|
||||
public getCameraCount(): number {
|
||||
return this._configs.size;
|
||||
}
|
||||
|
||||
public hasCameraID(cameraID: string): boolean {
|
||||
return this._configs.has(cameraID);
|
||||
}
|
||||
|
||||
public getCameraConfig(cameraID: string): CameraConfig | null {
|
||||
return this._configs.get(cameraID) ?? null;
|
||||
}
|
||||
|
||||
public getCameras(): CameraConfigs {
|
||||
return this._configs;
|
||||
}
|
||||
|
||||
public getCameraIDs(): Set<string> {
|
||||
return new Set(this._configs.keys());
|
||||
}
|
||||
|
||||
public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null {
|
||||
const cameraID = media.getCameraID();
|
||||
if (!cameraID) {
|
||||
return null;
|
||||
}
|
||||
return this.getCameraConfig(cameraID);
|
||||
}
|
||||
|
||||
public getEngineOfType(engine: Engine): CameraManagerEngine | null {
|
||||
return this._enginesByType.get(engine) ?? null;
|
||||
}
|
||||
|
||||
public getEngineForCameraID(cameraID: string): CameraManagerEngine | null {
|
||||
return this._engines.get(cameraID) ?? null;
|
||||
}
|
||||
|
||||
public getEnginesForCameraIDs(
|
||||
cameraIDs: Set<string>,
|
||||
): CameraManagerEngineCameraIDMap | null {
|
||||
const output: CameraManagerEngineCameraIDMap = new Map();
|
||||
|
||||
for (const cameraID of cameraIDs) {
|
||||
const engine = this.getEngineForCameraID(cameraID);
|
||||
if (!engine) {
|
||||
continue;
|
||||
}
|
||||
if (!output.has(engine)) {
|
||||
output.set(engine, new Set());
|
||||
}
|
||||
output.get(engine)?.add(cameraID);
|
||||
}
|
||||
return output.size ? output : null;
|
||||
}
|
||||
|
||||
public getEngineForMedia(media: ViewMedia): CameraManagerEngine | null {
|
||||
const cameraID = media.getCameraID();
|
||||
if (!cameraID) {
|
||||
return null;
|
||||
}
|
||||
return this.getEngineForCameraID(cameraID);
|
||||
}
|
||||
|
||||
public getAllEngines(): CameraManagerEngine[] {
|
||||
return uniq([...this._engines.values()]);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CameraConfig, FrigateCardView } from '../types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { FrigateEvent, FrigateRecording } from './frigate/types';
|
||||
|
||||
// ====
|
||||
@@ -18,6 +20,7 @@ export enum QueryResultsType {
|
||||
|
||||
export enum Engine {
|
||||
Frigate = 'frigate',
|
||||
Generic = 'generic',
|
||||
}
|
||||
|
||||
export interface DataQuery {
|
||||
@@ -85,6 +88,10 @@ export interface MediaMetadata {
|
||||
interface BaseCapabilities {
|
||||
canFavoriteEvents: boolean;
|
||||
canFavoriteRecordings: boolean;
|
||||
|
||||
supportsClips: boolean;
|
||||
supportsRecordings: boolean;
|
||||
supportsSnapshots: boolean;
|
||||
supportsTimeline: boolean;
|
||||
}
|
||||
|
||||
@@ -99,6 +106,13 @@ export interface CameraManagerCameraMetadata {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface CameraURLContext {
|
||||
media?: ViewMedia;
|
||||
view?: FrigateCardView;
|
||||
}
|
||||
|
||||
export type CameraConfigs = Map<string, CameraConfig>;
|
||||
|
||||
// ===========
|
||||
// Event Query
|
||||
// ===========
|
||||
|
||||
Reference in New Issue
Block a user