Files
advanced-camera-card/tests/camera-manager/manager.test.ts
T

1243 lines
40 KiB
TypeScript

import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
import add from 'date-fns/add';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { Camera } from '../../src/camera-manager/camera';
import { Capabilities } from '../../src/camera-manager/capabilities';
import { CameraManagerEngine } from '../../src/camera-manager/engine';
import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-factory.js';
import {
CameraManager,
QueryClassifier,
QueryResultClassifier,
} from '../../src/camera-manager/manager';
import { CameraManagerStore } from '../../src/camera-manager/store';
import {
CameraEndpoint,
CameraEndpoints,
CameraEndpointsContext,
CameraEvent,
CameraManagerCameraMetadata,
CameraManagerMediaCapabilities,
Engine,
EventQuery,
EventQueryResults,
MediaMetadata,
QueryResults,
QueryResultsType,
QueryType,
} from '../../src/camera-manager/types';
import { sortMedia } from '../../src/camera-manager/utils/sort-media';
import { CardController } from '../../src/card-controller/controller';
import { CameraConfig } from '../../src/config/types';
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry';
import { ViewMedia } from '../../src/view/media';
import {
TestViewMedia,
createCamera,
createCameraConfig,
createCapabilities,
createCardAPI,
createConfig,
createHASS,
generateViewMediaArray,
} from '../test-utils';
describe('QueryClassifier', async () => {
it('should classify event query', async () => {
expect(QueryClassifier.isEventQuery({ type: QueryType.Event })).toBeTruthy();
expect(QueryClassifier.isEventQuery({ type: QueryType.Recording })).toBeFalsy();
expect(
QueryClassifier.isEventQuery({ type: QueryType.RecordingSegments }),
).toBeFalsy();
expect(QueryClassifier.isEventQuery({ type: QueryType.MediaMetadata })).toBeFalsy();
});
it('should classify recording query', async () => {
expect(QueryClassifier.isRecordingQuery({ type: QueryType.Event })).toBeFalsy();
expect(QueryClassifier.isRecordingQuery({ type: QueryType.Recording })).toBeTruthy();
expect(
QueryClassifier.isRecordingQuery({ type: QueryType.RecordingSegments }),
).toBeFalsy();
expect(
QueryClassifier.isRecordingQuery({ type: QueryType.MediaMetadata }),
).toBeFalsy();
});
it('should classify recording segments query', async () => {
expect(
QueryClassifier.isRecordingSegmentsQuery({ type: QueryType.Event }),
).toBeFalsy();
expect(
QueryClassifier.isRecordingSegmentsQuery({ type: QueryType.Recording }),
).toBeFalsy();
expect(
QueryClassifier.isRecordingSegmentsQuery({ type: QueryType.RecordingSegments }),
).toBeTruthy();
expect(
QueryClassifier.isRecordingSegmentsQuery({ type: QueryType.MediaMetadata }),
).toBeFalsy();
});
it('should classify media metadata query', async () => {
expect(QueryClassifier.isMediaMetadataQuery({ type: QueryType.Event })).toBeFalsy();
expect(
QueryClassifier.isMediaMetadataQuery({ type: QueryType.Recording }),
).toBeFalsy();
expect(
QueryClassifier.isMediaMetadataQuery({ type: QueryType.RecordingSegments }),
).toBeFalsy();
expect(
QueryClassifier.isMediaMetadataQuery({ type: QueryType.MediaMetadata }),
).toBeTruthy();
});
});
describe('QueryResultClassifier', async () => {
const createResults = (type: Partial<QueryResultsType>): QueryResults => {
return {
type: type,
engine: Engine.Generic,
};
};
it('should classify event query result', async () => {
expect(
QueryResultClassifier.isEventQueryResult(createResults(QueryResultsType.Event)),
).toBeTruthy();
expect(
QueryResultClassifier.isEventQueryResult(
createResults(QueryResultsType.Recording),
),
).toBeFalsy();
expect(
QueryResultClassifier.isEventQueryResult(
createResults(QueryResultsType.RecordingSegments),
),
).toBeFalsy();
expect(
QueryResultClassifier.isEventQueryResult(
createResults(QueryResultsType.MediaMetadata),
),
).toBeFalsy();
});
it('should classify recording query result', async () => {
expect(
QueryResultClassifier.isRecordingQueryResult(
createResults(QueryResultsType.Event),
),
).toBeFalsy();
expect(
QueryResultClassifier.isRecordingQueryResult(
createResults(QueryResultsType.Recording),
),
).toBeTruthy();
expect(
QueryResultClassifier.isRecordingQueryResult(
createResults(QueryResultsType.RecordingSegments),
),
).toBeFalsy();
expect(
QueryResultClassifier.isRecordingQueryResult(
createResults(QueryResultsType.MediaMetadata),
),
).toBeFalsy();
});
it('should classify recording segments query result', async () => {
expect(
QueryResultClassifier.isRecordingSegmentsQueryResult(
createResults(QueryResultsType.Event),
),
).toBeFalsy();
expect(
QueryResultClassifier.isRecordingSegmentsQueryResult(
createResults(QueryResultsType.Recording),
),
).toBeFalsy();
expect(
QueryResultClassifier.isRecordingSegmentsQueryResult(
createResults(QueryResultsType.RecordingSegments),
),
).toBeTruthy();
expect(
QueryResultClassifier.isRecordingSegmentsQueryResult(
createResults(QueryResultsType.MediaMetadata),
),
).toBeFalsy();
});
it('should classify media metadata query result', async () => {
expect(
QueryResultClassifier.isMediaMetadataQueryResult(
createResults(QueryResultsType.Event),
),
).toBeFalsy();
expect(
QueryResultClassifier.isMediaMetadataQueryResult(
createResults(QueryResultsType.Recording),
),
).toBeFalsy();
expect(
QueryResultClassifier.isMediaMetadataQueryResult(
createResults(QueryResultsType.RecordingSegments),
),
).toBeFalsy();
expect(
QueryResultClassifier.isMediaMetadataQueryResult(
createResults(QueryResultsType.MediaMetadata),
),
).toBeTruthy();
});
});
describe('CameraManager', async () => {
const baseCameraConfig = {
id: 'id',
camera_entity: 'camera.foo',
engine: 'generic',
};
const baseEventQuery: EventQuery = {
type: QueryType.Event as const,
cameraIDs: new Set(['id']),
};
const baseEventQueryResults: EventQueryResults = {
type: QueryResultsType.Event as const,
engine: Engine.Generic,
};
const baseRecordingQuery = {
type: QueryType.Recording as const,
cameraIDs: new Set(['id']),
};
const baseRecordingQueryResults = {
type: QueryResultsType.Recording as const,
engine: Engine.Generic,
};
const createCameraManager = (
api: CardController,
engine?: CameraManagerEngine,
cameras: {
config?: CameraConfig;
engineType?: Engine | null;
capabilties?: Capabilities;
}[] = [{}],
factory?: CameraManagerEngineFactory,
): CameraManager => {
const camerasConfig = cameras?.map(
(camera) => camera.config ?? createCameraConfig(baseCameraConfig),
);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
cameras: camerasConfig,
}),
);
const mockFactory = factory ?? mock<CameraManagerEngineFactory>();
const mockEngine = engine ?? mock<CameraManagerEngine>();
vi.mocked(mockFactory.createEngine).mockResolvedValueOnce(mockEngine);
for (const camera of cameras ?? []) {
const engineType =
camera.engineType === undefined ? Engine.Generic : camera.engineType;
if (engineType) {
vi.mocked(mockEngine.createCamera).mockImplementationOnce(
async (
_hass: HomeAssistant,
_entityRegistryManager: EntityRegistryManager,
cameraConfig: CameraConfig,
): Promise<Camera> =>
createCamera(
cameraConfig,
mockEngine,
camera.capabilties ?? createCapabilities(),
),
);
}
vi.mocked(mockFactory.getEngineForCamera).mockResolvedValueOnce(engineType);
}
return new CameraManager(api, { factory: mockFactory });
};
it('should reset', async () => {
const store = new CameraManagerStore();
store.addCamera(
new Camera(createCameraConfig({ id: 'id' }), mock<CameraManagerEngine>()),
);
const manager = new CameraManager(createCardAPI(), { store: store });
expect(manager.getStore().getCameraCount()).toBe(1);
await manager.reset();
expect(manager.getStore().getCameraCount()).toBe(0);
});
describe('should initialize cameras from config', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('successfully', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = createCameraManager(api);
await manager.initializeCamerasFromConfig();
expect(manager.getStore().getCameraCount()).toBe(1);
expect(manager.isInitialized()).toBeTruthy();
});
it('without hass', async () => {
const manager = createCameraManager(createCardAPI());
await manager.initializeCamerasFromConfig();
expect(manager.getStore().getCameraCount()).toBe(0);
});
it('without a config', async () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(null);
const manager = createCameraManager(api);
await manager.initializeCamerasFromConfig();
expect(manager.getStore().getCameraCount()).toBe(0);
});
it('without an id', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = createCameraManager(api, mock<CameraManagerEngine>(), [
{
config: createCameraConfig({
// No id.
engine: 'generic',
}),
},
]);
expect(await manager.initializeCamerasFromConfig()).toBeFalsy();
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(
new Error(
'Could not determine camera id for the following camera, ' +
"may need to set 'id' parameter manually",
),
);
});
it('with a duplicate id', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const cameraConfig = createCameraConfig({
id: 'DUPLICATE',
engine: 'generic',
});
const manager = createCameraManager(api, mock<CameraManagerEngine>(), [
{
config: cameraConfig,
},
{
config: cameraConfig,
},
]);
expect(await manager.initializeCamerasFromConfig()).toBeFalsy();
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(
new Error(
'Duplicate Frigate camera id for the following camera, ' +
"use the 'id' parameter to uniquely identify cameras",
),
);
});
it('with no engine', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = createCameraManager(api, mock<CameraManagerEngine>(), [
{
config: createCameraConfig({
id: 'id',
}),
engineType: null,
},
]);
expect(await manager.initializeCamerasFromConfig()).toBeFalsy();
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(
new Error('Could not determine suitable engine for camera'),
);
});
it('should pass events to triggers manager', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const factory = mock<CameraManagerEngineFactory>();
const manager = createCameraManager(
api,
mock<CameraManagerEngine>(),
[{}],
factory,
);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const triggerCallback = factory.createEngine.mock.calls[0][1];
const cameraEvent: CameraEvent = {
cameraID: 'camera',
type: 'new',
};
triggerCallback?.(cameraEvent);
expect(api.getTriggersManager().handleCameraEvent).toBeCalledWith(cameraEvent);
});
describe('should fetch entity list when required', () => {
it('with entity based trigger', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = createCameraManager(api, mock<CameraManagerEngine>(), [
{
config: createCameraConfig({
...baseCameraConfig,
triggers: {
occupancy: true,
},
}),
},
]);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
expect(api.getEntityRegistryManager().fetchEntityList).toBeCalled();
});
it('without entity based trigger', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = createCameraManager(api);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
expect(api.getEntityRegistryManager().fetchEntityList).not.toBeCalled();
});
});
describe('generate default queries', () => {
it.each([
[
QueryType.Event as const,
'generateDefaultEventQuery',
'generateDefaultEventQueries',
],
[
QueryType.Recording as const,
'generateDefaultRecordingQuery',
'generateDefaultRecordingQueries',
],
[
QueryType.RecordingSegments as const,
'generateDefaultRecordingSegmentsQuery',
'generateDefaultRecordingSegmentsQueries',
],
])(
'basic %s',
async (
queryType: string,
engineMethodName: string,
managerMethodName: string,
) => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const queries = [{ type: queryType, cameraIDs: new Set(['id']) }];
engine[engineMethodName].mockReturnValue(queries);
expect(manager[managerMethodName]('id')).toEqual(queries);
},
);
it('without camera', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = createCameraManager(api, mock<CameraManagerEngine>());
expect(manager.generateDefaultEventQueries('not_a_camera')).toBeNull();
});
it('without queries', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
engine.generateDefaultEventQuery.mockReturnValue(null);
expect(manager.generateDefaultEventQueries('id')).toBeNull();
});
});
it('should merge defaults correctly', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine, [
{
config: createCameraConfig({
...baseCameraConfig,
triggers: {
events: ['snapshots'],
},
}),
},
]);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
expect(manager.getStore().getCamera('id')?.getConfig().triggers.events).toEqual([
'snapshots',
]);
});
});
describe('should get media metadata', () => {
const query = {
type: QueryType.MediaMetadata as const,
cameraIDs: new Set('id'),
};
it('with nothing', async () => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const queryResults = {
type: QueryResultsType.MediaMetadata as const,
engine: Engine.Generic,
metadata: {},
};
engine.getMediaMetadata.mockResolvedValue(new Map([[query, queryResults]]));
expect(await manager.getMediaMetadata()).toBeNull();
});
it.each([['days'], ['tags'], ['where'], ['what']])(
'with %s',
async (metadataType: string) => {
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const metadata: MediaMetadata = {
[metadataType]: new Set(['data']),
};
const queryResults = {
type: QueryResultsType.MediaMetadata as const,
engine: Engine.Generic,
metadata: metadata,
};
engine.getMediaMetadata.mockResolvedValue(new Map([[query, queryResults]]));
expect(await manager.getMediaMetadata()).toEqual(metadata);
},
);
});
describe('should get events', () => {
it('without hass', async () => {
const manager = createCameraManager(createCardAPI());
expect(await manager.getEvents(baseEventQuery)).toEqual(new Map());
});
it('without cameras', async () => {
const api = createCardAPI();
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const manager = createCameraManager(api);
expect(await manager.getEvents(baseEventQuery)).toEqual(new Map());
});
it('successfully', async () => {
const api = createCardAPI();
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const engineOptions = {};
const results = new Map([[baseEventQuery, baseEventQueryResults]]);
engine.getEvents.mockResolvedValue(results);
expect(await manager.getEvents(baseEventQuery, engineOptions)).toEqual(results);
expect(engine.getEvents).toBeCalledWith(
hass,
expect.anything(),
baseEventQuery,
engineOptions,
);
});
});
describe('should get recordings', () => {
it('successfully', async () => {
const api = createCardAPI();
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const engineOptions = {};
const results = new Map([[baseRecordingQuery, baseRecordingQueryResults]]);
engine.getRecordings.mockResolvedValue(results);
expect(await manager.getRecordings(baseRecordingQuery, engineOptions)).toEqual(
results,
);
});
});
describe('should get recording segments', () => {
const query = {
type: QueryType.RecordingSegments as const,
cameraIDs: new Set(['id']),
start: new Date(),
end: new Date(),
};
const queryResults = {
type: QueryResultsType.RecordingSegments as const,
engine: Engine.Generic,
segments: [],
};
it('successfully', async () => {
const api = createCardAPI();
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const engineOptions = {};
const results = new Map([[query, queryResults]]);
engine.getRecordingSegments.mockResolvedValue(results);
expect(await manager.getRecordingSegments(query, engineOptions)).toEqual(results);
});
});
describe('should execute media queries', () => {
it('events', async () => {
const api = createCardAPI();
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const engine = mock<CameraManagerEngine>();
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const results = new Map([[baseEventQuery, baseEventQueryResults]]);
engine.getEvents.mockResolvedValue(results);
const media = sortMedia(generateViewMediaArray({ count: 5 }));
engine.generateMediaFromEvents.mockReturnValue(media);
expect(await manager.executeMediaQueries([baseEventQuery])).toEqual(media);
});
it('no converted media', async () => {
const api = createCardAPI();
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const engine = mock<CameraManagerEngine>();
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const results = new Map([[baseEventQuery, baseEventQueryResults]]);
engine.getEvents.mockResolvedValue(results);
engine.generateMediaFromEvents.mockReturnValue(null);
expect(await manager.executeMediaQueries([baseEventQuery])).toEqual([]);
});
it('without matching camera engine during conversion', async () => {
const api = createCardAPI();
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const engine = mock<CameraManagerEngine>();
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const results = new Map([
[baseEventQuery, { ...baseEventQueryResults, engine: Engine.MotionEye }],
]);
engine.getEvents.mockResolvedValue(results);
expect(await manager.executeMediaQueries([baseEventQuery])).toEqual([]);
});
it('recordings', async () => {
const api = createCardAPI();
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const engine = mock<CameraManagerEngine>();
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const results = new Map([[baseRecordingQuery, baseRecordingQueryResults]]);
engine.getRecordings.mockResolvedValue(results);
const media = sortMedia(generateViewMediaArray({ count: 5 }));
engine.generateMediaFromRecordings.mockReturnValue(media);
expect(await manager.executeMediaQueries([baseRecordingQuery])).toEqual(media);
});
it('without hass', async () => {
const engine = mock<CameraManagerEngine>();
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
const manager = createCameraManager(createCardAPI(), engine);
const results = new Map([[baseEventQuery, baseEventQueryResults]]);
engine.getEvents.mockResolvedValue(results);
expect(await manager.executeMediaQueries([baseEventQuery])).toEqual([]);
});
});
describe('should extend media queries', () => {
const dateBase = new Date('2024-03-01T20:01:00');
const mediaTwoCameras = generateViewMediaArray({ count: 5 });
const mediaMixedStart: ViewMedia[] = [
new TestViewMedia({
startTime: dateBase,
}),
new TestViewMedia({
startTime: add(dateBase, { days: 1 }),
}),
new TestViewMedia({
startTime: add(dateBase, { days: 2 }),
}),
];
it('without hass', async () => {
const engine = mock<CameraManagerEngine>();
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
const manager = createCameraManager(createCardAPI(), engine);
expect(await manager.extendMediaQueries([baseEventQuery], [], 'later')).toBeNull();
});
it.each([
['empty query and results', new Map(), [], [], [], null],
[
'query without existing media',
new Map([[baseEventQuery, baseEventQueryResults]]),
[],
[{ ...baseEventQuery, limit: 50 }],
generateViewMediaArray({ count: 5 }),
{
queries: [{ ...baseEventQuery, limit: 50 }],
results: sortMedia(generateViewMediaArray({ count: 5 })),
},
],
[
'query that extends existing results',
new Map([[baseEventQuery, baseEventQueryResults]]),
generateViewMediaArray({ count: 5, cameraIDs: ['kitchen'] }),
[{ ...baseEventQuery, limit: 50 }],
generateViewMediaArray({ count: 5, cameraIDs: ['office'] }),
{
queries: [{ ...baseEventQuery, limit: 50 }],
results: sortMedia(mediaTwoCameras),
},
],
[
'query with existing media but no new media',
new Map([[baseEventQuery, baseEventQueryResults]]),
mediaTwoCameras,
[
{
...baseEventQuery,
limit: 50,
},
],
// Fetch identical media again.
mediaTwoCameras,
// Returns null to signify nothing new.
null,
],
[
'query fetching later',
new Map([[{ ...baseEventQuery, start: dateBase }, baseEventQueryResults]]),
mediaMixedStart,
[
{
...baseEventQuery,
limit: 50,
start: add(dateBase, { days: 2 }),
},
],
mediaTwoCameras,
{
queries: [{ ...baseEventQuery, limit: 50, start: dateBase }],
results: sortMedia(mediaMixedStart.concat(mediaTwoCameras)),
},
'later' as const,
],
[
'query fetching earlier',
new Map([[{ ...baseEventQuery, start: dateBase }, baseEventQueryResults]]),
mediaMixedStart,
[
{
...baseEventQuery,
limit: 50,
end: dateBase,
},
],
mediaTwoCameras,
{
queries: [{ ...baseEventQuery, limit: 50, start: dateBase }],
results: sortMedia(mediaMixedStart.concat(mediaTwoCameras)),
},
'earlier' as const,
],
])(
'handles %s',
async (
_name: string,
// The previously submitted query & results.
inputQueries: Map<EventQuery, EventQueryResults>,
// The previously received media.
inputMediaResults: ViewMedia[],
// The queries expected to be dispatched.
newChunkQueries: EventQuery[],
// The media received from the new queries.
outputMediaResults: ViewMedia[],
// The expect extended queries and results.
expected?: {
queries: EventQuery[];
results: ViewMedia[];
} | null,
direction?: 'earlier' | 'later',
) => {
const engine = mock<CameraManagerEngine>();
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
engine.getEvents.mockResolvedValue(inputQueries);
engine.generateMediaFromEvents.mockReturnValue(outputMediaResults);
expect(
await manager.extendMediaQueries(
[...inputQueries.keys()],
inputMediaResults,
direction ?? 'later',
),
).toEqual(expected);
// Make sure the issued queries are correct.
for (const query of newChunkQueries) {
expect(engine.getEvents).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
query,
undefined,
);
}
},
);
});
describe('should get media download path', () => {
it('without camera', async () => {
const manager = createCameraManager(createCardAPI());
expect(await manager.getMediaDownloadPath(new TestViewMedia())).toBeNull();
});
it('without hass', async () => {
const api = createCardAPI();
const manager = createCameraManager(api);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
expect(await manager.getMediaDownloadPath(new TestViewMedia())).toBeNull();
});
it('successfully', async () => {
const api = createCardAPI();
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const result: CameraEndpoint = {
endpoint: 'http://localhost/path/to/media',
};
vi.mocked(engine.getMediaDownloadPath).mockResolvedValue(result);
expect(
await manager.getMediaDownloadPath(new TestViewMedia({ cameraID: 'id' })),
).toBe(result);
});
});
describe('should get media capabilities', () => {
it('without camera', async () => {
const manager = createCameraManager(createCardAPI());
expect(manager.getMediaCapabilities(new TestViewMedia())).toBeNull();
});
it('successfully', async () => {
const api = createCardAPI();
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const result: CameraManagerMediaCapabilities = {
canFavorite: false,
canDownload: false,
};
vi.mocked(engine.getMediaCapabilities).mockReturnValue(result);
expect(manager.getMediaCapabilities(new TestViewMedia({ cameraID: 'id' }))).toBe(
result,
);
});
});
describe('should favorite media', () => {
it('without camera', async () => {
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(createCardAPI(), engine);
manager.favoriteMedia(new TestViewMedia(), true);
expect(engine.favoriteMedia).not.toBeCalled();
});
it('successfully', async () => {
const api = createCardAPI();
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const media = new TestViewMedia({ cameraID: 'id' });
manager.favoriteMedia(media, true);
expect(engine.favoriteMedia).toBeCalledWith(hass, expect.anything(), media, true);
});
});
describe('should get camera endpoints', () => {
it('without camera', () => {
const manager = createCameraManager(createCardAPI());
expect(manager.getCameraEndpoints('BAD')).toBeNull();
});
it('successfully', async () => {
const api = createCardAPI();
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const result: CameraEndpoints = {};
const context: CameraEndpointsContext = {};
vi.mocked(engine.getCameraEndpoints).mockReturnValue(result);
expect(manager.getCameraEndpoints('id', context)).toBe(result);
expect(engine.getCameraEndpoints).toBeCalledWith(expect.anything(), context);
});
});
describe('should get camera metadata', () => {
it('without camera', () => {
const manager = createCameraManager(createCardAPI());
expect(manager.getCameraMetadata('BAD')).toBeNull();
});
it('successfully', async () => {
const api = createCardAPI();
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const result: CameraManagerCameraMetadata = {
title: 'My Camera',
icon: 'mdi:camera',
};
vi.mocked(engine.getCameraMetadata).mockReturnValue(result);
expect(manager.getCameraMetadata('id')).toBe(result);
});
});
describe('should get camera capabilities', () => {
it('without camera', () => {
const manager = createCameraManager(createCardAPI());
expect(manager.getCameraCapabilities('BAD')).toBeNull();
});
it('successfully', async () => {
const api = createCardAPI();
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(api, engine);
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
expect(manager.getCameraCapabilities('id')).toEqual(createCapabilities());
});
});
describe('should get aggregate camera capabilities', () => {
it('without camera', () => {
const manager = createCameraManager(createCardAPI());
const capabilities = manager.getAggregateCameraCapabilities();
expect(capabilities.has('favorite-events')).toBeFalsy();
expect(capabilities.has('favorite-recordings')).toBeFalsy();
expect(capabilities.has('seek')).toBeFalsy();
expect(capabilities.has('live')).toBeFalsy();
expect(capabilities.has('clips')).toBeFalsy();
expect(capabilities.has('recordings')).toBeFalsy();
expect(capabilities.has('snapshots')).toBeFalsy();
});
it('successfully', async () => {
const api = createCardAPI();
const manager = createCameraManager(api, mock<CameraManagerEngine>(), [
{
capabilties: new Capabilities({
'favorite-events': false,
'favorite-recordings': false,
seek: false,
live: false,
clips: false,
recordings: false,
snapshots: false,
}),
},
{
config: createCameraConfig({ baseCameraConfig, id: 'another' }),
capabilties: new Capabilities({
'favorite-events': true,
'favorite-recordings': true,
seek: true,
live: true,
clips: true,
recordings: true,
snapshots: true,
ptz: {
panTilt: ['continuous'],
},
}),
},
]);
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const capabilities = manager.getAggregateCameraCapabilities();
expect(capabilities.has('favorite-events')).toBeTruthy();
expect(capabilities.has('favorite-recordings')).toBeTruthy();
expect(capabilities.has('seek')).toBeTruthy();
expect(capabilities.has('live')).toBeTruthy();
expect(capabilities.has('clips')).toBeTruthy();
expect(capabilities.has('recordings')).toBeTruthy();
expect(capabilities.has('snapshots')).toBeTruthy();
});
});
describe('should execute PTZ action', () => {
it('without camera', () => {
const engine = mock<CameraManagerEngine>();
const manager = createCameraManager(createCardAPI(), engine);
manager.executePTZAction('id', 'left', {});
expect(engine.executePTZAction).not.toBeCalled();
});
it('successfully', async () => {
const api = createCardAPI();
const engine = mock<CameraManagerEngine>();
const hass = createHASS();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const action = 'left';
const options = {};
manager.executePTZAction('id', action, options);
expect(engine.executePTZAction).toBeCalledWith(
hass,
expect.anything(),
action,
options,
);
});
});
describe('should determine if queries are fresh', () => {
beforeAll(() => {
const start = new Date('2024-03-02T20:35:00');
vi.useFakeTimers();
vi.setSystemTime(start);
});
afterAll(() => {
vi.useRealTimers();
});
it.each([
['not fresh', new Date('2024-03-02T20:32:00'), false],
['fresh on lower bound', new Date('2024-03-02T20:34:00'), true],
['fresh at current time', new Date('2024-03-02T20:35:00'), true],
['fresh in the future', new Date('2024-03-02T20:40:00'), true],
[
'unknown camera',
new Date('2024-03-02T20:35:00'),
// Default assumed to be fresh.
true,
[
{
...baseEventQuery,
cameraIDs: new Set(['BAD']),
},
],
],
])(
'%s',
async (
_name: string,
resultsTimestamp: Date,
expectedFresh: boolean,
queries: EventQuery[] = [baseEventQuery],
) => {
const api = createCardAPI();
const engine = mock<CameraManagerEngine>();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
engine.getQueryResultMaxAge.mockReturnValue(60);
expect(manager.areMediaQueriesResultsFresh(queries, resultsTimestamp)).toBe(
expectedFresh,
);
},
);
});
describe('should get media seek time', () => {
const startTime = new Date('2024-03-02T20:52:00');
const endTime = new Date('2024-03-02T20:53:00');
const middleTime = new Date('2024-03-02T20:52:30');
describe('invalid requests', () => {
it.each([
['null start and end', null, null, middleTime],
['no start', null, endTime, middleTime],
['no end', startTime, null, middleTime],
['target < start', endTime, endTime, startTime],
['target > end', startTime, startTime, endTime],
])(
'%s',
async (_name: string, start: Date | null, end: Date | null, target: Date) => {
const api = createCardAPI();
const engine = mock<CameraManagerEngine>();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
expect(
await manager.getMediaSeekTime(
new TestViewMedia({ startTime: start, endTime: end }),
target,
),
).toBeNull();
},
);
});
it('successfully', async () => {
const api = createCardAPI();
const engine = mock<CameraManagerEngine>();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = createCameraManager(api, engine);
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
engine.getMediaSeekTime.mockResolvedValue(42);
const media = new TestViewMedia({
cameraID: 'id',
startTime: startTime,
endTime: endTime,
});
expect(await manager.getMediaSeekTime(media, middleTime)).toBe(42);
expect(engine.getMediaSeekTime).toBeCalledWith(
expect.anything(),
expect.anything(),
media,
middleTime,
);
});
});
});