feat: Add hardened error handling and retries (#2451)

- Closes #1830
 - Closes #2099
This commit is contained in:
Dermot Duffy
2026-06-30 17:45:12 -07:00
committed by dermotduffy
parent 4bc787e2b7
commit 47bcce93d3
182 changed files with 7877 additions and 4043 deletions
+2 -6
View File
@@ -5,10 +5,9 @@ import { DeviceRegistryManager } from '../ha/registry/device';
import { EntityRegistryManager } from '../ha/registry/entity/types';
import { ResolvedMediaCache } from '../ha/resolved-media';
import { HomeAssistant } from '../ha/types';
import { localize } from '../localize/localize';
import { RecordingSegmentsCache } from './cache';
import { CameraManagerEngine } from './engine';
import { CameraInitializationError } from './error';
import { CameraNoEntityError } from './error';
import { CameraEventCallback, CameraManagerRequestCache, Engine } from './types';
import { getCameraEntityFromConfig } from './utils/camera-entity-from-config';
@@ -117,10 +116,7 @@ export class CameraManagerEngineFactory {
return Engine.Generic;
}
// Otherwise, it's probably a typo so throw an exception.
throw new CameraInitializationError(
localize('error.no_camera_entity'),
cameraConfig,
);
throw new CameraNoEntityError(cameraConfig);
}
switch (entity?.platform) {
+2 -3
View File
@@ -1,7 +1,6 @@
import { Entity, EntityRegistryManager } from '../ha/registry/entity/types';
import { localize } from '../localize/localize';
import { Camera, CameraInitializationOptions } from './camera';
import { CameraInitializationError } from './error';
import { CameraNoEntityError } from './error';
import { getCameraEntityFromConfig } from './utils/camera-entity-from-config';
export interface EntityCameraInitializationOptions extends CameraInitializationOptions {
@@ -19,7 +18,7 @@ export class EntityCamera extends Camera {
: null;
if (!entity || !cameraEntityID) {
throw new CameraInitializationError(localize('error.no_camera_entity'), config);
throw new CameraNoEntityError(config);
}
this._entity = entity;
return await super.initialize(options);
+32 -2
View File
@@ -1,4 +1,34 @@
import { localize } from '../localize/localize.js';
import { AdvancedCameraCardError } from '../types.js';
export class CameraInitializationError extends AdvancedCameraCardError {}
export class CameraNoIDError extends AdvancedCameraCardError {}
class CameraInitializationError extends AdvancedCameraCardError {}
export class CameraNoEngineError extends CameraInitializationError {
constructor(context?: unknown) {
super(localize('error.no_camera_engine'), context);
}
}
export class CameraNoIDError extends CameraInitializationError {
constructor(context?: unknown) {
super(localize('error.no_camera_id'), context);
}
}
export class CameraDuplicateIDError extends CameraInitializationError {
constructor(context?: unknown) {
super(localize('error.duplicate_camera_id'), context);
}
}
export class CameraNoEntityError extends CameraInitializationError {
constructor(context?: unknown) {
super(localize('error.no_camera_entity'), context);
}
}
export class ReolinkInitializationError extends CameraInitializationError {
constructor(context?: unknown) {
super(localize('error.camera_initialization_reolink'), context);
}
}
+2 -3
View File
@@ -5,7 +5,6 @@ import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/pt
import { CameraConfig } from '../../config/schema/cameras';
import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
import { HomeAssistant } from '../../ha/types';
import { localize } from '../../localize/localize';
import { SEVERITIES } from '../../severity';
import {
CapabilitiesRaw,
@@ -15,7 +14,7 @@ import {
} from '../../types';
import { errorToConsole } from '../../utils/basic';
import { Camera, CameraInitializationOptions } from '../camera';
import { CameraInitializationError } from '../error';
import { CameraNoEntityError } from '../error';
import { CameraEndpoints, CameraEndpointsContext } from '../types';
import { getCameraEntityFromConfig } from '../utils/camera-entity-from-config';
import {
@@ -123,7 +122,7 @@ export class FrigateCamera extends Camera {
if (cameraEntity && (!hasCameraName || hasAutoTriggers)) {
entity = await entityRegistryManager.getEntity(hass, cameraEntity);
if (!entity) {
throw new CameraInitializationError(localize('error.no_camera_entity'), config);
throw new CameraNoEntityError(config);
}
}
+13 -27
View File
@@ -11,7 +11,6 @@ import {
} from '../config/schema/actions/custom/ptz.js';
import { CameraConfig, Rotation } from '../config/schema/cameras.js';
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
import { localize } from '../localize/localize.js';
import { Endpoint } from '../types.js';
import {
allPromises,
@@ -28,7 +27,11 @@ import { ViewItemCapabilities } from '../view/types.js';
import { Capabilities } from './capabilities.js';
import { CameraManagerEngineFactory } from './engine-factory.js';
import { CameraManagerEngine } from './engine.js';
import { CameraInitializationError } from './error.js';
import {
CameraDuplicateIDError,
CameraNoEngineError,
CameraNoIDError,
} from './error.js';
import { CameraManagerReadOnlyConfigStore, CameraManagerStore } from './store.js';
import {
CameraEndpoints,
@@ -150,12 +153,12 @@ export class CameraManager {
this._store = options?.store ?? new CameraManagerStore();
}
public async initializeCamerasFromConfig(): Promise<boolean> {
public async initializeCamerasFromConfig(): Promise<void> {
const config = this._api.getConfigManager().getConfig();
const hass = this._api.getHASSManager().getHASS();
if (!config || !hass) {
return false;
return;
}
this._requestLimit.concurrency =
@@ -169,15 +172,7 @@ export class CameraManager {
recursivelyMergeObjectsNotArrays(config?.cameras_global, camera),
);
try {
await this._initializeCameras(cameras);
} catch (e: unknown) {
this._api
.getMessageManager()
.setErrorIfHigherPriority(e, localize('error.camera_initialization'));
return false;
}
return true;
await this._initializeCameras(cameras);
}
public async destroy(): Promise<void> {
@@ -214,12 +209,9 @@ export class CameraManager {
}))
: null;
if (!engine || !engineType) {
throw new CameraInitializationError(
localize('error.no_camera_engine'),
// Camera initialization may modify the configuration. Keep the
// original config unchanged.
cloneDeep(cameraConfig),
);
// Camera initialization may modify the configuration. Keep the
// original config unchanged.
throw new CameraNoEngineError(cloneDeep(cameraConfig));
}
engines.set(engineType, engine);
output.set(cameraConfig, engine);
@@ -272,18 +264,12 @@ export class CameraManager {
if (!cameraID) {
await destroyCameras();
throw new CameraInitializationError(
localize('error.no_camera_id'),
camera.getConfig(),
);
throw new CameraNoIDError(camera.getConfig());
}
if (cameraIDs.has(cameraID)) {
await destroyCameras();
throw new CameraInitializationError(
localize('error.duplicate_camera_id'),
camera.getConfig(),
);
throw new CameraDuplicateIDError(camera.getConfig());
}
// Always ensure the actual ID used in the card is in the configuration itself.
+2 -8
View File
@@ -7,7 +7,6 @@ import {
import { DeviceRegistryManager } from '../../ha/registry/device/index';
import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
import { HomeAssistant } from '../../ha/types';
import { localize } from '../../localize/localize';
import {
CapabilitiesRaw,
Endpoint,
@@ -17,7 +16,7 @@ import {
import { createSelectOptionAction } from '../../utils/action.js';
import { Camera, CameraInitializationOptions } from '../camera';
import { EntityCamera } from '../entity-camera';
import { CameraInitializationError } from '../error';
import { ReolinkInitializationError } from '../error';
import { CameraEndpointsContext, CameraProxyConfig } from '../types';
import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
@@ -44,8 +43,6 @@ interface ReolinkCameraInitializationOptions extends CameraInitializationOptions
deviceRegistryManager: DeviceRegistryManager;
}
class ReolinkInitializationError extends CameraInitializationError {}
// Button entities for continuous PTZ movement (press to start, press stop to
// end). Discovered from button.{name}_ptz_{action} entities. Zoom button
// entities are disabled by default in the Reolink integration.
@@ -153,10 +150,7 @@ export class ReolinkCamera extends EntityCamera {
const channelOrUID = match?.groups?.channel_or_uid ?? null;
if (hostid === null || channelOrUID === null) {
throw new ReolinkInitializationError(
localize('error.camera_initialization_reolink'),
this.getConfig(),
);
throw new ReolinkInitializationError(this.getConfig());
}
const channelCandidate = Number(channelOrUID);