Fetch metadata from engine.

This commit is contained in:
Dermot Duffy
2023-01-26 21:55:07 -08:00
parent bd19ed7a74
commit 67b134308a
10 changed files with 270 additions and 51 deletions
+8 -1
View File
@@ -62,6 +62,13 @@ export class CameraManagerEngineFactory {
}
output.get(engine)?.add(cameraID);
}
return output;
return output.size ? output : null;
}
public getAllEngines(
cameras: Map<string, CameraConfig>,
): CameraManagerEngine[] | null {
const engines = this.getEnginesForCameraIDs(cameras, new Set(cameras.keys()));
return engines ? [...engines.keys()] : null;
}
}
+7 -3
View File
@@ -5,6 +5,7 @@ import {
DataQuery,
EventQuery,
EventQueryResultsMap,
MediaMetadata,
PartialEventQuery,
PartialRecordingQuery,
PartialRecordingSegmentsQuery,
@@ -75,9 +76,7 @@ export interface CameraManagerEngine {
favorite: boolean,
): Promise<void>;
getQueryResultMaxAge(
query: DataQuery
): number | null;
getQueryResultMaxAge(query: DataQuery): number | null;
getMediaSeekTime(
hass: HomeAssistant,
@@ -85,4 +84,9 @@ export interface CameraManagerEngine {
media: ViewMedia,
target: Date,
): Promise<number | null>;
getMediaMetadata(
hass: HomeAssistant,
cameras: Map<string, CameraConfig>,
): Promise<MediaMetadata | null>;
}
+59 -4
View File
@@ -20,6 +20,7 @@ import {
FrigateEventQueryResults,
FrigateRecordingQueryResults,
FrigateRecordingSegmentsQueryResults,
MediaMetadata,
PartialEventQuery,
PartialRecordingQuery,
PartialRecordingSegmentsQuery,
@@ -36,6 +37,7 @@ import {
import { FrigateRecording } from './types';
import {
getEvents,
getEventSummary,
getRecordingSegments,
getRecordingsSummary,
NativeFrigateEventQuery,
@@ -44,7 +46,7 @@ import {
} from './requests';
import orderBy from 'lodash-es/orderBy';
import throttle from 'lodash-es/throttle';
import { runWhenIdleIfSupported } from '../../utils/basic';
import { allPromises, runWhenIdleIfSupported } from '../../utils/basic';
import { fromUnixTime } from 'date-fns';
import { sum } from 'lodash-es';
import { FrigateViewMediaClassifier } from './media-classifier';
@@ -214,10 +216,10 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
protected _buildInstanceToCameraIDMapFromQuery(
cameras: Map<string, CameraConfig>,
query: DataQuery,
cameraIDs: Set<string>,
): Map<string, Set<string>> {
const output: Map<string, Set<string>> = new Map();
for (const cameraID of query.cameraIDs) {
for (const cameraID of cameraIDs) {
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
const clientID = cameraConfig?.frigate.client_id;
if (clientID) {
@@ -295,7 +297,10 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
// 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);
const instances = this._buildInstanceToCameraIDMapFromQuery(
cameras,
query.cameraIDs,
);
await Promise.all(
Array.from(instances.keys()).map((instanceID) =>
@@ -611,6 +616,56 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
return cameraConfig;
}
public async getMediaMetadata(
hass: HomeAssistant,
cameras: Map<string, CameraConfig>,
): Promise<MediaMetadata | null> {
const what: Set<string> = new Set();
const where: Set<string> = new Set();
const days: Set<string> = new Set();
const instances = this._buildInstanceToCameraIDMapFromQuery(
cameras,
new Set(cameras.keys()),
);
const processQuery = async (
instanceID: string,
cameraIDs: Set<string>,
): Promise<void> => {
const cameraNames = this._getFrigateCameraNamesForCameraIDs(cameras, cameraIDs);
for (const entry of await getEventSummary(hass, instanceID)) {
if (!cameraNames.has(entry.camera)) {
// If this entry applies to a camera that *is* in this Frigate
// instance, but is *not* a configured camera in the card, skip it.
continue;
}
if (entry.label) {
what.add(entry.label);
}
if (entry.zones.length) {
entry.zones.forEach(where.add, where);
}
if (entry.day) {
days.add(entry.day);
}
}
};
await allPromises([...instances.entries()], ([instanceID, cameraIDs]) =>
processQuery(instanceID, cameraIDs),
);
if (!what.size && !where.size && !days.size) {
return null;
}
return {
...(what.size && { what: what }),
...(where.size && { where: where }),
...(days.size && { days: days }),
};
}
/**
* Garbage collect recording segments that no longer feature in the recordings
* returned by the Frigate backend.
+34 -11
View File
@@ -1,22 +1,29 @@
import { HomeAssistant } from 'custom-card-helpers';
import { localize } from '../../localize/localize';
import {
FrigateCardError,
RecordingSegment,
} from '../../types';
import { FrigateCardError, RecordingSegment } from '../../types';
import { homeAssistantWSRequest } from '../../utils/ha';
import { FrigateEvent, frigateEventsSchema, recordingSegmentsSchema, RecordingSummary, recordingSummarySchema, RetainResult, retainResultSchema } from './types';
import {
EventSummary,
eventSummarySchema,
FrigateEvent,
frigateEventsSchema,
recordingSegmentsSchema,
RecordingSummary,
recordingSummarySchema,
RetainResult,
retainResultSchema,
} from './types';
/**
* Get the recordings summary. May throw.
* @param hass The Home Assistant object.
* @param client_id The Frigate client_id.
* @param clientID The Frigate clientID.
* @param camera_name The Frigate camera name.
* @returns A RecordingSummary object.
*/
export const getRecordingsSummary = async (
hass: HomeAssistant,
client_id: string,
clientID: string,
camera_name: string,
): Promise<RecordingSummary> => {
return await homeAssistantWSRequest(
@@ -24,7 +31,7 @@ export const getRecordingsSummary = async (
recordingSummarySchema,
{
type: 'frigate/recordings/summary',
instance_id: client_id,
instance_id: clientID,
camera: camera_name,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
@@ -63,19 +70,19 @@ export const getRecordingSegments = async (
/**
* Request that Frigate retain an event. May throw.
* @param hass The HomeAssistant object.
* @param client_id The Frigate client_id.
* @param clientID The Frigate clientID.
* @param eventID The event ID to retain.
* @param retain `true` to retain or `false` to unretain.
*/
export async function retainEvent(
hass: HomeAssistant,
client_id: string,
clientID: string,
eventID: string,
retain: boolean,
): Promise<void> {
const retainRequest = {
type: 'frigate/event/retain',
instance_id: client_id,
instance_id: clientID,
event_id: eventID,
retain: retain,
};
@@ -126,3 +133,19 @@ export const getEvents = async (
true,
);
};
export const getEventSummary = async (
hass: HomeAssistant,
clientID: string,
): Promise<EventSummary> => {
return await homeAssistantWSRequest(
hass,
eventSummarySchema,
{
type: 'frigate/events/summary',
instance_id: clientID,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
true,
);
};
+17 -6
View File
@@ -1,4 +1,9 @@
import { z } from 'zod';
import { dayToDate } from '../../utils/basic';
const dayStringToDate = (arg: unknown): Date | unknown => {
return typeof arg === 'string' ? dayToDate(arg) : arg;
};
export const eventSchema = z.object({
camera: z.string(),
@@ -25,11 +30,7 @@ const recordingSummaryHourSchema = z.object({
export const recordingSummarySchema = z
.object({
day: z.preprocess((arg) => {
// Must provide the hour:minute:second on parsing or Javascript will
// assume *UTC* midnight.
return typeof arg === 'string' ? new Date(`${arg}T00:00:00`) : arg;
}, z.date()),
day: z.preprocess(dayStringToDate, z.date()),
events: z.number(),
hours: recordingSummaryHourSchema.array(),
})
@@ -54,4 +55,14 @@ export interface FrigateRecording {
startTime: Date;
endTime: Date;
events: number;
}
}
export const eventSummarySchema = z
.object({
camera: z.string(),
day: z.string(),
label: z.string(),
zones: z.string().array(),
})
.array();
export type EventSummary = z.infer<typeof eventSummarySchema>;
+41 -1
View File
@@ -1,11 +1,12 @@
import { HomeAssistant } from 'custom-card-helpers';
import { CameraConfig } from '../types.js';
import { arrayify, setify } from '../utils/basic.js';
import { allPromises, arrayify, setify } from '../utils/basic.js';
import {
DataQuery,
EventQuery,
EventQueryResults,
EventQueryResultsMap,
MediaMetadata,
MediaQuery,
PartialDataQuery,
PartialEventQuery,
@@ -115,6 +116,45 @@ export class CameraManager {
});
}
public async getMediaMetadata(
hass: HomeAssistant,
): Promise<MediaMetadata | null> {
const what: Set<string> = new Set();
const where: Set<string> = new Set();
const days: Set<string> = new Set();
const engines = this._engineFactory.getAllEngines(this._cameras);
if (!engines) {
return null;
}
const processMetadata = async (engine: CameraManagerEngine): Promise<void> => {
const engineMetadata = await engine.getMediaMetadata(hass, this._cameras);
if (engineMetadata) {
if (engineMetadata.what) {
engineMetadata.what.forEach(what.add, what);
}
if (engineMetadata.where) {
engineMetadata.where.forEach(where.add, where);
}
if (engineMetadata.days) {
engineMetadata.days.forEach(days.add, days);
}
}
}
await allPromises(engines, (engine) => processMetadata(engine));
if (!what.size && !where.size && !days.size) {
return null;
}
return {
...(what.size && { what: what }),
...(where.size && { where: where }),
...(days.size && { days: days }),
}
}
protected _generateDefaultQueries<PQT extends PartialDataQuery>(
cameraIDs: string | Set<string>,
partialQuery: PQT,
+6
View File
@@ -68,6 +68,12 @@ export type EventQueryResultsMap = ResultsMap<EventQuery>;
export type RecordingQueryResultsMap = ResultsMap<RecordingQuery>;
export type RecordingSegmentsQueryResultsMap = ResultsMap<RecordingSegmentsQuery>;
export interface MediaMetadata {
where?: Set<string>;
what?: Set<string>;
days?: Set<string>;
}
// ===========
// Event Query
// ===========