feat: Automatically detect Reolink NVR channel number (#2259)

- Closes: #2032, #1899 
 -  May or may not help with #2254

- Caution: I do not have an NVR-based Reolink camera to test. If
suddenly Reolink cameras don't work in the card, or in particular if
media doesn't correctly load, this is likely the culprit PR.
This commit is contained in:
Dermot Duffy
2025-12-07 22:48:33 -08:00
committed by GitHub
parent c38bf192e0
commit f276b8c57d
10 changed files with 256 additions and 41 deletions
+4 -7
View File
@@ -98,14 +98,11 @@ cameras:
# [...] # [...]
``` ```
| Option | Default | Description | | Option | Default | Description |
| ------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------ | ------- | ----------- |
| `channel` | autodetected (direct), or `0` (via NVR/Hub) | The channel number for the Reolink camera, used by the Reolink integration to identify different cameras. For cameras directly connected to Home Assistant (w/o an NVR), this value will be autodetected and need not be set. For cameras connected via a Hub/NVR, this value cannot currently be autodetected -- a default value of `0` will be used in this case. |
| `media_resolution` | `low` | Whether to retrieve `high` or `low` resolution media items. |
| `url` | | The URL of the Reolink camera/NVR UI. If set, this value will be (exclusively) used for a `Camera UI` menu button. |
> [!TIP] | `media_resolution` | `low` | Whether to retrieve `high` or `low` resolution media items. |
> If media for the "wrong" Reolink camera is showing up and you have an NVR connected camera, your `channel` value is likely incorrect. Try increasing it until you find the correct camera. | `url` | | The URL of the Reolink camera/NVR UI. If set, this value will be (exclusively) used for a `Camera UI` menu button. |
### PTZ Support ### PTZ Support
+8 -2
View File
@@ -1,6 +1,7 @@
import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher'; import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher';
import { CameraConfig } from '../config/schema/cameras'; import { CameraConfig } from '../config/schema/cameras';
import { BrowseMediaWalker } from '../ha/browse-media/walker'; import { BrowseMediaWalker } from '../ha/browse-media/walker';
import { DeviceRegistryManager } from '../ha/registry/device';
import { EntityRegistryManager } from '../ha/registry/entity/types'; import { EntityRegistryManager } from '../ha/registry/entity/types';
import { ResolvedMediaCache } from '../ha/resolved-media'; import { ResolvedMediaCache } from '../ha/resolved-media';
import { HomeAssistant } from '../ha/types'; import { HomeAssistant } from '../ha/types';
@@ -18,11 +19,15 @@ interface CameraManagerEngineFactoryOptions {
} }
export class CameraManagerEngineFactory { export class CameraManagerEngineFactory {
// Entity registry manager is required for the actual function of the factory.
protected _entityRegistryManager: EntityRegistryManager; protected _entityRegistryManager: EntityRegistryManager;
protected _deviceRegistryManager: DeviceRegistryManager;
constructor(entityRegistryManager: EntityRegistryManager) { constructor(
entityRegistryManager: EntityRegistryManager,
deviceRegistryManager: DeviceRegistryManager,
) {
this._entityRegistryManager = entityRegistryManager; this._entityRegistryManager = entityRegistryManager;
this._deviceRegistryManager = deviceRegistryManager;
} }
public async createEngine( public async createEngine(
@@ -65,6 +70,7 @@ export class CameraManagerEngineFactory {
const { ReolinkCameraManagerEngine } = await import('./reolink/engine-reolink'); const { ReolinkCameraManagerEngine } = await import('./reolink/engine-reolink');
cameraManagerEngine = new ReolinkCameraManagerEngine( cameraManagerEngine = new ReolinkCameraManagerEngine(
this._entityRegistryManager, this._entityRegistryManager,
this._deviceRegistryManager,
options.stateWatcher, options.stateWatcher,
new BrowseMediaWalker(), new BrowseMediaWalker(),
options.resolvedMediaCache, options.resolvedMediaCache,
+4 -1
View File
@@ -129,7 +129,10 @@ export class CameraManager {
this._api = api; this._api = api;
this._engineFactory = this._engineFactory =
options?.factory ?? options?.factory ??
new CameraManagerEngineFactory(this._api.getEntityRegistryManager()); new CameraManagerEngineFactory(
this._api.getEntityRegistryManager(),
this._api.getDeviceRegistryManager(),
);
this._store = options?.store ?? new CameraManagerStore(); this._store = options?.store ?? new CameraManagerStore();
} }
+37 -4
View File
@@ -1,6 +1,7 @@
import { ActionsExecutor } from '../../card-controller/actions/types'; import { ActionsExecutor } from '../../card-controller/actions/types';
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher'; import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz'; import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
import { DeviceRegistryManager } from '../../ha/registry/device/index';
import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types'; import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
import { HomeAssistant } from '../../ha/types'; import { HomeAssistant } from '../../ha/types';
import { localize } from '../../localize/localize'; import { localize } from '../../localize/localize';
@@ -18,6 +19,7 @@ const REOLINK_DEFAULT_CHANNEL = 0;
interface ReolinkCameraInitializationOptions extends CameraInitializationOptions { interface ReolinkCameraInitializationOptions extends CameraInitializationOptions {
entityRegistryManager: EntityRegistryManager; entityRegistryManager: EntityRegistryManager;
deviceRegistryManager: DeviceRegistryManager;
hass: HomeAssistant; hass: HomeAssistant;
} }
@@ -50,7 +52,7 @@ export class ReolinkCamera extends EntityCamera {
public async initialize(options: ReolinkCameraInitializationOptions): Promise<Camera> { public async initialize(options: ReolinkCameraInitializationOptions): Promise<Camera> {
await super.initialize(options); await super.initialize(options);
this._initializeChannel(); await this._initializeChannel(options.hass, options.deviceRegistryManager);
await this._initializeCapabilities( await this._initializeCapabilities(
options.hass, options.hass,
options.entityRegistryManager, options.entityRegistryManager,
@@ -59,7 +61,32 @@ export class ReolinkCamera extends EntityCamera {
return this; return this;
} }
protected _initializeChannel(): void { protected async _getChannelFromConfigurationURL(
hass: HomeAssistant,
deviceRegistryManager: DeviceRegistryManager,
): Promise<number | null> {
const deviceID = this._entity?.device_id;
if (!deviceID) {
return null;
}
const device = await deviceRegistryManager.getDevice(hass, deviceID);
if (!device?.configuration_url) {
return null;
}
try {
const url = new URL(device.configuration_url);
const channel = Number(url.searchParams.get('ch'));
return isNaN(channel) ? null : channel;
} catch {
// Ignore invalid URLs.
return null;
}
}
protected async _initializeChannel(
hass: HomeAssistant,
deviceRegistryManager: DeviceRegistryManager,
): Promise<void> {
const uniqueID = this._entity?.unique_id; const uniqueID = this._entity?.unique_id;
// Reolink camera unique IDs are dual-mode, they may be in either of these // Reolink camera unique IDs are dual-mode, they may be in either of these
@@ -95,8 +122,14 @@ export class ReolinkCamera extends EntityCamera {
const channelCandidate = Number(channelOrUID); const channelCandidate = Number(channelOrUID);
const isValidChannel = !isNaN(channelCandidate) && channelCandidate <= 999; const isValidChannel = !isNaN(channelCandidate) && channelCandidate <= 999;
const channel = const channel =
this._config.reolink.channel ?? // Channel from the unique ID itself (for directly connected cameras).
(isValidChannel ? channelCandidate : REOLINK_DEFAULT_CHANNEL); (isValidChannel ? channelCandidate : null) ??
// Channel from the configuration URL (for NVRs where the entity unique
// id is based on the UID).
(await this._getChannelFromConfigurationURL(hass, deviceRegistryManager)) ??
// Fallback.
REOLINK_DEFAULT_CHANNEL;
const reolinkCameraUID = !isValidChannel ? channelOrUID : null; const reolinkCameraUID = !isValidChannel ? channelOrUID : null;
this._reolinkChannel = channel; this._reolinkChannel = channel;
@@ -1,5 +1,6 @@
import { add, endOfDay, parse, startOfDay } from 'date-fns'; import { add, endOfDay, parse, startOfDay } from 'date-fns';
import { orderBy } from 'lodash-es'; import { orderBy } from 'lodash-es';
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
import { CameraConfig } from '../../config/schema/cameras'; import { CameraConfig } from '../../config/schema/cameras';
import { getViewMediaFromBrowseMediaArray } from '../../ha/browse-media/browse-media-to-view-media'; import { getViewMediaFromBrowseMediaArray } from '../../ha/browse-media/browse-media-to-view-media';
import { sortMostRecentFirst } from '../../ha/browse-media/sort'; import { sortMostRecentFirst } from '../../ha/browse-media/sort';
@@ -11,7 +12,11 @@ import {
MEDIA_CLASS_VIDEO, MEDIA_CLASS_VIDEO,
RichBrowseMedia, RichBrowseMedia,
} from '../../ha/browse-media/types'; } from '../../ha/browse-media/types';
import { BrowseMediaWalker } from '../../ha/browse-media/walker';
import { isMediaWithinDates } from '../../ha/browse-media/within-dates'; import { isMediaWithinDates } from '../../ha/browse-media/within-dates';
import { DeviceRegistryManager } from '../../ha/registry/device';
import { EntityRegistryManager } from '../../ha/registry/entity/types';
import { ResolvedMediaCache } from '../../ha/resolved-media';
import { HomeAssistant } from '../../ha/types'; import { HomeAssistant } from '../../ha/types';
import { Endpoint } from '../../types'; import { Endpoint } from '../../types';
import { allPromises, formatDate, isValidDate } from '../../utils/basic'; import { allPromises, formatDate, isValidDate } from '../../utils/basic';
@@ -23,7 +28,9 @@ import { CameraManagerReadOnlyConfigStore } from '../store';
import { import {
CameraEndpoints, CameraEndpoints,
CameraEndpointsContext, CameraEndpointsContext,
CameraEventCallback,
CameraManagerCameraMetadata, CameraManagerCameraMetadata,
CameraManagerRequestCache,
Engine, Engine,
EngineOptions, EngineOptions,
EventQuery, EventQuery,
@@ -50,6 +57,27 @@ export class ReolinkQueryResultsClassifier {
export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine { export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
protected _camerasCache = new BrowseMediaCache<BrowseMediaReolinkCameraMetadata>(); protected _camerasCache = new BrowseMediaCache<BrowseMediaReolinkCameraMetadata>();
protected _cache = new BrowseMediaCache<BrowseMediaMetadata>(); protected _cache = new BrowseMediaCache<BrowseMediaMetadata>();
protected _deviceRegistryManager: DeviceRegistryManager;
public constructor(
entityRegistryManager: EntityRegistryManager,
deviceRegistryManager: DeviceRegistryManager,
stateWatcher: StateWatcherSubscriptionInterface,
browseMediaManager: BrowseMediaWalker,
resolvedMediaCache: ResolvedMediaCache,
requestCache: CameraManagerRequestCache,
eventCallback?: CameraEventCallback,
) {
super(
entityRegistryManager,
stateWatcher,
browseMediaManager,
resolvedMediaCache,
requestCache,
eventCallback,
);
this._deviceRegistryManager = deviceRegistryManager;
}
public getEngineType(): Engine { public getEngineType(): Engine {
return Engine.Reolink; return Engine.Reolink;
@@ -147,6 +175,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
}); });
return await camera.initialize({ return await camera.initialize({
entityRegistryManager: this._entityRegistryManager, entityRegistryManager: this._entityRegistryManager,
deviceRegistryManager: this._deviceRegistryManager,
hass, hass,
stateWatcher: this._stateWatcher, stateWatcher: this._stateWatcher,
}); });
+2
View File
@@ -1,6 +1,7 @@
import type { CameraManager } from '../camera-manager/manager'; import type { CameraManager } from '../camera-manager/manager';
import type { ConditionStateManager } from '../conditions/state-manager'; import type { ConditionStateManager } from '../conditions/state-manager';
import type { Automation } from '../config/schema/automations'; import type { Automation } from '../config/schema/automations';
import type { DeviceRegistryManager } from '../ha/registry/device';
import type { EntityRegistryManager } from '../ha/registry/entity/types'; import type { EntityRegistryManager } from '../ha/registry/entity/types';
import type { ResolvedMediaCache } from '../ha/resolved-media'; import type { ResolvedMediaCache } from '../ha/resolved-media';
import type { EffectsControllerAPI } from '../types'; import type { EffectsControllerAPI } from '../types';
@@ -70,6 +71,7 @@ export interface CardAutomationsAPI {
export interface CardCameraAPI { export interface CardCameraAPI {
getActionsManager(): ActionsManager; getActionsManager(): ActionsManager;
getConfigManager(): ConfigManager; getConfigManager(): ConfigManager;
getDeviceRegistryManager(): DeviceRegistryManager;
getEntityRegistryManager(): EntityRegistryManager; getEntityRegistryManager(): EntityRegistryManager;
getHASSManager(): HASSManager; getHASSManager(): HASSManager;
getMessageManager(): MessageManager; getMessageManager(): MessageManager;
-1
View File
@@ -250,7 +250,6 @@ export const cameraConfigSchema = z
reolink: z reolink: z
.object({ .object({
url: z.string().optional(), url: z.string().optional(),
channel: z.number().optional(),
media_resolution: z media_resolution: z
.enum(['high', 'low']) .enum(['high', 'low'])
.default(cameraConfigDefault.reolink.media_resolution), .default(cameraConfigDefault.reolink.media_resolution),
+1
View File
@@ -6,6 +6,7 @@ const deviceSchema = z.object({
model: z.string().nullable(), model: z.string().nullable(),
config_entries: z.string().array(), config_entries: z.string().array(),
manufacturer: z.string().nullable(), manufacturer: z.string().nullable(),
configuration_url: z.string().nullable().optional(),
}); });
export type Device = z.infer<typeof deviceSchema>; export type Device = z.infer<typeof deviceSchema>;
+169 -26
View File
@@ -6,6 +6,7 @@ import { CameraProxyConfig } from '../../../src/camera-manager/types';
import { ActionsExecutor } from '../../../src/card-controller/actions/types'; import { ActionsExecutor } from '../../../src/card-controller/actions/types';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher'; import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { EntityRegistryManagerLive } from '../../../src/ha/registry/entity'; import { EntityRegistryManagerLive } from '../../../src/ha/registry/entity';
import { DeviceRegistryManager } from '../../../src/ha/registry/device';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock'; import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
import { import {
createCameraConfig, createCameraConfig,
@@ -97,6 +98,7 @@ describe('ReolinkCamera', () => {
await camera.initialize({ await camera.initialize({
hass: createHASS(), hass: createHASS(),
entityRegistryManager: mock<EntityRegistryManagerLive>(), entityRegistryManager: mock<EntityRegistryManagerLive>(),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}), }),
).rejects.toThrowError('Could not find camera entity'); ).rejects.toThrowError('Could not find camera entity');
@@ -143,37 +145,12 @@ describe('ReolinkCamera', () => {
await camera.initialize({ await camera.initialize({
hass: createHASS(), hass: createHASS(),
entityRegistryManager, entityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}), }),
).rejects.toThrowError('Could not initialize Reolink camera'); ).rejects.toThrowError('Could not initialize Reolink camera');
}); });
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1964
it('successfully with an NVR-connected camera with user-specified channel', async () => {
const config = createCameraConfig({
camera_entity: 'camera.office_reolink',
reolink: {
channel: 42,
},
});
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
const entityRegistryManager = new EntityRegistryManagerMock([
createRegistryEntity({
entity_id: 'camera.office_reolink',
unique_id: '9527000HXU4V1VHZ_9527000I7E5F1GYU_main',
platform: 'reolink',
}),
]);
await camera.initialize({
hass: createHASS(),
entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
});
expect(camera.getChannel()).toBe(42);
});
it('successfully with an NVR-connected camera without user-specified channel', async () => { it('successfully with an NVR-connected camera without user-specified channel', async () => {
const config = createCameraConfig({ const config = createCameraConfig({
camera_entity: 'camera.office_reolink', camera_entity: 'camera.office_reolink',
@@ -190,6 +167,7 @@ describe('ReolinkCamera', () => {
await camera.initialize({ await camera.initialize({
hass: createHASS(), hass: createHASS(),
entityRegistryManager, entityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}); });
@@ -206,11 +184,166 @@ describe('ReolinkCamera', () => {
await camera.initialize({ await camera.initialize({
hass: createHASS(), hass: createHASS(),
entityRegistryManager, entityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}); });
expect(camera.getChannel()).toBe(0); expect(camera.getChannel()).toBe(0);
}); });
it('successfully with an NVR-connected camera with channel in configuration_url', async () => {
const config = createCameraConfig({
camera_entity: 'camera.office_reolink',
});
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
const entityRegistryManager = new EntityRegistryManagerMock([
createRegistryEntity({
entity_id: 'camera.office_reolink',
unique_id: '9527000HXU4V1VHZ_9527000I7E5F1GYU_main',
device_id: 'device-id',
platform: 'reolink',
}),
]);
const deviceRegistryManager = mock<DeviceRegistryManager>();
deviceRegistryManager.getDevice.mockResolvedValue({
id: 'device-id',
configuration_url: 'http://192.168.1.1?ch=3',
config_entries: [],
manufacturer: 'Reolink',
model: 'RLN8-410',
});
await camera.initialize({
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager,
stateWatcher: mock<StateWatcher>(),
});
expect(camera.getChannel()).toBe(3);
});
describe('should detect channel from configuration URL', () => {
it('should return null if device_id is missing', async () => {
const config = createCameraConfig({
camera_entity: 'camera.office_reolink',
});
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
const entityRegistryManager = new EntityRegistryManagerMock([
createRegistryEntity({
entity_id: 'camera.office_reolink',
unique_id: '9527000HXU4V1VHZ_9527000I7E5F1GYU_main',
// No device_id
platform: 'reolink',
}),
]);
await camera.initialize({
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
});
expect(camera.getChannel()).toBe(0);
});
it('should return null if device has no configuration_url', async () => {
const config = createCameraConfig({
camera_entity: 'camera.office_reolink',
});
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
const entityRegistryManager = new EntityRegistryManagerMock([
createRegistryEntity({
entity_id: 'camera.office_reolink',
unique_id: '9527000HXU4V1VHZ_9527000I7E5F1GYU_main',
device_id: 'device-id',
platform: 'reolink',
}),
]);
const deviceRegistryManager = mock<DeviceRegistryManager>();
deviceRegistryManager.getDevice.mockResolvedValue({
id: 'device-id',
// No configuration_url
config_entries: [],
manufacturer: 'Reolink',
model: 'RLN8-410',
});
await camera.initialize({
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager,
stateWatcher: mock<StateWatcher>(),
});
expect(camera.getChannel()).toBe(0);
});
it('should return null if configuration_url is invalid', async () => {
const config = createCameraConfig({
camera_entity: 'camera.office_reolink',
});
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
const entityRegistryManager = new EntityRegistryManagerMock([
createRegistryEntity({
entity_id: 'camera.office_reolink',
unique_id: '9527000HXU4V1VHZ_9527000I7E5F1GYU_main',
device_id: 'device-id',
platform: 'reolink',
}),
]);
const deviceRegistryManager = mock<DeviceRegistryManager>();
deviceRegistryManager.getDevice.mockResolvedValue({
id: 'device-id',
configuration_url: 'invalid-url',
config_entries: [],
manufacturer: 'Reolink',
model: 'RLN8-410',
});
await camera.initialize({
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager,
stateWatcher: mock<StateWatcher>(),
});
expect(camera.getChannel()).toBe(0);
});
it('should return null if ch parameter is not a number', async () => {
const config = createCameraConfig({
camera_entity: 'camera.office_reolink',
});
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
const entityRegistryManager = new EntityRegistryManagerMock([
createRegistryEntity({
entity_id: 'camera.office_reolink',
unique_id: '9527000HXU4V1VHZ_9527000I7E5F1GYU_main',
device_id: 'device-id',
platform: 'reolink',
}),
]);
const deviceRegistryManager = mock<DeviceRegistryManager>();
deviceRegistryManager.getDevice.mockResolvedValue({
id: 'device-id',
configuration_url: 'http://192.168.1.1?ch=NOT_A_NUMBER',
config_entries: [],
manufacturer: 'Reolink',
model: 'RLN8-410',
});
await camera.initialize({
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager,
stateWatcher: mock<StateWatcher>(),
});
expect(camera.getChannel()).toBe(0);
});
});
}); });
describe('successfully with PTZ', () => { describe('successfully with PTZ', () => {
@@ -223,6 +356,7 @@ describe('ReolinkCamera', () => {
await camera.initialize({ await camera.initialize({
hass: createHASS(), hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager, entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}); });
@@ -261,6 +395,7 @@ describe('ReolinkCamera', () => {
platform: 'reolink', platform: 'reolink',
}), }),
]), ]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}); });
@@ -286,6 +421,7 @@ describe('ReolinkCamera', () => {
}), }),
}), }),
entityRegistryManager: ptzPopulatedEntityRegistryManager, entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}); });
@@ -318,6 +454,7 @@ describe('ReolinkCamera', () => {
await camera.initialize({ await camera.initialize({
hass: createHASS(), hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager, entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}); });
@@ -504,6 +641,7 @@ describe('ReolinkCamera', () => {
await camera.initialize({ await camera.initialize({
hass: createHASS(), hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]), entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}); });
const executor = mock<ActionsExecutor>(); const executor = mock<ActionsExecutor>();
@@ -535,6 +673,7 @@ describe('ReolinkCamera', () => {
cameraEntity, cameraEntity,
buttonEntityPTZLeft, buttonEntityPTZLeft,
]), ]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}); });
const executor = mock<ActionsExecutor>(); const executor = mock<ActionsExecutor>();
@@ -561,6 +700,7 @@ describe('ReolinkCamera', () => {
await camera.initialize({ await camera.initialize({
hass: createHASS(), hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager, entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}); });
const executor = mock<ActionsExecutor>(); const executor = mock<ActionsExecutor>();
@@ -601,6 +741,7 @@ describe('ReolinkCamera', () => {
await camera.initialize({ await camera.initialize({
hass: createHASS(), hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager, entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}); });
const executor = mock<ActionsExecutor>(); const executor = mock<ActionsExecutor>();
@@ -626,6 +767,7 @@ describe('ReolinkCamera', () => {
}), }),
}), }),
entityRegistryManager: ptzPopulatedEntityRegistryManager, entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}); });
const executor = mock<ActionsExecutor>(); const executor = mock<ActionsExecutor>();
@@ -656,6 +798,7 @@ describe('ReolinkCamera', () => {
await camera.initialize({ await camera.initialize({
hass: createHASS(), hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager, entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(), stateWatcher: mock<StateWatcher>(),
}); });
const executor = mock<ActionsExecutor>(); const executor = mock<ActionsExecutor>();
@@ -26,6 +26,7 @@ import {
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher'; import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { BrowseMedia, browseMediaSchema } from '../../../src/ha/browse-media/types'; import { BrowseMedia, browseMediaSchema } from '../../../src/ha/browse-media/types';
import { BrowseMediaWalker } from '../../../src/ha/browse-media/walker'; import { BrowseMediaWalker } from '../../../src/ha/browse-media/walker';
import { DeviceRegistryManager } from '../../../src/ha/registry/device';
import { EntityRegistryManager } from '../../../src/ha/registry/entity/types'; import { EntityRegistryManager } from '../../../src/ha/registry/entity/types';
import { ResolvedMediaCache } from '../../../src/ha/resolved-media'; import { ResolvedMediaCache } from '../../../src/ha/resolved-media';
import { homeAssistantWSRequest } from '../../../src/ha/ws-request'; import { homeAssistantWSRequest } from '../../../src/ha/ws-request';
@@ -183,6 +184,7 @@ const createEngine = (options?: {
}): ReolinkCameraManagerEngine => { }): ReolinkCameraManagerEngine => {
return new ReolinkCameraManagerEngine( return new ReolinkCameraManagerEngine(
options?.entityRegistryManager ?? new EntityRegistryManagerMock(), options?.entityRegistryManager ?? new EntityRegistryManagerMock(),
mock<DeviceRegistryManager>(),
mock<StateWatcher>(), mock<StateWatcher>(),
options?.browseMediaManager ?? new BrowseMediaWalker(), options?.browseMediaManager ?? new BrowseMediaWalker(),
new ResolvedMediaCache(), new ResolvedMediaCache(),