Fetch timeline events from events endpoint not media browser.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { format } from 'date-fns';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { FrigateCardError } from '../types';
|
||||
|
||||
@@ -85,3 +86,12 @@ export function errorToConsole(e: Error, func?: CallableFunction): void {
|
||||
export const isHoverableDevice = (): boolean => window.matchMedia(
|
||||
'(hover: hover) and (pointer: fine)',
|
||||
).matches;
|
||||
|
||||
/**
|
||||
* Format a date object to RFC3339.
|
||||
* @param date A Date object.
|
||||
* @returns A date and time.
|
||||
*/
|
||||
export const formatDateAndTime = (date: Date): string => {
|
||||
return format(date, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
+164
-1
@@ -1,7 +1,21 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import utcToZonedTime from 'date-fns-tz/utcToZonedTime';
|
||||
import differenceInHours from 'date-fns/differenceInHours';
|
||||
import differenceInMinutes from 'date-fns/differenceInMinutes';
|
||||
import differenceInSeconds from 'date-fns/differenceInSeconds';
|
||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import { z } from 'zod';
|
||||
import { localize } from '../localize/localize';
|
||||
import { ExtendedHomeAssistant, FrigateCardError } from '../types';
|
||||
import {
|
||||
BrowseRecordingQueryParameters,
|
||||
ClipsOrSnapshots,
|
||||
ExtendedHomeAssistant,
|
||||
FrigateCardError,
|
||||
FrigateEvent,
|
||||
FrigateEvents,
|
||||
frigateEventsSchema,
|
||||
} from '../types';
|
||||
import { formatDateAndTime, prettifyTitle } from './basic';
|
||||
import { homeAssistantWSRequest } from './ha';
|
||||
|
||||
export const FRIGATE_ICON_SVG_PATH =
|
||||
@@ -144,3 +158,152 @@ export async function retainEvent(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface FrigateGetEventsParameters {
|
||||
instance_id?: string;
|
||||
camera?: string;
|
||||
label?: string;
|
||||
zone?: string;
|
||||
after?: number;
|
||||
before?: number;
|
||||
limit?: number;
|
||||
has_clip?: boolean;
|
||||
has_snapshot?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events over websocket. May throw.
|
||||
* @param hass The Home Assistant object.
|
||||
* @param params The events search parameters.
|
||||
* @returns An array of 'FrigateEvent's.
|
||||
*/
|
||||
export const getEvents = async (
|
||||
hass: HomeAssistant,
|
||||
params?: FrigateGetEventsParameters,
|
||||
): Promise<FrigateEvents> => {
|
||||
return await homeAssistantWSRequest(
|
||||
hass,
|
||||
frigateEventsSchema,
|
||||
{
|
||||
type: 'frigate/events/get',
|
||||
...params,
|
||||
},
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get multiple sets of events.
|
||||
* @param hass The Home Assistant object.
|
||||
* @param params A Map of parameters keyed on any key.
|
||||
* @returns A Map of key -> events.
|
||||
*/
|
||||
export const getEventsMultiple = async <T>(
|
||||
hass: HomeAssistant,
|
||||
params: Map<T, FrigateGetEventsParameters>,
|
||||
): Promise<Map<T, FrigateEvents>> => {
|
||||
const output: Map<T, FrigateEvents> = new Map();
|
||||
const getEventsAndStore = async (
|
||||
key: T,
|
||||
param: FrigateGetEventsParameters,
|
||||
): Promise<void> => {
|
||||
output.set(key, await getEvents(hass, param));
|
||||
};
|
||||
await Promise.all(
|
||||
Array.from(params).map(([key, param]) => getEventsAndStore(key, param)),
|
||||
);
|
||||
return output;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an event generate a title.
|
||||
* @param event
|
||||
*/
|
||||
export const getEventTitle = (event: FrigateEvent): string => {
|
||||
const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const durationSeconds = Math.round(
|
||||
event.end_time
|
||||
? event.end_time - event.start_time
|
||||
: Date.now() / 1000 - event.start_time,
|
||||
);
|
||||
return `${formatDateAndTime(
|
||||
utcToZonedTime(event.start_time * 1000, localTimezone),
|
||||
)} [${durationSeconds}s, ${prettifyTitle(event.label)} ${Math.round(
|
||||
event.top_score * 100,
|
||||
)}%]`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a thumbnail URL for an event.
|
||||
* @param clientId The Frigate client id.
|
||||
* @param event The event.
|
||||
* @returns A string URL.
|
||||
*/
|
||||
export const getEventThumbnailURL = (clientId: string, event: FrigateEvent): string => {
|
||||
return `/api/frigate/${clientId}/thumbnail/${event.id}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a media content ID for an event.
|
||||
* @param clientId The Frigate client id.
|
||||
* @param cameraName The Frigate camera name.
|
||||
* @param id The event id.
|
||||
* @param mediaType The media type required.
|
||||
* @returns A string media content id.
|
||||
*/
|
||||
export const getEventMediaContentID = (
|
||||
clientId: string,
|
||||
cameraName: string,
|
||||
id: string,
|
||||
mediaType: ClipsOrSnapshots,
|
||||
): string => {
|
||||
return `media-source://frigate/${clientId}/event/${mediaType}/${cameraName}/${id}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a recording identifier.
|
||||
* @param hass The HomeAssistant object.
|
||||
* @param params The recording parameters to use in the identifer.
|
||||
* @returns A recording identifier.
|
||||
*/
|
||||
export const getRecordingMediaContentID = (
|
||||
params: BrowseRecordingQueryParameters,
|
||||
): string => {
|
||||
return [
|
||||
'media-source://frigate',
|
||||
params.clientId,
|
||||
'recordings',
|
||||
`${params.year}-${String(params.month).padStart(2, '0')}`,
|
||||
String(params.day).padStart(2, '0'),
|
||||
String(params.hour).padStart(2, '0'),
|
||||
params.cameraName,
|
||||
].join('/');
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience function to convert a timestamp to hours, minutes and seconds
|
||||
* string. Heavily inspired by, and returning the same format as, the Frigate
|
||||
* UI: https://github.com/blakeblackshear/frigate/blob/master/web/src/components/RecordingPlaylist.jsx#L97
|
||||
* @param event The Frigate event.
|
||||
* @returns A duration string.
|
||||
*/
|
||||
export function getEventDurationString(event: FrigateEvent): string {
|
||||
if (!event.end_time) {
|
||||
return localize('event.in_progress');
|
||||
}
|
||||
const start = fromUnixTime(event.start_time);
|
||||
const end = fromUnixTime(event.end_time);
|
||||
const hours = differenceInHours(end, start);
|
||||
const minutes = differenceInMinutes(end, start) - hours * 60;
|
||||
const seconds = differenceInSeconds(end, start) - hours * 60 * 60 - minutes * 60;
|
||||
let duration = '';
|
||||
|
||||
if (hours) {
|
||||
duration += `${hours}h `;
|
||||
}
|
||||
if (minutes) {
|
||||
duration += `${minutes}m `;
|
||||
}
|
||||
duration += `${seconds}s`;
|
||||
return duration;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import {
|
||||
differenceInHours,
|
||||
differenceInMinutes,
|
||||
differenceInSeconds,
|
||||
fromUnixTime,
|
||||
} from 'date-fns';
|
||||
import { homeAssistantWSRequest } from '.';
|
||||
import {
|
||||
dispatchErrorMessageEvent,
|
||||
@@ -14,7 +8,6 @@ import {
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import {
|
||||
BrowseMediaQueryParameters,
|
||||
BrowseRecordingQueryParameters,
|
||||
CameraConfig,
|
||||
FrigateBrowseMediaSource,
|
||||
frigateBrowseMediaSourceSchema,
|
||||
@@ -413,16 +406,17 @@ export const createEventParentForChildren = (
|
||||
/**
|
||||
* Given a media video child with a given media_content_id.
|
||||
* @param title The title to use for the child.
|
||||
* @param media_con
|
||||
* @param mediaContentID The media content id to use for the child.
|
||||
* @param children The children media items.
|
||||
* @returns A single parent containing the children.
|
||||
*/
|
||||
export const createVideoChild = (
|
||||
export const createChild = (
|
||||
title: string,
|
||||
mediaContentID: string,
|
||||
options?: {
|
||||
thumbnail?: string;
|
||||
recording?: FrigateRecording;
|
||||
event?: FrigateEvent;
|
||||
cameraID?: string,
|
||||
},
|
||||
): FrigateBrowseMediaSource => {
|
||||
@@ -436,8 +430,11 @@ export const createVideoChild = (
|
||||
thumbnail: options?.thumbnail ?? null,
|
||||
children: null
|
||||
}
|
||||
if (options?.recording || options?.cameraID) {
|
||||
if (options?.recording || options?.cameraID || options?.event) {
|
||||
result.frigate = {}
|
||||
if (options?.event) {
|
||||
result.frigate.event = options.event;
|
||||
}
|
||||
if (options?.recording) {
|
||||
result.frigate.recording = options.recording;
|
||||
}
|
||||
@@ -447,51 +444,3 @@ export const createVideoChild = (
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience function to convert a timestamp to hours, minutes and seconds
|
||||
* string. Heavily inspired by, and returning the same format as, the Frigate
|
||||
* UI: https://github.com/blakeblackshear/frigate/blob/master/web/src/components/RecordingPlaylist.jsx#L97
|
||||
* @param event The Frigate event.
|
||||
* @returns A duration string.
|
||||
*/
|
||||
export function getEventDurationString(event: FrigateEvent): string {
|
||||
if (!event.end_time) {
|
||||
return localize('event.in_progress');
|
||||
}
|
||||
const start = fromUnixTime(event.start_time);
|
||||
const end = fromUnixTime(event.end_time);
|
||||
const hours = differenceInHours(end, start);
|
||||
const minutes = differenceInMinutes(end, start) - hours * 60;
|
||||
const seconds = differenceInSeconds(end, start) - hours * 60 * 60 - minutes * 60;
|
||||
let duration = '';
|
||||
|
||||
if (hours) {
|
||||
duration += `${hours}h `;
|
||||
}
|
||||
if (minutes) {
|
||||
duration += `${minutes}m `;
|
||||
}
|
||||
duration += `${seconds}s`;
|
||||
return duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a recording identifier.
|
||||
* @param hass The HomeAssistant object.
|
||||
* @param params The recording parameters to use in the identifer.
|
||||
* @returns A recording identifier.
|
||||
*/
|
||||
export const generateRecordingIdentifier = (
|
||||
params: BrowseRecordingQueryParameters,
|
||||
): string => {
|
||||
return [
|
||||
'media-source://frigate',
|
||||
params.clientId,
|
||||
'recordings',
|
||||
`${params.year}-${String(params.month).padStart(2, '0')}`,
|
||||
String(params.day).padStart(2, '0'),
|
||||
String(params.hour).padStart(2, '0'),
|
||||
params.cameraName,
|
||||
].join('/');
|
||||
};
|
||||
|
||||
@@ -3,29 +3,26 @@ import { DataSet, DataView } from 'vis-data/esnext';
|
||||
import { IdType, TimelineItem } from 'vis-timeline/esnext';
|
||||
import { CAMERA_BIRDSEYE } from '../const.js';
|
||||
import {
|
||||
BrowseMediaQueryParameters,
|
||||
CameraConfig,
|
||||
ExtendedHomeAssistant,
|
||||
FrigateBrowseMediaSource,
|
||||
FrigateCardError,
|
||||
FrigateEvent,
|
||||
FrigateEvents,
|
||||
} from '../types.js';
|
||||
import { errorToConsole } from '../utils/basic.js';
|
||||
import {
|
||||
FrigateGetEventsParameters,
|
||||
getEventsMultiple,
|
||||
getRecordingSegments,
|
||||
getRecordingsSummary,
|
||||
RecordingSegments,
|
||||
RecordingSummary,
|
||||
} from './frigate.js';
|
||||
import {
|
||||
getBrowseMediaQueryParameters,
|
||||
isTrueMedia,
|
||||
multipleBrowseMediaQuery,
|
||||
} from './ha/browse-media.js';
|
||||
import { dispatchFrigateCardErrorEvent } from '../components/message.js';
|
||||
|
||||
const RECORDING_SEGMENT_TOLERANCE = 60;
|
||||
const TIMELINE_DATA_MANAGER_MAX_AGE_SECONDS = 10;
|
||||
const TIMELINE_DATA_MANAGER_MAX_FETCH_COUNT = 10000;
|
||||
|
||||
export interface FrigateCardTimelineItem extends TimelineItem {
|
||||
// DataView has issues using datasets with Date objects, so avoid them and use
|
||||
@@ -33,7 +30,6 @@ export interface FrigateCardTimelineItem extends TimelineItem {
|
||||
start: number;
|
||||
end?: number;
|
||||
event?: FrigateEvent;
|
||||
source?: FrigateBrowseMediaSource;
|
||||
}
|
||||
|
||||
type TimelineMediaType = 'all' | 'clips' | 'snapshots';
|
||||
@@ -126,23 +122,27 @@ export class TimelineDataManager {
|
||||
* Create a dataview for a given set of camera.
|
||||
* @param cameraIDs The cameraIDs to include.
|
||||
* @param showRecordings Whether or not to show recordings.
|
||||
* @returns
|
||||
* @returns A dataview.
|
||||
*/
|
||||
public createDataView(
|
||||
cameraIDs: Set<string>,
|
||||
showRecordings: boolean,
|
||||
mediaType: TimelineMediaType,
|
||||
): DataView<FrigateCardTimelineItem> {
|
||||
return new DataView(this._dataset, {
|
||||
filter: (item: FrigateCardTimelineItem) =>
|
||||
!!item.group &&
|
||||
cameraIDs.has(String(item.group)) &&
|
||||
(showRecordings || item.type !== 'background'),
|
||||
(showRecordings || item.type !== 'background') &&
|
||||
(mediaType === 'all' ||
|
||||
(mediaType === 'clips' && !!item.event?.has_clip) ||
|
||||
(mediaType === 'snapshots' && !!item.event?.has_snapshot)),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dataview for segments.
|
||||
* @returns
|
||||
* @returns A dataview.
|
||||
*/
|
||||
public createSegmentDataView(): DataView<RecordingSegmentsItem> {
|
||||
return new DataView(this._recordingSegments);
|
||||
@@ -171,51 +171,22 @@ export class TimelineDataManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a FrigateBrowseMediaSource object to the managed timeline.
|
||||
* @param cameraID The id the camera this object is from.
|
||||
* @param target The FrigateBrowseMediaSource to add.
|
||||
* Add events for the given camera.
|
||||
* @param cameraID The camera ID.
|
||||
* @param events The array of events.
|
||||
*/
|
||||
protected _addMediaSource(target: FrigateBrowseMediaSource): void {
|
||||
const items: FrigateCardTimelineItem[] = [];
|
||||
target.children?.forEach((child) => {
|
||||
const event = child.frigate?.event;
|
||||
const cameraID = child.frigate?.cameraID;
|
||||
if (
|
||||
cameraID &&
|
||||
event &&
|
||||
isTrueMedia(child) &&
|
||||
['video', 'image'].includes(child.media_content_type)
|
||||
) {
|
||||
let item = this._dataset.get(event.id);
|
||||
if (!item) {
|
||||
item = {
|
||||
id: event.id,
|
||||
group: cameraID,
|
||||
content: '',
|
||||
start: event.start_time * 1000,
|
||||
event: event,
|
||||
};
|
||||
}
|
||||
if (
|
||||
(child.media_content_type === 'video' &&
|
||||
['all', 'clips'].includes(this._mediaType)) ||
|
||||
(!item.source &&
|
||||
child.media_content_type === 'image' &&
|
||||
['all', 'snapshots'].includes(this._mediaType))
|
||||
) {
|
||||
item.source = child;
|
||||
}
|
||||
if (event.end_time) {
|
||||
item['end'] = event.end_time * 1000;
|
||||
item['type'] = 'range';
|
||||
} else {
|
||||
item['type'] = 'point';
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
this._dataset.update(items);
|
||||
protected _addEvents(cameraID: string, events: FrigateEvents): void {
|
||||
this._dataset.update(
|
||||
events.map((event) => ({
|
||||
id: event.id,
|
||||
group: cameraID,
|
||||
content: '',
|
||||
event: event,
|
||||
start: event.start_time * 1000,
|
||||
type: event.end_time ? 'range' : 'point',
|
||||
...(event.end_time && { end: event.end_time * 1000 }),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -485,37 +456,31 @@ export class TimelineDataManager {
|
||||
start: Date,
|
||||
end: Date,
|
||||
): Promise<void> {
|
||||
const params: BrowseMediaQueryParameters[] = [];
|
||||
const params: Map<string, FrigateGetEventsParameters> = new Map();
|
||||
|
||||
this._cameras.forEach((cameraConfig, cameraID) => {
|
||||
(this._mediaType === 'all' ? ['clips', 'snapshots'] : [this._mediaType]).forEach(
|
||||
(mediaType) => {
|
||||
if (cameraConfig?.frigate.camera_name !== CAMERA_BIRDSEYE) {
|
||||
const param = getBrowseMediaQueryParameters(hass, cameraID, cameraConfig, {
|
||||
before: end.getTime() / 1000,
|
||||
after: start.getTime() / 1000,
|
||||
unlimited: true,
|
||||
mediaType: mediaType as 'clips' | 'snapshots',
|
||||
});
|
||||
if (param) {
|
||||
params.push(param);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
if (
|
||||
cameraConfig.frigate.camera_name &&
|
||||
cameraConfig.frigate.camera_name !== CAMERA_BIRDSEYE
|
||||
) {
|
||||
params.set(cameraID, {
|
||||
instance_id: cameraConfig.frigate.client_id,
|
||||
camera: cameraConfig.frigate.camera_name,
|
||||
...(cameraConfig.frigate.label && { label: cameraConfig.frigate.label }),
|
||||
...(cameraConfig.frigate.zone && { label: cameraConfig.frigate.zone }),
|
||||
before: Math.floor(end.getTime() / 1000),
|
||||
after: Math.floor(start.getTime() / 1000),
|
||||
limit: TIMELINE_DATA_MANAGER_MAX_FETCH_COUNT,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!params.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let results: Map<BrowseMediaQueryParameters, FrigateBrowseMediaSource>;
|
||||
let results: Map<string, FrigateEvents>;
|
||||
try {
|
||||
results = await multipleBrowseMediaQuery(hass, params);
|
||||
results = await getEventsMultiple(hass, params);
|
||||
} catch (e) {
|
||||
return dispatchFrigateCardErrorEvent(element, e as FrigateCardError);
|
||||
}
|
||||
for (const result of results.values()) {
|
||||
this._addMediaSource(result);
|
||||
}
|
||||
results.forEach((params, cameraID) => this._addEvents(cameraID, params));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user