Add error handling for camera manager calls.

This commit is contained in:
Dermot Duffy
2023-01-24 19:36:54 -08:00
parent e85d46d84a
commit 54f63d0cf3
9 changed files with 118 additions and 74 deletions
-3
View File
@@ -1,3 +0,0 @@
import { FrigateCardError } from '../types';
export class CameraManagerError extends FrigateCardError {}
+15 -41
View File
@@ -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 <FrigateEventQueryResults>{
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 <FrigateEventQueryResults>{
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 {
+12 -1
View File
@@ -229,7 +229,18 @@ export class CameraManager {
): Promise<void> {
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,
')',
);
}
}
+15 -9
View File
@@ -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}
></frigate-card-thumbnail-feature-recording>`
: html``}
${this.show_favorite_control && event && this.hass && clientID
${this.show_favorite_control && this.media && this.hass && clientID
? html` <ha-icon
class="${classMap(starClasses)}"
icon=${this.media.isFavorite() ? 'mdi:star' : 'mdi:star-outline'}
title=${localize('thumbnail.retain_indefinitely')}
@click=${(ev: Event) => {
@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();
}
}}
/></ha-icon>`
+8 -1
View File
@@ -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,
+10 -2
View File
@@ -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;
}
+44 -11
View File
@@ -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<void> => {
(
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<string, CameraConfig>,
@@ -48,7 +53,7 @@ export const createViewForEvents = async (
targetView?: FrigateCardView;
limit?: number;
},
): Promise<View> => {
): Promise<View | null> => {
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<void> => {
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<string, CameraConfig>,
@@ -126,7 +141,7 @@ export const createViewForRecordings = async (
start?: Date;
end?: Date;
},
): Promise<View> => {
): Promise<View | null> => {
const cameraIDs: Set<string> = 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(
+14 -5
View File
@@ -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<string, CameraConfig>,
window: TimelineWindow,
): Promise<void> {
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 {
-1
View File
@@ -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.