@apocaliss92/scrypted-reolink-native 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apocaliss92/scrypted-reolink-native",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Use any reolink camera with Scrypted, even older/unsupported models without HTTP protocol support",
5
5
  "author": "@apocaliss92",
6
6
  "license": "Apache",
@@ -9,6 +9,8 @@ import {
9
9
  } from "./common";
10
10
  import { DebugLogOption } from "./debug-options";
11
11
  import type ReolinkNativePlugin from "./main";
12
+ import { ReolinkNativeNvrDevice } from "./nvr";
13
+ import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
12
14
 
13
15
  export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
14
16
  private lastPicture: { mo: MediaObject; atMs: number } | undefined;
@@ -28,10 +30,16 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
28
30
  return debugLogs.includes(DebugLogOption.BatteryInfo);
29
31
  }
30
32
 
31
- constructor(nativeId: string, public plugin: ReolinkNativePlugin, nvrDevice?: any) {
33
+ constructor(
34
+ nativeId: string,
35
+ public plugin: ReolinkNativePlugin,
36
+ nvrDevice?: ReolinkNativeNvrDevice,
37
+ multiFocalDevice?: ReolinkNativeMultiFocalDevice
38
+ ) {
32
39
  super(nativeId, plugin, {
33
40
  type: 'battery',
34
41
  nvrDevice,
42
+ multiFocalDevice,
35
43
  });
36
44
  }
37
45
 
@@ -112,8 +120,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
112
120
 
113
121
  logger.log('Starting periodic tasks for battery camera');
114
122
 
115
- // Check sleeping state every 5 seconds (non-blocking)
116
- if (!this.nvrDevice) {
123
+ if (!this.nvrDevice && !this.multiFocalDevice) {
117
124
  this.sleepCheckTimer = setInterval(async () => {
118
125
  try {
119
126
  const api = this.baichuanApi;
package/src/camera.ts CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  } from "./common";
7
7
  import { createBaichuanApi } from './connect';
8
8
  import ReolinkNativePlugin from "./main";
9
+ import { ReolinkNativeNvrDevice } from "./nvr";
10
+ import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
9
11
 
10
12
  export const moToB64 = async (mo: MediaObject) => {
11
13
  const bufferImage = await sdk.mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
@@ -28,10 +30,16 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
28
30
  private statusPollTimer: NodeJS.Timeout | undefined;
29
31
 
30
32
 
31
- constructor(nativeId: string, public plugin: ReolinkNativePlugin, nvrDevice?: any) {
33
+ constructor(
34
+ nativeId: string,
35
+ public plugin: ReolinkNativePlugin,
36
+ nvrDevice?: ReolinkNativeNvrDevice,
37
+ multiFocalDevice?: ReolinkNativeMultiFocalDevice
38
+ ) {
32
39
  super(nativeId, plugin, {
33
40
  type: 'regular',
34
41
  nvrDevice,
42
+ multiFocalDevice,
35
43
  });
36
44
  }
37
45
 
@@ -109,10 +117,6 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
109
117
  return api;
110
118
  }
111
119
 
112
- getClient(): ReolinkBaichuanApi | undefined {
113
- return this.baichuanApi;
114
- }
115
-
116
120
  private passiveRefreshTimer: ReturnType<typeof setTimeout> | undefined;
117
121
 
118
122
  async release() {
package/src/common.ts CHANGED
@@ -8,7 +8,7 @@ import { convertDebugLogsToApiOptions, DebugLogDisplayNames, DebugLogOption, get
8
8
  import { ReolinkBaichuanIntercom } from "./intercom";
9
9
  import ReolinkNativePlugin from "./main";
10
10
  import { ReolinkNativeNvrDevice } from "./nvr";
11
- import { ReolinkNativeMultiFocalDevice } from "./multifocal";
11
+ import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
12
12
  import { ReolinkPtzPresets } from "./presets";
13
13
  import {
14
14
  createRfc4571MediaObjectFromStreamManager,
@@ -526,7 +526,11 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
526
526
  protected multiFocalDevice?: ReolinkNativeMultiFocalDevice;
527
527
  thisDevice: Settings
528
528
 
529
- constructor(nativeId: string, public plugin: ReolinkNativePlugin, public options: CommonCameraMixinOptions) {
529
+ constructor(
530
+ nativeId: string,
531
+ public plugin: ReolinkNativePlugin,
532
+ public options: CommonCameraMixinOptions
533
+ ) {
530
534
  super(nativeId);
531
535
  this.protocol = !options.nvrDevice && !options.multiFocalDevice && options.type === 'battery' ? 'udp' : 'tcp';
532
536
 
@@ -644,12 +648,17 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
644
648
  }
645
649
  }
646
650
  }
651
+
647
652
  createStreamClient(): Promise<ReolinkBaichuanApi> {
648
653
  throw new Error("Method not implemented.");
649
654
  }
650
655
 
651
656
  public getAbilities(): DeviceCapabilities {
652
- return this.storageSettings.values.capabilities;
657
+ if (this.options.multiFocalDevice) {
658
+ return this.options.multiFocalDevice.getInterfaces(this.storageSettings.values.rtspChannel).capabilities;
659
+ } else {
660
+ return this.storageSettings.values.capabilities;
661
+ }
653
662
  }
654
663
 
655
664
  getBaichuanDebugOptions(): any | undefined {
@@ -1107,6 +1116,11 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1107
1116
  async updateDeviceInfo(): Promise<void> {
1108
1117
  const logger = this.getBaichuanLogger();
1109
1118
 
1119
+ if (this.options.multiFocalDevice) {
1120
+ this.info = this.options.multiFocalDevice.info;
1121
+ return;
1122
+ }
1123
+
1110
1124
  const { ipAddress, rtspChannel } = this.storageSettings.values;
1111
1125
  try {
1112
1126
  const api = await this.ensureClient();
@@ -1357,21 +1371,16 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1357
1371
  const vsos = await this.getVideoStreamOptions();
1358
1372
  const selected = selectStreamOption(vsos, vso);
1359
1373
 
1360
- // Check if this is a native stream (prefixed with "native_")
1361
-
1362
- // If stream has RTSP/RTMP URL (not native), add credentials and create MediaStreamUrl
1363
1374
  if (selected.url && (selected.container === 'rtsp' || selected.container === 'rtmp')) {
1364
1375
  const urlWithCredentials = this.addRtspCredentials(selected.url);
1365
1376
  const ret: MediaStreamUrl = {
1366
1377
  container: selected.container,
1367
- // url: selected.url,
1368
1378
  url: urlWithCredentials,
1369
1379
  mediaStreamOptions: selected,
1370
1380
  };
1371
1381
  return await this.createMediaObject(ret, ScryptedMimeTypes.MediaStreamUrl);
1372
1382
  }
1373
1383
 
1374
- // Use streamManager for native Baichuan streams (native_* or streams without URL)
1375
1384
  if (!this.streamManager) {
1376
1385
  throw new Error('StreamManager not initialized');
1377
1386
  }
@@ -1406,9 +1415,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1406
1415
  return await this.withBaichuanRetry(createStreamFn);
1407
1416
  }
1408
1417
 
1409
- // Client management
1410
1418
  async ensureClient(): Promise<ReolinkBaichuanApi> {
1411
- // If camera is connected to NVR, use NVR's shared Baichuan connection
1412
1419
  if (this.nvrDevice) {
1413
1420
  return await this.nvrDevice.ensureBaichuanClient();
1414
1421
  }
@@ -1444,38 +1451,41 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1444
1451
  const channel = this.storageSettings.values.rtspChannel;
1445
1452
 
1446
1453
  try {
1447
- const { capabilities, abilities, support, presets, objects } = await this.withBaichuanRetry(async () => {
1448
- const api = await this.ensureClient();
1449
- return await api.getDeviceCapabilities(channel);
1450
- });
1451
- this.classes = objects;
1452
- this.presets = presets;
1453
- this.storageSettings.values.capabilities = capabilities;
1454
- this.ptzPresets.setCachedPtzPresets(presets);
1455
-
1456
- try {
1457
- const { interfaces, type } = getDeviceInterfaces({
1458
- capabilities,
1459
- logger: this.console,
1454
+ if (this.options.multiFocalDevice) {
1455
+ // do nothing for now
1456
+ } else {
1457
+ const { capabilities, abilities, support, presets, objects } = await this.withBaichuanRetry(async () => {
1458
+ const api = await this.ensureClient();
1459
+ return await api.getDeviceCapabilities(channel);
1460
1460
  });
1461
+ this.classes = objects;
1462
+ this.presets = presets;
1463
+ this.ptzPresets.setCachedPtzPresets(presets);
1461
1464
 
1462
- const device: Device = {
1463
- nativeId: this.nativeId,
1464
- providerNativeId: this.nvrDevice?.nativeId ?? this.plugin?.nativeId,
1465
- name: this.name,
1466
- interfaces,
1467
- type,
1468
- info: this.info,
1469
- };
1470
-
1471
- logger.log(`Updating device interfaces: ${JSON.stringify(device)}`);
1465
+ try {
1466
+ const { interfaces, type } = getDeviceInterfaces({
1467
+ capabilities,
1468
+ logger: this.console,
1469
+ });
1470
+
1471
+ const device: Device = {
1472
+ nativeId: this.nativeId,
1473
+ providerNativeId: this.nvrDevice?.nativeId ?? this.plugin?.nativeId,
1474
+ name: this.name,
1475
+ interfaces,
1476
+ type,
1477
+ info: this.info,
1478
+ };
1479
+
1480
+ logger.log(`Updating device interfaces: ${JSON.stringify(device)}`);
1481
+
1482
+ await sdk.deviceManager.onDeviceDiscovered(device);
1483
+ } catch (e) {
1484
+ logger.error('Failed to update device interfaces', e);
1485
+ }
1472
1486
 
1473
- await sdk.deviceManager.onDeviceDiscovered(device);
1474
- } catch (e) {
1475
- logger.error('Failed to update device interfaces', e);
1487
+ logger.log(`Refreshed device capabilities: ${JSON.stringify({ capabilities, abilities, support, presets })}`);
1476
1488
  }
1477
-
1478
- logger.log(`Refreshed device capabilities: ${JSON.stringify({ capabilities, abilities, support, presets })}`);
1479
1489
  }
1480
1490
  catch (e) {
1481
1491
  logger.error('Failed to refresh abilities', e);
package/src/connect.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { BaichuanClientOptions, ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
1
+ import type { BaichuanTransport as BaichuanTransportParent, BaichuanClientOptions, ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
2
 
3
- export type BaichuanTransport = "tcp" | "udp";
3
+ export type BaichuanTransport = BaichuanTransportParent;
4
4
 
5
5
  export type BaichuanConnectInputs = {
6
6
  host: string;
package/src/main.ts CHANGED
@@ -4,9 +4,10 @@ import { ReolinkNativeCamera } from "./camera";
4
4
  import { ReolinkNativeBatteryCamera } from "./camera-battery";
5
5
  import { CommonCameraMixin } from "./common";
6
6
  import { createBaichuanApi } from "./connect";
7
- import { ReolinkNativeMultiFocalDevice } from "./multifocal";
7
+ import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
8
8
  import { ReolinkNativeNvrDevice } from "./nvr";
9
9
  import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces, multifocalSuffix, nvrSuffix } from "./utils";
10
+ import { BaichuanTransport } from "./connect";
10
11
 
11
12
  class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
12
13
  devices = new Map<string, BaseBaichuanClass>();
@@ -73,7 +74,6 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
73
74
  name,
74
75
  interfaces: [
75
76
  ScryptedInterface.Settings,
76
- ScryptedInterface.DeviceDiscovery,
77
77
  ScryptedInterface.DeviceProvider,
78
78
  ScryptedInterface.Reboot,
79
79
  ],
@@ -89,6 +89,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
89
89
  device.storageSettings.values.username = username;
90
90
  device.storageSettings.values.password = password;
91
91
  device.storageSettings.values.uid = detection.uid || '';
92
+ device.storageSettings.values.protocol = detection.transport || 'tcp' as BaichuanTransport;
92
93
 
93
94
  return nativeId;
94
95
  }
@@ -237,7 +238,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
237
238
  } else if (nativeId.endsWith(nvrSuffix)) {
238
239
  return new ReolinkNativeNvrDevice(nativeId, this);
239
240
  } else if (nativeId.endsWith(multifocalSuffix)) {
240
- return new ReolinkNativeMultiFocalDevice(nativeId, this, 'tcp');
241
+ return new ReolinkNativeMultiFocalDevice(nativeId, this);
241
242
  } else {
242
243
  return new ReolinkNativeCamera(nativeId, this);
243
244
  }
@@ -0,0 +1,371 @@
1
+ import type { DeviceCapabilities, DualLensChannelAnalysis, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
+ import sdk, { Device, DeviceProvider, Reboot, ScryptedDeviceType, Setting, Settings, SettingValue } from "@scrypted/sdk";
3
+ import { StorageSettings } from "@scrypted/sdk/storage-settings";
4
+ import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
5
+ import { ReolinkNativeCamera } from "./camera";
6
+ import { ReolinkNativeBatteryCamera } from "./camera-battery";
7
+ import { normalizeUid } from "./connect";
8
+ import ReolinkNativePlugin from "./main";
9
+ import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces, updateDeviceInfo } from "./utils";
10
+
11
+ export class ReolinkNativeMultiFocalDevice extends BaseBaichuanClass implements Settings, DeviceProvider, Reboot {
12
+ storageSettings = new StorageSettings(this, {
13
+ debugEvents: {
14
+ title: 'Debug Events',
15
+ type: 'boolean',
16
+ immediate: true,
17
+ },
18
+ ipAddress: {
19
+ title: 'IP address',
20
+ type: 'string',
21
+ onPut: async () => await this.reinit()
22
+ },
23
+ username: {
24
+ title: 'Username',
25
+ placeholder: 'admin',
26
+ defaultValue: 'admin',
27
+ type: 'string',
28
+ onPut: async () => await this.reinit()
29
+ },
30
+ password: {
31
+ title: 'Password',
32
+ type: 'password',
33
+ onPut: async () => await this.reinit()
34
+ },
35
+ uid: {
36
+ title: 'UID',
37
+ description: 'Reolink UID (required for UDP/battery multi-focal devices)',
38
+ type: 'string',
39
+ hide: true,
40
+ onPut: async () => await this.reinit()
41
+ },
42
+ protocol: {
43
+ type: 'string',
44
+ hide: true,
45
+ },
46
+ diagnosticsRun: {
47
+ subgroup: 'Diagnostics',
48
+ title: 'Run Diagnostics',
49
+ description: 'Collect diagnostics and display results in logs.',
50
+ type: 'button',
51
+ immediate: true,
52
+ onPut: async () => {
53
+ await this.runDiagnostics();
54
+ },
55
+ },
56
+ multifocalInfo: {
57
+ json: true,
58
+ hide: true,
59
+ },
60
+ capabilities: {
61
+ json: true,
62
+ hide: true,
63
+ }
64
+ });
65
+
66
+ plugin: ReolinkNativePlugin;
67
+ cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
68
+ private channelToNativeIdMap = new Map<number, string>();
69
+ private initReinitTimeout: NodeJS.Timeout | undefined;
70
+ isBattery: boolean;
71
+
72
+ constructor(nativeId: string, plugin: ReolinkNativePlugin) {
73
+ super(nativeId);
74
+ this.plugin = plugin;
75
+
76
+ this.isBattery = this.storageSettings.values.protocol === 'udp';
77
+
78
+ this.scheduleInit();
79
+ }
80
+
81
+ async reboot(): Promise<void> {
82
+ const api = await this.ensureBaichuanClient();
83
+ await api.reboot();
84
+ }
85
+
86
+ protected getConnectionConfig(): BaichuanConnectionConfig {
87
+ const { ipAddress, username, password, uid } = this.storageSettings.values;
88
+ if (!ipAddress || !username || !password) {
89
+ throw new Error('Missing device credentials');
90
+ }
91
+
92
+ const { protocol } = this.storageSettings.values;
93
+
94
+ const normalizedUid = this.isBattery ? normalizeUid(uid) : undefined;
95
+
96
+ if (protocol === 'udp' && !normalizedUid) {
97
+ throw new Error('UID is required for UDP multi-focal devices (BCUDP)');
98
+ }
99
+
100
+ return {
101
+ host: ipAddress,
102
+ username,
103
+ password,
104
+ uid: normalizedUid,
105
+ transport: protocol,
106
+ logger: this.console,
107
+ };
108
+ }
109
+
110
+ protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
111
+ return {
112
+ onError: undefined, // Use default error handling
113
+ onClose: async () => {
114
+ // Reinit after cleanup
115
+ await this.reinit();
116
+ if (!this.isBattery) {
117
+ setTimeout(async () => {
118
+ try {
119
+ await this.subscribeToEvents();
120
+ } catch (e) {
121
+ const logger = this.getBaichuanLogger();
122
+ logger.warn('Failed to resubscribe to events after reconnection', e);
123
+ }
124
+ }, 1000);
125
+ }
126
+ },
127
+ onSimpleEvent: (ev) => this.forwardNativeEvent(ev),
128
+ getEventSubscriptionEnabled: () => true,
129
+ };
130
+ }
131
+
132
+ protected async onBeforeCleanup(): Promise<void> {
133
+ await this.unsubscribeFromAllEvents();
134
+ }
135
+
136
+ protected isDebugEnabled(): boolean {
137
+ return this.storageSettings.values.debugEvents;
138
+ }
139
+
140
+ protected getDeviceName(): string {
141
+ return this.name || 'Multi-Focal Device';
142
+ }
143
+
144
+ async reinit(): Promise<void> {
145
+ // Cancel any pending init/reinit
146
+ if (this.initReinitTimeout) {
147
+ clearTimeout(this.initReinitTimeout);
148
+ this.initReinitTimeout = undefined;
149
+ }
150
+
151
+ // Schedule reinit with debounce
152
+ this.scheduleInit(true);
153
+ }
154
+
155
+ private scheduleInit(isReinit: boolean = false): void {
156
+ // Cancel any pending init/reinit
157
+ if (this.initReinitTimeout) {
158
+ clearTimeout(this.initReinitTimeout);
159
+ }
160
+
161
+ this.initReinitTimeout = setTimeout(async () => {
162
+ const logger = this.getBaichuanLogger();
163
+ if (isReinit) {
164
+ logger.log('Reinitializing multi-focal device...');
165
+ await this.cleanupBaichuanApi();
166
+ }
167
+ await this.init();
168
+ this.initReinitTimeout = undefined;
169
+ }, isReinit ? 500 : 2000);
170
+ }
171
+
172
+ async init(): Promise<void> {
173
+ const logger = this.getBaichuanLogger();
174
+ try {
175
+ this.storageSettings.settings.uid.hide = !this.isBattery;
176
+
177
+ await this.ensureBaichuanClient();
178
+ await this.updateDeviceInfo();
179
+ await this.reportDevices();
180
+ await this.subscribeToEvents();
181
+ } catch (e) {
182
+ logger.error('Failed to initialize multi-focal device', e);
183
+ if (e instanceof Error) {
184
+ logger.error(`Error message: ${e.message}`);
185
+ logger.error(`Error stack: ${e.stack}`);
186
+ } else {
187
+ logger.error(`Error details: ${JSON.stringify(e)}`);
188
+ }
189
+ }
190
+ }
191
+
192
+ async updateDeviceInfo(): Promise<void> {
193
+ const logger = this.getBaichuanLogger();
194
+ try {
195
+ const api = await this.ensureBaichuanClient();
196
+ const deviceData = await api.getInfo();
197
+
198
+ await updateDeviceInfo({
199
+ device: this,
200
+ deviceData,
201
+ ipAddress: this.storageSettings.values.ipAddress,
202
+ });
203
+
204
+ logger.log(`Device info updated: ${JSON.stringify(deviceData)}`);
205
+ } catch (e) {
206
+ logger.warn('Failed to fetch device info', e);
207
+ }
208
+ }
209
+
210
+ getInterfaces(channel: number) {
211
+ const logger = this.getBaichuanLogger();
212
+ const { capabilities: caps, multifocalInfo } = this.storageSettings.values;
213
+ const channelInfo = (multifocalInfo as DualLensChannelAnalysis).channels.find(c => c.channel === channel);
214
+
215
+ const capabilities: DeviceCapabilities = {
216
+ ...caps,
217
+ hasPan: channelInfo.hasPan,
218
+ hasTilt: channelInfo.hasTilt,
219
+ hasZoom: channelInfo.hasZoom,
220
+ hasPresets: channelInfo.hasPresets,
221
+ hasIntercom: channelInfo.hasIntercom,
222
+ };
223
+
224
+ const { interfaces } = getDeviceInterfaces({
225
+ capabilities,
226
+ logger,
227
+ });
228
+
229
+ return { interfaces, capabilities };
230
+ }
231
+
232
+ async reportDevices(): Promise<void> {
233
+ const api = await this.ensureBaichuanClient();
234
+ const logger = this.getBaichuanLogger();
235
+ const { username, password, ipAddress, uid } = this.storageSettings.values;
236
+
237
+ const { capabilities, support, abilities, features, objects, presets } = await api.getDeviceCapabilities();
238
+
239
+ const multifocalInfo = await api.getDualLensChannelInfo();
240
+ logger.log(`Sync entities from remote for ${multifocalInfo.channels.length} channels`);
241
+
242
+ this.storageSettings.values.multifocalInfo = multifocalInfo;
243
+ this.storageSettings.values.capabilities = capabilities;
244
+
245
+ logger.debug(`Multichannel info: ${JSON.stringify({ multifocalInfo, capabilities, support, abilities, features, objects, presets })}`);
246
+
247
+ for (const channelInfo of multifocalInfo?.channels ?? []) {
248
+ const { channel, lensType } = channelInfo;
249
+
250
+ const name = `${this.name} - ${lensType}`;
251
+ const nativeId = this.buildNativeId(channel);
252
+
253
+ this.channelToNativeIdMap.set(channel, nativeId);
254
+ const { interfaces, capabilities: deviceCapabilities } = this.getInterfaces(channel);
255
+
256
+ const device: Device = {
257
+ providerNativeId: this.nativeId,
258
+ name,
259
+ nativeId,
260
+ info: {
261
+ ...this.info,
262
+ metadata: {
263
+ channel,
264
+ lensType
265
+ }
266
+ },
267
+ interfaces,
268
+ type: ScryptedDeviceType.Camera,
269
+ };
270
+
271
+ await sdk.deviceManager.onDeviceDiscovered(device);
272
+
273
+ const camera = await this.getDevice(nativeId);
274
+
275
+ camera.storageSettings.values.rtspChannel = channel;
276
+ camera.classes = objects;
277
+ camera.presets = presets;
278
+ camera.storageSettings.values.username = username;
279
+ camera.storageSettings.values.password = password;
280
+ camera.storageSettings.values.ipAddress = ipAddress;
281
+ camera.storageSettings.values.capabilities = deviceCapabilities;
282
+ if (this.isBattery) {
283
+ camera.storageSettings.values.uid = uid;
284
+ }
285
+ }
286
+ }
287
+
288
+ async getDevice(nativeId: string) {
289
+ let device = this.cameraNativeMap.get(nativeId);
290
+ if (!device) {
291
+ if (nativeId.endsWith(batteryCameraSuffix)) {
292
+ device = new ReolinkNativeBatteryCamera(nativeId, this.plugin, undefined, this);
293
+ } else {
294
+ device = new ReolinkNativeCamera(nativeId, this.plugin, undefined, this);
295
+ }
296
+ }
297
+
298
+ return device;
299
+ }
300
+
301
+ async getSettings(): Promise<Setting[]> {
302
+ const settings = await this.storageSettings.getSettings();
303
+ return settings;
304
+ }
305
+
306
+ async putSetting(key: string, value: SettingValue): Promise<void> {
307
+ return this.storageSettings.putSetting(key, value);
308
+ }
309
+
310
+ async releaseDevice(id: string, nativeId: string) {
311
+ this.cameraNativeMap.delete(nativeId);
312
+ }
313
+
314
+ buildNativeId(channel: number): string {
315
+ const { protocol } = this.storageSettings.values;
316
+ return `${this.nativeId}-channel${channel}${protocol === "udp" ? batteryCameraSuffix : cameraSuffix}`;
317
+ }
318
+
319
+ forwardNativeEvent(ev: ReolinkSimpleEvent): void {
320
+ const logger = this.getBaichuanLogger();
321
+ const channel = ev?.channel;
322
+
323
+ if (channel === undefined) {
324
+ logger.debug('Event missing channel, ignoring');
325
+ return;
326
+ }
327
+
328
+ const nativeId = this.channelToNativeIdMap.get(channel);
329
+ if (!nativeId) {
330
+ logger.debug(`No camera found for channel ${channel}, ignoring event`);
331
+ return;
332
+ }
333
+
334
+ const camera = this.cameraNativeMap.get(nativeId);
335
+ if (!camera) {
336
+ logger.debug(`Camera ${nativeId} not yet initialized, ignoring event`);
337
+ return;
338
+ }
339
+
340
+ // Forward event to camera
341
+ if (camera.onSimpleEvent) {
342
+ camera.onSimpleEvent(ev);
343
+ }
344
+ }
345
+ async unsubscribeFromAllEvents(): Promise<void> {
346
+ await super.unsubscribeFromEvents();
347
+ }
348
+
349
+ private async runDiagnostics(): Promise<void> {
350
+ const logger = this.getBaichuanLogger();
351
+ logger.log(`Starting Multifocal diagnostics...`);
352
+
353
+ try {
354
+ const { ipAddress, username, password } = this.storageSettings.values;
355
+ if (!ipAddress || !username || !password) {
356
+ throw new Error('Missing device credentials');
357
+ }
358
+
359
+ const api = await this.ensureBaichuanClient();
360
+
361
+ const multifocalDiagnostics = await api.collectMultifocalDiagnostics(logger);
362
+
363
+ logger.log(`NVR diagnostics completed successfully.`);
364
+ logger.log(JSON.stringify(multifocalDiagnostics));
365
+ } catch (e) {
366
+ logger.error('Failed to run NVR diagnostics', e);
367
+ throw e;
368
+ }
369
+ }
370
+ }
371
+
package/src/nvr.ts CHANGED
@@ -124,12 +124,10 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
124
124
  }
125
125
 
126
126
  protected async onBeforeCleanup(): Promise<void> {
127
- // Unsubscribe from events if needed
128
127
  await this.unsubscribeFromAllEvents();
129
128
  }
130
129
 
131
130
  async reinit() {
132
- // Cancel any pending init/reinit
133
131
  if (this.initReinitTimeout) {
134
132
  clearTimeout(this.initReinitTimeout);
135
133
  this.initReinitTimeout = undefined;
@@ -307,7 +305,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
307
305
  if (eventSource !== 'Native') {
308
306
  await this.unsubscribeFromAllEvents();
309
307
  } else {
310
-
311
308
  this.subscribeToAllEvents().catch((e) => {
312
309
  logger.warn('Failed to subscribe to Native events', e);
313
310
  });
@@ -343,7 +340,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
343
340
 
344
341
  await this.updateDeviceInfo();
345
342
 
346
- // Initialize event subscriptions based on selected source
347
343
  await this.reinitEventSubscriptions();
348
344
 
349
345
  setInterval(async () => {