diff --git a/src/camera-manager/engine.ts b/src/camera-manager/engine.ts index 22fc836f..6e186f51 100644 --- a/src/camera-manager/engine.ts +++ b/src/camera-manager/engine.ts @@ -14,7 +14,7 @@ import { RecordingQueryResultsMap, RecordingSegmentsQuery, RecordingSegmentsQueryResultsMap, - CameraManagerEngineCapabilities, + CameraManagerCameraCapabilities, CameraManagerMediaCapabilities, CameraManagerCameraMetadata, } from './types'; @@ -100,6 +100,9 @@ export interface CameraManagerEngine { cameraConfig: CameraConfig, ): CameraManagerCameraMetadata; - getCapabilities(): CameraManagerEngineCapabilities | null; + getCameraCapabilities( + cameraConfig: CameraConfig, + ): CameraManagerCameraCapabilities | null; + getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null; } diff --git a/src/camera-manager/frigate/engine-frigate.ts b/src/camera-manager/frigate/engine-frigate.ts index 98fc83f0..84cf5f55 100644 --- a/src/camera-manager/frigate/engine-frigate.ts +++ b/src/camera-manager/frigate/engine-frigate.ts @@ -13,7 +13,7 @@ import { import { DateRange } from '../range'; import { CameraManagerCameraMetadata, - CameraManagerEngineCapabilities, + CameraManagerCameraCapabilities, CameraManagerMediaCapabilities, DataQuery, Engine, @@ -805,10 +805,14 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine { return seekMilliseconds / 1000; } - public getCapabilities(): CameraManagerEngineCapabilities { + public getCameraCapabilities( + cameraConfig: CameraConfig, + ): CameraManagerCameraCapabilities { + const isBirdseye = cameraConfig.frigate.camera_name === CAMERA_BIRDSEYE; return { - canFavoriteEvents: true, - canFavoriteRecordings: false, + canFavoriteEvents: !isBirdseye, + canFavoriteRecordings: !isBirdseye, + supportsTimeline: !isBirdseye, }; } diff --git a/src/camera-manager/manager.ts b/src/camera-manager/manager.ts index 318f8fc0..d53c6524 100644 --- a/src/camera-manager/manager.ts +++ b/src/camera-manager/manager.ts @@ -2,6 +2,7 @@ import { HomeAssistant } from 'custom-card-helpers'; import { CameraConfig, CardWideConfig } from '../types.js'; import { allPromises, arrayify, setify } from '../utils/basic.js'; import { + CameraManagerCameraCapabilities, CameraManagerCameraMetadata, CameraManagerCapabilities, CameraManagerMediaCapabilities, @@ -312,22 +313,6 @@ export class CameraManager { return engine.getMediaDownloadPath(cameraConfig, media); } - public getCapabilities(): CameraManagerCapabilities | null { - const engines = this._engineFactory.getAllEngines(this._cameras); - if (!engines) { - return null; - } - - return { - canFavoriteEvents: engines.some( - (engine) => engine.getCapabilities()?.canFavoriteEvents, - ), - canFavoriteRecordings: engines.some( - (engine) => engine.getCapabilities()?.canFavoriteRecordings, - ), - }; - } - public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null { const engine = this._engineFactory.getEngineForMedia(this._cameras, media); if (!engine) { @@ -534,12 +519,45 @@ export class CameraManager { public getCameraMetadata( hass: HomeAssistant, - cameraConfig?: CameraConfig, + cameraID: string, ): CameraManagerCameraMetadata | null { + const cameraConfig = this._cameras.get(cameraID); const engine = this._engineFactory.getEngineForCamera(cameraConfig); if (!engine || !cameraConfig) { return null; } return engine.getCameraMetadata(hass, cameraConfig); } + + public getCameraCapabilities( + cameraID: string, + ): CameraManagerCameraCapabilities | null { + const cameraConfig = this._cameras.get(cameraID); + if (!cameraConfig) { + return null; + } + + const engine = this._engineFactory.getEngineForCamera(cameraConfig); + if (!engine) { + return null; + } + + return engine.getCameraCapabilities(cameraConfig); + } + + public getAggregateCameraCapabilities( + cameraIDs?: Set, + ): CameraManagerCapabilities { + const perCameraCapabilities = [...(cameraIDs ?? this._cameras.keys())].map( + (cameraID) => this.getCameraCapabilities(cameraID), + ); + + return { + canFavoriteEvents: perCameraCapabilities.some((cap) => cap?.canFavoriteEvents), + canFavoriteRecordings: perCameraCapabilities.some( + (cap) => cap?.canFavoriteRecordings, + ), + supportsTimeline: perCameraCapabilities.some((cap) => cap?.supportsTimeline), + }; + } } diff --git a/src/camera-manager/types.ts b/src/camera-manager/types.ts index 2e85a6d0..88d49c4c 100644 --- a/src/camera-manager/types.ts +++ b/src/camera-manager/types.ts @@ -85,10 +85,11 @@ export interface MediaMetadata { interface BaseCapabilities { canFavoriteEvents: boolean; canFavoriteRecordings: boolean; + supportsTimeline: boolean; } export type CameraManagerCapabilities = BaseCapabilities; -export type CameraManagerEngineCapabilities = BaseCapabilities; +export type CameraManagerCameraCapabilities = BaseCapabilities; export interface CameraManagerMediaCapabilities { canFavorite: boolean; } diff --git a/src/card.ts b/src/card.ts index 20458682..eebdae04 100644 --- a/src/card.ts +++ b/src/card.ts @@ -399,12 +399,12 @@ class FrigateCard extends LitElement { }); if (this._cameras && this._cameras.size > 1) { - const menuItems = Array.from(this._cameras, ([camera, config]) => { + const menuItems = Array.from(this._cameras, ([cameraID, config]) => { const action = createFrigateCardCustomAction('camera_select', { - camera: camera, + camera: cameraID, }); const metadata = this._hass - ? this._cameraManager?.getCameraMetadata(this._hass, config) ?? undefined + ? this._cameraManager?.getCameraMetadata(this._hass, cameraID) ?? undefined : undefined; return { @@ -413,7 +413,7 @@ class FrigateCard extends LitElement { entity: config.camera_entity, state_color: true, title: metadata?.title, - selected: this._view?.camera === camera, + selected: this._view?.camera === cameraID, ...(action && { tap_action: action }), }; }); @@ -1427,7 +1427,9 @@ class FrigateCard extends LitElement { } else if (this._view?.is('live') && cameraEntity) { media_content_id = `media-source://camera/${cameraEntity}`; media_content_type = 'application/vnd.apple.mpegurl'; - title = this._cameraManager.getCameraMetadata(this._hass, cameraConfig)?.title ?? null; + title = + this._cameraManager.getCameraMetadata(this._hass, this._view.camera)?.title ?? + null; thumbnail = this._hass?.states[cameraEntity]?.attributes?.entity_picture ?? null; } diff --git a/src/components/gallery.ts b/src/components/gallery.ts index f786938d..27158c30 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -343,7 +343,6 @@ export class FrigateCardGalleryCore extends LitElement { .hass=${this.hass} .cameraManager=${this.cameraManager} .media=${media} - .cameraConfig=${this.cameras?.get(media.getCameraID())} .view=${this.view} ?details=${!!this.galleryConfig?.controls.thumbnails.show_details} ?show_favorite_control=${!!this.galleryConfig?.controls.thumbnails diff --git a/src/components/live/live.ts b/src/components/live/live.ts index e7f68134..2b5da9a5 100644 --- a/src/components/live/live.ts +++ b/src/components/live/live.ts @@ -489,7 +489,7 @@ export class FrigateCardLiveCarousel extends LitElement { } protected _renderLive( - camera: string, + cameraID: string, cameraConfig: CameraConfig, slideIndex: number, ): TemplateResult | void { @@ -501,7 +501,7 @@ export class FrigateCardLiveCarousel extends LitElement { // is rendering right now. const conditionState = { ...this.conditionState, - camera: camera, + camera: cameraID, }; const config = getOverriddenConfig( @@ -510,7 +510,7 @@ export class FrigateCardLiveCarousel extends LitElement { conditionState, ) as LiveConfig; - const cameraMetadata = this.cameraManager.getCameraMetadata(this.hass, cameraConfig); + const cameraMetadata = this.cameraManager.getCameraMetadata(this.hass, cameraID); return html`
@@ -560,7 +560,6 @@ export class FrigateCardLiveCarousel extends LitElement { if ( !slides.length || !this.liveConfig || - !this.cameras || !this.view || !this.hass || !this.cameraManager @@ -578,15 +577,15 @@ export class FrigateCardLiveCarousel extends LitElement { const cameraMetadataPrevious = prevID ? this.cameraManager.getCameraMetadata( this.hass, - this.cameras.get(prevID), + prevID, ) : null; const cameraMetadataCurrent = this.cameraManager.getCameraMetadata( this.hass, - this.cameras.get(this.view.camera), + this.view.camera, ); const cameraMetadataNext = nextID ? this.cameraManager.getCameraMetadata( this.hass, - this.cameras.get(nextID), + nextID, ) : null; // Notes on the below: diff --git a/src/components/media-filter.ts b/src/components/media-filter.ts index f8fd3f1a..e5a26697 100644 --- a/src/components/media-filter.ts +++ b/src/components/media-filter.ts @@ -272,14 +272,12 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) { protected willUpdate(changedProps: PropertyValues): void { if (changedProps.has('cameras') && this.cameras) { - this._cameraOptions = Array.from(this.cameras.entries()).map( - ([cameraID, cameraConfig]) => ({ - value: cameraID, - label: this.hass - ? this.cameraManager?.getCameraMetadata(this.hass, cameraConfig)?.title ?? '' - : '', - }), - ); + this._cameraOptions = Array.from(this.cameras.keys()).map((cameraID) => ({ + value: cameraID, + label: this.hass + ? this.cameraManager?.getCameraMetadata(this.hass, cameraID)?.title ?? '' + : '', + })); } if (changedProps.has('cameraManager') && this.hass && this.cameraManager) { @@ -413,7 +411,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) { const areRecordings = !!( this.view?.query && MediaQueriesClassifier.areRecordingQueries(this.view.query) ); - const managerCapabilities = this.cameraManager?.getCapabilities(); + const managerCapabilities = this.cameraManager?.getAggregateCameraCapabilities(); // Which media controls are shown depends on the view. const showFavoriteControl = areEvents diff --git a/src/components/surround.ts b/src/components/surround.ts index 6affa9b4..9e85a249 100644 --- a/src/components/surround.ts +++ b/src/components/surround.ts @@ -190,7 +190,6 @@ export class FrigateCardSurround extends LitElement { .config=${this.thumbnailConfig} .cameraManager=${this.cameraManager} .view=${this.view} - .cameras=${this.cameras} .selected=${this.view.queryResults?.getSelectedIndex() ?? undefined} @frigate-card:view:change=${(ev: CustomEvent) => changeDrawer(ev, 'close')} @frigate-card:thumbnail-carousel:tap=${( diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index 4e9625b7..b0283f6b 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -39,9 +39,6 @@ export class FrigateCardThumbnailCarousel extends LitElement { @property({ attribute: false }) public view?: Readonly; - @property({ attribute: false }) - public cameras?: Map; - @property({ attribute: false }) public cameraManager?: CameraManager; @@ -150,8 +147,7 @@ export class FrigateCardThumbnailCarousel extends LitElement { */ protected _renderThumbnail(index: number): TemplateResult | void { const media = this.view?.queryResults?.getResult(index) ?? null; - const cameraConfig = media ? this.cameras?.get(media.getCameraID()) : null; - if (!media || !cameraConfig || !this.view) { + if (!media || !this.view) { return; } @@ -166,7 +162,6 @@ export class FrigateCardThumbnailCarousel extends LitElement { .cameraManager=${this.cameraManager} .hass=${this.hass} .media=${media} - .cameraConfig=${cameraConfig} .view=${this.view} .seek=${seekTarget && media.includesTime(seekTarget) ? seekTarget : undefined} ?details=${!!this.config?.show_details} diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts index 65170344..e20f06b3 100644 --- a/src/components/thumbnail.ts +++ b/src/components/thumbnail.ts @@ -21,7 +21,7 @@ import { createFetchThumbnailTask, FetchThumbnailTaskArgs } from '../utils/thumb import { View } from '../view/view.js'; import { Task, TaskStatus } from '@lit-labs/task'; -import type { CameraConfig, ExtendedHomeAssistant } from '../types.js'; +import type { ExtendedHomeAssistant } from '../types.js'; import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js'; import { CameraManager } from '../camera-manager/manager.js'; import { ViewMediaClassifier } from '../view/media-classifier.js'; @@ -104,12 +104,12 @@ export class FrigateCardThumbnailFeatureEvent extends LitElement { return html`${this.thumbnail ? renderTask( - this, - this._embedThumbnailTask, - (embeddedThumbnail: string | null) => - embeddedThumbnail ? html`` : html``, - { inProgressFunc: () => imageOff }, - ) + this, + this._embedThumbnailTask, + (embeddedThumbnail: string | null) => + embeddedThumbnail ? html`` : html``, + { inProgressFunc: () => imageOff }, + ) : imageOff} `; } @@ -162,7 +162,7 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement { return html`
${what ? html`
${prettifyTitle(what.join(', '))}
` : ``} ${startTime - ? html`
+ ? html`
${localize('event.start')}: ${format(startTime, 'HH:mm:ss')}
@@ -170,17 +170,17 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement { ${localize('event.duration')}: ${endTime - ? getDurationString(startTime, endTime) - : localize('event.in_progress')}
` - : ``} + : ``} ${this.seek - ? html`
+ ? html`
${localize('event.seek')} ${format(this.seek, 'HH:mm:ss')}
` - : html``} + : html``}
${score ? html`
@@ -213,11 +213,11 @@ export class FrigateCardThumbnailDetailsRecording extends LitElement { return html`
${this.cameraTitle ?? ''}
${this.seek - ? html`
+ ? html`
${localize('recording.seek')} ${format(this.seek, 'HH:mm:ss')}
` - : html``} + : html``}
${eventCount !== null ? html`
@@ -245,9 +245,6 @@ export class FrigateCardThumbnail extends LitElement { @property({ attribute: true }) public media?: ViewMedia; - @property({ attribute: false }) - public cameraConfig?: CameraConfig; - @property({ attribute: true, type: Boolean }) public details = false; @@ -268,7 +265,7 @@ export class FrigateCardThumbnail extends LitElement { * @returns A template to display to the user. */ protected render(): TemplateResult | void { - if (!this.media || !this.cameraConfig || !this.cameraManager || !this.hass) { + if (!this.media || !this.cameraManager || !this.hass) { return; } @@ -293,7 +290,10 @@ export class FrigateCardThumbnail extends LitElement { this.hass && this.cameraManager?.getMediaCapabilities(this.media)?.canFavorite; - const cameraTitle = this.cameraManager.getCameraMetadata(this.hass, this.cameraConfig)?.title; + const cameraTitle = this.cameraManager.getCameraMetadata( + this.hass, + this.media.getCameraID(), + )?.title; return html` ${ViewMediaClassifier.isEvent(this.media) ? html`` : ViewMediaClassifier.isRecording(this.media) - ? html`` - : html``} + : html``} ${shouldShowFavoriteControl - ? html` { - stopEventFromActivatingCardWideActions(ev); - if (this.hass && this.media) { - try { - await this.cameraManager?.favoriteMedia( - this.hass, - this.media, - !this.media?.isFavorite(), - ); - } catch (e) { - errorToConsole(e as Error); - return; + stopEventFromActivatingCardWideActions(ev); + if (this.hass && this.media) { + try { + await this.cameraManager?.favoriteMedia( + this.hass, + this.media, + !this.media?.isFavorite(), + ); + } catch (e) { + errorToConsole(e as Error); + return; + } + this.requestUpdate(); } - this.requestUpdate(); - } - }} + }} />` - : ``} + : ``} ${this.details && ViewMediaClassifier.isEvent(this.media) - ? html`` - : this.details && ViewMediaClassifier.isRecording(this.media) - ? html`` - : html``} + : html``} ${shouldShowTimelineControl - ? html`` - : ''}`; + : ''}`; } /** diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index ed584496..ccc708b5 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -149,7 +149,6 @@ export class FrigateCardTimelineThumbnail extends LitElement { return html` - { - this._locked = !this._locked; - }} - aria-label="${this._locked - ? localize('timeline.unlock') - : localize('timeline.lock')}" - title="${this._locked ? localize('timeline.unlock') : localize('timeline.lock')}" - > - -
`; + const capabilities = this.cameraManager?.getAggregateCameraCapabilities( + this._getTimelineCameraIDs(), + ); + + return html` ${capabilities?.supportsTimeline + ? html`
+ { + this._locked = !this._locked; + }} + aria-label="${this._locked + ? localize('timeline.unlock') + : localize('timeline.lock')}" + title="${this._locked + ? localize('timeline.unlock') + : localize('timeline.lock')}" + > + +
` + : ''}`; } /** @@ -686,15 +693,10 @@ export class FrigateCardTimelineCore extends LitElement { if (!this.hass || !cameraConfig || !this.cameraManager) { return; } - const cameraMetadata = this.cameraManager.getCameraMetadata( - this.hass, - cameraConfig, - ); - if ( - cameraMetadata && - cameraConfig.frigate.camera_name && - cameraConfig.frigate.camera_name !== CAMERA_BIRDSEYE - ) { + const cameraMetadata = this.cameraManager.getCameraMetadata(this.hass, cameraID); + const cameraCapabilities = this.cameraManager.getCameraCapabilities(cameraID); + + if (cameraMetadata && cameraCapabilities?.supportsTimeline) { groups.push({ id: cameraID, content: cameraMetadata.title,