From 4901905ca12f285ecfd02d33750840cdaf8090c2 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Mon, 20 Jun 2022 03:03:27 +0000 Subject: [PATCH] Make error handling and console logging consistent --- src/card.ts | 20 ++++++++----- src/components/live.ts | 40 ++++++++++++++----------- src/components/timeline.ts | 35 ++++++++++++---------- src/localize/languages/en.json | 1 - src/localize/languages/pt-BR.json | 1 - src/types.ts | 40 +++++++++++++------------ src/utils/basic.ts | 17 +++++++++++ src/utils/frigate.ts | 4 +-- src/utils/ha/entity-registry.ts | 6 ++-- src/utils/ha/index.ts | 50 +++++++++++++++---------------- src/utils/ha/resolved-media.ts | 17 +++++++---- 11 files changed, 132 insertions(+), 99 deletions(-) diff --git a/src/card.ts b/src/card.ts index 7984ebe0..c7daac4f 100644 --- a/src/card.ts +++ b/src/card.ts @@ -75,7 +75,7 @@ import { frigateCardHasAction, getActionConfigGivenAction, } from './utils/action.js'; -import { contentsChanged } from './utils/basic.js'; +import { contentsChanged, errorToConsole } from './utils/basic.js'; import { getCameraIcon, getCameraID, getCameraTitle } from './utils/camera.js'; import { getEntityIcon, @@ -673,7 +673,7 @@ export class FrigateCard extends LitElement { try { entityList = await getAllEntities(this._hass); } catch (e) { - console.error(e); + errorToConsole(e as Error); } const cameras: Map = new Map(); @@ -688,7 +688,11 @@ export class FrigateCard extends LitElement { if (config.camera_entity) { try { entity = await getExtendedEntity(this._hass, config.camera_entity, cache); - } catch (e) {} + } catch (e) { + // Silently ignore errors here, as non-Frigate camera entities may not + // necessarily have a registry entry and otherwise this would cause + // log spam for those cases. + } } if (!config.frigate.camera_name && entity) { @@ -717,7 +721,7 @@ export class FrigateCard extends LitElement { cache, ); } catch (e) { - console.error(e); + errorToConsole(e as Error); } if (config.triggers.motion) { @@ -1180,7 +1184,7 @@ export class FrigateCard extends LitElement { try { response = await homeAssistantSignPath(this._hass, path); } catch (e) { - console.error(e); + errorToConsole(e as Error); } if (!response) { @@ -1860,7 +1864,7 @@ export class FrigateCard extends LitElement { } declare global { - interface HTMLElementTagNameMap { - "frigate-card": FrigateCard - } + interface HTMLElementTagNameMap { + 'frigate-card': FrigateCard; + } } diff --git a/src/components/live.ts b/src/components/live.ts index 9dc91315..5eea9682 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -9,7 +9,7 @@ import { LitElement, PropertyValues, TemplateResult, - unsafeCSS + unsafeCSS, } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { createRef, Ref, ref } from 'lit/directives/ref.js'; @@ -33,16 +33,16 @@ import { LiveProvider, MediaShowInfo, TransitionEffect, - WebRTCCardConfig + WebRTCCardConfig, } from '../types.js'; import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; -import { contentsChanged } from '../utils/basic.js'; +import { contentsChanged, errorToConsole } from '../utils/basic.js'; import { getCameraIcon, getCameraTitle } from '../utils/camera.js'; import { homeAssistantSignPath } from '../utils/ha'; import { getFullDependentBrowseMediaQueryParameters } from '../utils/ha/browse-media.js'; import { dispatchExistingMediaShowInfoAsEvent, - dispatchMediaShowEvent + dispatchMediaShowEvent, } from '../utils/media-info.js'; import { View } from '../view.js'; import { AutoMediaPlugin, AutoMediaPluginType } from './embla-plugins/automedia.js'; @@ -52,8 +52,8 @@ import { dispatchErrorMessageEvent } from './message.js'; import './next-prev-control.js'; import { FrigateCardNextPreviousControl } from './next-prev-control.js'; import './title-control.js'; -import "./surround-thumbnails"; -import "../patches/ha-camera-stream"; +import './surround-thumbnails'; +import '../patches/ha-camera-stream'; // Number of seconds a signed URL is valid for. const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60; @@ -913,7 +913,7 @@ export class FrigateCardLiveWebRTCCard extends LitElement { // Set the id to ensure that the relevant CSS styles will have // sufficient specifity to overcome some styles that are otherwise // applied to in Safari. - webrtcElement.id = "webrtc"; + webrtcElement.id = 'webrtc'; } return html`${webrtcElement}`; }; @@ -1021,7 +1021,11 @@ export class FrigateCardLiveJSMPEG extends LitElement { * @returns A URL or null. */ protected async _getURL(): Promise { - if (!this.hass || !this.cameraConfig?.frigate.client_id || !this.cameraConfig?.frigate.camera_name) { + if ( + !this.hass || + !this.cameraConfig?.frigate.client_id || + !this.cameraConfig?.frigate.camera_name + ) { return null; } @@ -1033,8 +1037,8 @@ export class FrigateCardLiveJSMPEG extends LitElement { `/jsmpeg/${this.cameraConfig.frigate.camera_name}`, URL_SIGN_EXPIRY_SECONDS, ); - } catch (err) { - console.warn(err); + } catch (e) { + errorToConsole(e as Error); return null; } if (!response) { @@ -1182,12 +1186,12 @@ export class FrigateCardLiveJSMPEG extends LitElement { } declare global { - interface HTMLElementTagNameMap { - "frigate-card-live-jsmpeg": FrigateCardLiveJSMPEG - "frigate-card-live-webrtc-card": FrigateCardLiveWebRTCCard - "frigate-card-live-ha": FrigateCardLiveFrigate - "frigate-card-live-provider": FrigateCardLiveProvider - "frigate-card-live-carousel": FrigateCardLiveCarousel - "frigate-card-live": FrigateCardLive - } + interface HTMLElementTagNameMap { + 'frigate-card-live-jsmpeg': FrigateCardLiveJSMPEG; + 'frigate-card-live-webrtc-card': FrigateCardLiveWebRTCCard; + 'frigate-card-live-ha': FrigateCardLiveFrigate; + 'frigate-card-live-provider': FrigateCardLiveProvider; + 'frigate-card-live-carousel': FrigateCardLiveCarousel; + 'frigate-card-live': FrigateCardLive; + } } diff --git a/src/components/timeline.ts b/src/components/timeline.ts index d4a9ead4..7396bfd6 100644 --- a/src/components/timeline.ts +++ b/src/components/timeline.ts @@ -6,7 +6,7 @@ import { fromUnixTime, getUnixTime, startOfHour, - sub + sub, } from 'date-fns'; import { CSSResultGroup, @@ -14,7 +14,7 @@ import { LitElement, PropertyValues, TemplateResult, - unsafeCSS + unsafeCSS, } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; @@ -28,7 +28,7 @@ import { TimelineItem, TimelineOptions, TimelineOptionsCluster, - TimelineWindow + TimelineWindow, } from 'vis-timeline/esnext'; import { CAMERA_BIRDSEYE } from '../const'; import { localize } from '../localize/localize'; @@ -42,10 +42,10 @@ import { frigateCardConfigDefaults, FrigateCardError, FrigateEvent, - TimelineConfig + TimelineConfig, } from '../types'; import { stopEventFromActivatingCardWideActions } from '../utils/action'; -import { dispatchFrigateCardEvent, prettifyTitle } from '../utils/basic'; +import { dispatchFrigateCardEvent, errorToConsole, prettifyTitle } from '../utils/basic'; import { getCameraTitle } from '../utils/camera.js'; import { getRecordingSegments, @@ -53,7 +53,7 @@ import { getUniqueFrigateCameraEventsID, getUniqueFrigateCameraID, RecordingSegments, - RecordingSummary + RecordingSummary, } from '../utils/frigate'; import { createEventParentForChildren, @@ -61,7 +61,7 @@ import { generateRecordingIdentifier, getBrowseMediaQueryParameters, isTrueMedia, - multipleBrowseMediaQuery + multipleBrowseMediaQuery, } from '../utils/ha/browse-media'; import { View, ViewContext } from '../view'; import { dispatchFrigateCardErrorEvent, dispatchMessageEvent } from './message.js'; @@ -300,7 +300,7 @@ class TimelineDataManager { this._dateStart, this._dateEnd, ), - ...(recordings ? [this._fetchRecordings(element, hass, cameras)] : []), + ...(recordings ? [this._fetchRecordings(hass, cameras)] : []), ]); return true; @@ -315,7 +315,6 @@ class TimelineDataManager { * @param end Fetch events that start earlier than this date. */ protected async _fetchRecordings( - element: HTMLElement, hass: ExtendedHomeAssistant, cameras: Map, ): Promise { @@ -329,7 +328,7 @@ class TimelineDataManager { if (!config.frigate.camera_name) { return; } - let summary: RecordingSummary; + let summary: RecordingSummary = []; try { summary = await getRecordingsSummary( hass, @@ -337,7 +336,9 @@ class TimelineDataManager { config.frigate.camera_name, ); } catch (e) { - return dispatchFrigateCardErrorEvent(element, e as FrigateCardError); + // Recording failure should not disrupt the rest of the timeline + // experience. + errorToConsole(e as Error); } for (const dayData of summary) { @@ -691,7 +692,9 @@ export class FrigateCardTimelineCore extends LitElement { ), ]); results.set(camera, { segments: cameraResults[0], summary: cameraResults[1] }); - } catch (e) {} + } catch (e) { + errorToConsole(e as Error); + } }; const cameras = camera ? [camera] : [...(this.cameras?.keys() ?? [])]; await Promise.all(cameras.map((camera) => fetch(camera, this.cameras?.get(camera)))); @@ -1239,8 +1242,8 @@ export class FrigateCardTimelineCore extends LitElement { } declare global { - interface HTMLElementTagNameMap { - "frigate-card-timeline-core": FrigateCardTimelineCore - "frigate-card-timeline": FrigateCardTimeline - } + interface HTMLElementTagNameMap { + 'frigate-card-timeline-core': FrigateCardTimelineCore; + 'frigate-card-timeline': FrigateCardTimeline; + } } diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 46dd9314..3630cf51 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -320,7 +320,6 @@ "invalid_configuration": "Invalid configuration", "invalid_configuration_no_hint": "No location hint available (bad or missing type?)", "invalid_elements_config": "Invalid picture elements configuration", - "invalid_keys": "Invalid keys", "invalid_response": "Received invalid response from Home Assistant for request", "jsmpeg_no_player": "Could not start JSMPEG player", "jsmpeg_no_sign": "Could not retrieve or sign JSMPEG websocket path", diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json index 1afc8cbe..5ecb1694 100644 --- a/src/localize/languages/pt-BR.json +++ b/src/localize/languages/pt-BR.json @@ -322,7 +322,6 @@ "invalid_configuration": "Configuração inválida", "invalid_configuration_no_hint": "Nenhuma dica de local disponível (tipo incorreto ou ausente?)", "invalid_elements_config": "Configuração de elementos de imagem inválida", - "invalid_keys": "Chaves inválidas", "invalid_response": "Resposta inválida recebida do Home Assistant para a solicitação", "jsmpeg_no_player": "Não foi possível iniciar o player JSMPEG", "jsmpeg_no_sign": "Não foi possível recuperar ou assinar o caminho do websocket JSMPEG", diff --git a/src/types.ts b/src/types.ts index 5913baee..449e47de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,7 +9,7 @@ import { NoActionConfig, Themes, ToggleActionConfig, - UrlActionConfig + UrlActionConfig, } from 'custom-card-helpers'; import { StyleInfo } from 'lit/directives/style-map.js'; import { z } from 'zod'; @@ -44,7 +44,8 @@ const FRIGATE_CARD_VIEWS = [ ] as const; export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number]; -export type FrigateCardUserSpecifiedView = typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED[number]; +export type FrigateCardUserSpecifiedView = + typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED[number]; export const FRIGATE_CARD_VIEW_DEFAULT = 'live' as const; const FRIGATE_MENU_STYLES = ['none', 'hidden', 'overlay', 'hover', 'outside'] as const; @@ -394,15 +395,17 @@ const cameraConfigSchema = z // Optional identifier to separate different camera configurations used in // this card. id: z.string().optional(), - - frigate: z.object({ - // No URL validation to allow relative URLs within HA (e.g. Frigate addon). - url: z.string().optional(), - client_id: z.string().default(cameraConfigDefault.frigate.client_id), - camera_name: z.string().optional(), - label: z.string().optional(), - zone: z.string().optional(), - }).default(cameraConfigDefault.frigate), + + frigate: z + .object({ + // No URL validation to allow relative URLs within HA (e.g. Frigate addon). + url: z.string().optional(), + client_id: z.string().default(cameraConfigDefault.frigate.client_id), + camera_name: z.string().optional(), + label: z.string().optional(), + zone: z.string().optional(), + }) + .default(cameraConfigDefault.frigate), // Camera identifiers for WebRTC. webrtc_card: webrtcCardCameraConfigSchema.optional(), @@ -414,14 +417,13 @@ const cameraConfigSchema = z }) .default(cameraConfigDefault.dependencies), - triggers: z.object({ - motion: z.boolean().default(cameraConfigDefault.triggers.motion), - occupancy: z.boolean().default(cameraConfigDefault.triggers.occupancy), - entities: z - .string() - .array() - .default(cameraConfigDefault.triggers.entities), - }).default(cameraConfigDefault.triggers), + triggers: z + .object({ + motion: z.boolean().default(cameraConfigDefault.triggers.motion), + occupancy: z.boolean().default(cameraConfigDefault.triggers.occupancy), + entities: z.string().array().default(cameraConfigDefault.triggers.entities), + }) + .default(cameraConfigDefault.triggers), }) .default(cameraConfigDefault); export type CameraConfig = z.infer; diff --git a/src/utils/basic.ts b/src/utils/basic.ts index 6f1fa47c..a4d1c483 100644 --- a/src/utils/basic.ts +++ b/src/utils/basic.ts @@ -1,4 +1,5 @@ import { isEqual } from 'lodash-es'; +import { FrigateCardError } from '../types'; /** * Dispatch a Frigate Card event. @@ -60,3 +61,19 @@ export function arrayMove(target: unknown[], from: number, to: number): void { export function contentsChanged(n: unknown, o: unknown): boolean { return !isEqual(n, o); } + +/** + * Log an error as a warning to the console. + * @param e The Error object. + * @param func The Console func to call. + */ +export function errorToConsole(e: Error, func?: CallableFunction): void { + if (!func) { + func = console.warn; + } + if (e instanceof FrigateCardError && e.context) { + func(e, e.context); + } else { + func(e); + } +} \ No newline at end of file diff --git a/src/utils/frigate.ts b/src/utils/frigate.ts index 6169c0bf..47e9e368 100644 --- a/src/utils/frigate.ts +++ b/src/utils/frigate.ts @@ -51,7 +51,7 @@ const recordingSegmentsSchema = recordingSegmentSchema.array(); export type RecordingSegments = z.infer; /** - * Get the recordings summary. + * Get the recordings summary. May throw. * @param hass The Home Assistant object. * @param client_id The Frigate client_id. * @param camera_name The Frigate camera name. @@ -70,7 +70,7 @@ export const getRecordingsSummary = async ( }; /** - * Get the recording segments.. + * Get the recording segments. May throw. * @param hass The Home Assistant object. * @param client_id The Frigate client_id. * @param camera_name The Frigate camera name. diff --git a/src/utils/ha/entity-registry.ts b/src/utils/ha/entity-registry.ts index 6567e66f..e77f5edf 100644 --- a/src/utils/ha/entity-registry.ts +++ b/src/utils/ha/entity-registry.ts @@ -5,7 +5,7 @@ import { EntityList, entityListSchema, ExtendedEntity, - extendedEntitySchema + extendedEntitySchema, } from '../../types.js'; export class ExtendedEntityCache { @@ -78,7 +78,7 @@ export const getExtendedEntity = async ( }; /** - * Get the extended entity information for an array of entities. + * Get the extended entity information for an array of entities. May throw. * @param hass The Home Assistant object. * @param entities An array of entity ids. * @param cache An optional ExtendedEntityCache. @@ -98,7 +98,7 @@ export const getExtendedEntities = async ( }; /** - * Get a list of all entities from the entity registry. + * Get a list of all entities from the entity registry. May throw. * @param hass The Home Assistant object. * @returns An entity list object. */ diff --git a/src/utils/ha/index.ts b/src/utils/ha/index.ts index 187aed60..8f07606c 100644 --- a/src/utils/ha/index.ts +++ b/src/utils/ha/index.ts @@ -9,7 +9,7 @@ import { FrigateCardError, SignedPath, signedPathSchema, - StateParameters + StateParameters, } from '../../types.js'; import { stateIcon } from '../icons/state-icon.js'; import { getParseErrorKeys } from '../zod.js'; @@ -26,27 +26,30 @@ export async function homeAssistantWSRequest( schema: ZodSchema, request: MessageBase, ): Promise { - const response = await hass.callWS(request); + let response; + try { + response = await hass.callWS(request); + } catch (e) { + if (!(e instanceof Error)) { + throw new FrigateCardError(localize('error.failed_response'), { + request: request, + response: e, + }); + } + throw e; + } if (!response) { - const error_message = `${localize('error.empty_response')}: ${JSON.stringify( - request, - )}`; - console.warn(error_message); - throw new Error(error_message); + throw new FrigateCardError(localize('error.empty_response'), { + request: request, + }); } const parseResult = schema.safeParse(response); if (!parseResult.success) { - const keys = getParseErrorKeys(parseResult.error); - const error_message = localize('error.invalid_response'); - console.warn( - `${error_message}: ${JSON.stringify(request)}. ${localize( - 'error.invalid_keys', - )}: ${keys}`, - ); - throw new FrigateCardError(error_message, { + throw new FrigateCardError(localize('error.invalid_response'), { request: request, - invalid_keys: keys, + response: response, + invalid_keys: getParseErrorKeys(parseResult.error), }); } return parseResult.data; @@ -96,9 +99,7 @@ export async function homeAssistantHTTPRequest( let signResponse: string | null | undefined; try { signResponse = await homeAssistantSignPath(hass, url); - } catch (e) { - console.warn(e); - } + } catch (e) {} if (!signResponse) { throw new FrigateCardError(localize('error.failed_sign'), { @@ -127,21 +128,20 @@ export async function homeAssistantHTTPRequest( try { raw_json = await response.json(); } catch (e) { - console.warn(e); throw new FrigateCardError(localize('error.undecodable_response'), { url: signedURL.toString(), }); } - try { - return schema.parse(raw_json); - } catch (e) { - console.warn(e); + const parseResult = schema.safeParse(raw_json); + if (!parseResult.success) { throw new FrigateCardError(localize('error.invalid_response'), { url: signedURL.toString(), - response: raw_json, + raw_json: raw_json, + invalid_keys: getParseErrorKeys(parseResult.error), }); } + return parseResult.data; } interface HassStateDifference { diff --git a/src/utils/ha/resolved-media.ts b/src/utils/ha/resolved-media.ts index e816df52..b9e6abfd 100644 --- a/src/utils/ha/resolved-media.ts +++ b/src/utils/ha/resolved-media.ts @@ -2,10 +2,11 @@ import { HomeAssistant } from 'custom-card-helpers'; import QuickLRU from 'quick-lru'; import { homeAssistantWSRequest } from '.'; import { - FrigateBrowseMediaSource, - ResolvedMedia, - resolvedMediaSchema + FrigateBrowseMediaSource, + ResolvedMedia, + resolvedMediaSchema, } from '../../types.js'; +import { errorToConsole } from '../basic'; // It's important the cache size be at least as large as the largest likely // media query or media items will from a given query will be evicted for other @@ -30,7 +31,6 @@ export class ResolvedMediaCache { return this._cache.has(id); } - /** * Get resolved media information given an id. * @param id The id. @@ -55,7 +55,7 @@ export class ResolvedMediaCache { * @param hass The Home Assistant object. * @param mediaSource The media source object. * @param cache An optional ResolvedMediaCache object. - * @returns + * @returns The resolved media or `null`. */ export const resolveMedia = async ( hass: HomeAssistant, @@ -73,7 +73,12 @@ export const resolveMedia = async ( type: 'media_source/resolve_media', media_content_id: mediaSource.media_content_id, }; - const resolvedMedia = await homeAssistantWSRequest(hass, resolvedMediaSchema, request); + let resolvedMedia: ResolvedMedia | null = null; + try { + resolvedMedia = await homeAssistantWSRequest(hass, resolvedMediaSchema, request); + } catch (e) { + errorToConsole(e as Error); + } if (cache && resolvedMedia) { cache.set(mediaSource.media_content_id, resolvedMedia); }