@apocaliss92/scrypted-reolink-native 0.1.7 → 0.1.8

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.8",
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,12 @@ 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
+ const logger = this.getLogger();
1337
+ return await this.nvrDevice.ensureBaichuanClient();
1338
+ }
1339
+
1332
1340
  // Reuse existing client if socket is still connected and logged in
1333
1341
  if (this.baichuanApi && this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
1334
1342
  return this.baichuanApi;
@@ -1337,6 +1345,17 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1337
1345
  // Prevent concurrent login storms
1338
1346
  if (this.ensureClientPromise) return await this.ensureClientPromise;
1339
1347
 
1348
+ // Apply backoff to avoid aggressive reconnection after disconnection
1349
+ if (this.lastDisconnectTime > 0) {
1350
+ const timeSinceDisconnect = Date.now() - this.lastDisconnectTime;
1351
+ if (timeSinceDisconnect < this.reconnectBackoffMs) {
1352
+ const waitTime = this.reconnectBackoffMs - timeSinceDisconnect;
1353
+ const logger = this.getLogger();
1354
+ logger.log(`[BaichuanClient] Waiting ${waitTime}ms before reconnection (backoff)`);
1355
+ await new Promise(resolve => setTimeout(resolve, waitTime));
1356
+ }
1357
+ }
1358
+
1340
1359
  this.ensureClientPromise = (async () => {
1341
1360
  const { ipAddress, username, password, uid } = this.storageSettings.values;
1342
1361
 
@@ -1355,11 +1374,8 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1355
1374
 
1356
1375
  const isConnected = this.baichuanApi.client.isSocketConnected();
1357
1376
  if (!isConnected) {
1358
- // Socket is closed, clean up
1359
1377
  try {
1360
- if (this.onSimpleEvent) {
1361
- this.baichuanApi.offSimpleEvent(this.onSimpleEvent);
1362
- }
1378
+ this.baichuanApi.offSimpleEvent(this.onSimpleEvent);
1363
1379
  }
1364
1380
  catch {
1365
1381
  // ignore
@@ -1425,21 +1441,45 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1425
1441
  this.closeListener = () => {
1426
1442
  const logger = this.getLogger();
1427
1443
  if (this.baichuanApi === api) {
1428
- logger.log(`[BaichuanClient] Socket closed, resetting client state for reconnection`);
1444
+ const now = Date.now();
1445
+ const timeSinceLastDisconnect = now - this.lastDisconnectTime;
1446
+ this.lastDisconnectTime = now;
1447
+
1448
+ logger.log(`[BaichuanClient] Socket closed, resetting client state for reconnection (last disconnect ${timeSinceLastDisconnect}ms ago)`);
1449
+
1450
+ // Reset client state
1429
1451
  this.baichuanApi = undefined;
1430
1452
  this.ensureClientPromise = undefined;
1431
1453
  this.closeListener = undefined;
1454
+
1455
+ // Remove event handler to prevent operations during reconnection
1456
+ try {
1457
+ if (this.onSimpleEvent) {
1458
+ api.offSimpleEvent(this.onSimpleEvent);
1459
+ }
1460
+ }
1461
+ catch {
1462
+ // ignore
1463
+ }
1432
1464
  }
1433
1465
  };
1434
1466
  api.client.on("close", this.closeListener);
1435
1467
 
1436
1468
  // Re-attach event handler if enabled
1469
+ // Note: We don't reattach here immediately to avoid operations being called
1470
+ // during reconnection. subscribeToEvents() will be called when needed.
1471
+ // However, if events were already subscribed, we need to reattach them.
1472
+ // We'll let subscribeToEvents() handle this, but we can also try here if needed.
1437
1473
  if (this.isEventDispatchEnabled?.() && this.onSimpleEvent) {
1438
1474
  try {
1439
- api.onSimpleEvent(this.onSimpleEvent);
1475
+ // Verify connection is fully ready before subscribing
1476
+ if (api.client.isSocketConnected() && api.client.loggedIn) {
1477
+ api.onSimpleEvent(this.onSimpleEvent);
1478
+ }
1440
1479
  }
1441
- catch {
1442
- // ignore
1480
+ catch (e) {
1481
+ const logger = this.getLogger();
1482
+ logger.warn(`[BaichuanClient] Failed to reattach event handler after reconnection, will retry via subscribeToEvents()`, e);
1443
1483
  }
1444
1484
  }
1445
1485
  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, 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";
@@ -34,6 +34,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
34
34
  });
35
35
  plugin: ReolinkNativePlugin;
36
36
  nvrApi: ReolinkCgiApi | undefined;
37
+ baichuanApi: ReolinkBaichuanApi | undefined;
38
+ baichuanApiPromise: Promise<ReolinkBaichuanApi> | undefined;
37
39
  discoveredDevices = new Map<string, {
38
40
  device: Device;
39
41
  description: string;
@@ -45,6 +47,14 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
45
47
  lastDevicesStatusCheck: number | undefined;
46
48
  cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
47
49
  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;
48
58
 
49
59
  constructor(nativeId: string, plugin: ReolinkNativePlugin) {
50
60
  super(nativeId);
@@ -73,6 +83,49 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
73
83
  }
74
84
  }
75
85
  this.nvrApi = undefined;
86
+
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;
76
129
  }
77
130
 
78
131
  async ensureClient(): Promise<ReolinkCgiApi> {
@@ -96,11 +149,373 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
96
149
  return this.nvrApi;
97
150
  }
98
151
 
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;
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
+ }
166
+
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
+ }
190
+
191
+ try {
192
+ if (this.baichuanApi.client.isSocketConnected()) {
193
+ await this.baichuanApi.close();
194
+ }
195
+ }
196
+ catch {
197
+ // ignore
198
+ }
199
+ }
200
+
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();
213
+
214
+ // Verify socket is connected before returning
215
+ if (!this.baichuanApi.client.isSocketConnected()) {
216
+ throw new Error('Socket not connected after login');
217
+ }
218
+
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);
338
+
339
+ return this.baichuanApi;
340
+ })();
341
+
342
+ try {
343
+ return await this.baichuanApiPromise;
344
+ }
345
+ finally {
346
+ // Allow future reconnects and avoid pinning rejected promises
347
+ this.baichuanApiPromise = undefined;
348
+ }
349
+ }
350
+
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
+ }
381
+
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
+ }
391
+
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');
398
+ return;
399
+ }
400
+
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
+ // 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;
489
+ }
490
+ }
491
+
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);
502
+ }
503
+ }
504
+
505
+ this.eventSubscriptionActive = false;
506
+ this.onSimpleEventHandler = undefined;
507
+ }
508
+
99
509
  async init() {
100
510
  const api = await this.ensureClient();
101
511
  const logger = this.getLogger();
102
512
  await this.updateDeviceInfo();
103
513
 
514
+ // Subscribe to events for all cameras
515
+ this.subscribeToAllEvents().catch((e) => {
516
+ logger.warn('Failed to subscribe to events during init', e);
517
+ });
518
+
104
519
  setInterval(async () => {
105
520
  if (this.processing || !api) {
106
521
  return;