diff --git a/src/camera/engine.ts b/src/camera/engine.ts index a090bb8f..22fc836f 100644 --- a/src/camera/engine.ts +++ b/src/camera/engine.ts @@ -16,6 +16,7 @@ import { RecordingSegmentsQueryResultsMap, CameraManagerEngineCapabilities, CameraManagerMediaCapabilities, + CameraManagerCameraMetadata, } from './types'; export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000; @@ -94,6 +95,11 @@ export interface CameraManagerEngine { cameras: Map, ): Promise; + getCameraMetadata( + hass: HomeAssistant, + cameraConfig: CameraConfig, + ): CameraManagerCameraMetadata; + getCapabilities(): CameraManagerEngineCapabilities | null; getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null; } diff --git a/src/camera/frigate/engine-frigate.ts b/src/camera/frigate/engine-frigate.ts index 90326cd3..3e7c6b5b 100644 --- a/src/camera/frigate/engine-frigate.ts +++ b/src/camera/frigate/engine-frigate.ts @@ -12,6 +12,7 @@ import { } from '../engine'; import { DateRange } from '../range'; import { + CameraManagerCameraMetadata, CameraManagerEngineCapabilities, CameraManagerMediaCapabilities, DataQuery, @@ -48,13 +49,19 @@ import { } from './requests'; import orderBy from 'lodash-es/orderBy'; import throttle from 'lodash-es/throttle'; -import { allPromises, formatDate, runWhenIdleIfSupported } from '../../utils/basic'; +import { + allPromises, + formatDate, + prettifyTitle, + runWhenIdleIfSupported, +} from '../../utils/basic'; import { fromUnixTime } from 'date-fns'; import { sum } from 'lodash-es'; import { FrigateViewMediaClassifier } from './media-classifier'; import { ViewMediaClassifier } from '../../view/media-classifier'; import { FrigateViewMediaFactory } from './media'; import { log } from '../../utils/debug'; +import { getEntityIcon, getEntityTitle } from '../../utils/ha'; const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60; const RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS = 60; @@ -559,10 +566,10 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine { continue; } const media = FrigateViewMediaFactory.createRecordingViewMedia( - hass, recording.cameraID, recording, cameraConfig, + this.getCameraMetadata(hass, cameraConfig).title, ); if (media) { output.push(media); @@ -809,4 +816,23 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine { canFavorite: ViewMediaClassifier.isEvent(media), }; } + + public getCameraMetadata( + hass: HomeAssistant, + cameraConfig: CameraConfig, + ): CameraManagerCameraMetadata { + return { + title: + cameraConfig.title ?? + getEntityTitle(hass, cameraConfig.camera_entity) ?? + getEntityTitle(hass, cameraConfig.webrtc_card?.entity) ?? + prettifyTitle(cameraConfig.frigate?.camera_name) ?? + cameraConfig.id ?? + '', + icon: + cameraConfig?.icon ?? + getEntityIcon(hass, cameraConfig.camera_entity) ?? + 'mdi:video', + }; + } } diff --git a/src/camera/frigate/media.ts b/src/camera/frigate/media.ts index 196ba49f..0b5f5a3e 100644 --- a/src/camera/frigate/media.ts +++ b/src/camera/frigate/media.ts @@ -156,10 +156,10 @@ export class FrigateViewMediaFactory { } static createRecordingViewMedia( - hass: HomeAssistant, cameraID: string, recording: FrigateRecording, cameraConfig: CameraConfig, + cameraTitle: string, ): FrigateRecordingViewMedia | null { if (!cameraConfig.frigate.client_id || !cameraConfig.frigate.camera_name) { return null; @@ -175,7 +175,7 @@ export class FrigateViewMediaFactory { cameraConfig.frigate.camera_name, recording, ), - getRecordingTitle(hass, cameraConfig, recording), + getRecordingTitle(cameraTitle, recording), ); } } diff --git a/src/camera/frigate/util.ts b/src/camera/frigate/util.ts index fe8d6075..0be47429 100644 --- a/src/camera/frigate/util.ts +++ b/src/camera/frigate/util.ts @@ -1,8 +1,6 @@ -import { HomeAssistant } from 'custom-card-helpers'; import utcToZonedTime from 'date-fns-tz/utcToZonedTime'; import { CameraConfig, ClipsOrSnapshots } from '../../types'; import { formatDateAndTime, prettifyTitle } from '../../utils/basic'; -import { getCameraTitle } from '../../utils/camera'; import { FrigateEvent, FrigateRecording } from './types'; /** @@ -24,14 +22,10 @@ export const getEventTitle = (event: FrigateEvent): string => { }; export const getRecordingTitle = ( - hass: HomeAssistant, - cameraConfig: CameraConfig, + cameraTitle: string, recording: FrigateRecording, ): string => { - const cameraTitle = getCameraTitle(hass, cameraConfig); - return `${cameraTitle ? `${cameraTitle} ` : ''}${formatDateAndTime( - recording.startTime, - )}`; + return `${cameraTitle} ${formatDateAndTime(recording.startTime)}`; }; /** diff --git a/src/camera/manager.ts b/src/camera/manager.ts index 0017b059..6c04cdfa 100644 --- a/src/camera/manager.ts +++ b/src/camera/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 { + CameraManagerCameraMetadata, CameraManagerCapabilities, CameraManagerMediaCapabilities, DataQuery, @@ -539,4 +540,15 @@ export class CameraManager { 'desc', ); } + + public getCameraMetadata( + hass: HomeAssistant, + cameraConfig?: CameraConfig, + ): CameraManagerCameraMetadata | null { + const engine = this._engineFactory.getEngineForCamera(cameraConfig); + if (!engine || !cameraConfig) { + return null; + } + return engine.getCameraMetadata(hass, cameraConfig); + } } diff --git a/src/camera/types.ts b/src/camera/types.ts index 109fd439..c7ca2380 100644 --- a/src/camera/types.ts +++ b/src/camera/types.ts @@ -85,6 +85,11 @@ export interface CameraManagerMediaCapabilities { canFavorite: boolean; } +export interface CameraManagerCameraMetadata { + title: string; + icon: string; +} + // =========== // Event Query // =========== diff --git a/src/card.ts b/src/card.ts index 08355943..df7d5e37 100644 --- a/src/card.ts +++ b/src/card.ts @@ -67,7 +67,7 @@ import { getActionConfigGivenAction, } from './utils/action.js'; import { contentsChanged, errorToConsole } from './utils/basic.js'; -import { getCameraIcon, getCameraID, getCameraTitle } from './utils/camera.js'; +import { getCameraID } from './utils/camera.js'; import { getEntityIcon, getEntityTitle, @@ -403,12 +403,16 @@ export class FrigateCard extends LitElement { const action = createFrigateCardCustomAction('camera_select', { camera: camera, }); + const metadata = this._hass + ? this._cameraManager?.getCameraMetadata(this._hass, config) ?? undefined + : undefined; + return { enabled: true, - icon: getCameraIcon(this._hass, config), + icon: metadata?.icon, entity: config.camera_entity, state_color: true, - title: getCameraTitle(this._hass, config), + title: metadata?.title, selected: this._view?.camera === camera, ...(action && { tap_action: action }), }; @@ -1104,7 +1108,8 @@ export class FrigateCard extends LitElement { */ protected willUpdate(changedProps: PropertyValues): void { if ( - this._cameras && this._cardWideConfig && + this._cameras && + this._cardWideConfig && (changedProps.has('_config') || changedProps.has('_cameras') || changedProps.has('_cardWideConfig')) @@ -1393,7 +1398,12 @@ export class FrigateCard extends LitElement { * @returns */ protected _mediaPlayerAction(mediaPlayer: string, action: 'play' | 'stop'): void { - if (!['play', 'stop'].includes(action) || !this._view) { + if ( + !['play', 'stop'].includes(action) || + !this._view || + !this._hass || + !this._cameraManager + ) { return; } @@ -1417,7 +1427,7 @@ export 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 = getCameraTitle(this._hass, cameraConfig); + title = this._cameraManager.getCameraMetadata(this._hass, cameraConfig)?.title ?? null; thumbnail = this._hass?.states[cameraEntity]?.attributes?.entity_picture ?? null; } diff --git a/src/components/live/live.ts b/src/components/live/live.ts index 91f0600b..f21d736b 100644 --- a/src/components/live/live.ts +++ b/src/components/live/live.ts @@ -32,7 +32,6 @@ import { } from '../../types.js'; import { stopEventFromActivatingCardWideActions } from '../../utils/action.js'; import { contentsChanged } from '../../utils/basic.js'; -import { getCameraIcon, getCameraTitle } from '../../utils/camera.js'; import { dispatchExistingMediaLoadedInfoAsEvent, dispatchMediaUnloadedEvent, @@ -87,7 +86,7 @@ export const getStateObjOrDispatchError = ( if (stateObj.state === 'unavailable') { dispatchMessageEvent(element, localize('error.live_camera_unavailable'), 'info', { icon: 'mdi:connection', - context: getCameraTitle(hass, cameraConfig), + context: cameraConfig, }); return null; } @@ -261,6 +260,7 @@ export class FrigateCardLive extends LitElement { .conditionState=${this.conditionState} .liveOverrides=${this.liveOverrides} .cardWideConfig=${this.cardWideConfig} + .cameraManager=${this.cameraManager} > `, @@ -304,6 +304,9 @@ export class FrigateCardLiveCarousel extends LitElement { @property({ attribute: false }) public cardWideConfig?: CardWideConfig; + @property({ attribute: false }) + public cameraManager?: CameraManager; + // Index between camera name and slide number. protected _cameraToSlide: Record = {}; protected _refMediaCarousel: Ref = createRef(); @@ -317,10 +320,7 @@ export class FrigateCardLiveCarousel extends LitElement { const frigateCardMediaCarousel = this._refMediaCarousel.value; - if ( - frigateCardMediaCarousel && - changedProperties.has('inBackground') - ) { + if (frigateCardMediaCarousel && changedProperties.has('inBackground')) { // If this has changed to be in the background (i.e. preloaded but not // visible) take the appropriate play/pause/mute/unmute actions. if (this.inBackground) { @@ -493,7 +493,7 @@ export class FrigateCardLiveCarousel extends LitElement { cameraConfig: CameraConfig, slideIndex: number, ): TemplateResult | void { - if (!this.liveConfig) { + if (!this.liveConfig || !this.hass || !this.cameraManager) { return; } // The conditionState object contains the currently live camera, which (in @@ -510,12 +510,14 @@ export class FrigateCardLiveCarousel extends LitElement { conditionState, ) as LiveConfig; + const cameraMetadata = this.cameraManager.getCameraMetadata(this.hass, cameraConfig); + return html`
{ this._setViewCameraID(prevID); @@ -619,8 +640,8 @@ export class FrigateCardLiveCarousel extends LitElement { .hass=${this.hass} .direction=${'next'} .controlConfig=${config.controls.next_previous} - .label=${getCameraTitle(this.hass, nextID ? this.cameras.get(nextID) : null)} - .icon=${getCameraIcon(this.hass, nextID ? this.cameras.get(nextID) : null)} + .label=${cameraMetadataNext?.title ?? ''} + .icon=${cameraMetadataNext?.icon} ?disabled=${nextID === null} @click=${(ev) => { this._setViewCameraID(nextID); diff --git a/src/components/media-filter.ts b/src/components/media-filter.ts index d1c5c3a9..d1a81e50 100644 --- a/src/components/media-filter.ts +++ b/src/components/media-filter.ts @@ -19,7 +19,6 @@ import { customElement, property } from 'lit/decorators.js'; import { CameraManager } from '../camera/manager'; import { DateRange } from '../camera/range'; import { CameraConfig, ExtendedHomeAssistant } from '../types'; -import { getCameraTitle } from '../utils/camera'; import { View } from '../view/view'; import { MediaFilterControls, @@ -193,7 +192,9 @@ export class FrigateCardMediaFilter extends LitElement { this._cameraOptions = Array.from(this.cameras.entries()).map( ([cameraID, cameraConfig]) => ({ value: cameraID, - label: getCameraTitle(this.hass, cameraConfig), + label: this.hass + ? this.cameraManager?.getCameraMetadata(this.hass, cameraConfig)?.title ?? '' + : '', }), ); } diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts index c9de4450..e7256366 100644 --- a/src/components/thumbnail.ts +++ b/src/components/thumbnail.ts @@ -16,7 +16,6 @@ import thumbnailFeatureRecordingStyle from '../scss/thumbnail-feature-recording. import thumbnailStyle from '../scss/thumbnail.scss'; import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; import { errorToConsole, getDurationString, prettifyTitle } from '../utils/basic.js'; -import { getCameraTitle } from '../utils/camera.js'; import { renderTask } from '../utils/task.js'; import { createFetchThumbnailTask, FetchThumbnailTaskArgs } from '../utils/thumbnail.js'; import { View } from '../view/view.js'; @@ -270,7 +269,7 @@ export class FrigateCardThumbnail extends LitElement { * @returns A template to display to the user. */ protected render(): TemplateResult | void { - if (!this.media || !this.cameraConfig) { + if (!this.media || !this.cameraConfig || !this.cameraManager || !this.hass) { return; } @@ -295,6 +294,8 @@ export class FrigateCardThumbnail extends LitElement { this.hass && this.cameraManager?.getMediaCapabilities(this.media)?.canFavorite; + const cameraTitle = this.cameraManager.getCameraMetadata(this.hass, this.cameraConfig)?.title; + return html` ${ViewMediaClassifier.isEvent(this.media) ? html`` : html``} @@ -343,7 +342,7 @@ export class FrigateCardThumbnail extends LitElement { : this.details && ViewMediaClassifier.isRecording(this.media) ? html`` : html``} diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index 67269885..58cdb705 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -42,7 +42,7 @@ import { dispatchFrigateCardEvent, isHoverableDevice, } from '../utils/basic'; -import { getAllDependentCameras, getCameraTitle } from '../utils/camera.js'; +import { getAllDependentCameras } from '../utils/camera.js'; import { createViewForEvents, @@ -675,16 +675,22 @@ export class FrigateCardTimelineCore extends LitElement { this._getTimelineCameraIDs().forEach((cameraID) => { const cameraConfig = this.cameras?.get(cameraID); - if (cameraConfig) { - if ( - cameraConfig.frigate.camera_name && - cameraConfig.frigate.camera_name !== CAMERA_BIRDSEYE - ) { - groups.push({ - id: cameraID, - content: getCameraTitle(this.hass, cameraConfig), - }); - } + 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 + ) { + groups.push({ + id: cameraID, + content: cameraMetadata.title, + }); } }); return new DataSet(groups); diff --git a/src/editor.ts b/src/editor.ts index 00100d3e..bb5fa1b1 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -140,10 +140,14 @@ import { THUMBNAIL_WIDTH_MAX, THUMBNAIL_WIDTH_MIN, } from './types.js'; -import { arrayMove } from './utils/basic.js'; -import { getCameraID, getCameraTitle } from './utils/camera.js'; +import { arrayMove, prettifyTitle } from './utils/basic.js'; +import { getCameraID } from './utils/camera.js'; import { FRIGATE_ICON_SVG_PATH } from './camera/frigate/icon.js'; -import { getEntitiesFromHASS, sideLoadHomeAssistantElements } from './utils/ha'; +import { + getEntitiesFromHASS, + getEntityTitle, + sideLoadHomeAssistantElements, +} from './utils/ha'; import { setLowPerformanceProfile } from './performance.js'; const MENU_BUTTONS = 'buttons'; @@ -708,8 +712,29 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor cameraIndex: number, cameraConfig: RawFrigateCardConfig, ): string { + // Attempt to render a recognizable name for the camera, starting with the + // most likely to be useful and working our ways towards the least useful. + // This is only used for the editor since the card itself can use the + // cameraManager. return ( - getCameraTitle(this.hass, cameraConfig) || + (typeof cameraConfig?.title === 'string' && cameraConfig.title) || + (typeof cameraConfig?.camera_entity === 'string' + ? getEntityTitle(this.hass, cameraConfig.camera_entity) + : '') || + (typeof cameraConfig?.webrtc_card === 'object' && + cameraConfig.webrtc_card && + typeof cameraConfig.webrtc_card['entity'] === 'string' && + cameraConfig.webrtc_card['entity']) || + // Usage of engine specific logic here is allowed as an exception, since + // the camera manager cannot be started with an unparsed and unloaded + // config. + (typeof cameraConfig?.frigate === 'object' && + cameraConfig.frigate && + typeof cameraConfig?.frigate['camera_name'] === 'string' && + cameraConfig.frigate['camera_name'] + ? prettifyTitle(cameraConfig.frigate['camera_name']) + : '') || + (typeof cameraConfig?.id === 'string' && cameraConfig.id) || localize('editor.camera') + ' #' + cameraIndex ); } diff --git a/src/utils/basic.ts b/src/utils/basic.ts index 53823d0c..e7cabecc 100644 --- a/src/utils/basic.ts +++ b/src/utils/basic.ts @@ -33,6 +33,7 @@ export function dispatchFrigateCardEvent( * @returns A prettified name. */ export function prettifyTitle(input: string): string; +export function prettifyTitle(input?: string): string | undefined; export function prettifyTitle(input?: string): string | undefined { if (!input) { return undefined; diff --git a/src/utils/camera.ts b/src/utils/camera.ts index 4f718d5f..bcc19710 100644 --- a/src/utils/camera.ts +++ b/src/utils/camera.ts @@ -1,7 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; import { CameraConfig, RawFrigateCardConfig } from '../types.js'; -import { prettifyTitle } from './basic.js'; -import { getEntityIcon, getEntityTitle } from './ha'; /** * Get a camera id. @@ -26,53 +23,6 @@ export function getCameraID( ); } -/** - * Get a camera text title. - * @param hass The Home Assistant object. - * @param config The camera config (either parsed or raw). - * @returns A title string. - */ -export function getCameraTitle( - hass?: HomeAssistant, - config?: CameraConfig | RawFrigateCardConfig | null, -): string { - // Attempt to render a recognizable name for the camera, - // starting with the most likely to be useful and working our - // ways towards the least useful. Extra type checking here since this is also - // used on raw configuration in the editor. - return ( - (typeof config?.title === 'string' && config.title) || - (typeof config?.camera_entity === 'string' - ? getEntityTitle(hass, config.camera_entity) - : '') || - (typeof config?.webrtc_card === 'object' && - config.webrtc_card && - typeof config.webrtc_card['entity'] === 'string' && - config.webrtc_card['entity']) || - (typeof config?.frigate === 'object' && - config.frigate && - typeof config?.frigate['camera_name'] === 'string' && - config.frigate['camera_name'] - ? prettifyTitle(config.frigate['camera_name']) - : '') || - (typeof config?.id === 'string' && config.id) || - '' - ); -} - -/** - * Get a camera icon. - * @param hass The Home Assistant object. - * @param config The camera config. - * @returns An icon string. - */ -export function getCameraIcon( - hass?: HomeAssistant, - config?: CameraConfig | null, -): string { - return config?.icon || getEntityIcon(hass, config?.camera_entity) || 'mdi:video'; -} - /** * Get all cameras that depend on a given camera. * @param cameras Cameras map. diff --git a/src/utils/ha/browse-media.ts b/src/utils/ha/browse-media.ts index f744c870..1128c19c 100644 --- a/src/utils/ha/browse-media.ts +++ b/src/utils/ha/browse-media.ts @@ -5,7 +5,6 @@ import { dispatchErrorMessageEvent } from '../../components/message.js'; import { localize } from '../../localize/localize.js'; import { BrowseMediaQueryParameters, - BrowseRecordingQueryParameters, CameraConfig, ClipsOrSnapshots, FrigateBrowseMediaSource, @@ -15,7 +14,7 @@ import { MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_VIDEO, } from '../../types.js'; -import { getAllDependentCameras, getCameraTitle } from '../camera.js'; +import { getAllDependentCameras } from '../camera.js'; /** * Return the Frigate event_id given a FrigateBrowseMediaSource object. @@ -189,7 +188,7 @@ export const mergeFrigateBrowseMediaSources = async ( * @returns A BrowseMediaQueryParameters object. */ export const getBrowseMediaQueryParameters = ( - hass: HomeAssistant, + _hass: HomeAssistant, cameraID: string, cameraConfig?: CameraConfig, overrides?: Partial, @@ -202,7 +201,7 @@ export const getBrowseMediaQueryParameters = ( cameraName: cameraConfig.frigate.camera_name, label: cameraConfig.frigate.label, zone: cameraConfig.frigate.zone, - title: getCameraTitle(hass, cameraConfig), + title: '', // TODO: Replace: getCameraTitle(hass, cameraConfig), cameraID: cameraID, ...overrides, };