diff --git a/src/camera/error.ts b/src/camera/error.ts deleted file mode 100644 index b131e5c3..00000000 --- a/src/camera/error.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { FrigateCardError } from '../types'; - -export class CameraManagerError extends FrigateCardError {} diff --git a/src/camera/frigate/engine-frigate.ts b/src/camera/frigate/engine-frigate.ts index 6f48409c..208958ea 100644 --- a/src/camera/frigate/engine-frigate.ts +++ b/src/camera/frigate/engine-frigate.ts @@ -4,18 +4,16 @@ import endOfHour from 'date-fns/endOfHour'; import getUnixTime from 'date-fns/getUnixTime'; import startOfHour from 'date-fns/startOfHour'; import { CAMERA_BIRDSEYE } from '../../const'; -import { CameraConfig, RecordingSegment } from '../../types'; +import { CameraConfig } from '../../types'; import { MediaQueriesResults } from '../../view/media-queries-results'; import { MediaQueriesClassifier } from '../../view/media-queries-classifier'; import { ViewMedia, ViewMediaFactory } from '../../view/media'; import { ViewMediaClassifier } from '../../view/media-classifier'; -import { errorToConsole } from '../../utils/basic'; import { RecordingSegmentsCache } from '../cache'; import { CameraManagerEngine, CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT, } from '../engine'; -import { CameraManagerError } from '../error'; import { DateRange } from '../range'; import { Engine, @@ -33,7 +31,7 @@ import { RecordingQuery, RecordingSegmentsQuery, } from '../types'; -import { FrigateRecording, RecordingSummary } from './types'; +import { FrigateRecording } from './types'; import { getEvents, getRecordingSegments, @@ -156,13 +154,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine { return; } - try { - await retainEvent(hass, clientID, media.getID(cameraConfig), favorite); - } catch (e) { - errorToConsole(e as Error); - throw new CameraManagerError((e as Error).message); - } - + await retainEvent(hass, clientID, media.getID(cameraConfig), favorite); media.setFavorite(favorite); } @@ -189,17 +181,12 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine { limit: query?.limit ?? CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT, }; - try { - return { - type: QueryResultsType.Event, - engine: Engine.Frigate, - events: await getEvents(hass, nativeQuery), - expiry: add(new Date(), { seconds: EVENT_REQUEST_CACHE_MAX_AGE_SECONDS }), - }; - } catch (e) { - errorToConsole(e as Error); - throw new CameraManagerError((e as Error).message, query); - } + return { + type: QueryResultsType.Event, + engine: Engine.Frigate, + events: await getEvents(hass, nativeQuery), + expiry: add(new Date(), { seconds: EVENT_REQUEST_CACHE_MAX_AGE_SECONDS }), + }; } public async getRecordings( @@ -215,17 +202,11 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine { return null; } - let recordingSummary: RecordingSummary; - try { - recordingSummary = await getRecordingsSummary( - hass, - cameraConfig.frigate.client_id, - cameraConfig.frigate.camera_name, - ); - } catch (e) { - errorToConsole(e as Error); - throw new CameraManagerError((e as Error).message, query); - } + const recordingSummary = await getRecordingsSummary( + hass, + cameraConfig.frigate.client_id, + cameraConfig.frigate.camera_name, + ); let recordings: FrigateRecording[] = []; @@ -303,14 +284,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine { before: Math.floor(query.end.getTime() / 1000), }; - let segments: RecordingSegment[]; - try { - segments = await getRecordingSegments(hass, request); - } catch (e) { - errorToConsole(e as Error); - throw new CameraManagerError((e as Error).message, query); - } - + const segments = await getRecordingSegments(hass, request); this._recordingSegmentsCache.add(query.cameraID, range, segments); return { diff --git a/src/camera/manager.ts b/src/camera/manager.ts index c433dba0..2d3afda2 100644 --- a/src/camera/manager.ts +++ b/src/camera/manager.ts @@ -229,7 +229,18 @@ export class CameraManager { ): Promise { const engine = this._engineFactory.getEngineForCamera(cameraConfig); if (engine) { - engine.favoriteMedia(hass, cameraConfig, media, favorite); + const queryStartTime = new Date(); + await engine.favoriteMedia(hass, cameraConfig, media, favorite); + + console.debug( + 'Frigate Card CameraManager favorite request (', + `Duration: ${(new Date().getTime() - queryStartTime.getTime()) / 1000}s,`, + 'Media:', + media.getID(), + ', Favorite:', + favorite, + ')', + ); } } diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts index 4bb51cfe..a116a90e 100644 --- a/src/components/thumbnail.ts +++ b/src/components/thumbnail.ts @@ -9,7 +9,7 @@ import thumbnailFeatureEventStyle from '../scss/thumbnail-feature-event.scss'; import thumbnailFeatureRecordingStyle from '../scss/thumbnail-feature-recording.scss'; import thumbnailStyle from '../scss/thumbnail.scss'; import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; -import { getDurationString, prettifyTitle } from '../utils/basic.js'; +import { errorToConsole, getDurationString, prettifyTitle } from '../utils/basic.js'; import { getCameraTitle } from '../utils/camera.js'; import { renderTask } from '../utils/task.js'; import { createFetchThumbnailTask } from '../utils/thumbnail.js'; @@ -289,20 +289,26 @@ export class FrigateCardThumbnail extends LitElement { .date=${this.media.getStartTime() ?? undefined} >` : html``} - ${this.show_favorite_control && event && this.hass && clientID + ${this.show_favorite_control && this.media && this.hass && clientID ? html` { + @click=${async (ev: Event) => { stopEventFromActivatingCardWideActions(ev); if (this.hass && this.cameraConfig && this.media) { - this.cameraManager?.favoriteMedia( - this.hass, - this.cameraConfig, - this.media, - !this.media?.isFavorite(), - ); + try { + await this.cameraManager?.favoriteMedia( + this.hass, + this.cameraConfig, + this.media, + !this.media?.isFavorite(), + ); + } catch (e) { + errorToConsole(e as Error); + return; + } + this.requestUpdate(); } }} />` diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index 7f290255..f33d2149 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -471,6 +471,7 @@ export class FrigateCardTimelineCore extends LitElement { ['background', 'group-label'].includes(properties.what) ) { view = await createViewForRecordings( + this, this.hass, this.cameraManager, this.cameras, @@ -487,6 +488,7 @@ export class FrigateCardTimelineCore extends LitElement { ); } else if (this.timelineConfig?.show_recordings && properties.what === 'axis') { view = await createViewForRecordings( + this, this.hass, this.cameraManager, this.cameras, @@ -641,6 +643,7 @@ export class FrigateCardTimelineCore extends LitElement { return null; } const view = await createViewForEvents( + this, this.hass, this.cameraManager, this.cameras, @@ -651,6 +654,9 @@ export class FrigateCardTimelineCore extends LitElement { mediaType: this.timelineConfig?.media, }, ); + if (!view) { + return null; + } view.mergeInContext( this._generateTimelineContext({ noSetWindow: options?.noSetWindow }), ); @@ -813,7 +819,8 @@ export class FrigateCardTimelineCore extends LitElement { ); }, } - : (false as TimelineOptionsCluster), + : // Timeline type information is incorrect requiring this 'as'. + (false as unknown as TimelineOptionsCluster), minHeight: '100%', maxHeight: '100%', zoomMax: 1 * 24 * 60 * 60 * 1000, diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 82e16687..e8fc69f6 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -27,7 +27,7 @@ import { ViewerConfig, } from '../types.js'; import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; -import { contentsChanged } from '../utils/basic.js'; +import { contentsChanged, errorToConsole } from '../utils/basic.js'; import { getFullDependentBrowseMediaQueryParametersOrDispatchError } from '../utils/ha/browse-media.js'; import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js'; import { View } from '../view/view.js'; @@ -56,6 +56,7 @@ import { ViewMedia } from '../view/media.js'; import { ViewMediaClassifier } from '../view/media-classifier'; import { guard } from 'lit/directives/guard.js'; import { localize } from '../localize/localize.js'; +import { MediaQueriesResults } from '../view/media-queries-results.js'; export interface MediaSeek { // Specifies the point at which this recording should be played, the @@ -384,7 +385,14 @@ export class FrigateCardViewerCarousel extends LitElement { const clipQuery = this.view.query.clone(); clipQuery.convertToClipsQueries(); - const results = await this.cameraManager.executeMediaQuery(this.hass, clipQuery); + let results: MediaQueriesResults | null; + try { + results = await this.cameraManager.executeMediaQuery(this.hass, clipQuery); + } catch (e) { + errorToConsole(e as Error); + return; + } + if (!results) { return; } diff --git a/src/utils/media-to-view.ts b/src/utils/media-to-view.ts index 1aac8a47..fcba2c4b 100644 --- a/src/utils/media-to-view.ts +++ b/src/utils/media-to-view.ts @@ -16,6 +16,10 @@ import { getAllDependentCameras } from './camera.js'; import { ViewMedia } from '../view/media'; import { ViewMediaClassifier } from '../view/media-classifier'; import { HomeAssistant } from 'custom-card-helpers'; +import { dispatchFrigateCardErrorEvent } from '../components/message'; +import { MediaQueriesResults } from '../view/media-queries-results'; +import { errorToConsole } from './basic'; +import { RecordingSegmentsQueryResults } from '../camera/types'; export const changeViewToRecentEventsForCameraAndDependents = async ( element: HTMLElement, @@ -29,14 +33,15 @@ export const changeViewToRecentEventsForCameraAndDependents = async ( }, ): Promise => { ( - await createViewForEvents(hass, cameraManager, cameras, view, { + await createViewForEvents(element, hass, cameraManager, cameras, view, { ...options, limit: 50, // Capture the 50 most recent events. }) - ).dispatchChangeEvent(element); + )?.dispatchChangeEvent(element); }; export const createViewForEvents = async ( + element: HTMLElement, hass: HomeAssistant, cameraManager: CameraManager, cameras: Map, @@ -48,7 +53,7 @@ export const createViewForEvents = async ( targetView?: FrigateCardView; limit?: number; }, -): Promise => { +): Promise => { let query: EventMediaQueries; if (options?.query) { query = options.query; @@ -64,7 +69,15 @@ export const createViewForEvents = async ( }); query = new EventMediaQueries(queries); } - const queryResults = await cameraManager.executeMediaQuery(hass, query); + + let queryResults: MediaQueriesResults | null; + try { + queryResults = await cameraManager.executeMediaQuery(hass, query); + } catch (e) { + errorToConsole(e as Error); + dispatchFrigateCardErrorEvent(element, e as Error); + return null; + } return view?.evolve({ view: options?.targetView, @@ -94,18 +107,19 @@ export const changeViewToRecentRecordingForCameraAndDependents = async ( ): Promise => { const now = new Date(); ( - await createViewForRecordings(hass, cameraManager, cameras, view, { + await createViewForRecordings(element, hass, cameraManager, cameras, 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 }), }) - ).dispatchChangeEvent(element); + )?.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. @@ -115,6 +129,7 @@ export const changeViewToRecentRecordingForCameraAndDependents = async ( * restrict to. */ export const createViewForRecordings = async ( + element: HTMLElement, hass: HomeAssistant, cameraManager: CameraManager, cameras: Map, @@ -126,7 +141,7 @@ export const createViewForRecordings = async ( start?: Date; end?: Date; }, -): Promise => { +): Promise => { const cameraIDs: Set = options?.cameraIDs ? options.cameraIDs : new Set(getAllDependentCameras(cameras, view.camera)); @@ -137,7 +152,15 @@ export const createViewForRecordings = async ( }); const query = new RecordingMediaQueries(queries); - const queryResults = await cameraManager.executeMediaQuery(hass, query); + let queryResults: MediaQueriesResults | null; + + try { + queryResults = await cameraManager.executeMediaQuery(hass, query); + } catch (e) { + errorToConsole(e as Error); + dispatchFrigateCardErrorEvent(element, e as Error); + return null; + } let viewerContext: ViewContext | undefined = {}; const mediaArray = queryResults?.getResults(); @@ -199,9 +222,19 @@ export const generateMediaViewerContext = async ( end: end, }, )[0]; - const segments = (await cameraManager.getRecordingSegments(hass, query)).get( - query, - ); + let segments: RecordingSegmentsQueryResults | null; + + try { + segments = (await cameraManager.getRecordingSegments(hass, query)).get( + query, + ) ?? null; + } catch (e) { + errorToConsole(e as Error); + // View context is never critical. Ignore errors which will at least + // allow the video to load even if it doesn't seek to the correct + // location. + return {}; + } if (segments) { seekSeconds = getSeekTimeInSegments( diff --git a/src/utils/timeline-source.ts b/src/utils/timeline-source.ts index 36b52734..7cdfe4f7 100644 --- a/src/utils/timeline-source.ts +++ b/src/utils/timeline-source.ts @@ -10,7 +10,7 @@ import { capEndDate, convertRangeToCacheFriendlyTimes } from '../camera/util'; import { EventMediaQueries } from "../view/media-queries"; import { ViewMedia } from '../view/media'; import { compressRanges, ExpiringMemoryRangeSet, MemoryRangeSet } from '../camera/range'; -import { ModifyInterface } from './basic.js'; +import { errorToConsole, ModifyInterface } from './basic.js'; // Allow timeline freshness to be at least this number of seconds out of date // (caching times in the data-engine may increase the effective delay). @@ -90,10 +90,19 @@ export class TimelineDataSource { cameras: Map, window: TimelineWindow, ): Promise { - await Promise.all([ - this._refreshEvents(hass, cameras, window), - this._refreshRecordings(hass, window), - ]); + try { + await Promise.all([ + this._refreshEvents(hass, cameras, window), + this._refreshRecordings(hass, window), + ]); + } catch (e) { + errorToConsole(e as Error); + + // Intentionally ignore errors here, since it is likely the user will + // change the range again and a subsequent call may work. To do otherwise + // would be jarring to the timeline experience in the case of transient + // errors from the backend. + } } public getCacheFriendlyEventWindow(window: TimelineWindow): TimelineWindow { diff --git a/src/view/view.ts b/src/view/view.ts index 182ca7c9..a66056c7 100644 --- a/src/view/view.ts +++ b/src/view/view.ts @@ -1,5 +1,4 @@ // Medium: -// - TODO: Callers of all async methods of data-engine need to catch errors. // - TODO: Add garbage collecting of segments not present in the recording summaries anymore. // - TODO: Do I need to dedup recordings? (i.e. multiple zones on same camera may need to be dedup'd somewhere before returning the view). The media getID() call may be useful for this. // - TODO: Move frigate specific view-media under the camera manager.