@apocaliss92/scrypted-reolink-native 0.1.8 → 0.1.10

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,19 +1,31 @@
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
+ import { BaseBaichuanClass, type BaichuanConnectionConfig, type BaichuanConnectionCallbacks } from "./baichuan-base";
4
5
  import { ReolinkNativeCamera } from "./camera";
5
6
  import { ReolinkNativeBatteryCamera } from "./camera-battery";
6
7
  import { normalizeUid } from "./connect";
7
8
  import ReolinkNativePlugin from "./main";
8
9
  import { getDeviceInterfaces, updateDeviceInfo } from "./utils";
9
10
 
10
- export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settings, DeviceDiscovery, DeviceProvider, Reboot {
11
+ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Settings, DeviceDiscovery, DeviceProvider, Reboot {
11
12
  storageSettings = new StorageSettings(this, {
12
13
  debugEvents: {
13
14
  title: 'Debug Events',
14
15
  type: 'boolean',
15
16
  immediate: true,
16
17
  },
18
+ eventSource: {
19
+ title: 'Event Source',
20
+ description: 'Select the source for camera events: Native (Baichuan) or CGI (HTTP polling)',
21
+ type: 'string',
22
+ choices: ['Native', 'CGI'],
23
+ defaultValue: 'Native',
24
+ immediate: true,
25
+ onPut: async () => {
26
+ await this.reinitEventSubscriptions();
27
+ }
28
+ },
17
29
  ipAddress: {
18
30
  title: 'IP address',
19
31
  type: 'string',
@@ -34,8 +46,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
34
46
  });
35
47
  plugin: ReolinkNativePlugin;
36
48
  nvrApi: ReolinkCgiApi | undefined;
37
- baichuanApi: ReolinkBaichuanApi | undefined;
38
- baichuanApiPromise: Promise<ReolinkBaichuanApi> | undefined;
49
+ // baichuanApi, ensureClientPromise, connectionTime inherited from BaseBaichuanClass
39
50
  discoveredDevices = new Map<string, {
40
51
  device: Device;
41
52
  description: string;
@@ -46,15 +57,8 @@ 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
- private eventSubscriptionActive = false;
51
- private onSimpleEventHandler?: (ev: ReolinkSimpleEvent) => void;
52
- private closeListener?: () => void;
53
- 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;
58
62
 
59
63
  constructor(nativeId: string, plugin: ReolinkNativePlugin) {
60
64
  super(nativeId);
@@ -70,11 +74,53 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
70
74
  await api.Reboot();
71
75
  }
72
76
 
73
- getLogger() {
74
- return this.console;
77
+ // BaseBaichuanClass abstract methods implementation
78
+ protected getConnectionConfig(): BaichuanConnectionConfig {
79
+ const { ipAddress, username, password } = this.storageSettings.values;
80
+ if (!ipAddress || !username || !password) {
81
+ throw new Error('Missing NVR credentials');
82
+ }
83
+
84
+ return {
85
+ host: ipAddress,
86
+ username,
87
+ password,
88
+ transport: 'tcp',
89
+ logger: this.console,
90
+ };
91
+ }
92
+
93
+ protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
94
+ return {
95
+ onError: undefined, // Use default error handling
96
+ onClose: async () => {
97
+ // Reinit after cleanup
98
+ await this.reinit();
99
+ },
100
+ onSimpleEvent: (ev) => this.forwardNativeEvent(ev),
101
+ getEventSubscriptionEnabled: () => {
102
+ const eventSource = this.storageSettings.values.eventSource || 'Native';
103
+ return eventSource === 'Native';
104
+ },
105
+ };
106
+ }
107
+
108
+
109
+ protected isDebugEnabled(): boolean {
110
+ return this.storageSettings.values.debugEvents || false;
111
+ }
112
+
113
+ protected getDeviceName(): string {
114
+ return this.name || 'NVR';
115
+ }
116
+
117
+ protected async onBeforeCleanup(): Promise<void> {
118
+ // Unsubscribe from events if needed
119
+ await this.unsubscribeFromAllEvents();
75
120
  }
76
121
 
77
122
  async reinit() {
123
+ // Cleanup CGI API
78
124
  if (this.nvrApi) {
79
125
  try {
80
126
  await this.nvrApi.logout();
@@ -84,48 +130,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
84
130
  }
85
131
  this.nvrApi = undefined;
86
132
 
87
- // Clear any pending resubscribe timeout
88
- if (this.resubscribeTimeout) {
89
- clearTimeout(this.resubscribeTimeout);
90
- this.resubscribeTimeout = undefined;
91
- }
92
-
93
- // Unsubscribe from events first
94
- await this.unsubscribeFromAllEvents();
95
-
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;
106
- }
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;
117
- }
118
-
119
- try {
120
- if (this.baichuanApi.client.isSocketConnected()) {
121
- await this.baichuanApi.close();
122
- }
123
- } catch {
124
- // ignore
125
- }
126
- }
127
- this.baichuanApi = undefined;
128
- this.baichuanApiPromise = undefined;
133
+ // Cleanup Baichuan API (this handles all listeners and connection)
134
+ await super.cleanupBaichuanApi();
129
135
  }
130
136
 
131
137
  async ensureClient(): Promise<ReolinkCgiApi> {
@@ -149,372 +155,146 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
149
155
  return this.nvrApi;
150
156
  }
151
157
 
152
- async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
153
- // Reuse existing client if socket is still connected and logged in
154
- if (this.baichuanApi && this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
155
- return this.baichuanApi;
156
- }
157
-
158
- // Prevent concurrent login storms
159
- if (this.baichuanApiPromise) return await this.baichuanApiPromise;
158
+ private forwardNativeEvent(ev: ReolinkSimpleEvent): void {
159
+ const logger = this.getBaichuanLogger();
160
160
 
161
- this.baichuanApiPromise = (async () => {
162
- const { ipAddress, username, password } = this.storageSettings.values;
163
- if (!ipAddress || !username || !password) {
164
- throw new Error('Missing NVR credentials');
165
- }
161
+ const eventSource = this.storageSettings.values.eventSource || 'Native';
162
+ if (eventSource !== 'Native') {
163
+ return;
164
+ }
166
165
 
167
- // Clean up old client if exists
168
- 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
- }
166
+ try {
167
+ logger.debug(`Baichuan event: ${JSON.stringify(ev)}`);
190
168
 
191
- try {
192
- if (this.baichuanApi.client.isSocketConnected()) {
193
- await this.baichuanApi.close();
194
- }
195
- }
196
- catch {
197
- // ignore
198
- }
169
+ // Find camera for this channel
170
+ const channel = ev?.channel;
171
+ if (channel === undefined) {
172
+ logger.debug('Event has no channel, ignoring');
173
+ return;
199
174
  }
200
175
 
201
- // Create new Baichuan client
202
- const { ReolinkBaichuanApi } = await import("@apocaliss92/reolink-baichuan-js");
203
- this.baichuanApi = new ReolinkBaichuanApi({
204
- host: ipAddress,
205
- username,
206
- password,
207
- transport: 'tcp',
208
- logger: this.getLogger(),
209
- // rebootAfterDisconnectionsPerMinute: 5,
210
- });
211
-
212
- await this.baichuanApi.login();
176
+ const nativeId = this.channelToNativeIdMap.get(channel);
177
+ const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
213
178
 
214
- // Verify socket is connected before returning
215
- if (!this.baichuanApi.client.isSocketConnected()) {
216
- throw new Error('Socket not connected after login');
179
+ if (!targetCamera) {
180
+ logger.debug(`No camera found for channel ${channel}, ignoring event`);
181
+ return;
217
182
  }
218
183
 
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);
184
+ // Convert event to camera's processEvents format
185
+ const objects: string[] = [];
186
+ let motion = false;
243
187
 
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) {
188
+ switch (ev?.type) {
189
+ case 'motion':
190
+ motion = true;
191
+ break;
192
+ case 'doorbell':
193
+ // Handle doorbell if camera supports it
291
194
  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);
195
+ if (typeof (targetCamera as any).handleDoorbellEvent === 'function') {
196
+ (targetCamera as any).handleDoorbellEvent();
197
+ }
313
198
  }
314
- catch {
315
- // ignore
199
+ catch (e) {
200
+ logger.warn(`Error handling doorbell event for camera channel ${channel}`, e);
316
201
  }
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);
338
-
339
- return this.baichuanApi;
340
- })();
202
+ motion = true;
203
+ break;
204
+ case 'people':
205
+ case 'vehicle':
206
+ case 'animal':
207
+ case 'face':
208
+ case 'package':
209
+ case 'other':
210
+ objects.push(ev.type);
211
+ motion = true;
212
+ break;
213
+ default:
214
+ logger.debug(`Unknown event type: ${ev?.type}`);
215
+ return;
216
+ }
341
217
 
342
- try {
343
- return await this.baichuanApiPromise;
218
+ // Process events on the target camera
219
+ targetCamera.processEvents({ motion, objects }).catch((e) => {
220
+ logger.warn(`Error processing events for camera channel ${channel}`, e);
221
+ });
344
222
  }
345
- finally {
346
- // Allow future reconnects and avoid pinning rejected promises
347
- this.baichuanApiPromise = undefined;
223
+ catch (e) {
224
+ logger.warn('Error in NVR Native event forwarder', e);
348
225
  }
349
226
  }
350
227
 
351
- async subscribeToAllEvents(): Promise<void> {
352
- 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
367
- if (this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
368
- logger.log('Event subscription already active');
369
- return;
370
- }
371
- // Connection is invalid, unsubscribe first
372
- try {
373
- this.baichuanApi.offSimpleEvent(this.onSimpleEventHandler);
374
- }
375
- catch {
376
- // ignore
377
- }
378
- this.eventSubscriptionActive = false;
379
- this.onSimpleEventHandler = undefined;
380
- }
228
+ async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
229
+ // Use base class implementation
230
+ return await super.ensureBaichuanClient();
231
+ }
381
232
 
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
- }
233
+ async subscribeToAllEvents(): Promise<void> {
234
+ const logger = this.getBaichuanLogger();
235
+ const eventSource = this.storageSettings.values.eventSource || 'Native';
391
236
 
392
- // Get Baichuan client connection
393
- const api = await this.ensureBaichuanClient();
394
-
395
- // Verify connection is ready
396
- if (!api.client.isSocketConnected() || !api.client.loggedIn) {
397
- logger.warn('Cannot subscribe to events: connection not ready');
237
+ // Only subscribe if Native is selected
238
+ if (eventSource !== 'Native') {
239
+ await this.unsubscribeFromAllEvents();
398
240
  return;
399
241
  }
400
242
 
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
- }
243
+ // Use base class implementation
244
+ await super.subscribeToEvents();
245
+ logger.log('Subscribed to all events for NVR cameras');
246
+ }
425
247
 
426
- if (!targetCamera) {
427
- if (this.storageSettings.values.debugEvents) {
428
- logger.debug(`No camera found for channel ${channel}, ignoring event`);
429
- }
430
- return;
431
- }
248
+ async unsubscribeFromAllEvents(): Promise<void> {
249
+ // Use base class implementation
250
+ await super.unsubscribeFromEvents();
251
+ }
432
252
 
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
- }
253
+ /**
254
+ * Reinitialize event subscriptions based on selected event source
255
+ */
256
+ private async reinitEventSubscriptions(): Promise<void> {
257
+ const logger = this.getBaichuanLogger();
258
+ const { eventSource } = this.storageSettings.values;
468
259
 
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
- };
260
+ // Unsubscribe from Native events if switching away
261
+ if (eventSource !== 'Native') {
262
+ await this.unsubscribeFromAllEvents();
263
+ } else {
478
264
 
479
- // Subscribe to events
480
- try {
481
- await api.onSimpleEvent(this.onSimpleEventHandler);
482
- this.eventSubscriptionActive = true;
483
- logger.log('Subscribed to all events for NVR cameras');
484
- }
485
- catch (e) {
486
- logger.warn('Failed to subscribe to events', e);
487
- this.eventSubscriptionActive = false;
488
- this.onSimpleEventHandler = undefined;
265
+ this.subscribeToAllEvents().catch((e) => {
266
+ logger.warn('Failed to subscribe to Native events', e);
267
+ });
489
268
  }
269
+
270
+ logger.log(`Event source set to: ${eventSource}`);
490
271
  }
491
272
 
492
- async unsubscribeFromAllEvents(): Promise<void> {
493
- const logger = this.getLogger();
494
-
495
- if (this.onSimpleEventHandler && this.baichuanApi) {
496
- try {
497
- this.baichuanApi.offSimpleEvent(this.onSimpleEventHandler);
498
- logger.log('Unsubscribed from all events');
499
- }
500
- catch (e) {
501
- logger.warn('Error unsubscribing from events', e);
273
+ /**
274
+ * Forward events from CGI source to cameras
275
+ */
276
+ private forwardCgiEvents(eventsRes: Record<number, EventsResponse>): void {
277
+ const logger = this.getBaichuanLogger();
278
+
279
+ logger.debug(`CGI Events call result: ${JSON.stringify(eventsRes)}`);
280
+
281
+ // Use channel map for efficient lookup
282
+ for (const [channel, nativeId] of this.channelToNativeIdMap.entries()) {
283
+ const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
284
+ const cameraEventsData = eventsRes[channel];
285
+ if (cameraEventsData && targetCamera) {
286
+ targetCamera.processEvents(cameraEventsData);
502
287
  }
503
288
  }
504
-
505
- this.eventSubscriptionActive = false;
506
- this.onSimpleEventHandler = undefined;
507
289
  }
508
290
 
509
291
  async init() {
510
292
  const api = await this.ensureClient();
511
- const logger = this.getLogger();
293
+ const logger = this.getBaichuanLogger();
512
294
  await this.updateDeviceInfo();
513
295
 
514
- // Subscribe to events for all cameras
515
- this.subscribeToAllEvents().catch((e) => {
516
- logger.warn('Failed to subscribe to events during init', e);
517
- });
296
+ // Initialize event subscriptions based on selected source
297
+ await this.reinitEventSubscriptions();
518
298
 
519
299
  setInterval(async () => {
520
300
  if (this.processing || !api) {
@@ -535,33 +315,22 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
535
315
  const { hubData } = await api.getHubInfo();
536
316
  const { devicesData, channelsResponse, response } = await api.getDevicesInfo();
537
317
  logger.log('Hub info data fetched');
538
- if (this.storageSettings.values.debugEvents) {
539
- logger.log(`${JSON.stringify({ hubData, devicesData, channelsResponse, response })}`);
540
- }
318
+ logger.debug(`${JSON.stringify({ hubData, devicesData, channelsResponse, response })}`);
541
319
 
542
320
  await this.discoverDevices(true);
543
321
  }
544
322
 
545
- const eventsRes = await api.getAllChannelsEvents();
546
-
547
- if (this.storageSettings.values.debugEvents) {
548
- logger.debug(`Events call result: ${JSON.stringify(eventsRes)}`);
323
+ // Only fetch and forward CGI events if CGI is selected as event source
324
+ const { eventSource } = this.storageSettings.values;
325
+ if (eventSource === 'CGI') {
326
+ const eventsRes = await api.getAllChannelsEvents();
327
+ this.forwardCgiEvents(eventsRes.parsed);
549
328
  }
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
329
 
330
+ // Always fetch battery info (not event-related)
560
331
  const { batteryInfoData, response } = await api.getAllChannelsBatteryInfo();
561
332
 
562
- if (this.storageSettings.values.debugEvents) {
563
- logger.debug(`Battery info call result: ${JSON.stringify({ batteryInfoData, response })}`);
564
- }
333
+ logger.debug(`Battery info call result: ${JSON.stringify({ batteryInfoData, response })}`);
565
334
 
566
335
  this.cameraNativeMap.forEach((camera) => {
567
336
  if (camera) {
@@ -578,7 +347,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
578
347
  }
579
348
  });
580
349
  } catch (e) {
581
- this.console.error('Error on events flow', e);
350
+ logger.error('Error on events flow', e);
582
351
  } finally {
583
352
  this.processing = false;
584
353
  }
@@ -597,7 +366,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
597
366
  deviceData,
598
367
  });
599
368
  } catch (e) {
600
- this.getLogger().warn('Failed to fetch device info', e);
369
+ this.getBaichuanLogger().warn('Failed to fetch device info', e);
601
370
  }
602
371
  }
603
372
 
@@ -651,7 +420,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
651
420
 
652
421
  async syncEntitiesFromRemote() {
653
422
  const api = await this.ensureClient();
654
- const logger = this.getLogger();
423
+ const logger = this.getBaichuanLogger();
655
424
 
656
425
  logger.log('Starting channels discovery using getDevicesInfo...');
657
426
 
@@ -687,6 +456,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
687
456
  }
688
457
  };
689
458
 
459
+ this.channelToNativeIdMap.set(channel, nativeId);
460
+
690
461
  if (sdk.deviceManager.getNativeIds().includes(nativeId)) {
691
462
  continue;
692
463
  }
@@ -761,7 +532,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
761
532
  await sdk.deviceManager.onDeviceDiscovered(actualDevice);
762
533
 
763
534
  const device = await this.getDevice(adopt.nativeId);
764
- this.console.log('Adopted device', entry, device?.name);
535
+ this.getBaichuanLogger().debug('Adopted device', entry, device?.name);
765
536
  const { username, password, ipAddress } = this.storageSettings.values;
766
537
 
767
538
  device.storageSettings.values.rtspChannel = entry.rtspChannel;