Fetch timeline events from events endpoint not media browser.
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"crypto": "^1.0.1",
|
||||
"custom-card-helpers": "^1.9.0",
|
||||
"date-fns": "^2.29.2",
|
||||
"date-fns-tz": "^1.3.7",
|
||||
"embla-carousel": "^7.0.2",
|
||||
"embla-carousel-wheel-gestures": "^3.0.0",
|
||||
"home-assistant-js-websocket": "^8.0.0",
|
||||
|
||||
@@ -216,6 +216,7 @@ export class FrigateCardLive extends LitElement {
|
||||
html`<frigate-card-surround
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.fetch=${true}
|
||||
.thumbnailConfig=${config.controls.thumbnails}
|
||||
.timelineConfig=${config.controls.timeline}
|
||||
.browseMediaParams=${browseMediaParams ?? undefined}
|
||||
|
||||
@@ -32,7 +32,7 @@ import './surround-basic.js';
|
||||
import './timeline-core.js';
|
||||
|
||||
interface ThumbnailViewContext {
|
||||
// Whetherr or not to fetch thumbnails.
|
||||
// Whether or not to fetch thumbnails.
|
||||
fetch?: boolean;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@ export class FrigateCardSurround extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public inBackground?: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
public fetch = false;
|
||||
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public browseMediaParams?: BrowseMediaQueryParameters | BrowseMediaQueryParameters[];
|
||||
|
||||
@@ -76,12 +79,13 @@ export class FrigateCardSurround extends LitElement {
|
||||
*/
|
||||
protected async _fetchMedia(): Promise<void> {
|
||||
if (
|
||||
!this.fetch ||
|
||||
this.inBackground ||
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
this.view.target ||
|
||||
!this.thumbnailConfig ||
|
||||
this.thumbnailConfig.mode === 'none' ||
|
||||
this.view.target ||
|
||||
!this.browseMediaParams ||
|
||||
!(this.view.context?.thumbnails?.fetch ?? true)
|
||||
) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import thumbnailStyle from '../scss/thumbnail.scss';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { errorToConsole, prettifyTitle } from '../utils/basic.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 { createFetchThumbnailTask } from '../utils/thumbnail.js';
|
||||
import { View } from '../view.js';
|
||||
|
||||
@@ -47,16 +47,19 @@ import { stopEventFromActivatingCardWideActions } from '../utils/action';
|
||||
import {
|
||||
contentsChanged,
|
||||
dispatchFrigateCardEvent,
|
||||
formatDateAndTime,
|
||||
isHoverableDevice,
|
||||
prettifyTitle,
|
||||
} from '../utils/basic';
|
||||
import { getAllDependentCameras, getCameraTitle } from '../utils/camera.js';
|
||||
|
||||
import {
|
||||
createEventParentForChildren,
|
||||
createVideoChild,
|
||||
generateRecordingIdentifier,
|
||||
} from '../utils/ha/browse-media';
|
||||
getEventMediaContentID,
|
||||
getEventThumbnailURL,
|
||||
getEventTitle,
|
||||
getRecordingMediaContentID,
|
||||
} from '../utils/frigate';
|
||||
|
||||
import { createEventParentForChildren, createChild } from '../utils/ha/browse-media';
|
||||
import {
|
||||
FrigateCardTimelineItem,
|
||||
RecordingSegmentsItem,
|
||||
@@ -216,16 +219,17 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
* @returns The tooltip as a string to render.
|
||||
*/
|
||||
protected _getTooltip(item: TimelineItem): string {
|
||||
const source = (<FrigateCardTimelineItem>item).source;
|
||||
if (!this._isHoverableDevice || !source) {
|
||||
const event = (<FrigateCardTimelineItem>item).event;
|
||||
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
|
||||
// the drawer.
|
||||
return '';
|
||||
}
|
||||
|
||||
const eventAttr = source.frigate?.event
|
||||
? `event='${JSON.stringify(source.frigate.event)}'`
|
||||
: '';
|
||||
const eventAttr = `event='${JSON.stringify(event)}'`;
|
||||
const detailsAttr = this.thumbnailDetails ? 'details' : '';
|
||||
|
||||
// Cannot use Lit data-bindings as visjs requires a string for tooltips.
|
||||
@@ -233,10 +237,10 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
// whitelist in `_getOptions()` .
|
||||
return `
|
||||
<frigate-card-timeline-thumbnail
|
||||
thumbnail="${source.thumbnail}"
|
||||
thumbnail="${getEventThumbnailURL(clientId, event)}"
|
||||
${detailsAttr}
|
||||
${eventAttr}
|
||||
label="${source.title}"
|
||||
label="${getEventTitle(event)}"
|
||||
>
|
||||
</frigate-card-timeline-thumbnail>`;
|
||||
}
|
||||
@@ -324,12 +328,9 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
// hours, otherwise only show the matching hour from all cameras.
|
||||
if (!onlyShowMatchingHour || isMatchingHour) {
|
||||
children.push(
|
||||
createVideoChild(
|
||||
`${prettifyTitle(config.frigate.camera_name)} ${format(
|
||||
hour,
|
||||
'yyyy-MM-dd HH:mm',
|
||||
)}`,
|
||||
generateRecordingIdentifier({
|
||||
createChild(
|
||||
`${prettifyTitle(config.frigate.camera_name)} ${formatDateAndTime(hour)}`,
|
||||
getRecordingMediaContentID({
|
||||
clientId: config.frigate.client_id,
|
||||
year: dayData.day.getFullYear(),
|
||||
month: dayData.day.getMonth() + 1,
|
||||
@@ -812,9 +813,40 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
let childIndex = -1;
|
||||
const children: FrigateBrowseMediaSource[] = [];
|
||||
this._dataview?.get({ order: sortTimelineItemsYoungestToOldest }).forEach((item) => {
|
||||
if (item.event && item.source) {
|
||||
children.push(item.source);
|
||||
if (selected.includes(item.event.id)) {
|
||||
const cameraID = item.group ? String(item.group) : null;
|
||||
const cameraConfig = cameraID ? this.cameras?.get(cameraID) : null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1190,6 +1222,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
this.timelineDataManager &&
|
||||
this._refTimeline.value &&
|
||||
options &&
|
||||
this.timelineConfig &&
|
||||
(changedProperties.has('timelineConfig') ||
|
||||
(this.mini &&
|
||||
changedProperties.has('view') &&
|
||||
@@ -1212,7 +1245,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
|
||||
this._dataview = this.timelineDataManager.createDataView(
|
||||
this._getTimelineCameraIDs(),
|
||||
!!this.timelineConfig?.show_recordings,
|
||||
!!this.timelineConfig.show_recordings,
|
||||
this.timelineConfig.media,
|
||||
);
|
||||
|
||||
if (this.mini && groups.length === 1) {
|
||||
|
||||
@@ -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: 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
|
||||
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
@@ -49,6 +48,7 @@ export class FrigateCardTimeline extends LitElement {
|
||||
.view=${this.view}
|
||||
.thumbnailConfig=${this.timelineConfig.controls.thumbnails}
|
||||
.cameras=${this.cameras}
|
||||
.fetch=${false}
|
||||
>
|
||||
<frigate-card-timeline-core
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -137,6 +137,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
return html` <frigate-card-surround
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.fetch=${false}
|
||||
.thumbnailConfig=${this.viewerConfig.controls.thumbnails}
|
||||
.timelineConfig=${this.viewerConfig.controls.timeline}
|
||||
.timelineDataManager=${this.timelineDataManager}
|
||||
|
||||
@@ -26,6 +26,8 @@ export const THUMBNAIL_WIDTH_MIN = 75;
|
||||
* Internal types.
|
||||
*/
|
||||
|
||||
export type ClipsOrSnapshots = 'clips' | 'snapshots';
|
||||
|
||||
export const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
|
||||
'live',
|
||||
'clip',
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1060,6 +1060,11 @@ custom-card-helpers@^1.9.0:
|
||||
superstruct "^0.15.3"
|
||||
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:
|
||||
version "2.29.2"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.2.tgz#0d4b3d0f3dff0f920820a070920f0d9662c51931"
|
||||
|
||||
Reference in New Issue
Block a user