Add a performance option to control media chunk size.
This commit is contained in:
@@ -2,7 +2,6 @@ import { HomeAssistant } from 'custom-card-helpers';
|
||||
import add from 'date-fns/add';
|
||||
import endOfHour from 'date-fns/endOfHour';
|
||||
import startOfHour from 'date-fns/startOfHour';
|
||||
import { CAMERA_BIRDSEYE } from '../../const';
|
||||
import { CameraConfig, CardWideConfig } from '../../types';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { RequestCache, RecordingSegmentsCache } from '../cache';
|
||||
@@ -76,6 +75,8 @@ import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||
const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
||||
const RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
||||
|
||||
const CAMERA_BIRDSEYE = 'birdseye' as const;
|
||||
|
||||
class FrigateQueryResultsClassifier {
|
||||
public static isFrigateEventQueryResults(
|
||||
results: QueryResults,
|
||||
|
||||
@@ -44,6 +44,7 @@ import { localize } from '../localize/localize.js';
|
||||
import { CameraInitializationError } from './error.js';
|
||||
import { CameraManagerStore } from './store.js';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
||||
|
||||
class QueryClassifier {
|
||||
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
|
||||
@@ -356,7 +357,6 @@ export class CameraManager {
|
||||
queries: T[],
|
||||
results: ViewMedia[],
|
||||
direction: 'earlier' | 'later',
|
||||
chunkSize: number,
|
||||
): Promise<ExtendedMediaQueryResult<T> | null> {
|
||||
const getTimeFromResults = (want: 'earliest' | 'latest'): Date | null => {
|
||||
let output: Date | null = null;
|
||||
@@ -374,6 +374,10 @@ export class CameraManager {
|
||||
return output;
|
||||
};
|
||||
|
||||
const chunkSize =
|
||||
this._cardWideConfig?.performance?.features.media_chunk_size ??
|
||||
MEDIA_CHUNK_SIZE_DEFAULT;
|
||||
|
||||
// The queries associated with the chunk to fetch.
|
||||
const newChunkQueries: T[] = [];
|
||||
|
||||
|
||||
@@ -1916,6 +1916,7 @@ class FrigateCard extends LitElement {
|
||||
.view=${this._view}
|
||||
.timelineConfig=${this._getConfig().timeline}
|
||||
.cameraManager=${this._cameraManager}
|
||||
.cardWideConfig=${this._cardWideConfig}
|
||||
>
|
||||
</frigate-card-timeline>`
|
||||
: ``}
|
||||
|
||||
+48
-51
@@ -33,12 +33,10 @@ import { EventQuery, MediaQuery, RecordingQuery } from '../camera-manager/types'
|
||||
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||
import { errorToConsole } from '../utils/basic';
|
||||
import './media-filter';
|
||||
import "./surround-basic";
|
||||
import './surround-basic';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { localize } from '../localize/localize';
|
||||
|
||||
const GALLERY_MEDIA_CHUNK_SIZE = 100;
|
||||
|
||||
const GALLERY_MEDIA_FILTER_MENU_ICONS = {
|
||||
closed: 'mdi:filter-cog-outline',
|
||||
open: 'mdi:filter-cog',
|
||||
@@ -70,7 +68,8 @@ export class FrigateCardGallery extends LitElement {
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
!this.view.isGalleryView() ||
|
||||
!this.cameraManager
|
||||
!this.cameraManager ||
|
||||
!this.cardWideConfig
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -81,18 +80,20 @@ export class FrigateCardGallery extends LitElement {
|
||||
this,
|
||||
this.hass,
|
||||
this.cameraManager,
|
||||
this.cardWideConfig,
|
||||
this.view,
|
||||
);
|
||||
} else {
|
||||
const mediaType = this.view.is('snapshots')
|
||||
? 'snapshots'
|
||||
: this.view.is('clips')
|
||||
? 'clips'
|
||||
: null;
|
||||
? 'clips'
|
||||
: null;
|
||||
changeViewToRecentEventsForCameraAndDependents(
|
||||
this,
|
||||
this.hass,
|
||||
this.cameraManager,
|
||||
this.cardWideConfig,
|
||||
this.view,
|
||||
{
|
||||
...(mediaType && { mediaType: mediaType }),
|
||||
@@ -105,22 +106,22 @@ export class FrigateCardGallery extends LitElement {
|
||||
return html`
|
||||
<frigate-card-surround-basic
|
||||
.drawerIcons=${{
|
||||
...(this.galleryConfig &&
|
||||
this.galleryConfig.controls.filter.mode !== 'none' && {
|
||||
[this.galleryConfig.controls.filter.mode]: GALLERY_MEDIA_FILTER_MENU_ICONS,
|
||||
}),
|
||||
}}
|
||||
...(this.galleryConfig &&
|
||||
this.galleryConfig.controls.filter.mode !== 'none' && {
|
||||
[this.galleryConfig.controls.filter.mode]: GALLERY_MEDIA_FILTER_MENU_ICONS,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
${this.galleryConfig && this.galleryConfig.controls.filter.mode !== 'none'
|
||||
? html` <frigate-card-media-filter
|
||||
? html` <frigate-card-media-filter
|
||||
.hass=${this.hass}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.view=${this.view}
|
||||
.mediaLimit=${GALLERY_MEDIA_CHUNK_SIZE}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
slot=${this.galleryConfig.controls.filter.mode}
|
||||
>
|
||||
</frigate-card-media-filter>`
|
||||
: ''}
|
||||
: ''}
|
||||
<frigate-card-gallery-core
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
@@ -212,10 +213,10 @@ export class FrigateCardGalleryCore extends LitElement {
|
||||
const columns = this.galleryConfig?.controls.thumbnails.show_details
|
||||
? Math.max(1, Math.floor(this.clientWidth / THUMBNAIL_DETAILS_WIDTH_MIN))
|
||||
: Math.max(
|
||||
1,
|
||||
Math.ceil(this.clientWidth / THUMBNAIL_WIDTH_MAX),
|
||||
Math.ceil(this.clientWidth / thumbnailSize),
|
||||
);
|
||||
1,
|
||||
Math.ceil(this.clientWidth / THUMBNAIL_WIDTH_MAX),
|
||||
Math.ceil(this.clientWidth / thumbnailSize),
|
||||
);
|
||||
|
||||
this.style.setProperty('--frigate-card-gallery-columns', String(columns));
|
||||
}
|
||||
@@ -253,7 +254,6 @@ export class FrigateCardGalleryCore extends LitElement {
|
||||
rawQueries,
|
||||
existingMedia,
|
||||
'earlier',
|
||||
GALLERY_MEDIA_CHUNK_SIZE,
|
||||
);
|
||||
} catch (e) {
|
||||
errorToConsole(e as Error);
|
||||
@@ -264,8 +264,8 @@ export class FrigateCardGalleryCore extends LitElement {
|
||||
const newMediaQueries = MediaQueriesClassifier.areEventQueries(query)
|
||||
? new EventMediaQueries(extension.queries as EventQuery[])
|
||||
: MediaQueriesClassifier.areRecordingQueries(query)
|
||||
? new RecordingMediaQueries(extension.queries as RecordingQuery[])
|
||||
: null;
|
||||
? new RecordingMediaQueries(extension.queries as RecordingQuery[])
|
||||
: null;
|
||||
|
||||
if (newMediaQueries) {
|
||||
this.view
|
||||
@@ -301,11 +301,13 @@ export class FrigateCardGalleryCore extends LitElement {
|
||||
this._showExtensionLoader = true;
|
||||
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
|
||||
// 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.
|
||||
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.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (
|
||||
!this._media ||
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
!this.view.isGalleryView()
|
||||
) {
|
||||
if (!this._media || !this.hass || !this.view || !this.view.isGalleryView()) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -332,40 +329,40 @@ export class FrigateCardGalleryCore extends LitElement {
|
||||
|
||||
return html`
|
||||
${this._media.map(
|
||||
(media, index) =>
|
||||
html`<frigate-card-thumbnail
|
||||
(media, index) =>
|
||||
html`<frigate-card-thumbnail
|
||||
.hass=${this.hass}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.media=${media}
|
||||
.view=${this.view}
|
||||
?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
|
||||
?show_favorite_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_favorite_control}
|
||||
.show_favorite_control}
|
||||
?show_timeline_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_timeline_control}
|
||||
.show_timeline_control}
|
||||
@click=${(ev: Event) => {
|
||||
if (this.view && this._media) {
|
||||
this.view
|
||||
.evolve({
|
||||
view: 'media',
|
||||
queryResults: this.view.queryResults?.clone().selectResult(
|
||||
// Media in the gallery is reversed vs the queryResults (see
|
||||
// note above).
|
||||
this._media.length - index - 1
|
||||
),
|
||||
})
|
||||
.dispatchChangeEvent(this);
|
||||
}
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
if (this.view && this._media) {
|
||||
this.view
|
||||
.evolve({
|
||||
view: 'media',
|
||||
queryResults: this.view.queryResults?.clone().selectResult(
|
||||
// Media in the gallery is reversed vs the queryResults (see
|
||||
// note above).
|
||||
this._media.length - index - 1,
|
||||
),
|
||||
})
|
||||
.dispatchChangeEvent(this);
|
||||
}
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
>
|
||||
</frigate-card-thumbnail>`,
|
||||
)}
|
||||
)}
|
||||
${this._showExtensionLoader
|
||||
? html`${renderProgressIndicator({
|
||||
cardWideConfig: this.cardWideConfig,
|
||||
componentRef: this._refLoader,
|
||||
})}`
|
||||
cardWideConfig: this.cardWideConfig,
|
||||
componentRef: this._refLoader,
|
||||
})}`
|
||||
: ''}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -228,6 +228,7 @@ export class FrigateCardLive extends LitElement {
|
||||
.timelineConfig=${config.controls.timeline}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.inBackground=${this._inBackground}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
@frigate-card:message=${(ev: CustomEvent<Message>) => {
|
||||
this._renderKey++;
|
||||
this._messageReceivedPostRender = true;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import { DateRange } from '../camera-manager/range';
|
||||
import { localize } from '../localize/localize';
|
||||
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 { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin';
|
||||
import './select';
|
||||
@@ -32,10 +32,8 @@ import { View } from '../view/view';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import {
|
||||
EventQuery,
|
||||
MediaMetadata,
|
||||
QueryType,
|
||||
RecordingQuery,
|
||||
} from '../camera-manager/types';
|
||||
import format from 'date-fns/format';
|
||||
import endOfMonth from 'date-fns/endOfMonth';
|
||||
@@ -43,6 +41,7 @@ import isEqual from 'lodash-es/isEqual';
|
||||
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
|
||||
import './select.js';
|
||||
import orderBy from 'lodash-es/orderBy';
|
||||
import { CardWideConfig } from '../types';
|
||||
|
||||
interface MediaFilterCoreDefaults {
|
||||
mediaType?: MediaFilterMediaType;
|
||||
@@ -83,7 +82,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
public view?: View;
|
||||
|
||||
@property({ attribute: false })
|
||||
public mediaLimit?: number;
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
static elementDefinitions = {
|
||||
'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
|
||||
// to 'clips' or 'snapshots' in order to ensure the right icon is shown as
|
||||
// selected in the menu.
|
||||
const limit = this.cardWideConfig?.performance?.features.media_chunk_size;
|
||||
|
||||
if (
|
||||
mediaType === MediaFilterMediaType.Clips ||
|
||||
mediaType === MediaFilterMediaType.Snapshots
|
||||
@@ -209,7 +210,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
const where = getArrayValueAsSet(this._refWhere.value?.value);
|
||||
const what = getArrayValueAsSet(this._refWhat.value?.value);
|
||||
|
||||
const queries: EventQuery[] = [
|
||||
const queries = new EventMediaQueries([
|
||||
{
|
||||
type: QueryType.Event,
|
||||
cameraIDs: cameraIDs,
|
||||
@@ -217,38 +218,51 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
...(where && { where: where }),
|
||||
...(favorite !== null && { favorite: favorite }),
|
||||
...(when && { start: when.start, end: when.end }),
|
||||
...(this.mediaLimit && { limit: this.mediaLimit }),
|
||||
...(limit && { limit: limit }),
|
||||
...(mediaType === MediaFilterMediaType.Clips && { hasClip: true }),
|
||||
...(mediaType === MediaFilterMediaType.Snapshots && {
|
||||
hasSnapshot: true,
|
||||
}),
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
(
|
||||
await createViewForEvents(this, this.hass, this.cameraManager, this.view, {
|
||||
query: new EventMediaQueries(queries),
|
||||
|
||||
// See 'A note on views' above for these two arguments.
|
||||
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
|
||||
targetView: mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots',
|
||||
})
|
||||
await executeMediaQueryForView(
|
||||
this,
|
||||
this.hass,
|
||||
this.cameraManager,
|
||||
this.view,
|
||||
queries,
|
||||
{
|
||||
// See 'A note on views' above for these two arguments.
|
||||
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
|
||||
targetView: mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots',
|
||||
},
|
||||
)
|
||||
)?.dispatchChangeEvent(this);
|
||||
} else if (mediaType === MediaFilterMediaType.Recordings) {
|
||||
const query: RecordingQuery = {
|
||||
type: QueryType.Recording,
|
||||
cameraIDs: cameraIDs,
|
||||
...(when && { start: when.start, end: when.end }),
|
||||
};
|
||||
const queries = new RecordingMediaQueries([
|
||||
{
|
||||
type: QueryType.Recording,
|
||||
cameraIDs: cameraIDs,
|
||||
...(limit && { limit: limit }),
|
||||
...(when && { start: when.start, end: when.end }),
|
||||
},
|
||||
]);
|
||||
|
||||
(
|
||||
await createViewForRecordings(this, this.hass, this.cameraManager, this.view, {
|
||||
query: new RecordingMediaQueries([query]),
|
||||
|
||||
// See 'A note on views' above for these two arguments.
|
||||
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
|
||||
targetView: 'recordings',
|
||||
})
|
||||
await executeMediaQueryForView(
|
||||
this,
|
||||
this.hass,
|
||||
this.cameraManager,
|
||||
this.view,
|
||||
queries,
|
||||
{
|
||||
// See 'A note on views' above for these two arguments.
|
||||
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
|
||||
targetView: 'recordings',
|
||||
},
|
||||
)
|
||||
)?.dispatchChangeEvent(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import surroundStyle from '../scss/surround.scss';
|
||||
import {
|
||||
CardWideConfig,
|
||||
ClipsOrSnapshotsOrAll,
|
||||
ExtendedHomeAssistant,
|
||||
MiniTimelineControlConfig,
|
||||
@@ -57,6 +58,9 @@ export class FrigateCardSurround extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
protected _cameraIDsForTimeline?: Set<string>;
|
||||
|
||||
/**
|
||||
@@ -68,6 +72,7 @@ export class FrigateCardSurround extends LitElement {
|
||||
protected async _fetchMedia(): Promise<void> {
|
||||
if (
|
||||
!this.cameraManager ||
|
||||
!this.cardWideConfig ||
|
||||
!this.fetchMedia ||
|
||||
this.inBackground ||
|
||||
!this.hass ||
|
||||
@@ -83,6 +88,7 @@ export class FrigateCardSurround extends LitElement {
|
||||
this,
|
||||
this.hass,
|
||||
this.cameraManager,
|
||||
this.cardWideConfig,
|
||||
this.view,
|
||||
{
|
||||
targetView: this.view.view,
|
||||
@@ -218,9 +224,9 @@ export class FrigateCardSurround extends LitElement {
|
||||
.cameraIDs=${this._cameraIDsForTimeline}
|
||||
.mini=${true}
|
||||
.timelineConfig=${this.timelineConfig}
|
||||
.thumbnailDetails=${this.thumbnailConfig?.show_details}
|
||||
.thumbnailSize=${this.thumbnailConfig?.size}
|
||||
.thumbnailConfig=${this.thumbnailConfig}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</frigate-card-timeline-core>`
|
||||
: ''}
|
||||
|
||||
@@ -30,9 +30,11 @@ import { localize } from '../localize/localize';
|
||||
import timelineCoreStyle from '../scss/timeline-core.scss';
|
||||
import {
|
||||
CameraConfig,
|
||||
CardWideConfig,
|
||||
ExtendedHomeAssistant,
|
||||
frigateCardConfigDefaults,
|
||||
FrigateCardView,
|
||||
ThumbnailsControlConfig,
|
||||
TimelineCoreConfig,
|
||||
} from '../types';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action';
|
||||
@@ -41,10 +43,9 @@ import {
|
||||
dispatchFrigateCardEvent,
|
||||
isHoverableDevice,
|
||||
} from '../utils/basic';
|
||||
|
||||
import {
|
||||
createViewForEvents,
|
||||
createViewForRecordings,
|
||||
createQueriesForRecordingsView,
|
||||
executeMediaQueryForView,
|
||||
findClosestMediaIndex,
|
||||
} from '../utils/media-to-view';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
@@ -168,10 +169,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
public timelineConfig?: TimelineCoreConfig;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public thumbnailDetails? = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public thumbnailSize?: number;
|
||||
public thumbnailConfig?: ThumbnailsControlConfig;
|
||||
|
||||
// Whether or not this is a mini-timeline (in mini-mode the component takes a
|
||||
// supportive role for other views).
|
||||
@@ -186,6 +184,9 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
@state()
|
||||
protected _locked = false;
|
||||
|
||||
@@ -228,7 +229,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
return `
|
||||
<frigate-card-timeline-thumbnail
|
||||
item='${item.id}'
|
||||
${this.thumbnailDetails ? 'details' : ''}
|
||||
${this.thumbnailConfig?.show_details ? 'details' : ''}
|
||||
>
|
||||
</frigate-card-timeline-thumbnail>`;
|
||||
}
|
||||
@@ -392,7 +393,6 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
): Promise<void> {
|
||||
const results = this.view?.queryResults;
|
||||
const media = results?.getResults();
|
||||
const cameraIDs = this._getTimelineCameraIDs();
|
||||
if (
|
||||
!media ||
|
||||
!results ||
|
||||
@@ -400,7 +400,6 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
!this.view ||
|
||||
!this.hass ||
|
||||
!this.cameraManager ||
|
||||
!cameraIDs ||
|
||||
// Skip range changes that do not have hammerjs pan directions associated
|
||||
// with them, as these outliers cause media matching issues below.
|
||||
!properties.event.additionalEvent
|
||||
@@ -418,7 +417,6 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
findClosestMediaIndex(
|
||||
media,
|
||||
targetTime,
|
||||
cameraIDs,
|
||||
properties.event.additionalEvent === 'panright' ? 'end' : 'start',
|
||||
),
|
||||
);
|
||||
@@ -467,6 +465,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
!this._timeline ||
|
||||
!this.view ||
|
||||
!this.cameraManager ||
|
||||
!this.cardWideConfig ||
|
||||
!timelineCameraIDs ||
|
||||
!properties.what
|
||||
) {
|
||||
@@ -479,34 +478,50 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
this.timelineConfig?.show_recordings &&
|
||||
['background', 'group-label'].includes(properties.what)
|
||||
) {
|
||||
view = await createViewForRecordings(
|
||||
this,
|
||||
this.hass,
|
||||
const query = createQueriesForRecordingsView(
|
||||
this.cameraManager,
|
||||
this.view,
|
||||
{
|
||||
targetTime:
|
||||
properties.what === 'background'
|
||||
? properties.time
|
||||
: this._timeline.getWindow().end,
|
||||
...(properties.group && {
|
||||
cameraIDs: new Set([String(properties.group)]),
|
||||
}),
|
||||
},
|
||||
this.cardWideConfig,
|
||||
new Set([String(properties.group)]),
|
||||
);
|
||||
if (query) {
|
||||
view = await executeMediaQueryForView(
|
||||
this,
|
||||
this.hass,
|
||||
this.cameraManager,
|
||||
this.view,
|
||||
query,
|
||||
{
|
||||
targetView: 'recording',
|
||||
targetTime:
|
||||
properties.what === 'background'
|
||||
? properties.time
|
||||
: this._timeline.getWindow().end,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if (this.timelineConfig?.show_recordings && properties.what === 'axis') {
|
||||
view = await createViewForRecordings(
|
||||
this,
|
||||
this.hass,
|
||||
const query = createQueriesForRecordingsView(
|
||||
this.cameraManager,
|
||||
this.view,
|
||||
this.cardWideConfig,
|
||||
timelineCameraIDs,
|
||||
{
|
||||
cameraIDs: timelineCameraIDs,
|
||||
start: startOfHour(properties.time),
|
||||
end: endOfHour(properties.time),
|
||||
targetTime: properties.time,
|
||||
},
|
||||
);
|
||||
if (query) {
|
||||
view = await executeMediaQueryForView(
|
||||
this,
|
||||
this.hass,
|
||||
this.cameraManager,
|
||||
this.view,
|
||||
query,
|
||||
{
|
||||
targetView: 'recording',
|
||||
targetTime: properties.time,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if (properties.item && properties.what === 'item') {
|
||||
const newResults = this.view.queryResults
|
||||
?.clone()
|
||||
@@ -618,7 +633,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
protected _createEventMediaQuerys(options?: {
|
||||
window?: TimelineWindow;
|
||||
}): EventMediaQueries | null {
|
||||
if (!this._timeline || !this._timelineSource) {
|
||||
if (!this._timeline || !this._timelineSource || !this.cardWideConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -644,15 +659,14 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
if (!this.hass || !this.cameraManager || !this.view || !query) {
|
||||
return null;
|
||||
}
|
||||
const view = await createViewForEvents(
|
||||
const view = await executeMediaQueryForView(
|
||||
this,
|
||||
this.hass,
|
||||
this.cameraManager,
|
||||
this.view,
|
||||
query,
|
||||
{
|
||||
query: query,
|
||||
targetView: options?.targetView,
|
||||
mediaType: this.timelineConfig?.media,
|
||||
},
|
||||
);
|
||||
if (!view) {
|
||||
@@ -1002,11 +1016,11 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
* @param changedProps The changed properties
|
||||
*/
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('thumbnailSize')) {
|
||||
if (this.thumbnailSize !== undefined) {
|
||||
if (changedProps.has('thumbnailConfig')) {
|
||||
if (this.thumbnailConfig) {
|
||||
this.style.setProperty(
|
||||
'--frigate-card-thumbnail-size',
|
||||
`${this.thumbnailSize}px`,
|
||||
`${this.thumbnailConfig.size}px`,
|
||||
);
|
||||
} else {
|
||||
this.style.removeProperty('--frigate-card-thumbnail-size');
|
||||
@@ -1028,7 +1042,11 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
changedProps.has('cameraIDs')
|
||||
) {
|
||||
const cameraIDs = this._getTimelineCameraIDs();
|
||||
if (cameraIDs && this.cameraManager && this.timelineConfig) {
|
||||
if (
|
||||
cameraIDs &&
|
||||
this.cameraManager &&
|
||||
this.timelineConfig
|
||||
) {
|
||||
this._timelineSource = new TimelineDataSource(
|
||||
this.cameraManager,
|
||||
cameraIDs,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import timelineStyle from '../scss/timeline.scss';
|
||||
import { ExtendedHomeAssistant, TimelineConfig } from '../types';
|
||||
import { CardWideConfig, ExtendedHomeAssistant, TimelineConfig } from '../types';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { View } from '../view/view';
|
||||
import './surround.js';
|
||||
@@ -10,7 +10,7 @@ import './timeline-core.js';
|
||||
// This file is kept separate from timeline-core.ts to avoid a circular dependency:
|
||||
// FrigateCardTimeline ->
|
||||
// FrigateCardSurround ->
|
||||
// FrigateCardTimelineCore
|
||||
// FrigateCardTimelineCore
|
||||
|
||||
@customElement('frigate-card-timeline')
|
||||
export class FrigateCardTimeline extends LitElement {
|
||||
@@ -26,6 +26,9 @@ export class FrigateCardTimeline extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
@@ -45,9 +48,9 @@ export class FrigateCardTimeline extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.timelineConfig=${this.timelineConfig}
|
||||
.thumbnailDetails=${this.timelineConfig.controls.thumbnails.show_details}
|
||||
.thumbnailSize=${this.timelineConfig.controls.thumbnails.size}
|
||||
.thumbnailConfig=${this.timelineConfig.controls.thumbnails}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</frigate-card-timeline-core>
|
||||
</frigate-card-surround>`;
|
||||
|
||||
@@ -95,7 +95,8 @@ export class FrigateCardViewer extends LitElement {
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
!this.viewerConfig ||
|
||||
!this.cameraManager
|
||||
!this.cameraManager ||
|
||||
!this.cardWideConfig
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -115,6 +116,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
this,
|
||||
this.hass,
|
||||
this.cameraManager,
|
||||
this.cardWideConfig,
|
||||
this.view,
|
||||
{
|
||||
targetView: 'recording',
|
||||
@@ -125,6 +127,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
this,
|
||||
this.hass,
|
||||
this.cameraManager,
|
||||
this.cardWideConfig,
|
||||
this.view,
|
||||
{
|
||||
targetView: 'media',
|
||||
@@ -141,6 +144,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
.thumbnailConfig=${this.viewerConfig.controls.thumbnails}
|
||||
.timelineConfig=${this.viewerConfig.controls.timeline}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
<frigate-card-viewer-carousel
|
||||
.hass=${this.hass}
|
||||
|
||||
+7
-1
@@ -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 TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting` as const;
|
||||
|
||||
@@ -213,9 +212,16 @@ export const CONF_OVERRIDES = 'overrides' 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_MEDIA_CHUNK_SIZE = `${CONF_PERFORMANCE}.features.media_chunk_size`;
|
||||
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_BORDER_RADIUS = `${CONF_PERFORMANCE}.style.border_radius`;
|
||||
|
||||
// Taken from https://github.dev/home-assistant/frontend/blob/b5861869e39290fd2e15737e89571dfc543b3ad3/src/data/media-player.ts#L93
|
||||
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;
|
||||
|
||||
@@ -127,6 +127,8 @@ import {
|
||||
CONF_VIEW_UPDATE_CYCLE_CAMERA,
|
||||
CONF_VIEW_UPDATE_FORCE,
|
||||
CONF_VIEW_UPDATE_SECONDS,
|
||||
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
|
||||
MEDIA_CHUNK_SIZE_MAX,
|
||||
} from './const.js';
|
||||
import { localize } from './localize/localize.js';
|
||||
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,
|
||||
this._defaults.performance.features.animated_progress_indicator,
|
||||
)}
|
||||
${this._renderNumberInput(CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE, {
|
||||
max: MEDIA_CHUNK_SIZE_MAX,
|
||||
})}
|
||||
`,
|
||||
)}
|
||||
${this._putInSubmenu(
|
||||
|
||||
@@ -255,7 +255,8 @@
|
||||
"warning": "This card is in low profile mode so defaults have changed to optimize performance",
|
||||
"features": {
|
||||
"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",
|
||||
"profiles": {
|
||||
|
||||
@@ -243,6 +243,24 @@
|
||||
"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"
|
||||
},
|
||||
"performance": {
|
||||
"warning": "",
|
||||
"features": {
|
||||
"editor_label": "",
|
||||
"animated_progress_indicator": "",
|
||||
"media_chunk_size": ""
|
||||
},
|
||||
"profile": "",
|
||||
"profiles": {
|
||||
"low": "",
|
||||
"high": ""
|
||||
},
|
||||
"style": {
|
||||
"editor_label": "",
|
||||
"box_shadow": "",
|
||||
"border_radius": ""
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"camera_select": "Visualizza per le telecamere appena selezionate",
|
||||
"dark_mode": "Tema scuro",
|
||||
|
||||
@@ -243,6 +243,24 @@
|
||||
"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"
|
||||
},
|
||||
"performance": {
|
||||
"warning": "",
|
||||
"features": {
|
||||
"editor_label": "",
|
||||
"animated_progress_indicator": "",
|
||||
"media_chunk_size": ""
|
||||
},
|
||||
"profile": "",
|
||||
"profiles": {
|
||||
"low": "",
|
||||
"high": ""
|
||||
},
|
||||
"style": {
|
||||
"editor_label": "",
|
||||
"box_shadow": "",
|
||||
"border_radius": ""
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"camera_select": "Visualização de câmeras recém-selecionadas",
|
||||
"dark_mode": "Modo escuro",
|
||||
|
||||
+8
-5
@@ -40,6 +40,7 @@ import {
|
||||
CONF_MENU_BUTTONS_TIMELINE,
|
||||
CONF_MENU_STYLE,
|
||||
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
|
||||
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
|
||||
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
|
||||
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
|
||||
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_TIMELINE_CONTROL]: 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_TIMELINE_CONTROL]: 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_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.
|
||||
[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.
|
||||
[CONF_PERFORMANCE_STYLE_BORDER_RADIUS]: false,
|
||||
[CONF_PERFORMANCE_STYLE_BOX_SHADOW]: false,
|
||||
|
||||
+16
-13
@@ -13,6 +13,7 @@ import {
|
||||
} from 'custom-card-helpers';
|
||||
import { StyleInfo } from 'lit/directives/style-map.js';
|
||||
import { z } from 'zod';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT, MEDIA_CHUNK_SIZE_MAX } from './const.js';
|
||||
import { deepRemoveDefaults } from './utils/zod.js';
|
||||
|
||||
// The min allowed size of buttons.
|
||||
@@ -48,7 +49,7 @@ const FRIGATE_CARD_VIEWS = [
|
||||
'media',
|
||||
] 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;
|
||||
|
||||
const FRIGATE_MENU_STYLES = [
|
||||
@@ -66,7 +67,7 @@ const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
|
||||
export const FRIGATE_MENU_PRIORITY_MAX = 100;
|
||||
|
||||
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 = [
|
||||
'all',
|
||||
@@ -74,9 +75,9 @@ const MEDIA_ACTION_NEGATIVE_CONDITIONS = [
|
||||
'hidden',
|
||||
'never',
|
||||
] as const;
|
||||
export type LazyUnloadCondition = 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 LazyUnloadCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
|
||||
export type AutoMuteCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
|
||||
export type AutoPauseCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
|
||||
|
||||
const MEDIA_ACTION_POSITIVE_CONDITIONS = [
|
||||
'all',
|
||||
@@ -84,14 +85,10 @@ const MEDIA_ACTION_POSITIVE_CONDITIONS = [
|
||||
'visible',
|
||||
'never',
|
||||
] as const;
|
||||
export type AutoUnmuteCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
|
||||
export type AutoPlayCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
|
||||
export type AutoUnmuteCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
|
||||
export type AutoPlayCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
|
||||
|
||||
const ENGINES = [
|
||||
'auto',
|
||||
'frigate',
|
||||
'generic',
|
||||
] as const;
|
||||
const ENGINES = ['auto', 'frigate', 'generic'] as const;
|
||||
|
||||
export class FrigateCardError extends Error {
|
||||
context?: unknown;
|
||||
@@ -225,7 +222,7 @@ const FRIGATE_CARD_ACTIONS = [
|
||||
'camera_select',
|
||||
'media_player',
|
||||
] as const;
|
||||
export type FrigateCardAction = typeof FRIGATE_CARD_ACTIONS[number];
|
||||
export type FrigateCardAction = (typeof FRIGATE_CARD_ACTIONS)[number];
|
||||
|
||||
const frigateCardGeneralActionSchema = frigateCardCustomactionsBaseSchema.extend({
|
||||
frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS),
|
||||
@@ -1196,6 +1193,7 @@ const performanceConfigDefault = {
|
||||
profile: 'high' as const,
|
||||
features: {
|
||||
animated_progress_indicator: true,
|
||||
media_chunk_size: MEDIA_CHUNK_SIZE_DEFAULT,
|
||||
},
|
||||
style: {
|
||||
border_radius: true,
|
||||
@@ -1211,6 +1209,11 @@ const performanceConfigSchema = z
|
||||
animated_progress_indicator: z
|
||||
.boolean()
|
||||
.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),
|
||||
style: z
|
||||
|
||||
+61
-118
@@ -1,7 +1,5 @@
|
||||
import add from 'date-fns/add';
|
||||
import sub from 'date-fns/sub';
|
||||
import { ViewContext } from 'view';
|
||||
import { ClipsOrSnapshotsOrAll, FrigateCardView } from '../types';
|
||||
import { CardWideConfig, ClipsOrSnapshotsOrAll, FrigateCardView } from '../types';
|
||||
import { View } from '../view/view';
|
||||
import {
|
||||
EventMediaQueries,
|
||||
@@ -16,71 +14,54 @@ import { dispatchFrigateCardErrorEvent } from '../components/message';
|
||||
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||
import { errorToConsole } from './basic';
|
||||
import { MediaQuery } from '../camera-manager/types';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const';
|
||||
|
||||
export const changeViewToRecentEventsForCameraAndDependents = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
cameraManager: CameraManager,
|
||||
cardWideConfig: CardWideConfig,
|
||||
view: View,
|
||||
options?: {
|
||||
mediaType?: ClipsOrSnapshotsOrAll;
|
||||
targetView?: FrigateCardView;
|
||||
},
|
||||
): 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, {
|
||||
...options,
|
||||
limit: 50, // Capture the 50 most recent events.
|
||||
await executeMediaQueryForView(element, hass, cameraManager, view, queries, {
|
||||
targetView: options?.targetView,
|
||||
})
|
||||
)?.dispatchChangeEvent(element);
|
||||
};
|
||||
|
||||
export const createViewForEvents = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
const createQueriesForEventsView = (
|
||||
cameraManager: CameraManager,
|
||||
view: View,
|
||||
cardWideConfig: CardWideConfig,
|
||||
cameraIDs: Set<string>,
|
||||
options?: {
|
||||
query?: EventMediaQueries;
|
||||
cameraIDs?: Set<string>;
|
||||
mediaType?: ClipsOrSnapshotsOrAll;
|
||||
targetCameraID?: string;
|
||||
targetView?: FrigateCardView;
|
||||
limit?: number;
|
||||
},
|
||||
): Promise<View | null> => {
|
||||
const cameras = cameraManager.getCameras();
|
||||
if (!cameras) {
|
||||
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, {
|
||||
...(options?.limit && { limit: options.limit }),
|
||||
...(options?.mediaType === 'clips' && { hasClip: true }),
|
||||
...(options?.mediaType === 'snapshots' && { hasSnapshot: true }),
|
||||
});
|
||||
if (!eventQueries) {
|
||||
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,
|
||||
): EventMediaQueries | null => {
|
||||
const limit =
|
||||
cardWideConfig.performance?.features.media_chunk_size ?? MEDIA_CHUNK_SIZE_DEFAULT;
|
||||
const eventQueries = cameraManager.generateDefaultEventQueries(cameraIDs, {
|
||||
limit: limit,
|
||||
...(options?.mediaType === 'clips' && { hasClip: true }),
|
||||
...(options?.mediaType === 'snapshots' && { hasSnapshot: true }),
|
||||
});
|
||||
return eventQueries ? new EventMediaQueries(eventQueries) : null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -96,89 +77,61 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
cameraManager: CameraManager,
|
||||
cardWideConfig: CardWideConfig,
|
||||
view: View,
|
||||
options?: {
|
||||
targetView?: 'recording' | 'recordings';
|
||||
},
|
||||
): 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, {
|
||||
...options,
|
||||
// Fetch 7 days worth of recordings (including recordings that are for the
|
||||
// current hour).
|
||||
start: sub(now, { days: 7 }),
|
||||
end: add(now, { hours: 1 }),
|
||||
await executeMediaQueryForView(element, hass, cameraManager, view, queries, {
|
||||
targetView: options?.targetView,
|
||||
})
|
||||
)?.dispatchChangeEvent(element);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
export const createQueriesForRecordingsView = (
|
||||
cameraManager: CameraManager,
|
||||
view: View,
|
||||
cardWideConfig: CardWideConfig,
|
||||
cameraIDs: Set<string>,
|
||||
options?: {
|
||||
query?: RecordingMediaQueries;
|
||||
cameraIDs?: Set<string>;
|
||||
targetCameraID?: string;
|
||||
targetView?: 'recording' | 'recordings';
|
||||
targetTime?: Date;
|
||||
start?: Date;
|
||||
end?: Date;
|
||||
},
|
||||
): Promise<View | null> => {
|
||||
const cameras = cameraManager.getCameras();
|
||||
if (!cameras) {
|
||||
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, {
|
||||
...(options?.start && { start: options.start }),
|
||||
...(options?.end && { end: options.end }),
|
||||
});
|
||||
|
||||
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,
|
||||
): RecordingMediaQueries | null => {
|
||||
const limit =
|
||||
cardWideConfig.performance?.features.media_chunk_size ?? MEDIA_CHUNK_SIZE_DEFAULT;
|
||||
const recordingQueries = cameraManager.generateDefaultRecordingQueries(cameraIDs, {
|
||||
limit: limit,
|
||||
...(options?.start && { start: options.start }),
|
||||
...(options?.end && { end: options.end }),
|
||||
});
|
||||
return recordingQueries ? new RecordingMediaQueries(recordingQueries) : null;
|
||||
};
|
||||
|
||||
const executeMediaQueryForView = async (
|
||||
export const executeMediaQueryForView = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
cameraManager: CameraManager,
|
||||
view: View,
|
||||
query: MediaQueries,
|
||||
options?: {
|
||||
cameraIDs?: Set<string>;
|
||||
targetCameraID?: string;
|
||||
targetView?: FrigateCardView;
|
||||
targetTime?: Date;
|
||||
@@ -207,9 +160,9 @@ const executeMediaQueryForView = async (
|
||||
const queryResults = new MediaQueriesResults(mediaArray, selectedIndex);
|
||||
let viewerContext: ViewContext | undefined = {};
|
||||
|
||||
if (options?.targetTime && options.cameraIDs) {
|
||||
if (options?.targetTime) {
|
||||
queryResults.selectBestResult((media) =>
|
||||
findClosestMediaIndex(media, options.targetTime as Date, options.cameraIDs),
|
||||
findClosestMediaIndex(media, options.targetTime as Date),
|
||||
);
|
||||
viewerContext = {
|
||||
mediaViewer: {
|
||||
@@ -234,7 +187,6 @@ const executeMediaQueryForView = async (
|
||||
* Find the closest matching media object.
|
||||
* @param mediaArray The media. Must be sorted most recent first.
|
||||
* @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
|
||||
* event/recording. If not specified, the first match is returned rather than
|
||||
* the best match.
|
||||
@@ -243,7 +195,6 @@ const executeMediaQueryForView = async (
|
||||
export const findClosestMediaIndex = (
|
||||
mediaArray: ViewMedia[],
|
||||
targetTime: Date,
|
||||
cameraIDs?: Set<string>,
|
||||
refPoint?: 'start' | 'end',
|
||||
): number | null => {
|
||||
let bestMatch:
|
||||
@@ -253,15 +204,7 @@ export const findClosestMediaIndex = (
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (!cameraIDs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const [i, media] of mediaArray.entries()) {
|
||||
if (!cameraIDs.has(media.getCameraID())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (media.includesTime(targetTime)) {
|
||||
const start = media.getStartTime();
|
||||
const end = media.getEndTime();
|
||||
|
||||
@@ -132,7 +132,7 @@ export class TimelineDataSource {
|
||||
}
|
||||
|
||||
const mediaArray = await this._cameraManager.executeMediaQueries(hass, eventQueries);
|
||||
const data: FrigateCardTimelineItem[] = []
|
||||
const data: FrigateCardTimelineItem[] = [];
|
||||
for (const media of mediaArray ?? []) {
|
||||
const endTime = media.getEndTime();
|
||||
const startTime = media.getStartTime();
|
||||
|
||||
Reference in New Issue
Block a user