feat: Implement basic general folder support (#2051)

- Related: #1748
This commit is contained in:
Dermot Duffy
2025-05-21 19:59:21 -07:00
committed by GitHub
parent 2eb0d9e35e
commit c6a4c8aea2
350 changed files with 12837 additions and 4509 deletions
@@ -3,10 +3,7 @@ import { mock } from 'vitest-mock-extended';
import { BrowseMediaCamera } from '../../../src/camera-manager/browse-media/camera';
import { CameraManagerEngine } from '../../../src/camera-manager/engine';
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
import {
Entity,
EntityRegistryManager,
} from '../../../src/utils/ha/registry/entity/types';
import { Entity, EntityRegistryManager } from '../../../src/ha/registry/entity/types';
import { createCameraConfig, createHASS } from '../../test-utils';
describe('BrowseMediaCamera', () => {
@@ -1,55 +0,0 @@
import { describe, expect, it } from 'vitest';
import { BrowseMediaMetadata } from '../../../../src/camera-manager/browse-media/types';
import { getViewMediaFromBrowseMediaArray } from '../../../../src/camera-manager/browse-media/utils/browse-media-to-view-media';
import {
MEDIA_CLASS_IMAGE,
MEDIA_CLASS_VIDEO,
RichBrowseMedia,
} from '../../../../src/utils/ha/browse-media/types';
import { createBrowseMedia } from './test-utils';
const createBrowseMediaChildren = (
overrides: Partial<RichBrowseMedia<BrowseMediaMetadata>>[],
) => {
return overrides.map((override) => createBrowseMedia(override));
};
describe('getViewMediaFromBrowseMediaArray', () => {
it('should ignore absent metadata', () => {
const children = createBrowseMediaChildren([{ _metadata: undefined }]);
expect(getViewMediaFromBrowseMediaArray(children)).toEqual([]);
});
it('should ignore unknown media class', () => {
const children = createBrowseMediaChildren([{ media_class: 'UNKNOWN' }]);
expect(getViewMediaFromBrowseMediaArray(children)).toEqual([]);
});
it('should generate clip view media', () => {
const children = createBrowseMediaChildren([{ media_class: MEDIA_CLASS_VIDEO }]);
const viewMedia = getViewMediaFromBrowseMediaArray(children);
expect(viewMedia).toHaveLength(1);
expect(viewMedia?.[0].getMediaType()).toBe('clip');
expect(viewMedia?.[0].getID()).toBe('camera.test/2024-11-19 07:23:00');
});
it('should generate snapshot view media', () => {
const children = createBrowseMediaChildren([{ media_class: MEDIA_CLASS_IMAGE }]);
const viewMedia = getViewMediaFromBrowseMediaArray(children);
expect(viewMedia).toHaveLength(1);
expect(viewMedia?.[0].getMediaType()).toBe('snapshot');
expect(viewMedia?.[0].getID()).toBe('camera.test/2024-11-19 07:23:00');
});
it('should de-duplicate snapshot/clip duplicates', () => {
const children = createBrowseMediaChildren([
{ media_class: MEDIA_CLASS_IMAGE },
{ media_class: MEDIA_CLASS_IMAGE },
{ media_class: MEDIA_CLASS_VIDEO },
]);
const viewMedia = getViewMediaFromBrowseMediaArray(children);
expect(viewMedia).toHaveLength(1);
expect(viewMedia?.[0].getMediaType()).toBe('clip');
expect(viewMedia?.[0].getID()).toBe('camera.test/2024-11-19 07:23:00');
});
});
@@ -1,23 +0,0 @@
import { BrowseMediaMetadata } from '../../../../src/camera-manager/browse-media/types';
import { RichBrowseMedia } from '../../../../src/utils/ha/browse-media/types';
export const createBrowseMedia = (
media?: Partial<RichBrowseMedia<BrowseMediaMetadata>>,
): RichBrowseMedia<BrowseMediaMetadata> => {
return {
title: 'Test Media',
media_class: 'video',
media_content_type: 'video/mp4',
media_content_id: 'test',
can_play: true,
can_expand: false,
thumbnail: null,
children: null,
_metadata: {
cameraID: 'camera.test',
startDate: new Date('2024-11-19T07:23:00'),
endDate: new Date('2025-11-19T07:24:00'),
},
...media,
};
};
@@ -1,40 +0,0 @@
import { sub } from 'date-fns';
import { describe, expect, it } from 'vitest';
import { isMediaWithinDates } from '../../../../src/camera-manager/browse-media/utils/within-dates';
import { createBrowseMedia } from './test-utils';
describe('isMediaWithinDates', () => {
const rangeStart = new Date('2024-11-19T07:00:00');
const rangeEnd = new Date('2024-11-19T08:00:00');
it('should never match media without metadata', () => {
const media = createBrowseMedia({ _metadata: undefined });
expect(isMediaWithinDates(media, rangeStart, rangeEnd)).toBe(false);
});
it('should always match media without start or end date', () => {
expect(isMediaWithinDates(createBrowseMedia(), undefined, undefined)).toBe(true);
});
it('should match without a start date in the range', () => {
expect(isMediaWithinDates(createBrowseMedia(), undefined, rangeEnd)).toBe(true);
});
it('should match without an end date in the range', () => {
expect(isMediaWithinDates(createBrowseMedia(), rangeStart, undefined)).toBe(true);
});
it('should match when ranges overlap', () => {
expect(isMediaWithinDates(createBrowseMedia(), rangeStart, rangeEnd)).toBe(true);
});
it('should not match when ranges do not overlap', () => {
expect(
isMediaWithinDates(
createBrowseMedia(),
sub(rangeStart, { days: 1 }),
sub(rangeEnd, { days: 1 }),
),
).toBe(false);
});
});
+21 -9
View File
@@ -1,17 +1,29 @@
import { add, sub } from 'date-fns';
import sortBy from 'lodash-es/sortBy';
import { sortBy } from 'lodash-es';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
MemoryRequestCache,
RecordingSegmentsCache,
} from '../../src/camera-manager/cache.js';
import { RecordingSegmentsCache } from '../../src/camera-manager/cache.js';
import { DateRange } from '../../src/camera-manager/range.js';
import { RecordingSegment } from '../../src/camera-manager/types.js';
import {
CameraManagerRequestCache,
CameraQuery,
Engine,
EventQuery,
QueryResultsType,
QueryReturnType,
QueryType,
RecordingSegment,
} from '../../src/camera-manager/types.js';
describe('MemoryRequestCache', () => {
const cache = new MemoryRequestCache();
const request = { request: 'foo' };
const response = { response: 'bar' };
const cache = new CameraManagerRequestCache();
const request: CameraQuery = {
type: QueryType.Event,
cameraIDs: new Set(['camera-1']),
};
const response: QueryReturnType<EventQuery> = {
type: QueryResultsType.Event,
engine: Engine.Generic,
};
beforeEach(() => {
cache.clear();
+3 -3
View File
@@ -8,15 +8,15 @@ import { ReolinkCameraManagerEngine } from '../../src/camera-manager/reolink/eng
import { Engine } from '../../src/camera-manager/types.js';
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
import { CardWideConfig } from '../../src/config/schema/types.js';
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity/types.js';
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
import { EntityRegistryManager } from '../../src/ha/registry/entity/types.js';
import { ResolvedMediaCache } from '../../src/ha/resolved-media.js';
import { EntityRegistryManagerMock } from '../ha/registry/entity/mock.js';
import {
createCameraConfig,
createHASS,
createRegistryEntity,
createStateEntity,
} from '../test-utils';
import { EntityRegistryManagerMock } from '../utils/ha/registry/entity/mock.js';
vi.mock('../../src/utils/ha/entity-registry');
vi.mock('../../src/utils/ha/entity-registry/cache');
+29 -32
View File
@@ -9,12 +9,9 @@ import { ActionsExecutor } from '../../../src/card-controller/actions/types';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { PTZAction } from '../../../src/config/schema/actions/custom/ptz';
import { CameraTriggerEventType } from '../../../src/config/schema/cameras';
import {
Entity,
EntityRegistryManager,
} from '../../../src/utils/ha/registry/entity/types';
import { Entity, EntityRegistryManager } from '../../../src/ha/registry/entity/types';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
vi.mock('../../../src/camera-manager/frigate/requests');
@@ -91,8 +88,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
}),
expect(camera.getConfig().frigate.camera_name).toBe('fnt_dr');
});
expect(camera.getConfig().frigate.camera_name).toBe('fnt_dr');
});
it('with a camera_entity without camera_name match', async () => {
@@ -115,8 +112,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
}),
expect(camera.getConfig().frigate.camera_name).toBeUndefined();
});
expect(camera.getConfig().frigate.camera_name).toBeUndefined();
});
it('with a camera_entity without platform match', async () => {
@@ -139,8 +136,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
}),
expect(camera.getConfig().frigate.camera_name).toBeUndefined();
});
expect(camera.getConfig().frigate.camera_name).toBeUndefined();
});
});
});
@@ -161,8 +158,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
}),
expect(camera.getCapabilities()?.has('favorite-events')).toBeTruthy();
});
expect(camera.getCapabilities()?.has('favorite-events')).toBeTruthy();
expect(camera.getCapabilities()?.has('favorite-recordings')).toBeFalsy();
expect(camera.getCapabilities()?.has('seek')).toBeTruthy();
expect(camera.getCapabilities()?.has('clips')).toBeTruthy();
@@ -188,8 +185,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
}),
expect(camera.getCapabilities()?.has('favorite-events')).toBeFalsy();
});
expect(camera.getCapabilities()?.has('favorite-events')).toBeFalsy();
expect(camera.getCapabilities()?.has('favorite-recordings')).toBeFalsy();
expect(camera.getCapabilities()?.has('seek')).toBeFalsy();
expect(camera.getCapabilities()?.has('clips')).toBeFalsy();
@@ -248,8 +245,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
}),
expect(camera.getCapabilities()?.has('ptz')).toBeTruthy();
});
expect(camera.getCapabilities()?.has('ptz')).toBeTruthy();
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
left: ['continuous'],
right: ['continuous'],
@@ -284,8 +281,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
}),
expect(camera.getCapabilities()?.has('ptz')).toBeTruthy();
});
expect(camera.getCapabilities()?.has('ptz')).toBeTruthy();
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
left: [],
right: [],
@@ -319,13 +316,13 @@ describe('FrigateCamera', () => {
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
frigateEventWatcher: eventWatcher,
}),
expect(eventWatcher.subscribe).toBeCalledWith(
hass,
expect.objectContaining({
instanceID: 'CLIENT_ID',
}),
);
});
expect(eventWatcher.subscribe).toBeCalledWith(
hass,
expect.objectContaining({
instanceID: 'CLIENT_ID',
}),
);
});
it('should not subscribe with no trigger events', async () => {
@@ -349,8 +346,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
frigateEventWatcher: eventWatcher,
}),
expect(eventWatcher.subscribe).not.toBeCalled();
});
expect(eventWatcher.subscribe).not.toBeCalled();
});
it('should not subscribe without trigger capability', async () => {
@@ -395,8 +392,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
frigateEventWatcher: eventWatcher,
}),
expect(eventWatcher.subscribe).not.toBeCalled();
});
expect(eventWatcher.subscribe).not.toBeCalled();
});
it('should unsubscribe on destroy', async () => {
@@ -416,8 +413,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
frigateEventWatcher: eventWatcher,
}),
expect(eventWatcher.unsubscribe).not.toBeCalled();
});
expect(eventWatcher.unsubscribe).not.toBeCalled();
await camera.destroy();
expect(eventWatcher.unsubscribe).toBeCalled();
@@ -1,31 +1,32 @@
import { describe, expect, it } from 'vitest';
import { RecordingSegmentsCache, RequestCache } from '../../../src/camera-manager/cache';
import { RecordingSegmentsCache } from '../../../src/camera-manager/cache';
import { FrigateCameraManagerEngine } from '../../../src/camera-manager/frigate/engine-frigate';
import {
FrigateEventViewMedia,
FrigateRecordingViewMedia,
} from '../../../src/camera-manager/frigate/media';
import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/types.js';
import { CameraManagerRequestCache } from '../../../src/camera-manager/types';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { CameraConfig } from '../../../src/config/schema/cameras';
import { AdvancedCameraCardView } from '../../../src/config/schema/common/const';
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
import { ViewMedia } from '../../../src/view/media';
import { ViewMedia, ViewMediaType } from '../../../src/view/item';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
import { TestViewMedia, createCameraConfig, createHASS } from '../../test-utils';
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
const createEngine = (): FrigateCameraManagerEngine => {
return new FrigateCameraManagerEngine(
new EntityRegistryManagerMock(),
new StateWatcher(),
new RecordingSegmentsCache(),
new RequestCache(),
new CameraManagerRequestCache(),
);
};
const createRecordingMedia = (): FrigateRecordingViewMedia => {
return new FrigateRecordingViewMedia(
'recording',
ViewMediaType.Recording,
'camera-1',
{
cameraID: 'camera-1',
@@ -58,7 +59,7 @@ const createEvent = (): FrigateEvent => {
const createClipMedia = (): FrigateEventViewMedia => {
return new FrigateEventViewMedia(
'clip',
ViewMediaType.Clip,
'camera-1',
createEvent(),
'event-clip-content-id',
@@ -68,7 +69,7 @@ const createClipMedia = (): FrigateEventViewMedia => {
const createSnapshotMedia = (): FrigateEventViewMedia => {
return new FrigateEventViewMedia(
'snapshot',
ViewMediaType.Snapshot,
'camera-1',
createEvent(),
'event-snapshot-content-id',
@@ -133,7 +134,9 @@ describe('getMediaDownloadPath', () => {
const endpoint = await createEngine().getMediaDownloadPath(
createHASS(),
createFrigateCameraConfig(),
new ViewMedia('clip', 'camera-1'),
new ViewMedia(ViewMediaType.Clip, {
cameraID: 'camera-1',
}),
);
expect(endpoint).toBeNull();
});
@@ -247,9 +250,9 @@ describe('getCameraEndpoints', () => {
});
describe('with event media type', () => {
it.each([['clip' as const], ['snapshot' as const]])(
it.each([[ViewMediaType.Clip], [ViewMediaType.Snapshot]])(
'%s',
(mediaType: 'clip' | 'snapshot') => {
(mediaType: ViewMediaType) => {
const endpoints = createEngine().getCameraEndpoints(
createCameraConfig({
frigate: {
@@ -284,7 +287,10 @@ describe('getCameraEndpoints', () => {
},
}),
{
media: new TestViewMedia({ mediaType: 'recording', startTime: startTime }),
media: new TestViewMedia({
mediaType: ViewMediaType.Recording,
startTime: startTime,
}),
},
);
@@ -306,7 +312,7 @@ describe('getCameraEndpoints', () => {
},
}),
{
media: new TestViewMedia({ mediaType: 'recording' }),
media: new TestViewMedia({ mediaType: ViewMediaType.Recording }),
},
);
@@ -18,10 +18,10 @@ import {
retainResultSchema,
} from '../../../src/camera-manager/frigate/types';
import { RecordingSegment } from '../../../src/camera-manager/types';
import { homeAssistantWSRequest } from '../../../src/utils/ha/ws-request';
import { homeAssistantWSRequest } from '../../../src/ha/ws-request';
import { createFrigateEvent, createHASS } from '../../test-utils';
vi.mock('../../../src/utils/ha/ws-request');
vi.mock('../../../src/ha/ws-request');
describe('frigate requests', () => {
afterEach(() => {
+47 -34
View File
@@ -7,16 +7,14 @@ import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-fact
import { CameraManagerEngine } from '../../src/camera-manager/engine.js';
import {
CameraManager,
QueryClassifier,
CameraQueryClassifier,
QueryResultClassifier,
} from '../../src/camera-manager/manager.js';
import {
CameraEndpoint,
CameraEndpoints,
CameraEndpointsContext,
CameraEvent,
CameraManagerCameraMetadata,
CameraManagerMediaCapabilities,
Engine,
EventQuery,
EventQueryResults,
@@ -25,12 +23,13 @@ import {
QueryResultsType,
QueryType,
} from '../../src/camera-manager/types.js';
import { sortMedia } from '../../src/camera-manager/utils/sort-media.js';
import { CardController } from '../../src/card-controller/controller.js';
import { sortItems } from '../../src/card-controller/view/sort.js';
import { CameraConfig } from '../../src/config/schema/cameras.js';
import { HomeAssistant } from '../../src/ha/types.js';
import { PTZMovementType } from '../../src/types.js';
import { ViewMedia } from '../../src/view/media.js';
import { Endpoint, PTZMovementType } from '../../src/types.js';
import { ViewFolder, ViewItem, ViewMedia } from '../../src/view/item.js';
import { ViewItemCapabilities } from '../../src/view/types.js';
import {
TestViewMedia,
createCamera,
@@ -38,53 +37,66 @@ import {
createCapabilities,
createCardAPI,
createConfig,
createFolder,
createHASS,
generateViewMediaArray,
} from '../test-utils.js';
describe('QueryClassifier', async () => {
it('should classify event query', async () => {
expect(QueryClassifier.isEventQuery({ type: QueryType.Event })).toBeTruthy();
expect(QueryClassifier.isEventQuery({ type: QueryType.Recording })).toBeFalsy();
expect(CameraQueryClassifier.isEventQuery({ type: QueryType.Event })).toBeTruthy();
expect(
QueryClassifier.isEventQuery({ type: QueryType.RecordingSegments }),
CameraQueryClassifier.isEventQuery({ type: QueryType.Recording }),
).toBeFalsy();
expect(
CameraQueryClassifier.isEventQuery({ type: QueryType.RecordingSegments }),
).toBeFalsy();
expect(
CameraQueryClassifier.isEventQuery({ type: QueryType.MediaMetadata }),
).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 }),
CameraQueryClassifier.isRecordingQuery({ type: QueryType.Event }),
).toBeFalsy();
expect(
QueryClassifier.isRecordingQuery({ type: QueryType.MediaMetadata }),
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', async () => {
expect(
QueryClassifier.isRecordingSegmentsQuery({ type: QueryType.Event }),
CameraQueryClassifier.isRecordingSegmentsQuery({ type: QueryType.Event }),
).toBeFalsy();
expect(
QueryClassifier.isRecordingSegmentsQuery({ type: QueryType.Recording }),
CameraQueryClassifier.isRecordingSegmentsQuery({ type: QueryType.Recording }),
).toBeFalsy();
expect(
QueryClassifier.isRecordingSegmentsQuery({ type: QueryType.RecordingSegments }),
CameraQueryClassifier.isRecordingSegmentsQuery({
type: QueryType.RecordingSegments,
}),
).toBeTruthy();
expect(
QueryClassifier.isRecordingSegmentsQuery({ type: QueryType.MediaMetadata }),
CameraQueryClassifier.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 }),
CameraQueryClassifier.isMediaMetadataQuery({ type: QueryType.Event }),
).toBeFalsy();
expect(
QueryClassifier.isMediaMetadataQuery({ type: QueryType.RecordingSegments }),
CameraQueryClassifier.isMediaMetadataQuery({ type: QueryType.Recording }),
).toBeFalsy();
expect(
QueryClassifier.isMediaMetadataQuery({ type: QueryType.MediaMetadata }),
CameraQueryClassifier.isMediaMetadataQuery({ type: QueryType.RecordingSegments }),
).toBeFalsy();
expect(
CameraQueryClassifier.isMediaMetadataQuery({ type: QueryType.MediaMetadata }),
).toBeTruthy();
});
});
@@ -641,7 +653,7 @@ describe('CameraManager', async () => {
const results = new Map([[baseEventQuery, baseEventQueryResults]]);
engine.getEvents.mockResolvedValue(results);
const media = sortMedia(generateViewMediaArray({ count: 5 }));
const media = sortItems(generateViewMediaArray({ count: 5 }));
engine.generateMediaFromEvents.mockReturnValue(media);
expect(await manager.executeMediaQueries([baseEventQuery])).toEqual(media);
@@ -697,7 +709,7 @@ describe('CameraManager', async () => {
const results = new Map([[baseRecordingQuery, baseRecordingQueryResults]]);
engine.getRecordings.mockResolvedValue(results);
const media = sortMedia(generateViewMediaArray({ count: 5 }));
const media = sortItems(generateViewMediaArray({ count: 5 }));
engine.generateMediaFromRecordings.mockReturnValue(media);
expect(await manager.executeMediaQueries([baseRecordingQuery])).toEqual(media);
@@ -718,7 +730,7 @@ describe('CameraManager', async () => {
describe('should extend media queries', () => {
const dateBase = new Date('2024-03-01T20:01:00');
const mediaTwoCameras = generateViewMediaArray({ count: 5 });
const mediaMixedStart: ViewMedia[] = [
const mediaMixedStart: ViewItem[] = [
new TestViewMedia({
startTime: dateBase,
}),
@@ -728,6 +740,7 @@ describe('CameraManager', async () => {
new TestViewMedia({
startTime: add(dateBase, { days: 2 }),
}),
new ViewFolder(createFolder()),
];
it('without hass', async () => {
@@ -748,7 +761,7 @@ describe('CameraManager', async () => {
generateViewMediaArray({ count: 5 }),
{
queries: [{ ...baseEventQuery, limit: 50 }],
results: sortMedia(generateViewMediaArray({ count: 5 })),
results: sortItems(generateViewMediaArray({ count: 5 })),
},
],
[
@@ -759,7 +772,7 @@ describe('CameraManager', async () => {
generateViewMediaArray({ count: 5, cameraIDs: ['office'] }),
{
queries: [{ ...baseEventQuery, limit: 50 }],
results: sortMedia(mediaTwoCameras),
results: sortItems(mediaTwoCameras),
},
],
[
@@ -793,7 +806,7 @@ describe('CameraManager', async () => {
mediaTwoCameras,
{
queries: [{ ...baseEventQuery, limit: 50, start: dateBase }],
results: sortMedia(mediaMixedStart.concat(mediaTwoCameras)),
results: sortItems(mediaMixedStart.concat(mediaTwoCameras)),
},
'later' as const,
],
@@ -811,7 +824,7 @@ describe('CameraManager', async () => {
mediaTwoCameras,
{
queries: [{ ...baseEventQuery, limit: 50, start: dateBase }],
results: sortMedia(mediaMixedStart.concat(mediaTwoCameras)),
results: sortItems(mediaMixedStart.concat(mediaTwoCameras)),
},
'earlier' as const,
],
@@ -823,7 +836,7 @@ describe('CameraManager', async () => {
inputQueries: Map<EventQuery, EventQueryResults>,
// The previously received media.
inputMediaResults: ViewMedia[],
inputResults: ViewItem[],
// The queries expected to be dispatched.
newChunkQueries: EventQuery[],
@@ -834,7 +847,7 @@ describe('CameraManager', async () => {
// The expect extended queries and results.
expected?: {
queries: EventQuery[];
results: ViewMedia[];
results: ViewItem[];
} | null,
direction?: 'earlier' | 'later',
) => {
@@ -853,7 +866,7 @@ describe('CameraManager', async () => {
expect(
await manager.extendMediaQueries(
[...inputQueries.keys()],
inputMediaResults,
inputResults,
direction ?? 'later',
),
).toEqual(expected);
@@ -895,7 +908,7 @@ describe('CameraManager', async () => {
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const result: CameraEndpoint = {
const result: Endpoint = {
endpoint: 'http://localhost/path/to/media',
};
vi.mocked(engine.getMediaDownloadPath).mockResolvedValue(result);
@@ -919,7 +932,7 @@ describe('CameraManager', async () => {
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
const result: CameraManagerMediaCapabilities = {
const result: ViewItemCapabilities = {
canFavorite: false,
canDownload: false,
};
+2 -2
View File
@@ -6,14 +6,14 @@ import { CameraProxyConfig } from '../../../src/camera-manager/types';
import { ActionsExecutor } from '../../../src/card-controller/actions/types';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { ProxyConfig } from '../../../src/config/schema/cameras';
import { EntityRegistryManagerLive } from '../../../src/utils/ha/registry/entity';
import { EntityRegistryManagerLive } from '../../../src/ha/registry/entity';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
import {
createCameraConfig,
createHASS,
createRegistryEntity,
createStateEntity,
} from '../../test-utils';
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
describe('ReolinkCamera', () => {
const cameraEntity = createRegistryEntity({
@@ -9,7 +9,6 @@ import {
vi,
} from 'vitest';
import { mock } from 'vitest-mock-extended';
import { RequestCache } from '../../../src/camera-manager/cache';
import {
ReolinkCameraManagerEngine,
ReolinkQueryResultsClassifier,
@@ -17,6 +16,7 @@ import {
import { ReolinkEventQueryResults } from '../../../src/camera-manager/reolink/types';
import { CameraManagerStore } from '../../../src/camera-manager/store';
import {
CameraManagerRequestCache,
Engine,
EventQuery,
QueryResultsType,
@@ -24,14 +24,12 @@ import {
QueryType,
} from '../../../src/camera-manager/types';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { BrowseMediaManager } from '../../../src/utils/ha/browse-media/browse-media-manager';
import {
BrowseMedia,
browseMediaSchema,
} from '../../../src/utils/ha/browse-media/types';
import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity/types';
import { ResolvedMediaCache } from '../../../src/utils/ha/resolved-media';
import { homeAssistantWSRequest } from '../../../src/utils/ha/ws-request';
import { BrowseMedia, browseMediaSchema } from '../../../src/ha/browse-media/types';
import { BrowseMediaWalker } from '../../../src/ha/browse-media/walker';
import { EntityRegistryManager } from '../../../src/ha/registry/entity/types';
import { ResolvedMediaCache } from '../../../src/ha/resolved-media';
import { homeAssistantWSRequest } from '../../../src/ha/ws-request';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
import {
createCamera,
createCameraConfig,
@@ -39,9 +37,8 @@ import {
createRegistryEntity,
createStore,
} from '../../test-utils';
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
vi.mock('../../../src/utils/ha/ws-request');
vi.mock('../../../src/ha/ws-request');
const TEST_CAMERAS: BrowseMedia = {
title: 'Reolink',
@@ -181,15 +178,15 @@ const TEST_FILES: BrowseMedia = {
};
const createEngine = (options?: {
browseMediaManager?: BrowseMediaManager;
browseMediaManager?: BrowseMediaWalker;
entityRegistryManager?: EntityRegistryManager;
}): ReolinkCameraManagerEngine => {
return new ReolinkCameraManagerEngine(
options?.entityRegistryManager ?? new EntityRegistryManagerMock(),
mock<StateWatcher>(),
options?.browseMediaManager ?? new BrowseMediaManager(),
options?.browseMediaManager ?? new BrowseMediaWalker(),
new ResolvedMediaCache(),
new RequestCache(),
new CameraManagerRequestCache(),
);
};
+28 -12
View File
@@ -6,8 +6,8 @@ import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-fact
import { CameraManagerStore } from '../../src/camera-manager/store.js';
import { Engine } from '../../src/camera-manager/types.js';
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity/types.js';
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media.js';
import { EntityRegistryManager } from '../../src/ha/registry/entity/types.js';
import { ResolvedMediaCache } from '../../src/ha/resolved-media.js';
import { TestViewMedia, createCameraConfig } from '../test-utils.js';
describe('CameraManagerStore', async () => {
@@ -139,14 +139,22 @@ describe('CameraManagerStore', async () => {
}
});
it('getCameraConfigForMedia', async () => {
const store = setupStore();
describe('getCameraConfigForMedia', () => {
it('should return the camera config for media with a camera ID', async () => {
const store = setupStore();
const media_1 = new TestViewMedia({ cameraID: 'camera-visible' });
expect(store.getCameraConfigForMedia(media_1)).toBe(configVisible);
const media_1 = new TestViewMedia({ cameraID: 'camera-visible' });
expect(store.getCameraConfigForMedia(media_1)).toBe(configVisible);
const media_2 = new TestViewMedia({ cameraID: 'camera-not-exist' });
expect(store.getCameraConfigForMedia(media_2)).toBeNull();
const media_2 = new TestViewMedia({ cameraID: 'camera-not-exist' });
expect(store.getCameraConfigForMedia(media_2)).toBeNull();
});
it('should return null as the camera config for media without a camera ID', async () => {
const store = setupStore();
const media = new TestViewMedia({ cameraID: null });
expect(store.getCameraConfigForMedia(media)).toBeNull();
});
});
it('getEngineOfType', async () => {
@@ -199,10 +207,18 @@ describe('CameraManagerStore', async () => {
});
});
it('getEngineForMedia', async () => {
const store = setupStore();
const media = new TestViewMedia({ cameraID: 'camera-visible' });
expect(store.getEngineForMedia(media)).toBe(engineGeneric);
describe('getEngineForMedia', () => {
it('should return the engine for media with a camera ID', async () => {
const store = setupStore();
const media = new TestViewMedia({ cameraID: 'camera-visible' });
expect(store.getEngineForMedia(media)).toBe(engineGeneric);
});
it('should return null as the engine for media without a camera ID', async () => {
const store = setupStore();
const media = new TestViewMedia({ cameraID: null });
expect(store.getEngineForMedia(media)).toBeNull();
});
});
describe('getAllDependentCameras', () => {
@@ -1,57 +0,0 @@
import { describe, expect, it } from 'vitest';
import { sortMedia } from '../../../src/camera-manager/utils/sort-media.js';
import { TestViewMedia } from '../../test-utils.js';
describe('sortMedia', () => {
const media_1 = new TestViewMedia({
id: 'id-1',
startTime: new Date('2023-04-29T14:25'),
cameraID: 'camera-1',
});
const media_2 = new TestViewMedia({
id: 'id-2',
startTime: new Date('2023-04-29T14:26'),
cameraID: 'camera-1',
});
const media_3_dup_id = new TestViewMedia({
id: 'id-2',
startTime: new Date('2023-04-29T14:26'),
cameraID: 'camera-1',
});
const media_4_no_id = new TestViewMedia({
id: null,
startTime: new Date('2023-04-29T14:27'),
cameraID: 'camera-1',
});
it('should sort sorted media', () => {
const media = [media_1, media_2];
expect(sortMedia(media)).toEqual(media);
});
it('should sort unsorted media', () => {
expect(sortMedia([media_2, media_1])).toEqual([media_1, media_2]);
});
it('should remove duplicate id', () => {
expect(sortMedia([media_1, media_2, media_3_dup_id])).toEqual([media_1, media_2]);
});
it('should sort by id when time not available', () => {
expect(
sortMedia([
new TestViewMedia({ id: 'snake' }),
new TestViewMedia({ id: 'zebra' }),
new TestViewMedia({ id: 'aardvark' }),
]),
).toEqual([
new TestViewMedia({ id: 'aardvark' }),
new TestViewMedia({ id: 'snake' }),
new TestViewMedia({ id: 'zebra' }),
]);
});
it('should remove de-duplicate by object if no id', () => {
expect(sortMedia([media_1, media_2, media_4_no_id, media_4_no_id])).toEqual([
media_1,
media_2,
media_4_no_id,
]);
});
});