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',
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { ViewContext } from 'view';
|
||||
import {
|
||||
FRIGATE_CARD_VIEW_DEFAULT,
|
||||
@@ -5,18 +6,21 @@ import {
|
||||
FrigateCardView,
|
||||
ViewDisplayMode,
|
||||
} from '../config/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import { log } from '../utils/debug';
|
||||
import { executeMediaQueryForView } from '../utils/media-to-view';
|
||||
import { View } from '../view/view';
|
||||
import { getCameraIDsForViewName } from '../view/view-to-cameras';
|
||||
import { CardViewAPI } from './types';
|
||||
|
||||
interface ViewManagerSetViewDefaultParameters {
|
||||
cameraID?: string;
|
||||
substream?: string;
|
||||
|
||||
// When failSafe is true, the view will be changed to an "always-works" view
|
||||
// (e.g. `live`) if the proposed view is unsupported. By default the view will
|
||||
// just not be changed.
|
||||
// When failSafe is true, the view will be changed to the default view, or the
|
||||
// `live` view if the default view is not supported, or failing that an error
|
||||
// message is shown. Without `failSafe` the view will just not be changed if
|
||||
// unsupported.
|
||||
failSafe?: boolean;
|
||||
}
|
||||
|
||||
@@ -49,9 +53,11 @@ export class ViewManager {
|
||||
const config = this._api.getConfigManager().getConfig();
|
||||
if (config) {
|
||||
let forceCameraID: string | null = params?.cameraID ?? null;
|
||||
const viewName = config.view.default;
|
||||
|
||||
if (!forceCameraID && this._view?.camera && config.view.update_cycle_camera) {
|
||||
const cameraIDs = [
|
||||
...this._api.getCameraManager().getStore().getVisibleCameraIDs(),
|
||||
...getCameraIDsForViewName(this._api.getCameraManager(), viewName),
|
||||
];
|
||||
const currentIndex = cameraIDs.indexOf(this._view.camera);
|
||||
const targetIndex = currentIndex + 1 >= cameraIDs.length ? 0 : currentIndex + 1;
|
||||
@@ -60,7 +66,7 @@ export class ViewManager {
|
||||
|
||||
this.setViewByParameters({
|
||||
...params,
|
||||
viewName: config.view.default,
|
||||
viewName: viewName,
|
||||
...(forceCameraID && { cameraID: forceCameraID }),
|
||||
});
|
||||
|
||||
@@ -76,38 +82,82 @@ export class ViewManager {
|
||||
if (config) {
|
||||
let cameraID: string | null = null;
|
||||
|
||||
const cameras = this._api.getCameraManager().getStore().getVisibleCameraIDs();
|
||||
if (cameras.size) {
|
||||
if (params?.cameraID && cameras.has(params.cameraID)) {
|
||||
cameraID = params.cameraID;
|
||||
} else {
|
||||
// Reset to the default camera.
|
||||
cameraID = cameras.keys().next().value;
|
||||
}
|
||||
}
|
||||
let viewName = params?.viewName ?? this._view?.view ?? config.view.default;
|
||||
if (cameraID && viewName) {
|
||||
if (!this.isViewSupportedByCamera(cameraID, viewName)) {
|
||||
if (params.failSafe) {
|
||||
const allCameraIDs = this._api.getCameraManager().getStore().getCameraIDs();
|
||||
if (params?.cameraID && allCameraIDs.has(params.cameraID)) {
|
||||
cameraID = params.cameraID;
|
||||
} else {
|
||||
const viewCameraIDs = getCameraIDsForViewName(
|
||||
this._api.getCameraManager(),
|
||||
viewName,
|
||||
);
|
||||
|
||||
// Reset to the default camera.
|
||||
cameraID = viewCameraIDs.keys().next().value;
|
||||
}
|
||||
|
||||
if (!cameraID) {
|
||||
if (params.failSafe) {
|
||||
const camerasToCapabilities = [
|
||||
...this._api.getCameraManager().getStore().getCameras(),
|
||||
].reduce((acc, [cameraID, camera]) => {
|
||||
const capabilities = camera.getCapabilities()?.getRawCapabilities();
|
||||
if (capabilities) {
|
||||
acc[cameraID] = capabilities;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this._api.getMessageManager().setMessageIfHigherPriority({
|
||||
type: 'error',
|
||||
message: localize('error.no_supported_cameras'),
|
||||
context: {
|
||||
view: viewName,
|
||||
cameras_capabilities: camerasToCapabilities,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isViewSupportedByCamera(cameraID, viewName)) {
|
||||
if (params.failSafe) {
|
||||
if (this.isViewSupportedByCamera(cameraID, FRIGATE_CARD_VIEW_DEFAULT)) {
|
||||
viewName = FRIGATE_CARD_VIEW_DEFAULT;
|
||||
} else {
|
||||
const capabilities = this._api
|
||||
.getCameraManager()
|
||||
.getStore()
|
||||
.getCamera(cameraID)
|
||||
?.getCapabilities()
|
||||
?.getRawCapabilities();
|
||||
this._api.getMessageManager().setMessageIfHigherPriority({
|
||||
type: 'error',
|
||||
message: localize('error.no_supported_camera'),
|
||||
context: {
|
||||
view: viewName,
|
||||
camera: cameraID,
|
||||
...(capabilities && { camera_capabilities: capabilities }),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const displayMode =
|
||||
this._view?.displayMode ??
|
||||
this._getDefaultDisplayModeForView(viewName, config);
|
||||
let view: View = new View({
|
||||
view: viewName,
|
||||
camera: cameraID,
|
||||
displayMode: displayMode,
|
||||
});
|
||||
if (params.substream) {
|
||||
view = this._createViewWithSelectedSubstream(view, params.substream);
|
||||
}
|
||||
this._setView(view);
|
||||
}
|
||||
|
||||
const displayMode =
|
||||
this._view?.displayMode ?? this._getDefaultDisplayModeForView(viewName, config);
|
||||
let view: View = new View({
|
||||
view: viewName,
|
||||
camera: cameraID,
|
||||
displayMode: displayMode,
|
||||
});
|
||||
if (params.substream) {
|
||||
view = this._createViewWithSelectedSubstream(view, params.substream);
|
||||
}
|
||||
this._setView(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +171,12 @@ export class ViewManager {
|
||||
this._view = null;
|
||||
}
|
||||
|
||||
protected _getCameraIDsInvolvedInView(view: View): Set<string> {
|
||||
return view.supportsMultipleDisplayModes() && view.isGrid()
|
||||
? getCameraIDsForViewName(this._api.getCameraManager(), view.view)
|
||||
: getCameraIDsForViewName(this._api.getCameraManager(), view.view, view.camera);
|
||||
}
|
||||
|
||||
public async setViewWithNewDisplayMode(displayMode: ViewDisplayMode): Promise<void> {
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
|
||||
@@ -129,18 +185,10 @@ export class ViewManager {
|
||||
displayMode: displayMode,
|
||||
});
|
||||
|
||||
const cameraCount = this._api
|
||||
.getCameraManager()
|
||||
.getStore()
|
||||
.getVisibleCameraCount();
|
||||
const queryCameraCount = view.query?.getQueryCameraIDs()?.size ?? 0;
|
||||
const generateNewQuery =
|
||||
view?.query &&
|
||||
queryCameraCount &&
|
||||
((view.isGrid() && queryCameraCount < cameraCount) ||
|
||||
(!view.isGrid() && queryCameraCount > 1));
|
||||
const expectedCameraIDs = this._getCameraIDsInvolvedInView(view);
|
||||
const queryCameraIDs = view.query?.getQueryCameraIDs();
|
||||
|
||||
if (generateNewQuery && view && view.query) {
|
||||
if (!isEqual(expectedCameraIDs, queryCameraIDs) && view && view.query) {
|
||||
// If the user requests a grid but the current query does not have a
|
||||
// query for more than one camera, reset the query results, change the
|
||||
// existing query to refer to all cameras and execute it to fetch new
|
||||
@@ -150,13 +198,7 @@ export class ViewManager {
|
||||
viewWithNewQuery = await executeMediaQueryForView(
|
||||
this._api.getCameraManager(),
|
||||
view,
|
||||
view.query
|
||||
.clone()
|
||||
.setQueryCameraIDs(
|
||||
view.isGrid()
|
||||
? this._api.getCameraManager().getStore().getVisibleCameraIDs()
|
||||
: view.camera,
|
||||
),
|
||||
view.query.clone().setQueryCameraIDs(expectedCameraIDs),
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
this._api.getMessageManager().setErrorIfHigherPriority(e);
|
||||
@@ -190,42 +232,7 @@ export class ViewManager {
|
||||
}
|
||||
|
||||
public isViewSupportedByCamera(cameraID: string, view: FrigateCardView): boolean {
|
||||
const dependentCamerasCapabilities = this._api
|
||||
.getCameraManager()
|
||||
.getAggregateCameraCapabilities(
|
||||
this._api.getCameraManager().getStore().getAllDependentCameras(cameraID),
|
||||
);
|
||||
const allCamerasCapabilities = this._api
|
||||
.getCameraManager()
|
||||
.getAggregateCameraCapabilities(
|
||||
this._api.getCameraManager().getStore().getCameraIDs(),
|
||||
);
|
||||
|
||||
switch (view) {
|
||||
case 'live':
|
||||
case 'image':
|
||||
case 'diagnostics':
|
||||
return true;
|
||||
case 'clip':
|
||||
case 'clips':
|
||||
return !!dependentCamerasCapabilities?.supportsClips;
|
||||
case 'snapshot':
|
||||
case 'snapshots':
|
||||
return !!dependentCamerasCapabilities?.supportsSnapshots;
|
||||
case 'recording':
|
||||
case 'recordings':
|
||||
return !!dependentCamerasCapabilities?.supportsRecordings;
|
||||
case 'timeline':
|
||||
// Show the timeline if any camera supports it, even cameras unrelated
|
||||
// to the currently selected camera.
|
||||
return !!allCamerasCapabilities?.supportsTimeline;
|
||||
case 'media':
|
||||
return (
|
||||
!!dependentCamerasCapabilities?.supportsClips ||
|
||||
!!dependentCamerasCapabilities?.supportsSnapshots ||
|
||||
!!dependentCamerasCapabilities?.supportsRecordings
|
||||
);
|
||||
}
|
||||
return !!getCameraIDsForViewName(this._api.getCameraManager(), view, cameraID).size;
|
||||
}
|
||||
|
||||
protected _getDefaultDisplayModeForView(
|
||||
|
||||
@@ -182,11 +182,11 @@ export class MediaFilterController {
|
||||
return null;
|
||||
};
|
||||
|
||||
const visibleCameraIDs = cameraManager.getStore().getVisibleCameraIDs();
|
||||
if (!visibleCameraIDs.size || !values.mediaType) {
|
||||
const cameraIDs =
|
||||
getArrayValueAsSet(values.camera) ?? this._getAllCameraIDs(cameraManager);
|
||||
if (!cameraIDs.size || !values.mediaType) {
|
||||
return;
|
||||
}
|
||||
const cameraIDs = getArrayValueAsSet(values.camera) ?? visibleCameraIDs;
|
||||
|
||||
const when = this._getWhen(values.when);
|
||||
const favorite = values.favorite
|
||||
@@ -280,10 +280,16 @@ export class MediaFilterController {
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
|
||||
protected _getAllCameraIDs(cameraManager: CameraManager): Set<string> {
|
||||
return cameraManager.getStore().getCameraIDsWithCapability({
|
||||
anyCapabilities: ['clips', 'snapshots', 'recordings'],
|
||||
});
|
||||
}
|
||||
|
||||
public computeInitialDefaultsFromView(cameraManager: CameraManager, view: View): void {
|
||||
const queries = view.query?.getQueries();
|
||||
const visibleCameraIDs = cameraManager.getStore().getVisibleCameraIDs();
|
||||
if (!queries || !visibleCameraIDs.size) {
|
||||
const allCameraIDs = this._getAllCameraIDs(cameraManager);
|
||||
if (!queries || !allCameraIDs.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -300,7 +306,7 @@ export class MediaFilterController {
|
||||
);
|
||||
// Special note: If all visible cameras are selected, this is the same as no
|
||||
// selector at all.
|
||||
if (cameraIDSets.length === 1 && !isEqual(queries[0].cameraIDs, visibleCameraIDs)) {
|
||||
if (cameraIDSets.length === 1 && !isEqual(queries[0].cameraIDs, allCameraIDs)) {
|
||||
cameraIDs = [...queries[0].cameraIDs];
|
||||
}
|
||||
|
||||
@@ -375,8 +381,7 @@ export class MediaFilterController {
|
||||
}
|
||||
|
||||
public computeCameraOptions(cameraManager: CameraManager): void {
|
||||
const cameras = cameraManager.getStore().getVisibleCameraIDs();
|
||||
this._cameraOptions = [...cameras].map((cameraID) => ({
|
||||
this._cameraOptions = [...this._getAllCameraIDs(cameraManager)].map((cameraID) => ({
|
||||
value: cameraID,
|
||||
label: cameraManager.getCameraMetadata(cameraID)?.title ?? cameraID,
|
||||
}));
|
||||
@@ -451,9 +456,9 @@ export class MediaFilterController {
|
||||
events: events,
|
||||
recordings: recordings,
|
||||
favorites: events
|
||||
? !!managerCapabilities?.canFavoriteEvents
|
||||
? managerCapabilities?.has('favorite-events')
|
||||
: recordings
|
||||
? !!managerCapabilities?.canFavoriteRecordings
|
||||
? managerCapabilities?.has('favorite-recordings')
|
||||
: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { getEntityIcon, getEntityTitle } from '../utils/ha';
|
||||
import { hasUsablePTZ } from '../utils/ptz';
|
||||
import { hasSubstream } from '../utils/substream';
|
||||
import { View } from '../view/view';
|
||||
import { getCameraIDsForViewName } from '../view/view-to-cameras';
|
||||
|
||||
export interface MenuButtonControllerOptions {
|
||||
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
||||
@@ -62,16 +63,12 @@ export class MenuButtonController {
|
||||
view: View,
|
||||
options?: MenuButtonControllerOptions,
|
||||
): MenuItem[] {
|
||||
const visibleCameraIDs = cameraManager.getStore().getVisibleCameraIDs();
|
||||
const selectedCameraID = view.camera;
|
||||
const substreamAwareCameraID =
|
||||
view.context?.live?.overrides?.get(selectedCameraID) ?? selectedCameraID;
|
||||
const selectedCameraConfig = cameraManager
|
||||
.getStore()
|
||||
.getCameraConfig(selectedCameraID);
|
||||
const allSelectedCameraIDs = cameraManager
|
||||
.getStore()
|
||||
.getAllDependentCameras(selectedCameraID);
|
||||
|
||||
const substreamAwareCameraCapabilities =
|
||||
cameraManager.getCameraCapabilities(substreamAwareCameraID);
|
||||
@@ -98,9 +95,12 @@ export class MenuButtonController {
|
||||
) as FrigateCardCustomAction,
|
||||
});
|
||||
|
||||
if (visibleCameraIDs.size) {
|
||||
// Show all cameras in the menu rather than just cameras that support the
|
||||
// current view for a less surprising UX.
|
||||
const menuCameraIDs = cameraManager.getStore().getCameraIDsWithCapability('menu');
|
||||
if (menuCameraIDs.size) {
|
||||
const menuItems = Array.from(
|
||||
cameraManager.getStore().getCameraConfigEntries(visibleCameraIDs),
|
||||
cameraManager.getStore().getCameraConfigEntries(menuCameraIDs),
|
||||
([cameraID, config]) => {
|
||||
const action = createFrigateCardCameraAction('camera_select', cameraID);
|
||||
const metadata = cameraManager.getCameraMetadata(cameraID);
|
||||
@@ -126,10 +126,15 @@ export class MenuButtonController {
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedCameraID && allSelectedCameraIDs && view.is('live')) {
|
||||
const dependencies = [...allSelectedCameraIDs];
|
||||
const substreamCameraIDs = cameraManager
|
||||
.getStore()
|
||||
.getAllDependentCameras(selectedCameraID, 'substream');
|
||||
|
||||
if (dependencies.length === 2) {
|
||||
if (selectedCameraID && substreamCameraIDs && view.is('live')) {
|
||||
const substreams = [...substreamCameraIDs].filter((cameraID) => cameraID !== selectedCameraID);
|
||||
const streams = [selectedCameraID, ...substreams];
|
||||
|
||||
if (streams.length === 2) {
|
||||
// If there are only two dependencies (the main camera, and 1 other)
|
||||
// then use a button not a menu to toggle.
|
||||
buttons.push({
|
||||
@@ -145,14 +150,14 @@ export class MenuButtonController {
|
||||
hasSubstream(view) ? 'live_substream_off' : 'live_substream_on',
|
||||
) as FrigateCardCustomAction,
|
||||
});
|
||||
} else if (dependencies.length > 2) {
|
||||
const menuItems = Array.from(dependencies, (cameraID) => {
|
||||
} else if (streams.length > 2) {
|
||||
const menuItems = Array.from(streams, (streamID) => {
|
||||
const action = createFrigateCardCameraAction(
|
||||
'live_substream_select',
|
||||
cameraID,
|
||||
streamID,
|
||||
);
|
||||
const metadata = cameraManager.getCameraMetadata(cameraID) ?? undefined;
|
||||
const cameraConfig = cameraManager.getStore().getCameraConfig(cameraID);
|
||||
const metadata = cameraManager.getCameraMetadata(streamID) ?? undefined;
|
||||
const cameraConfig = cameraManager.getStore().getCameraConfig(streamID);
|
||||
return {
|
||||
enabled: true,
|
||||
icon: metadata?.icon,
|
||||
@@ -161,7 +166,7 @@ export class MenuButtonController {
|
||||
title: metadata?.title,
|
||||
selected:
|
||||
(view.context?.live?.overrides?.get(selectedCameraID) ??
|
||||
selectedCameraID) === cameraID,
|
||||
selectedCameraID) === streamID,
|
||||
...(action && { tap_action: action }),
|
||||
};
|
||||
});
|
||||
@@ -409,7 +414,8 @@ export class MenuButtonController {
|
||||
});
|
||||
}
|
||||
|
||||
if (view.supportsMultipleDisplayModes() && visibleCameraIDs.size > 1) {
|
||||
const viewCameraIDs = getCameraIDsForViewName(cameraManager, view.view);
|
||||
if (view.supportsMultipleDisplayModes() && viewCameraIDs.size > 1) {
|
||||
const isGrid = view.isGrid();
|
||||
buttons.push({
|
||||
icon: isGrid ? 'mdi:grid-off' : 'mdi:grid',
|
||||
|
||||
@@ -137,7 +137,7 @@ export class PTZController {
|
||||
|
||||
const ptzCapabilities = this._cameraManager.getCameraCapabilities(
|
||||
this._cameraID,
|
||||
)?.ptz;
|
||||
)?.getPTZCapabilities();
|
||||
|
||||
const defaultActions: FrigateCardPTZActions = {};
|
||||
const panTilt = ptzCapabilities?.panTilt;
|
||||
|
||||
@@ -4,14 +4,15 @@ import { DataSet } from 'vis-data';
|
||||
import { IdType, TimelineItem, TimelineWindow } from 'vis-timeline/esnext';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import {
|
||||
compressRanges,
|
||||
ExpiringMemoryRangeSet,
|
||||
MemoryRangeSet,
|
||||
compressRanges,
|
||||
} from '../camera-manager/range';
|
||||
import { EventQuery, RecordingQuery, RecordingSegment } from '../camera-manager/types';
|
||||
import { capEndDate, convertRangeToCacheFriendlyTimes } from '../camera-manager/utils';
|
||||
import { capEndDate } from '../camera-manager/utils/cap-end-date';
|
||||
import { convertRangeToCacheFriendlyTimes } from '../camera-manager/utils/range-to-cache-friendly';
|
||||
import { ClipsOrSnapshotsOrAll } from '../types';
|
||||
import { ModifyInterface, errorToConsole } from '../utils/basic.js';
|
||||
import { errorToConsole, ModifyInterface } from '../utils/basic.js';
|
||||
import { ViewMedia } from '../view/media';
|
||||
|
||||
// Allow timeline freshness to be at least this number of seconds out of date
|
||||
|
||||
@@ -324,7 +324,7 @@ export class FrigateCardLiveGrid extends LitElement {
|
||||
}
|
||||
|
||||
protected _needsGrid(): boolean {
|
||||
const cameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||
const cameraIDs = this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
||||
return (
|
||||
!!this.view?.isGrid() &&
|
||||
!!this.view?.supportsMultipleDisplayModes() &&
|
||||
@@ -343,8 +343,8 @@ export class FrigateCardLiveGrid extends LitElement {
|
||||
if (!this.conditionsManagerEpoch || !this.nonOverriddenLiveConfig) {
|
||||
return;
|
||||
}
|
||||
const cameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||
if (!cameraIDs || !this._needsGrid()) {
|
||||
const cameraIDs = this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
||||
if (!cameraIDs?.size || !this._needsGrid()) {
|
||||
return this._renderCarousel();
|
||||
}
|
||||
return html`
|
||||
@@ -416,8 +416,8 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
}
|
||||
|
||||
protected _getSelectedCameraIndex(): number {
|
||||
const cameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||
if (!cameraIDs || !this.view || this.viewFilterCameraID) {
|
||||
const cameraIDs = this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
||||
if (!cameraIDs?.size || !this.view || this.viewFilterCameraID) {
|
||||
// If the carousel is limited to a single cameraID, the first (only)
|
||||
// element is always the selected one.
|
||||
return 0;
|
||||
@@ -481,7 +481,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
|
||||
const cameraIDs = this.viewFilterCameraID
|
||||
? new Set([this.viewFilterCameraID])
|
||||
: this.cameraManager.getStore().getVisibleCameraIDs();
|
||||
: this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
||||
|
||||
const slides: TemplateResult[] = [];
|
||||
const cameraToSlide: Record<string, number> = {};
|
||||
@@ -508,8 +508,8 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
}
|
||||
|
||||
protected _setViewHandler(ev: CustomEvent<CarouselSelected>): void {
|
||||
const cameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||
if (cameraIDs && ev.detail.index !== this._getSelectedCameraIndex()) {
|
||||
const cameraIDs = this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
||||
if (cameraIDs?.size && ev.detail.index !== this._getSelectedCameraIndex()) {
|
||||
this._setViewCameraID([...cameraIDs][ev.detail.index]);
|
||||
}
|
||||
}
|
||||
@@ -597,7 +597,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
|
||||
protected _getCameraIDsOfNeighbors(): [string | null, string | null] {
|
||||
const cameraIDs = this.cameraManager
|
||||
? [...this.cameraManager.getStore().getVisibleCameraIDs()]
|
||||
? [...this.cameraManager?.getStore().getCameraIDsWithCapability('live')]
|
||||
: [];
|
||||
if (this.viewFilterCameraID || cameraIDs.length <= 1 || !this.view || !this.hass) {
|
||||
return [null, null];
|
||||
|
||||
@@ -159,14 +159,22 @@ export class FrigateCardSurround extends LitElement {
|
||||
}
|
||||
|
||||
protected _getCameraIDsForTimeline(): Set<string> | null {
|
||||
if (!this.view) {
|
||||
if (!this.view || !this.cameraManager) {
|
||||
return null;
|
||||
}
|
||||
if (this.view?.is('live')) {
|
||||
return this.view.isGrid()
|
||||
? this.cameraManager?.getStore().getVisibleCameraIDs() ?? null
|
||||
: this.cameraManager?.getStore().getAllDependentCameras(this.view.camera) ??
|
||||
null;
|
||||
if (this.view.is('live')) {
|
||||
const capabilitySearch = {
|
||||
anyCapabilities: ['clips' as const, 'snapshots' as const, 'recordings' as const],
|
||||
};
|
||||
if (this.view.supportsMultipleDisplayModes() && this.view.isGrid()) {
|
||||
return this.cameraManager
|
||||
.getStore()
|
||||
.getCameraIDsWithCapability(capabilitySearch);
|
||||
} else {
|
||||
return this.cameraManager
|
||||
.getStore()
|
||||
.getAllDependentCameras(this.view.camera, capabilitySearch);
|
||||
}
|
||||
}
|
||||
if (this.view.isViewerView()) {
|
||||
return this.view.query?.getQueryCameraIDs() ?? null;
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { rangesOverlap } from '../camera-manager/range';
|
||||
import { MediaQuery } from '../camera-manager/types';
|
||||
import { convertRangeToCacheFriendlyTimes } from '../camera-manager/utils';
|
||||
import { convertRangeToCacheFriendlyTimes } from '../camera-manager/utils/range-to-cache-friendly';
|
||||
import {
|
||||
FrigateCardTimelineItem,
|
||||
TimelineDataSource,
|
||||
@@ -72,7 +72,6 @@ import { MediaQueriesResults } from '../view/media-queries-results';
|
||||
import { View } from '../view/view';
|
||||
import './date-picker.js';
|
||||
import { DatePickerEvent, FrigateCardDatePicker } from './date-picker.js';
|
||||
import { dispatchMessageEvent } from './message.js';
|
||||
import './thumbnail.js';
|
||||
|
||||
interface FrigateCardGroupData {
|
||||
@@ -275,18 +274,11 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
request.detail.view = this.view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
const cameraIDs = this._getTimelineCameraIDs();
|
||||
|
||||
if (!this.hass || !this.view || !this.timelineConfig || !cameraIDs) {
|
||||
if (!this.hass || !this.view || !this.timelineConfig || !this.cameraIDs?.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const capabilities = this.cameraManager?.getAggregateCameraCapabilities(cameraIDs);
|
||||
const panMode = this._getEffectivePanMode();
|
||||
|
||||
const panTitle =
|
||||
@@ -306,55 +298,43 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
? 'mdi:play-box-lock'
|
||||
: 'mdi:camera-lock';
|
||||
|
||||
return html` ${capabilities?.supportsTimeline
|
||||
? html` <div
|
||||
@frigate-card:timeline:thumbnail-data-request=${this._handleThumbnailDataRequest.bind(
|
||||
this,
|
||||
)}
|
||||
class="timeline"
|
||||
${ref(this._refTimeline)}
|
||||
>
|
||||
<div class="timeline-tools">
|
||||
${this._shouldSupportSeeking()
|
||||
? html` <ha-icon
|
||||
.icon=${panIcon}
|
||||
@click=${() => {
|
||||
this._panMode =
|
||||
panMode === 'pan'
|
||||
? 'seek'
|
||||
: panMode === 'seek'
|
||||
? 'seek-in-media'
|
||||
: panMode === 'seek-in-media'
|
||||
? 'seek-in-camera'
|
||||
: 'pan';
|
||||
}}
|
||||
aria-label="${panTitle}"
|
||||
title="${panTitle}"
|
||||
>
|
||||
</ha-icon>`
|
||||
: ''}
|
||||
<frigate-card-date-picker
|
||||
${ref(this._refDatePicker)}
|
||||
@frigate-card:date-picker:change=${(ev: CustomEvent<DatePickerEvent>) => {
|
||||
if (ev.detail.date) {
|
||||
this._timeline?.moveTo(ev.detail.date);
|
||||
}
|
||||
return html` <div
|
||||
@frigate-card:timeline:thumbnail-data-request=${this._handleThumbnailDataRequest.bind(
|
||||
this,
|
||||
)}
|
||||
class="timeline"
|
||||
${ref(this._refTimeline)}
|
||||
>
|
||||
<div class="timeline-tools">
|
||||
${this._shouldSupportSeeking()
|
||||
? html` <ha-icon
|
||||
.icon=${panIcon}
|
||||
@click=${() => {
|
||||
this._panMode =
|
||||
panMode === 'pan'
|
||||
? 'seek'
|
||||
: panMode === 'seek'
|
||||
? 'seek-in-media'
|
||||
: panMode === 'seek-in-media'
|
||||
? 'seek-in-camera'
|
||||
: 'pan';
|
||||
}}
|
||||
aria-label="${panTitle}"
|
||||
title="${panTitle}"
|
||||
>
|
||||
</frigate-card-date-picker>
|
||||
</div>
|
||||
</div>`
|
||||
: ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the keys of the cameras in scope for this timeline.
|
||||
* @returns A set of camera ids (may be empty).
|
||||
*/
|
||||
protected _getTimelineCameraIDs(): Set<string> | null {
|
||||
return (
|
||||
this.cameraIDs ?? this.cameraManager?.getStore().getVisibleCameraIDs() ?? null
|
||||
);
|
||||
</ha-icon>`
|
||||
: ''}
|
||||
<frigate-card-date-picker
|
||||
${ref(this._refDatePicker)}
|
||||
@frigate-card:date-picker:change=${(ev: CustomEvent<DatePickerEvent>) => {
|
||||
if (ev.detail.date) {
|
||||
this._timeline?.moveTo(ev.detail.date);
|
||||
}
|
||||
}}
|
||||
>
|
||||
</frigate-card-date-picker>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -555,7 +535,6 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
stopEventFromActivatingCardWideActions(properties.event);
|
||||
}
|
||||
|
||||
const timelineCameraIDs = this._getTimelineCameraIDs();
|
||||
if (
|
||||
this._ignoreClick ||
|
||||
!this.hass ||
|
||||
@@ -563,7 +542,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
!this.view ||
|
||||
!this.cameraManager ||
|
||||
!this.cardWideConfig ||
|
||||
!timelineCameraIDs ||
|
||||
!this.cameraIDs ||
|
||||
!this.cameraIDs.size ||
|
||||
!this._timelineSource ||
|
||||
!properties.what
|
||||
) {
|
||||
@@ -767,14 +747,13 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
*/
|
||||
protected _getGroups(): DataGroupCollectionType {
|
||||
const groups: FrigateCardGroupData[] = [];
|
||||
(this._getTimelineCameraIDs() ?? []).forEach((cameraID) => {
|
||||
(this.cameraIDs ?? []).forEach((cameraID: string) => {
|
||||
if (!this.hass || !this.cameraManager) {
|
||||
return;
|
||||
}
|
||||
const cameraMetadata = this.cameraManager.getCameraMetadata(cameraID);
|
||||
const cameraCapabilities = this.cameraManager.getCameraCapabilities(cameraID);
|
||||
|
||||
if (cameraMetadata && cameraCapabilities?.supportsTimeline) {
|
||||
if (cameraMetadata) {
|
||||
groups.push({
|
||||
id: cameraID,
|
||||
content: cameraMetadata.title,
|
||||
@@ -1142,11 +1121,10 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
changedProps.has('timelineConfig') ||
|
||||
changedProps.has('cameraIDs')
|
||||
) {
|
||||
const cameraIDs = this._getTimelineCameraIDs();
|
||||
if (cameraIDs && this.cameraManager && this.timelineConfig) {
|
||||
if (this.cameraIDs?.size && this.cameraManager && this.timelineConfig) {
|
||||
this._timelineSource = new TimelineDataSource(
|
||||
this.cameraManager,
|
||||
cameraIDs,
|
||||
this.cameraIDs,
|
||||
this.timelineConfig.events_media_type,
|
||||
this.timelineConfig.show_recordings,
|
||||
);
|
||||
@@ -1191,12 +1169,6 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
|
||||
const groups = this._getGroups();
|
||||
if (!groups.length) {
|
||||
if (!this.mini) {
|
||||
// Don't show an empty timeline, show a message instead.
|
||||
dispatchMessageEvent(this, localize('error.timeline_no_cameras'), 'info', {
|
||||
icon: 'mdi:chart-gantt',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ export class FrigateCardTimeline extends LitElement {
|
||||
.timelineConfig=${this.timelineConfig}
|
||||
.thumbnailConfig=${this.timelineConfig.controls.thumbnails}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cameraIDs=${this.cameraManager?.getStore().getCameraIDsWithCapability({
|
||||
anyCapabilities: ['clips', 'snapshots', 'recordings'],
|
||||
})}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
.itemClickAction=${this.timelineConfig.controls.thumbnails.mode === 'none'
|
||||
? 'play'
|
||||
|
||||
@@ -678,4 +678,10 @@ const UPGRADES = [
|
||||
typeof data === 'object' && data ? data[CONF_AUTOMATIONS] : {},
|
||||
);
|
||||
},
|
||||
upgradeArrayOfObjects(
|
||||
CONF_CAMERAS,
|
||||
upgradeMoveToWithOverrides('hide', 'capabilities', {
|
||||
transform: (val) => (val === true ? { disable_except: 'substream' } : null),
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
+11
-12
@@ -9,6 +9,7 @@ import {
|
||||
} from '@dermotduffy/custom-card-helpers';
|
||||
import { z } from 'zod';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT, MEDIA_CHUNK_SIZE_MAX } from '../const.js';
|
||||
import { capabilityKeys } from '../types.js';
|
||||
import { deepRemoveDefaults } from '../utils/zod.js';
|
||||
|
||||
// *************************************************************************
|
||||
@@ -44,6 +45,8 @@ const FRIGATE_CARD_VIEWS = [
|
||||
] as const;
|
||||
|
||||
export type FrigateCardView = (typeof FRIGATE_CARD_VIEWS)[number];
|
||||
|
||||
// The default view (may not be supported on all cameras).
|
||||
export const FRIGATE_CARD_VIEW_DEFAULT = 'live' as const;
|
||||
|
||||
export const MEDIA_ACTION_NEGATIVE_CONDITIONS = ['unselected', 'hidden'] as const;
|
||||
@@ -730,12 +733,7 @@ const timelineCoreConfigDefault = {
|
||||
pan_mode: 'pan' as const,
|
||||
};
|
||||
|
||||
export const timelinePanModeSchema = z.enum([
|
||||
'pan',
|
||||
'seek',
|
||||
'seek-in-media',
|
||||
'seek-in-camera',
|
||||
]);
|
||||
const timelinePanModeSchema = z.enum(['pan', 'seek', 'seek-in-media', 'seek-in-camera']);
|
||||
export type TimelinePanMode = z.infer<typeof timelinePanModeSchema>;
|
||||
|
||||
const timelineCoreConfigSchema = z.object({
|
||||
@@ -757,9 +755,7 @@ const timelineCoreConfigSchema = z.object({
|
||||
.optional()
|
||||
.default(timelineCoreConfigDefault.show_recordings),
|
||||
style: z.enum(['stack', 'ribbon']).optional().default(timelineCoreConfigDefault.style),
|
||||
pan_mode: timelinePanModeSchema
|
||||
.optional()
|
||||
.default(timelineCoreConfigDefault.pan_mode),
|
||||
pan_mode: timelinePanModeSchema.optional().default(timelineCoreConfigDefault.pan_mode),
|
||||
});
|
||||
export type TimelineCoreConfig = z.infer<typeof timelineCoreConfigSchema>;
|
||||
|
||||
@@ -1119,7 +1115,6 @@ const cameraConfigDefault = {
|
||||
frigate: {
|
||||
client_id: 'frigate' as const,
|
||||
},
|
||||
hide: false,
|
||||
image: {
|
||||
refresh_seconds: 1,
|
||||
},
|
||||
@@ -1151,8 +1146,12 @@ export const cameraConfigSchema = z
|
||||
icon: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
|
||||
// Used to hide the camera (e.g. when used only as a dependency).
|
||||
hide: z.boolean().optional(),
|
||||
capabilities: z
|
||||
.object({
|
||||
disable: z.enum(capabilityKeys).array().optional(),
|
||||
disable_except: z.enum(capabilityKeys).array().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
// Optional identifier to separate different camera configurations used in
|
||||
// this card.
|
||||
|
||||
+4
-1
@@ -8,6 +8,10 @@ export const CONF_CAMERAS_ARRAY_CAMERA_ENTITY =
|
||||
`${CONF_CAMERAS}.#.camera_entity` as const;
|
||||
export const CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME =
|
||||
`${CONF_CAMERAS}.#.frigate.camera_name` as const;
|
||||
export const CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE =
|
||||
`${CONF_CAMERAS}.#.capabilities.disable` as const;
|
||||
export const CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT =
|
||||
`${CONF_CAMERAS}.#.capabilities.disable_except` as const;
|
||||
export const CONF_CAMERAS_ARRAY_CAST_METHOD = `${CONF_CAMERAS}.#.cast.method` as const;
|
||||
export const CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH =
|
||||
`${CONF_CAMERAS}.#.cast.dashboard.dashboard_path` as const;
|
||||
@@ -25,7 +29,6 @@ export const CONF_CAMERAS_ARRAY_FRIGATE_ZONES =
|
||||
export const CONF_CAMERAS_ARRAY_GO2RTC_MODES = `${CONF_CAMERAS}.#.go2rtc.modes` as const;
|
||||
export const CONF_CAMERAS_ARRAY_GO2RTC_STREAM =
|
||||
`${CONF_CAMERAS}.#.go2rtc.stream` as const;
|
||||
export const CONF_CAMERAS_ARRAY_HIDE = `${CONF_CAMERAS}.#.hide` as const;
|
||||
export const CONF_CAMERAS_ARRAY_ICON = `${CONF_CAMERAS}.#.icon` as const;
|
||||
export const CONF_CAMERAS_ARRAY_ID = `${CONF_CAMERAS}.#.id` as const;
|
||||
export const CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS =
|
||||
|
||||
+81
-11
@@ -31,8 +31,9 @@ import {
|
||||
THUMBNAIL_WIDTH_MIN,
|
||||
} from './config/types.js';
|
||||
import {
|
||||
CONF_CAMERAS,
|
||||
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
|
||||
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT,
|
||||
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE,
|
||||
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH,
|
||||
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH,
|
||||
CONF_CAMERAS_ARRAY_CAST_METHOD,
|
||||
@@ -49,7 +50,6 @@ import {
|
||||
CONF_CAMERAS_ARRAY_FRIGATE_ZONES,
|
||||
CONF_CAMERAS_ARRAY_GO2RTC_MODES,
|
||||
CONF_CAMERAS_ARRAY_GO2RTC_STREAM,
|
||||
CONF_CAMERAS_ARRAY_HIDE,
|
||||
CONF_CAMERAS_ARRAY_ICON,
|
||||
CONF_CAMERAS_ARRAY_ID,
|
||||
CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS,
|
||||
@@ -67,8 +67,9 @@ import {
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY,
|
||||
CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY,
|
||||
CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL,
|
||||
CONF_DIMENSIONS_ASPECT_RATIO,
|
||||
CONF_CAMERAS,
|
||||
CONF_DIMENSIONS_ASPECT_RATIO_MODE,
|
||||
CONF_DIMENSIONS_ASPECT_RATIO,
|
||||
CONF_DIMENSIONS_MAX_HEIGHT,
|
||||
CONF_DIMENSIONS_MIN_HEIGHT,
|
||||
CONF_IMAGE_MODE,
|
||||
@@ -91,7 +92,6 @@ import {
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_EVENTS_MEDIA_TYPE,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA_TYPE,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
|
||||
@@ -100,6 +100,7 @@ import {
|
||||
CONF_LIVE_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_MODE,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_STYLE,
|
||||
CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS,
|
||||
@@ -157,8 +158,8 @@ import {
|
||||
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
|
||||
CONF_MEDIA_VIEWER_ZOOMABLE,
|
||||
CONF_MENU_ALIGNMENT,
|
||||
CONF_MENU_BUTTONS,
|
||||
CONF_MENU_BUTTON_SIZE,
|
||||
CONF_MENU_BUTTONS,
|
||||
CONF_MENU_POSITION,
|
||||
CONF_MENU_STYLE,
|
||||
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
|
||||
@@ -182,14 +183,14 @@ import {
|
||||
CONF_VIEW_DEFAULT,
|
||||
CONF_VIEW_INTERACTION_SECONDS,
|
||||
CONF_VIEW_RESET_AFTER_INTERACTION,
|
||||
CONF_VIEW_TRIGGERS,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_INTERACTION_MODE,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS,
|
||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||
CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS,
|
||||
CONF_VIEW_TRIGGERS_UNTRIGGER_SECONDS,
|
||||
CONF_VIEW_TRIGGERS,
|
||||
CONF_VIEW_UPDATE_CYCLE_CAMERA,
|
||||
CONF_VIEW_UPDATE_FORCE,
|
||||
CONF_VIEW_UPDATE_SECONDS,
|
||||
@@ -208,6 +209,7 @@ import {
|
||||
|
||||
const MENU_BUTTONS = 'buttons';
|
||||
const MENU_CAMERAS = 'cameras';
|
||||
const MENU_CAMERAS_CAPABILITIES = 'cameras.capabilities';
|
||||
const MENU_CAMERAS_CAST = 'cameras.cast';
|
||||
const MENU_CAMERAS_DEPENDENCIES = 'cameras.dependencies';
|
||||
const MENU_CAMERAS_DIMENSIONS = 'cameras.dimensions';
|
||||
@@ -741,6 +743,50 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
},
|
||||
];
|
||||
|
||||
protected _capabilities: EditorSelectOption[] = [
|
||||
{ value: '', label: '' },
|
||||
{
|
||||
value: 'live',
|
||||
label: localize('config.cameras.capabilities.capabilities.live'),
|
||||
},
|
||||
{
|
||||
value: 'substream',
|
||||
label: localize('config.cameras.capabilities.capabilities.substream'),
|
||||
},
|
||||
{
|
||||
value: 'clips',
|
||||
label: localize('config.cameras.capabilities.capabilities.clips'),
|
||||
},
|
||||
{
|
||||
value: 'recordings',
|
||||
label: localize('config.cameras.capabilities.capabilities.recordings'),
|
||||
},
|
||||
{
|
||||
value: 'snapshots',
|
||||
label: localize('config.cameras.capabilities.capabilities.snapshots'),
|
||||
},
|
||||
{
|
||||
value: 'favorite-events',
|
||||
label: localize('config.cameras.capabilities.capabilities.favorite-events'),
|
||||
},
|
||||
{
|
||||
value: 'favorite-recordings',
|
||||
label: localize('config.cameras.capabilities.capabilities.favorite-recordings'),
|
||||
},
|
||||
{
|
||||
value: 'seek',
|
||||
label: localize('config.cameras.capabilities.capabilities.seek'),
|
||||
},
|
||||
{
|
||||
value: 'ptz',
|
||||
label: localize('config.cameras.capabilities.capabilities.ptz'),
|
||||
},
|
||||
{
|
||||
value: 'menu',
|
||||
label: localize('config.cameras.capabilities.capabilities.menu'),
|
||||
},
|
||||
];
|
||||
|
||||
public setConfig(config: RawFrigateCardConfig): void {
|
||||
// Note: This does not use Zod to parse the configuration, so it may be
|
||||
// partially or completely invalid. It's more useful to have a partially
|
||||
@@ -1688,10 +1734,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
${this._renderStringInput(
|
||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_ID, cameraIndex),
|
||||
)}
|
||||
${this._renderSwitch(
|
||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_HIDE, cameraIndex),
|
||||
this._defaults.cameras.hide,
|
||||
)}
|
||||
${this._putInSubmenu(
|
||||
MENU_CAMERAS_ENGINE,
|
||||
true,
|
||||
@@ -1933,6 +1975,34 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
)}
|
||||
`,
|
||||
)}
|
||||
${this._putInSubmenu(
|
||||
MENU_CAMERAS_CAPABILITIES,
|
||||
cameraIndex,
|
||||
'config.cameras.capabilities.editor_label',
|
||||
{ name: 'mdi:cog-stop' },
|
||||
html`
|
||||
${this._renderOptionSelector(
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE,
|
||||
cameraIndex,
|
||||
),
|
||||
this._capabilities,
|
||||
{
|
||||
multiple: true,
|
||||
},
|
||||
)}
|
||||
${this._renderOptionSelector(
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT,
|
||||
cameraIndex,
|
||||
),
|
||||
this._capabilities,
|
||||
{
|
||||
multiple: true,
|
||||
},
|
||||
)}
|
||||
`,
|
||||
)}
|
||||
</div>`
|
||||
: ``}
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,23 @@
|
||||
"config": {
|
||||
"cameras": {
|
||||
"camera_entity": "Camera Entity",
|
||||
"capabilities": {
|
||||
"disable": "Disable",
|
||||
"disable_except": "Disable except",
|
||||
"editor_label": "Camera capabilities",
|
||||
"capabilities": {
|
||||
"clips": "Clips",
|
||||
"favorite-events": "Favorite Events",
|
||||
"favorite-recordings": "Favorite Recordings",
|
||||
"live": "Live",
|
||||
"menu": "Menu",
|
||||
"ptz": "PTZ",
|
||||
"recordings": "Recordings",
|
||||
"seek": "Seeking",
|
||||
"snapshots": "Snapshots",
|
||||
"substream": "Substream"
|
||||
}
|
||||
},
|
||||
"cast": {
|
||||
"dashboard": {
|
||||
"dashboard_path": "Dashboard path",
|
||||
@@ -50,7 +67,7 @@
|
||||
"frigate": {
|
||||
"camera_name": "Frigate camera name (Autodetected from entity)",
|
||||
"client_id": "Frigate client id (For >1 Frigate server)",
|
||||
"editor_label": "Frigate Options",
|
||||
"editor_label": "Frigate options",
|
||||
"labels": "Frigate labels/object filters",
|
||||
"url": "Frigate server URL",
|
||||
"zones": "Frigate zones"
|
||||
@@ -87,7 +104,7 @@
|
||||
"webrtc-card": "WebRTC Card (i.e. AlexxIT's WebRTC Card)"
|
||||
},
|
||||
"motioneye": {
|
||||
"editor_label": "MotionEye Options",
|
||||
"editor_label": "MotionEye options",
|
||||
"images": {
|
||||
"directory_pattern": "Images directory pattern",
|
||||
"file_pattern": "Images file pattern"
|
||||
@@ -527,9 +544,9 @@
|
||||
"no_camera_name": "Could not determine a Frigate camera name for camera (or one of its dependents), please specify either 'camera_entity' or 'camera_name'",
|
||||
"no_dashboard_or_view": "Both 'dashboard_path' and 'view_path' parameters are required for the 'dashboard' cast method",
|
||||
"no_live_camera": "The camera_entity parameter must be set and valid for this live provider",
|
||||
"no_visible_cameras": "No visible cameras found, you must configure at least one non-hidden camera",
|
||||
"no_supported_camera": "The selected camera does not support this view",
|
||||
"no_supported_cameras": "No cameras support this view",
|
||||
"reconnecting": "Reconnecting",
|
||||
"timeline_no_cameras": "No Frigate cameras to show in timeline",
|
||||
"too_many_automations": "Too many nested automation calls, please check your configuration for loops",
|
||||
"troubleshooting": "Check troubleshooting",
|
||||
"unknown": "Unknown error",
|
||||
|
||||
@@ -10,6 +10,23 @@
|
||||
"config": {
|
||||
"cameras": {
|
||||
"camera_entity": "Entità della telecamera",
|
||||
"capabilities": {
|
||||
"disable": "",
|
||||
"disable_except": "",
|
||||
"editor_label": "",
|
||||
"capabilities": {
|
||||
"clips": "",
|
||||
"favorite-events": "",
|
||||
"favorite-recordings": "",
|
||||
"live": "",
|
||||
"menu": "",
|
||||
"ptz": "",
|
||||
"recordings": "",
|
||||
"seek": "",
|
||||
"snapshots": "",
|
||||
"substream": ""
|
||||
}
|
||||
},
|
||||
"cast": {
|
||||
"dashboard": {
|
||||
"dashboard_path": "",
|
||||
@@ -518,7 +535,8 @@
|
||||
"no_camera_name": "Impossibile determinare un nome della telecamera in Frigate, si prega di specificare 'camera_enty' o 'camera_name'",
|
||||
"no_dashboard_or_view": "",
|
||||
"no_live_camera": "Il parametro fotocamera_enty deve essere impostato e valido per questo provider live",
|
||||
"no_visible_cameras": "Nessuna telecamera visibile trovata, è necessario configurare almeno una telecamera non nascosta",
|
||||
"no_supported_camera": "",
|
||||
"no_supported_cameras": "",
|
||||
"reconnecting": "Riconnessione",
|
||||
"timeline_no_cameras": "Nessuna telecamera damostrare in Frigate nella timeline",
|
||||
"too_many_automations": "",
|
||||
|
||||
@@ -10,6 +10,23 @@
|
||||
"config": {
|
||||
"cameras": {
|
||||
"camera_entity": "Entidade da Câmera",
|
||||
"capabilities": {
|
||||
"disable": "",
|
||||
"disable_except": "",
|
||||
"editor_label": "",
|
||||
"capabilities": {
|
||||
"clips": "",
|
||||
"favorite-events": "",
|
||||
"favorite-recordings": "",
|
||||
"live": "",
|
||||
"menu": "",
|
||||
"ptz": "",
|
||||
"recordings": "",
|
||||
"seek": "",
|
||||
"snapshots": "",
|
||||
"substream": ""
|
||||
}
|
||||
},
|
||||
"cast": {
|
||||
"dashboard": {
|
||||
"dashboard_path": "",
|
||||
@@ -525,7 +542,8 @@
|
||||
"no_camera_name": "Não foi possível determinar o nome da câmera da Frigate, especifique 'camera_entity' ou 'camera_name' para a câmera a seguir",
|
||||
"no_dashboard_or_view": "",
|
||||
"no_live_camera": "O parâmetro camera_entity deve ser definido e válido para este provedor ativo",
|
||||
"no_visible_cameras": "Nenhuma câmera visível encontrada, você deve configurar pelo menos uma câmera não oculta",
|
||||
"no_supported_camera": "",
|
||||
"no_supported_cameras": "",
|
||||
"reconnecting": "Reconectando",
|
||||
"timeline_no_cameras": "Nenhuma câmera do Frigate para mostrar na linha do tempo",
|
||||
"too_many_automations": "",
|
||||
|
||||
@@ -10,6 +10,23 @@
|
||||
"config": {
|
||||
"cameras": {
|
||||
"camera_entity": "Entidade da Câmera",
|
||||
"capabilities": {
|
||||
"disable": "",
|
||||
"disable_except": "",
|
||||
"editor_label": "",
|
||||
"capabilities": {
|
||||
"clips": "",
|
||||
"favorite-events": "",
|
||||
"favorite-recordings": "",
|
||||
"live": "",
|
||||
"menu": "",
|
||||
"ptz": "",
|
||||
"recordings": "",
|
||||
"seek": "",
|
||||
"snapshots": "",
|
||||
"substream": ""
|
||||
}
|
||||
},
|
||||
"cast": {
|
||||
"dashboard": {
|
||||
"dashboard_path": "",
|
||||
@@ -511,7 +528,8 @@
|
||||
"no_camera_name": "Não foi possível determinar o nome da câmera da Frigate, especifique 'camera_entity' ou 'camera_name' para a câmera a seguir",
|
||||
"no_dashboard_or_view": "",
|
||||
"no_live_camera": "O parâmetro camera_entity deve ser definido e válido para este serviço ativo",
|
||||
"no_visible_cameras": "Sem camaras visiveis",
|
||||
"no_supported_camera": "",
|
||||
"no_supported_cameras": "",
|
||||
"reconnecting": "A voltar a ligar",
|
||||
"timeline_no_cameras": "Nenhuma câmera do Frigate para mostrar na linha do tempo",
|
||||
"too_many_automations": "",
|
||||
|
||||
+45
-1
@@ -1,4 +1,8 @@
|
||||
import { HomeAssistant, LovelaceCardConfig, Themes } from '@dermotduffy/custom-card-helpers';
|
||||
import {
|
||||
HomeAssistant,
|
||||
LovelaceCardConfig,
|
||||
Themes,
|
||||
} from '@dermotduffy/custom-card-helpers';
|
||||
import { StyleInfo } from 'lit/directives/style-map.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -87,6 +91,46 @@ export interface CardHelpers {
|
||||
}>;
|
||||
}
|
||||
|
||||
export type PTZMovementType = 'relative' | 'continuous';
|
||||
|
||||
export interface PTZCapabilities {
|
||||
panTilt?: PTZMovementType[];
|
||||
zoom?: PTZMovementType[];
|
||||
presets?: string[];
|
||||
}
|
||||
|
||||
export interface CapabilitiesRaw {
|
||||
live?: boolean;
|
||||
substream?: boolean;
|
||||
|
||||
clips?: boolean;
|
||||
recordings?: boolean;
|
||||
snapshots?: boolean;
|
||||
|
||||
'favorite-events'?: boolean;
|
||||
'favorite-recordings'?: boolean;
|
||||
|
||||
seek?: boolean;
|
||||
|
||||
ptz?: PTZCapabilities;
|
||||
|
||||
menu?: boolean;
|
||||
}
|
||||
|
||||
export type CapabilityKey = keyof CapabilitiesRaw;
|
||||
export const capabilityKeys: readonly [CapabilityKey, ...CapabilityKey[]] = [
|
||||
'clips',
|
||||
'favorite-events',
|
||||
'favorite-recordings',
|
||||
'live',
|
||||
'menu',
|
||||
'ptz',
|
||||
'recordings',
|
||||
'seek',
|
||||
'snapshots',
|
||||
'substream',
|
||||
] as const;
|
||||
|
||||
// *************************************************************************
|
||||
// Home Assistant API types.
|
||||
// *************************************************************************
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ViewContext } from 'view';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { MediaQuery } from '../camera-manager/types';
|
||||
import { CapabilitySearchOptions, MediaQuery } from '../camera-manager/types';
|
||||
import { dispatchFrigateCardErrorEvent } from '../components/message';
|
||||
import { CardWideConfig, FrigateCardView } from '../config/types';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const';
|
||||
@@ -30,9 +30,16 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
|
||||
useCache?: boolean;
|
||||
},
|
||||
): Promise<void> => {
|
||||
const capabilitySearch: CapabilitySearchOptions =
|
||||
!options?.eventsMediaType || options?.eventsMediaType === 'all'
|
||||
? {
|
||||
anyCapabilities: ['clips', 'snapshots'],
|
||||
}
|
||||
: options.eventsMediaType;
|
||||
|
||||
const cameraIDs = options?.allCameras
|
||||
? cameraManager.getStore().getVisibleCameraIDs()
|
||||
: cameraManager.getStore().getAllDependentCameras(view.camera);
|
||||
? cameraManager.getStore().getCameraIDsWithCapability(capabilitySearch)
|
||||
: cameraManager.getStore().getAllDependentCameras(view.camera, capabilitySearch);
|
||||
if (!cameraIDs.size) {
|
||||
return;
|
||||
}
|
||||
@@ -99,8 +106,8 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
|
||||
},
|
||||
): Promise<void> => {
|
||||
const cameraIDs = options?.allCameras
|
||||
? cameraManager.getStore().getVisibleCameraIDs()
|
||||
: cameraManager.getStore().getAllDependentCameras(view.camera);
|
||||
? cameraManager.getStore().getCameraIDsWithCapability('recordings')
|
||||
: cameraManager.getStore().getAllDependentCameras(view.camera, 'recordings');
|
||||
if (!cameraIDs.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
+8
-3
@@ -1,8 +1,8 @@
|
||||
import { CameraManagerCameraCapabilities } from '../camera-manager/types';
|
||||
import { Capabilities } from '../camera-manager/capabilities';
|
||||
import { FrigateCardPTZConfig, PTZ_CONTROL_ACTIONS } from '../config/types';
|
||||
|
||||
export const hasUsablePTZ = (
|
||||
capabilities: CameraManagerCameraCapabilities | null,
|
||||
capabilities: Capabilities | null,
|
||||
config: FrigateCardPTZConfig,
|
||||
): boolean => {
|
||||
for (const actionName of PTZ_CONTROL_ACTIONS) {
|
||||
@@ -10,5 +10,10 @@ export const hasUsablePTZ = (
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return !!capabilities?.ptz;
|
||||
const ptzCapabilities = capabilities?.getPTZCapabilities();
|
||||
return (
|
||||
!!ptzCapabilities?.panTilt?.length ||
|
||||
!!ptzCapabilities?.zoom?.length ||
|
||||
!!ptzCapabilities?.presets?.length
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { FrigateCardView } from '../config/types';
|
||||
|
||||
/**
|
||||
* Get cameraIDs that are relevant for a given view name based on camera
|
||||
* capability (if camera specified).
|
||||
*/
|
||||
export const getCameraIDsForViewName = (
|
||||
cameraManager: CameraManager,
|
||||
viewName: FrigateCardView,
|
||||
cameraID?: string,
|
||||
): Set<string> => {
|
||||
switch (viewName) {
|
||||
case 'image':
|
||||
case 'diagnostics':
|
||||
return cameraManager.getStore().getCameraIDs();
|
||||
|
||||
case 'live':
|
||||
case 'clip':
|
||||
case 'clips':
|
||||
case 'snapshot':
|
||||
case 'snapshots':
|
||||
case 'recording':
|
||||
case 'recordings':
|
||||
const capability =
|
||||
viewName === 'clip'
|
||||
? 'clips'
|
||||
: viewName === 'snapshot'
|
||||
? 'snapshots'
|
||||
: viewName === 'recording'
|
||||
? 'recordings'
|
||||
: viewName;
|
||||
return cameraID
|
||||
? cameraManager.getStore().getAllDependentCameras(cameraID, capability)
|
||||
: cameraManager.getStore().getCameraIDsWithCapability(capability);
|
||||
|
||||
case 'timeline':
|
||||
return cameraManager.getStore().getCameraIDsWithCapability({
|
||||
anyCapabilities: ['clips', 'snapshots', 'recordings'],
|
||||
});
|
||||
|
||||
case 'media':
|
||||
return cameraID
|
||||
? cameraManager.getStore().getAllDependentCameras(cameraID, {
|
||||
anyCapabilities: ['clips', 'snapshots', 'recordings'],
|
||||
})
|
||||
: cameraManager.getStore().getCameraIDsWithCapability({
|
||||
anyCapabilities: ['clips', 'snapshots', 'recordings'],
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user