Files
advanced-camera-card/tests/camera-manager/reolink/engine-reolink.test.ts
T
Dermot Duffy dada65e008 feat: Add experimental reolink media support (#1694)
* feat: Add experimental rich reolink support.

* Formatting fix
2024-11-25 20:19:12 -08:00

1034 lines
32 KiB
TypeScript

import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import { mock } from 'vitest-mock-extended';
import { BrowseMediaMetadata } from '../../../src/camera-manager/browse-media/types';
import { RequestCache } from '../../../src/camera-manager/cache';
import {
ReolinkCameraManagerEngine,
ReolinkQueryResultsClassifier,
} from '../../../src/camera-manager/reolink/engine-reolink';
import { ReolinkEventQueryResults } from '../../../src/camera-manager/reolink/types';
import { CameraManagerStore } from '../../../src/camera-manager/store';
import {
Engine,
EventQuery,
QueryResultsType,
QueryReturnType,
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';
import { ResolvedMediaCache } from '../../../src/utils/ha/resolved-media';
import { homeAssistantWSRequest } from '../../../src/utils/ha/ws-request';
import {
createCamera,
createCameraConfig,
createHASS,
createRegistryEntity,
createStore,
} from '../../test-utils';
vi.mock('../../../src/utils/ha/ws-request');
const TEST_DIRECTORIES: BrowseMedia = {
title: 'Back Yard Low res.',
media_class: 'channel',
media_content_type: 'playlist',
media_content_id: 'media-source://reolink/DAYS|01J8XHYTNH77WE3C654K03KX1F|0|sub',
children_media_class: 'directory',
can_play: false,
can_expand: true,
thumbnail: null,
children: [
{
title: '2024/11/4',
media_class: 'directory',
media_content_type: 'playlist',
media_content_id:
'media-source://reolink/DAY|01J8XHYTNH77WE3C654K03KX1F|0|sub|2024|11|4',
children_media_class: null,
can_play: false,
can_expand: true,
thumbnail: null,
},
{
title: '2024/11/5',
media_class: 'directory',
media_content_type: 'playlist',
media_content_id:
'media-source://reolink/DAY|01J8XHYTNH77WE3C654K03KX1F|0|sub|2024|11|5',
children_media_class: null,
can_play: false,
can_expand: true,
thumbnail: null,
},
{
title: '2024/11/6',
media_class: 'directory',
media_content_type: 'playlist',
media_content_id:
'media-source://reolink/DAY|01J8XHYTNH77WE3C654K03KX1F|0|sub|2024|11|6',
children_media_class: null,
can_play: false,
can_expand: true,
thumbnail: null,
},
],
};
const TEST_FILES: BrowseMedia = {
title: 'Back Yard Low res. 2024/11/4',
media_class: 'channel',
media_content_type: 'playlist',
media_content_id: 'media-source://reolink/FILES|01J8XHYTNH77WE3C654K03KX1F|0|sub',
children_media_class: 'video',
can_play: false,
can_expand: true,
thumbnail: null,
children: [
{
title: '21:23:53 0:00:34',
media_class: 'video',
media_content_type: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052353_211_S.mp4',
children_media_class: null,
can_play: true,
can_expand: false,
thumbnail: null,
},
{
title: '21:29:05 0:00:41',
media_class: 'video',
media_content_type: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052905_211_S.mp4',
children_media_class: null,
can_play: true,
can_expand: false,
thumbnail: null,
},
{
title: '22:04:49 0:00:35',
media_class: 'video',
media_content_type: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_060449_211_S.mp4',
children_media_class: null,
can_play: true,
can_expand: false,
thumbnail: null,
},
],
};
const createEngine = (options?: {
browseMediaManager?: BrowseMediaManager<BrowseMediaMetadata>;
entityRegistryManager?: EntityRegistryManager;
}): ReolinkCameraManagerEngine => {
return new ReolinkCameraManagerEngine(
options?.entityRegistryManager ?? mock<EntityRegistryManager>(),
mock<StateWatcher>(),
options?.browseMediaManager ?? new BrowseMediaManager<BrowseMediaMetadata>(),
new ResolvedMediaCache(),
new RequestCache(),
);
};
const createPopulatedEngine = (): ReolinkCameraManagerEngine => {
const entityRegistryManager = mock<EntityRegistryManager>();
const entity = createRegistryEntity({
unique_id: '85270002TS7D4RUP_0_main',
platform: 'reolink',
config_entry_id: '01J8XHYTNH77WE3C654K03KX1F',
});
vi.mocked(entityRegistryManager.getEntity).mockResolvedValue(entity);
return createEngine({ entityRegistryManager });
};
const createStoreWithReolinkCamera = async (
engine: ReolinkCameraManagerEngine,
): Promise<CameraManagerStore> => {
const store = new CameraManagerStore();
const camera = await engine.createCamera(
createHASS(),
createCameraConfig({ camera_entity: 'camera.office', id: 'office' }),
);
store.addCamera(camera);
return store;
};
describe('ReolinkQueryResultsClassifier', () => {
it('should correctly identify matching results', () => {
expect(
ReolinkQueryResultsClassifier.isReolinkEventQueryResults({
type: QueryResultsType.Event,
engine: Engine.Reolink,
}),
).toBeTruthy();
});
it('should correctly identify non-matching results', () => {
expect(
ReolinkQueryResultsClassifier.isReolinkEventQueryResults({
type: QueryResultsType.Event,
engine: Engine.MotionEye,
}),
).toBeFalsy();
});
});
describe('ReolinkCameraManagerEngine', () => {
it('should get correct engine type', () => {
const engine = createEngine();
expect(engine.getEngineType()).toBe('reolink');
});
it('should create camera', async () => {
const entityRegistryManager = mock<EntityRegistryManager>();
const entity = createRegistryEntity({
unique_id: '85270002TS7D4RUP_0_main',
platform: 'reolink',
});
vi.mocked(entityRegistryManager.getEntity).mockResolvedValue(entity);
const engine = createEngine({ entityRegistryManager });
const config = createCameraConfig({
camera_entity: 'camera.office',
unique_id: 'office',
});
const camera = await engine.createCamera(createHASS(), config);
expect(camera.getConfig()).toBe(config);
expect(camera.getEngine()).toBe(engine);
expect(camera.getCapabilities()?.getRawCapabilities()).toEqual({
'favorite-events': false,
'favorite-recordings': false,
clips: true,
live: true,
menu: true,
recordings: false,
seek: false,
snapshots: false,
substream: true,
});
});
it('should get camera metadata', () => {
const cameraConfig = createCameraConfig({
title: 'Office',
camera_entity: 'camera.office',
icon: 'mdi:camera',
});
const engine = createEngine();
expect(engine.getCameraMetadata(createHASS(), cameraConfig)).toEqual({
engineLogo: '/src/camera-manager/reolink/assets/reolink.svg',
icon: 'mdi:camera',
title: 'Office',
});
});
describe('should get camera endpoints', () => {
it('should return ui endpoint', () => {
const cameraConfig = createCameraConfig({
reolink: {
url: 'http://path-to-reolink',
},
});
const engine = createEngine();
expect(engine.getCameraEndpoints(cameraConfig)).toEqual(
expect.objectContaining({
ui: { endpoint: 'http://path-to-reolink' },
}),
);
});
it('should return go2rtc endpoint', () => {
const cameraConfig = createCameraConfig({
go2rtc: {
url: 'http://path-to-go2rtc',
stream: 'stream',
},
});
const engine = createEngine();
expect(engine.getCameraEndpoints(cameraConfig)).toEqual(
expect.objectContaining({
go2rtc: { endpoint: 'http://path-to-go2rtc/api/ws?src=stream', sign: false },
}),
);
});
});
describe('should get events', () => {
afterEach(() => {
vi.clearAllMocks();
});
describe('should get no event for unsupported features', () => {
it.each([
['with favorite', { favorite: true }],
['with tags', { tags: new Set(['gate']) }],
['with what', { what: new Set(['car']) }],
['with where', { where: new Set(['office']) }],
['with hasSnapshot', { hasSnapshot: true }],
])('%s', async (_name: string, query: Partial<EventQuery>) => {
const engine = createEngine();
expect(
await engine.getEvents(createHASS(), createStore(), {
...query,
cameraIDs: new Set(['office']),
type: QueryType.Event,
}),
).toBeNull();
});
});
it('should get events successfully without cache', async () => {
const engine = createPopulatedEngine();
const store = await createStoreWithReolinkCamera(engine);
vi.mocked(homeAssistantWSRequest)
.mockResolvedValueOnce(TEST_DIRECTORIES)
.mockResolvedValueOnce(TEST_FILES);
const events = await engine.getEvents(
createHASS(),
store,
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
end: new Date('2024-11-04T22:00:00'),
},
{
useCache: false,
},
);
expect(events).toEqual(
new Map([
[
{
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
type: 'event-query',
},
{
browseMedia: [
{
_metadata: {
cameraID: 'office',
endDate: new Date('2024-11-04T21:29:46'),
startDate: new Date('2024-11-04T21:29:05'),
},
can_expand: false,
can_play: true,
children_media_class: null,
media_class: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052905_211_S.mp4',
media_content_type: 'video',
thumbnail: null,
title: '21:29:05 0:00:41',
},
{
_metadata: {
cameraID: 'office',
endDate: new Date('2024-11-04T21:24:27'),
startDate: new Date('2024-11-04T21:23:53'),
},
can_expand: false,
can_play: true,
children_media_class: null,
media_class: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052353_211_S.mp4',
media_content_type: 'video',
thumbnail: null,
title: '21:23:53 0:00:34',
},
],
engine: 'reolink',
type: 'event-results',
},
],
]),
);
});
it('should cache event requests', async () => {
const engine = createPopulatedEngine();
const store = await createStoreWithReolinkCamera(engine);
vi.mocked(homeAssistantWSRequest)
.mockResolvedValueOnce(TEST_DIRECTORIES)
.mockResolvedValueOnce(TEST_FILES);
for (let i = 0; i < 10; i++) {
await engine.getEvents(
createHASS(),
store,
{
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
end: new Date('2024-11-04T22:00:00'),
},
{
useCache: true,
},
);
}
expect(homeAssistantWSRequest).toHaveBeenCalledTimes(2);
});
it('should request high resolution if configured', async () => {
const engine = createPopulatedEngine();
const camera = await engine.createCamera(
createHASS(),
createCameraConfig({
camera_entity: 'camera.office',
id: 'office',
reolink: {
media_resolution: 'high',
},
}),
);
const store = new CameraManagerStore();
store.addCamera(camera);
vi.mocked(homeAssistantWSRequest)
.mockResolvedValueOnce(TEST_DIRECTORIES)
.mockResolvedValueOnce(TEST_FILES);
const hass = createHASS();
await engine.getEvents(hass, store, {
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
end: new Date('2024-11-04T22:00:00'),
});
expect(homeAssistantWSRequest).toHaveBeenCalledWith(
hass,
browseMediaSchema,
expect.objectContaining({
media_content_id:
// Media source request will refer to 'main' not 'sub'.
'media-source://reolink/RES|01J8XHYTNH77WE3C654K03KX1F|0|main',
}),
);
});
describe('should ignore malformed directories', () => {
it('malformed directory title', async () => {
const engine = createPopulatedEngine();
const store = await createStoreWithReolinkCamera(engine);
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce({
title: 'Back Yard Low res.',
media_class: 'channel',
media_content_type: 'playlist',
media_content_id:
'media-source://reolink/DAYS|01J8XHYTNH77WE3C654K03KX1F|0|sub',
children_media_class: 'directory',
can_play: false,
can_expand: true,
thumbnail: null,
children: [
{
// Malformed date.
title: '__MALFORMED__',
media_class: 'directory',
media_content_type: 'playlist',
media_content_id:
'media-source://reolink/DAY|01J8XHYTNH77WE3C654K03KX1F|0|sub|2024|11|4',
children_media_class: null,
can_play: false,
can_expand: true,
thumbnail: null,
},
],
});
const events = await engine.getEvents(createHASS(), store, {
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
end: new Date('2024-11-04T22:00:00'),
});
expect(events).toEqual(
new Map([
[
{
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
type: 'event-query',
},
{
browseMedia: [],
engine: 'reolink',
type: 'event-results',
},
],
]),
);
});
});
describe('should ignore invalid cameras', () => {
it('camera without a config entry id', async () => {
const entityRegistryManager = mock<EntityRegistryManager>();
const entity = createRegistryEntity({
unique_id: '85270002TS7D4RUP_0_main',
platform: 'reolink',
// Cannot fetch events without a config_entry_id.
config_entry_id: null,
});
vi.mocked(entityRegistryManager.getEntity).mockResolvedValue(entity);
const engine = createEngine({ entityRegistryManager });
const store = await createStoreWithReolinkCamera(engine);
const events = await engine.getEvents(createHASS(), store, {
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
end: new Date('2024-11-04T22:00:00'),
});
expect(events).toEqual(
new Map([
[
{
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
type: 'event-query',
},
{
browseMedia: [],
engine: 'reolink',
type: 'event-results',
},
],
]),
);
});
it('should reject requests for non-reolink cameras', async () => {
const engine = createPopulatedEngine();
const store = new CameraManagerStore();
store.addCamera(createCamera(createCameraConfig({ id: 'office' }), engine));
const hass = createHASS();
const events = await engine.getEvents(hass, store, {
type: QueryType.Event,
cameraIDs: new Set(['office']),
});
expect(events).toEqual(
new Map([
[
{
cameraIDs: new Set(['office']),
type: 'event-query',
},
{
browseMedia: [],
engine: 'reolink',
type: 'event-results',
},
],
]),
);
});
});
describe('should ignore malformed media', () => {
it('malformed file title', async () => {
const engine = createPopulatedEngine();
const store = await createStoreWithReolinkCamera(engine);
vi.mocked(homeAssistantWSRequest)
.mockResolvedValueOnce(TEST_DIRECTORIES)
.mockResolvedValueOnce({
title: 'Back Yard Low res. 2024/11/4',
media_class: 'channel',
media_content_type: 'playlist',
media_content_id:
'media-source://reolink/FILES|01J8XHYTNH77WE3C654K03KX1F|0|sub',
children_media_class: 'video',
can_play: false,
can_expand: true,
thumbnail: null,
children: [
{
// Title not matching format.
title: '21:23:53 0:00:34 _____MALFORMED_____',
media_class: 'video',
media_content_type: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052353_211_S.mp4',
children_media_class: null,
can_play: true,
can_expand: false,
thumbnail: null,
},
],
});
const events = await engine.getEvents(createHASS(), store, {
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
end: new Date('2024-11-04T22:00:00'),
});
expect(events).toEqual(
new Map([
[
{
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
type: 'event-query',
},
{
browseMedia: [],
engine: 'reolink',
type: 'event-results',
},
],
]),
);
});
it('malformed start time', async () => {
const engine = createPopulatedEngine();
const store = await createStoreWithReolinkCamera(engine);
vi.mocked(homeAssistantWSRequest)
.mockResolvedValueOnce(TEST_DIRECTORIES)
.mockResolvedValueOnce({
title: 'Back Yard Low res. 2024/11/4',
media_class: 'channel',
media_content_type: 'playlist',
media_content_id:
'media-source://reolink/FILES|01J8XHYTNH77WE3C654K03KX1F|0|sub',
children_media_class: 'video',
can_play: false,
can_expand: true,
thumbnail: null,
children: [
{
// Invalid hour.
title: '29:23:53 0:00:34',
media_class: 'video',
media_content_type: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052353_211_S.mp4',
children_media_class: null,
can_play: true,
can_expand: false,
thumbnail: null,
},
],
});
const events = await engine.getEvents(createHASS(), store, {
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
end: new Date('2024-11-04T22:00:00'),
});
expect(events).toEqual(
new Map([
[
{
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
type: 'event-query',
},
{
browseMedia: [],
engine: 'reolink',
type: 'event-results',
},
],
]),
);
});
it('malformed duration', async () => {
const engine = createPopulatedEngine();
const store = await createStoreWithReolinkCamera(engine);
vi.mocked(homeAssistantWSRequest)
.mockResolvedValueOnce(TEST_DIRECTORIES)
.mockResolvedValueOnce({
title: 'Back Yard Low res. 2024/11/4',
media_class: 'channel',
media_content_type: 'playlist',
media_content_id:
'media-source://reolink/FILES|01J8XHYTNH77WE3C654K03KX1F|0|sub',
children_media_class: 'video',
can_play: false,
can_expand: true,
thumbnail: null,
children: [
{
// Invalid duration.
title: '21:23:53 INVALID_DURATION',
media_class: 'video',
media_content_type: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052353_211_S.mp4',
children_media_class: null,
can_play: true,
can_expand: false,
thumbnail: null,
},
],
});
const events = await engine.getEvents(createHASS(), store, {
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
end: new Date('2024-11-04T22:00:00'),
});
expect(events).toEqual(
new Map([
[
{
cameraIDs: new Set(['office']),
end: new Date('2024-11-04T22:00:00'),
start: new Date('2024-11-04T21:00:00'),
type: 'event-query',
},
{
browseMedia: [
{
_metadata: {
cameraID: 'office',
// End time and start time will match.
endDate: new Date('2024-11-04T21:23:53'),
startDate: new Date('2024-11-04T21:23:53'),
},
can_expand: false,
can_play: true,
children_media_class: null,
media_class: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052353_211_S.mp4',
media_content_type: 'video',
thumbnail: null,
title: '21:23:53 INVALID_DURATION',
},
],
engine: 'reolink',
type: 'event-results',
},
],
]),
);
});
});
});
describe('should generate media from events', () => {
it('should generate media successfully', () => {
const query: EventQuery = {
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
end: new Date('2024-11-04T22:00:00'),
};
const results: ReolinkEventQueryResults = {
browseMedia: [
{
_metadata: {
cameraID: 'office',
endDate: new Date('2024-11-04T22:00:00'),
startDate: new Date('2024-11-04T21:00:00'),
},
can_expand: false,
can_play: true,
children_media_class: null,
media_class: 'video',
media_content_id:
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052353_211_S.mp4',
media_content_type: 'video',
thumbnail: null,
title: '21:23:53 1:00:00',
},
],
engine: Engine.Reolink,
type: QueryResultsType.Event,
};
const store = new CameraManagerStore();
const engine = createEngine();
const media = engine.generateMediaFromEvents(createHASS(), store, query, results);
expect(media?.length).toBe(1);
expect(media?.[0].getCameraID()).toBe('office');
expect(media?.[0].getStartTime()).toEqual(new Date('2024-11-04T21:00:00'));
expect(media?.[0].getEndTime()).toEqual(new Date('2024-11-04T22:00:00'));
expect(media?.[0].getVideoContentType()).toBe('mp4');
expect(media?.[0].getID()).toBe('office/2024-11-04 21:00:00');
expect(media?.[0].getContentID()).toBe(
'media-source://reolink/FILE|01J8XHYTNH77WE3C654K03KX1F|0|sub|Rec_20241105_052353_211_S.mp4',
);
expect(media?.[0].getTitle()).toBe('2024-11-04 21:00');
});
it('should reject non-reolink results', () => {
const query: EventQuery = {
type: QueryType.Event,
cameraIDs: new Set(['office']),
start: new Date('2024-11-04T21:00:00'),
end: new Date('2024-11-04T22:00:00'),
};
const results: QueryReturnType<EventQuery> = {
engine: Engine.Frigate,
type: QueryResultsType.Event,
};
const store = new CameraManagerStore();
const engine = createEngine();
const media = engine.generateMediaFromEvents(createHASS(), store, query, results);
expect(media).toBeNull();
});
});
describe('should get media metadata', () => {
beforeAll(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-11-17T15:06:00'));
});
afterAll(() => {
vi.useRealTimers();
});
beforeEach(() => {
vi.clearAllMocks();
});
it('should get media metadata successfully', async () => {
const engine = createPopulatedEngine();
const store = await createStoreWithReolinkCamera(engine);
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(TEST_DIRECTORIES);
const metadata = await engine.getMediaMetadata(
createHASS(),
store,
{
type: QueryType.MediaMetadata,
cameraIDs: new Set(['office']),
},
{ useCache: false },
);
expect(metadata).toEqual(
new Map([
[
{
cameraIDs: new Set(['office']),
type: 'media-metadata',
},
{
cached: false,
engine: 'reolink',
expiry: new Date('2024-11-17T15:07:00'),
metadata: {
days: new Set(['2024-11-06', '2024-11-05', '2024-11-04']),
},
type: 'media-metadata-results',
},
],
]),
);
});
it('should cache media metadata', async () => {
const engine = createPopulatedEngine();
const store = await createStoreWithReolinkCamera(engine);
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(TEST_DIRECTORIES);
for (let i = 0; i < 10; i++) {
await engine.getMediaMetadata(
createHASS(),
store,
{
type: QueryType.MediaMetadata,
cameraIDs: new Set(['office']),
},
{ useCache: true },
);
}
expect(homeAssistantWSRequest).toHaveBeenCalledTimes(1);
});
describe('should ignore invalid cameras', () => {
it('should get no metdata for camera without a config entry id', async () => {
const entityRegistryManager = mock<EntityRegistryManager>();
const entity = createRegistryEntity({
unique_id: '85270002TS7D4RUP_0_main',
platform: 'reolink',
// Cannot fetch events without a config_entry_id.
config_entry_id: null,
});
vi.mocked(entityRegistryManager.getEntity).mockResolvedValue(entity);
const engine = createEngine({ entityRegistryManager });
const store = await createStoreWithReolinkCamera(engine);
const metadata = await engine.getMediaMetadata(createHASS(), store, {
type: QueryType.MediaMetadata,
cameraIDs: new Set(['office']),
});
expect(metadata).toEqual(
new Map([
[
{
cameraIDs: new Set(['office']),
type: 'media-metadata',
},
{
cached: false,
engine: 'reolink',
expiry: new Date('2024-11-17T15:07:00'),
metadata: {},
type: 'media-metadata-results',
},
],
]),
);
});
it('should get no metdata for non-reolink camera', async () => {
const engine = createPopulatedEngine();
const store = new CameraManagerStore();
store.addCamera(createCamera(createCameraConfig({ id: 'office' }), engine));
const metadata = await engine.getMediaMetadata(createHASS(), store, {
type: QueryType.MediaMetadata,
cameraIDs: new Set(['office']),
});
expect(metadata).toEqual(
new Map([
[
{
cameraIDs: new Set(['office']),
type: 'media-metadata',
},
{
cached: false,
engine: 'reolink',
expiry: new Date('2024-11-17T15:07:00'),
metadata: {},
type: 'media-metadata-results',
},
],
]),
);
});
});
describe('should ignore malformed directories', () => {
it('malformed directory title', async () => {
const engine = createPopulatedEngine();
const store = await createStoreWithReolinkCamera(engine);
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce({
title: 'Back Yard Low res.',
media_class: 'channel',
media_content_type: 'playlist',
media_content_id:
'media-source://reolink/DAYS|01J8XHYTNH77WE3C654K03KX1F|0|sub',
children_media_class: 'directory',
can_play: false,
can_expand: true,
thumbnail: null,
children: [
{
// Malformed date.
title: '__MALFORMED__',
media_class: 'directory',
media_content_type: 'playlist',
media_content_id:
'media-source://reolink/DAY|01J8XHYTNH77WE3C654K03KX1F|0|sub|2024|11|4',
children_media_class: null,
can_play: false,
can_expand: true,
thumbnail: null,
},
],
});
const metadata = await engine.getMediaMetadata(createHASS(), store, {
type: QueryType.MediaMetadata,
cameraIDs: new Set(['office']),
});
expect(metadata).toEqual(
new Map([
[
{
cameraIDs: new Set(['office']),
type: 'media-metadata',
},
{
cached: false,
engine: 'reolink',
expiry: new Date('2024-11-17T15:07:00'),
metadata: {},
type: 'media-metadata-results',
},
],
]),
);
});
});
});
});