feat: Make cameras optional (#2343)

This commit is contained in:
Dermot Duffy authored and GitHub committed 2026-02-13 20:21:05 -08:00
1 parent 21f9469784
commit 93f1f08e51
61 files changed
+1312 -557

No files matched your search

+89 -3
View File
@@ -1,4 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { FoldersManager } from '../../../src/card-controller/folders/manager';
import { ViewFactory } from '../../../src/card-controller/view/factory';
import { ViewModifier } from '../../../src/card-controller/view/types';
import { AdvancedCameraCardView } from '../../../src/config/schema/common/const';
@@ -9,6 +11,7 @@ import {
createCapabilities,
createCardAPI,
createConfig,
createFolder,
createStore,
createView,
} from '../../test-utils';
@@ -20,7 +23,7 @@ describe('getViewDefault', () => {
expect(factory.getViewDefault()).toBeNull();
});
it('should throw if no cameras support view', () => {
it('should throw when no cameras support view', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
@@ -47,6 +50,23 @@ describe('getViewDefault', () => {
expect(() => factory.getViewDefault()).toThrowError(/No cameras support this view/);
});
it('should use folders view as default when folders exist without cameras', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getFoldersManager).mockReturnValue(mock<FoldersManager>());
vi.mocked(api.getFoldersManager().hasFolders).mockReturnValue(true);
vi.mocked(api.getFoldersManager().getFolder).mockReturnValue(createFolder());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(createStore([]));
const factory = new ViewFactory(api);
const view = factory.getViewDefault();
expect(view?.view).toBe('folders');
expect(view?.camera).toBeNull();
});
it('should create view', () => {
const factory = new ViewFactory(createPopulatedAPI());
const view = factory.getViewDefault();
@@ -217,8 +237,6 @@ describe('getViewByParameters', () => {
const factory = new ViewFactory(api);
expect(() =>
factory.getViewByParameters({
// Since no camera is specified, and no camera supports the capabilities
// necessary for this view, the view will be null.
params: {
view: 'snapshots',
},
@@ -226,6 +244,56 @@ describe('getViewByParameters', () => {
).toThrowError(/No cameras support this view/);
});
describe('should handle no camera for view with failsafe', () => {
it('should choose default view when it does not require a camera', () => {
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(createStore([]));
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
default: 'diagnostics',
},
}),
);
const factory = new ViewFactory(api);
const view = factory.getViewByParameters({
params: {
view: 'live',
},
failSafe: true,
});
expect(view?.is('diagnostics')).toBeTruthy();
expect(view?.camera).toBeNull();
});
it('should choose default view and camera when it requires a camera', () => {
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([{ cameraID: 'camera.one' }]),
);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
view: {
default: 'live',
},
}),
);
const factory = new ViewFactory(api);
const view = factory.getViewByParameters({
params: {
view: 'snapshots',
},
failSafe: true,
});
expect(view?.is('live')).toBeTruthy();
expect(view?.camera).toBe('camera.one');
});
});
describe('should handle unsupported view', () => {
it('should throw without failsafe', () => {
const api = createCardAPI();
@@ -285,6 +353,24 @@ describe('getViewByParameters', () => {
});
expect(view?.is('live')).toBeTruthy();
});
it('should throw and omit capabilities when camera object is missing from store', () => {
const api = createCardAPI();
const store = createStore([{ cameraID: 'camera.one' }]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.spyOn(store, 'getCamera').mockReturnValue(null);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
const factory = new ViewFactory(api);
expect(() =>
factory.getViewByParameters({
params: {
camera: 'camera.one',
view: 'snapshots',
},
}),
).toThrowError(/The selected camera or media does not support this view/);
});
});
it('should call modifiers', () => {
@@ -21,29 +21,27 @@ const createAPIWithSubstreams = (
config?: RawAdvancedCameraCardConfig,
): CardController => {
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
createStore([
{
cameraID: 'camera.office',
capabilities: createCapabilities({
live: true,
substream: true,
}),
config: createCameraConfig({
dependencies: {
all_cameras: true,
},
}),
},
{
cameraID: 'camera.kitchen',
capabilities: createCapabilities({
substream: true,
}),
},
]),
);
const store = createStore([
{
cameraID: 'camera.office',
capabilities: createCapabilities({
live: true,
substream: true,
}),
config: createCameraConfig({
dependencies: {
all_cameras: true,
},
}),
},
{
cameraID: 'camera.kitchen',
capabilities: createCapabilities({
substream: true,
}),
},
]);
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
return api;
};
@@ -97,11 +95,25 @@ describe('should turn on substream', () => {
expect(hasSubstream(view)).toBe(false);
const api = createCardAPI();
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
const cameraManager = createCameraManager(createStore([]));
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
const modifier = new SubstreamOnViewModifier(api);
modifier.modify(view);
expect(hasSubstream(view)).toBe(false);
});
it('without camera', () => {
const view = createView({
camera: null,
view: 'live',
});
const api = createCardAPI();
const modifier = new SubstreamOnViewModifier(api);
modifier.modify(view);
expect(hasSubstream(view)).toBe(false);
});
});
@@ -257,7 +257,7 @@ describe('ViewQueryExecutor', () => {
const store = createStore([
{
cameraID: 'camera.office',
capabilities: createCapabilities({ [mode]: true }),
capabilities: createCapabilities({ live: true, [mode]: true }),
config: createCameraConfig({
media: {
type: mode === 'reviews' ? 'reviews' : 'events',
@@ -546,6 +546,34 @@ describe('ViewQueryExecutor', () => {
expect(view.query).toBeNull();
expect(view.queryResults).toBeNull();
});
it('should handle non-grid view without a camera', async () => {
const api = createPopulatedAPI();
const viewQueryExecutor = new ViewQueryExecutor(api);
const view = createView({
view: 'live',
camera: null,
});
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
applyViewModifiers(view, modifiers);
expect(view.query).toBeNull();
});
it('should handle diagnostics view without a camera', async () => {
const api = createPopulatedAPI();
const viewQueryExecutor = new ViewQueryExecutor(api);
const view = createView({
view: 'diagnostics',
camera: null,
});
const modifiers = await viewQueryExecutor.getNewQueryModifiers(view);
applyViewModifiers(view, modifiers);
expect(view.query).toBeNull();
});
});
describe('with a timeline view', () => {
@@ -799,6 +827,7 @@ describe('ViewQueryExecutor', () => {
'should execute default folder query with %s view',
async (viewName: AdvancedCameraCardView) => {
const api = createPopulatedAPI();
vi.mocked(api.getFoldersManager().hasFolders).mockReturnValue(true);
vi.mocked(api.getFoldersManager().getFolder).mockReturnValue({
type: 'ha',
id: 'office',
@@ -827,6 +856,7 @@ describe('ViewQueryExecutor', () => {
it('should execute default folder query with folder view and handle null results', async () => {
const api = createPopulatedAPI();
vi.mocked(api.getFoldersManager().hasFolders).mockReturnValue(true);
vi.mocked(api.getFoldersManager().getFolder).mockReturnValue(null);
vi.mocked(api.getFoldersManager().getDefaultQueryParameters).mockReturnValue(
null,