Add out-of-the-box Frigate PTZ support.
This commit is contained in:
@@ -15,14 +15,14 @@ import { Entity } from '../../utils/ha/entity-registry/types';
|
||||
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { RequestCache } from '../cache';
|
||||
import { Camera } from '../camera';
|
||||
import { CameraManagerEngine } from '../engine';
|
||||
import { CameraInitializationError } from '../error';
|
||||
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||
import { rangesOverlap } from '../range';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||
import {
|
||||
CameraConfigs,
|
||||
CameraEndpoint,
|
||||
CameraManagerCameraCapabilities,
|
||||
CameraManagerMediaCapabilities,
|
||||
DataQuery,
|
||||
EventQuery,
|
||||
@@ -141,7 +141,7 @@ export class BrowseMediaCameraManagerEngine
|
||||
hass: HomeAssistant,
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<CameraConfig> {
|
||||
): Promise<Camera> {
|
||||
const entity = cameraConfig.camera_entity
|
||||
? await entityRegistryManager.getEntity(hass, cameraConfig.camera_entity)
|
||||
: null;
|
||||
@@ -152,11 +152,20 @@ export class BrowseMediaCameraManagerEngine
|
||||
);
|
||||
}
|
||||
this._cameraEntities.set(cameraConfig.camera_entity, entity);
|
||||
return cameraConfig;
|
||||
|
||||
return new Camera(cameraConfig, this, {
|
||||
canFavoriteEvents: false,
|
||||
canFavoriteRecordings: false,
|
||||
canSeek: false,
|
||||
supportsClips: true,
|
||||
supportsRecordings: false,
|
||||
supportsSnapshots: true,
|
||||
supportsTimeline: true,
|
||||
});
|
||||
}
|
||||
|
||||
public generateDefaultEventQuery(
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
cameraIDs: Set<string>,
|
||||
query: PartialEventQuery,
|
||||
): EventQuery[] | null {
|
||||
@@ -191,21 +200,6 @@ export class BrowseMediaCameraManagerEngine
|
||||
return null;
|
||||
}
|
||||
|
||||
public getCameraCapabilities(
|
||||
cameraConfig: CameraConfig,
|
||||
): CameraManagerCameraCapabilities | null {
|
||||
const parentCapabilities = super.getCameraCapabilities(cameraConfig);
|
||||
if (!parentCapabilities) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...parentCapabilities,
|
||||
supportsClips: true,
|
||||
supportsSnapshots: true,
|
||||
supportsTimeline: true,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public getMediaCapabilities(_media: ViewMedia): CameraManagerMediaCapabilities {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CameraConfig } from '../config/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import { CameraManagerEngine } from './engine';
|
||||
import { CameraNoIDError } from './error';
|
||||
import { CameraManagerCameraCapabilities } from './types';
|
||||
|
||||
export class Camera {
|
||||
protected _config: CameraConfig;
|
||||
protected _engine: CameraManagerEngine;
|
||||
protected _capabilities: CameraManagerCameraCapabilities;
|
||||
|
||||
constructor(
|
||||
config: CameraConfig,
|
||||
engine: CameraManagerEngine,
|
||||
capabilities: CameraManagerCameraCapabilities,
|
||||
) {
|
||||
this._config = config;
|
||||
this._engine = engine;
|
||||
this._capabilities = capabilities;
|
||||
}
|
||||
|
||||
public getConfig(): CameraConfig {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
public setID(cameraID: string): void {
|
||||
this._config.id = cameraID;
|
||||
}
|
||||
|
||||
public getID(): string {
|
||||
if (this._config.id) {
|
||||
return this._config.id;
|
||||
}
|
||||
throw new CameraNoIDError(localize('error.no_camera_id'));
|
||||
}
|
||||
|
||||
public getEngine(): CameraManagerEngine {
|
||||
return this._engine;
|
||||
}
|
||||
|
||||
public getCapabilities(): CameraManagerCameraCapabilities {
|
||||
return this._capabilities;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { CameraConfig } from '../config/types';
|
||||
import {
|
||||
CameraConfig,
|
||||
PTZAction,
|
||||
PTZPhase,
|
||||
} from '../config/types';
|
||||
import { ExtendedHomeAssistant } from '../types';
|
||||
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { Camera } from './camera';
|
||||
import { CameraManagerReadOnlyConfigStore } from './store';
|
||||
import {
|
||||
CameraConfigs,
|
||||
CameraEndpoint,
|
||||
CameraEndpoints,
|
||||
CameraEndpointsContext,
|
||||
CameraManagerCameraCapabilities,
|
||||
CameraManagerCameraMetadata,
|
||||
CameraManagerMediaCapabilities,
|
||||
DataQuery,
|
||||
@@ -37,57 +41,57 @@ export interface CameraManagerEngine {
|
||||
hass: HomeAssistant,
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<CameraConfig>;
|
||||
): Promise<Camera>;
|
||||
|
||||
generateDefaultEventQuery(
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
cameraIDs: Set<string>,
|
||||
query: PartialEventQuery,
|
||||
): EventQuery[] | null;
|
||||
|
||||
generateDefaultRecordingQuery(
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
cameraIDs: Set<string>,
|
||||
query: PartialRecordingQuery,
|
||||
): RecordingQuery[] | null;
|
||||
|
||||
generateDefaultRecordingSegmentsQuery(
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
cameraIDs: Set<string>,
|
||||
query: PartialRecordingSegmentsQuery,
|
||||
): RecordingSegmentsQuery[] | null;
|
||||
|
||||
getEvents(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: EventQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<EventQueryResultsMap | null>;
|
||||
|
||||
getRecordings(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: RecordingQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<RecordingQueryResultsMap | null>;
|
||||
|
||||
getRecordingSegments(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: RecordingSegmentsQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<RecordingSegmentsQueryResultsMap | null>;
|
||||
|
||||
generateMediaFromEvents(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: EventQuery,
|
||||
results: QueryReturnType<EventQuery>,
|
||||
): ViewMedia[] | null;
|
||||
|
||||
generateMediaFromRecordings(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: RecordingQuery,
|
||||
results: QueryReturnType<RecordingQuery>,
|
||||
): ViewMedia[] | null;
|
||||
@@ -109,7 +113,7 @@ export interface CameraManagerEngine {
|
||||
|
||||
getMediaSeekTime(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
media: ViewMedia,
|
||||
target: Date,
|
||||
engineOptions?: EngineOptions,
|
||||
@@ -117,7 +121,7 @@ export interface CameraManagerEngine {
|
||||
|
||||
getMediaMetadata(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: MediaMetadataQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<MediaMetadataQueryResultsMap | null>;
|
||||
@@ -127,14 +131,20 @@ export interface CameraManagerEngine {
|
||||
cameraConfig: CameraConfig,
|
||||
): CameraManagerCameraMetadata;
|
||||
|
||||
getCameraCapabilities(
|
||||
cameraConfig: CameraConfig,
|
||||
): CameraManagerCameraCapabilities | null;
|
||||
|
||||
getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null;
|
||||
|
||||
getCameraEndpoints(
|
||||
cameraConfig: CameraConfig,
|
||||
context?: CameraEndpointsContext,
|
||||
): CameraEndpoints | null;
|
||||
|
||||
executePTZAction(
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
action: PTZAction,
|
||||
options?: {
|
||||
phase?: PTZPhase,
|
||||
preset?: string,
|
||||
}
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FrigateCardError } from '../types.js';
|
||||
|
||||
export class CameraInitializationError extends FrigateCardError {}
|
||||
export class CameraNoIDError extends FrigateCardError {}
|
||||
|
||||
@@ -9,11 +9,12 @@ import orderBy from 'lodash-es/orderBy';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import uniq from 'lodash-es/uniq';
|
||||
import uniqWith from 'lodash-es/uniqWith';
|
||||
import { CameraConfig } from '../../config/types';
|
||||
import { CameraConfig, PTZAction, PTZPhase } from '../../config/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { ExtendedHomeAssistant } from '../../types';
|
||||
import {
|
||||
allPromises,
|
||||
errorToConsole,
|
||||
formatDate,
|
||||
prettifyTitle,
|
||||
runWhenIdleIfSupported,
|
||||
@@ -24,6 +25,7 @@ import { Entity } from '../../utils/ha/entity-registry/types';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { ViewMediaClassifier } from '../../view/media-classifier';
|
||||
import { RecordingSegmentsCache, RequestCache } from '../cache';
|
||||
import { Camera } from '../camera';
|
||||
import {
|
||||
CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
||||
CameraManagerEngine,
|
||||
@@ -31,12 +33,11 @@ import {
|
||||
import { CameraInitializationError } from '../error';
|
||||
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||
import { DateRange } from '../range';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||
import {
|
||||
CameraConfigs,
|
||||
CameraEndpoint,
|
||||
CameraEndpoints,
|
||||
CameraEndpointsContext,
|
||||
CameraManagerCameraCapabilities,
|
||||
CameraManagerCameraMetadata,
|
||||
CameraManagerMediaCapabilities,
|
||||
DataQuery,
|
||||
@@ -51,6 +52,8 @@ import {
|
||||
PartialEventQuery,
|
||||
PartialRecordingQuery,
|
||||
PartialRecordingSegmentsQuery,
|
||||
PTZCapabilities,
|
||||
PTZMovementType,
|
||||
QueryResults,
|
||||
QueryResultsType,
|
||||
QueryReturnType,
|
||||
@@ -74,12 +77,14 @@ import {
|
||||
getRecordingSegments,
|
||||
getRecordingsSummary,
|
||||
retainEvent,
|
||||
getPTZInfo,
|
||||
} from './requests';
|
||||
import {
|
||||
FrigateEventQueryResults,
|
||||
FrigateRecording,
|
||||
FrigateRecordingQueryResults,
|
||||
FrigateRecordingSegmentsQueryResults,
|
||||
PTZInfo,
|
||||
} from './types';
|
||||
|
||||
const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
||||
@@ -144,7 +149,7 @@ export class FrigateCameraManagerEngine
|
||||
hass: HomeAssistant,
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<CameraConfig> {
|
||||
): Promise<Camera> {
|
||||
const hasCameraName = !!cameraConfig.frigate?.camera_name;
|
||||
const hasAutoTriggers =
|
||||
cameraConfig.triggers.motion || cameraConfig.triggers.occupancy;
|
||||
@@ -207,7 +212,64 @@ export class FrigateCameraManagerEngine
|
||||
cameraConfig.triggers.entities = uniq(cameraConfig.triggers.entities);
|
||||
}
|
||||
|
||||
return cameraConfig;
|
||||
const ptz = await this._getPTZCapabilities(hass, cameraConfig);
|
||||
|
||||
const isBirdseye = this._isBirdseye(cameraConfig);
|
||||
return new Camera(cameraConfig, this, {
|
||||
canFavoriteEvents: !isBirdseye,
|
||||
canFavoriteRecordings: !isBirdseye,
|
||||
canSeek: true,
|
||||
supportsClips: !isBirdseye,
|
||||
supportsSnapshots: !isBirdseye,
|
||||
supportsRecordings: !isBirdseye,
|
||||
supportsTimeline: !isBirdseye,
|
||||
|
||||
...(ptz && { ptz: ptz }),
|
||||
});
|
||||
}
|
||||
|
||||
protected async _getPTZCapabilities(
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<PTZCapabilities | null> {
|
||||
if (!cameraConfig.frigate.camera_name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let ptzInfo: PTZInfo | null = null;
|
||||
try {
|
||||
ptzInfo = await getPTZInfo(
|
||||
hass,
|
||||
cameraConfig.frigate.client_id,
|
||||
cameraConfig.frigate.camera_name,
|
||||
);
|
||||
} catch (e) {
|
||||
errorToConsole(e as Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const panTilt: PTZMovementType[] = [
|
||||
...(ptzInfo.features?.includes('pt') ? ['continuous' as const] : []),
|
||||
...(ptzInfo.features?.includes('pt-r') ? ['relative' as const] : []),
|
||||
];
|
||||
const zoom: PTZMovementType[] = [
|
||||
...(ptzInfo.features?.includes('zoom') ? ['continuous' as const] : []),
|
||||
...(ptzInfo.features?.includes('zoom-r') ? ['relative' as const] : []),
|
||||
];
|
||||
const presets = ptzInfo.presets;
|
||||
|
||||
if (panTilt.length || zoom.length || presets?.length) {
|
||||
return {
|
||||
...(panTilt && { panTilt: panTilt }),
|
||||
...(zoom && { zoom: zoom }),
|
||||
...(presets && { presets: presets }),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected _isBirdseye(cameraConfig: CameraConfig): boolean {
|
||||
return cameraConfig.frigate.camera_name === CAMERA_BIRDSEYE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -330,13 +392,11 @@ export class FrigateCameraManagerEngine
|
||||
}
|
||||
|
||||
public generateDefaultEventQuery(
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
cameraIDs: Set<string>,
|
||||
query?: PartialEventQuery,
|
||||
): EventQuery[] | null {
|
||||
const relevantCameraConfigs = Array.from(cameraIDs).map((cameraID) =>
|
||||
cameras.get(cameraID),
|
||||
);
|
||||
const relevantCameraConfigs = [...store.getCameraConfigs(cameraIDs)];
|
||||
|
||||
// If all cameras specify exactly the same zones or labels (incl. none), we
|
||||
// can use a single batch query which will be better performance wise,
|
||||
@@ -365,7 +425,7 @@ export class FrigateCameraManagerEngine
|
||||
|
||||
const output: EventQuery[] = [];
|
||||
for (const cameraID of cameraIDs) {
|
||||
const cameraConfig = cameras.get(cameraID);
|
||||
const cameraConfig = store.getCameraConfig(cameraID);
|
||||
if (cameraConfig) {
|
||||
output.push({
|
||||
type: QueryType.Event,
|
||||
@@ -384,7 +444,7 @@ export class FrigateCameraManagerEngine
|
||||
}
|
||||
|
||||
public generateDefaultRecordingQuery(
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
cameraIDs: Set<string>,
|
||||
query?: PartialRecordingQuery,
|
||||
): RecordingQuery[] {
|
||||
@@ -398,7 +458,7 @@ export class FrigateCameraManagerEngine
|
||||
}
|
||||
|
||||
public generateDefaultRecordingSegmentsQuery(
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
cameraIDs: Set<string>,
|
||||
query: PartialRecordingSegmentsQuery,
|
||||
): RecordingSegmentsQuery[] | null {
|
||||
@@ -431,12 +491,12 @@ export class FrigateCameraManagerEngine
|
||||
}
|
||||
|
||||
protected _buildInstanceToCameraIDMapFromQuery(
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
cameraIDs: Set<string>,
|
||||
): Map<string, Set<string>> {
|
||||
const output: Map<string, Set<string>> = new Map();
|
||||
for (const cameraID of cameraIDs) {
|
||||
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
||||
const cameraConfig = this._getQueryableCameraConfig(store, cameraID);
|
||||
const clientID = cameraConfig?.frigate.client_id;
|
||||
if (clientID) {
|
||||
if (!output.has(clientID)) {
|
||||
@@ -449,12 +509,12 @@ export class FrigateCameraManagerEngine
|
||||
}
|
||||
|
||||
protected _getFrigateCameraNamesForCameraIDs(
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
cameraIDs: Set<string>,
|
||||
): Set<string> {
|
||||
const output = new Set<string>();
|
||||
for (const cameraID of cameraIDs) {
|
||||
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
||||
const cameraConfig = this._getQueryableCameraConfig(store, cameraID);
|
||||
if (cameraConfig?.frigate.camera_name) {
|
||||
output.add(cameraConfig.frigate.camera_name);
|
||||
}
|
||||
@@ -464,7 +524,7 @@ export class FrigateCameraManagerEngine
|
||||
|
||||
public async getEvents(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: EventQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<EventQueryResultsMap | null> {
|
||||
@@ -487,7 +547,7 @@ export class FrigateCameraManagerEngine
|
||||
|
||||
const nativeQuery: NativeFrigateEventQuery = {
|
||||
instance_id: instanceID,
|
||||
cameras: Array.from(this._getFrigateCameraNamesForCameraIDs(cameras, cameraIDs)),
|
||||
cameras: Array.from(this._getFrigateCameraNamesForCameraIDs(store, cameraIDs)),
|
||||
...(query.what && { labels: Array.from(query.what) }),
|
||||
...(query.where && { zones: Array.from(query.where) }),
|
||||
...(query.tags && { sub_labels: Array.from(query.tags) }),
|
||||
@@ -518,10 +578,7 @@ export class FrigateCameraManagerEngine
|
||||
// Frigate allows multiple cameras to be searched for events in a single
|
||||
// query. Break them down into groups of cameras per Frigate instance, then
|
||||
// query once per instance for all cameras in that instance.
|
||||
const instances = this._buildInstanceToCameraIDMapFromQuery(
|
||||
cameras,
|
||||
query.cameraIDs,
|
||||
);
|
||||
const instances = this._buildInstanceToCameraIDMapFromQuery(store, query.cameraIDs);
|
||||
|
||||
await Promise.all(
|
||||
Array.from(instances.keys()).map((instanceID) =>
|
||||
@@ -533,7 +590,7 @@ export class FrigateCameraManagerEngine
|
||||
|
||||
public async getRecordings(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: RecordingQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<RecordingQueryResultsMap | null> {
|
||||
@@ -551,7 +608,7 @@ export class FrigateCameraManagerEngine
|
||||
return;
|
||||
}
|
||||
|
||||
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
||||
const cameraConfig = this._getQueryableCameraConfig(store, cameraID);
|
||||
if (!cameraConfig || !cameraConfig.frigate.camera_name) {
|
||||
return;
|
||||
}
|
||||
@@ -619,7 +676,7 @@ export class FrigateCameraManagerEngine
|
||||
|
||||
public async getRecordingSegments(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: RecordingSegmentsQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<RecordingSegmentsQueryResultsMap | null> {
|
||||
@@ -630,7 +687,7 @@ export class FrigateCameraManagerEngine
|
||||
cameraID: string,
|
||||
): Promise<void> => {
|
||||
const query = { ...baseQuery, cameraIDs: new Set([cameraID]) };
|
||||
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
||||
const cameraConfig = this._getQueryableCameraConfig(store, cameraID);
|
||||
if (!cameraConfig || !cameraConfig.frigate.camera_name) {
|
||||
return;
|
||||
}
|
||||
@@ -687,12 +744,12 @@ export class FrigateCameraManagerEngine
|
||||
Array.from(query.cameraIDs).map((cameraID) => processQuery(query, cameraID)),
|
||||
);
|
||||
|
||||
runWhenIdleIfSupported(() => this._throttledSegmentGarbageCollector(hass, cameras));
|
||||
runWhenIdleIfSupported(() => this._throttledSegmentGarbageCollector(hass, store));
|
||||
return output.size ? output : null;
|
||||
}
|
||||
|
||||
protected _getCameraIDMatch(
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: DataQuery,
|
||||
instanceID: string,
|
||||
cameraName: string,
|
||||
@@ -704,7 +761,7 @@ export class FrigateCameraManagerEngine
|
||||
if (query.cameraIDs.size === 1) {
|
||||
return [...query.cameraIDs][0];
|
||||
}
|
||||
for (const [cameraID, cameraConfig] of cameras.entries()) {
|
||||
for (const [cameraID, cameraConfig] of store.getCameraConfigEntries()) {
|
||||
if (
|
||||
cameraConfig.frigate.client_id === instanceID &&
|
||||
cameraConfig.frigate.camera_name === cameraName
|
||||
@@ -717,7 +774,7 @@ export class FrigateCameraManagerEngine
|
||||
|
||||
public generateMediaFromEvents(
|
||||
_hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: EventQuery,
|
||||
results: QueryReturnType<EventQuery>,
|
||||
): ViewMedia[] | null {
|
||||
@@ -728,7 +785,7 @@ export class FrigateCameraManagerEngine
|
||||
const output: ViewMedia[] = [];
|
||||
for (const event of results.events) {
|
||||
const cameraID = this._getCameraIDMatch(
|
||||
cameras,
|
||||
store,
|
||||
query,
|
||||
results.instanceID,
|
||||
event.camera,
|
||||
@@ -736,7 +793,7 @@ export class FrigateCameraManagerEngine
|
||||
if (!cameraID) {
|
||||
continue;
|
||||
}
|
||||
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
||||
const cameraConfig = this._getQueryableCameraConfig(store, cameraID);
|
||||
if (!cameraConfig) {
|
||||
continue;
|
||||
}
|
||||
@@ -771,7 +828,7 @@ export class FrigateCameraManagerEngine
|
||||
|
||||
public generateMediaFromRecordings(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
_query: RecordingQuery,
|
||||
results: QueryReturnType<RecordingQuery>,
|
||||
): ViewMedia[] | null {
|
||||
@@ -781,7 +838,7 @@ export class FrigateCameraManagerEngine
|
||||
|
||||
const output: ViewMedia[] = [];
|
||||
for (const recording of results.recordings) {
|
||||
const cameraConfig = this._getQueryableCameraConfig(cameras, recording.cameraID);
|
||||
const cameraConfig = this._getQueryableCameraConfig(store, recording.cameraID);
|
||||
if (!cameraConfig) {
|
||||
continue;
|
||||
}
|
||||
@@ -809,7 +866,7 @@ export class FrigateCameraManagerEngine
|
||||
|
||||
public async getMediaSeekTime(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
media: ViewMedia,
|
||||
target: Date,
|
||||
engineOptions?: EngineOptions,
|
||||
@@ -828,7 +885,7 @@ export class FrigateCameraManagerEngine
|
||||
type: QueryType.RecordingSegments,
|
||||
};
|
||||
|
||||
const results = await this.getRecordingSegments(hass, cameras, query, engineOptions);
|
||||
const results = await this.getRecordingSegments(hass, store, query, engineOptions);
|
||||
|
||||
if (results) {
|
||||
return this._getSeekTimeInSegments(
|
||||
@@ -843,11 +900,11 @@ export class FrigateCameraManagerEngine
|
||||
}
|
||||
|
||||
protected _getQueryableCameraConfig(
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
cameraID: string,
|
||||
): CameraConfig | null {
|
||||
const cameraConfig = cameras.get(cameraID);
|
||||
if (!cameraConfig || cameraConfig.frigate.camera_name == CAMERA_BIRDSEYE) {
|
||||
const cameraConfig = store.getCameraConfig(cameraID);
|
||||
if (!cameraConfig || this._isBirdseye(cameraConfig)) {
|
||||
return null;
|
||||
}
|
||||
return cameraConfig;
|
||||
@@ -865,7 +922,7 @@ export class FrigateCameraManagerEngine
|
||||
|
||||
public async getMediaMetadata(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: MediaMetadataQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<MediaMetadataQueryResultsMap | null> {
|
||||
@@ -885,16 +942,13 @@ export class FrigateCameraManagerEngine
|
||||
const days: Set<string> = new Set();
|
||||
const tags: Set<string> = new Set();
|
||||
|
||||
const instances = this._buildInstanceToCameraIDMapFromQuery(
|
||||
cameras,
|
||||
query.cameraIDs,
|
||||
);
|
||||
const instances = this._buildInstanceToCameraIDMapFromQuery(store, query.cameraIDs);
|
||||
|
||||
const processEventSummary = async (
|
||||
instanceID: string,
|
||||
cameraIDs: Set<string>,
|
||||
): Promise<void> => {
|
||||
const cameraNames = this._getFrigateCameraNamesForCameraIDs(cameras, cameraIDs);
|
||||
const cameraNames = this._getFrigateCameraNamesForCameraIDs(store, cameraIDs);
|
||||
for (const entry of await getEventSummary(hass, instanceID)) {
|
||||
if (!cameraNames.has(entry.camera)) {
|
||||
// If this entry applies to a camera that *is* in this Frigate
|
||||
@@ -919,7 +973,7 @@ export class FrigateCameraManagerEngine
|
||||
const processRecordings = async (cameraIDs: Set<string>): Promise<void> => {
|
||||
const recordings = await this.getRecordings(
|
||||
hass,
|
||||
cameras,
|
||||
store,
|
||||
{
|
||||
type: QueryType.Recording,
|
||||
cameraIDs: cameraIDs,
|
||||
@@ -977,7 +1031,7 @@ export class FrigateCameraManagerEngine
|
||||
*/
|
||||
protected async _garbageCollectSegments(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
): Promise<void> {
|
||||
const cameraIDs = this._recordingSegmentsCache.getCameraIDs();
|
||||
const recordingQuery: RecordingQuery = {
|
||||
@@ -992,7 +1046,7 @@ export class FrigateCameraManagerEngine
|
||||
return `${cameraID}/${startTime.getDate()}/${startTime.getHours()}`;
|
||||
};
|
||||
|
||||
const results = await this.getRecordings(hass, cameras, recordingQuery);
|
||||
const results = await this.getRecordings(hass, store, recordingQuery);
|
||||
if (!results) {
|
||||
return;
|
||||
}
|
||||
@@ -1053,21 +1107,6 @@ export class FrigateCameraManagerEngine
|
||||
return seekMilliseconds / 1000;
|
||||
}
|
||||
|
||||
public getCameraCapabilities(
|
||||
cameraConfig: CameraConfig,
|
||||
): CameraManagerCameraCapabilities {
|
||||
const isBirdseye = cameraConfig.frigate.camera_name === CAMERA_BIRDSEYE;
|
||||
return {
|
||||
canFavoriteEvents: !isBirdseye,
|
||||
canFavoriteRecordings: !isBirdseye,
|
||||
canSeek: true,
|
||||
supportsClips: !isBirdseye,
|
||||
supportsSnapshots: !isBirdseye,
|
||||
supportsRecordings: !isBirdseye,
|
||||
supportsTimeline: !isBirdseye,
|
||||
};
|
||||
}
|
||||
|
||||
public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities {
|
||||
return {
|
||||
canFavorite: ViewMediaClassifier.isEvent(media),
|
||||
@@ -1189,4 +1228,44 @@ export class FrigateCameraManagerEngine
|
||||
...(webrtcCard && { webrtcCard: webrtcCard }),
|
||||
};
|
||||
}
|
||||
|
||||
public async executePTZAction(
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
action: PTZAction,
|
||||
options?: {
|
||||
phase?: PTZPhase;
|
||||
preset?: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
const cameraEntity = cameraConfig.camera_entity;
|
||||
|
||||
if (action === 'preset' && !options?.preset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Awkward translation between card action and service parameters:
|
||||
// https://github.com/blakeblackshear/frigate-hass-integration/blob/dev/custom_components/frigate/services.yaml
|
||||
await hass.callService('frigate', 'ptz', {
|
||||
entity_id: cameraEntity,
|
||||
action:
|
||||
options?.phase === 'stop'
|
||||
? 'stop'
|
||||
: action === 'zoom_in' || action === 'zoom_out'
|
||||
? 'zoom'
|
||||
: action === 'preset'
|
||||
? 'preset'
|
||||
: 'move',
|
||||
...(options?.phase !== 'stop' && {
|
||||
argument:
|
||||
action === 'zoom_in'
|
||||
? 'in'
|
||||
: action === 'zoom_out'
|
||||
? 'out'
|
||||
: action === 'preset'
|
||||
? options?.preset
|
||||
: action,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
eventSummarySchema,
|
||||
FrigateEvent,
|
||||
frigateEventsSchema,
|
||||
PTZInfo,
|
||||
ptzInfoSchema,
|
||||
recordingSegmentsSchema,
|
||||
RecordingSummary,
|
||||
recordingSummarySchema,
|
||||
@@ -151,3 +153,20 @@ export const getEventSummary = async (
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
export const getPTZInfo = async (
|
||||
hass: HomeAssistant,
|
||||
clientID: string,
|
||||
cameraName: string,
|
||||
): Promise<PTZInfo> => {
|
||||
return await homeAssistantWSRequest(
|
||||
hass,
|
||||
ptzInfoSchema,
|
||||
{
|
||||
type: 'frigate/ptz/info',
|
||||
instance_id: clientID,
|
||||
camera: cameraName,
|
||||
},
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -76,6 +76,13 @@ export const eventSummarySchema = z
|
||||
.array();
|
||||
export type EventSummary = z.infer<typeof eventSummarySchema>;
|
||||
|
||||
export const ptzInfoSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
features: z.string().array().optional(),
|
||||
presets: z.string().array().optional(),
|
||||
});
|
||||
export type PTZInfo = z.infer<typeof ptzInfoSchema>;
|
||||
|
||||
// ==============================
|
||||
// Frigate concrete query results
|
||||
// ==============================
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { CameraConfig } from '../../config/types';
|
||||
import {
|
||||
CameraConfig,
|
||||
PTZAction,
|
||||
PTZPhase
|
||||
} from '../../config/types';
|
||||
import { ExtendedHomeAssistant } from '../../types';
|
||||
import { getEntityIcon, getEntityTitle } from '../../utils/ha';
|
||||
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { Camera } from '../camera';
|
||||
import { CameraManagerEngine } from '../engine';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||
import {
|
||||
CameraConfigs,
|
||||
CameraEndpoint,
|
||||
CameraEndpoints,
|
||||
CameraEndpointsContext,
|
||||
CameraManagerCameraCapabilities,
|
||||
CameraManagerCameraMetadata,
|
||||
CameraManagerMediaCapabilities,
|
||||
DataQuery,
|
||||
@@ -29,7 +33,7 @@ import {
|
||||
RecordingQuery,
|
||||
RecordingQueryResultsMap,
|
||||
RecordingSegmentsQuery,
|
||||
RecordingSegmentsQueryResultsMap,
|
||||
RecordingSegmentsQueryResultsMap
|
||||
} from '../types';
|
||||
import { getCameraEntityFromConfig, getDefaultGo2RTCEndpoint } from '../utils.js';
|
||||
|
||||
@@ -42,12 +46,20 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
_hass: HomeAssistant,
|
||||
_entityRegistryManager: EntityRegistryManager,
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<CameraConfig> {
|
||||
return cameraConfig;
|
||||
): Promise<Camera> {
|
||||
return new Camera(cameraConfig, this, {
|
||||
canFavoriteEvents: false,
|
||||
canFavoriteRecordings: false,
|
||||
canSeek: false,
|
||||
supportsClips: false,
|
||||
supportsRecordings: false,
|
||||
supportsSnapshots: false,
|
||||
supportsTimeline: false,
|
||||
});
|
||||
}
|
||||
|
||||
public generateDefaultEventQuery(
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
_cameraIDs: Set<string>,
|
||||
_query: PartialEventQuery,
|
||||
): EventQuery[] | null {
|
||||
@@ -55,7 +67,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
}
|
||||
|
||||
public generateDefaultRecordingQuery(
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
_cameraIDs: Set<string>,
|
||||
_query: PartialRecordingQuery,
|
||||
): RecordingQuery[] | null {
|
||||
@@ -63,7 +75,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
}
|
||||
|
||||
public generateDefaultRecordingSegmentsQuery(
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
_cameraIDs: Set<string>,
|
||||
_query: PartialRecordingSegmentsQuery,
|
||||
): RecordingSegmentsQuery[] | null {
|
||||
@@ -72,7 +84,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public async getEvents(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
_query: EventQuery,
|
||||
_engineOptions?: EngineOptions,
|
||||
): Promise<EventQueryResultsMap | null> {
|
||||
@@ -81,7 +93,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public async getRecordings(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
_query: RecordingQuery,
|
||||
_engineOptions?: EngineOptions,
|
||||
): Promise<RecordingQueryResultsMap | null> {
|
||||
@@ -90,7 +102,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public async getRecordingSegments(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
_query: RecordingSegmentsQuery,
|
||||
_engineOptions?: EngineOptions,
|
||||
): Promise<RecordingSegmentsQueryResultsMap | null> {
|
||||
@@ -99,7 +111,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public generateMediaFromEvents(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
_query: EventQuery,
|
||||
_results: QueryReturnType<EventQuery>,
|
||||
): ViewMedia[] | null {
|
||||
@@ -108,7 +120,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public generateMediaFromRecordings(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
_query: RecordingQuery,
|
||||
_results: QueryReturnType<RecordingQuery>,
|
||||
): ViewMedia[] | null {
|
||||
@@ -138,7 +150,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public async getMediaSeekTime(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
_media: ViewMedia,
|
||||
_target: Date,
|
||||
_engineOptions?: EngineOptions,
|
||||
@@ -148,7 +160,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
|
||||
public async getMediaMetadata(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
_query: MediaMetadataQuery,
|
||||
_engineOptions?: EngineOptions,
|
||||
): Promise<MediaMetadataQueryResultsMap | null> {
|
||||
@@ -173,20 +185,6 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
};
|
||||
}
|
||||
|
||||
public getCameraCapabilities(
|
||||
_cameraConfig: CameraConfig,
|
||||
): CameraManagerCameraCapabilities | null {
|
||||
return {
|
||||
canFavoriteEvents: false,
|
||||
canFavoriteRecordings: false,
|
||||
canSeek: false,
|
||||
supportsClips: false,
|
||||
supportsRecordings: false,
|
||||
supportsSnapshots: false,
|
||||
supportsTimeline: false,
|
||||
};
|
||||
}
|
||||
|
||||
public getMediaCapabilities(_media: ViewMedia): CameraManagerMediaCapabilities | null {
|
||||
return null;
|
||||
}
|
||||
@@ -202,4 +200,16 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
public async executePTZAction(
|
||||
_hass: HomeAssistant,
|
||||
_cameraConfig: CameraConfig,
|
||||
_action: PTZAction,
|
||||
_options?: {
|
||||
phase?: PTZPhase;
|
||||
preset?: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
// Pass.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import add from 'date-fns/add';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import merge from 'lodash-es/merge.js';
|
||||
import sum from 'lodash-es/sum';
|
||||
import { CardCameraAPI } from '../card-controller/types.js';
|
||||
import { CameraConfig, CamerasConfig } from '../config/types.js';
|
||||
import { CameraConfig, CamerasConfig, PTZAction, PTZPhase } from '../config/types.js';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import { allPromises, arrayify, setify } from '../utils/basic.js';
|
||||
import { getCameraID } from '../utils/camera.js';
|
||||
import { log } from '../utils/debug.js';
|
||||
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
||||
import { ViewMedia } from '../view/media.js';
|
||||
import { CameraManagerEngineFactory } from './engine-factory.js';
|
||||
import { CameraManagerEngine } from './engine.js';
|
||||
@@ -49,7 +47,7 @@ import {
|
||||
RecordingSegmentsQuery,
|
||||
RecordingSegmentsQueryResults,
|
||||
RecordingSegmentsQueryResultsMap,
|
||||
ResultsMap,
|
||||
ResultsMap
|
||||
} from './types.js';
|
||||
import { sortMedia } from './utils.js';
|
||||
|
||||
@@ -102,31 +100,26 @@ export interface ExtendedMediaQueryResult<T extends MediaQuery> {
|
||||
results: ViewMedia[];
|
||||
}
|
||||
|
||||
interface InitializedCamera {
|
||||
inputConfig: CameraConfig;
|
||||
initializedConfig: CameraConfig;
|
||||
engine: CameraManagerEngine;
|
||||
}
|
||||
|
||||
export class CameraManager {
|
||||
protected _api: CardCameraAPI;
|
||||
protected _engineFactory: CameraManagerEngineFactory;
|
||||
protected _store = new CameraManagerStore();
|
||||
protected _store: CameraManagerStore;
|
||||
|
||||
constructor(api: CardCameraAPI) {
|
||||
constructor(api: CardCameraAPI, store?: CameraManagerStore) {
|
||||
this._api = api;
|
||||
this._engineFactory = new CameraManagerEngineFactory(
|
||||
this._api.getEntityRegistryManager(),
|
||||
this._api.getResolvedMediaCache(),
|
||||
);
|
||||
this._store = store ?? new CameraManagerStore();
|
||||
}
|
||||
|
||||
public async initializeCamerasFromConfig(): Promise<void> {
|
||||
public async initializeCamerasFromConfig(): Promise<boolean> {
|
||||
const config = this._api.getConfigManager().getConfig();
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
|
||||
if (!config || !hass) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this._store.reset();
|
||||
@@ -143,7 +136,9 @@ export class CameraManager {
|
||||
await this._initializeCameras(cameras);
|
||||
} catch (e: unknown) {
|
||||
this._api.getMessageManager().setErrorIfHigherPriority(e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async _getEnginesForCameras(
|
||||
@@ -172,7 +167,9 @@ export class CameraManager {
|
||||
if (!engine || !engineType) {
|
||||
throw new CameraInitializationError(
|
||||
localize('error.no_camera_engine'),
|
||||
cameraConfig,
|
||||
// Camera initialization may modify the configuration. Keep the
|
||||
// original config unchanged.
|
||||
cloneDeep(cameraConfig),
|
||||
);
|
||||
}
|
||||
engines.set(engineType, engine);
|
||||
@@ -181,27 +178,6 @@ export class CameraManager {
|
||||
return output;
|
||||
}
|
||||
|
||||
protected async _initializeCamera(
|
||||
hass: HomeAssistant,
|
||||
engine: CameraManagerEngine,
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
inputCameraConfig: CameraConfig,
|
||||
): Promise<InitializedCamera> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
protected async _initializeCameras(camerasConfig: CamerasConfig): Promise<void> {
|
||||
const initializationStartTime = new Date();
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
@@ -229,12 +205,11 @@ export class CameraManager {
|
||||
const engineByConfig = await this._getEnginesForCameras(camerasConfig);
|
||||
|
||||
// Configuration is initialized in parallel.
|
||||
const results = await allPromises(
|
||||
const cameras = await allPromises(
|
||||
engineByConfig.entries(),
|
||||
async ([cameraConfig, engine]) =>
|
||||
await this._initializeCamera(
|
||||
await engine.initializeCamera(
|
||||
hass,
|
||||
engine,
|
||||
this._api.getEntityRegistryManager(),
|
||||
cameraConfig,
|
||||
),
|
||||
@@ -242,27 +217,26 @@ export class CameraManager {
|
||||
|
||||
// Do the additions based off the result-order, to ensure the map order is
|
||||
// preserved.
|
||||
results.forEach((result) => {
|
||||
const id = getCameraID(result.initializedConfig);
|
||||
cameras.forEach((camera) => {
|
||||
const cameraID = getCameraID(camera.getConfig());
|
||||
|
||||
if (!id) {
|
||||
if (!cameraID) {
|
||||
throw new CameraInitializationError(
|
||||
localize('error.no_camera_id'),
|
||||
result.inputConfig,
|
||||
camera.getConfig(),
|
||||
);
|
||||
}
|
||||
|
||||
if (this._store.hasCameraID(id)) {
|
||||
if (this._store.hasCameraID(cameraID)) {
|
||||
throw new CameraInitializationError(
|
||||
localize('error.duplicate_camera_id'),
|
||||
result.inputConfig,
|
||||
camera.getConfig(),
|
||||
);
|
||||
}
|
||||
|
||||
// Always ensure the actual ID used in the card is in the configuration itself.
|
||||
result.initializedConfig.id = id;
|
||||
|
||||
this._store.addCamera(id, result.initializedConfig, result.engine);
|
||||
camera.setID(cameraID);
|
||||
this._store.addCamera(camera);
|
||||
});
|
||||
|
||||
if (!this._store.getVisibleCameraCount()) {
|
||||
@@ -372,20 +346,16 @@ export class CameraManager {
|
||||
for (const [engine, cameraIDs] of engines) {
|
||||
let queries: DataQuery[] | null = null;
|
||||
if (QueryClassifier.isEventQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultEventQuery(
|
||||
this._store.getVisibleCameras(),
|
||||
cameraIDs,
|
||||
partialQuery,
|
||||
);
|
||||
queries = engine.generateDefaultEventQuery(this._store, cameraIDs, partialQuery);
|
||||
} else if (QueryClassifier.isRecordingQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultRecordingQuery(
|
||||
this._store.getVisibleCameras(),
|
||||
this._store,
|
||||
cameraIDs,
|
||||
partialQuery,
|
||||
);
|
||||
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultRecordingSegmentsQuery(
|
||||
this._store.getVisibleCameras(),
|
||||
this._store,
|
||||
cameraIDs,
|
||||
partialQuery,
|
||||
);
|
||||
@@ -596,7 +566,7 @@ export class CameraManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await engine.getMediaSeekTime(hass, this._store.getCameras(), media, target);
|
||||
return await engine.getMediaSeekTime(hass, this._store, media, target);
|
||||
}
|
||||
|
||||
protected async _handleQuery<QT extends DataQuery>(
|
||||
@@ -624,28 +594,28 @@ export class CameraManager {
|
||||
if (QueryClassifier.isEventQuery(query)) {
|
||||
engineResult = (await engine.getEvents(
|
||||
hass,
|
||||
this._store.getCameras(),
|
||||
this._store,
|
||||
query,
|
||||
engineOptions,
|
||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||
} else if (QueryClassifier.isRecordingQuery(query)) {
|
||||
engineResult = (await engine.getRecordings(
|
||||
hass,
|
||||
this._store.getCameras(),
|
||||
this._store,
|
||||
query,
|
||||
engineOptions,
|
||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||
} else if (QueryClassifier.isRecordingSegmentsQuery(query)) {
|
||||
engineResult = (await engine.getRecordingSegments(
|
||||
hass,
|
||||
this._store.getCameras(),
|
||||
this._store,
|
||||
query,
|
||||
engineOptions,
|
||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||
} else if (QueryClassifier.isMediaMetadataQuery(query)) {
|
||||
engineResult = (await engine.getMediaMetadata(
|
||||
hass,
|
||||
this._store.getCameras(),
|
||||
this._store,
|
||||
query,
|
||||
engineOptions,
|
||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||
@@ -710,22 +680,12 @@ export class CameraManager {
|
||||
QueryClassifier.isEventQuery(query) &&
|
||||
QueryResultClassifier.isEventQueryResult(result)
|
||||
) {
|
||||
media = engine.generateMediaFromEvents(
|
||||
hass,
|
||||
this._store.getCameras(),
|
||||
query,
|
||||
result,
|
||||
);
|
||||
media = engine.generateMediaFromEvents(hass, this._store, query, result);
|
||||
} else if (
|
||||
QueryClassifier.isRecordingQuery(query) &&
|
||||
QueryResultClassifier.isRecordingQuery(result)
|
||||
) {
|
||||
media = engine.generateMediaFromRecordings(
|
||||
hass,
|
||||
this._store.getCameras(),
|
||||
query,
|
||||
result,
|
||||
);
|
||||
media = engine.generateMediaFromRecordings(hass, this._store, query, result);
|
||||
}
|
||||
if (media) {
|
||||
mediaArray.push(...media);
|
||||
@@ -761,12 +721,7 @@ export class CameraManager {
|
||||
public getCameraCapabilities(
|
||||
cameraID: string,
|
||||
): CameraManagerCameraCapabilities | null {
|
||||
const cameraConfig = this._store.getCameraConfig(cameraID);
|
||||
const engine = this._store.getEngineForCameraID(cameraID);
|
||||
if (!cameraConfig || !engine) {
|
||||
return null;
|
||||
}
|
||||
return engine.getCameraCapabilities(cameraConfig);
|
||||
return this._store.getCamera(cameraID)?.getCapabilities() ?? null;
|
||||
}
|
||||
|
||||
public getAggregateCameraCapabilities(
|
||||
@@ -787,6 +742,26 @@ export class CameraManager {
|
||||
supportsRecordings: perCameraCapabilities.some((cap) => cap?.supportsRecordings),
|
||||
supportsSnapshots: perCameraCapabilities.some((cap) => cap?.supportsSnapshots),
|
||||
supportsTimeline: perCameraCapabilities.some((cap) => cap?.supportsTimeline),
|
||||
|
||||
supportsPTZ: perCameraCapabilities.some((cap) => !!cap?.ptz),
|
||||
};
|
||||
}
|
||||
|
||||
public async executePTZAction(
|
||||
cameraID: string,
|
||||
action: PTZAction,
|
||||
options: {
|
||||
phase?: PTZPhase;
|
||||
preset?: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
const engine = this._store.getEngineForCameraID(cameraID);
|
||||
const cameraConfig = this._store.getCameraConfig(cameraID);
|
||||
|
||||
if (!engine || !cameraConfig || !hass) {
|
||||
return;
|
||||
}
|
||||
return engine.executePTZAction(hass, cameraConfig, action, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,25 +8,23 @@ import { CameraConfig } from '../../config/types';
|
||||
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
|
||||
import {
|
||||
BrowseMediaStep,
|
||||
BrowseMediaTarget,
|
||||
BrowseMediaTarget
|
||||
} from '../../utils/ha/browse-media/browse-media-manager';
|
||||
import {
|
||||
BROWSE_MEDIA_CACHE_SECONDS,
|
||||
BrowseMedia,
|
||||
MEDIA_CLASS_IMAGE,
|
||||
BrowseMedia, BROWSE_MEDIA_CACHE_SECONDS, MEDIA_CLASS_IMAGE,
|
||||
MEDIA_CLASS_VIDEO,
|
||||
RichBrowseMedia,
|
||||
RichBrowseMedia
|
||||
} from '../../utils/ha/browse-media/types';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import {
|
||||
BrowseMediaCameraManagerEngine,
|
||||
getViewMediaFromBrowseMediaArray,
|
||||
isMediaWithinDates,
|
||||
isMediaWithinDates
|
||||
} from '../browse-media/engine-browse-media';
|
||||
import { BrowseMediaMetadata } from '../browse-media/types';
|
||||
import { CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT } from '../engine';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||
import {
|
||||
CameraConfigs,
|
||||
CameraEndpoint,
|
||||
CameraEndpoints,
|
||||
CameraEndpointsContext,
|
||||
@@ -41,7 +39,7 @@ import {
|
||||
MediaMetadataQueryResultsMap,
|
||||
QueryResults,
|
||||
QueryResultsType,
|
||||
QueryReturnType,
|
||||
QueryReturnType
|
||||
} from '../types';
|
||||
import motioneyeLogo from './assets/motioneye-logo.svg';
|
||||
import { MotionEyeEventQueryResults } from './types';
|
||||
@@ -126,7 +124,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
// Get media directories that match a given criteria.
|
||||
protected async _getMatchingDirectories(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
cameraID: string,
|
||||
matchOptions?: {
|
||||
start?: Date;
|
||||
@@ -136,11 +134,11 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
} | null,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<RichBrowseMedia<BrowseMediaMetadata>[] | null> {
|
||||
const cameraEntityID = cameras.get(cameraID)?.camera_entity;
|
||||
const cameraConfig = store.getCameraConfig(cameraID);
|
||||
const cameraEntityID = cameraConfig?.camera_entity;
|
||||
const entity = cameraEntityID ? this._cameraEntities.get(cameraEntityID) : null;
|
||||
const configID = entity?.config_entry_id;
|
||||
const deviceID = entity?.device_id;
|
||||
const cameraConfig = cameras.get(cameraID);
|
||||
|
||||
if (!configID || !deviceID || !cameraConfig) {
|
||||
return null;
|
||||
@@ -206,7 +204,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
|
||||
public async getEvents(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: EventQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<EventQueryResultsMap | null> {
|
||||
@@ -225,14 +223,14 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
return;
|
||||
}
|
||||
|
||||
const cameraConfig = cameras.get(cameraID);
|
||||
const cameraConfig = store.getCameraConfig(cameraID);
|
||||
if (!cameraConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const directories = await this._getMatchingDirectories(
|
||||
hass,
|
||||
cameras,
|
||||
store,
|
||||
cameraID,
|
||||
perCameraQuery,
|
||||
engineOptions,
|
||||
@@ -309,7 +307,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
|
||||
public generateMediaFromEvents(
|
||||
_hass: HomeAssistant,
|
||||
_cameras: CameraConfigs,
|
||||
_store: CameraManagerReadOnlyConfigStore,
|
||||
_query: EventQuery,
|
||||
results: QueryReturnType<EventQuery>,
|
||||
): ViewMedia[] | null {
|
||||
@@ -321,7 +319,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
|
||||
public async getMediaMetadata(
|
||||
hass: HomeAssistant,
|
||||
cameras: CameraConfigs,
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: MediaMetadataQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<MediaMetadataQueryResultsMap | null> {
|
||||
@@ -340,7 +338,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
const getDaysForCamera = async (cameraID: string): Promise<void> => {
|
||||
const directories = await this._getMatchingDirectories(
|
||||
hass,
|
||||
cameras,
|
||||
store,
|
||||
cameraID,
|
||||
null,
|
||||
engineOptions,
|
||||
|
||||
+82
-38
@@ -1,7 +1,8 @@
|
||||
import { CameraConfig } from '../config/types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { Camera } from './camera';
|
||||
import { CameraManagerEngine } from './engine';
|
||||
import { CameraConfigs, Engine } from './types';
|
||||
import { Engine } from './types';
|
||||
|
||||
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
|
||||
|
||||
@@ -10,74 +11,82 @@ export interface CameraManagerReadOnlyConfigStore {
|
||||
getCameraConfigForMedia(media: ViewMedia): CameraConfig | null;
|
||||
|
||||
hasCameraID(cameraID: string): boolean;
|
||||
hasVisibleCameraID(cameraID: string): boolean;
|
||||
|
||||
getCamera(cameraID: string): Camera | null;
|
||||
getCameraCount(): number;
|
||||
getVisibleCameraCount(): number;
|
||||
|
||||
getCameras(): CameraConfigs;
|
||||
getVisibleCameras(): CameraConfigs;
|
||||
getCameraConfigs(cameraIDs?: Iterable<string>): IterableIterator<CameraConfig>;
|
||||
getCameraConfigEntries(
|
||||
cameraIDs?: Iterable<string>,
|
||||
): IterableIterator<[string, CameraConfig]>;
|
||||
|
||||
getCameraIDs(): Set<string>;
|
||||
getVisibleCameraIDs(): Set<string>;
|
||||
|
||||
getAllDependentCameras(cameraID: string): Set<string>;
|
||||
}
|
||||
|
||||
export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
||||
protected _allConfigs: Map<string, CameraConfig> = new Map();
|
||||
protected _visibleConfigs: Map<string, CameraConfig> = new Map();
|
||||
protected _enginesByCamera: Map<string, CameraManagerEngine> = new Map();
|
||||
protected _cameras: Map<string, Camera> = new Map();
|
||||
protected _enginesByType: Map<Engine, CameraManagerEngine> = new Map();
|
||||
|
||||
public addCamera(
|
||||
cameraID: string,
|
||||
cameraConfig: CameraConfig,
|
||||
engine: CameraManagerEngine,
|
||||
): void {
|
||||
if (!cameraConfig.hide) {
|
||||
this._visibleConfigs.set(cameraID, cameraConfig);
|
||||
}
|
||||
this._allConfigs.set(cameraID, cameraConfig);
|
||||
this._enginesByCamera.set(cameraID, engine);
|
||||
this._enginesByType.set(engine.getEngineType(), engine);
|
||||
public addCamera(camera: Camera): void {
|
||||
this._cameras.set(camera.getID(), camera);
|
||||
this._enginesByType.set(camera.getEngine().getEngineType(), camera.getEngine());
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._allConfigs.clear();
|
||||
this._visibleConfigs.clear();
|
||||
this._enginesByCamera.clear();
|
||||
this._cameras.clear();
|
||||
this._enginesByType.clear();
|
||||
}
|
||||
|
||||
public getCamera(cameraID: string): Camera | null {
|
||||
return this._cameras.get(cameraID) ?? null;
|
||||
}
|
||||
public getCameraConfig(cameraID: string): CameraConfig | null {
|
||||
return this._allConfigs.get(cameraID) ?? null;
|
||||
return this._cameras.get(cameraID)?.getConfig() ?? null;
|
||||
}
|
||||
|
||||
public hasCameraID(cameraID: string): boolean {
|
||||
return this._allConfigs.has(cameraID);
|
||||
}
|
||||
public hasVisibleCameraID(cameraID: string): boolean {
|
||||
return this._visibleConfigs.has(cameraID);
|
||||
return this._cameras.has(cameraID);
|
||||
}
|
||||
|
||||
public getCameraCount(): number {
|
||||
return this._allConfigs.size;
|
||||
return this._cameras.size;
|
||||
}
|
||||
public getVisibleCameraCount(): number {
|
||||
return this._visibleConfigs.size;
|
||||
return this.getVisibleCameraIDs().size;
|
||||
}
|
||||
|
||||
public getCameras(): CameraConfigs {
|
||||
return this._allConfigs;
|
||||
public getCameras(): Map<string, Camera> {
|
||||
return this._cameras;
|
||||
}
|
||||
public getVisibleCameras(): CameraConfigs {
|
||||
return this._visibleConfigs;
|
||||
|
||||
public *getCameraConfigs(
|
||||
cameraIDs?: Iterable<string>,
|
||||
): IterableIterator<CameraConfig> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const [_cameraID, config] of this.getCameraConfigEntries(cameraIDs)) {
|
||||
yield config;
|
||||
}
|
||||
}
|
||||
public *getCameraConfigEntries(
|
||||
cameraIDs?: Iterable<string>,
|
||||
): IterableIterator<[string, CameraConfig]> {
|
||||
for (const cameraID of cameraIDs ?? this._cameras.keys()) {
|
||||
const config = this.getCameraConfig(cameraID);
|
||||
|
||||
if (config) {
|
||||
yield [cameraID, config];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getCameraIDs(): Set<string> {
|
||||
return new Set(this._allConfigs.keys());
|
||||
return new Set(this._cameras.keys());
|
||||
}
|
||||
public getVisibleCameraIDs(): Set<string> {
|
||||
return new Set(this._visibleConfigs.keys());
|
||||
return this._getMatchingCameraIDs((camera) => !camera.getConfig().hide);
|
||||
}
|
||||
|
||||
public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null {
|
||||
@@ -89,7 +98,7 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
||||
}
|
||||
|
||||
public getEngineForCameraID(cameraID: string): CameraManagerEngine | null {
|
||||
return this._enginesByCamera.get(cameraID) ?? null;
|
||||
return this._cameras.get(cameraID)?.getEngine() ?? null;
|
||||
}
|
||||
|
||||
public getEnginesForCameraIDs(
|
||||
@@ -114,7 +123,42 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
||||
return this.getEngineForCameraID(media.getCameraID());
|
||||
}
|
||||
|
||||
public getAllEngines(): CameraManagerEngine[] {
|
||||
return [...this._enginesByType.values()];
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
public getAllDependentCameras(cameraID: string): Set<string> {
|
||||
const cameraIDs: Set<string> = new Set();
|
||||
const getDependentCameras = (cameraID: string): void => {
|
||||
const cameraConfig = this.getCameraConfig(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) {
|
||||
this.getCameraIDs().forEach((cameraID) => dependentCameras.add(cameraID));
|
||||
}
|
||||
for (const eventCameraID of dependentCameras) {
|
||||
if (!cameraIDs.has(eventCameraID)) {
|
||||
getDependentCameras(eventCameraID);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
getDependentCameras(cameraID);
|
||||
return cameraIDs;
|
||||
}
|
||||
|
||||
protected _getMatchingCameraIDs(func: (camera: Camera) => boolean): Set<string> {
|
||||
const output = new Set<string>();
|
||||
for (const [cameraID, camera] of this._cameras.entries()) {
|
||||
if (func(camera)) {
|
||||
output.add(cameraID);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CameraConfig, FrigateCardView } from '../config/types';
|
||||
import { FrigateCardView } from '../config/types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
|
||||
// ====
|
||||
@@ -91,6 +91,14 @@ export interface MediaMetadata {
|
||||
what?: Set<string>;
|
||||
}
|
||||
|
||||
export type PTZMovementType = 'relative' | 'continuous';
|
||||
|
||||
export interface PTZCapabilities {
|
||||
panTilt?: PTZMovementType[];
|
||||
zoom?: PTZMovementType[];
|
||||
presets?: string[];
|
||||
}
|
||||
|
||||
interface BaseCapabilities {
|
||||
canFavoriteEvents: boolean;
|
||||
canFavoriteRecordings: boolean;
|
||||
@@ -102,8 +110,14 @@ interface BaseCapabilities {
|
||||
supportsTimeline: boolean;
|
||||
}
|
||||
|
||||
export type CameraManagerCapabilities = BaseCapabilities;
|
||||
export type CameraManagerCameraCapabilities = BaseCapabilities;
|
||||
export interface CameraManagerCapabilities extends BaseCapabilities {
|
||||
supportsPTZ: boolean;
|
||||
}
|
||||
|
||||
export interface CameraManagerCameraCapabilities extends BaseCapabilities {
|
||||
ptz?: PTZCapabilities;
|
||||
}
|
||||
|
||||
export interface CameraManagerMediaCapabilities {
|
||||
canFavorite: boolean;
|
||||
canDownload: boolean;
|
||||
@@ -132,8 +146,6 @@ export interface CameraEndpoints {
|
||||
webrtcCard?: CameraEndpoint;
|
||||
}
|
||||
|
||||
export type CameraConfigs = Map<string, CameraConfig>;
|
||||
|
||||
export interface EngineOptions {
|
||||
useCache?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user