fix: Only fetch Reolink media for cameras that support it (#1747)
This commit is contained in:
@@ -20,7 +20,6 @@ import {
|
|||||||
PartialEventQuery,
|
PartialEventQuery,
|
||||||
QueryType,
|
QueryType,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { BrowseMediaMetadata } from './types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base class for cameras that read events from HA BrowseMedia interface.
|
* A base class for cameras that read events from HA BrowseMedia interface.
|
||||||
@@ -29,7 +28,7 @@ export class BrowseMediaCameraManagerEngine
|
|||||||
extends GenericCameraManagerEngine
|
extends GenericCameraManagerEngine
|
||||||
implements CameraManagerEngine
|
implements CameraManagerEngine
|
||||||
{
|
{
|
||||||
protected _browseMediaManager: BrowseMediaManager<BrowseMediaMetadata>;
|
protected _browseMediaManager: BrowseMediaManager;
|
||||||
protected _entityRegistryManager: EntityRegistryManager;
|
protected _entityRegistryManager: EntityRegistryManager;
|
||||||
protected _resolvedMediaCache: ResolvedMediaCache;
|
protected _resolvedMediaCache: ResolvedMediaCache;
|
||||||
protected _requestCache: RequestCache;
|
protected _requestCache: RequestCache;
|
||||||
@@ -37,7 +36,7 @@ export class BrowseMediaCameraManagerEngine
|
|||||||
public constructor(
|
public constructor(
|
||||||
entityRegistryManager: EntityRegistryManager,
|
entityRegistryManager: EntityRegistryManager,
|
||||||
stateWatcher: StateWatcherSubscriptionInterface,
|
stateWatcher: StateWatcherSubscriptionInterface,
|
||||||
browseMediaManager: BrowseMediaManager<BrowseMediaMetadata>,
|
browseMediaManager: BrowseMediaManager,
|
||||||
resolvedMediaCache: ResolvedMediaCache,
|
resolvedMediaCache: ResolvedMediaCache,
|
||||||
requestCache: RequestCache,
|
requestCache: RequestCache,
|
||||||
eventCallback?: CameraEventCallback,
|
eventCallback?: CameraEventCallback,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
|
import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
|
||||||
import reolinkLogo from './assets/reolink.svg';
|
import reolinkLogo from './assets/reolink.svg';
|
||||||
import { ReolinkCamera } from './camera';
|
import { ReolinkCamera } from './camera';
|
||||||
import { ReolinkEventQueryResults } from './types';
|
import { BrowseMediaReolinkCameraMetadata, ReolinkEventQueryResults } from './types';
|
||||||
|
|
||||||
export class ReolinkQueryResultsClassifier {
|
export class ReolinkQueryResultsClassifier {
|
||||||
public static isReolinkEventQueryResults(
|
public static isReolinkEventQueryResults(
|
||||||
@@ -51,8 +51,7 @@ export class ReolinkQueryResultsClassifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||||
protected _directoryCache = new MemoryRequestCache<string, BrowseMedia>();
|
protected _cache = new MemoryRequestCache<string, BrowseMedia>();
|
||||||
protected _fileCache = new MemoryRequestCache<string, BrowseMedia>();
|
|
||||||
|
|
||||||
public getEngineType(): Engine {
|
public getEngineType(): Engine {
|
||||||
return Engine.Reolink;
|
return Engine.Reolink;
|
||||||
@@ -115,6 +114,21 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _reolinkCameraMetadataGenerator(
|
||||||
|
media: BrowseMedia,
|
||||||
|
): BrowseMediaReolinkCameraMetadata | null {
|
||||||
|
// Example: "media-source://reolink/CAM|01J8XHYTNH77WE3C654K03KX1F|0"
|
||||||
|
const result = media.media_content_id.match(
|
||||||
|
/^media-source:\/\/reolink\/CAM\|(?<configEntryID>.+)\|(?<channel>\d+)$/,
|
||||||
|
);
|
||||||
|
return result?.groups
|
||||||
|
? {
|
||||||
|
configEntryID: result.groups.configEntryID,
|
||||||
|
channel: Number(result.groups.channel),
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
public async createCamera(
|
public async createCamera(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
@@ -164,6 +178,34 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First fetch all the Reolink cameras that show up under the media root,
|
||||||
|
// that match the expected camera. Some Reolink cameras will not show up
|
||||||
|
// here causing errors.
|
||||||
|
// https://github.com/dermotduffy/frigate-hass-card/issues/1723
|
||||||
|
const camerasWithMedia = await this._browseMediaManager.walkBrowseMedias(
|
||||||
|
hass,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
targets: [`media-source://reolink`],
|
||||||
|
metadataGenerator: (
|
||||||
|
media: BrowseMedia,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_parent?: RichBrowseMedia<BrowseMediaReolinkCameraMetadata>,
|
||||||
|
) => this._reolinkCameraMetadataGenerator(media),
|
||||||
|
matcher: (media: RichBrowseMedia<BrowseMediaReolinkCameraMetadata>): boolean =>
|
||||||
|
media._metadata?.channel === camera.getChannel() &&
|
||||||
|
media._metadata?.configEntryID === configID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
...(engineOptions?.useCache !== false && { cache: this._cache }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!camerasWithMedia?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return await this._browseMediaManager.walkBrowseMedias(
|
return await this._browseMediaManager.walkBrowseMedias(
|
||||||
hass,
|
hass,
|
||||||
[
|
[
|
||||||
@@ -172,7 +214,6 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
|||||||
`media-source://reolink/RES|${configID}|${camera.getChannel()}|` +
|
`media-source://reolink/RES|${configID}|${camera.getChannel()}|` +
|
||||||
`${cameraConfig.reolink?.media_resolution === 'low' ? 'sub' : 'main'}`,
|
`${cameraConfig.reolink?.media_resolution === 'low' ? 'sub' : 'main'}`,
|
||||||
],
|
],
|
||||||
concurrency: Infinity,
|
|
||||||
metadataGenerator: (
|
metadataGenerator: (
|
||||||
media: BrowseMedia,
|
media: BrowseMedia,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
@@ -186,7 +227,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
...(engineOptions?.useCache !== false && { cache: this._directoryCache }),
|
...(engineOptions?.useCache !== false && { cache: this._cache }),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -251,7 +292,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
...(engineOptions?.useCache !== false && { cache: this._fileCache }),
|
...(engineOptions?.useCache !== false && { cache: this._cache }),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { RichBrowseMedia } from '../../utils/ha/browse-media/types';
|
|||||||
import { BrowseMediaMetadata } from '../browse-media/types';
|
import { BrowseMediaMetadata } from '../browse-media/types';
|
||||||
import { Engine, EventQueryResults } from '../types';
|
import { Engine, EventQueryResults } from '../types';
|
||||||
|
|
||||||
|
export interface BrowseMediaReolinkCameraMetadata {
|
||||||
|
configEntryID: string;
|
||||||
|
channel: number;
|
||||||
|
}
|
||||||
|
|
||||||
// ==============================
|
// ==============================
|
||||||
// Reolink concrete query results
|
// Reolink concrete query results
|
||||||
// ==============================
|
// ==============================
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ export interface BrowseMediaStep<M> {
|
|||||||
|
|
||||||
type BrowseMediaStepAdvancer<M> = (media: RichBrowseMedia<M>[]) => BrowseMediaStep<M>[];
|
type BrowseMediaStepAdvancer<M> = (media: RichBrowseMedia<M>[]) => BrowseMediaStep<M>[];
|
||||||
|
|
||||||
export class BrowseMediaManager<M> {
|
export class BrowseMediaManager {
|
||||||
// Walk down a browse media tree according to instructions included in `steps`.
|
// Walk down a browse media tree according to instructions included in `steps`.
|
||||||
public async walkBrowseMedias(
|
public async walkBrowseMedias<M>(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
steps: BrowseMediaStep<M>[] | null,
|
steps: BrowseMediaStep<M>[] | null,
|
||||||
options?: {
|
options?: {
|
||||||
@@ -77,7 +77,7 @@ export class BrowseMediaManager<M> {
|
|||||||
).flat();
|
).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _walkBrowseMedia(
|
protected async _walkBrowseMedia<M>(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
step: BrowseMediaStep<M>,
|
step: BrowseMediaStep<M>,
|
||||||
options?: {
|
options?: {
|
||||||
@@ -92,7 +92,6 @@ export class BrowseMediaManager<M> {
|
|||||||
async (target) =>
|
async (target) =>
|
||||||
await this._browseMedia(hass, target, {
|
await this._browseMedia(hass, target, {
|
||||||
cache: options?.cache,
|
cache: options?.cache,
|
||||||
matcher: step.matcher,
|
|
||||||
metadataGenerator: step.metadataGenerator,
|
metadataGenerator: step.metadataGenerator,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -121,12 +120,11 @@ export class BrowseMediaManager<M> {
|
|||||||
return await this.walkBrowseMedias(hass, nextSteps, options);
|
return await this.walkBrowseMedias(hass, nextSteps, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _browseMedia(
|
protected async _browseMedia<M>(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
target: string | RichBrowseMedia<M>,
|
target: string | RichBrowseMedia<M>,
|
||||||
options?: {
|
options?: {
|
||||||
cache?: BrowseMediaCache<M>;
|
cache?: BrowseMediaCache<M>;
|
||||||
matcher?: RichBrowseMediaPredicate<M>;
|
|
||||||
metadataGenerator?: RichMetadataGenerator<M>;
|
metadataGenerator?: RichMetadataGenerator<M>;
|
||||||
},
|
},
|
||||||
): Promise<RichBrowseMedia<M>> {
|
): Promise<RichBrowseMedia<M>> {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
vi,
|
vi,
|
||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { BrowseMediaMetadata } from '../../../src/camera-manager/browse-media/types';
|
|
||||||
import { RequestCache } from '../../../src/camera-manager/cache';
|
import { RequestCache } from '../../../src/camera-manager/cache';
|
||||||
import {
|
import {
|
||||||
ReolinkCameraManagerEngine,
|
ReolinkCameraManagerEngine,
|
||||||
@@ -43,6 +42,29 @@ import {
|
|||||||
|
|
||||||
vi.mock('../../../src/utils/ha/ws-request');
|
vi.mock('../../../src/utils/ha/ws-request');
|
||||||
|
|
||||||
|
const TEST_CAMERAS: BrowseMedia = {
|
||||||
|
title: 'Reolink',
|
||||||
|
media_class: 'channel',
|
||||||
|
media_content_type: 'playlist',
|
||||||
|
media_content_id: 'media-source://reolink',
|
||||||
|
children_media_class: 'directory',
|
||||||
|
can_play: false,
|
||||||
|
can_expand: true,
|
||||||
|
thumbnail: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: 'Back Yard',
|
||||||
|
media_class: 'directory',
|
||||||
|
media_content_type: 'playlist',
|
||||||
|
media_content_id: 'media-source://reolink/CAM|01J8XHYTNH77WE3C654K03KX1F|0',
|
||||||
|
children_media_class: null,
|
||||||
|
can_play: false,
|
||||||
|
can_expand: true,
|
||||||
|
thumbnail: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const TEST_DIRECTORIES: BrowseMedia = {
|
const TEST_DIRECTORIES: BrowseMedia = {
|
||||||
title: 'Back Yard Low res.',
|
title: 'Back Yard Low res.',
|
||||||
media_class: 'channel',
|
media_class: 'channel',
|
||||||
@@ -136,13 +158,13 @@ const TEST_FILES: BrowseMedia = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createEngine = (options?: {
|
const createEngine = (options?: {
|
||||||
browseMediaManager?: BrowseMediaManager<BrowseMediaMetadata>;
|
browseMediaManager?: BrowseMediaManager;
|
||||||
entityRegistryManager?: EntityRegistryManager;
|
entityRegistryManager?: EntityRegistryManager;
|
||||||
}): ReolinkCameraManagerEngine => {
|
}): ReolinkCameraManagerEngine => {
|
||||||
return new ReolinkCameraManagerEngine(
|
return new ReolinkCameraManagerEngine(
|
||||||
options?.entityRegistryManager ?? mock<EntityRegistryManager>(),
|
options?.entityRegistryManager ?? mock<EntityRegistryManager>(),
|
||||||
mock<StateWatcher>(),
|
mock<StateWatcher>(),
|
||||||
options?.browseMediaManager ?? new BrowseMediaManager<BrowseMediaMetadata>(),
|
options?.browseMediaManager ?? new BrowseMediaManager(),
|
||||||
new ResolvedMediaCache(),
|
new ResolvedMediaCache(),
|
||||||
new RequestCache(),
|
new RequestCache(),
|
||||||
);
|
);
|
||||||
@@ -306,6 +328,7 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
const store = await createStoreWithReolinkCamera(engine);
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
|
|
||||||
vi.mocked(homeAssistantWSRequest)
|
vi.mocked(homeAssistantWSRequest)
|
||||||
|
.mockResolvedValueOnce(TEST_CAMERAS)
|
||||||
.mockResolvedValueOnce(TEST_DIRECTORIES)
|
.mockResolvedValueOnce(TEST_DIRECTORIES)
|
||||||
.mockResolvedValueOnce(TEST_FILES);
|
.mockResolvedValueOnce(TEST_FILES);
|
||||||
|
|
||||||
@@ -380,6 +403,7 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
const store = await createStoreWithReolinkCamera(engine);
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
|
|
||||||
vi.mocked(homeAssistantWSRequest)
|
vi.mocked(homeAssistantWSRequest)
|
||||||
|
.mockResolvedValueOnce(TEST_CAMERAS)
|
||||||
.mockResolvedValueOnce(TEST_DIRECTORIES)
|
.mockResolvedValueOnce(TEST_DIRECTORIES)
|
||||||
.mockResolvedValueOnce(TEST_FILES);
|
.mockResolvedValueOnce(TEST_FILES);
|
||||||
|
|
||||||
@@ -399,7 +423,7 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(homeAssistantWSRequest).toHaveBeenCalledTimes(2);
|
expect(homeAssistantWSRequest).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should request high resolution if configured', async () => {
|
it('should request high resolution if configured', async () => {
|
||||||
@@ -419,6 +443,7 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
store.addCamera(camera);
|
store.addCamera(camera);
|
||||||
|
|
||||||
vi.mocked(homeAssistantWSRequest)
|
vi.mocked(homeAssistantWSRequest)
|
||||||
|
.mockResolvedValueOnce(TEST_CAMERAS)
|
||||||
.mockResolvedValueOnce(TEST_DIRECTORIES)
|
.mockResolvedValueOnce(TEST_DIRECTORIES)
|
||||||
.mockResolvedValueOnce(TEST_FILES);
|
.mockResolvedValueOnce(TEST_FILES);
|
||||||
|
|
||||||
@@ -446,7 +471,9 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
const engine = createPopulatedEngine();
|
const engine = createPopulatedEngine();
|
||||||
const store = await createStoreWithReolinkCamera(engine);
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
|
|
||||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce({
|
vi.mocked(homeAssistantWSRequest)
|
||||||
|
.mockResolvedValueOnce(TEST_CAMERAS)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
title: 'Back Yard Low res.',
|
title: 'Back Yard Low res.',
|
||||||
media_class: 'channel',
|
media_class: 'channel',
|
||||||
media_content_type: 'playlist',
|
media_content_type: 'playlist',
|
||||||
@@ -568,6 +595,103 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should ignore no cameras', async () => {
|
||||||
|
const engine = createPopulatedEngine();
|
||||||
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
|
|
||||||
|
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce({
|
||||||
|
title: 'Reolink',
|
||||||
|
media_class: 'channel',
|
||||||
|
media_content_type: 'playlist',
|
||||||
|
media_content_id: 'media-source://reolink',
|
||||||
|
children_media_class: 'directory',
|
||||||
|
can_play: false,
|
||||||
|
can_expand: true,
|
||||||
|
thumbnail: null,
|
||||||
|
children: [
|
||||||
|
// No cameras.
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
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 ignore malformed camera', async () => {
|
||||||
|
const engine = createPopulatedEngine();
|
||||||
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
|
|
||||||
|
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce({
|
||||||
|
title: 'Reolink',
|
||||||
|
media_class: 'channel',
|
||||||
|
media_content_type: 'playlist',
|
||||||
|
media_content_id: 'media-source://reolink',
|
||||||
|
children_media_class: 'directory',
|
||||||
|
can_play: false,
|
||||||
|
can_expand: true,
|
||||||
|
thumbnail: null,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: 'Back Yard',
|
||||||
|
media_class: 'directory',
|
||||||
|
media_content_type: 'playlist',
|
||||||
|
media_content_id: 'media-source://reolink/__MALFORMED__',
|
||||||
|
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 malformed media', () => {
|
describe('should ignore malformed media', () => {
|
||||||
@@ -576,6 +700,7 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
const store = await createStoreWithReolinkCamera(engine);
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
|
|
||||||
vi.mocked(homeAssistantWSRequest)
|
vi.mocked(homeAssistantWSRequest)
|
||||||
|
.mockResolvedValueOnce(TEST_CAMERAS)
|
||||||
.mockResolvedValueOnce(TEST_DIRECTORIES)
|
.mockResolvedValueOnce(TEST_DIRECTORIES)
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
title: 'Back Yard Low res. 2024/11/4',
|
title: 'Back Yard Low res. 2024/11/4',
|
||||||
@@ -634,6 +759,7 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
const store = await createStoreWithReolinkCamera(engine);
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
|
|
||||||
vi.mocked(homeAssistantWSRequest)
|
vi.mocked(homeAssistantWSRequest)
|
||||||
|
.mockResolvedValueOnce(TEST_CAMERAS)
|
||||||
.mockResolvedValueOnce(TEST_DIRECTORIES)
|
.mockResolvedValueOnce(TEST_DIRECTORIES)
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
title: 'Back Yard Low res. 2024/11/4',
|
title: 'Back Yard Low res. 2024/11/4',
|
||||||
@@ -692,6 +818,7 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
const store = await createStoreWithReolinkCamera(engine);
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
|
|
||||||
vi.mocked(homeAssistantWSRequest)
|
vi.mocked(homeAssistantWSRequest)
|
||||||
|
.mockResolvedValueOnce(TEST_CAMERAS)
|
||||||
.mockResolvedValueOnce(TEST_DIRECTORIES)
|
.mockResolvedValueOnce(TEST_DIRECTORIES)
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
title: 'Back Yard Low res. 2024/11/4',
|
title: 'Back Yard Low res. 2024/11/4',
|
||||||
@@ -850,7 +977,9 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
const engine = createPopulatedEngine();
|
const engine = createPopulatedEngine();
|
||||||
const store = await createStoreWithReolinkCamera(engine);
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
|
|
||||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(TEST_DIRECTORIES);
|
vi.mocked(homeAssistantWSRequest)
|
||||||
|
.mockResolvedValueOnce(TEST_CAMERAS)
|
||||||
|
.mockResolvedValueOnce(TEST_DIRECTORIES);
|
||||||
|
|
||||||
const metadata = await engine.getMediaMetadata(
|
const metadata = await engine.getMediaMetadata(
|
||||||
createHASS(),
|
createHASS(),
|
||||||
@@ -887,7 +1016,9 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
const engine = createPopulatedEngine();
|
const engine = createPopulatedEngine();
|
||||||
const store = await createStoreWithReolinkCamera(engine);
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
|
|
||||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce(TEST_DIRECTORIES);
|
vi.mocked(homeAssistantWSRequest)
|
||||||
|
.mockResolvedValueOnce(TEST_CAMERAS)
|
||||||
|
.mockResolvedValueOnce(TEST_DIRECTORIES);
|
||||||
|
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
await engine.getMediaMetadata(
|
await engine.getMediaMetadata(
|
||||||
@@ -901,7 +1032,7 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(homeAssistantWSRequest).toHaveBeenCalledTimes(1);
|
expect(homeAssistantWSRequest).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should ignore invalid cameras', () => {
|
describe('should ignore invalid cameras', () => {
|
||||||
@@ -979,7 +1110,9 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
const engine = createPopulatedEngine();
|
const engine = createPopulatedEngine();
|
||||||
const store = await createStoreWithReolinkCamera(engine);
|
const store = await createStoreWithReolinkCamera(engine);
|
||||||
|
|
||||||
vi.mocked(homeAssistantWSRequest).mockResolvedValueOnce({
|
vi.mocked(homeAssistantWSRequest)
|
||||||
|
.mockResolvedValueOnce(TEST_CAMERAS)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
title: 'Back Yard Low res.',
|
title: 'Back Yard Low res.',
|
||||||
media_class: 'channel',
|
media_class: 'channel',
|
||||||
media_content_type: 'playlist',
|
media_content_type: 'playlist',
|
||||||
|
|||||||
Reference in New Issue
Block a user