From ecdd02c7ce513b340db696ccf219ee63177d7c37 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Fri, 8 Mar 2024 20:00:35 -0800 Subject: [PATCH] Always render a view even if default is unsupported. --- src/camera-manager/frigate/camera.ts | 2 +- src/camera-manager/store.ts | 5 ++ src/card-controller/initialization-manager.ts | 2 +- src/card-controller/view-manager.ts | 26 ++++++++-- tests/camera-manager/frigate/camera.test.ts | 8 ++- tests/camera-manager/store.test.ts | 11 ++++ tests/card-controller/view-manager.test.ts | 51 +++++++++++++++++++ 7 files changed, 98 insertions(+), 7 deletions(-) diff --git a/src/camera-manager/frigate/camera.ts b/src/camera-manager/frigate/camera.ts index 35eb4a29..50a62cca 100644 --- a/src/camera-manager/frigate/camera.ts +++ b/src/camera-manager/frigate/camera.ts @@ -128,7 +128,7 @@ export class FrigateCamera extends Camera { hass: HomeAssistant, cameraConfig: CameraConfig, ): Promise { - if (!cameraConfig.frigate.camera_name) { + if (!cameraConfig.frigate.camera_name || isBirdseye(cameraConfig)) { return null; } diff --git a/src/camera-manager/store.ts b/src/camera-manager/store.ts index c27e3ebe..1f5d2846 100644 --- a/src/camera-manager/store.ts +++ b/src/camera-manager/store.ts @@ -24,6 +24,7 @@ export interface CameraManagerReadOnlyConfigStore { getCameraIDs(): Set; getVisibleCameraIDs(): Set; + getDefaultCameraID(): string | null; getAllDependentCameras(cameraID: string): Set; } @@ -61,6 +62,10 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore { return this.getVisibleCameraIDs().size; } + public getDefaultCameraID(): string | null { + return this._cameras.keys().next().value ?? null; + } + public getCameras(): Map { return this._cameras; } diff --git a/src/card-controller/initialization-manager.ts b/src/card-controller/initialization-manager.ts index aba58c22..3c640198 100644 --- a/src/card-controller/initialization-manager.ts +++ b/src/card-controller/initialization-manager.ts @@ -98,7 +98,7 @@ export class InitializationManager { if (hasViewRelatedActions) { this._api.getQueryStringManager().executeViewRelated(); } else { - this._api.getViewManager().setViewDefault(); + this._api.getViewManager().setViewDefault({ failSafe: true }); } } diff --git a/src/card-controller/view-manager.ts b/src/card-controller/view-manager.ts index fe5c9657..a9a6e13f 100644 --- a/src/card-controller/view-manager.ts +++ b/src/card-controller/view-manager.ts @@ -1,13 +1,23 @@ import { ViewContext } from 'view'; -import { FrigateCardConfig, FrigateCardView, ViewDisplayMode } from '../config/types'; -import { View } from '../view/view'; +import { + FRIGATE_CARD_VIEW_DEFAULT, + FrigateCardConfig, + FrigateCardView, + ViewDisplayMode, +} from '../config/types'; import { log } from '../utils/debug'; import { executeMediaQueryForView } from '../utils/media-to-view'; +import { View } from '../view/view'; import { CardViewAPI } from './types'; interface ViewManagerSetViewDefaultParameters { cameraID?: string; substream?: string; + + // When failSafe is true, the view will be changed to an "always-works" view + // (e.g. `live`) if the proposed view is unsupported. By default the view will + // just not be changed. + failSafe?: boolean; } export interface ViewManagerSetViewParameters @@ -75,8 +85,16 @@ export class ViewManager { cameraID = cameras.keys().next().value; } } - const viewName = params?.viewName ?? this._view?.view ?? config.view.default; - if (cameraID && viewName && this.isViewSupportedByCamera(cameraID, viewName)) { + let viewName = params?.viewName ?? this._view?.view ?? config.view.default; + if (cameraID && viewName) { + if (!this.isViewSupportedByCamera(cameraID, viewName)) { + if (params.failSafe) { + viewName = FRIGATE_CARD_VIEW_DEFAULT; + } else { + return; + } + } + const displayMode = this._view?.displayMode ?? this._getDefaultDisplayModeForView(viewName, config); diff --git a/tests/camera-manager/frigate/camera.test.ts b/tests/camera-manager/frigate/camera.test.ts index f9698fcf..bfa8bdbe 100644 --- a/tests/camera-manager/frigate/camera.test.ts +++ b/tests/camera-manager/frigate/camera.test.ts @@ -143,13 +143,18 @@ describe('FrigateCamera', () => { it('basic non-birdseye', async () => { const camera = new FrigateCamera( - createCameraConfig(), + createCameraConfig({ + frigate: { + camera_name: 'front_door', + }, + }), mock(), ); await camera.initialize(createHASS(), mock()); expect(camera.getCapabilities()).toEqual(nonBirdseyeBaseCapabilities); + expect(vi.mocked(getPTZInfo)).toBeCalled(); }); it('basic birdseye', async () => { @@ -173,6 +178,7 @@ describe('FrigateCamera', () => { supportsRecordings: false, supportsTimeline: false, }); + expect(vi.mocked(getPTZInfo)).not.toBeCalled(); }); describe('with ptz', () => { diff --git a/tests/camera-manager/store.test.ts b/tests/camera-manager/store.test.ts index fa67beab..02b37e51 100644 --- a/tests/camera-manager/store.test.ts +++ b/tests/camera-manager/store.test.ts @@ -61,6 +61,17 @@ describe('CameraManagerStore', async () => { expect(store.getVisibleCameraCount()).toBe(1); }); + describe('getDefaultCameraID', () => { + it('with camera', async () => { + const store = setupStore(); + expect(store.getDefaultCameraID()).toBe('camera-visible'); + }); + it('without camera', async () => { + const store = new CameraManagerStore(); + expect(store.getDefaultCameraID()).toBeNull(); + }); + }); + describe('getCamera', async () => { it('present', async () => { const store = setupStore(); diff --git a/tests/card-controller/view-manager.test.ts b/tests/card-controller/view-manager.test.ts index 07844a7a..883954c7 100644 --- a/tests/card-controller/view-manager.test.ts +++ b/tests/card-controller/view-manager.test.ts @@ -324,6 +324,57 @@ describe('ViewManager.setViewByParameters', () => { expect(manager.getView()).toBeNull(); }); + describe('should handle unsupported view', () => { + it('without failsafe', () => { + const api = createCardAPI(); + vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); + vi.mocked(api.getCameraManager().getStore).mockReturnValue( + createStore([ + { + cameraID: 'camera.kitchen', + }, + ]), + ); + vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); + vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(createCameraCapabilities({ + supportsSnapshots: false, + })); + + const manager = new ViewManager(api); + manager.setViewByParameters({ + viewName: 'snapshots', + }); + + expect(manager.hasView()).toBeFalsy(); + expect(manager.getView()).toBeNull(); + }); + + it('with failsafe', () => { + const api = createCardAPI(); + vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager()); + vi.mocked(api.getCameraManager().getStore).mockReturnValue( + createStore([ + { + cameraID: 'camera.kitchen', + }, + ]), + ); + vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); + vi.mocked(api.getCameraManager().getCameraCapabilities).mockReturnValue(createCameraCapabilities({ + supportsSnapshots: false, + })); + + const manager = new ViewManager(api); + manager.setViewByParameters({ + viewName: 'snapshots', + failSafe: true, + }); + + expect(manager.hasView()).toBeTruthy(); + expect(manager.getView()?.view).toBe('live'); + }); + }); + describe('should set view by parameters and respect display mode in config for view', () => { it.each([ ['media' as const],