Fetch timeline events from events endpoint not media browser.
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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('/');
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
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,
|
id: event.id,
|
||||||
group: cameraID,
|
group: cameraID,
|
||||||
content: '',
|
content: '',
|
||||||
start: event.start_time * 1000,
|
|
||||||
event: event,
|
event: event,
|
||||||
};
|
start: event.start_time * 1000,
|
||||||
}
|
type: event.end_time ? 'range' : 'point',
|
||||||
if (
|
...(event.end_time && { end: event.end_time * 1000 }),
|
||||||
(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 }),
|
||||||
|
before: Math.floor(end.getTime() / 1000),
|
||||||
|
after: Math.floor(start.getTime() / 1000),
|
||||||
|
limit: TIMELINE_DATA_MANAGER_MAX_FETCH_COUNT,
|
||||||
});
|
});
|
||||||
if (param) {
|
|
||||||
params.push(param);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user