Repository: dermotduffy/frigate-hass-card
Pull request #1395 (2024-03-09 04:02:56)

Always render a view even if default is unsupported


Patch:
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<PTZCapabilities | null> {
-    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<string>;
   getVisibleCameraIDs(): Set<string>;
+  getDefaultCameraID(): string | null;
 
   getAllDependentCameras(cameraID: string): Set<string>;
 }
@@ -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<string, Camera> {
     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);


Test patch:
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<CameraManagerEngine>(),
       );
 
       await camera.initialize(createHASS(), mock<EntityRegistryManager>());
 
       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],