Convert to batch queries.
This commit is contained in:
@@ -1,35 +1,32 @@
|
|||||||
import { CameraConfig } from '../types';
|
import { CameraConfig } from '../types';
|
||||||
import { RecordingSegmentsCache } from './cache';
|
import { RecordingSegmentsCache, RequestCache } from './cache';
|
||||||
import { CameraManagerEngine } from './engine';
|
import { CameraManagerEngine } from './engine';
|
||||||
import { FrigateCameraManagerEngine } from './frigate/engine-frigate';
|
import { FrigateCameraManagerEngine } from './frigate/engine-frigate';
|
||||||
import { DataQuery } from './types';
|
import { Engine } from './types';
|
||||||
|
|
||||||
|
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
|
||||||
|
|
||||||
export class CameraManagerEngineFactory {
|
export class CameraManagerEngineFactory {
|
||||||
protected _engines: Map<string, CameraManagerEngine> = new Map();
|
protected _engines: Map<Engine, CameraManagerEngine> = new Map();
|
||||||
|
|
||||||
protected _getOrCreateEngine(engineKey: string): CameraManagerEngine | null {
|
public getEngine(engine: Engine): CameraManagerEngine | null {
|
||||||
const cachedEngine = this._engines.get(engineKey);
|
const cachedEngine = this._engines.get(engine);
|
||||||
if (cachedEngine) {
|
if (cachedEngine) {
|
||||||
return cachedEngine;
|
return cachedEngine;
|
||||||
}
|
}
|
||||||
let newEngine: CameraManagerEngine | null = null;
|
let cameraManagerEngine: CameraManagerEngine | null = null;
|
||||||
switch (engineKey) {
|
switch (engine) {
|
||||||
case 'frigate':
|
case Engine.Frigate:
|
||||||
newEngine = new FrigateCameraManagerEngine(new RecordingSegmentsCache());
|
cameraManagerEngine = new FrigateCameraManagerEngine(
|
||||||
|
new RecordingSegmentsCache(),
|
||||||
|
new RequestCache(),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (newEngine) {
|
if (cameraManagerEngine) {
|
||||||
this._engines.set(engineKey, newEngine);
|
this._engines.set(engine, cameraManagerEngine);
|
||||||
}
|
}
|
||||||
return newEngine;
|
return cameraManagerEngine;
|
||||||
}
|
|
||||||
|
|
||||||
public getEngineForQuery(
|
|
||||||
cameras: Map<string, CameraConfig>,
|
|
||||||
query: DataQuery,
|
|
||||||
): CameraManagerEngine | null {
|
|
||||||
const cameraConfig = cameras.get(query.cameraID);
|
|
||||||
return cameraConfig ? this.getEngineForCamera(cameraConfig) : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEngineForCamera(cameraConfig?: CameraConfig): CameraManagerEngine | null {
|
public getEngineForCamera(cameraConfig?: CameraConfig): CameraManagerEngine | null {
|
||||||
@@ -37,10 +34,34 @@ export class CameraManagerEngineFactory {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let engineKey: string | null = null;
|
let engine: Engine | null = null;
|
||||||
if (cameraConfig.frigate.camera_name) {
|
if (cameraConfig.frigate.camera_name) {
|
||||||
engineKey = 'frigate';
|
engine = Engine.Frigate;
|
||||||
}
|
}
|
||||||
return engineKey ? this._getOrCreateEngine(engineKey) : null;
|
return engine ? this.getEngine(engine) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEnginesForCameraIDs(
|
||||||
|
cameras: Map<string, CameraConfig>,
|
||||||
|
cameraIDs: Set<string>,
|
||||||
|
): CameraManagerEngineCameraIDMap | null {
|
||||||
|
const output: CameraManagerEngineCameraIDMap = new Map();
|
||||||
|
|
||||||
|
for (const cameraID of cameraIDs) {
|
||||||
|
const cameraConfig = cameras.get(cameraID);
|
||||||
|
if (!cameraConfig) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const engine = this.getEngineForCamera(cameraConfig);
|
||||||
|
if (!engine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!output.has(engine)) {
|
||||||
|
output.set(engine, new Set());
|
||||||
|
}
|
||||||
|
output.get(engine)?.add(cameraID);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-20
@@ -1,65 +1,67 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { CameraConfig } from '../types';
|
import { CameraConfig } from '../types';
|
||||||
import { MediaQueriesResults } from "../view/media-queries-results";
|
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
import {
|
import {
|
||||||
|
DataQuery,
|
||||||
EventQuery,
|
EventQuery,
|
||||||
|
EventQueryResultsMap,
|
||||||
PartialEventQuery,
|
PartialEventQuery,
|
||||||
PartialRecordingQuery,
|
PartialRecordingQuery,
|
||||||
PartialRecordingSegmentsQuery,
|
PartialRecordingSegmentsQuery,
|
||||||
QueryReturnType,
|
QueryReturnType,
|
||||||
RecordingQuery,
|
RecordingQuery,
|
||||||
|
RecordingQueryResultsMap,
|
||||||
RecordingSegmentsQuery,
|
RecordingSegmentsQuery,
|
||||||
|
RecordingSegmentsQueryResultsMap,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { MediaQueries } from '../view/media-queries';
|
|
||||||
|
|
||||||
export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000;
|
export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000;
|
||||||
|
|
||||||
export interface CameraManagerEngine {
|
export interface CameraManagerEngine {
|
||||||
generateDefaultEventQuery(
|
generateDefaultEventQuery(
|
||||||
cameraID: string,
|
cameras: Map<string, CameraConfig>,
|
||||||
cameraConfig: CameraConfig,
|
cameraIDs: Set<string>,
|
||||||
query: PartialEventQuery,
|
query: PartialEventQuery,
|
||||||
): EventQuery | null;
|
): EventQuery[] | null;
|
||||||
|
|
||||||
generateDefaultRecordingQuery(
|
generateDefaultRecordingQuery(
|
||||||
cameraID: string,
|
cameras: Map<string, CameraConfig>,
|
||||||
cameraConfig: CameraConfig,
|
cameraIDs: Set<string>,
|
||||||
query: PartialRecordingQuery,
|
query: PartialRecordingQuery,
|
||||||
): RecordingQuery | null;
|
): RecordingQuery[] | null;
|
||||||
|
|
||||||
generateDefaultRecordingSegmentsQuery(
|
generateDefaultRecordingSegmentsQuery(
|
||||||
cameraID: string,
|
cameras: Map<string, CameraConfig>,
|
||||||
cameraConfig: CameraConfig,
|
cameraIDs: Set<string>,
|
||||||
query: PartialRecordingSegmentsQuery,
|
query: PartialRecordingSegmentsQuery,
|
||||||
): RecordingSegmentsQuery | null;
|
): RecordingSegmentsQuery[] | null;
|
||||||
|
|
||||||
getEvents(
|
getEvents(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: Map<string, CameraConfig>,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
): Promise<QueryReturnType<EventQuery> | null>;
|
): Promise<EventQueryResultsMap | null>;
|
||||||
|
|
||||||
getRecordings(
|
getRecordings(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: Map<string, CameraConfig>,
|
||||||
query: RecordingQuery,
|
query: RecordingQuery,
|
||||||
): Promise<QueryReturnType<RecordingQuery> | null>;
|
): Promise<RecordingQueryResultsMap | null>;
|
||||||
|
|
||||||
getRecordingSegments(
|
getRecordingSegments(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: Map<string, CameraConfig>,
|
||||||
query: RecordingSegmentsQuery,
|
query: RecordingSegmentsQuery,
|
||||||
): Promise<QueryReturnType<RecordingSegmentsQuery> | null>;
|
): Promise<RecordingSegmentsQueryResultsMap | null>;
|
||||||
|
|
||||||
generateMediaFromEvents(
|
generateMediaFromEvents(
|
||||||
cameraConfig: CameraConfig,
|
cameras: Map<string, CameraConfig>,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
results: QueryReturnType<EventQuery>,
|
results: QueryReturnType<EventQuery>,
|
||||||
): ViewMedia[] | null;
|
): ViewMedia[] | null;
|
||||||
|
|
||||||
generateMediaFromRecordings(
|
generateMediaFromRecordings(
|
||||||
cameraConfig: CameraConfig,
|
cameras: Map<string, CameraConfig>,
|
||||||
query: RecordingQuery,
|
query: RecordingQuery,
|
||||||
results: QueryReturnType<RecordingQuery>,
|
results: QueryReturnType<RecordingQuery>,
|
||||||
): ViewMedia[] | null;
|
): ViewMedia[] | null;
|
||||||
@@ -73,10 +75,9 @@ export interface CameraManagerEngine {
|
|||||||
favorite: boolean,
|
favorite: boolean,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
areMediaQueriesResultsFresh(
|
getQueryResultMaxAge(
|
||||||
queries: MediaQueries,
|
query: DataQuery
|
||||||
results: MediaQueriesResults,
|
): number | null;
|
||||||
): boolean;
|
|
||||||
|
|
||||||
getMediaSeekTime(
|
getMediaSeekTime(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import endOfHour from 'date-fns/endOfHour';
|
|||||||
import startOfHour from 'date-fns/startOfHour';
|
import startOfHour from 'date-fns/startOfHour';
|
||||||
import { CAMERA_BIRDSEYE } from '../../const';
|
import { CAMERA_BIRDSEYE } from '../../const';
|
||||||
import { CameraConfig, RecordingSegment } from '../../types';
|
import { CameraConfig, RecordingSegment } from '../../types';
|
||||||
import { MediaQueriesResults } from '../../view/media-queries-results';
|
|
||||||
import { MediaQueriesClassifier } from '../../view/media-queries-classifier';
|
|
||||||
import { ViewMedia } from '../../view/media';
|
import { ViewMedia } from '../../view/media';
|
||||||
import { RecordingSegmentsCache } from '../cache';
|
import { RequestCache, RecordingSegmentsCache } from '../cache';
|
||||||
import {
|
import {
|
||||||
CameraManagerEngine,
|
CameraManagerEngine,
|
||||||
CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
||||||
} from '../engine';
|
} from '../engine';
|
||||||
import { DateRange } from '../range';
|
import { DateRange } from '../range';
|
||||||
import {
|
import {
|
||||||
|
DataQuery,
|
||||||
Engine,
|
Engine,
|
||||||
EventQuery,
|
EventQuery,
|
||||||
|
EventQueryResults,
|
||||||
|
EventQueryResultsMap,
|
||||||
FrigateEventQueryResults,
|
FrigateEventQueryResults,
|
||||||
FrigateRecordingQueryResults,
|
FrigateRecordingQueryResults,
|
||||||
FrigateRecordingSegmentsQueryResults,
|
FrigateRecordingSegmentsQueryResults,
|
||||||
@@ -27,7 +28,10 @@ import {
|
|||||||
QueryReturnType,
|
QueryReturnType,
|
||||||
QueryType,
|
QueryType,
|
||||||
RecordingQuery,
|
RecordingQuery,
|
||||||
|
RecordingQueryResults,
|
||||||
|
RecordingQueryResultsMap,
|
||||||
RecordingSegmentsQuery,
|
RecordingSegmentsQuery,
|
||||||
|
RecordingSegmentsQueryResultsMap,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { FrigateRecording } from './types';
|
import { FrigateRecording } from './types';
|
||||||
import {
|
import {
|
||||||
@@ -38,7 +42,6 @@ import {
|
|||||||
NativeFrigateRecordingSegmentsQuery,
|
NativeFrigateRecordingSegmentsQuery,
|
||||||
retainEvent,
|
retainEvent,
|
||||||
} from './requests';
|
} from './requests';
|
||||||
import { MediaQueries } from '../../view/media-queries';
|
|
||||||
import orderBy from 'lodash-es/orderBy';
|
import orderBy from 'lodash-es/orderBy';
|
||||||
import throttle from 'lodash-es/throttle';
|
import throttle from 'lodash-es/throttle';
|
||||||
import { runWhenIdleIfSupported } from '../../utils/basic';
|
import { runWhenIdleIfSupported } from '../../utils/basic';
|
||||||
@@ -78,6 +81,7 @@ class FrigateQueryResultsClassifier {
|
|||||||
|
|
||||||
export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
||||||
protected _recordingSegmentsCache: RecordingSegmentsCache;
|
protected _recordingSegmentsCache: RecordingSegmentsCache;
|
||||||
|
protected _requestCache: RequestCache;
|
||||||
|
|
||||||
// Garbage collect segments at most once an hour.
|
// Garbage collect segments at most once an hour.
|
||||||
protected _throttledSegmentGarbageCollector = throttle(
|
protected _throttledSegmentGarbageCollector = throttle(
|
||||||
@@ -86,8 +90,12 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
{ leading: false, trailing: true },
|
{ leading: false, trailing: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
constructor(recordingSegmentsCache: RecordingSegmentsCache) {
|
constructor(
|
||||||
|
recordingSegmentsCache: RecordingSegmentsCache,
|
||||||
|
requestCache: RequestCache,
|
||||||
|
) {
|
||||||
this._recordingSegmentsCache = recordingSegmentsCache;
|
this._recordingSegmentsCache = recordingSegmentsCache;
|
||||||
|
this._requestCache = requestCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMediaDownloadPath(
|
public getMediaDownloadPath(
|
||||||
@@ -113,46 +121,81 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultEventQuery(
|
public generateDefaultEventQuery(
|
||||||
cameraID: string,
|
cameras: Map<string, CameraConfig>,
|
||||||
cameraConfig: CameraConfig,
|
cameraIDs: Set<string>,
|
||||||
query: PartialEventQuery,
|
query: PartialEventQuery,
|
||||||
): EventQuery | null {
|
): EventQuery[] | null {
|
||||||
return {
|
const relevantCameraConfigs = Array.from(cameraIDs).map((cameraID) =>
|
||||||
|
cameras.get(cameraID),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there isn't a label or zone specified, we can come up with a single
|
||||||
|
// batch query for Frigate that will match across all cameras.
|
||||||
|
const canDoBatchQuery = relevantCameraConfigs.every(
|
||||||
|
(cameraConfig) => !cameraConfig?.frigate.label && !cameraConfig?.frigate.zone,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (canDoBatchQuery) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
type: QueryType.Event,
|
type: QueryType.Event,
|
||||||
cameraID: cameraID,
|
cameraIDs: cameraIDs,
|
||||||
...(cameraConfig.frigate.label && { what: cameraConfig.frigate.label }),
|
|
||||||
...(cameraConfig.frigate.zone && { where: cameraConfig.frigate.zone }),
|
|
||||||
...query,
|
...query,
|
||||||
};
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: EventQuery[] = [];
|
||||||
|
for (const cameraID of cameraIDs) {
|
||||||
|
const cameraConfig = cameras.get(cameraID);
|
||||||
|
if (cameraConfig) {
|
||||||
|
output.push({
|
||||||
|
type: QueryType.Event,
|
||||||
|
cameraIDs: new Set([cameraID]),
|
||||||
|
...(cameraConfig.frigate.label && {
|
||||||
|
what: new Set([cameraConfig.frigate.label]),
|
||||||
|
}),
|
||||||
|
...(cameraConfig.frigate.zone && {
|
||||||
|
where: new Set([cameraConfig.frigate.zone]),
|
||||||
|
}),
|
||||||
|
...query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output.length ? output : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultRecordingQuery(
|
public generateDefaultRecordingQuery(
|
||||||
cameraID: string,
|
_cameras: Map<string, CameraConfig>,
|
||||||
_cameraConfig: CameraConfig,
|
cameraIDs: Set<string>,
|
||||||
query: PartialRecordingQuery,
|
query: PartialRecordingQuery,
|
||||||
): RecordingQuery | null {
|
): RecordingQuery[] | null {
|
||||||
return {
|
return [
|
||||||
|
{
|
||||||
type: QueryType.Recording,
|
type: QueryType.Recording,
|
||||||
cameraID: cameraID,
|
cameraIDs: cameraIDs,
|
||||||
...query,
|
...query,
|
||||||
};
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultRecordingSegmentsQuery(
|
public generateDefaultRecordingSegmentsQuery(
|
||||||
cameraID: string,
|
_cameras: Map<string, CameraConfig>,
|
||||||
_cameraConfig: CameraConfig,
|
cameraIDs: Set<string>,
|
||||||
query: PartialRecordingSegmentsQuery,
|
query: PartialRecordingSegmentsQuery,
|
||||||
): RecordingSegmentsQuery | null {
|
): RecordingSegmentsQuery[] | null {
|
||||||
if (!query.start || !query.end) {
|
if (!query.start || !query.end) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return [
|
||||||
|
{
|
||||||
type: QueryType.RecordingSegments,
|
type: QueryType.RecordingSegments,
|
||||||
cameraID: cameraID,
|
cameraIDs: cameraIDs,
|
||||||
start: query.start,
|
start: query.start,
|
||||||
end: query.end,
|
end: query.end,
|
||||||
...query,
|
...query,
|
||||||
};
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async favoriteMedia(
|
public async favoriteMedia(
|
||||||
@@ -169,49 +212,118 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
media.setFavorite(favorite);
|
media.setFavorite(favorite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _buildInstanceToCameraIDMapFromQuery(
|
||||||
|
cameras: Map<string, CameraConfig>,
|
||||||
|
query: DataQuery,
|
||||||
|
): Map<string, Set<string>> {
|
||||||
|
const output: Map<string, Set<string>> = new Map();
|
||||||
|
for (const cameraID of query.cameraIDs) {
|
||||||
|
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
||||||
|
const clientID = cameraConfig?.frigate.client_id;
|
||||||
|
if (clientID) {
|
||||||
|
if (!output.has(clientID)) {
|
||||||
|
output.set(clientID, new Set());
|
||||||
|
}
|
||||||
|
output.get(clientID)?.add(cameraID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getFrigateCameraNamesForCameraIDs(
|
||||||
|
cameras: Map<string, CameraConfig>,
|
||||||
|
cameraIDs: Set<string>,
|
||||||
|
): Set<string> {
|
||||||
|
const output = new Set<string>();
|
||||||
|
for (const cameraID of cameraIDs) {
|
||||||
|
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
||||||
|
if (cameraConfig?.frigate.camera_name) {
|
||||||
|
output.add(cameraConfig.frigate.camera_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
public async getEvents(
|
public async getEvents(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: Map<string, CameraConfig>,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
): Promise<QueryReturnType<EventQuery> | null> {
|
): Promise<EventQueryResultsMap | null> {
|
||||||
const cameraConfig = this._getQueryableCameraConfig(cameras, query.cameraID);
|
const output: EventQueryResultsMap = new Map();
|
||||||
if (!cameraConfig) {
|
|
||||||
return null;
|
const processInstanceQuery = async (
|
||||||
|
instanceID: string,
|
||||||
|
cameraIDs?: Set<string>,
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!cameraIDs || !cameraIDs.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const instanceQuery = { ...query, cameraIDs: cameraIDs };
|
||||||
|
const cachedResult = this._requestCache.get(instanceQuery);
|
||||||
|
if (cachedResult) {
|
||||||
|
output.set(query, cachedResult as EventQueryResults);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nativeQuery: NativeFrigateEventQuery = {
|
const nativeQuery: NativeFrigateEventQuery = {
|
||||||
instance_id: cameraConfig.frigate.client_id,
|
instance_id: instanceID,
|
||||||
camera: cameraConfig.frigate.camera_name,
|
cameras: Array.from(this._getFrigateCameraNamesForCameraIDs(cameras, cameraIDs)),
|
||||||
...(query.what && { label: query.what }),
|
...(query.what && { label: Array.from(query.what) }),
|
||||||
...(query.where && { zone: query.where }),
|
...(query.where && { zone: Array.from(query.where) }),
|
||||||
...(query?.end && { before: Math.floor(query.end.getTime() / 1000) }),
|
...(query.end && { before: Math.floor(query.end.getTime() / 1000) }),
|
||||||
...(query?.start && { after: Math.floor(query.start.getTime() / 1000) }),
|
...(query.start && { after: Math.floor(query.start.getTime() / 1000) }),
|
||||||
...(query?.limit && { limit: query.limit }),
|
...(query.limit && { limit: query.limit }),
|
||||||
...(query?.hasClip && { has_clip: query.hasClip }),
|
...(query.hasClip && { has_clip: query.hasClip }),
|
||||||
...(query?.hasSnapshot && { has_snapshot: query.hasSnapshot }),
|
...(query.hasSnapshot && { has_snapshot: query.hasSnapshot }),
|
||||||
limit: query?.limit ?? CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
limit: query?.limit ?? CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <FrigateEventQueryResults>{
|
const result: FrigateEventQueryResults = {
|
||||||
type: QueryResultsType.Event,
|
type: QueryResultsType.Event,
|
||||||
engine: Engine.Frigate,
|
engine: Engine.Frigate,
|
||||||
|
instanceID: instanceID,
|
||||||
events: await getEvents(hass, nativeQuery),
|
events: await getEvents(hass, nativeQuery),
|
||||||
expiry: add(new Date(), { seconds: EVENT_REQUEST_CACHE_MAX_AGE_SECONDS }),
|
expiry: add(new Date(), { seconds: EVENT_REQUEST_CACHE_MAX_AGE_SECONDS }),
|
||||||
cached: false,
|
cached: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._requestCache.set(query, { ...result, cached: true }, result.expiry);
|
||||||
|
output.set(instanceQuery, result);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Frigate allows multiple cameras to be searched for events in a single
|
||||||
|
// query. Break them down into groups of cameras per Frigate instance, then
|
||||||
|
// query once per instance for all cameras in that instance.
|
||||||
|
const instances = this._buildInstanceToCameraIDMapFromQuery(cameras, query);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(instances.keys()).map((instanceID) =>
|
||||||
|
processInstanceQuery(instanceID, instances.get(instanceID)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return output.size ? output : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRecordings(
|
public async getRecordings(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: Map<string, CameraConfig>,
|
||||||
query: RecordingQuery,
|
query: RecordingQuery,
|
||||||
): Promise<QueryReturnType<RecordingQuery> | null> {
|
): Promise<RecordingQueryResultsMap | null> {
|
||||||
const cameraConfig = this._getQueryableCameraConfig(cameras, query.cameraID);
|
const output: RecordingQueryResultsMap = new Map();
|
||||||
if (!cameraConfig) {
|
|
||||||
return null;
|
const processQuery = async (query: RecordingQuery): Promise<void> => {
|
||||||
|
const cachedResult = this._requestCache.get(query);
|
||||||
|
if (cachedResult) {
|
||||||
|
output.set(query, cachedResult as RecordingQueryResults);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// There will only ever be a single cameraID specified for queries in this
|
||||||
|
// inner function.
|
||||||
|
const cameraID = [...query.cameraIDs][0];
|
||||||
|
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
||||||
if (!cameraConfig || !cameraConfig.frigate.camera_name) {
|
if (!cameraConfig || !cameraConfig.frigate.camera_name) {
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordingSummary = await getRecordingsSummary(
|
const recordingSummary = await getRecordingsSummary(
|
||||||
@@ -232,7 +344,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
(!query.end || endHour <= query.end)
|
(!query.end || endHour <= query.end)
|
||||||
) {
|
) {
|
||||||
recordings.push({
|
recordings.push({
|
||||||
cameraID: query.cameraID,
|
cameraID: cameraID,
|
||||||
startTime: startHour,
|
startTime: startHour,
|
||||||
endTime: endHour,
|
endTime: endHour,
|
||||||
events: hourData.events,
|
events: hourData.events,
|
||||||
@@ -251,44 +363,65 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
).slice(0, query.limit);
|
).slice(0, query.limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <FrigateRecordingQueryResults>{
|
const result: FrigateRecordingQueryResults = {
|
||||||
type: QueryResultsType.Recording,
|
type: QueryResultsType.Recording,
|
||||||
engine: Engine.Frigate,
|
engine: Engine.Frigate,
|
||||||
|
instanceID: cameraConfig.frigate.client_id,
|
||||||
recordings: recordings,
|
recordings: recordings,
|
||||||
expiry: add(new Date(), {
|
expiry: add(new Date(), {
|
||||||
seconds: RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS,
|
seconds: RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS,
|
||||||
}),
|
}),
|
||||||
cached: false,
|
cached: false,
|
||||||
};
|
};
|
||||||
|
this._requestCache.set(query, { ...result, cached: true }, result.expiry);
|
||||||
|
output.set(query, result);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Frigate recordings can only be queried for a single camera, so fan out
|
||||||
|
// the inbound query into multiple outbound queries.
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(query.cameraIDs).map((cameraID) =>
|
||||||
|
processQuery({ ...query, cameraIDs: new Set([cameraID]) }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return output.size ? output : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRecordingSegments(
|
public async getRecordingSegments(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: Map<string, CameraConfig>,
|
||||||
query: RecordingSegmentsQuery,
|
query: RecordingSegmentsQuery,
|
||||||
): Promise<QueryReturnType<RecordingSegmentsQuery> | null> {
|
): Promise<RecordingSegmentsQueryResultsMap | null> {
|
||||||
const cameraConfig = this._getQueryableCameraConfig(cameras, query.cameraID);
|
const output: RecordingSegmentsQueryResultsMap = new Map();
|
||||||
|
|
||||||
|
const processQuery = async (query: RecordingSegmentsQuery): Promise<void> => {
|
||||||
|
// There will only ever be a single cameraID specified for queries in this
|
||||||
|
// inner function.
|
||||||
|
const cameraID = [...query.cameraIDs][0];
|
||||||
|
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
||||||
if (!cameraConfig || !cameraConfig.frigate.camera_name) {
|
if (!cameraConfig || !cameraConfig.frigate.camera_name) {
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const range: DateRange = { start: query.start, end: query.end };
|
const range: DateRange = { start: query.start, end: query.end };
|
||||||
|
|
||||||
// A note on Frigate Recording Segments:
|
// A note on Frigate Recording Segments:
|
||||||
// - Unlike other query types, there is an internal cache at the engine
|
// - There is an internal cache at the engine level for segments to allow
|
||||||
// level for segments to allow caching "within an existing query" (e.g. if
|
// caching "within an existing query" (e.g. if we already cached hour
|
||||||
// we already cached hour 1-8, we will avoid a fetch if we request hours
|
// 1-8, we will avoid a fetch if we request hours 2-3 even though the
|
||||||
// 2-3 even though the query is different -- the segments won't be). This
|
// query is different -- the segments won't be). This is since the
|
||||||
// is since the volume of data in segment transfers can be high, and the
|
// volume of data in segment transfers can be high, and the segments can
|
||||||
// segments can be used in high frequency situations (e.g. video seeking).
|
// be used in high frequency situations (e.g. video seeking).
|
||||||
const cachedSegments = this._recordingSegmentsCache.get(query.cameraID, range);
|
const cachedSegments = this._recordingSegmentsCache.get(cameraID, range);
|
||||||
if (cachedSegments) {
|
if (cachedSegments) {
|
||||||
return {
|
output.set(query, <FrigateRecordingSegmentsQueryResults>{
|
||||||
type: QueryResultsType.RecordingSegments,
|
type: QueryResultsType.RecordingSegments,
|
||||||
engine: Engine.Frigate,
|
engine: Engine.Frigate,
|
||||||
|
instanceID: cameraConfig.frigate.client_id,
|
||||||
segments: cachedSegments,
|
segments: cachedSegments,
|
||||||
cached: true,
|
cached: true,
|
||||||
};
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const request: NativeFrigateRecordingSegmentsQuery = {
|
const request: NativeFrigateRecordingSegmentsQuery = {
|
||||||
@@ -299,20 +432,55 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const segments = await getRecordingSegments(hass, request);
|
const segments = await getRecordingSegments(hass, request);
|
||||||
this._recordingSegmentsCache.add(query.cameraID, range, segments);
|
this._recordingSegmentsCache.add(cameraID, range, segments);
|
||||||
|
|
||||||
runWhenIdleIfSupported(() => this._throttledSegmentGarbageCollector(hass, cameras));
|
output.set(query, <FrigateRecordingSegmentsQueryResults>{
|
||||||
|
|
||||||
return {
|
|
||||||
type: QueryResultsType.RecordingSegments,
|
type: QueryResultsType.RecordingSegments,
|
||||||
engine: Engine.Frigate,
|
engine: Engine.Frigate,
|
||||||
|
instanceID: cameraConfig.frigate.client_id,
|
||||||
segments: segments,
|
segments: segments,
|
||||||
cached: false,
|
cached: false,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Frigate recording segments can only be queried for a single camera, so
|
||||||
|
// fan out the inbound query into multiple outbound queries.
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(query.cameraIDs).map((cameraID) =>
|
||||||
|
processQuery({ ...query, cameraIDs: new Set([cameraID]) }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
runWhenIdleIfSupported(() => this._throttledSegmentGarbageCollector(hass, cameras));
|
||||||
|
return output.size ? output : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getCameraIDMatch(
|
||||||
|
cameras: Map<string, CameraConfig>,
|
||||||
|
query: DataQuery,
|
||||||
|
instanceID: string,
|
||||||
|
cameraName: string,
|
||||||
|
): string | null {
|
||||||
|
// If the query is only for a single cameraID, all results are assumed to
|
||||||
|
// belong to it for performance reasons. Otherwise, we need to map the
|
||||||
|
// instanceID and camera name for the known cameras, and get the precise
|
||||||
|
// cameraID that matches the expected instance ID / camera name.
|
||||||
|
if (query.cameraIDs.size === 1) {
|
||||||
|
return [...query.cameraIDs][0];
|
||||||
|
}
|
||||||
|
for (const [cameraID, cameraConfig] of cameras.entries()) {
|
||||||
|
if (
|
||||||
|
cameraConfig.frigate.client_id === instanceID &&
|
||||||
|
cameraConfig.frigate.camera_name === cameraName
|
||||||
|
) {
|
||||||
|
return cameraID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateMediaFromEvents(
|
public generateMediaFromEvents(
|
||||||
cameraConfig: CameraConfig,
|
cameras: Map<string, CameraConfig>,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
results: QueryReturnType<EventQuery>,
|
results: QueryReturnType<EventQuery>,
|
||||||
): ViewMedia[] | null {
|
): ViewMedia[] | null {
|
||||||
@@ -322,6 +490,19 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
const output: ViewMedia[] = [];
|
const output: ViewMedia[] = [];
|
||||||
for (const event of results.events) {
|
for (const event of results.events) {
|
||||||
|
const cameraID = this._getCameraIDMatch(
|
||||||
|
cameras,
|
||||||
|
query,
|
||||||
|
results.instanceID,
|
||||||
|
event.camera,
|
||||||
|
);
|
||||||
|
if (!cameraID) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
||||||
|
if (!cameraConfig) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let mediaType: 'clip' | 'snapshot' | null = null;
|
let mediaType: 'clip' | 'snapshot' | null = null;
|
||||||
if (
|
if (
|
||||||
!query.hasClip &&
|
!query.hasClip &&
|
||||||
@@ -339,7 +520,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
const media = FrigateViewMediaFactory.createEventViewMedia(
|
const media = FrigateViewMediaFactory.createEventViewMedia(
|
||||||
mediaType,
|
mediaType,
|
||||||
query.cameraID,
|
cameraID,
|
||||||
cameraConfig,
|
cameraConfig,
|
||||||
event,
|
event,
|
||||||
);
|
);
|
||||||
@@ -351,8 +532,8 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateMediaFromRecordings(
|
public generateMediaFromRecordings(
|
||||||
cameraConfig: CameraConfig,
|
cameras: Map<string, CameraConfig>,
|
||||||
query: RecordingQuery,
|
_query: RecordingQuery,
|
||||||
results: QueryReturnType<RecordingQuery>,
|
results: QueryReturnType<RecordingQuery>,
|
||||||
): ViewMedia[] | null {
|
): ViewMedia[] | null {
|
||||||
if (!FrigateQueryResultsClassifier.isFrigateRecordingQueryResults(results)) {
|
if (!FrigateQueryResultsClassifier.isFrigateRecordingQueryResults(results)) {
|
||||||
@@ -361,8 +542,12 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
const output: ViewMedia[] = [];
|
const output: ViewMedia[] = [];
|
||||||
for (const recording of results.recordings) {
|
for (const recording of results.recordings) {
|
||||||
|
const cameraConfig = this._getQueryableCameraConfig(cameras, recording.cameraID);
|
||||||
|
if (!cameraConfig) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const media = FrigateViewMediaFactory.createRecordingViewMedia(
|
const media = FrigateViewMediaFactory.createRecordingViewMedia(
|
||||||
query.cameraID,
|
recording.cameraID,
|
||||||
recording,
|
recording,
|
||||||
cameraConfig,
|
cameraConfig,
|
||||||
);
|
);
|
||||||
@@ -373,23 +558,13 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
public areMediaQueriesResultsFresh(
|
public getQueryResultMaxAge(query: DataQuery): number | null {
|
||||||
queries: MediaQueries,
|
if (query.type === QueryType.Event) {
|
||||||
results: MediaQueriesResults,
|
return EVENT_REQUEST_CACHE_MAX_AGE_SECONDS;
|
||||||
): boolean {
|
} else if (query.type === QueryType.Recording) {
|
||||||
let freshThreshold: number | null = null;
|
return RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS;
|
||||||
if (MediaQueriesClassifier.areEventQueries(queries)) {
|
|
||||||
freshThreshold = EVENT_REQUEST_CACHE_MAX_AGE_SECONDS;
|
|
||||||
} else if (MediaQueriesClassifier.areRecordingQueries(queries)) {
|
|
||||||
freshThreshold = RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS;
|
|
||||||
}
|
}
|
||||||
const now = new Date();
|
return null;
|
||||||
const resultsTimestamp = results.getResultsTimestamp();
|
|
||||||
return (
|
|
||||||
!freshThreshold ||
|
|
||||||
!resultsTimestamp ||
|
|
||||||
add(resultsTimestamp, { seconds: freshThreshold }) >= now
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMediaSeekTime(
|
public async getMediaSeekTime(
|
||||||
@@ -404,18 +579,26 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cameraID = media.getCameraID();
|
||||||
const query: RecordingSegmentsQuery = {
|
const query: RecordingSegmentsQuery = {
|
||||||
cameraID: media.getCameraID(),
|
cameraIDs: new Set([cameraID]),
|
||||||
start: start,
|
start: start,
|
||||||
end: end,
|
end: end,
|
||||||
type: QueryType.RecordingSegments,
|
type: QueryType.RecordingSegments,
|
||||||
};
|
};
|
||||||
|
|
||||||
const segments = await this.getRecordingSegments(hass, cameras, query);
|
const results = await this.getRecordingSegments(hass, cameras, query);
|
||||||
const out = segments
|
|
||||||
? this._getSeekTimeInSegments(start, target, segments.segments)
|
if (results) {
|
||||||
: null;
|
return this._getSeekTimeInSegments(
|
||||||
return out;
|
start,
|
||||||
|
target,
|
||||||
|
// There will only be a single result since Frigate recording segments
|
||||||
|
// searches are per camera which is specified singularly above.
|
||||||
|
Array.from(results.values())[0].segments,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _getQueryableCameraConfig(
|
protected _getQueryableCameraConfig(
|
||||||
@@ -438,10 +621,10 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
cameras: Map<string, CameraConfig>,
|
cameras: Map<string, CameraConfig>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const cameraIDs = this._recordingSegmentsCache.getCameraIDs();
|
const cameraIDs = this._recordingSegmentsCache.getCameraIDs();
|
||||||
const recordingQueries: RecordingQuery[] = cameraIDs.map((cameraID) => ({
|
const recordingQuery: RecordingQuery = {
|
||||||
cameraID: cameraID,
|
cameraIDs: new Set(cameraIDs),
|
||||||
type: QueryType.Recording,
|
type: QueryType.Recording,
|
||||||
}));
|
};
|
||||||
|
|
||||||
const countSegments = () =>
|
const countSegments = () =>
|
||||||
sum(
|
sum(
|
||||||
@@ -451,19 +634,6 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
);
|
);
|
||||||
const segmentsStart = countSegments();
|
const segmentsStart = countSegments();
|
||||||
|
|
||||||
const results: Map<RecordingQuery, FrigateRecordingQueryResults> = new Map();
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
recordingQueries.map((query) =>
|
|
||||||
(async () => {
|
|
||||||
const recordings = await this.getRecordings(hass, cameras, query);
|
|
||||||
if (recordings && recordings.engine === Engine.Frigate) {
|
|
||||||
results.set(query, recordings as FrigateRecordingQueryResults);
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Performance: _recordingSegments is potentially very large (e.g. 10K - 1M
|
// Performance: _recordingSegments is potentially very large (e.g. 10K - 1M
|
||||||
// items) and each item must be examined, so care required here to stick to
|
// items) and each item must be examined, so care required here to stick to
|
||||||
// nothing worse than O(n) performance.
|
// nothing worse than O(n) performance.
|
||||||
@@ -471,16 +641,28 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
return `${cameraID}/${startTime.getDate()}/${startTime.getHours()}`;
|
return `${cameraID}/${startTime.getDate()}/${startTime.getHours()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const results = await this.getRecordings(hass, cameras, recordingQuery);
|
||||||
|
if (!results) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const [query, result] of results) {
|
for (const [query, result] of results) {
|
||||||
|
if (!FrigateQueryResultsClassifier.isFrigateRecordingQueryResults(result)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const goodHours: Set<string> = new Set();
|
const goodHours: Set<string> = new Set();
|
||||||
for (const recording of result.recordings) {
|
for (const recording of result.recordings) {
|
||||||
goodHours.add(getHourID(recording.cameraID, recording.startTime));
|
goodHours.add(getHourID(recording.cameraID, recording.startTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Frigate recordings are always executed individually, so there'll only
|
||||||
|
// be a single results.
|
||||||
|
const cameraID = Array.from(query.cameraIDs)[0];
|
||||||
this._recordingSegmentsCache.expireMatches(
|
this._recordingSegmentsCache.expireMatches(
|
||||||
query.cameraID,
|
cameraID,
|
||||||
(segment: RecordingSegment) => {
|
(segment: RecordingSegment) => {
|
||||||
const hourID = getHourID(query.cameraID, fromUnixTime(segment.start_time));
|
const hourID = getHourID(cameraID, fromUnixTime(segment.start_time));
|
||||||
// ~O(1) lookup time for a JS set.
|
// ~O(1) lookup time for a JS set.
|
||||||
return goodHours.has(hourID);
|
return goodHours.has(hourID);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -95,9 +95,9 @@ export async function retainEvent(
|
|||||||
|
|
||||||
export interface NativeFrigateEventQuery {
|
export interface NativeFrigateEventQuery {
|
||||||
instance_id?: string;
|
instance_id?: string;
|
||||||
camera?: string;
|
cameras?: string[];
|
||||||
label?: string;
|
labels?: string[];
|
||||||
zone?: string;
|
zones?: string[];
|
||||||
after?: number;
|
after?: number;
|
||||||
before?: number;
|
before?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
|||||||
+116
-86
@@ -5,6 +5,7 @@ import {
|
|||||||
DataQuery,
|
DataQuery,
|
||||||
EventQuery,
|
EventQuery,
|
||||||
EventQueryResults,
|
EventQueryResults,
|
||||||
|
EventQueryResultsMap,
|
||||||
MediaQuery,
|
MediaQuery,
|
||||||
PartialDataQuery,
|
PartialDataQuery,
|
||||||
PartialEventQuery,
|
PartialEventQuery,
|
||||||
@@ -17,16 +18,21 @@ import {
|
|||||||
QueryType,
|
QueryType,
|
||||||
RecordingQuery,
|
RecordingQuery,
|
||||||
RecordingQueryResults,
|
RecordingQueryResults,
|
||||||
|
RecordingQueryResultsMap,
|
||||||
RecordingSegmentsQuery,
|
RecordingSegmentsQuery,
|
||||||
RecordingSegmentsQueryResults,
|
RecordingSegmentsQueryResults,
|
||||||
|
RecordingSegmentsQueryResultsMap,
|
||||||
|
ResultsMap,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import orderBy from 'lodash-es/orderBy';
|
import orderBy from 'lodash-es/orderBy';
|
||||||
import { CameraManagerEngineFactory } from './engine-factory.js';
|
import { CameraManagerEngineFactory } from './engine-factory.js';
|
||||||
import { ViewMedia } from '../view/media.js';
|
import { ViewMedia } from '../view/media.js';
|
||||||
import { MediaQueriesResults } from '../view/media-queries-results';
|
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||||
import { MemoryRequestCache } from './cache.js';
|
|
||||||
import { MediaQueries } from '../view/media-queries.js';
|
import { MediaQueries } from '../view/media-queries.js';
|
||||||
import uniqBy from 'lodash-es/uniqBy';
|
import uniqBy from 'lodash-es/uniqBy';
|
||||||
|
import { CameraManagerEngine } from './engine.js';
|
||||||
|
import sum from 'lodash-es/sum';
|
||||||
|
import add from 'date-fns/add';
|
||||||
|
|
||||||
export class QueryClassifier {
|
export class QueryClassifier {
|
||||||
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
|
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
|
||||||
@@ -67,27 +73,22 @@ export interface ExtendedMediaQueryResult<T extends MediaQuery> {
|
|||||||
results: ViewMedia[];
|
results: ViewMedia[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RequestCache = MemoryRequestCache<DataQuery, QueryResults>;
|
|
||||||
|
|
||||||
export class CameraManager {
|
export class CameraManager {
|
||||||
protected _engineFactory: CameraManagerEngineFactory;
|
protected _engineFactory: CameraManagerEngineFactory;
|
||||||
protected _cameras: Map<string, CameraConfig>;
|
protected _cameras: Map<string, CameraConfig>;
|
||||||
protected _requestCache: RequestCache;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
engineFactory: CameraManagerEngineFactory,
|
engineFactory: CameraManagerEngineFactory,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: Map<string, CameraConfig>,
|
||||||
requestCache: RequestCache,
|
|
||||||
) {
|
) {
|
||||||
this._engineFactory = engineFactory;
|
this._engineFactory = engineFactory;
|
||||||
this._cameras = cameras;
|
this._cameras = cameras;
|
||||||
this._requestCache = requestCache;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultEventQueries(
|
public generateDefaultEventQueries(
|
||||||
cameraIDs: string | Set<string>,
|
cameraIDs: string | Set<string>,
|
||||||
partialQuery: PartialEventQuery,
|
partialQuery: PartialEventQuery,
|
||||||
): EventQuery[] {
|
): EventQuery[] | null {
|
||||||
return this._generateDefaultQueries(cameraIDs, {
|
return this._generateDefaultQueries(cameraIDs, {
|
||||||
...partialQuery,
|
...partialQuery,
|
||||||
type: QueryType.Event,
|
type: QueryType.Event,
|
||||||
@@ -97,7 +98,7 @@ export class CameraManager {
|
|||||||
public generateDefaultRecordingQueries(
|
public generateDefaultRecordingQueries(
|
||||||
cameraIDs: string | Set<string>,
|
cameraIDs: string | Set<string>,
|
||||||
partialQuery: PartialRecordingQuery,
|
partialQuery: PartialRecordingQuery,
|
||||||
): RecordingQuery[] {
|
): RecordingQuery[] | null {
|
||||||
return this._generateDefaultQueries(cameraIDs, {
|
return this._generateDefaultQueries(cameraIDs, {
|
||||||
...partialQuery,
|
...partialQuery,
|
||||||
type: QueryType.Recording,
|
type: QueryType.Recording,
|
||||||
@@ -107,73 +108,76 @@ export class CameraManager {
|
|||||||
public generateDefaultRecordingSegmentsQueries(
|
public generateDefaultRecordingSegmentsQueries(
|
||||||
cameraIDs: string | Set<string>,
|
cameraIDs: string | Set<string>,
|
||||||
partialQuery: PartialRecordingSegmentsQuery,
|
partialQuery: PartialRecordingSegmentsQuery,
|
||||||
): RecordingSegmentsQuery[] {
|
): RecordingSegmentsQuery[] | null {
|
||||||
return this._generateDefaultQueries(cameraIDs, {
|
return this._generateDefaultQueries(cameraIDs, {
|
||||||
...partialQuery,
|
...partialQuery,
|
||||||
type: QueryType.RecordingSegments,
|
type: QueryType.RecordingSegments,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _generateDefaultQueries<PQT extends Partial<DataQuery>>(
|
protected _generateDefaultQueries<PQT extends PartialDataQuery>(
|
||||||
cameraIDs: string | Set<string>,
|
cameraIDs: string | Set<string>,
|
||||||
partialQuery: PQT,
|
partialQuery: PQT,
|
||||||
): PartialQueryConcreteType<PQT>[] {
|
): PartialQueryConcreteType<PQT>[] | null {
|
||||||
const concreteQueries: PartialQueryConcreteType<PQT>[] = [];
|
const concreteQueries: PartialQueryConcreteType<PQT>[] = [];
|
||||||
const _cameraIDs = setify(cameraIDs);
|
const _cameraIDs = setify(cameraIDs);
|
||||||
|
|
||||||
_cameraIDs.forEach((cameraID) => {
|
const engines = this._engineFactory.getEnginesForCameraIDs(
|
||||||
const cameraConfig = this._cameras.get(cameraID);
|
this._cameras,
|
||||||
if (!cameraConfig) {
|
_cameraIDs,
|
||||||
return;
|
);
|
||||||
|
|
||||||
|
if (!engines) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
for (const [engine, cameraIDs] of engines) {
|
||||||
if (!engine) {
|
let queries: DataQuery[] | null = null;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let query: DataQuery | null = null;
|
|
||||||
if (QueryClassifier.isEventQuery(partialQuery)) {
|
if (QueryClassifier.isEventQuery(partialQuery)) {
|
||||||
query = engine.generateDefaultEventQuery(cameraID, cameraConfig, partialQuery);
|
queries = engine.generateDefaultEventQuery(
|
||||||
|
this._cameras,
|
||||||
|
cameraIDs,
|
||||||
|
partialQuery,
|
||||||
|
);
|
||||||
} else if (QueryClassifier.isRecordingQuery(partialQuery)) {
|
} else if (QueryClassifier.isRecordingQuery(partialQuery)) {
|
||||||
query = engine.generateDefaultRecordingQuery(
|
queries = engine.generateDefaultRecordingQuery(
|
||||||
cameraID,
|
this._cameras,
|
||||||
cameraConfig,
|
cameraIDs,
|
||||||
partialQuery,
|
partialQuery,
|
||||||
);
|
);
|
||||||
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
|
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
|
||||||
query = engine.generateDefaultRecordingSegmentsQuery(
|
queries = engine.generateDefaultRecordingSegmentsQuery(
|
||||||
cameraID,
|
this._cameras,
|
||||||
cameraConfig,
|
cameraIDs,
|
||||||
partialQuery,
|
partialQuery,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query) {
|
for (const query of queries ?? []) {
|
||||||
concreteQueries.push(query as PartialQueryConcreteType<PQT>);
|
concreteQueries.push(query as PartialQueryConcreteType<PQT>);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return concreteQueries;
|
return concreteQueries;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getEvents(
|
public async getEvents(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
query: EventQuery | EventQuery[],
|
query: EventQuery | EventQuery[],
|
||||||
): Promise<Map<EventQuery, EventQueryResults>> {
|
): Promise<EventQueryResultsMap> {
|
||||||
return await this._handleQuery(hass, query);
|
return await this._handleQuery(hass, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRecordings(
|
public async getRecordings(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
query: RecordingQuery | RecordingQuery[],
|
query: RecordingQuery | RecordingQuery[],
|
||||||
): Promise<Map<RecordingQuery, RecordingQueryResults>> {
|
): Promise<RecordingQueryResultsMap> {
|
||||||
return await this._handleQuery(hass, query);
|
return await this._handleQuery(hass, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRecordingSegments(
|
public async getRecordingSegments(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
query: RecordingSegmentsQuery | RecordingSegmentsQuery[],
|
query: RecordingSegmentsQuery | RecordingSegmentsQuery[],
|
||||||
): Promise<Map<RecordingSegmentsQuery, RecordingSegmentsQueryResults>> {
|
): Promise<RecordingSegmentsQueryResultsMap> {
|
||||||
return await this._handleQuery(hass, query);
|
return await this._handleQuery(hass, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,18 +305,31 @@ export class CameraManager {
|
|||||||
queries: MediaQueries,
|
queries: MediaQueries,
|
||||||
results: MediaQueriesResults,
|
results: MediaQueriesResults,
|
||||||
): boolean {
|
): boolean {
|
||||||
const cameraIDs: Set<string> = new Set();
|
const now = new Date();
|
||||||
(queries.getQueries() ?? []).forEach((query) => cameraIDs.add(query.cameraID));
|
const resultsTimestamp = results.getResultsTimestamp();
|
||||||
for (const cameraID of cameraIDs) {
|
|
||||||
const cameraConfig = this._cameras.get(cameraID);
|
if (!resultsTimestamp) {
|
||||||
if (!cameraConfig) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
|
||||||
if (!engine || !engine.areMediaQueriesResultsFresh(queries, results)) {
|
for (const query of queries.getQueries() ?? []) {
|
||||||
|
const engines = this._engineFactory.getEnginesForCameraIDs(
|
||||||
|
this._cameras,
|
||||||
|
query.cameraIDs,
|
||||||
|
);
|
||||||
|
for (const [engine, cameraIDs] of engines ?? []) {
|
||||||
|
const maxAgeSeconds = engine.getQueryResultMaxAge({
|
||||||
|
...query,
|
||||||
|
cameraIDs: cameraIDs,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
maxAgeSeconds !== null &&
|
||||||
|
add(resultsTimestamp, { seconds: maxAgeSeconds }) < now
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,83 +361,96 @@ export class CameraManager {
|
|||||||
): Promise<Map<QT, QueryReturnType<QT>>> {
|
): Promise<Map<QT, QueryReturnType<QT>>> {
|
||||||
const _queries = arrayify(query);
|
const _queries = arrayify(query);
|
||||||
const results = new Map<QT, QueryReturnType<QT>>();
|
const results = new Map<QT, QueryReturnType<QT>>();
|
||||||
|
|
||||||
const queryStartTime = new Date();
|
const queryStartTime = new Date();
|
||||||
let queryCachedCount = 0;
|
|
||||||
|
const processEngineQuery = async (
|
||||||
|
engine: CameraManagerEngine,
|
||||||
|
query?: QT,
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let engineResult: Map<QT, QueryReturnType<QT>> | null = null;
|
||||||
|
if (QueryClassifier.isEventQuery(query)) {
|
||||||
|
engineResult = (await engine.getEvents(hass, this._cameras, query)) as Map<
|
||||||
|
QT,
|
||||||
|
QueryReturnType<QT>
|
||||||
|
> | null;
|
||||||
|
} else if (QueryClassifier.isRecordingQuery(query)) {
|
||||||
|
engineResult = (await engine.getRecordings(hass, this._cameras, query)) as Map<
|
||||||
|
QT,
|
||||||
|
QueryReturnType<QT>
|
||||||
|
> | null;
|
||||||
|
} else if (QueryClassifier.isRecordingSegmentsQuery(query)) {
|
||||||
|
engineResult = (await engine.getRecordingSegments(
|
||||||
|
hass,
|
||||||
|
this._cameras,
|
||||||
|
query,
|
||||||
|
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
engineResult?.forEach((value, key) => results.set(key, value));
|
||||||
|
};
|
||||||
|
|
||||||
const processQuery = async (query: QT): Promise<void> => {
|
const processQuery = async (query: QT): Promise<void> => {
|
||||||
const cachedResult: QueryReturnType<QT> | null = this._requestCache.get(
|
const engines = this._engineFactory.getEnginesForCameraIDs(
|
||||||
query,
|
this._cameras,
|
||||||
) as QueryReturnType<QT> | null;
|
query.cameraIDs,
|
||||||
if (cachedResult) {
|
);
|
||||||
queryCachedCount++;
|
if (!engines) {
|
||||||
results.set(query, cachedResult);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await Promise.all(
|
||||||
const engine = this._engineFactory.getEngineForQuery(this._cameras, query);
|
Array.from(engines.keys()).map((engine) =>
|
||||||
if (!engine) {
|
processEngineQuery(engine, { ...query, cameraIDs: engines.get(engine) }),
|
||||||
return;
|
),
|
||||||
}
|
);
|
||||||
|
|
||||||
let result: QueryResults | null = null;
|
|
||||||
if (QueryClassifier.isEventQuery(query)) {
|
|
||||||
result = await engine.getEvents(hass, this._cameras, query);
|
|
||||||
} else if (QueryClassifier.isRecordingQuery(query)) {
|
|
||||||
result = await engine.getRecordings(hass, this._cameras, query);
|
|
||||||
} else if (QueryClassifier.isRecordingSegmentsQuery(query)) {
|
|
||||||
result = await engine.getRecordingSegments(hass, this._cameras, query);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The engine may independently cached the results. Respect that in our
|
|
||||||
// debug logging.
|
|
||||||
if (result?.cached) {
|
|
||||||
queryCachedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
if (result.expiry) {
|
|
||||||
this._requestCache.set(query, { ...result, cached: true }, result.expiry);
|
|
||||||
}
|
|
||||||
results.set(query, result as QueryReturnType<QT>);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await Promise.all(_queries.map((query) => processQuery(query)));
|
await Promise.all(_queries.map((query) => processQuery(query)));
|
||||||
|
|
||||||
|
const cachedOutputQueries = sum(
|
||||||
|
Array.from(results.values()).map((result) => Number(result.cached)),
|
||||||
|
);
|
||||||
|
|
||||||
console.debug(
|
console.debug(
|
||||||
'Frigate Card CameraManager request (Cached:',
|
'Frigate Card CameraManager request [Input queries:',
|
||||||
`${queryCachedCount}/${_queries.length},`,
|
_queries.length,
|
||||||
`Duration: ${(new Date().getTime() - queryStartTime.getTime()) / 1000}s,`,
|
', Cached output queries:',
|
||||||
'Queries:',
|
cachedOutputQueries,
|
||||||
|
', Total output queries:',
|
||||||
|
results.size,
|
||||||
|
', Duration:',
|
||||||
|
`${(new Date().getTime() - queryStartTime.getTime()) / 1000}s,`,
|
||||||
|
', Queries:',
|
||||||
_queries,
|
_queries,
|
||||||
', Results:',
|
', Results:',
|
||||||
results,
|
results,
|
||||||
')',
|
']',
|
||||||
);
|
);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _convertQueryResultsToMedia<QT extends DataQuery>(
|
protected _convertQueryResultsToMedia<QT extends DataQuery>(
|
||||||
results: Map<QT, QueryReturnType<QT>>,
|
results: ResultsMap<QT>,
|
||||||
): ViewMedia[] {
|
): ViewMedia[] {
|
||||||
const mediaArray: ViewMedia[] = [];
|
const mediaArray: ViewMedia[] = [];
|
||||||
for (const [query, result] of results.entries()) {
|
for (const [query, result] of results.entries()) {
|
||||||
const cameraConfig = this._cameras.get(query.cameraID);
|
const engine = this._engineFactory.getEngine(result.engine);
|
||||||
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
|
||||||
|
|
||||||
if (engine && cameraConfig) {
|
if (engine) {
|
||||||
let media: ViewMedia[] | null = null;
|
let media: ViewMedia[] | null = null;
|
||||||
if (
|
if (
|
||||||
QueryClassifier.isEventQuery(query) &&
|
QueryClassifier.isEventQuery(query) &&
|
||||||
QueryResultClassifier.isEventQueryResult(result)
|
QueryResultClassifier.isEventQueryResult(result)
|
||||||
) {
|
) {
|
||||||
media = engine.generateMediaFromEvents(cameraConfig, query, result);
|
media = engine.generateMediaFromEvents(this._cameras, query, result);
|
||||||
} else if (
|
} else if (
|
||||||
QueryClassifier.isRecordingQuery(query) &&
|
QueryClassifier.isRecordingQuery(query) &&
|
||||||
QueryResultClassifier.isRecordingQuery(result)
|
QueryResultClassifier.isRecordingQuery(result)
|
||||||
) {
|
) {
|
||||||
media = engine.generateMediaFromRecordings(cameraConfig, query, result);
|
media = engine.generateMediaFromRecordings(this._cameras, query, result);
|
||||||
}
|
}
|
||||||
if (media) {
|
if (media) {
|
||||||
mediaArray.push(...media);
|
mediaArray.push(...media);
|
||||||
|
|||||||
+11
-3
@@ -23,7 +23,7 @@ export enum Engine {
|
|||||||
|
|
||||||
export interface DataQuery {
|
export interface DataQuery {
|
||||||
type: QueryType;
|
type: QueryType;
|
||||||
cameraID: string;
|
cameraIDs: Set<string>;
|
||||||
}
|
}
|
||||||
export type PartialDataQuery = Partial<DataQuery>;
|
export type PartialDataQuery = Partial<DataQuery>;
|
||||||
|
|
||||||
@@ -63,6 +63,11 @@ export type PartialQueryConcreteType<PQT> = PQT extends PartialEventQuery
|
|||||||
? RecordingSegmentsQuery
|
? RecordingSegmentsQuery
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
|
export type ResultsMap<QT> = Map<QT, QueryReturnType<QT>>;
|
||||||
|
export type EventQueryResultsMap = ResultsMap<EventQuery>;
|
||||||
|
export type RecordingQueryResultsMap = ResultsMap<RecordingQuery>;
|
||||||
|
export type RecordingSegmentsQueryResultsMap = ResultsMap<RecordingSegmentsQuery>;
|
||||||
|
|
||||||
// ===========
|
// ===========
|
||||||
// Event Query
|
// Event Query
|
||||||
// ===========
|
// ===========
|
||||||
@@ -77,10 +82,10 @@ export interface EventQuery extends MediaQuery {
|
|||||||
hasClip?: boolean;
|
hasClip?: boolean;
|
||||||
|
|
||||||
// Frigate equivalent: label
|
// Frigate equivalent: label
|
||||||
what?: string;
|
what?: Set<string>;
|
||||||
|
|
||||||
// Frigate equivalent: zone
|
// Frigate equivalent: zone
|
||||||
where?: string;
|
where?: Set<string>;
|
||||||
}
|
}
|
||||||
export type PartialEventQuery = Partial<EventQuery>;
|
export type PartialEventQuery = Partial<EventQuery>;
|
||||||
|
|
||||||
@@ -121,15 +126,18 @@ export interface RecordingSegmentsQueryResults extends QueryResults {
|
|||||||
|
|
||||||
export interface FrigateEventQueryResults extends EventQueryResults {
|
export interface FrigateEventQueryResults extends EventQueryResults {
|
||||||
engine: Engine.Frigate;
|
engine: Engine.Frigate;
|
||||||
|
instanceID: string;
|
||||||
events: FrigateEvent[];
|
events: FrigateEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FrigateRecordingQueryResults extends RecordingQueryResults {
|
export interface FrigateRecordingQueryResults extends RecordingQueryResults {
|
||||||
engine: Engine.Frigate;
|
engine: Engine.Frigate;
|
||||||
|
instanceID: string;
|
||||||
recordings: FrigateRecording[];
|
recordings: FrigateRecording[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FrigateRecordingSegmentsQueryResults
|
export interface FrigateRecordingSegmentsQueryResults
|
||||||
extends RecordingSegmentsQueryResults {
|
extends RecordingSegmentsQueryResults {
|
||||||
engine: Engine.Frigate;
|
engine: Engine.Frigate;
|
||||||
|
instanceID: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1106,7 +1106,6 @@ export class FrigateCard extends LitElement {
|
|||||||
this._cameraManager = new CameraManager(
|
this._cameraManager = new CameraManager(
|
||||||
new CameraManagerEngineFactory(),
|
new CameraManagerEngineFactory(),
|
||||||
this._cameras,
|
this._cameras,
|
||||||
new RequestCache(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
}) // Whether or not to set the timeline window.
|
}) // Whether or not to set the timeline window.
|
||||||
.mergeInContext({
|
.mergeInContext({
|
||||||
...(canSeek && { mediaViewer: { seek: targetTime } }),
|
...(canSeek && { mediaViewer: { seek: targetTime } }),
|
||||||
...this._setWindowInContext(properties)
|
...this._setWindowInContext(properties),
|
||||||
})
|
})
|
||||||
.dispatchChangeEvent(this);
|
.dispatchChangeEvent(this);
|
||||||
}
|
}
|
||||||
@@ -616,9 +616,12 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
options?.window ?? this._timeline.getWindow(),
|
options?.window ?? this._timeline.getWindow(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return new EventMediaQueries(
|
const eventQueries =
|
||||||
this._timelineSource.getTimelineEventQueries(cacheFriendlyWindow),
|
this._timelineSource.getTimelineEventQueries(cacheFriendlyWindow);
|
||||||
);
|
if (!eventQueries) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new EventMediaQueries(eventQueries);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _createViewWithEventMediaQuery(
|
protected async _createViewWithEventMediaQuery(
|
||||||
|
|||||||
@@ -53,12 +53,15 @@ export const createViewForEvents = async (
|
|||||||
? options.cameraIDs
|
? options.cameraIDs
|
||||||
: new Set(getAllDependentCameras(cameras, view.camera));
|
: new Set(getAllDependentCameras(cameras, view.camera));
|
||||||
|
|
||||||
const queries = cameraManager.generateDefaultEventQueries(cameraIDs, {
|
const eventQueries = cameraManager.generateDefaultEventQueries(cameraIDs, {
|
||||||
...(options?.limit && { limit: options.limit }),
|
...(options?.limit && { limit: options.limit }),
|
||||||
...(options?.mediaType === 'clips' && { hasClip: true }),
|
...(options?.mediaType === 'clips' && { hasClip: true }),
|
||||||
...(options?.mediaType === 'snapshots' && { hasSnapshot: true }),
|
...(options?.mediaType === 'snapshots' && { hasSnapshot: true }),
|
||||||
});
|
});
|
||||||
query = new EventMediaQueries(queries);
|
if (!eventQueries) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
query = new EventMediaQueries(eventQueries);
|
||||||
}
|
}
|
||||||
|
|
||||||
let queryResults: MediaQueriesResults | null;
|
let queryResults: MediaQueriesResults | null;
|
||||||
@@ -137,12 +140,16 @@ export const createViewForRecordings = async (
|
|||||||
? options.cameraIDs
|
? options.cameraIDs
|
||||||
: new Set(getAllDependentCameras(cameras, view.camera));
|
: new Set(getAllDependentCameras(cameras, view.camera));
|
||||||
|
|
||||||
const queries = cameraManager.generateDefaultRecordingQueries(cameraIDs, {
|
const recordingQueries = cameraManager.generateDefaultRecordingQueries(cameraIDs, {
|
||||||
...(options?.start && { start: options.start }),
|
...(options?.start && { start: options.start }),
|
||||||
...(options?.end && { end: options.end }),
|
...(options?.end && { end: options.end }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const query = new RecordingMediaQueries(queries);
|
if (!recordingQueries) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = new RecordingMediaQueries(recordingQueries);
|
||||||
let queryResults: MediaQueriesResults | null;
|
let queryResults: MediaQueriesResults | null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ClipsOrSnapshotsOrAll, RecordingSegment } from '../types';
|
|||||||
import { CameraManager } from '../camera/manager';
|
import { CameraManager } from '../camera/manager';
|
||||||
import { EventQuery } from '../camera/types';
|
import { EventQuery } from '../camera/types';
|
||||||
import { capEndDate, convertRangeToCacheFriendlyTimes } from '../camera/util';
|
import { capEndDate, convertRangeToCacheFriendlyTimes } from '../camera/util';
|
||||||
import { EventMediaQueries } from "../view/media-queries";
|
import { EventMediaQueries } from '../view/media-queries';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
import { compressRanges, ExpiringMemoryRangeSet, MemoryRangeSet } from '../camera/range';
|
import { compressRanges, ExpiringMemoryRangeSet, MemoryRangeSet } from '../camera/range';
|
||||||
import { errorToConsole, ModifyInterface } from './basic.js';
|
import { errorToConsole, ModifyInterface } from './basic.js';
|
||||||
@@ -85,10 +85,7 @@ export class TimelineDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async refresh(
|
public async refresh(hass: HomeAssistant, window: TimelineWindow): Promise<void> {
|
||||||
hass: HomeAssistant,
|
|
||||||
window: TimelineWindow,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this._refreshEvents(hass, window),
|
this._refreshEvents(hass, window),
|
||||||
@@ -110,7 +107,7 @@ export class TimelineDataSource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTimelineEventQueries(window: TimelineWindow): EventQuery[] {
|
public getTimelineEventQueries(window: TimelineWindow): EventQuery[] | null {
|
||||||
return this._cameraManager.generateDefaultEventQueries(this._cameraIDs, {
|
return this._cameraManager.generateDefaultEventQueries(this._cameraIDs, {
|
||||||
start: window.start,
|
start: window.start,
|
||||||
end: window.end,
|
end: window.end,
|
||||||
@@ -135,9 +132,11 @@ export class TimelineDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cacheFriendlyWindow = this.getCacheFriendlyEventWindow(window);
|
const cacheFriendlyWindow = this.getCacheFriendlyEventWindow(window);
|
||||||
const query = new EventMediaQueries(
|
const eventQueries = this.getTimelineEventQueries(cacheFriendlyWindow)
|
||||||
this.getTimelineEventQueries(cacheFriendlyWindow),
|
if (!eventQueries) {
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
const query = new EventMediaQueries(eventQueries);
|
||||||
|
|
||||||
const results = await this._cameraManager.executeMediaQueries(hass, query);
|
const results = await this._cameraManager.executeMediaQueries(hass, query);
|
||||||
for (const media of results?.getResults() ?? []) {
|
for (const media of results?.getResults() ?? []) {
|
||||||
@@ -224,7 +223,7 @@ export class TimelineDataSource {
|
|||||||
endCap: true,
|
endCap: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const queries = this._cameraManager.generateDefaultRecordingSegmentsQueries(
|
const recordingQueries = this._cameraManager.generateDefaultRecordingSegmentsQueries(
|
||||||
this._cameraIDs,
|
this._cameraIDs,
|
||||||
{
|
{
|
||||||
start: cacheFriendlyWindow.start,
|
start: cacheFriendlyWindow.start,
|
||||||
@@ -232,17 +231,22 @@ export class TimelineDataSource {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = await this._cameraManager.getRecordingSegments(hass, queries);
|
if (!recordingQueries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const results = await this._cameraManager.getRecordingSegments(hass, recordingQueries);
|
||||||
|
|
||||||
const newSegments: Map<string, RecordingSegment[]> = new Map();
|
const newSegments: Map<string, RecordingSegment[]> = new Map();
|
||||||
for (const [query, result] of results) {
|
for (const [query, result] of results) {
|
||||||
let destination: RecordingSegment[] | undefined = newSegments.get(query.cameraID);
|
for (const cameraID of query.cameraIDs) {
|
||||||
|
let destination: RecordingSegment[] | undefined = newSegments.get(cameraID);
|
||||||
if (!destination) {
|
if (!destination) {
|
||||||
destination = [];
|
destination = [];
|
||||||
newSegments.set(query.cameraID, destination);
|
newSegments.set(cameraID, destination);
|
||||||
}
|
}
|
||||||
result.segments.forEach((segment) => destination?.push(segment));
|
result.segments.forEach((segment) => destination?.push(segment));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [cameraID, segments] of newSegments.entries()) {
|
for (const [cameraID, segments] of newSegments.entries()) {
|
||||||
const existingRecordings = getExistingRecordingsForCameraID(cameraID);
|
const existingRecordings = getExistingRecordingsForCameraID(cameraID);
|
||||||
|
|||||||
Reference in New Issue
Block a user