@apocaliss92/scrypted-reolink-native 0.1.7 → 0.1.9

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.7",
3
+ "version": "0.1.9",
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",
@@ -36,10 +36,15 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
36
36
  }
37
37
 
38
38
  async takePicture(options?: RequestPictureOptions): Promise<MediaObject> {
39
- // const { snapshotCacheMinutes = 5 } = this.storageSettings.values;
40
- // const cacheMs = snapshotCacheMinutes * 60_000;
41
- // if (!this.forceNewSnapshot && cacheMs > 0 && this.lastPicture && Date.now() - this.lastPicture.atMs < cacheMs) {
42
- if (!this.forceNewSnapshot && this.lastPicture) {
39
+ // Allow new snapshot if:
40
+ // 1. forceNewSnapshot is true, OR
41
+ // 2. Camera is awake AND last snapshot was taken at least 10 seconds ago
42
+ const minSnapshotIntervalMs = 10_000; // 10 seconds
43
+ const now = Date.now();
44
+ const shouldTakeNewSnapshot = this.forceNewSnapshot ||
45
+ (!this.sleeping && this.lastPicture && (now - this.lastPicture.atMs >= minSnapshotIntervalMs));
46
+
47
+ if (!shouldTakeNewSnapshot && this.lastPicture) {
43
48
  this.console.log(`Returning cached snapshot, taken at ${new Date(this.lastPicture.atMs).toLocaleString()}`);
44
49
  return this.lastPicture.mo;
45
50
  }
package/src/camera.ts CHANGED
@@ -209,7 +209,7 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
209
209
  try {
210
210
  return this.withBaichuanRetry(async () => {
211
211
  const client = await this.ensureClient();
212
- const snapshotBuffer = await client.getSnapshot();
212
+ const snapshotBuffer = await client.getSnapshot(this.storageSettings.values.rtspChannel);
213
213
  const mo = await this.createMediaObject(snapshotBuffer, 'image/jpeg');
214
214
 
215
215
  return mo;
package/src/common.ts CHANGED
@@ -487,6 +487,8 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
487
487
  protected ensureClientPromise: Promise<ReolinkBaichuanApi> | undefined;
488
488
  protected connectionTime: number | undefined;
489
489
  private closeListener?: () => void;
490
+ private lastDisconnectTime: number = 0;
491
+ private readonly reconnectBackoffMs: number = 2000; // 2 seconds minimum between reconnects
490
492
  protected readonly protocol: BaichuanTransport;
491
493
  private debugLogsResetTimeout: NodeJS.Timeout | undefined;
492
494
 
@@ -1329,6 +1331,11 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1329
1331
 
1330
1332
  // Client management
1331
1333
  async ensureClient(): Promise<ReolinkBaichuanApi> {
1334
+ // If camera is connected to NVR, use NVR's shared Baichuan connection
1335
+ if (this.nvrDevice) {
1336
+ return await this.nvrDevice.ensureBaichuanClient();
1337
+ }
1338
+
1332
1339
  // Reuse existing client if socket is still connected and logged in
1333
1340
  if (this.baichuanApi && this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
1334
1341
  return this.baichuanApi;
@@ -1337,6 +1344,17 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1337
1344
  // Prevent concurrent login storms
1338
1345
  if (this.ensureClientPromise) return await this.ensureClientPromise;
1339
1346
 
1347
+ // Apply backoff to avoid aggressive reconnection after disconnection
1348
+ if (this.lastDisconnectTime > 0) {
1349
+ const timeSinceDisconnect = Date.now() - this.lastDisconnectTime;
1350
+ if (timeSinceDisconnect < this.reconnectBackoffMs) {
1351
+ const waitTime = this.reconnectBackoffMs - timeSinceDisconnect;
1352
+ const logger = this.getLogger();
1353
+ logger.log(`[BaichuanClient] Waiting ${waitTime}ms before reconnection (backoff)`);
1354
+ await new Promise(resolve => setTimeout(resolve, waitTime));
1355
+ }
1356
+ }
1357
+
1340
1358
  this.ensureClientPromise = (async () => {
1341
1359
  const { ipAddress, username, password, uid } = this.storageSettings.values;
1342
1360
 
@@ -1355,11 +1373,8 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1355
1373
 
1356
1374
  const isConnected = this.baichuanApi.client.isSocketConnected();
1357
1375
  if (!isConnected) {
1358
- // Socket is closed, clean up
1359
1376
  try {
1360
- if (this.onSimpleEvent) {
1361
- this.baichuanApi.offSimpleEvent(this.onSimpleEvent);
1362
- }
1377
+ this.baichuanApi.offSimpleEvent(this.onSimpleEvent);
1363
1378
  }
1364
1379
  catch {
1365
1380
  // ignore
@@ -1425,21 +1440,45 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1425
1440
  this.closeListener = () => {
1426
1441
  const logger = this.getLogger();
1427
1442
  if (this.baichuanApi === api) {
1428
- logger.log(`[BaichuanClient] Socket closed, resetting client state for reconnection`);
1443
+ const now = Date.now();
1444
+ const timeSinceLastDisconnect = now - this.lastDisconnectTime;
1445
+ this.lastDisconnectTime = now;
1446
+
1447
+ logger.log(`[BaichuanClient] Socket closed, resetting client state for reconnection (last disconnect ${timeSinceLastDisconnect}ms ago)`);
1448
+
1449
+ // Reset client state
1429
1450
  this.baichuanApi = undefined;
1430
1451
  this.ensureClientPromise = undefined;
1431
1452
  this.closeListener = undefined;
1453
+
1454
+ // Remove event handler to prevent operations during reconnection
1455
+ try {
1456
+ if (this.onSimpleEvent) {
1457
+ api.offSimpleEvent(this.onSimpleEvent);
1458
+ }
1459
+ }
1460
+ catch {
1461
+ // ignore
1462
+ }
1432
1463
  }
1433
1464
  };
1434
1465
  api.client.on("close", this.closeListener);
1435
1466
 
1436
1467
  // Re-attach event handler if enabled
1468
+ // Note: We don't reattach here immediately to avoid operations being called
1469
+ // during reconnection. subscribeToEvents() will be called when needed.
1470
+ // However, if events were already subscribed, we need to reattach them.
1471
+ // We'll let subscribeToEvents() handle this, but we can also try here if needed.
1437
1472
  if (this.isEventDispatchEnabled?.() && this.onSimpleEvent) {
1438
1473
  try {
1439
- api.onSimpleEvent(this.onSimpleEvent);
1474
+ // Verify connection is fully ready before subscribing
1475
+ if (api.client.isSocketConnected() && api.client.loggedIn) {
1476
+ api.onSimpleEvent(this.onSimpleEvent);
1477
+ }
1440
1478
  }
1441
- catch {
1442
- // ignore
1479
+ catch (e) {
1480
+ const logger = this.getLogger();
1481
+ logger.warn(`[BaichuanClient] Failed to reattach event handler after reconnection, will retry via subscribeToEvents()`, e);
1443
1482
  }
1444
1483
  }
1445
1484
  return api;
package/src/nvr.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DeviceInfoResponse, DeviceInputData, ReolinkCgiApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
1
+ import type { DeviceInfoResponse, DeviceInputData, EventsResponse, ReolinkBaichuanApi, ReolinkCgiApi, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
2
  import sdk, { AdoptDevice, Device, DeviceDiscovery, DeviceProvider, DiscoveredDevice, Reboot, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
3
3
  import { StorageSettings } from "@scrypted/sdk/storage-settings";
4
4
  import { ReolinkNativeCamera } from "./camera";
@@ -14,6 +14,17 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
14
14
  type: 'boolean',
15
15
  immediate: true,
16
16
  },
17
+ eventSource: {
18
+ title: 'Event Source',
19
+ description: 'Select the source for camera events: Native (Baichuan) or CGI (HTTP polling)',
20
+ type: 'string',
21
+ choices: ['Native', 'CGI'],
22
+ defaultValue: 'Native',
23
+ immediate: true,
24
+ onPut: async () => {
25
+ await this.reinitEventSubscriptions();
26
+ }
27
+ },
17
28
  ipAddress: {
18
29
  title: 'IP address',
19
30
  type: 'string',
@@ -34,6 +45,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
34
45
  });
35
46
  plugin: ReolinkNativePlugin;
36
47
  nvrApi: ReolinkCgiApi | undefined;
48
+ baichuanApi: ReolinkBaichuanApi | undefined;
49
+ baichuanApiPromise: Promise<ReolinkBaichuanApi> | undefined;
37
50
  discoveredDevices = new Map<string, {
38
51
  device: Device;
39
52
  description: string;
@@ -44,7 +57,11 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
44
57
  lastErrorsCheck: number | undefined;
45
58
  lastDevicesStatusCheck: number | undefined;
46
59
  cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
60
+ private channelToNativeIdMap = new Map<number, string>();
47
61
  processing = false;
62
+ private eventSubscriptionActive = false;
63
+ private errorListener?: (err: unknown) => void;
64
+ private closeListener?: () => void;
48
65
 
49
66
  constructor(nativeId: string, plugin: ReolinkNativePlugin) {
50
67
  super(nativeId);
@@ -64,7 +81,98 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
64
81
  return this.console;
65
82
  }
66
83
 
84
+ /**
85
+ * Centralized cleanup method for Baichuan API
86
+ * Removes all listeners, closes connection, and resets state
87
+ */
88
+ private async cleanupBaichuanApi(): Promise<void> {
89
+ if (!this.baichuanApi) {
90
+ return;
91
+ }
92
+
93
+ const api = this.baichuanApi;
94
+
95
+ // Unsubscribe from events first
96
+ await this.unsubscribeFromAllEvents();
97
+
98
+ // Remove all listeners
99
+ try {
100
+ api.client.off("close", this.closeListener);
101
+ } catch {
102
+ // ignore
103
+ }
104
+
105
+ try {
106
+ api.client.off("error", this.errorListener);
107
+ } catch {
108
+ // ignore
109
+ }
110
+
111
+ // Close connection if still connected
112
+ try {
113
+ if (api.client.isSocketConnected()) {
114
+ await api.close();
115
+ }
116
+ } catch {
117
+ // ignore
118
+ }
119
+
120
+ // Reset state
121
+ this.baichuanApi = undefined;
122
+ this.baichuanApiPromise = undefined;
123
+ }
124
+
125
+ /**
126
+ * Attach error and close listeners to Baichuan API
127
+ */
128
+ private attachBaichuanListeners(api: ReolinkBaichuanApi): void {
129
+ const logger = this.getLogger();
130
+
131
+ // Error listener
132
+ this.errorListener = (err: unknown) => {
133
+ const msg = (err as any)?.message || (err as any)?.toString?.() || String(err);
134
+
135
+ // Only log if it's not a recoverable error to avoid spam
136
+ if (typeof msg === 'string' && (
137
+ msg.includes('Baichuan socket closed') ||
138
+ msg.includes('Baichuan UDP stream closed') ||
139
+ msg.includes('Not running')
140
+ )) {
141
+ logger.debug(`[NVR BaichuanClient] error (recoverable): ${msg}`);
142
+ return;
143
+ }
144
+ logger.error(`[NVR BaichuanClient] error: ${msg}`);
145
+ };
146
+
147
+ // Close listener
148
+ this.closeListener = async () => {
149
+ try {
150
+ const wasConnected = api.client.isSocketConnected();
151
+ const wasLoggedIn = api.client.loggedIn;
152
+ logger.log(`[NVR BaichuanClient] Connection state before close: connected=${wasConnected}, loggedIn=${wasLoggedIn}`);
153
+
154
+ // Try to get last message info if available
155
+ const client = api.client as any;
156
+ if (client?.lastRx || client?.lastTx) {
157
+ logger.log(`[NVR BaichuanClient] Last message info: lastRx=${JSON.stringify(client.lastRx)}, lastTx=${JSON.stringify(client.lastTx)}`);
158
+ }
159
+ }
160
+ catch (e) {
161
+ logger.debug(`[NVR BaichuanClient] Could not get connection state: ${e}`);
162
+ }
163
+
164
+ // Cleanup and reinit
165
+ await this.cleanupBaichuanApi();
166
+ await this.reinit();
167
+ };
168
+
169
+ // Attach listeners
170
+ api.client.on("error", this.errorListener);
171
+ api.client.on("close", this.closeListener);
172
+ }
173
+
67
174
  async reinit() {
175
+ // Cleanup CGI API
68
176
  if (this.nvrApi) {
69
177
  try {
70
178
  await this.nvrApi.logout();
@@ -73,6 +181,9 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
73
181
  }
74
182
  }
75
183
  this.nvrApi = undefined;
184
+
185
+ // Cleanup Baichuan API (this handles all listeners and connection)
186
+ await this.cleanupBaichuanApi();
76
187
  }
77
188
 
78
189
  async ensureClient(): Promise<ReolinkCgiApi> {
@@ -96,11 +207,242 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
96
207
  return this.nvrApi;
97
208
  }
98
209
 
210
+ private forwardNativeEvent(ev: ReolinkSimpleEvent): void {
211
+ const logger = this.getLogger();
212
+
213
+ const eventSource = this.storageSettings.values.eventSource || 'Native';
214
+ if (eventSource !== 'Native') {
215
+ return;
216
+ }
217
+
218
+ try {
219
+ if (this.storageSettings.values.debugEvents) {
220
+ logger.log(`NVR Baichuan event: ${JSON.stringify(ev)}`);
221
+ }
222
+
223
+ // Find camera for this channel
224
+ const channel = ev?.channel;
225
+ if (channel === undefined) {
226
+ if (this.storageSettings.values.debugEvents) {
227
+ logger.debug('Event has no channel, ignoring');
228
+ }
229
+ return;
230
+ }
231
+
232
+ const nativeId = this.channelToNativeIdMap.get(channel);
233
+ const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
234
+
235
+ if (!targetCamera) {
236
+ if (this.storageSettings.values.debugEvents) {
237
+ logger.debug(`No camera found for channel ${channel}, ignoring event`);
238
+ }
239
+ return;
240
+ }
241
+
242
+ // Convert event to camera's processEvents format
243
+ const objects: string[] = [];
244
+ let motion = false;
245
+
246
+ switch (ev?.type) {
247
+ case 'motion':
248
+ motion = true;
249
+ break;
250
+ case 'doorbell':
251
+ // Handle doorbell if camera supports it
252
+ try {
253
+ if (typeof (targetCamera as any).handleDoorbellEvent === 'function') {
254
+ (targetCamera as any).handleDoorbellEvent();
255
+ }
256
+ }
257
+ catch (e) {
258
+ logger.warn(`Error handling doorbell event for camera channel ${channel}`, e);
259
+ }
260
+ motion = true;
261
+ break;
262
+ case 'people':
263
+ case 'vehicle':
264
+ case 'animal':
265
+ case 'face':
266
+ case 'package':
267
+ case 'other':
268
+ objects.push(ev.type);
269
+ motion = true;
270
+ break;
271
+ default:
272
+ if (this.storageSettings.values.debugEvents) {
273
+ logger.debug(`Unknown event type: ${ev?.type}`);
274
+ }
275
+ return;
276
+ }
277
+
278
+ // Process events on the target camera
279
+ targetCamera.processEvents({ motion, objects }).catch((e) => {
280
+ logger.warn(`Error processing events for camera channel ${channel}`, e);
281
+ });
282
+ }
283
+ catch (e) {
284
+ logger.warn('Error in NVR Native event forwarder', e);
285
+ }
286
+ }
287
+
288
+ async onSimpleEventHandler(ev: ReolinkSimpleEvent) {
289
+ this.forwardNativeEvent(ev);
290
+ }
291
+
292
+ async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
293
+ // Reuse existing client if socket is still connected and logged in
294
+ if (this.baichuanApi && this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
295
+ return this.baichuanApi;
296
+ }
297
+
298
+ // Prevent concurrent login storms
299
+ if (this.baichuanApiPromise) return await this.baichuanApiPromise;
300
+
301
+ this.baichuanApiPromise = (async () => {
302
+ const { ipAddress, username, password } = this.storageSettings.values;
303
+ if (!ipAddress || !username || !password) {
304
+ throw new Error('Missing NVR credentials');
305
+ }
306
+
307
+ // Clean up old client if exists
308
+ if (this.baichuanApi) {
309
+ await this.cleanupBaichuanApi();
310
+ }
311
+
312
+ // Create new Baichuan client
313
+ const { ReolinkBaichuanApi } = await import("@apocaliss92/reolink-baichuan-js");
314
+ this.baichuanApi = new ReolinkBaichuanApi({
315
+ host: ipAddress,
316
+ username,
317
+ password,
318
+ transport: 'tcp',
319
+ logger: this.getLogger(),
320
+ });
321
+
322
+ await this.baichuanApi.login();
323
+
324
+ // Verify socket is connected before returning
325
+ if (!this.baichuanApi.client.isSocketConnected()) {
326
+ throw new Error('Socket not connected after login');
327
+ }
328
+
329
+ // Attach listeners (error and close)
330
+ this.attachBaichuanListeners(this.baichuanApi);
331
+
332
+ return this.baichuanApi;
333
+ })();
334
+
335
+ try {
336
+ return await this.baichuanApiPromise;
337
+ }
338
+ finally {
339
+ // Allow future reconnects and avoid pinning rejected promises
340
+ this.baichuanApiPromise = undefined;
341
+ }
342
+ }
343
+
344
+ async subscribeToAllEvents(): Promise<void> {
345
+ const logger = this.getLogger();
346
+
347
+ // If already subscribed and connection is valid, return
348
+ if (this.eventSubscriptionActive && this.baichuanApi) {
349
+ if (this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
350
+ logger.log('Event subscription already active');
351
+ return;
352
+ }
353
+ // Connection is invalid, reset subscription state
354
+ this.eventSubscriptionActive = false;
355
+ }
356
+
357
+ // Unsubscribe first if handler exists (idempotent)
358
+ await this.unsubscribeFromAllEvents();
359
+
360
+ // Get Baichuan client connection
361
+ const api = await this.ensureBaichuanClient();
362
+
363
+ // Verify connection is ready
364
+ if (!api.client.isSocketConnected() || !api.client.loggedIn) {
365
+ logger.warn('Cannot subscribe to events: connection not ready');
366
+ return;
367
+ }
368
+
369
+ // Subscribe to events
370
+ try {
371
+ await api.onSimpleEvent(this.onSimpleEventHandler);
372
+ this.eventSubscriptionActive = true;
373
+ logger.log('Subscribed to all events for NVR cameras');
374
+ }
375
+ catch (e) {
376
+ logger.warn('Failed to subscribe to events', e);
377
+ this.eventSubscriptionActive = false;
378
+ }
379
+ }
380
+
381
+ async unsubscribeFromAllEvents(): Promise<void> {
382
+ const logger = this.getLogger();
383
+
384
+ // Only unsubscribe if we have an active subscription
385
+ if (this.eventSubscriptionActive && this.baichuanApi) {
386
+ try {
387
+ this.baichuanApi.offSimpleEvent(this.onSimpleEventHandler);
388
+ logger.log('Unsubscribed from all events');
389
+ }
390
+ catch (e) {
391
+ logger.warn('Error unsubscribing from events', e);
392
+ }
393
+ }
394
+
395
+ this.eventSubscriptionActive = false;
396
+ }
397
+
398
+ /**
399
+ * Reinitialize event subscriptions based on selected event source
400
+ */
401
+ private async reinitEventSubscriptions(): Promise<void> {
402
+ const logger = this.getLogger();
403
+ const { eventSource } = this.storageSettings.values;
404
+
405
+ // Unsubscribe from Native events if switching away
406
+ if (eventSource !== 'Native') {
407
+ await this.unsubscribeFromAllEvents();
408
+ } else {
409
+
410
+ this.subscribeToAllEvents().catch((e) => {
411
+ logger.warn('Failed to subscribe to Native events', e);
412
+ });
413
+ }
414
+
415
+ logger.log(`Event source set to: ${eventSource}`);
416
+ }
417
+
418
+ /**
419
+ * Forward events from CGI source to cameras
420
+ */
421
+ private forwardCgiEvents(eventsRes: Record<number, EventsResponse>): void {
422
+ const logger = this.getLogger();
423
+
424
+ if (this.storageSettings.values.debugEvents) {
425
+ logger.debug(`CGI Events call result: ${JSON.stringify(eventsRes)}`);
426
+ }
427
+
428
+ // Use channel map for efficient lookup
429
+ for (const [channel, nativeId] of this.channelToNativeIdMap.entries()) {
430
+ const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
431
+ const cameraEventsData = eventsRes[channel];
432
+ if (cameraEventsData && targetCamera) {
433
+ targetCamera.processEvents(cameraEventsData);
434
+ }
435
+ }
436
+ }
437
+
99
438
  async init() {
100
439
  const api = await this.ensureClient();
101
440
  const logger = this.getLogger();
102
441
  await this.updateDeviceInfo();
103
442
 
443
+ // Initialize event subscriptions based on selected source
444
+ await this.reinitEventSubscriptions();
445
+
104
446
  setInterval(async () => {
105
447
  if (this.processing || !api) {
106
448
  return;
@@ -127,21 +469,14 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
127
469
  await this.discoverDevices(true);
128
470
  }
129
471
 
130
- const eventsRes = await api.getAllChannelsEvents();
131
-
132
- if (this.storageSettings.values.debugEvents) {
133
- logger.debug(`Events call result: ${JSON.stringify(eventsRes)}`);
472
+ // Only fetch and forward CGI events if CGI is selected as event source
473
+ const { eventSource } = this.storageSettings.values;
474
+ if (eventSource === 'CGI') {
475
+ const eventsRes = await api.getAllChannelsEvents();
476
+ this.forwardCgiEvents(eventsRes.parsed);
134
477
  }
135
- this.cameraNativeMap.forEach((camera) => {
136
- if (camera) {
137
- const channel = camera.storageSettings.values.rtspChannel;
138
- const cameraEventsData = eventsRes?.parsed[channel];
139
- if (cameraEventsData) {
140
- camera.processEvents(cameraEventsData);
141
- }
142
- }
143
- });
144
478
 
479
+ // Always fetch battery info (not event-related)
145
480
  const { batteryInfoData, response } = await api.getAllChannelsBatteryInfo();
146
481
 
147
482
  if (this.storageSettings.values.debugEvents) {
@@ -272,6 +607,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
272
607
  }
273
608
  };
274
609
 
610
+ this.channelToNativeIdMap.set(channel, nativeId);
611
+
275
612
  if (sdk.deviceManager.getNativeIds().includes(nativeId)) {
276
613
  continue;
277
614
  }