Fetch timeline events from events endpoint not media browser.

This commit is contained in:
Dermot Duffy
2022-09-24 11:31:03 -07:00
parent 0491018aab
commit a2e80d93a3
13 changed files with 300 additions and 165 deletions
+1
View File
@@ -23,6 +23,7 @@
"crypto": "^1.0.1", "crypto": "^1.0.1",
"custom-card-helpers": "^1.9.0", "custom-card-helpers": "^1.9.0",
"date-fns": "^2.29.2", "date-fns": "^2.29.2",
"date-fns-tz": "^1.3.7",
"embla-carousel": "^7.0.2", "embla-carousel": "^7.0.2",
"embla-carousel-wheel-gestures": "^3.0.0", "embla-carousel-wheel-gestures": "^3.0.0",
"home-assistant-js-websocket": "^8.0.0", "home-assistant-js-websocket": "^8.0.0",
+1
View File
@@ -216,6 +216,7 @@ export class FrigateCardLive extends LitElement {
html`<frigate-card-surround html`<frigate-card-surround
.hass=${this.hass} .hass=${this.hass}
.view=${this.view} .view=${this.view}
.fetch=${true}
.thumbnailConfig=${config.controls.thumbnails} .thumbnailConfig=${config.controls.thumbnails}
.timelineConfig=${config.controls.timeline} .timelineConfig=${config.controls.timeline}
.browseMediaParams=${browseMediaParams ?? undefined} .browseMediaParams=${browseMediaParams ?? undefined}
+6 -2
View File
@@ -32,7 +32,7 @@ import './surround-basic.js';
import './timeline-core.js'; import './timeline-core.js';
interface ThumbnailViewContext { interface ThumbnailViewContext {
// Whetherr or not to fetch thumbnails. // Whether or not to fetch thumbnails.
fetch?: boolean; fetch?: boolean;
} }
@@ -59,6 +59,9 @@ export class FrigateCardSurround extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public inBackground?: boolean; public inBackground?: boolean;
@property({ attribute: false })
public fetch = false;
@property({ attribute: false, hasChanged: contentsChanged }) @property({ attribute: false, hasChanged: contentsChanged })
public browseMediaParams?: BrowseMediaQueryParameters | BrowseMediaQueryParameters[]; public browseMediaParams?: BrowseMediaQueryParameters | BrowseMediaQueryParameters[];
@@ -76,12 +79,13 @@ export class FrigateCardSurround extends LitElement {
*/ */
protected async _fetchMedia(): Promise<void> { protected async _fetchMedia(): Promise<void> {
if ( if (
!this.fetch ||
this.inBackground || this.inBackground ||
!this.hass || !this.hass ||
!this.view || !this.view ||
this.view.target ||
!this.thumbnailConfig || !this.thumbnailConfig ||
this.thumbnailConfig.mode === 'none' || this.thumbnailConfig.mode === 'none' ||
this.view.target ||
!this.browseMediaParams || !this.browseMediaParams ||
!(this.view.context?.thumbnails?.fetch ?? true) !(this.view.context?.thumbnails?.fetch ?? true)
) { ) {
+1 -1
View File
@@ -11,7 +11,7 @@ import thumbnailStyle from '../scss/thumbnail.scss';
import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
import { errorToConsole, prettifyTitle } from '../utils/basic.js'; import { errorToConsole, prettifyTitle } from '../utils/basic.js';
import { retainEvent } from '../utils/frigate.js'; import { retainEvent } from '../utils/frigate.js';
import { getEventDurationString } from '../utils/ha/browse-media.js'; import { getEventDurationString } from '../utils/frigate.js';
import { renderTask } from '../utils/task.js'; import { renderTask } from '../utils/task.js';
import { createFetchThumbnailTask } from '../utils/thumbnail.js'; import { createFetchThumbnailTask } from '../utils/thumbnail.js';
import { View } from '../view.js'; import { View } from '../view.js';
+56 -22
View File
@@ -47,16 +47,19 @@ import { stopEventFromActivatingCardWideActions } from '../utils/action';
import { import {
contentsChanged, contentsChanged,
dispatchFrigateCardEvent, dispatchFrigateCardEvent,
formatDateAndTime,
isHoverableDevice, isHoverableDevice,
prettifyTitle, prettifyTitle,
} from '../utils/basic'; } from '../utils/basic';
import { getAllDependentCameras, getCameraTitle } from '../utils/camera.js'; import { getAllDependentCameras, getCameraTitle } from '../utils/camera.js';
import { import {
createEventParentForChildren, getEventMediaContentID,
createVideoChild, getEventThumbnailURL,
generateRecordingIdentifier, getEventTitle,
} from '../utils/ha/browse-media'; getRecordingMediaContentID,
} from '../utils/frigate';
import { createEventParentForChildren, createChild } from '../utils/ha/browse-media';
import { import {
FrigateCardTimelineItem, FrigateCardTimelineItem,
RecordingSegmentsItem, RecordingSegmentsItem,
@@ -216,16 +219,17 @@ export class FrigateCardTimelineCore extends LitElement {
* @returns The tooltip as a string to render. * @returns The tooltip as a string to render.
*/ */
protected _getTooltip(item: TimelineItem): string { protected _getTooltip(item: TimelineItem): string {
const source = (<FrigateCardTimelineItem>item).source; const event = (<FrigateCardTimelineItem>item).event;
if (!this._isHoverableDevice || !source) { const clientId = item.group
? this.cameras?.get(String(item.group))?.frigate.client_id
: null;
if (!this._isHoverableDevice || !event || !clientId) {
// Don't display tooltips on touch devices, they just get in the way of // Don't display tooltips on touch devices, they just get in the way of
// the drawer. // the drawer.
return ''; return '';
} }
const eventAttr = source.frigate?.event const eventAttr = `event='${JSON.stringify(event)}'`;
? `event='${JSON.stringify(source.frigate.event)}'`
: '';
const detailsAttr = this.thumbnailDetails ? 'details' : ''; const detailsAttr = this.thumbnailDetails ? 'details' : '';
// Cannot use Lit data-bindings as visjs requires a string for tooltips. // Cannot use Lit data-bindings as visjs requires a string for tooltips.
@@ -233,10 +237,10 @@ export class FrigateCardTimelineCore extends LitElement {
// whitelist in `_getOptions()` . // whitelist in `_getOptions()` .
return ` return `
<frigate-card-timeline-thumbnail <frigate-card-timeline-thumbnail
thumbnail="${source.thumbnail}" thumbnail="${getEventThumbnailURL(clientId, event)}"
${detailsAttr} ${detailsAttr}
${eventAttr} ${eventAttr}
label="${source.title}" label="${getEventTitle(event)}"
> >
</frigate-card-timeline-thumbnail>`; </frigate-card-timeline-thumbnail>`;
} }
@@ -324,12 +328,9 @@ export class FrigateCardTimelineCore extends LitElement {
// hours, otherwise only show the matching hour from all cameras. // hours, otherwise only show the matching hour from all cameras.
if (!onlyShowMatchingHour || isMatchingHour) { if (!onlyShowMatchingHour || isMatchingHour) {
children.push( children.push(
createVideoChild( createChild(
`${prettifyTitle(config.frigate.camera_name)} ${format( `${prettifyTitle(config.frigate.camera_name)} ${formatDateAndTime(hour)}`,
hour, getRecordingMediaContentID({
'yyyy-MM-dd HH:mm',
)}`,
generateRecordingIdentifier({
clientId: config.frigate.client_id, clientId: config.frigate.client_id,
year: dayData.day.getFullYear(), year: dayData.day.getFullYear(),
month: dayData.day.getMonth() + 1, month: dayData.day.getMonth() + 1,
@@ -812,9 +813,40 @@ export class FrigateCardTimelineCore extends LitElement {
let childIndex = -1; let childIndex = -1;
const children: FrigateBrowseMediaSource[] = []; const children: FrigateBrowseMediaSource[] = [];
this._dataview?.get({ order: sortTimelineItemsYoungestToOldest }).forEach((item) => { this._dataview?.get({ order: sortTimelineItemsYoungestToOldest }).forEach((item) => {
if (item.event && item.source) { const cameraID = item.group ? String(item.group) : null;
children.push(item.source); const cameraConfig = cameraID ? this.cameras?.get(cameraID) : null;
if (selected.includes(item.event.id)) { const event = item.event;
const media =
event?.has_clip && this.timelineConfig?.media !== 'snapshots'
? 'clips'
: event?.has_snapshot
? 'snapshots'
: null;
if (
cameraID &&
cameraConfig &&
event &&
media &&
cameraConfig.frigate.camera_name
) {
children.push(
createChild(
getEventTitle(event),
getEventMediaContentID(
cameraConfig.frigate.client_id,
cameraConfig.frigate.camera_name,
event.id,
media,
),
{
thumbnail: getEventThumbnailURL(cameraConfig.frigate.client_id, event),
event: event,
cameraID: cameraID,
},
),
);
if (selected.includes(event.id)) {
childIndex = children.length - 1; childIndex = children.length - 1;
} }
} }
@@ -1190,6 +1222,7 @@ export class FrigateCardTimelineCore extends LitElement {
this.timelineDataManager && this.timelineDataManager &&
this._refTimeline.value && this._refTimeline.value &&
options && options &&
this.timelineConfig &&
(changedProperties.has('timelineConfig') || (changedProperties.has('timelineConfig') ||
(this.mini && (this.mini &&
changedProperties.has('view') && changedProperties.has('view') &&
@@ -1212,7 +1245,8 @@ export class FrigateCardTimelineCore extends LitElement {
this._dataview = this.timelineDataManager.createDataView( this._dataview = this.timelineDataManager.createDataView(
this._getTimelineCameraIDs(), this._getTimelineCameraIDs(),
!!this.timelineConfig?.show_recordings, !!this.timelineConfig.show_recordings,
this.timelineConfig.media,
); );
if (this.mini && groups.length === 1) { if (this.mini && groups.length === 1) {
+1 -1
View File
@@ -1,7 +1,6 @@
// TODO: When a media viewer is first loaded the selected child won't work (because the underlying carousel has not yet rendered) // TODO: When a media viewer is first loaded the selected child won't work (because the underlying carousel has not yet rendered)
// TODO: delete segments if not in summary? is this actually necessary? could it create gaps in data? better off stopping access via summary? // TODO: delete segments if not in summary? is this actually necessary? could it create gaps in data? better off stopping access via summary?
// TODO: support filtering created dataviews by recordings or mediatype (so storage )
// TODO: Make minitimeline configurable in the editor // TODO: Make minitimeline configurable in the editor
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
@@ -49,6 +48,7 @@ export class FrigateCardTimeline extends LitElement {
.view=${this.view} .view=${this.view}
.thumbnailConfig=${this.timelineConfig.controls.thumbnails} .thumbnailConfig=${this.timelineConfig.controls.thumbnails}
.cameras=${this.cameras} .cameras=${this.cameras}
.fetch=${false}
> >
<frigate-card-timeline-core <frigate-card-timeline-core
.hass=${this.hass} .hass=${this.hass}
+1
View File
@@ -137,6 +137,7 @@ export class FrigateCardViewer extends LitElement {
return html` <frigate-card-surround return html` <frigate-card-surround
.hass=${this.hass} .hass=${this.hass}
.view=${this.view} .view=${this.view}
.fetch=${false}
.thumbnailConfig=${this.viewerConfig.controls.thumbnails} .thumbnailConfig=${this.viewerConfig.controls.thumbnails}
.timelineConfig=${this.viewerConfig.controls.timeline} .timelineConfig=${this.viewerConfig.controls.timeline}
.timelineDataManager=${this.timelineDataManager} .timelineDataManager=${this.timelineDataManager}
+2
View File
@@ -26,6 +26,8 @@ export const THUMBNAIL_WIDTH_MIN = 75;
* Internal types. * Internal types.
*/ */
export type ClipsOrSnapshots = 'clips' | 'snapshots';
export const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [ export const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
'live', 'live',
'clip', 'clip',
+10
View File
@@ -1,3 +1,4 @@
import { format } from 'date-fns';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { FrigateCardError } from '../types'; import { FrigateCardError } from '../types';
@@ -85,3 +86,12 @@ export function errorToConsole(e: Error, func?: CallableFunction): void {
export const isHoverableDevice = (): boolean => window.matchMedia( export const isHoverableDevice = (): boolean => window.matchMedia(
'(hover: hover) and (pointer: fine)', '(hover: hover) and (pointer: fine)',
).matches; ).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
View File
@@ -1,7 +1,21 @@
import { HomeAssistant } from 'custom-card-helpers'; 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 { z } from 'zod';
import { localize } from '../localize/localize'; 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'; import { homeAssistantWSRequest } from './ha';
export const FRIGATE_ICON_SVG_PATH = 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;
}
+7 -58
View File
@@ -1,10 +1,4 @@
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import {
differenceInHours,
differenceInMinutes,
differenceInSeconds,
fromUnixTime,
} from 'date-fns';
import { homeAssistantWSRequest } from '.'; import { homeAssistantWSRequest } from '.';
import { import {
dispatchErrorMessageEvent, dispatchErrorMessageEvent,
@@ -14,7 +8,6 @@ import {
import { localize } from '../../localize/localize.js'; import { localize } from '../../localize/localize.js';
import { import {
BrowseMediaQueryParameters, BrowseMediaQueryParameters,
BrowseRecordingQueryParameters,
CameraConfig, CameraConfig,
FrigateBrowseMediaSource, FrigateBrowseMediaSource,
frigateBrowseMediaSourceSchema, frigateBrowseMediaSourceSchema,
@@ -413,16 +406,17 @@ export const createEventParentForChildren = (
/** /**
* Given a media video child with a given media_content_id. * Given a media video child with a given media_content_id.
* @param title The title to use for the child. * @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. * @param children The children media items.
* @returns A single parent containing the children. * @returns A single parent containing the children.
*/ */
export const createVideoChild = ( export const createChild = (
title: string, title: string,
mediaContentID: string, mediaContentID: string,
options?: { options?: {
thumbnail?: string; thumbnail?: string;
recording?: FrigateRecording; recording?: FrigateRecording;
event?: FrigateEvent;
cameraID?: string, cameraID?: string,
}, },
): FrigateBrowseMediaSource => { ): FrigateBrowseMediaSource => {
@@ -436,8 +430,11 @@ export const createVideoChild = (
thumbnail: options?.thumbnail ?? null, thumbnail: options?.thumbnail ?? null,
children: null children: null
} }
if (options?.recording || options?.cameraID) { if (options?.recording || options?.cameraID || options?.event) {
result.frigate = {} result.frigate = {}
if (options?.event) {
result.frigate.event = options.event;
}
if (options?.recording) { if (options?.recording) {
result.frigate.recording = options.recording; result.frigate.recording = options.recording;
} }
@@ -447,51 +444,3 @@ export const createVideoChild = (
} }
return result; 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('/');
};
+45 -80
View File
@@ -3,29 +3,26 @@ import { DataSet, DataView } from 'vis-data/esnext';
import { IdType, TimelineItem } from 'vis-timeline/esnext'; import { IdType, TimelineItem } from 'vis-timeline/esnext';
import { CAMERA_BIRDSEYE } from '../const.js'; import { CAMERA_BIRDSEYE } from '../const.js';
import { import {
BrowseMediaQueryParameters,
CameraConfig, CameraConfig,
ExtendedHomeAssistant, ExtendedHomeAssistant,
FrigateBrowseMediaSource,
FrigateCardError, FrigateCardError,
FrigateEvent, FrigateEvent,
FrigateEvents,
} from '../types.js'; } from '../types.js';
import { errorToConsole } from '../utils/basic.js'; import { errorToConsole } from '../utils/basic.js';
import { import {
FrigateGetEventsParameters,
getEventsMultiple,
getRecordingSegments, getRecordingSegments,
getRecordingsSummary, getRecordingsSummary,
RecordingSegments, RecordingSegments,
RecordingSummary, RecordingSummary,
} from './frigate.js'; } from './frigate.js';
import {
getBrowseMediaQueryParameters,
isTrueMedia,
multipleBrowseMediaQuery,
} from './ha/browse-media.js';
import { dispatchFrigateCardErrorEvent } from '../components/message.js'; import { dispatchFrigateCardErrorEvent } from '../components/message.js';
const RECORDING_SEGMENT_TOLERANCE = 60; const RECORDING_SEGMENT_TOLERANCE = 60;
const TIMELINE_DATA_MANAGER_MAX_AGE_SECONDS = 10; const TIMELINE_DATA_MANAGER_MAX_AGE_SECONDS = 10;
const TIMELINE_DATA_MANAGER_MAX_FETCH_COUNT = 10000;
export interface FrigateCardTimelineItem extends TimelineItem { export interface FrigateCardTimelineItem extends TimelineItem {
// DataView has issues using datasets with Date objects, so avoid them and use // DataView has issues using datasets with Date objects, so avoid them and use
@@ -33,7 +30,6 @@ export interface FrigateCardTimelineItem extends TimelineItem {
start: number; start: number;
end?: number; end?: number;
event?: FrigateEvent; event?: FrigateEvent;
source?: FrigateBrowseMediaSource;
} }
type TimelineMediaType = 'all' | 'clips' | 'snapshots'; type TimelineMediaType = 'all' | 'clips' | 'snapshots';
@@ -126,23 +122,27 @@ export class TimelineDataManager {
* Create a dataview for a given set of camera. * Create a dataview for a given set of camera.
* @param cameraIDs The cameraIDs to include. * @param cameraIDs The cameraIDs to include.
* @param showRecordings Whether or not to show recordings. * @param showRecordings Whether or not to show recordings.
* @returns * @returns A dataview.
*/ */
public createDataView( public createDataView(
cameraIDs: Set<string>, cameraIDs: Set<string>,
showRecordings: boolean, showRecordings: boolean,
mediaType: TimelineMediaType,
): DataView<FrigateCardTimelineItem> { ): DataView<FrigateCardTimelineItem> {
return new DataView(this._dataset, { return new DataView(this._dataset, {
filter: (item: FrigateCardTimelineItem) => filter: (item: FrigateCardTimelineItem) =>
!!item.group && !!item.group &&
cameraIDs.has(String(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. * Create a dataview for segments.
* @returns * @returns A dataview.
*/ */
public createSegmentDataView(): DataView<RecordingSegmentsItem> { public createSegmentDataView(): DataView<RecordingSegmentsItem> {
return new DataView(this._recordingSegments); return new DataView(this._recordingSegments);
@@ -171,51 +171,22 @@ export class TimelineDataManager {
} }
/** /**
* Add a FrigateBrowseMediaSource object to the managed timeline. * Add events for the given camera.
* @param cameraID The id the camera this object is from. * @param cameraID The camera ID.
* @param target The FrigateBrowseMediaSource to add. * @param events The array of events.
*/ */
protected _addMediaSource(target: FrigateBrowseMediaSource): void { protected _addEvents(cameraID: string, events: FrigateEvents): void {
const items: FrigateCardTimelineItem[] = []; this._dataset.update(
target.children?.forEach((child) => { events.map((event) => ({
const event = child.frigate?.event; id: event.id,
const cameraID = child.frigate?.cameraID; group: cameraID,
if ( content: '',
cameraID && event: event,
event && start: event.start_time * 1000,
isTrueMedia(child) && type: event.end_time ? 'range' : 'point',
['video', 'image'].includes(child.media_content_type) ...(event.end_time && { end: event.end_time * 1000 }),
) { })),
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);
} }
/** /**
@@ -485,37 +456,31 @@ export class TimelineDataManager {
start: Date, start: Date,
end: Date, end: Date,
): Promise<void> { ): Promise<void> {
const params: BrowseMediaQueryParameters[] = []; const params: Map<string, FrigateGetEventsParameters> = new Map();
this._cameras.forEach((cameraConfig, cameraID) => { this._cameras.forEach((cameraConfig, cameraID) => {
(this._mediaType === 'all' ? ['clips', 'snapshots'] : [this._mediaType]).forEach( if (
(mediaType) => { cameraConfig.frigate.camera_name &&
if (cameraConfig?.frigate.camera_name !== CAMERA_BIRDSEYE) { cameraConfig.frigate.camera_name !== CAMERA_BIRDSEYE
const param = getBrowseMediaQueryParameters(hass, cameraID, cameraConfig, { ) {
before: end.getTime() / 1000, params.set(cameraID, {
after: start.getTime() / 1000, instance_id: cameraConfig.frigate.client_id,
unlimited: true, camera: cameraConfig.frigate.camera_name,
mediaType: mediaType as 'clips' | 'snapshots', ...(cameraConfig.frigate.label && { label: cameraConfig.frigate.label }),
}); ...(cameraConfig.frigate.zone && { label: cameraConfig.frigate.zone }),
if (param) { before: Math.floor(end.getTime() / 1000),
params.push(param); after: Math.floor(start.getTime() / 1000),
} limit: TIMELINE_DATA_MANAGER_MAX_FETCH_COUNT,
} });
}, }
);
}); });
if (!params.length) { let results: Map<string, FrigateEvents>;
return;
}
let results: Map<BrowseMediaQueryParameters, FrigateBrowseMediaSource>;
try { try {
results = await multipleBrowseMediaQuery(hass, params); results = await getEventsMultiple(hass, params);
} catch (e) { } catch (e) {
return dispatchFrigateCardErrorEvent(element, e as FrigateCardError); return dispatchFrigateCardErrorEvent(element, e as FrigateCardError);
} }
for (const result of results.values()) { results.forEach((params, cameraID) => this._addEvents(cameraID, params));
this._addMediaSource(result);
}
} }
} }
+5
View File
@@ -1060,6 +1060,11 @@ custom-card-helpers@^1.9.0:
superstruct "^0.15.3" superstruct "^0.15.3"
typescript "^4.5.4" typescript "^4.5.4"
date-fns-tz@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.3.7.tgz#e8e9d2aaceba5f1cc0e677631563081fdcb0e69a"
integrity sha512-1t1b8zyJo+UI8aR+g3iqr5fkUHWpd58VBx8J/ZSQ+w7YrGlw80Ag4sA86qkfCXRBLmMc4I2US+aPMd4uKvwj5g==
date-fns@^2.29.2: date-fns@^2.29.2:
version "2.29.2" version "2.29.2"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.2.tgz#0d4b3d0f3dff0f920820a070920f0d9662c51931" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.2.tgz#0d4b3d0f3dff0f920820a070920f0d9662c51931"