Allow camera capabilities to be configured.
This commit is contained in:
@@ -14,6 +14,7 @@ import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media'
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { RequestCache } from '../cache';
|
||||
import { Camera } from '../camera';
|
||||
import { Capabilities } from '../capabilities';
|
||||
import { CameraManagerEngine } from '../engine';
|
||||
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||
import { rangesOverlap } from '../range';
|
||||
@@ -142,15 +143,23 @@ export class BrowseMediaCameraManagerEngine
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<Camera> {
|
||||
const camera = new BrowseMediaCamera(cameraConfig, this, {
|
||||
capabilities: {
|
||||
canFavoriteEvents: false,
|
||||
canFavoriteRecordings: false,
|
||||
canSeek: false,
|
||||
supportsClips: true,
|
||||
supportsRecordings: false,
|
||||
supportsSnapshots: true,
|
||||
supportsTimeline: true,
|
||||
},
|
||||
capabilities: new Capabilities(
|
||||
{
|
||||
'favorite-events': false,
|
||||
'favorite-recordings': false,
|
||||
clips: true,
|
||||
live: true,
|
||||
menu: true,
|
||||
recordings: false,
|
||||
seek: false,
|
||||
snapshots: true,
|
||||
substream: true,
|
||||
},
|
||||
{
|
||||
disable: cameraConfig.capabilities?.disable,
|
||||
disableExcept: cameraConfig.capabilities?.disable_except,
|
||||
},
|
||||
),
|
||||
eventCallback: this._eventCallback,
|
||||
});
|
||||
return await camera.initialize(hass, entityRegistryManager);
|
||||
|
||||
@@ -10,14 +10,15 @@ import {
|
||||
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||
import { CameraManagerEngine } from './engine';
|
||||
import { CameraNoIDError } from './error';
|
||||
import { CameraEventCallback, CameraManagerCameraCapabilities } from './types';
|
||||
import { CameraEventCallback } from './types';
|
||||
import { Capabilities } from './capabilities';
|
||||
|
||||
type DestroyCallback = () => Promise<void>;
|
||||
|
||||
export class Camera {
|
||||
protected _config: CameraConfig;
|
||||
protected _engine: CameraManagerEngine;
|
||||
protected _capabilities?: CameraManagerCameraCapabilities;
|
||||
protected _capabilities?: Capabilities;
|
||||
protected _eventCallback?: CameraEventCallback;
|
||||
protected _destroyCallbacks: DestroyCallback[] = [];
|
||||
|
||||
@@ -25,7 +26,7 @@ export class Camera {
|
||||
config: CameraConfig,
|
||||
engine: CameraManagerEngine,
|
||||
options?: {
|
||||
capabilities?: CameraManagerCameraCapabilities;
|
||||
capabilities?: Capabilities;
|
||||
eventCallback?: CameraEventCallback;
|
||||
},
|
||||
) {
|
||||
@@ -97,7 +98,7 @@ export class Camera {
|
||||
return this._engine;
|
||||
}
|
||||
|
||||
public getCapabilities(): CameraManagerCameraCapabilities | null {
|
||||
public getCapabilities(): Capabilities | null {
|
||||
return this._capabilities ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
CapabilitiesRaw,
|
||||
CapabilityKey,
|
||||
capabilityKeys,
|
||||
PTZCapabilities,
|
||||
} from '../types';
|
||||
import { CapabilitySearchOptions } from './types';
|
||||
|
||||
export class Capabilities {
|
||||
private _capabilities: CapabilitiesRaw;
|
||||
|
||||
constructor(
|
||||
capabilities: CapabilitiesRaw,
|
||||
options?: {
|
||||
disable?: CapabilityKey[];
|
||||
disableExcept?: CapabilityKey[];
|
||||
},
|
||||
) {
|
||||
this._capabilities = capabilities;
|
||||
|
||||
for (const key of options?.disable ?? []) {
|
||||
this._disable(key);
|
||||
}
|
||||
for (const key of capabilityKeys) {
|
||||
if (options?.disableExcept?.length && !options.disableExcept.includes(key)) {
|
||||
this._disable(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected _disable(capability: CapabilityKey): void {
|
||||
delete this._capabilities[capability];
|
||||
}
|
||||
|
||||
public matches(capability: CapabilitySearchOptions): boolean {
|
||||
let result = true;
|
||||
if (typeof capability === 'string') {
|
||||
result &&= this.has(capability);
|
||||
}
|
||||
if (typeof capability === 'object' && capability.allCapabilities) {
|
||||
result &&= capability.allCapabilities.every((capability) => this.has(capability));
|
||||
}
|
||||
if (typeof capability === 'object' && capability.anyCapabilities) {
|
||||
result &&= capability.anyCapabilities.some((capability) => this.has(capability));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public has(capability: CapabilityKey): boolean {
|
||||
return !!this._capabilities[capability];
|
||||
}
|
||||
|
||||
public getPTZCapabilities(): PTZCapabilities | null {
|
||||
return this._capabilities.ptz ?? null;
|
||||
}
|
||||
|
||||
public getRawCapabilities(): CapabilitiesRaw {
|
||||
return this._capabilities;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { MemoryRequestCache, RecordingSegmentsCache, RequestCache } from './cach
|
||||
import { CameraManagerEngine } from './engine';
|
||||
import { CameraInitializationError } from './error';
|
||||
import { CameraEventCallback, Engine } from './types';
|
||||
import { getCameraEntityFromConfig } from './utils';
|
||||
import { getCameraEntityFromConfig } from './utils/camera-entity-from-config';
|
||||
|
||||
export class CameraManagerEngineFactory {
|
||||
protected _entityRegistryManager: EntityRegistryManager;
|
||||
@@ -24,7 +24,10 @@ export class CameraManagerEngineFactory {
|
||||
this._resolvedMediaCache = resolvedMediaCache;
|
||||
}
|
||||
|
||||
public async createEngine(engine: Engine, eventCallback?: CameraEventCallback): Promise<CameraManagerEngine> {
|
||||
public async createEngine(
|
||||
engine: Engine,
|
||||
eventCallback?: CameraEventCallback,
|
||||
): Promise<CameraManagerEngine> {
|
||||
let cameraManagerEngine: CameraManagerEngine;
|
||||
switch (engine) {
|
||||
case Engine.Generic:
|
||||
|
||||
@@ -2,14 +2,15 @@ import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import uniq from 'lodash-es/uniq';
|
||||
import { CameraConfig } from '../../config/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { PTZCapabilities, PTZMovementType } from '../../types';
|
||||
import { errorToConsole } from '../../utils/basic';
|
||||
import { subscribeToTrigger } from '../../utils/ha';
|
||||
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
||||
import { Entity } from '../../utils/ha/entity-registry/types';
|
||||
import { Camera } from '../camera';
|
||||
import { Capabilities } from '../capabilities';
|
||||
import { CameraInitializationError } from '../error';
|
||||
import { PTZCapabilities, PTZMovementType } from '../types';
|
||||
import { getCameraEntityFromConfig } from '../utils.js';
|
||||
import { getCameraEntityFromConfig } from '../utils/camera-entity-from-config';
|
||||
import { getPTZInfo } from './requests';
|
||||
import { PTZInfo, frigateEventChangeTriggerResponseSchema } from './types';
|
||||
|
||||
@@ -98,16 +99,24 @@ export class FrigateCamera extends Camera {
|
||||
const config = this.getConfig();
|
||||
const ptz = await this._getPTZCapabilities(hass, config);
|
||||
const birdseye = isBirdseye(config);
|
||||
this._capabilities = {
|
||||
canFavoriteEvents: !birdseye,
|
||||
canFavoriteRecordings: !birdseye,
|
||||
canSeek: true,
|
||||
supportsClips: !birdseye,
|
||||
supportsSnapshots: !birdseye,
|
||||
supportsRecordings: !birdseye,
|
||||
supportsTimeline: !birdseye,
|
||||
...(ptz && { ptz: ptz }),
|
||||
};
|
||||
this._capabilities = new Capabilities(
|
||||
{
|
||||
'favorite-events': !birdseye,
|
||||
'favorite-recordings': false,
|
||||
seek: !birdseye,
|
||||
clips: !birdseye,
|
||||
snapshots: !birdseye,
|
||||
recordings: !birdseye,
|
||||
live: true,
|
||||
menu: true,
|
||||
substream: true,
|
||||
...(ptz && { ptz: ptz }),
|
||||
},
|
||||
{
|
||||
disable: config.capabilities?.disable,
|
||||
disableExcept: config.capabilities?.disable_except,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
protected _getFrigateCameraNameFromEntity(entity: Entity): string | null {
|
||||
|
||||
@@ -23,8 +23,8 @@ import { ViewMediaClassifier } from '../../view/media-classifier';
|
||||
import { RecordingSegmentsCache, RequestCache } from '../cache';
|
||||
import { Camera } from '../camera';
|
||||
import {
|
||||
CameraManagerEngine,
|
||||
CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
||||
CameraManagerEngine,
|
||||
} from '../engine';
|
||||
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||
import { DateRange } from '../range';
|
||||
@@ -59,18 +59,18 @@ import {
|
||||
RecordingSegmentsQuery,
|
||||
RecordingSegmentsQueryResultsMap,
|
||||
} from '../types';
|
||||
import { getDefaultGo2RTCEndpoint } from '../utils.js';
|
||||
import { getDefaultGo2RTCEndpoint } from '../utils/go2rtc-endpoint';
|
||||
import frigateLogo from './assets/frigate-logo-dark.svg';
|
||||
import { FrigateCamera, isBirdseye } from './camera';
|
||||
import { FrigateViewMediaFactory } from './media';
|
||||
import { FrigateViewMediaClassifier } from './media-classifier';
|
||||
import {
|
||||
getEvents,
|
||||
getEventSummary,
|
||||
getRecordingSegments,
|
||||
getRecordingsSummary,
|
||||
NativeFrigateEventQuery,
|
||||
NativeFrigateRecordingSegmentsQuery,
|
||||
getEventSummary,
|
||||
getEvents,
|
||||
getRecordingSegments,
|
||||
getRecordingsSummary,
|
||||
retainEvent,
|
||||
} from './requests';
|
||||
import {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getEntityIcon, getEntityTitle } from '../../utils/ha';
|
||||
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { Camera } from '../camera';
|
||||
import { Capabilities } from '../capabilities';
|
||||
import { CameraManagerEngine } from '../engine';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||
import {
|
||||
@@ -32,7 +33,8 @@ import {
|
||||
RecordingSegmentsQuery,
|
||||
RecordingSegmentsQueryResultsMap,
|
||||
} from '../types';
|
||||
import { getCameraEntityFromConfig, getDefaultGo2RTCEndpoint } from '../utils.js';
|
||||
import { getCameraEntityFromConfig } from '../utils/camera-entity-from-config';
|
||||
import { getDefaultGo2RTCEndpoint } from '../utils/go2rtc-endpoint';
|
||||
|
||||
export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
protected _eventCallback?: CameraEventCallback;
|
||||
@@ -51,15 +53,23 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
cameraConfig: CameraConfig,
|
||||
): Promise<Camera> {
|
||||
return await new Camera(cameraConfig, this, {
|
||||
capabilities: {
|
||||
canFavoriteEvents: false,
|
||||
canFavoriteRecordings: false,
|
||||
canSeek: false,
|
||||
supportsClips: false,
|
||||
supportsRecordings: false,
|
||||
supportsSnapshots: false,
|
||||
supportsTimeline: false,
|
||||
},
|
||||
capabilities: new Capabilities(
|
||||
{
|
||||
'favorite-events': false,
|
||||
'favorite-recordings': false,
|
||||
clips: false,
|
||||
live: true,
|
||||
menu: true,
|
||||
recordings: false,
|
||||
seek: false,
|
||||
snapshots: false,
|
||||
substream: true,
|
||||
},
|
||||
{
|
||||
disable: cameraConfig.capabilities?.disable,
|
||||
disableExcept: cameraConfig.capabilities?.disable_except,
|
||||
},
|
||||
),
|
||||
eventCallback: this._eventCallback,
|
||||
}).initialize(hass, entityRegistryManager);
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ import { localize } from '../localize/localize.js';
|
||||
import {
|
||||
allPromises,
|
||||
arrayify,
|
||||
isTruthy,
|
||||
recursivelyMergeObjectsNotArrays,
|
||||
setify,
|
||||
} from '../utils/basic.js';
|
||||
import { getCameraID } from '../utils/camera.js';
|
||||
import { log } from '../utils/debug.js';
|
||||
import { ViewMedia } from '../view/media.js';
|
||||
import { Capabilities } from './capabilities.js';
|
||||
import { CameraManagerEngineFactory } from './engine-factory.js';
|
||||
import { CameraManagerEngine } from './engine.js';
|
||||
import { CameraInitializationError } from './error.js';
|
||||
@@ -22,9 +24,7 @@ import {
|
||||
CameraEndpoint,
|
||||
CameraEndpoints,
|
||||
CameraEndpointsContext,
|
||||
CameraManagerCameraCapabilities,
|
||||
CameraManagerCameraMetadata,
|
||||
CameraManagerCapabilities,
|
||||
CameraManagerMediaCapabilities,
|
||||
DataQuery,
|
||||
Engine,
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
RecordingSegmentsQueryResultsMap,
|
||||
ResultsMap,
|
||||
} from './types.js';
|
||||
import { sortMedia } from './utils.js';
|
||||
import { sortMedia } from './utils/sort-media.js';
|
||||
|
||||
export class QueryClassifier {
|
||||
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
|
||||
@@ -260,10 +260,6 @@ export class CameraManager {
|
||||
this._store.addCamera(camera);
|
||||
});
|
||||
|
||||
if (!this._store.getVisibleCameraCount()) {
|
||||
throw new CameraInitializationError(localize('error.no_visible_cameras'));
|
||||
}
|
||||
|
||||
log(
|
||||
this._api.getConfigManager().getCardWideConfig(),
|
||||
'Frigate Card CameraManager initialized (Cameras: ',
|
||||
@@ -739,33 +735,29 @@ export class CameraManager {
|
||||
return engine.getCameraMetadata(hass, cameraConfig);
|
||||
}
|
||||
|
||||
public getCameraCapabilities(
|
||||
cameraID: string,
|
||||
): CameraManagerCameraCapabilities | null {
|
||||
public getCameraCapabilities(cameraID: string): Capabilities | null {
|
||||
return this._store.getCamera(cameraID)?.getCapabilities() ?? null;
|
||||
}
|
||||
|
||||
public getAggregateCameraCapabilities(
|
||||
cameraIDs?: Set<string>,
|
||||
): CameraManagerCapabilities | null {
|
||||
const perCameraCapabilities = [...(cameraIDs ?? this._store.getCameraIDs())].map(
|
||||
(cameraID) => this.getCameraCapabilities(cameraID),
|
||||
);
|
||||
public getAggregateCameraCapabilities(cameraIDs?: Set<string>): Capabilities {
|
||||
const cameras = [...(cameraIDs ?? this._store.getCameraIDs())]
|
||||
.map((cameraID) => this._store.getCamera(cameraID))
|
||||
.filter(isTruthy);
|
||||
|
||||
return {
|
||||
canFavoriteEvents: perCameraCapabilities.some((cap) => cap?.canFavoriteEvents),
|
||||
canFavoriteRecordings: perCameraCapabilities.some(
|
||||
(cap) => cap?.canFavoriteRecordings,
|
||||
return new Capabilities({
|
||||
live: cameras.some((camera) => camera.getCapabilities()?.has('live')),
|
||||
clips: cameras.some((camera) => camera.getCapabilities()?.has('clips')),
|
||||
recordings: cameras.some((camera) => camera.getCapabilities()?.has('recordings')),
|
||||
snapshots: cameras.some((camera) => camera.getCapabilities()?.has('snapshots')),
|
||||
'favorite-events': cameras.some((camera) =>
|
||||
camera.getCapabilities()?.has('favorite-events'),
|
||||
),
|
||||
canSeek: perCameraCapabilities.some((cap) => cap?.canSeek),
|
||||
|
||||
supportsClips: perCameraCapabilities.some((cap) => cap?.supportsClips),
|
||||
supportsRecordings: perCameraCapabilities.some((cap) => cap?.supportsRecordings),
|
||||
supportsSnapshots: perCameraCapabilities.some((cap) => cap?.supportsSnapshots),
|
||||
supportsTimeline: perCameraCapabilities.some((cap) => cap?.supportsTimeline),
|
||||
|
||||
supportsPTZ: perCameraCapabilities.some((cap) => !!cap?.ptz),
|
||||
};
|
||||
'favorite-recordings': cameras.some((camera) =>
|
||||
camera.getCapabilities()?.has('favorite-recordings'),
|
||||
),
|
||||
seek: cameras.some((camera) => camera.getCapabilities()?.has('seek')),
|
||||
menu: cameras.some((camera) => camera.getCapabilities()?.has('menu')),
|
||||
});
|
||||
}
|
||||
|
||||
public async executePTZAction(
|
||||
|
||||
+43
-32
@@ -1,9 +1,10 @@
|
||||
import { CameraConfig } from '../config/types';
|
||||
import { CapabilityKey } from '../types';
|
||||
import { allPromises } from '../utils/basic';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { Camera } from './camera';
|
||||
import { CameraManagerEngine } from './engine';
|
||||
import { Engine } from './types';
|
||||
import { CapabilitySearchOptions, Engine } from './types';
|
||||
|
||||
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
|
||||
|
||||
@@ -14,8 +15,8 @@ export interface CameraManagerReadOnlyConfigStore {
|
||||
hasCameraID(cameraID: string): boolean;
|
||||
|
||||
getCamera(cameraID: string): Camera | null;
|
||||
getCameras(): Map<string, Camera>;
|
||||
getCameraCount(): number;
|
||||
getVisibleCameraCount(): number;
|
||||
|
||||
getCameraConfigs(cameraIDs?: Iterable<string>): IterableIterator<CameraConfig>;
|
||||
getCameraConfigEntries(
|
||||
@@ -23,10 +24,15 @@ export interface CameraManagerReadOnlyConfigStore {
|
||||
): IterableIterator<[string, CameraConfig]>;
|
||||
|
||||
getCameraIDs(): Set<string>;
|
||||
getVisibleCameraIDs(): Set<string>;
|
||||
getDefaultCameraID(): string | null;
|
||||
|
||||
getAllDependentCameras(cameraID: string): Set<string>;
|
||||
getCameraIDsWithCapability(
|
||||
capability: CapabilityKey | CapabilitySearchOptions,
|
||||
): Set<string>;
|
||||
getAllDependentCameras(
|
||||
cameraID: string,
|
||||
capability?: CapabilityKey | CapabilitySearchOptions,
|
||||
): Set<string>;
|
||||
}
|
||||
|
||||
export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
||||
@@ -47,6 +53,9 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
||||
public getCamera(cameraID: string): Camera | null {
|
||||
return this._cameras.get(cameraID) ?? null;
|
||||
}
|
||||
public getCameras(): Map<string, Camera> {
|
||||
return this._cameras;
|
||||
}
|
||||
public getCameraConfig(cameraID: string): CameraConfig | null {
|
||||
return this._cameras.get(cameraID)?.getConfig() ?? null;
|
||||
}
|
||||
@@ -58,18 +67,11 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
||||
public getCameraCount(): number {
|
||||
return this._cameras.size;
|
||||
}
|
||||
public getVisibleCameraCount(): number {
|
||||
return this.getVisibleCameraIDs().size;
|
||||
}
|
||||
|
||||
public getDefaultCameraID(): string | null {
|
||||
return this._cameras.keys().next().value ?? null;
|
||||
}
|
||||
|
||||
public getCameras(): Map<string, Camera> {
|
||||
return this._cameras;
|
||||
}
|
||||
|
||||
public *getCameraConfigs(
|
||||
cameraIDs?: Iterable<string>,
|
||||
): IterableIterator<CameraConfig> {
|
||||
@@ -93,8 +95,17 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
||||
public getCameraIDs(): Set<string> {
|
||||
return new Set(this._cameras.keys());
|
||||
}
|
||||
public getVisibleCameraIDs(): Set<string> {
|
||||
return this._getMatchingCameraIDs((camera) => !camera.getConfig().hide);
|
||||
|
||||
public getCameraIDsWithCapability(
|
||||
capability: CapabilityKey | CapabilitySearchOptions,
|
||||
): Set<string> {
|
||||
const output: Set<string> = new Set();
|
||||
for (const camera of this._cameras.values()) {
|
||||
if (camera.getCapabilities()?.matches(capability)) {
|
||||
output.add(camera.getID());
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null {
|
||||
@@ -138,35 +149,35 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
||||
* @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();
|
||||
public getAllDependentCameras(
|
||||
cameraID: string,
|
||||
capability?: CapabilitySearchOptions,
|
||||
): Set<string> {
|
||||
const visitedCameraIDs = new Set<string>();
|
||||
const matchingCameraIDs: Set<string> = new Set();
|
||||
const getDependentCameras = (cameraID: string): void => {
|
||||
const cameraConfig = this.getCameraConfig(cameraID);
|
||||
if (cameraConfig) {
|
||||
cameraIDs.add(cameraID);
|
||||
visitedCameraIDs.add(cameraID);
|
||||
|
||||
const camera = this.getCamera(cameraID);
|
||||
const cameraConfig = camera?.getConfig();
|
||||
|
||||
if (camera && cameraConfig) {
|
||||
if (!capability || camera.getCapabilities()?.matches(capability)) {
|
||||
matchingCameraIDs.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);
|
||||
for (const dependentCameraID of dependentCameras) {
|
||||
if (!visitedCameraIDs.has(dependentCameraID)) {
|
||||
getDependentCameras(dependentCameraID);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
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;
|
||||
return matchingCameraIDs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CapabilityKey } from '../types';
|
||||
import { FrigateCardView } from '../config/types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
|
||||
@@ -91,32 +92,11 @@ 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;
|
||||
canSeek: boolean;
|
||||
|
||||
supportsClips: boolean;
|
||||
supportsRecordings: boolean;
|
||||
supportsSnapshots: boolean;
|
||||
supportsTimeline: boolean;
|
||||
}
|
||||
|
||||
export interface CameraManagerCapabilities extends BaseCapabilities {
|
||||
supportsPTZ: boolean;
|
||||
}
|
||||
|
||||
export interface CameraManagerCameraCapabilities extends BaseCapabilities {
|
||||
ptz?: PTZCapabilities;
|
||||
interface CapabilitySearchAllAny {
|
||||
allCapabilities?: CapabilityKey[];
|
||||
anyCapabilities?: CapabilityKey[];
|
||||
}
|
||||
export type CapabilitySearchOptions = CapabilityKey | CapabilitySearchAllAny;
|
||||
|
||||
export interface CameraManagerMediaCapabilities {
|
||||
canFavorite: boolean;
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import endOfDay from 'date-fns/endOfDay';
|
||||
import endOfHour from 'date-fns/endOfHour';
|
||||
import endOfMinute from 'date-fns/endOfMinute';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import startOfHour from 'date-fns/startOfHour';
|
||||
import orderBy from 'lodash-es/orderBy';
|
||||
import uniqBy from 'lodash-es/uniqBy';
|
||||
import { CameraConfig } from '../config/types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { DateRange } from './range';
|
||||
import { CameraEndpoint } from './types';
|
||||
|
||||
export const convertRangeToCacheFriendlyTimes = (
|
||||
range: DateRange,
|
||||
options?: {
|
||||
endCap?: boolean;
|
||||
},
|
||||
): DateRange => {
|
||||
const widthSeconds = (range.end.getTime() - range.start.getTime()) / 1000;
|
||||
let cacheableStart: Date;
|
||||
let cacheableEnd: Date;
|
||||
|
||||
if (widthSeconds <= 60 * 60) {
|
||||
cacheableStart = startOfHour(range.start);
|
||||
cacheableEnd = endOfHour(range.end);
|
||||
} else {
|
||||
cacheableStart = startOfDay(range.start);
|
||||
cacheableEnd = endOfDay(range.end);
|
||||
}
|
||||
|
||||
if (options?.endCap) {
|
||||
cacheableEnd = endOfMinute(capEndDate(cacheableEnd));
|
||||
}
|
||||
|
||||
return {
|
||||
start: cacheableStart,
|
||||
end: cacheableEnd,
|
||||
};
|
||||
};
|
||||
|
||||
export const capEndDate = (end: Date): Date => {
|
||||
const now = new Date();
|
||||
return end > now ? now : end;
|
||||
};
|
||||
|
||||
export const sortMedia = (mediaArray: ViewMedia[]): ViewMedia[] => {
|
||||
return orderBy(
|
||||
// Ensure uniqueness by the ID (if specified), otherwise all elements
|
||||
// are assumed to be unique.
|
||||
uniqBy(mediaArray, (media) => media.getID() ?? media),
|
||||
|
||||
// Sort all items leading oldest -> youngest (so media is loaded in this
|
||||
// order in the viewer which matches the left-to-right timeline order).
|
||||
(media) => media.getStartTime() ?? media.getID(),
|
||||
'asc',
|
||||
);
|
||||
};
|
||||
|
||||
export const getCameraEntityFromConfig = (cameraConfig: CameraConfig): string | null => {
|
||||
return cameraConfig.camera_entity ?? cameraConfig.webrtc_card?.entity ?? null;
|
||||
};
|
||||
|
||||
export const getDefaultGo2RTCEndpoint = (
|
||||
cameraConfig: CameraConfig,
|
||||
options?: {
|
||||
url?: string;
|
||||
stream?: string;
|
||||
},
|
||||
): CameraEndpoint | null => {
|
||||
const url = options?.url ?? cameraConfig.go2rtc?.url;
|
||||
const stream = options?.stream ?? cameraConfig.go2rtc?.stream;
|
||||
|
||||
if (!url || !stream) {
|
||||
return null;
|
||||
}
|
||||
const endpoint = `${url}/api/ws?src=${stream}`;
|
||||
|
||||
return {
|
||||
endpoint: endpoint,
|
||||
|
||||
// Only sign the endpoint if it's local to HA.
|
||||
sign: endpoint.startsWith('/'),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CameraConfig } from '../../config/types';
|
||||
|
||||
export const getCameraEntityFromConfig = (cameraConfig: CameraConfig): string | null => {
|
||||
return cameraConfig.camera_entity ?? cameraConfig.webrtc_card?.entity ?? null;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export const capEndDate = (end: Date): Date => {
|
||||
const now = new Date();
|
||||
return end > now ? now : end;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CameraConfig } from '../../config/types';
|
||||
import { CameraEndpoint } from '../types';
|
||||
|
||||
export const getDefaultGo2RTCEndpoint = (
|
||||
cameraConfig: CameraConfig,
|
||||
options?: {
|
||||
url?: string;
|
||||
stream?: string;
|
||||
},
|
||||
): CameraEndpoint | null => {
|
||||
const url = options?.url ?? cameraConfig.go2rtc?.url;
|
||||
const stream = options?.stream ?? cameraConfig.go2rtc?.stream;
|
||||
|
||||
if (!url || !stream) {
|
||||
return null;
|
||||
}
|
||||
const endpoint = `${url}/api/ws?src=${stream}`;
|
||||
|
||||
return {
|
||||
endpoint: endpoint,
|
||||
|
||||
// Only sign the endpoint if it's local to HA.
|
||||
sign: endpoint.startsWith('/'),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import endOfDay from 'date-fns/endOfDay';
|
||||
import endOfHour from 'date-fns/endOfHour';
|
||||
import endOfMinute from 'date-fns/endOfMinute';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import startOfHour from 'date-fns/startOfHour';
|
||||
import { DateRange } from '../range';
|
||||
import { capEndDate } from './cap-end-date';
|
||||
|
||||
export const convertRangeToCacheFriendlyTimes = (
|
||||
range: DateRange,
|
||||
options?: {
|
||||
endCap?: boolean;
|
||||
},
|
||||
): DateRange => {
|
||||
const widthSeconds = (range.end.getTime() - range.start.getTime()) / 1000;
|
||||
let cacheableStart: Date;
|
||||
let cacheableEnd: Date;
|
||||
|
||||
if (widthSeconds <= 60 * 60) {
|
||||
cacheableStart = startOfHour(range.start);
|
||||
cacheableEnd = endOfHour(range.end);
|
||||
} else {
|
||||
cacheableStart = startOfDay(range.start);
|
||||
cacheableEnd = endOfDay(range.end);
|
||||
}
|
||||
|
||||
if (options?.endCap) {
|
||||
cacheableEnd = endOfMinute(capEndDate(cacheableEnd));
|
||||
}
|
||||
|
||||
return {
|
||||
start: cacheableStart,
|
||||
end: cacheableEnd,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import orderBy from 'lodash-es/orderBy';
|
||||
import uniqBy from 'lodash-es/uniqBy';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
|
||||
export const sortMedia = (mediaArray: ViewMedia[]): ViewMedia[] => {
|
||||
return orderBy(
|
||||
// Ensure uniqueness by the ID (if specified), otherwise all elements
|
||||
// are assumed to be unique.
|
||||
uniqBy(mediaArray, (media) => media.getID() ?? media),
|
||||
|
||||
// Sort all items leading oldest -> youngest (so media is loaded in this
|
||||
// order in the viewer which matches the left-to-right timeline order).
|
||||
(media) => media.getStartTime() ?? media.getID(),
|
||||
'asc',
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user