Merge pull request #943 from dermotduffy/configurable-chunk-size

Add a performance option to control number of loaded media items
This commit is contained in:
Dermot Duffy
2023-02-13 21:32:30 -08:00
committed by GitHub
20 changed files with 311 additions and 263 deletions
+2
View File
@@ -736,6 +736,7 @@ performance:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `animated_progress_indicator` | `true` | :heavy_multiplication_x: | Will show the animated progress indicator 'spinner' when `true` or a simple loading icon when `false`.| | `animated_progress_indicator` | `true` | :heavy_multiplication_x: | Will show the animated progress indicator 'spinner' when `true` or a simple loading icon when `false`.|
| `media_chunk_size` | `50` | :heavy_multiplication_x: | How many media items to fetch and render at a time (e.g. thumbnails under a live view, or number of snapshots to load in the media viewer). This may only make partial sense in some contexts (e.g. the 'infinite gallery' is still infinite, just loads thumbnails this many items at a time) or not at all (e.g. the timeline will show the number of events dictated by the time span the user navigates to).|
#### Style Options #### Style Options
@@ -2241,6 +2242,7 @@ performance:
profile: high profile: high
features: features:
animated_progress_indicator: true animated_progress_indicator: true
media_chunk_size: 50
style: style:
border_radius: true border_radius: true
box_shadow: true box_shadow: true
+2 -1
View File
@@ -2,7 +2,6 @@ import { HomeAssistant } from 'custom-card-helpers';
import add from 'date-fns/add'; import add from 'date-fns/add';
import endOfHour from 'date-fns/endOfHour'; import endOfHour from 'date-fns/endOfHour';
import startOfHour from 'date-fns/startOfHour'; import startOfHour from 'date-fns/startOfHour';
import { CAMERA_BIRDSEYE } from '../../const';
import { CameraConfig, CardWideConfig } from '../../types'; import { CameraConfig, CardWideConfig } from '../../types';
import { ViewMedia } from '../../view/media'; import { ViewMedia } from '../../view/media';
import { RequestCache, RecordingSegmentsCache } from '../cache'; import { RequestCache, RecordingSegmentsCache } from '../cache';
@@ -76,6 +75,8 @@ import { GenericCameraManagerEngine } from '../generic/engine-generic';
const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60; const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
const RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS = 60; const RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
const CAMERA_BIRDSEYE = 'birdseye' as const;
class FrigateQueryResultsClassifier { class FrigateQueryResultsClassifier {
public static isFrigateEventQueryResults( public static isFrigateEventQueryResults(
results: QueryResults, results: QueryResults,
+5 -1
View File
@@ -44,6 +44,7 @@ import { localize } from '../localize/localize.js';
import { CameraInitializationError } from './error.js'; import { CameraInitializationError } from './error.js';
import { CameraManagerStore } from './store.js'; import { CameraManagerStore } from './store.js';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
class QueryClassifier { class QueryClassifier {
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery { public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
@@ -356,7 +357,6 @@ export class CameraManager {
queries: T[], queries: T[],
results: ViewMedia[], results: ViewMedia[],
direction: 'earlier' | 'later', direction: 'earlier' | 'later',
chunkSize: number,
): Promise<ExtendedMediaQueryResult<T> | null> { ): Promise<ExtendedMediaQueryResult<T> | null> {
const getTimeFromResults = (want: 'earliest' | 'latest'): Date | null => { const getTimeFromResults = (want: 'earliest' | 'latest'): Date | null => {
let output: Date | null = null; let output: Date | null = null;
@@ -374,6 +374,10 @@ export class CameraManager {
return output; return output;
}; };
const chunkSize =
this._cardWideConfig?.performance?.features.media_chunk_size ??
MEDIA_CHUNK_SIZE_DEFAULT;
// The queries associated with the chunk to fetch. // The queries associated with the chunk to fetch.
const newChunkQueries: T[] = []; const newChunkQueries: T[] = [];
+1
View File
@@ -1916,6 +1916,7 @@ class FrigateCard extends LitElement {
.view=${this._view} .view=${this._view}
.timelineConfig=${this._getConfig().timeline} .timelineConfig=${this._getConfig().timeline}
.cameraManager=${this._cameraManager} .cameraManager=${this._cameraManager}
.cardWideConfig=${this._cardWideConfig}
> >
</frigate-card-timeline>` </frigate-card-timeline>`
: ``} : ``}
+12 -15
View File
@@ -33,12 +33,10 @@ import { EventQuery, MediaQuery, RecordingQuery } from '../camera-manager/types'
import { MediaQueriesResults } from '../view/media-queries-results'; import { MediaQueriesResults } from '../view/media-queries-results';
import { errorToConsole } from '../utils/basic'; import { errorToConsole } from '../utils/basic';
import './media-filter'; import './media-filter';
import "./surround-basic"; import './surround-basic';
import { ViewMedia } from '../view/media'; import { ViewMedia } from '../view/media';
import { localize } from '../localize/localize'; import { localize } from '../localize/localize';
const GALLERY_MEDIA_CHUNK_SIZE = 100;
const GALLERY_MEDIA_FILTER_MENU_ICONS = { const GALLERY_MEDIA_FILTER_MENU_ICONS = {
closed: 'mdi:filter-cog-outline', closed: 'mdi:filter-cog-outline',
open: 'mdi:filter-cog', open: 'mdi:filter-cog',
@@ -70,7 +68,8 @@ export class FrigateCardGallery extends LitElement {
!this.hass || !this.hass ||
!this.view || !this.view ||
!this.view.isGalleryView() || !this.view.isGalleryView() ||
!this.cameraManager !this.cameraManager ||
!this.cardWideConfig
) { ) {
return; return;
} }
@@ -81,6 +80,7 @@ export class FrigateCardGallery extends LitElement {
this, this,
this.hass, this.hass,
this.cameraManager, this.cameraManager,
this.cardWideConfig,
this.view, this.view,
); );
} else { } else {
@@ -93,6 +93,7 @@ export class FrigateCardGallery extends LitElement {
this, this,
this.hass, this.hass,
this.cameraManager, this.cameraManager,
this.cardWideConfig,
this.view, this.view,
{ {
...(mediaType && { mediaType: mediaType }), ...(mediaType && { mediaType: mediaType }),
@@ -116,7 +117,7 @@ export class FrigateCardGallery extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.cameraManager=${this.cameraManager} .cameraManager=${this.cameraManager}
.view=${this.view} .view=${this.view}
.mediaLimit=${GALLERY_MEDIA_CHUNK_SIZE} .cardWideConfig=${this.cardWideConfig}
slot=${this.galleryConfig.controls.filter.mode} slot=${this.galleryConfig.controls.filter.mode}
> >
</frigate-card-media-filter>` </frigate-card-media-filter>`
@@ -253,7 +254,6 @@ export class FrigateCardGalleryCore extends LitElement {
rawQueries, rawQueries,
existingMedia, existingMedia,
'earlier', 'earlier',
GALLERY_MEDIA_CHUNK_SIZE,
); );
} catch (e) { } catch (e) {
errorToConsole(e as Error); errorToConsole(e as Error);
@@ -301,11 +301,13 @@ export class FrigateCardGalleryCore extends LitElement {
this._showExtensionLoader = true; this._showExtensionLoader = true;
const oldView: View | undefined = changedProps.get('view'); const oldView: View | undefined = changedProps.get('view');
if (oldView?.queryResults?.getResults() !== this.view?.queryResults?.getResults()) { if (
oldView?.queryResults?.getResults() !== this.view?.queryResults?.getResults()
) {
// Gallery places the most recent media at the top (the query results place // Gallery places the most recent media at the top (the query results place
// the most recent media at the end for use in the viewer). This is copied // the most recent media at the end for use in the viewer). This is copied
// to a new array to avoid reversing the query results in place. // to a new array to avoid reversing the query results in place.
this._media = [...this.view?.queryResults?.getResults() ?? []].reverse(); this._media = [...(this.view?.queryResults?.getResults() ?? [])].reverse();
} }
} }
} }
@@ -315,12 +317,7 @@ export class FrigateCardGalleryCore extends LitElement {
* @returns A rendered template. * @returns A rendered template.
*/ */
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if ( if (!this._media || !this.hass || !this.view || !this.view.isGalleryView()) {
!this._media ||
!this.hass ||
!this.view ||
!this.view.isGalleryView()
) {
return html``; return html``;
} }
@@ -351,7 +348,7 @@ export class FrigateCardGalleryCore extends LitElement {
queryResults: this.view.queryResults?.clone().selectResult( queryResults: this.view.queryResults?.clone().selectResult(
// Media in the gallery is reversed vs the queryResults (see // Media in the gallery is reversed vs the queryResults (see
// note above). // note above).
this._media.length - index - 1 this._media.length - index - 1,
), ),
}) })
.dispatchChangeEvent(this); .dispatchChangeEvent(this);
+1
View File
@@ -228,6 +228,7 @@ export class FrigateCardLive extends LitElement {
.timelineConfig=${config.controls.timeline} .timelineConfig=${config.controls.timeline}
.cameraManager=${this.cameraManager} .cameraManager=${this.cameraManager}
.inBackground=${this._inBackground} .inBackground=${this._inBackground}
.cardWideConfig=${this.cardWideConfig}
@frigate-card:message=${(ev: CustomEvent<Message>) => { @frigate-card:message=${(ev: CustomEvent<Message>) => {
this._renderKey++; this._renderKey++;
this._messageReceivedPostRender = true; this._messageReceivedPostRender = true;
+31 -17
View File
@@ -13,7 +13,7 @@ import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { DateRange } from '../camera-manager/range'; import { DateRange } from '../camera-manager/range';
import { localize } from '../localize/localize'; import { localize } from '../localize/localize';
import mediaFilterStyle from '../scss/media-filter.scss'; import mediaFilterStyle from '../scss/media-filter.scss';
import { createViewForEvents, createViewForRecordings } from '../utils/media-to-view.js'; import { executeMediaQueryForView } from '../utils/media-to-view.js';
import { errorToConsole, formatDate, prettifyTitle } from '../utils/basic'; import { errorToConsole, formatDate, prettifyTitle } from '../utils/basic';
import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin'; import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin';
import './select'; import './select';
@@ -32,10 +32,8 @@ import { View } from '../view/view';
import { CameraManager } from '../camera-manager/manager'; import { CameraManager } from '../camera-manager/manager';
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import { import {
EventQuery,
MediaMetadata, MediaMetadata,
QueryType, QueryType,
RecordingQuery,
} from '../camera-manager/types'; } from '../camera-manager/types';
import format from 'date-fns/format'; import format from 'date-fns/format';
import endOfMonth from 'date-fns/endOfMonth'; import endOfMonth from 'date-fns/endOfMonth';
@@ -43,6 +41,7 @@ import isEqual from 'lodash-es/isEqual';
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries'; import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
import './select.js'; import './select.js';
import orderBy from 'lodash-es/orderBy'; import orderBy from 'lodash-es/orderBy';
import { CardWideConfig } from '../types';
interface MediaFilterCoreDefaults { interface MediaFilterCoreDefaults {
mediaType?: MediaFilterMediaType; mediaType?: MediaFilterMediaType;
@@ -83,7 +82,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
public view?: View; public view?: View;
@property({ attribute: false }) @property({ attribute: false })
public mediaLimit?: number; public cardWideConfig?: CardWideConfig;
static elementDefinitions = { static elementDefinitions = {
'frigate-card-select': FrigateCardSelect, 'frigate-card-select': FrigateCardSelect,
@@ -202,6 +201,8 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
// - Similarly, if the user chooses clips or snapshots, set the actual view // - Similarly, if the user chooses clips or snapshots, set the actual view
// to 'clips' or 'snapshots' in order to ensure the right icon is shown as // to 'clips' or 'snapshots' in order to ensure the right icon is shown as
// selected in the menu. // selected in the menu.
const limit = this.cardWideConfig?.performance?.features.media_chunk_size;
if ( if (
mediaType === MediaFilterMediaType.Clips || mediaType === MediaFilterMediaType.Clips ||
mediaType === MediaFilterMediaType.Snapshots mediaType === MediaFilterMediaType.Snapshots
@@ -209,7 +210,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
const where = getArrayValueAsSet(this._refWhere.value?.value); const where = getArrayValueAsSet(this._refWhere.value?.value);
const what = getArrayValueAsSet(this._refWhat.value?.value); const what = getArrayValueAsSet(this._refWhat.value?.value);
const queries: EventQuery[] = [ const queries = new EventMediaQueries([
{ {
type: QueryType.Event, type: QueryType.Event,
cameraIDs: cameraIDs, cameraIDs: cameraIDs,
@@ -217,38 +218,51 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
...(where && { where: where }), ...(where && { where: where }),
...(favorite !== null && { favorite: favorite }), ...(favorite !== null && { favorite: favorite }),
...(when && { start: when.start, end: when.end }), ...(when && { start: when.start, end: when.end }),
...(this.mediaLimit && { limit: this.mediaLimit }), ...(limit && { limit: limit }),
...(mediaType === MediaFilterMediaType.Clips && { hasClip: true }), ...(mediaType === MediaFilterMediaType.Clips && { hasClip: true }),
...(mediaType === MediaFilterMediaType.Snapshots && { ...(mediaType === MediaFilterMediaType.Snapshots && {
hasSnapshot: true, hasSnapshot: true,
}), }),
}, },
]; ]);
( (
await createViewForEvents(this, this.hass, this.cameraManager, this.view, { await executeMediaQueryForView(
query: new EventMediaQueries(queries), this,
this.hass,
this.cameraManager,
this.view,
queries,
{
// See 'A note on views' above for these two arguments. // See 'A note on views' above for these two arguments.
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }), ...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
targetView: mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots', targetView: mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots',
}) },
)
)?.dispatchChangeEvent(this); )?.dispatchChangeEvent(this);
} else if (mediaType === MediaFilterMediaType.Recordings) { } else if (mediaType === MediaFilterMediaType.Recordings) {
const query: RecordingQuery = { const queries = new RecordingMediaQueries([
{
type: QueryType.Recording, type: QueryType.Recording,
cameraIDs: cameraIDs, cameraIDs: cameraIDs,
...(limit && { limit: limit }),
...(when && { start: when.start, end: when.end }), ...(when && { start: when.start, end: when.end }),
}; },
]);
( (
await createViewForRecordings(this, this.hass, this.cameraManager, this.view, { await executeMediaQueryForView(
query: new RecordingMediaQueries([query]), this,
this.hass,
this.cameraManager,
this.view,
queries,
{
// See 'A note on views' above for these two arguments. // See 'A note on views' above for these two arguments.
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }), ...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
targetView: 'recordings', targetView: 'recordings',
}) },
)
)?.dispatchChangeEvent(this); )?.dispatchChangeEvent(this);
} }
} }
+8 -2
View File
@@ -9,6 +9,7 @@ import {
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import surroundStyle from '../scss/surround.scss'; import surroundStyle from '../scss/surround.scss';
import { import {
CardWideConfig,
ClipsOrSnapshotsOrAll, ClipsOrSnapshotsOrAll,
ExtendedHomeAssistant, ExtendedHomeAssistant,
MiniTimelineControlConfig, MiniTimelineControlConfig,
@@ -57,6 +58,9 @@ export class FrigateCardSurround extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public cameraManager?: CameraManager; public cameraManager?: CameraManager;
@property({ attribute: false })
public cardWideConfig?: CardWideConfig;
protected _cameraIDsForTimeline?: Set<string>; protected _cameraIDsForTimeline?: Set<string>;
/** /**
@@ -68,6 +72,7 @@ export class FrigateCardSurround extends LitElement {
protected async _fetchMedia(): Promise<void> { protected async _fetchMedia(): Promise<void> {
if ( if (
!this.cameraManager || !this.cameraManager ||
!this.cardWideConfig ||
!this.fetchMedia || !this.fetchMedia ||
this.inBackground || this.inBackground ||
!this.hass || !this.hass ||
@@ -83,6 +88,7 @@ export class FrigateCardSurround extends LitElement {
this, this,
this.hass, this.hass,
this.cameraManager, this.cameraManager,
this.cardWideConfig,
this.view, this.view,
{ {
targetView: this.view.view, targetView: this.view.view,
@@ -218,9 +224,9 @@ export class FrigateCardSurround extends LitElement {
.cameraIDs=${this._cameraIDsForTimeline} .cameraIDs=${this._cameraIDsForTimeline}
.mini=${true} .mini=${true}
.timelineConfig=${this.timelineConfig} .timelineConfig=${this.timelineConfig}
.thumbnailDetails=${this.thumbnailConfig?.show_details} .thumbnailConfig=${this.thumbnailConfig}
.thumbnailSize=${this.thumbnailConfig?.size}
.cameraManager=${this.cameraManager} .cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
> >
</frigate-card-timeline-core>` </frigate-card-timeline-core>`
: ''} : ''}
+45 -27
View File
@@ -30,9 +30,11 @@ import { localize } from '../localize/localize';
import timelineCoreStyle from '../scss/timeline-core.scss'; import timelineCoreStyle from '../scss/timeline-core.scss';
import { import {
CameraConfig, CameraConfig,
CardWideConfig,
ExtendedHomeAssistant, ExtendedHomeAssistant,
frigateCardConfigDefaults, frigateCardConfigDefaults,
FrigateCardView, FrigateCardView,
ThumbnailsControlConfig,
TimelineCoreConfig, TimelineCoreConfig,
} from '../types'; } from '../types';
import { stopEventFromActivatingCardWideActions } from '../utils/action'; import { stopEventFromActivatingCardWideActions } from '../utils/action';
@@ -41,10 +43,9 @@ import {
dispatchFrigateCardEvent, dispatchFrigateCardEvent,
isHoverableDevice, isHoverableDevice,
} from '../utils/basic'; } from '../utils/basic';
import { import {
createViewForEvents, createQueriesForRecordingsView,
createViewForRecordings, executeMediaQueryForView,
findClosestMediaIndex, findClosestMediaIndex,
} from '../utils/media-to-view'; } from '../utils/media-to-view';
import { CameraManager } from '../camera-manager/manager'; import { CameraManager } from '../camera-manager/manager';
@@ -168,10 +169,7 @@ export class FrigateCardTimelineCore extends LitElement {
public timelineConfig?: TimelineCoreConfig; public timelineConfig?: TimelineCoreConfig;
@property({ attribute: true, type: Boolean }) @property({ attribute: true, type: Boolean })
public thumbnailDetails? = false; public thumbnailConfig?: ThumbnailsControlConfig;
@property({ attribute: false })
public thumbnailSize?: number;
// Whether or not this is a mini-timeline (in mini-mode the component takes a // Whether or not this is a mini-timeline (in mini-mode the component takes a
// supportive role for other views). // supportive role for other views).
@@ -186,6 +184,9 @@ export class FrigateCardTimelineCore extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public cameraManager?: CameraManager; public cameraManager?: CameraManager;
@property({ attribute: false })
public cardWideConfig?: CardWideConfig;
@state() @state()
protected _locked = false; protected _locked = false;
@@ -228,7 +229,7 @@ export class FrigateCardTimelineCore extends LitElement {
return ` return `
<frigate-card-timeline-thumbnail <frigate-card-timeline-thumbnail
item='${item.id}' item='${item.id}'
${this.thumbnailDetails ? 'details' : ''} ${this.thumbnailConfig?.show_details ? 'details' : ''}
> >
</frigate-card-timeline-thumbnail>`; </frigate-card-timeline-thumbnail>`;
} }
@@ -392,7 +393,6 @@ export class FrigateCardTimelineCore extends LitElement {
): Promise<void> { ): Promise<void> {
const results = this.view?.queryResults; const results = this.view?.queryResults;
const media = results?.getResults(); const media = results?.getResults();
const cameraIDs = this._getTimelineCameraIDs();
if ( if (
!media || !media ||
!results || !results ||
@@ -400,7 +400,6 @@ export class FrigateCardTimelineCore extends LitElement {
!this.view || !this.view ||
!this.hass || !this.hass ||
!this.cameraManager || !this.cameraManager ||
!cameraIDs ||
// Skip range changes that do not have hammerjs pan directions associated // Skip range changes that do not have hammerjs pan directions associated
// with them, as these outliers cause media matching issues below. // with them, as these outliers cause media matching issues below.
!properties.event.additionalEvent !properties.event.additionalEvent
@@ -418,7 +417,6 @@ export class FrigateCardTimelineCore extends LitElement {
findClosestMediaIndex( findClosestMediaIndex(
media, media,
targetTime, targetTime,
cameraIDs,
properties.event.additionalEvent === 'panright' ? 'end' : 'start', properties.event.additionalEvent === 'panright' ? 'end' : 'start',
), ),
); );
@@ -467,6 +465,7 @@ export class FrigateCardTimelineCore extends LitElement {
!this._timeline || !this._timeline ||
!this.view || !this.view ||
!this.cameraManager || !this.cameraManager ||
!this.cardWideConfig ||
!timelineCameraIDs || !timelineCameraIDs ||
!properties.what !properties.what
) { ) {
@@ -479,34 +478,50 @@ export class FrigateCardTimelineCore extends LitElement {
this.timelineConfig?.show_recordings && this.timelineConfig?.show_recordings &&
['background', 'group-label'].includes(properties.what) ['background', 'group-label'].includes(properties.what)
) { ) {
view = await createViewForRecordings( const query = createQueriesForRecordingsView(
this.cameraManager,
this.cardWideConfig,
new Set([String(properties.group)]),
);
if (query) {
view = await executeMediaQueryForView(
this, this,
this.hass, this.hass,
this.cameraManager, this.cameraManager,
this.view, this.view,
query,
{ {
targetView: 'recording',
targetTime: targetTime:
properties.what === 'background' properties.what === 'background'
? properties.time ? properties.time
: this._timeline.getWindow().end, : this._timeline.getWindow().end,
...(properties.group && {
cameraIDs: new Set([String(properties.group)]),
}),
}, },
); );
}
} else if (this.timelineConfig?.show_recordings && properties.what === 'axis') { } else if (this.timelineConfig?.show_recordings && properties.what === 'axis') {
view = await createViewForRecordings( const query = createQueriesForRecordingsView(
this.cameraManager,
this.cardWideConfig,
timelineCameraIDs,
{
start: startOfHour(properties.time),
end: endOfHour(properties.time),
},
);
if (query) {
view = await executeMediaQueryForView(
this, this,
this.hass, this.hass,
this.cameraManager, this.cameraManager,
this.view, this.view,
query,
{ {
cameraIDs: timelineCameraIDs, targetView: 'recording',
start: startOfHour(properties.time),
end: endOfHour(properties.time),
targetTime: properties.time, targetTime: properties.time,
}, },
); );
}
} else if (properties.item && properties.what === 'item') { } else if (properties.item && properties.what === 'item') {
const newResults = this.view.queryResults const newResults = this.view.queryResults
?.clone() ?.clone()
@@ -618,7 +633,7 @@ export class FrigateCardTimelineCore extends LitElement {
protected _createEventMediaQuerys(options?: { protected _createEventMediaQuerys(options?: {
window?: TimelineWindow; window?: TimelineWindow;
}): EventMediaQueries | null { }): EventMediaQueries | null {
if (!this._timeline || !this._timelineSource) { if (!this._timeline || !this._timelineSource || !this.cardWideConfig) {
return null; return null;
} }
@@ -644,15 +659,14 @@ export class FrigateCardTimelineCore extends LitElement {
if (!this.hass || !this.cameraManager || !this.view || !query) { if (!this.hass || !this.cameraManager || !this.view || !query) {
return null; return null;
} }
const view = await createViewForEvents( const view = await executeMediaQueryForView(
this, this,
this.hass, this.hass,
this.cameraManager, this.cameraManager,
this.view, this.view,
query,
{ {
query: query,
targetView: options?.targetView, targetView: options?.targetView,
mediaType: this.timelineConfig?.media,
}, },
); );
if (!view) { if (!view) {
@@ -1002,11 +1016,11 @@ export class FrigateCardTimelineCore extends LitElement {
* @param changedProps The changed properties * @param changedProps The changed properties
*/ */
protected willUpdate(changedProps: PropertyValues): void { protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('thumbnailSize')) { if (changedProps.has('thumbnailConfig')) {
if (this.thumbnailSize !== undefined) { if (this.thumbnailConfig) {
this.style.setProperty( this.style.setProperty(
'--frigate-card-thumbnail-size', '--frigate-card-thumbnail-size',
`${this.thumbnailSize}px`, `${this.thumbnailConfig.size}px`,
); );
} else { } else {
this.style.removeProperty('--frigate-card-thumbnail-size'); this.style.removeProperty('--frigate-card-thumbnail-size');
@@ -1028,7 +1042,11 @@ export class FrigateCardTimelineCore extends LitElement {
changedProps.has('cameraIDs') changedProps.has('cameraIDs')
) { ) {
const cameraIDs = this._getTimelineCameraIDs(); const cameraIDs = this._getTimelineCameraIDs();
if (cameraIDs && this.cameraManager && this.timelineConfig) { if (
cameraIDs &&
this.cameraManager &&
this.timelineConfig
) {
this._timelineSource = new TimelineDataSource( this._timelineSource = new TimelineDataSource(
this.cameraManager, this.cameraManager,
cameraIDs, cameraIDs,
+6 -3
View File
@@ -1,7 +1,7 @@
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import timelineStyle from '../scss/timeline.scss'; import timelineStyle from '../scss/timeline.scss';
import { ExtendedHomeAssistant, TimelineConfig } from '../types'; import { CardWideConfig, ExtendedHomeAssistant, TimelineConfig } from '../types';
import { CameraManager } from '../camera-manager/manager'; import { CameraManager } from '../camera-manager/manager';
import { View } from '../view/view'; import { View } from '../view/view';
import './surround.js'; import './surround.js';
@@ -26,6 +26,9 @@ export class FrigateCardTimeline extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public cameraManager?: CameraManager; public cameraManager?: CameraManager;
@property({ attribute: false })
public cardWideConfig?: CardWideConfig;
/** /**
* Master render method. * Master render method.
* @returns A rendered template. * @returns A rendered template.
@@ -45,9 +48,9 @@ export class FrigateCardTimeline extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.view=${this.view} .view=${this.view}
.timelineConfig=${this.timelineConfig} .timelineConfig=${this.timelineConfig}
.thumbnailDetails=${this.timelineConfig.controls.thumbnails.show_details} .thumbnailConfig=${this.timelineConfig.controls.thumbnails}
.thumbnailSize=${this.timelineConfig.controls.thumbnails.size}
.cameraManager=${this.cameraManager} .cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
> >
</frigate-card-timeline-core> </frigate-card-timeline-core>
</frigate-card-surround>`; </frigate-card-surround>`;
+5 -1
View File
@@ -95,7 +95,8 @@ export class FrigateCardViewer extends LitElement {
!this.hass || !this.hass ||
!this.view || !this.view ||
!this.viewerConfig || !this.viewerConfig ||
!this.cameraManager !this.cameraManager ||
!this.cardWideConfig
) { ) {
return; return;
} }
@@ -115,6 +116,7 @@ export class FrigateCardViewer extends LitElement {
this, this,
this.hass, this.hass,
this.cameraManager, this.cameraManager,
this.cardWideConfig,
this.view, this.view,
{ {
targetView: 'recording', targetView: 'recording',
@@ -125,6 +127,7 @@ export class FrigateCardViewer extends LitElement {
this, this,
this.hass, this.hass,
this.cameraManager, this.cameraManager,
this.cardWideConfig,
this.view, this.view,
{ {
targetView: 'media', targetView: 'media',
@@ -141,6 +144,7 @@ export class FrigateCardViewer extends LitElement {
.thumbnailConfig=${this.viewerConfig.controls.thumbnails} .thumbnailConfig=${this.viewerConfig.controls.thumbnails}
.timelineConfig=${this.viewerConfig.controls.timeline} .timelineConfig=${this.viewerConfig.controls.timeline}
.cameraManager=${this.cameraManager} .cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
> >
<frigate-card-viewer-carousel <frigate-card-viewer-carousel
.hass=${this.hass} .hass=${this.hass}
+7 -1
View File
@@ -1,4 +1,3 @@
export const CAMERA_BIRDSEYE = 'birdseye' as const;
export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card' as const; export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card' as const;
export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting` as const; export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting` as const;
@@ -213,9 +212,16 @@ export const CONF_OVERRIDES = 'overrides' as const;
const CONF_PERFORMANCE = 'performance' as const; const CONF_PERFORMANCE = 'performance' as const;
export const CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR = `${CONF_PERFORMANCE}.features.animated_progress_indicator`; export const CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR = `${CONF_PERFORMANCE}.features.animated_progress_indicator`;
export const CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE = `${CONF_PERFORMANCE}.features.media_chunk_size`;
export const CONF_PERFORMANCE_PROFILE = `${CONF_PERFORMANCE}.profile`; export const CONF_PERFORMANCE_PROFILE = `${CONF_PERFORMANCE}.profile`;
export const CONF_PERFORMANCE_STYLE_BOX_SHADOW = `${CONF_PERFORMANCE}.style.box_shadow`; export const CONF_PERFORMANCE_STYLE_BOX_SHADOW = `${CONF_PERFORMANCE}.style.box_shadow`;
export const CONF_PERFORMANCE_STYLE_BORDER_RADIUS = `${CONF_PERFORMANCE}.style.border_radius`; export const CONF_PERFORMANCE_STYLE_BORDER_RADIUS = `${CONF_PERFORMANCE}.style.border_radius`;
// Taken from https://github.dev/home-assistant/frontend/blob/b5861869e39290fd2e15737e89571dfc543b3ad3/src/data/media-player.ts#L93 // Taken from https://github.dev/home-assistant/frontend/blob/b5861869e39290fd2e15737e89571dfc543b3ad3/src/data/media-player.ts#L93
export const MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA = 131072; export const MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA = 131072;
// The number of media items to fetch at a time (for clips/snapshot views, and
// gallery chunks). Smaller values will cause more frequent smaller fetches, but
// improved rendering performance.
export const MEDIA_CHUNK_SIZE_DEFAULT = 50;
export const MEDIA_CHUNK_SIZE_MAX = 1000;
+5
View File
@@ -127,6 +127,8 @@ import {
CONF_VIEW_UPDATE_CYCLE_CAMERA, CONF_VIEW_UPDATE_CYCLE_CAMERA,
CONF_VIEW_UPDATE_FORCE, CONF_VIEW_UPDATE_FORCE,
CONF_VIEW_UPDATE_SECONDS, CONF_VIEW_UPDATE_SECONDS,
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
MEDIA_CHUNK_SIZE_MAX,
} from './const.js'; } from './const.js';
import { localize } from './localize/localize.js'; import { localize } from './localize/localize.js';
import frigate_card_editor_style from './scss/editor.scss'; import frigate_card_editor_style from './scss/editor.scss';
@@ -1853,6 +1855,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR, CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
this._defaults.performance.features.animated_progress_indicator, this._defaults.performance.features.animated_progress_indicator,
)} )}
${this._renderNumberInput(CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE, {
max: MEDIA_CHUNK_SIZE_MAX,
})}
`, `,
)} )}
${this._putInSubmenu( ${this._putInSubmenu(
+2 -1
View File
@@ -255,7 +255,8 @@
"warning": "This card is in low profile mode so defaults have changed to optimize performance", "warning": "This card is in low profile mode so defaults have changed to optimize performance",
"features": { "features": {
"editor_label": "Feature Options", "editor_label": "Feature Options",
"animated_progress_indicator": "Animated Progress Indicator" "animated_progress_indicator": "Animated Progress Indicator",
"media_chunk_size": "Media chunk size"
}, },
"profile": "Performance profile", "profile": "Performance profile",
"profiles": { "profiles": {
+18
View File
@@ -243,6 +243,24 @@
"overrides": { "overrides": {
"info": "Questa configurazione della scheda ha specificato manualmente le sostituzioni configurate che possono sostituire i valori mostrati nell'editor visivo, consultare l'editor di codice per visualizzare/modificare queste sostituzioni" "info": "Questa configurazione della scheda ha specificato manualmente le sostituzioni configurate che possono sostituire i valori mostrati nell'editor visivo, consultare l'editor di codice per visualizzare/modificare queste sostituzioni"
}, },
"performance": {
"warning": "",
"features": {
"editor_label": "",
"animated_progress_indicator": "",
"media_chunk_size": ""
},
"profile": "",
"profiles": {
"low": "",
"high": ""
},
"style": {
"editor_label": "",
"box_shadow": "",
"border_radius": ""
}
},
"view": { "view": {
"camera_select": "Visualizza per le telecamere appena selezionate", "camera_select": "Visualizza per le telecamere appena selezionate",
"dark_mode": "Tema scuro", "dark_mode": "Tema scuro",
+18
View File
@@ -243,6 +243,24 @@
"overrides": { "overrides": {
"info": "Esta configuração do cartão especificou manualmente as substituições configuradas que podem substituir os valores mostrados no editor visual, consulte o editor de código para visualizar/modificar essas substituições" "info": "Esta configuração do cartão especificou manualmente as substituições configuradas que podem substituir os valores mostrados no editor visual, consulte o editor de código para visualizar/modificar essas substituições"
}, },
"performance": {
"warning": "",
"features": {
"editor_label": "",
"animated_progress_indicator": "",
"media_chunk_size": ""
},
"profile": "",
"profiles": {
"low": "",
"high": ""
},
"style": {
"editor_label": "",
"box_shadow": "",
"border_radius": ""
}
},
"view": { "view": {
"camera_select": "Visualização de câmeras recém-selecionadas", "camera_select": "Visualização de câmeras recém-selecionadas",
"dark_mode": "Modo escuro", "dark_mode": "Modo escuro",
+8 -5
View File
@@ -40,6 +40,7 @@ import {
CONF_MENU_BUTTONS_TIMELINE, CONF_MENU_BUTTONS_TIMELINE,
CONF_MENU_STYLE, CONF_MENU_STYLE,
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR, CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
CONF_PERFORMANCE_STYLE_BORDER_RADIUS, CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
CONF_PERFORMANCE_STYLE_BOX_SHADOW, CONF_PERFORMANCE_STYLE_BOX_SHADOW,
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE, CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
@@ -108,11 +109,6 @@ const LOW_PROFILE_DEFAULTS = {
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false, [CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false, [CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false, [CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
// Refresh the live camera image every 10 seconds (same as stock Home
// Assistant Picture Glance).
[CONF_LIVE_IMAGE_REFRESH_SECONDS]: 10,
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false, [CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false, [CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false, [CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
@@ -120,9 +116,16 @@ const LOW_PROFILE_DEFAULTS = {
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false, [CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false, [CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
// Refresh the live camera image every 10 seconds (same as stock Home
// Assistant Picture Glance).
[CONF_LIVE_IMAGE_REFRESH_SECONDS]: 10,
// Disable all optional performance related features. // Disable all optional performance related features.
[CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR]: false, [CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR]: false,
// Load fewer media items by default.
[CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE]: 10,
// Disable all expensive CSS features. // Disable all expensive CSS features.
[CONF_PERFORMANCE_STYLE_BORDER_RADIUS]: false, [CONF_PERFORMANCE_STYLE_BORDER_RADIUS]: false,
[CONF_PERFORMANCE_STYLE_BOX_SHADOW]: false, [CONF_PERFORMANCE_STYLE_BOX_SHADOW]: false,
+16 -13
View File
@@ -13,6 +13,7 @@ import {
} from 'custom-card-helpers'; } from 'custom-card-helpers';
import { StyleInfo } from 'lit/directives/style-map.js'; import { StyleInfo } from 'lit/directives/style-map.js';
import { z } from 'zod'; import { z } from 'zod';
import { MEDIA_CHUNK_SIZE_DEFAULT, MEDIA_CHUNK_SIZE_MAX } from './const.js';
import { deepRemoveDefaults } from './utils/zod.js'; import { deepRemoveDefaults } from './utils/zod.js';
// The min allowed size of buttons. // The min allowed size of buttons.
@@ -48,7 +49,7 @@ const FRIGATE_CARD_VIEWS = [
'media', 'media',
] as const; ] as const;
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number]; export type FrigateCardView = (typeof FRIGATE_CARD_VIEWS)[number];
export const FRIGATE_CARD_VIEW_DEFAULT = 'live' as const; export const FRIGATE_CARD_VIEW_DEFAULT = 'live' as const;
const FRIGATE_MENU_STYLES = [ const FRIGATE_MENU_STYLES = [
@@ -66,7 +67,7 @@ const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
export const FRIGATE_MENU_PRIORITY_MAX = 100; export const FRIGATE_MENU_PRIORITY_MAX = 100;
const LIVE_PROVIDERS = ['auto', 'image', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const; const LIVE_PROVIDERS = ['auto', 'image', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
export type LiveProvider = typeof LIVE_PROVIDERS[number]; export type LiveProvider = (typeof LIVE_PROVIDERS)[number];
const MEDIA_ACTION_NEGATIVE_CONDITIONS = [ const MEDIA_ACTION_NEGATIVE_CONDITIONS = [
'all', 'all',
@@ -74,9 +75,9 @@ const MEDIA_ACTION_NEGATIVE_CONDITIONS = [
'hidden', 'hidden',
'never', 'never',
] as const; ] as const;
export type LazyUnloadCondition = typeof MEDIA_ACTION_NEGATIVE_CONDITIONS[number]; export type LazyUnloadCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
export type AutoMuteCondition = typeof MEDIA_ACTION_NEGATIVE_CONDITIONS[number]; export type AutoMuteCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
export type AutoPauseCondition = typeof MEDIA_ACTION_NEGATIVE_CONDITIONS[number]; export type AutoPauseCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
const MEDIA_ACTION_POSITIVE_CONDITIONS = [ const MEDIA_ACTION_POSITIVE_CONDITIONS = [
'all', 'all',
@@ -84,14 +85,10 @@ const MEDIA_ACTION_POSITIVE_CONDITIONS = [
'visible', 'visible',
'never', 'never',
] as const; ] as const;
export type AutoUnmuteCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number]; export type AutoUnmuteCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
export type AutoPlayCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number]; export type AutoPlayCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
const ENGINES = [ const ENGINES = ['auto', 'frigate', 'generic'] as const;
'auto',
'frigate',
'generic',
] as const;
export class FrigateCardError extends Error { export class FrigateCardError extends Error {
context?: unknown; context?: unknown;
@@ -225,7 +222,7 @@ const FRIGATE_CARD_ACTIONS = [
'camera_select', 'camera_select',
'media_player', 'media_player',
] as const; ] as const;
export type FrigateCardAction = typeof FRIGATE_CARD_ACTIONS[number]; export type FrigateCardAction = (typeof FRIGATE_CARD_ACTIONS)[number];
const frigateCardGeneralActionSchema = frigateCardCustomactionsBaseSchema.extend({ const frigateCardGeneralActionSchema = frigateCardCustomactionsBaseSchema.extend({
frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS), frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS),
@@ -1196,6 +1193,7 @@ const performanceConfigDefault = {
profile: 'high' as const, profile: 'high' as const,
features: { features: {
animated_progress_indicator: true, animated_progress_indicator: true,
media_chunk_size: MEDIA_CHUNK_SIZE_DEFAULT,
}, },
style: { style: {
border_radius: true, border_radius: true,
@@ -1211,6 +1209,11 @@ const performanceConfigSchema = z
animated_progress_indicator: z animated_progress_indicator: z
.boolean() .boolean()
.default(performanceConfigDefault.features.animated_progress_indicator), .default(performanceConfigDefault.features.animated_progress_indicator),
media_chunk_size: z
.number()
.min(0)
.max(MEDIA_CHUNK_SIZE_MAX)
.default(performanceConfigDefault.features.media_chunk_size),
}) })
.default(performanceConfigDefault.features), .default(performanceConfigDefault.features),
style: z style: z
+55 -112
View File
@@ -1,7 +1,5 @@
import add from 'date-fns/add';
import sub from 'date-fns/sub';
import { ViewContext } from 'view'; import { ViewContext } from 'view';
import { ClipsOrSnapshotsOrAll, FrigateCardView } from '../types'; import { CardWideConfig, ClipsOrSnapshotsOrAll, FrigateCardView } from '../types';
import { View } from '../view/view'; import { View } from '../view/view';
import { import {
EventMediaQueries, EventMediaQueries,
@@ -16,71 +14,54 @@ import { dispatchFrigateCardErrorEvent } from '../components/message';
import { MediaQueriesResults } from '../view/media-queries-results'; import { MediaQueriesResults } from '../view/media-queries-results';
import { errorToConsole } from './basic'; import { errorToConsole } from './basic';
import { MediaQuery } from '../camera-manager/types'; import { MediaQuery } from '../camera-manager/types';
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const';
export const changeViewToRecentEventsForCameraAndDependents = async ( export const changeViewToRecentEventsForCameraAndDependents = async (
element: HTMLElement, element: HTMLElement,
hass: HomeAssistant, hass: HomeAssistant,
cameraManager: CameraManager, cameraManager: CameraManager,
cardWideConfig: CardWideConfig,
view: View, view: View,
options?: { options?: {
mediaType?: ClipsOrSnapshotsOrAll; mediaType?: ClipsOrSnapshotsOrAll;
targetView?: FrigateCardView; targetView?: FrigateCardView;
}, },
): Promise<void> => { ): Promise<void> => {
const cameras = cameraManager.getCameras();
if (!cameras) {
return;
}
const cameraIDs = new Set(getAllDependentCameras(cameras, view.camera));
const queries = createQueriesForEventsView(cameraManager, cardWideConfig, cameraIDs, {
mediaType: options?.mediaType,
});
if (!queries) {
return;
}
( (
await createViewForEvents(element, hass, cameraManager, view, { await executeMediaQueryForView(element, hass, cameraManager, view, queries, {
...options, targetView: options?.targetView,
limit: 50, // Capture the 50 most recent events.
}) })
)?.dispatchChangeEvent(element); )?.dispatchChangeEvent(element);
}; };
export const createViewForEvents = async ( const createQueriesForEventsView = (
element: HTMLElement,
hass: HomeAssistant,
cameraManager: CameraManager, cameraManager: CameraManager,
view: View, cardWideConfig: CardWideConfig,
cameraIDs: Set<string>,
options?: { options?: {
query?: EventMediaQueries;
cameraIDs?: Set<string>;
mediaType?: ClipsOrSnapshotsOrAll; mediaType?: ClipsOrSnapshotsOrAll;
targetCameraID?: string;
targetView?: FrigateCardView;
limit?: number;
}, },
): Promise<View | null> => { ): EventMediaQueries | null => {
const cameras = cameraManager.getCameras(); const limit =
if (!cameras) { cardWideConfig.performance?.features.media_chunk_size ?? MEDIA_CHUNK_SIZE_DEFAULT;
return null;
}
let query: EventMediaQueries;
const cameraIDs: Set<string> = options?.cameraIDs
? options.cameraIDs
: new Set(getAllDependentCameras(cameras, view.camera));
if (options?.query) {
query = options.query;
} else {
const eventQueries = cameraManager.generateDefaultEventQueries(cameraIDs, { const eventQueries = cameraManager.generateDefaultEventQueries(cameraIDs, {
...(options?.limit && { limit: options.limit }), limit: limit,
...(options?.mediaType === 'clips' && { hasClip: true }), ...(options?.mediaType === 'clips' && { hasClip: true }),
...(options?.mediaType === 'snapshots' && { hasSnapshot: true }), ...(options?.mediaType === 'snapshots' && { hasSnapshot: true }),
}); });
if (!eventQueries) { return eventQueries ? new EventMediaQueries(eventQueries) : null;
return null;
}
query = new EventMediaQueries(eventQueries);
}
if (!query) {
return null;
}
return executeMediaQueryForView(element, hass, cameraManager, view, query, {
cameraIDs: cameraIDs,
targetView: options?.targetView,
targetCameraID: options?.targetCameraID,
});
}; };
/** /**
@@ -96,89 +77,61 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
element: HTMLElement, element: HTMLElement,
hass: HomeAssistant, hass: HomeAssistant,
cameraManager: CameraManager, cameraManager: CameraManager,
cardWideConfig: CardWideConfig,
view: View, view: View,
options?: { options?: {
targetView?: 'recording' | 'recordings'; targetView?: 'recording' | 'recordings';
}, },
): Promise<void> => { ): Promise<void> => {
const now = new Date(); const cameras = cameraManager.getCameras();
if (!cameras) {
return;
}
const cameraIDs = new Set(getAllDependentCameras(cameras, view.camera));
const queries = createQueriesForRecordingsView(
cameraManager,
cardWideConfig,
cameraIDs,
);
if (!queries) {
return;
}
( (
await createViewForRecordings(element, hass, cameraManager, view, { await executeMediaQueryForView(element, hass, cameraManager, view, queries, {
...options, targetView: options?.targetView,
// Fetch 7 days worth of recordings (including recordings that are for the
// current hour).
start: sub(now, { days: 7 }),
end: add(now, { hours: 1 }),
}) })
)?.dispatchChangeEvent(element); )?.dispatchChangeEvent(element);
}; };
/** export const createQueriesForRecordingsView = (
* Create a view for recordings.
* @param element The element to dispatch the view change from.
* @param hass The Home Assistant object.
* @param cameraManager The datamanager to use for data access.
* @param cameras The camera configurations.
* @param view The current view.
* @param options A specific window (start and end) to fetch recordings for, a
* targetTime to seek to, a targetView to dispatch to and a set of cameraIDs to
* restrict to.
*/
export const createViewForRecordings = async (
element: HTMLElement,
hass: HomeAssistant,
cameraManager: CameraManager, cameraManager: CameraManager,
view: View, cardWideConfig: CardWideConfig,
cameraIDs: Set<string>,
options?: { options?: {
query?: RecordingMediaQueries;
cameraIDs?: Set<string>;
targetCameraID?: string;
targetView?: 'recording' | 'recordings';
targetTime?: Date;
start?: Date; start?: Date;
end?: Date; end?: Date;
}, },
): Promise<View | null> => { ): RecordingMediaQueries | null => {
const cameras = cameraManager.getCameras(); const limit =
if (!cameras) { cardWideConfig.performance?.features.media_chunk_size ?? MEDIA_CHUNK_SIZE_DEFAULT;
return null;
}
const cameraIDs: Set<string> = options?.cameraIDs
? options.cameraIDs
: new Set(getAllDependentCameras(cameras, view.camera));
let query: RecordingMediaQueries;
if (options?.query) {
query = options.query;
} else {
const recordingQueries = cameraManager.generateDefaultRecordingQueries(cameraIDs, { const recordingQueries = cameraManager.generateDefaultRecordingQueries(cameraIDs, {
limit: limit,
...(options?.start && { start: options.start }), ...(options?.start && { start: options.start }),
...(options?.end && { end: options.end }), ...(options?.end && { end: options.end }),
}); });
return recordingQueries ? new RecordingMediaQueries(recordingQueries) : null;
if (!recordingQueries) {
return null;
}
query = new RecordingMediaQueries(recordingQueries);
}
return executeMediaQueryForView(element, hass, cameraManager, view, query, {
cameraIDs: cameraIDs,
targetView: options?.targetView,
targetCameraID: options?.targetCameraID,
targetTime: options?.targetTime,
});
}; };
const executeMediaQueryForView = async ( export const executeMediaQueryForView = async (
element: HTMLElement, element: HTMLElement,
hass: HomeAssistant, hass: HomeAssistant,
cameraManager: CameraManager, cameraManager: CameraManager,
view: View, view: View,
query: MediaQueries, query: MediaQueries,
options?: { options?: {
cameraIDs?: Set<string>;
targetCameraID?: string; targetCameraID?: string;
targetView?: FrigateCardView; targetView?: FrigateCardView;
targetTime?: Date; targetTime?: Date;
@@ -207,9 +160,9 @@ const executeMediaQueryForView = async (
const queryResults = new MediaQueriesResults(mediaArray, selectedIndex); const queryResults = new MediaQueriesResults(mediaArray, selectedIndex);
let viewerContext: ViewContext | undefined = {}; let viewerContext: ViewContext | undefined = {};
if (options?.targetTime && options.cameraIDs) { if (options?.targetTime) {
queryResults.selectBestResult((media) => queryResults.selectBestResult((media) =>
findClosestMediaIndex(media, options.targetTime as Date, options.cameraIDs), findClosestMediaIndex(media, options.targetTime as Date),
); );
viewerContext = { viewerContext = {
mediaViewer: { mediaViewer: {
@@ -234,7 +187,6 @@ const executeMediaQueryForView = async (
* Find the closest matching media object. * Find the closest matching media object.
* @param mediaArray The media. Must be sorted most recent first. * @param mediaArray The media. Must be sorted most recent first.
* @param targetTime The target time used to find the relevant child. * @param targetTime The target time used to find the relevant child.
* @param cameraIDs The camera IDs to search for.
* @param refPoint Whether to find based on the start or end of the * @param refPoint Whether to find based on the start or end of the
* event/recording. If not specified, the first match is returned rather than * event/recording. If not specified, the first match is returned rather than
* the best match. * the best match.
@@ -243,7 +195,6 @@ const executeMediaQueryForView = async (
export const findClosestMediaIndex = ( export const findClosestMediaIndex = (
mediaArray: ViewMedia[], mediaArray: ViewMedia[],
targetTime: Date, targetTime: Date,
cameraIDs?: Set<string>,
refPoint?: 'start' | 'end', refPoint?: 'start' | 'end',
): number | null => { ): number | null => {
let bestMatch: let bestMatch:
@@ -253,15 +204,7 @@ export const findClosestMediaIndex = (
} }
| undefined; | undefined;
if (!cameraIDs) {
return null;
}
for (const [i, media] of mediaArray.entries()) { for (const [i, media] of mediaArray.entries()) {
if (!cameraIDs.has(media.getCameraID())) {
continue;
}
if (media.includesTime(targetTime)) { if (media.includesTime(targetTime)) {
const start = media.getStartTime(); const start = media.getStartTime();
const end = media.getEndTime(); const end = media.getEndTime();
+1 -1
View File
@@ -132,7 +132,7 @@ export class TimelineDataSource {
} }
const mediaArray = await this._cameraManager.executeMediaQueries(hass, eventQueries); const mediaArray = await this._cameraManager.executeMediaQueries(hass, eventQueries);
const data: FrigateCardTimelineItem[] = [] const data: FrigateCardTimelineItem[] = [];
for (const media of mediaArray ?? []) { for (const media of mediaArray ?? []) {
const endTime = media.getEndTime(); const endTime = media.getEndTime();
const startTime = media.getStartTime(); const startTime = media.getStartTime();