1706 lines
56 KiB
TypeScript
1706 lines
56 KiB
TypeScript
import { add } from 'date-fns';
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { mock } from 'vitest-mock-extended';
|
|
|
|
import type { Camera } from '../../src/camera-manager/camera.js';
|
|
import { Capabilities } from '../../src/camera-manager/capabilities.js';
|
|
import type { CameraManagerEngineFactory } from '../../src/camera-manager/engine-factory.js';
|
|
import type { CameraManagerEngine } from '../../src/camera-manager/engine.js';
|
|
import {
|
|
CameraDuplicateIDError,
|
|
CameraNoEngineError,
|
|
CameraNoIDError,
|
|
} from '../../src/camera-manager/error.js';
|
|
import {
|
|
CameraManager,
|
|
CameraQueryClassifier,
|
|
QueryResultClassifier,
|
|
} from '../../src/camera-manager/manager.js';
|
|
import {
|
|
Engine,
|
|
QueryResultsType,
|
|
QueryType,
|
|
type CameraEvent,
|
|
type CameraManagerCameraMetadata,
|
|
type EventQuery,
|
|
type EventQueryResults,
|
|
type MediaMetadata,
|
|
type QueryResults,
|
|
} from '../../src/camera-manager/types.js';
|
|
import type { CardController } from '../../src/card-controller/controller.js';
|
|
import type { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
|
import { sortItems } from '../../src/card-controller/view/sort.js';
|
|
import type { CameraConfig } from '../../src/config/schema/cameras.js';
|
|
import { advancedCameraCardConfigSchema } from '../../src/config/schema/types.js';
|
|
import { QuerySource } from '../../src/query-source.js';
|
|
import { PTZMovementType, type Endpoint } from '../../src/types.js';
|
|
import { ViewFolder, type ViewItem, type ViewMedia } from '../../src/view/item.js';
|
|
import type { ViewItemCapabilities } from '../../src/view/types.js';
|
|
import { createCameraConfig, createConfig } from '../config/test-utils';
|
|
import { createCardAPI, createFolder, createHASS } from '../test-utils.js';
|
|
import { generateViewMediaArray, TestViewMedia } from '../view/test-utils';
|
|
import { createCapabilities, createInitializedCamera } from './test-utils';
|
|
|
|
describe('QueryClassifier', () => {
|
|
it('should classify event query', () => {
|
|
expect(CameraQueryClassifier.isEventQuery({ type: QueryType.Event })).toBeTruthy();
|
|
expect(
|
|
CameraQueryClassifier.isEventQuery({ type: QueryType.Recording }),
|
|
).toBeFalsy();
|
|
expect(
|
|
CameraQueryClassifier.isEventQuery({ type: QueryType.RecordingSegments }),
|
|
).toBeFalsy();
|
|
expect(
|
|
CameraQueryClassifier.isEventQuery({ type: QueryType.MediaMetadata }),
|
|
).toBeFalsy();
|
|
});
|
|
it('should classify recording query', () => {
|
|
expect(
|
|
CameraQueryClassifier.isRecordingQuery({ type: QueryType.Event }),
|
|
).toBeFalsy();
|
|
expect(
|
|
CameraQueryClassifier.isRecordingQuery({ type: QueryType.Recording }),
|
|
).toBeTruthy();
|
|
expect(
|
|
CameraQueryClassifier.isRecordingQuery({ type: QueryType.RecordingSegments }),
|
|
).toBeFalsy();
|
|
expect(
|
|
CameraQueryClassifier.isRecordingQuery({ type: QueryType.MediaMetadata }),
|
|
).toBeFalsy();
|
|
});
|
|
it('should classify recording segments query', () => {
|
|
expect(
|
|
CameraQueryClassifier.isRecordingSegmentsQuery({ type: QueryType.Event }),
|
|
).toBeFalsy();
|
|
expect(
|
|
CameraQueryClassifier.isRecordingSegmentsQuery({ type: QueryType.Recording }),
|
|
).toBeFalsy();
|
|
expect(
|
|
CameraQueryClassifier.isRecordingSegmentsQuery({
|
|
type: QueryType.RecordingSegments,
|
|
}),
|
|
).toBeTruthy();
|
|
expect(
|
|
CameraQueryClassifier.isRecordingSegmentsQuery({ type: QueryType.MediaMetadata }),
|
|
).toBeFalsy();
|
|
});
|
|
it('should classify media metadata query', () => {
|
|
expect(
|
|
CameraQueryClassifier.isMediaMetadataQuery({ type: QueryType.Event }),
|
|
).toBeFalsy();
|
|
expect(
|
|
CameraQueryClassifier.isMediaMetadataQuery({ type: QueryType.Recording }),
|
|
).toBeFalsy();
|
|
expect(
|
|
CameraQueryClassifier.isMediaMetadataQuery({ type: QueryType.RecordingSegments }),
|
|
).toBeFalsy();
|
|
expect(
|
|
CameraQueryClassifier.isMediaMetadataQuery({ type: QueryType.MediaMetadata }),
|
|
).toBeTruthy();
|
|
});
|
|
it('should classify review query', () => {
|
|
expect(CameraQueryClassifier.isReviewQuery({ type: QueryType.Event })).toBeFalsy();
|
|
expect(
|
|
CameraQueryClassifier.isReviewQuery({ type: QueryType.Recording }),
|
|
).toBeFalsy();
|
|
expect(
|
|
CameraQueryClassifier.isReviewQuery({ type: QueryType.RecordingSegments }),
|
|
).toBeFalsy();
|
|
expect(
|
|
CameraQueryClassifier.isReviewQuery({ type: QueryType.MediaMetadata }),
|
|
).toBeFalsy();
|
|
expect(CameraQueryClassifier.isReviewQuery({ type: QueryType.Review })).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();
|
|
});
|
|
it('should classify review query result', async () => {
|
|
expect(
|
|
QueryResultClassifier.isReviewQueryResult(createResults(QueryResultsType.Event)),
|
|
).toBeFalsy();
|
|
expect(
|
|
QueryResultClassifier.isReviewQueryResult(
|
|
createResults(QueryResultsType.Recording),
|
|
),
|
|
).toBeFalsy();
|
|
expect(
|
|
QueryResultClassifier.isReviewQueryResult(
|
|
createResults(QueryResultsType.RecordingSegments),
|
|
),
|
|
).toBeFalsy();
|
|
expect(
|
|
QueryResultClassifier.isReviewQueryResult(
|
|
createResults(QueryResultsType.MediaMetadata),
|
|
),
|
|
).toBeFalsy();
|
|
expect(
|
|
QueryResultClassifier.isReviewQueryResult(createResults(QueryResultsType.Review)),
|
|
).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('CameraManager', () => {
|
|
const baseCameraConfig = {
|
|
id: 'id',
|
|
camera_entity: 'camera.foo',
|
|
engine: 'generic',
|
|
};
|
|
|
|
const baseEventQuery: EventQuery = {
|
|
source: QuerySource.Camera,
|
|
type: QueryType.Event as const,
|
|
cameraIDs: new Set(['id']),
|
|
};
|
|
|
|
const baseEventQueryResults: EventQueryResults = {
|
|
type: QueryResultsType.Event as const,
|
|
engine: Engine.Generic,
|
|
};
|
|
|
|
const baseRecordingQuery = {
|
|
source: QuerySource.Camera as const,
|
|
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;
|
|
stateWatcher?: StateWatcherSubscriptionInterface;
|
|
}[] = [{}],
|
|
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 (cameraConfig: CameraConfig): Promise<Camera> =>
|
|
await createInitializedCamera(
|
|
cameraConfig,
|
|
mockEngine,
|
|
camera.capabilties ?? createCapabilities(),
|
|
camera.stateWatcher,
|
|
),
|
|
);
|
|
}
|
|
vi.mocked(mockFactory.getEngineForCamera).mockResolvedValueOnce(engineType);
|
|
}
|
|
|
|
return new CameraManager(api, { factory: mockFactory });
|
|
};
|
|
|
|
it('should construct', () => {
|
|
const manager = new CameraManager(createCardAPI());
|
|
expect(manager.getStore()).toBeTruthy();
|
|
});
|
|
|
|
describe('should initialize cameras from config', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should succeed', 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('should handle missing hass', async () => {
|
|
const manager = createCameraManager(createCardAPI());
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
expect(manager.getStore().getCameraCount()).toBe(0);
|
|
});
|
|
|
|
it('should handle missing 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('should handle missing cameras in config', async () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
advancedCameraCardConfigSchema.parse({
|
|
type: 'advanced-camera-card' as const,
|
|
}),
|
|
);
|
|
|
|
const manager = new CameraManager(api);
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
expect(manager.getStore().getCameraCount()).toBe(0);
|
|
});
|
|
|
|
it('should reject missing id', async () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const manager = createCameraManager(api, mock<CameraManagerEngine>(), [
|
|
{
|
|
config: createCameraConfig({
|
|
// No id.
|
|
engine: 'generic',
|
|
}),
|
|
},
|
|
]);
|
|
await expect(manager.initializeCamerasFromConfig()).rejects.toThrow(
|
|
CameraNoIDError,
|
|
);
|
|
});
|
|
|
|
it('should reject 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,
|
|
},
|
|
]);
|
|
await expect(manager.initializeCamerasFromConfig()).rejects.toThrow(
|
|
CameraDuplicateIDError,
|
|
);
|
|
});
|
|
|
|
it('should await camera destruction before throwing on duplicate id', async () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const cameraConfig = createCameraConfig({
|
|
id: 'DUPLICATE',
|
|
engine: 'generic',
|
|
});
|
|
|
|
// Camera.destroy() awaits its _destroyCallbacks, one of which is the
|
|
// trigger-path unsubscribe. By returning a deferred Promise from each
|
|
// camera's stateWatcher.unsubscribe, we make destroy completion
|
|
// externally observable without spying on any Camera method.
|
|
const order: string[] = [];
|
|
const buildStateWatcher = (): StateWatcherSubscriptionInterface => {
|
|
const watcher = mock<StateWatcherSubscriptionInterface>();
|
|
vi.mocked(watcher.unsubscribe).mockImplementation(
|
|
() =>
|
|
new Promise<void>((resolve) =>
|
|
setTimeout(() => {
|
|
order.push('destroy-done');
|
|
resolve();
|
|
}, 0),
|
|
),
|
|
);
|
|
return watcher;
|
|
};
|
|
|
|
const cameraEntry = {
|
|
config: cameraConfig,
|
|
capabilties: createCapabilities({ trigger: true }),
|
|
};
|
|
const manager = createCameraManager(api, mock<CameraManagerEngine>(), [
|
|
{ ...cameraEntry, stateWatcher: buildStateWatcher() },
|
|
{ ...cameraEntry, stateWatcher: buildStateWatcher() },
|
|
]);
|
|
|
|
await manager.initializeCamerasFromConfig().catch(() => order.push('throw'));
|
|
|
|
expect(order).toEqual(['destroy-done', 'destroy-done', 'throw']);
|
|
});
|
|
|
|
it('should reject missing engine', async () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const manager = createCameraManager(api, mock<CameraManagerEngine>(), [
|
|
{
|
|
config: createCameraConfig({
|
|
id: 'id',
|
|
}),
|
|
engineType: null,
|
|
},
|
|
]);
|
|
await expect(manager.initializeCamerasFromConfig()).rejects.toThrow(
|
|
CameraNoEngineError,
|
|
);
|
|
});
|
|
|
|
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,
|
|
);
|
|
await manager.initializeCamerasFromConfig();
|
|
const eventCallback = factory.createEngine.mock.calls[0][1].eventCallback;
|
|
|
|
const cameraEvent: CameraEvent = {
|
|
cameraID: 'camera',
|
|
id: 'event-1',
|
|
type: 'new',
|
|
};
|
|
eventCallback?.(cameraEvent);
|
|
expect(api.getCameraTriggersManager().handleCameraEvent).toHaveBeenCalledWith(
|
|
cameraEvent,
|
|
);
|
|
});
|
|
|
|
describe('should fetch entity list when required', () => {
|
|
it.each([['occupancy' as const], ['motion' as const], ['doorbell' as const]])(
|
|
'should fetch with %s trigger',
|
|
async (triggerKey) => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const manager = createCameraManager(api, mock<CameraManagerEngine>(), [
|
|
{
|
|
config: createCameraConfig({
|
|
...baseCameraConfig,
|
|
triggers: {
|
|
[triggerKey]: true,
|
|
},
|
|
}),
|
|
},
|
|
]);
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
expect(api.getEntityRegistryManager().fetchEntityList).toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it('should skip without entity based trigger', async () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const manager = createCameraManager(api);
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
expect(api.getEntityRegistryManager().fetchEntityList).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('generate default queries', () => {
|
|
it.each([
|
|
[
|
|
QueryType.Event as const,
|
|
'generateDefaultEventQuery',
|
|
'generateDefaultEventQueries',
|
|
],
|
|
[
|
|
QueryType.Recording as const,
|
|
'generateDefaultRecordingQuery',
|
|
'generateDefaultRecordingQueries',
|
|
],
|
|
[
|
|
QueryType.RecordingSegments as const,
|
|
'generateDefaultRecordingSegmentsQuery',
|
|
'generateDefaultRecordingSegmentsQueries',
|
|
],
|
|
[
|
|
QueryType.Review as const,
|
|
'generateDefaultReviewQuery',
|
|
'generateDefaultReviewQueries',
|
|
],
|
|
])(
|
|
'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);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
const queries = [{ type: queryType, cameraIDs: new Set(['id']) }];
|
|
engine[engineMethodName].mockReturnValue(queries);
|
|
expect(manager[managerMethodName]('id')).toEqual(queries);
|
|
},
|
|
);
|
|
|
|
it('should handle missing 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('should handle missing queries', async () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(api, engine);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
engine.generateDefaultEventQuery.mockReturnValue(null);
|
|
expect(manager.generateDefaultEventQueries('id')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('getDefaultQueryParameters', () => {
|
|
it('should return empty object for non-existent camera', async () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const manager = createCameraManager(api, mock<CameraManagerEngine>());
|
|
|
|
expect(
|
|
manager.getDefaultQueryParameters('not_a_camera', QueryType.Event),
|
|
).toEqual({});
|
|
});
|
|
|
|
it('should return parameters from engine for existing camera', async () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(api, engine);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
engine.getDefaultQueryParameters.mockReturnValue({ what: new Set(['person']) });
|
|
expect(manager.getDefaultQueryParameters('id', QueryType.Event)).toEqual({
|
|
what: new Set(['person']),
|
|
});
|
|
});
|
|
});
|
|
|
|
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: {
|
|
media_events: ['snapshots'],
|
|
},
|
|
}),
|
|
},
|
|
]);
|
|
await manager.initializeCamerasFromConfig();
|
|
expect(
|
|
manager.getStore().getCamera('id')?.getConfig().triggers.media_events,
|
|
).toEqual(['snapshots']);
|
|
});
|
|
});
|
|
|
|
describe('should get media metadata', () => {
|
|
const query = {
|
|
type: QueryType.MediaMetadata as const,
|
|
cameraIDs: new Set('id'),
|
|
};
|
|
|
|
it('should handle empty metadata', async () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(api, engine);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
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);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
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('should handle missing hass', async () => {
|
|
const manager = createCameraManager(createCardAPI());
|
|
expect(await manager.getEvents(baseEventQuery)).toEqual(new Map());
|
|
});
|
|
|
|
it('should handle missing 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('should succeed', async () => {
|
|
const api = createCardAPI();
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(api, engine);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
const engineOptions = {};
|
|
const results = new Map([[baseEventQuery, baseEventQueryResults]]);
|
|
engine.getEvents.mockResolvedValue(results);
|
|
expect(await manager.getEvents(baseEventQuery, engineOptions)).toEqual(results);
|
|
expect(engine.getEvents).toHaveBeenCalledWith(
|
|
hass,
|
|
expect.anything(),
|
|
baseEventQuery,
|
|
engineOptions,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('should review media', () => {
|
|
it('should handle missing camera', async () => {
|
|
const api = createCardAPI();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
const manager = createCameraManager(api);
|
|
const media = new TestViewMedia();
|
|
|
|
await manager.reviewMedia(media, true);
|
|
});
|
|
|
|
it('should succeed', 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);
|
|
await manager.initializeCamerasFromConfig();
|
|
const media = new TestViewMedia({ cameraID: 'id' });
|
|
|
|
await manager.reviewMedia(media, true);
|
|
|
|
expect(engine.reviewMedia).toHaveBeenCalledWith(
|
|
hass,
|
|
expect.anything(),
|
|
media,
|
|
true,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('should get recordings', () => {
|
|
it('should succeed', async () => {
|
|
const api = createCardAPI();
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(api, engine);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
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('should succeed', async () => {
|
|
const api = createCardAPI();
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(api, engine);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
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('should handle 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);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
const results = new Map([[baseEventQuery, baseEventQueryResults]]);
|
|
engine.getEvents.mockResolvedValue(results);
|
|
const media = sortItems(generateViewMediaArray({ count: 5 }));
|
|
engine.generateMediaFromEvents.mockReturnValue(media);
|
|
|
|
expect(await manager.executeMediaQueries([baseEventQuery])).toEqual(media);
|
|
});
|
|
|
|
it('should handle 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);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
const results = new Map([[baseEventQuery, baseEventQueryResults]]);
|
|
engine.getEvents.mockResolvedValue(results);
|
|
engine.generateMediaFromEvents.mockReturnValue(null);
|
|
|
|
expect(await manager.executeMediaQueries([baseEventQuery])).toEqual([]);
|
|
});
|
|
|
|
it('should handle missing 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);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
const results = new Map([
|
|
[baseEventQuery, { ...baseEventQueryResults, engine: Engine.MotionEye }],
|
|
]);
|
|
engine.getEvents.mockResolvedValue(results);
|
|
|
|
expect(await manager.executeMediaQueries([baseEventQuery])).toEqual([]);
|
|
});
|
|
|
|
it('should handle 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);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
const results = new Map([[baseRecordingQuery, baseRecordingQueryResults]]);
|
|
engine.getRecordings.mockResolvedValue(results);
|
|
const media = sortItems(generateViewMediaArray({ count: 5 }));
|
|
engine.generateMediaFromRecordings.mockReturnValue(media);
|
|
|
|
expect(await manager.executeMediaQueries([baseRecordingQuery])).toEqual(media);
|
|
});
|
|
|
|
it('should handle missing 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 merge compatible queries', () => {
|
|
it('should merge queries with identical properties', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const manager = createCameraManager(api, engine, [
|
|
{ config: createCameraConfig({ ...baseCameraConfig, id: 'cam1' }) },
|
|
{ config: createCameraConfig({ ...baseCameraConfig, id: 'cam2' }) },
|
|
]);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
const query1: EventQuery = {
|
|
source: QuerySource.Camera,
|
|
type: QueryType.Event,
|
|
cameraIDs: new Set(['cam1']),
|
|
reviewed: false,
|
|
};
|
|
const query2: EventQuery = {
|
|
source: QuerySource.Camera,
|
|
type: QueryType.Event,
|
|
cameraIDs: new Set(['cam2']),
|
|
reviewed: false,
|
|
};
|
|
|
|
engine.getEvents.mockResolvedValue(new Map());
|
|
engine.generateMediaFromEvents.mockReturnValue([]);
|
|
|
|
await manager.executeMediaQueries([query1, query2]);
|
|
|
|
// Should be called once with merged cameraIDs
|
|
expect(engine.getEvents).toHaveBeenCalledTimes(1);
|
|
expect(engine.getEvents).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
cameraIDs: new Set(['cam1', 'cam2']),
|
|
reviewed: false,
|
|
}),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it('should not merge queries with different properties', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const manager = createCameraManager(api, engine, [
|
|
{ config: createCameraConfig({ ...baseCameraConfig, id: 'cam1' }) },
|
|
{ config: createCameraConfig({ ...baseCameraConfig, id: 'cam2' }) },
|
|
]);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
const query1: EventQuery = {
|
|
source: QuerySource.Camera,
|
|
type: QueryType.Event,
|
|
cameraIDs: new Set(['cam1']),
|
|
reviewed: false,
|
|
};
|
|
const query2: EventQuery = {
|
|
source: QuerySource.Camera,
|
|
type: QueryType.Event,
|
|
cameraIDs: new Set(['cam2']),
|
|
reviewed: true,
|
|
};
|
|
|
|
engine.getEvents.mockResolvedValue(new Map());
|
|
engine.generateMediaFromEvents.mockReturnValue([]);
|
|
|
|
await manager.executeMediaQueries([query1, query2]);
|
|
|
|
// Should be called twice for different queries
|
|
expect(engine.getEvents).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should handle single query without merging', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
vi.mocked(engine.getEngineType).mockReturnValue(Engine.Generic);
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
const manager = createCameraManager(api, engine);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
engine.getEvents.mockResolvedValue(
|
|
new Map([[baseEventQuery, baseEventQueryResults]]),
|
|
);
|
|
engine.generateMediaFromEvents.mockReturnValue([]);
|
|
|
|
await manager.executeMediaQueries([baseEventQuery]);
|
|
|
|
expect(engine.getEvents).toHaveBeenCalledTimes(1);
|
|
expect(engine.getEvents).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.anything(),
|
|
baseEventQuery,
|
|
undefined,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('should extend media queries', () => {
|
|
const dateBase = new Date('2024-03-01T20:01:00');
|
|
const mediaTwoCameras = generateViewMediaArray({ count: 5 });
|
|
const mediaMixedStart: ViewItem[] = [
|
|
new TestViewMedia({
|
|
startTime: dateBase,
|
|
}),
|
|
new TestViewMedia({
|
|
startTime: add(dateBase, { days: 1 }),
|
|
}),
|
|
new TestViewMedia({
|
|
startTime: add(dateBase, { days: 2 }),
|
|
}),
|
|
new ViewFolder(createFolder(), []),
|
|
];
|
|
|
|
it('should handle missing 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: sortItems(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: sortItems(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: sortItems(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: sortItems(mediaMixedStart.concat(mediaTwoCameras)),
|
|
},
|
|
'earlier' as const,
|
|
],
|
|
])(
|
|
'handles %s',
|
|
async (
|
|
_name: string,
|
|
// The previously submitted query & results.
|
|
inputQueries: Map<EventQuery, EventQueryResults>,
|
|
|
|
// The previously received media.
|
|
inputResults: ViewItem[],
|
|
|
|
// 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: ViewItem[];
|
|
} | 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);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
engine.getEvents.mockResolvedValue(inputQueries);
|
|
engine.generateMediaFromEvents.mockReturnValue(outputMediaResults);
|
|
|
|
expect(
|
|
await manager.extendMediaQueries(
|
|
[...inputQueries.keys()],
|
|
inputResults,
|
|
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('should handle missing camera', async () => {
|
|
const manager = createCameraManager(createCardAPI());
|
|
expect(await manager.getMediaDownloadPath(new TestViewMedia())).toBeNull();
|
|
});
|
|
|
|
it('should handle missing hass', async () => {
|
|
const api = createCardAPI();
|
|
const manager = createCameraManager(api);
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
|
expect(await manager.getMediaDownloadPath(new TestViewMedia())).toBeNull();
|
|
});
|
|
|
|
it('should succeed', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(api, engine);
|
|
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
const result: Endpoint = {
|
|
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('should handle missing camera', async () => {
|
|
const manager = createCameraManager(createCardAPI());
|
|
expect(manager.getMediaCapabilities(new TestViewMedia())).toBeNull();
|
|
});
|
|
|
|
it('should succeed', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(api, engine);
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
const result: ViewItemCapabilities = {
|
|
canFavorite: false,
|
|
canDownload: false,
|
|
};
|
|
vi.mocked(engine.getMediaCapabilities).mockReturnValue(result);
|
|
expect(manager.getMediaCapabilities(new TestViewMedia({ cameraID: 'id' }))).toBe(
|
|
result,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('should favorite media', () => {
|
|
it('should handle missing camera', async () => {
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(createCardAPI(), engine);
|
|
manager.favoriteMedia(new TestViewMedia(), true);
|
|
|
|
expect(engine.favoriteMedia).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should succeed', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(api, engine);
|
|
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
const media = new TestViewMedia({ cameraID: 'id' });
|
|
manager.favoriteMedia(media, true);
|
|
expect(engine.favoriteMedia).toHaveBeenCalledWith(
|
|
hass,
|
|
expect.anything(),
|
|
media,
|
|
true,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('should get camera endpoints', () => {
|
|
it('should handle missing camera', () => {
|
|
const manager = createCameraManager(createCardAPI());
|
|
expect(manager.getCameraEndpoints('BAD')).toBeNull();
|
|
});
|
|
|
|
it('should succeed', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(api, engine);
|
|
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
expect(manager.getCameraEndpoints('id', { view: 'live' })).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('should get camera metadata', () => {
|
|
it('should handle missing camera', () => {
|
|
const manager = createCameraManager(createCardAPI());
|
|
expect(manager.getCameraMetadata('BAD')).toBeNull();
|
|
});
|
|
|
|
it('should succeed', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(api, engine);
|
|
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
const result: CameraManagerCameraMetadata = {
|
|
title: 'My Camera',
|
|
icon: {
|
|
icon: 'mdi:camera',
|
|
},
|
|
};
|
|
vi.mocked(engine.getCameraMetadata).mockReturnValue(result);
|
|
|
|
expect(manager.getCameraMetadata('id')).toBe(result);
|
|
});
|
|
});
|
|
|
|
describe('should get camera capabilities', () => {
|
|
it('should handle missing camera', () => {
|
|
const manager = createCameraManager(createCardAPI());
|
|
expect(manager.getCameraCapabilities('BAD')).toBeNull();
|
|
});
|
|
|
|
it('should succeed', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(api, engine);
|
|
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
expect(manager.getCameraCapabilities('id')).toEqual(createCapabilities());
|
|
});
|
|
});
|
|
|
|
describe('should get aggregate camera capabilities', () => {
|
|
it('should handle missing 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('should succeed', 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: {
|
|
left: [PTZMovementType.Continuous],
|
|
},
|
|
}),
|
|
},
|
|
]);
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
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('should handle missing camera', () => {
|
|
const engine = mock<CameraManagerEngine>();
|
|
const manager = createCameraManager(createCardAPI(), engine);
|
|
|
|
manager.executePTZAction('id', 'left', {});
|
|
|
|
// No visible action.
|
|
});
|
|
|
|
it('should succeed with null hass', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
const action = {
|
|
action: 'perform-action' as const,
|
|
perform_action: 'action',
|
|
};
|
|
const manager = createCameraManager(api, engine, [
|
|
{
|
|
config: createCameraConfig({
|
|
baseCameraConfig,
|
|
id: 'another',
|
|
ptz: {
|
|
actions_left: action,
|
|
},
|
|
}),
|
|
},
|
|
]);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
|
manager.executePTZAction('another', 'left');
|
|
|
|
expect(api.getActionsManager().executeActions).toHaveBeenCalledWith({
|
|
actions: action,
|
|
});
|
|
});
|
|
|
|
it('should succeed', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
const action = {
|
|
action: 'perform-action' as const,
|
|
perform_action: 'action',
|
|
};
|
|
const manager = createCameraManager(api, engine, [
|
|
{
|
|
config: createCameraConfig({
|
|
baseCameraConfig,
|
|
id: 'another',
|
|
ptz: {
|
|
actions_left: action,
|
|
},
|
|
}),
|
|
},
|
|
]);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
manager.executePTZAction('another', 'left');
|
|
|
|
expect(api.getActionsManager().executeActions).toHaveBeenCalledWith({
|
|
actions: action,
|
|
});
|
|
});
|
|
|
|
describe('with rotation', () => {
|
|
it.each([
|
|
// No rotation
|
|
[undefined, 'left', 'left', undefined],
|
|
[undefined, 'right', 'right', undefined],
|
|
[undefined, 'up', 'up', undefined],
|
|
[undefined, 'down', 'down', undefined],
|
|
[0, 'left', 'left', undefined],
|
|
[0, 'right', 'right', undefined],
|
|
[0, 'up', 'up', undefined],
|
|
[0, 'down', 'down', undefined],
|
|
|
|
// 90° rotation (clockwise view rotation means controls rotate counter-clockwise)
|
|
[90, 'left', 'down', undefined],
|
|
[90, 'right', 'up', undefined],
|
|
[90, 'up', 'left', undefined],
|
|
[90, 'down', 'right', undefined],
|
|
|
|
// 180° rotation
|
|
[180, 'left', 'right', undefined],
|
|
[180, 'right', 'left', undefined],
|
|
[180, 'up', 'down', undefined],
|
|
[180, 'down', 'up', undefined],
|
|
|
|
// 270° rotation
|
|
[270, 'left', 'up', undefined],
|
|
[270, 'right', 'down', undefined],
|
|
[270, 'up', 'right', undefined],
|
|
[270, 'down', 'left', undefined],
|
|
|
|
// Non-directional actions should pass through unchanged
|
|
[90, 'zoom_in', 'zoom_in', undefined],
|
|
[90, 'zoom_out', 'zoom_out', undefined],
|
|
[90, 'preset', 'preset', 'test-preset'],
|
|
[180, 'zoom_in', 'zoom_in', undefined],
|
|
[180, 'zoom_out', 'zoom_out', undefined],
|
|
[180, 'preset', 'preset', 'test-preset'],
|
|
[270, 'zoom_in', 'zoom_in', undefined],
|
|
[270, 'zoom_out', 'zoom_out', undefined],
|
|
[270, 'preset', 'preset', 'test-preset'],
|
|
] as const)(
|
|
'rotates %s° %s to %s',
|
|
async (rotation, inputAction, expectedAction, preset) => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
const hass = createHASS();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
const leftAction = {
|
|
action: 'perform-action' as const,
|
|
perform_action: 'left-action',
|
|
};
|
|
const rightAction = {
|
|
action: 'perform-action' as const,
|
|
perform_action: 'right-action',
|
|
};
|
|
const upAction = {
|
|
action: 'perform-action' as const,
|
|
perform_action: 'up-action',
|
|
};
|
|
const downAction = {
|
|
action: 'perform-action' as const,
|
|
perform_action: 'down-action',
|
|
};
|
|
const zoomInAction = {
|
|
action: 'perform-action' as const,
|
|
perform_action: 'zoom-in-action',
|
|
};
|
|
const zoomOutAction = {
|
|
action: 'perform-action' as const,
|
|
perform_action: 'zoom-out-action',
|
|
};
|
|
const presetAction = {
|
|
action: 'perform-action' as const,
|
|
perform_action: 'preset-action',
|
|
};
|
|
|
|
const manager = createCameraManager(api, engine, [
|
|
{
|
|
config: createCameraConfig({
|
|
baseCameraConfig,
|
|
id: 'rotated-camera',
|
|
dimensions: rotation !== undefined ? { rotation } : undefined,
|
|
ptz: {
|
|
actions_left: leftAction,
|
|
actions_right: rightAction,
|
|
actions_up: upAction,
|
|
actions_down: downAction,
|
|
actions_zoom_in: zoomInAction,
|
|
actions_zoom_out: zoomOutAction,
|
|
presets: {
|
|
'test-preset': presetAction,
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
]);
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
manager.executePTZAction(
|
|
'rotated-camera',
|
|
inputAction,
|
|
preset ? { preset } : undefined,
|
|
);
|
|
|
|
// Map expected action to the corresponding action object
|
|
const expectedActionMap = {
|
|
left: leftAction,
|
|
right: rightAction,
|
|
up: upAction,
|
|
down: downAction,
|
|
zoom_in: zoomInAction,
|
|
zoom_out: zoomOutAction,
|
|
preset: presetAction,
|
|
};
|
|
|
|
expect(api.getActionsManager().executeActions).toHaveBeenCalledWith({
|
|
actions: expectedActionMap[expectedAction],
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
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);
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
engine.getQueryResultMaxAge.mockReturnValue(60);
|
|
expect(manager.areMediaQueriesResultsFresh(resultsTimestamp, queries)).toBe(
|
|
expectedFresh,
|
|
);
|
|
},
|
|
);
|
|
|
|
it('should always return false for null queries', () => {
|
|
const manager = createCameraManager(createCardAPI(), mock<CameraManagerEngine>());
|
|
expect(manager.areMediaQueriesResultsFresh(new Date(), null)).toBe(false);
|
|
});
|
|
});
|
|
|
|
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);
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
expect(
|
|
await manager.getMediaSeekTime(
|
|
new TestViewMedia({ startTime: start, endTime: end }),
|
|
target,
|
|
),
|
|
).toBeNull();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should succeed', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
const manager = createCameraManager(api, engine);
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
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).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.anything(),
|
|
media,
|
|
middleTime,
|
|
);
|
|
});
|
|
|
|
it('should handle null return value', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
const manager = createCameraManager(api, engine);
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
engine.getMediaSeekTime.mockResolvedValue(null);
|
|
|
|
const media = new TestViewMedia({
|
|
cameraID: 'id',
|
|
startTime: startTime,
|
|
endTime: endTime,
|
|
});
|
|
expect(await manager.getMediaSeekTime(media, middleTime)).toBeNull();
|
|
});
|
|
});
|
|
|
|
it('should destroy', async () => {
|
|
const api = createCardAPI();
|
|
const engine = mock<CameraManagerEngine>();
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
const manager = createCameraManager(api, engine);
|
|
|
|
await manager.initializeCamerasFromConfig();
|
|
|
|
expect(manager.getStore().getCameraCount()).toBe(1);
|
|
|
|
await manager.destroy();
|
|
|
|
expect(manager.getStore().getCameraCount()).toBe(0);
|
|
});
|
|
});
|