@apocaliss92/scrypted-reolink-native 0.1.8 → 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/src/nvr.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DeviceInfoResponse, DeviceInputData, ReolinkBaichuanApi, ReolinkCgiApi, ReolinkSimpleEvent } 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',
@@ -46,15 +57,11 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
46
57
  lastErrorsCheck: number | undefined;
47
58
  lastDevicesStatusCheck: number | undefined;
48
59
  cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
60
+ private channelToNativeIdMap = new Map<number, string>();
49
61
  processing = false;
50
62
  private eventSubscriptionActive = false;
51
- private onSimpleEventHandler?: (ev: ReolinkSimpleEvent) => void;
52
- private closeListener?: () => void;
53
63
  private errorListener?: (err: unknown) => void;
54
- private lastDisconnectTime: number = 0;
55
- private lastErrorBeforeClose: { error: string; timestamp: number } | undefined;
56
- private readonly reconnectBackoffMs: number = 2000; // 2 seconds minimum between reconnects
57
- private resubscribeTimeout?: NodeJS.Timeout;
64
+ private closeListener?: () => void;
58
65
 
59
66
  constructor(nativeId: string, plugin: ReolinkNativePlugin) {
60
67
  super(nativeId);
@@ -74,58 +81,109 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
74
81
  return this.console;
75
82
  }
76
83
 
77
- async reinit() {
78
- if (this.nvrApi) {
79
- try {
80
- await this.nvrApi.logout();
81
- } catch {
82
- // ignore
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;
84
91
  }
85
- this.nvrApi = undefined;
86
92
 
87
- // Clear any pending resubscribe timeout
88
- if (this.resubscribeTimeout) {
89
- clearTimeout(this.resubscribeTimeout);
90
- this.resubscribeTimeout = undefined;
91
- }
93
+ const api = this.baichuanApi;
92
94
 
93
95
  // Unsubscribe from events first
94
96
  await this.unsubscribeFromAllEvents();
95
97
 
96
- if (this.baichuanApi) {
97
- // Remove close listener
98
- if (this.closeListener) {
99
- try {
100
- this.baichuanApi.client.off("close", this.closeListener);
101
- }
102
- catch {
103
- // ignore
104
- }
105
- this.closeListener = undefined;
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();
106
115
  }
107
-
108
- // Remove error listener
109
- if (this.errorListener) {
110
- try {
111
- this.baichuanApi.client.off("error", this.errorListener);
112
- }
113
- catch {
114
- // ignore
115
- }
116
- this.errorListener = undefined;
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;
117
143
  }
144
+ logger.error(`[NVR BaichuanClient] error: ${msg}`);
145
+ };
118
146
 
147
+ // Close listener
148
+ this.closeListener = async () => {
119
149
  try {
120
- if (this.baichuanApi.client.isSocketConnected()) {
121
- await this.baichuanApi.close();
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)}`);
122
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
+
174
+ async reinit() {
175
+ // Cleanup CGI API
176
+ if (this.nvrApi) {
177
+ try {
178
+ await this.nvrApi.logout();
123
179
  } catch {
124
180
  // ignore
125
181
  }
126
182
  }
127
- this.baichuanApi = undefined;
128
- this.baichuanApiPromise = undefined;
183
+ this.nvrApi = undefined;
184
+
185
+ // Cleanup Baichuan API (this handles all listeners and connection)
186
+ await this.cleanupBaichuanApi();
129
187
  }
130
188
 
131
189
  async ensureClient(): Promise<ReolinkCgiApi> {
@@ -149,6 +207,88 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
149
207
  return this.nvrApi;
150
208
  }
151
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
+
152
292
  async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
153
293
  // Reuse existing client if socket is still connected and logged in
154
294
  if (this.baichuanApi && this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
@@ -166,36 +306,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
166
306
 
167
307
  // Clean up old client if exists
168
308
  if (this.baichuanApi) {
169
- // Remove close listener from old client
170
- if (this.closeListener) {
171
- try {
172
- this.baichuanApi.client.off("close", this.closeListener);
173
- }
174
- catch {
175
- // ignore
176
- }
177
- this.closeListener = undefined;
178
- }
179
-
180
- // Remove error listener from old client
181
- if (this.errorListener) {
182
- try {
183
- this.baichuanApi.client.off("error", this.errorListener);
184
- }
185
- catch {
186
- // ignore
187
- }
188
- this.errorListener = undefined;
189
- }
190
-
191
- try {
192
- if (this.baichuanApi.client.isSocketConnected()) {
193
- await this.baichuanApi.close();
194
- }
195
- }
196
- catch {
197
- // ignore
198
- }
309
+ await this.cleanupBaichuanApi();
199
310
  }
200
311
 
201
312
  // Create new Baichuan client
@@ -206,7 +317,6 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
206
317
  password,
207
318
  transport: 'tcp',
208
319
  logger: this.getLogger(),
209
- // rebootAfterDisconnectionsPerMinute: 5,
210
320
  });
211
321
 
212
322
  await this.baichuanApi.login();
@@ -216,125 +326,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
216
326
  throw new Error('Socket not connected after login');
217
327
  }
218
328
 
219
- // Listen for errors to understand why socket might close
220
- this.errorListener = (err: unknown) => {
221
- const logger = this.getLogger();
222
- const msg = (err as any)?.message || (err as any)?.toString?.() || String(err);
223
-
224
- // Store last error before close
225
- this.lastErrorBeforeClose = {
226
- error: msg,
227
- timestamp: Date.now()
228
- };
229
-
230
- // Only log if it's not a recoverable error to avoid spam
231
- if (typeof msg === 'string' && (
232
- msg.includes('Baichuan socket closed') ||
233
- msg.includes('Baichuan UDP stream closed') ||
234
- msg.includes('Not running')
235
- )) {
236
- // Log even recoverable errors for debugging
237
- logger.debug(`[NVR BaichuanClient] error (recoverable): ${msg}`);
238
- return;
239
- }
240
- logger.error(`[NVR BaichuanClient] error: ${msg}`);
241
- };
242
- this.baichuanApi.client.on("error", this.errorListener);
243
-
244
- // Listen for socket disconnection to reset client state
245
- this.closeListener = () => {
246
- const logger = this.getLogger();
247
- const now = Date.now();
248
- const timeSinceLastDisconnect = now - this.lastDisconnectTime;
249
- this.lastDisconnectTime = now;
250
-
251
- // Log detailed information about the close
252
- const errorInfo = this.lastErrorBeforeClose
253
- ? ` (last error: ${this.lastErrorBeforeClose.error} at ${new Date(this.lastErrorBeforeClose.timestamp).toISOString()}, ${now - this.lastErrorBeforeClose.timestamp}ms before close)`
254
- : '';
255
-
256
- logger.log(`[NVR BaichuanClient] Socket closed, resetting client state (last disconnect ${timeSinceLastDisconnect}ms ago)${errorInfo}`);
257
-
258
- // Log connection state before close
259
- try {
260
- const wasConnected = this.baichuanApi?.client.isSocketConnected();
261
- const wasLoggedIn = this.baichuanApi?.client.loggedIn;
262
- logger.log(`[NVR BaichuanClient] Connection state before close: connected=${wasConnected}, loggedIn=${wasLoggedIn}`);
263
-
264
- // Try to get last message info if available
265
- const client = this.baichuanApi?.client as any;
266
- if (client?.lastRx || client?.lastTx) {
267
- logger.log(`[NVR BaichuanClient] Last message info: lastRx=${JSON.stringify(client.lastRx)}, lastTx=${JSON.stringify(client.lastTx)}`);
268
- }
269
- }
270
- catch (e) {
271
- logger.debug(`[NVR BaichuanClient] Could not get connection state: ${e}`);
272
- }
273
-
274
- // Clear any pending resubscribe timeout
275
- if (this.resubscribeTimeout) {
276
- clearTimeout(this.resubscribeTimeout);
277
- this.resubscribeTimeout = undefined;
278
- }
279
-
280
- const wasSubscribed = this.eventSubscriptionActive;
281
- const api = this.baichuanApi; // Save reference before clearing
282
-
283
- // Reset state
284
- this.baichuanApi = undefined;
285
- this.baichuanApiPromise = undefined;
286
- this.eventSubscriptionActive = false;
287
- this.onSimpleEventHandler = undefined;
288
-
289
- // Remove event handler from closed client
290
- if (api && this.onSimpleEventHandler) {
291
- try {
292
- api.offSimpleEvent(this.onSimpleEventHandler);
293
- }
294
- catch {
295
- // ignore
296
- }
297
- }
298
-
299
- // Remove close listener (it will be re-added on next connection)
300
- if (api && this.closeListener) {
301
- try {
302
- api.client.off("close", this.closeListener);
303
- }
304
- catch {
305
- // ignore
306
- }
307
- }
308
-
309
- // Remove error listener
310
- if (api && this.errorListener) {
311
- try {
312
- api.client.off("error", this.errorListener);
313
- }
314
- catch {
315
- // ignore
316
- }
317
- }
318
-
319
- this.closeListener = undefined;
320
- this.errorListener = undefined;
321
- this.lastErrorBeforeClose = undefined;
322
-
323
- // Try to resubscribe when connection is restored (async, don't block)
324
- // Only if we had an active subscription and enough time has passed
325
- if (wasSubscribed && timeSinceLastDisconnect >= this.reconnectBackoffMs) {
326
- this.resubscribeTimeout = setTimeout(async () => {
327
- this.resubscribeTimeout = undefined;
328
- try {
329
- await this.subscribeToAllEvents();
330
- }
331
- catch (e) {
332
- logger.warn('Failed to resubscribe to events after reconnection', e);
333
- }
334
- }, this.reconnectBackoffMs); // Wait for backoff period before resubscribing
335
- }
336
- };
337
- this.baichuanApi.client.on("close", this.closeListener);
329
+ // Attach listeners (error and close)
330
+ this.attachBaichuanListeners(this.baichuanApi);
338
331
 
339
332
  return this.baichuanApi;
340
333
  })();
@@ -350,132 +343,29 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
350
343
 
351
344
  async subscribeToAllEvents(): Promise<void> {
352
345
  const logger = this.getLogger();
353
-
354
- // Apply backoff to avoid aggressive reconnection after disconnection
355
- // if (this.lastDisconnectTime > 0) {
356
- // const timeSinceDisconnect = Date.now() - this.lastDisconnectTime;
357
- // if (timeSinceDisconnect < this.reconnectBackoffMs) {
358
- // const waitTime = this.reconnectBackoffMs - timeSinceDisconnect;
359
- // logger.log(`[NVR] Waiting ${waitTime}ms before subscribing to events (backoff)`);
360
- // await new Promise(resolve => setTimeout(resolve, waitTime));
361
- // }
362
- // }
363
-
364
- // If already subscribed, return
365
- if (this.eventSubscriptionActive && this.onSimpleEventHandler && this.baichuanApi) {
366
- // Verify connection is still valid
346
+
347
+ // If already subscribed and connection is valid, return
348
+ if (this.eventSubscriptionActive && this.baichuanApi) {
367
349
  if (this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
368
350
  logger.log('Event subscription already active');
369
351
  return;
370
352
  }
371
- // Connection is invalid, unsubscribe first
372
- try {
373
- this.baichuanApi.offSimpleEvent(this.onSimpleEventHandler);
374
- }
375
- catch {
376
- // ignore
377
- }
353
+ // Connection is invalid, reset subscription state
378
354
  this.eventSubscriptionActive = false;
379
- this.onSimpleEventHandler = undefined;
380
355
  }
381
356
 
382
- // Unsubscribe first if handler exists
383
- if (this.onSimpleEventHandler && this.baichuanApi) {
384
- try {
385
- this.baichuanApi.offSimpleEvent(this.onSimpleEventHandler);
386
- }
387
- catch {
388
- // ignore
389
- }
390
- }
357
+ // Unsubscribe first if handler exists (idempotent)
358
+ await this.unsubscribeFromAllEvents();
391
359
 
392
360
  // Get Baichuan client connection
393
361
  const api = await this.ensureBaichuanClient();
394
-
362
+
395
363
  // Verify connection is ready
396
364
  if (!api.client.isSocketConnected() || !api.client.loggedIn) {
397
365
  logger.warn('Cannot subscribe to events: connection not ready');
398
366
  return;
399
367
  }
400
368
 
401
- // Create event handler that distributes events to cameras
402
- this.onSimpleEventHandler = (ev: ReolinkSimpleEvent) => {
403
- try {
404
- if (this.storageSettings.values.debugEvents) {
405
- logger.log(`NVR Baichuan event: ${JSON.stringify(ev)}`);
406
- }
407
-
408
- // Find camera for this channel
409
- const channel = ev?.channel;
410
- if (channel === undefined) {
411
- if (this.storageSettings.values.debugEvents) {
412
- logger.debug('Event has no channel, ignoring');
413
- }
414
- return;
415
- }
416
-
417
- // Find camera with matching channel
418
- let targetCamera: ReolinkNativeCamera | ReolinkNativeBatteryCamera | undefined;
419
- for (const camera of this.cameraNativeMap.values()) {
420
- if (camera && camera.storageSettings.values.rtspChannel === channel) {
421
- targetCamera = camera;
422
- break;
423
- }
424
- }
425
-
426
- if (!targetCamera) {
427
- if (this.storageSettings.values.debugEvents) {
428
- logger.debug(`No camera found for channel ${channel}, ignoring event`);
429
- }
430
- return;
431
- }
432
-
433
- // Convert event to camera's processEvents format
434
- const objects: string[] = [];
435
- let motion = false;
436
-
437
- switch (ev?.type) {
438
- case 'motion':
439
- motion = true;
440
- break;
441
- case 'doorbell':
442
- // Handle doorbell if camera supports it
443
- try {
444
- if (typeof (targetCamera as any).handleDoorbellEvent === 'function') {
445
- (targetCamera as any).handleDoorbellEvent();
446
- }
447
- }
448
- catch (e) {
449
- logger.warn(`Error handling doorbell event for camera channel ${channel}`, e);
450
- }
451
- motion = true;
452
- break;
453
- case 'people':
454
- case 'vehicle':
455
- case 'animal':
456
- case 'face':
457
- case 'package':
458
- case 'other':
459
- objects.push(ev.type);
460
- motion = true;
461
- break;
462
- default:
463
- if (this.storageSettings.values.debugEvents) {
464
- logger.debug(`Unknown event type: ${ev?.type}`);
465
- }
466
- return;
467
- }
468
-
469
- // Process events on the target camera
470
- targetCamera.processEvents({ motion, objects }).catch((e) => {
471
- logger.warn(`Error processing events for camera channel ${channel}`, e);
472
- });
473
- }
474
- catch (e) {
475
- logger.warn('Error in NVR onSimpleEvent handler', e);
476
- }
477
- };
478
-
479
369
  // Subscribe to events
480
370
  try {
481
371
  await api.onSimpleEvent(this.onSimpleEventHandler);
@@ -485,14 +375,14 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
485
375
  catch (e) {
486
376
  logger.warn('Failed to subscribe to events', e);
487
377
  this.eventSubscriptionActive = false;
488
- this.onSimpleEventHandler = undefined;
489
378
  }
490
379
  }
491
380
 
492
381
  async unsubscribeFromAllEvents(): Promise<void> {
493
382
  const logger = this.getLogger();
494
-
495
- if (this.onSimpleEventHandler && this.baichuanApi) {
383
+
384
+ // Only unsubscribe if we have an active subscription
385
+ if (this.eventSubscriptionActive && this.baichuanApi) {
496
386
  try {
497
387
  this.baichuanApi.offSimpleEvent(this.onSimpleEventHandler);
498
388
  logger.log('Unsubscribed from all events');
@@ -501,9 +391,48 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
501
391
  logger.warn('Error unsubscribing from events', e);
502
392
  }
503
393
  }
504
-
394
+
505
395
  this.eventSubscriptionActive = false;
506
- this.onSimpleEventHandler = undefined;
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
+ }
507
436
  }
508
437
 
509
438
  async init() {
@@ -511,10 +440,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
511
440
  const logger = this.getLogger();
512
441
  await this.updateDeviceInfo();
513
442
 
514
- // Subscribe to events for all cameras
515
- this.subscribeToAllEvents().catch((e) => {
516
- logger.warn('Failed to subscribe to events during init', e);
517
- });
443
+ // Initialize event subscriptions based on selected source
444
+ await this.reinitEventSubscriptions();
518
445
 
519
446
  setInterval(async () => {
520
447
  if (this.processing || !api) {
@@ -542,21 +469,14 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
542
469
  await this.discoverDevices(true);
543
470
  }
544
471
 
545
- const eventsRes = await api.getAllChannelsEvents();
546
-
547
- if (this.storageSettings.values.debugEvents) {
548
- 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);
549
477
  }
550
- this.cameraNativeMap.forEach((camera) => {
551
- if (camera) {
552
- const channel = camera.storageSettings.values.rtspChannel;
553
- const cameraEventsData = eventsRes?.parsed[channel];
554
- if (cameraEventsData) {
555
- camera.processEvents(cameraEventsData);
556
- }
557
- }
558
- });
559
478
 
479
+ // Always fetch battery info (not event-related)
560
480
  const { batteryInfoData, response } = await api.getAllChannelsBatteryInfo();
561
481
 
562
482
  if (this.storageSettings.values.debugEvents) {
@@ -687,6 +607,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
687
607
  }
688
608
  };
689
609
 
610
+ this.channelToNativeIdMap.set(channel, nativeId);
611
+
690
612
  if (sdk.deviceManager.getNativeIds().includes(nativeId)) {
691
613
  continue;
692
614
  }