Fetch metadata from engine.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
// ===========
|
||||
|
||||
Reference in New Issue
Block a user