Add error handling for camera manager calls.
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
import { FrigateCardError } from '../types';
|
||||
|
||||
export class CameraManagerError extends FrigateCardError {}
|
||||
@@ -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
@@ -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,
|
||||
')',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
Reference in New Issue
Block a user