@apocaliss92/scrypted-reolink-native 0.1.9 → 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,13 +1,14 @@
1
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',
@@ -45,8 +46,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
45
46
  });
46
47
  plugin: ReolinkNativePlugin;
47
48
  nvrApi: ReolinkCgiApi | undefined;
48
- baichuanApi: ReolinkBaichuanApi | undefined;
49
- baichuanApiPromise: Promise<ReolinkBaichuanApi> | undefined;
49
+ // baichuanApi, ensureClientPromise, connectionTime inherited from BaseBaichuanClass
50
50
  discoveredDevices = new Map<string, {
51
51
  device: Device;
52
52
  description: string;
@@ -59,9 +59,6 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
59
59
  cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
60
60
  private channelToNativeIdMap = new Map<number, string>();
61
61
  processing = false;
62
- private eventSubscriptionActive = false;
63
- private errorListener?: (err: unknown) => void;
64
- private closeListener?: () => void;
65
62
 
66
63
  constructor(nativeId: string, plugin: ReolinkNativePlugin) {
67
64
  super(nativeId);
@@ -77,98 +74,49 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
77
74
  await api.Reboot();
78
75
  }
79
76
 
80
- getLogger() {
81
- return this.console;
82
- }
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
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');
118
82
  }
119
83
 
120
- // Reset state
121
- this.baichuanApi = undefined;
122
- this.baichuanApiPromise = undefined;
84
+ return {
85
+ host: ipAddress,
86
+ username,
87
+ password,
88
+ transport: 'tcp',
89
+ logger: this.console,
90
+ };
123
91
  }
124
92
 
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}`);
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
+ },
145
105
  };
106
+ }
146
107
 
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
108
 
164
- // Cleanup and reinit
165
- await this.cleanupBaichuanApi();
166
- await this.reinit();
167
- };
109
+ protected isDebugEnabled(): boolean {
110
+ return this.storageSettings.values.debugEvents || false;
111
+ }
112
+
113
+ protected getDeviceName(): string {
114
+ return this.name || 'NVR';
115
+ }
168
116
 
169
- // Attach listeners
170
- api.client.on("error", this.errorListener);
171
- api.client.on("close", this.closeListener);
117
+ protected async onBeforeCleanup(): Promise<void> {
118
+ // Unsubscribe from events if needed
119
+ await this.unsubscribeFromAllEvents();
172
120
  }
173
121
 
174
122
  async reinit() {
@@ -183,7 +131,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
183
131
  this.nvrApi = undefined;
184
132
 
185
133
  // Cleanup Baichuan API (this handles all listeners and connection)
186
- await this.cleanupBaichuanApi();
134
+ await super.cleanupBaichuanApi();
187
135
  }
188
136
 
189
137
  async ensureClient(): Promise<ReolinkCgiApi> {
@@ -208,7 +156,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
208
156
  }
209
157
 
210
158
  private forwardNativeEvent(ev: ReolinkSimpleEvent): void {
211
- const logger = this.getLogger();
159
+ const logger = this.getBaichuanLogger();
212
160
 
213
161
  const eventSource = this.storageSettings.values.eventSource || 'Native';
214
162
  if (eventSource !== 'Native') {
@@ -216,16 +164,12 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
216
164
  }
217
165
 
218
166
  try {
219
- if (this.storageSettings.values.debugEvents) {
220
- logger.log(`NVR Baichuan event: ${JSON.stringify(ev)}`);
221
- }
167
+ logger.debug(`Baichuan event: ${JSON.stringify(ev)}`);
222
168
 
223
169
  // Find camera for this channel
224
170
  const channel = ev?.channel;
225
171
  if (channel === undefined) {
226
- if (this.storageSettings.values.debugEvents) {
227
- logger.debug('Event has no channel, ignoring');
228
- }
172
+ logger.debug('Event has no channel, ignoring');
229
173
  return;
230
174
  }
231
175
 
@@ -233,9 +177,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
233
177
  const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
234
178
 
235
179
  if (!targetCamera) {
236
- if (this.storageSettings.values.debugEvents) {
237
- logger.debug(`No camera found for channel ${channel}, ignoring event`);
238
- }
180
+ logger.debug(`No camera found for channel ${channel}, ignoring event`);
239
181
  return;
240
182
  }
241
183
 
@@ -269,9 +211,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
269
211
  motion = true;
270
212
  break;
271
213
  default:
272
- if (this.storageSettings.values.debugEvents) {
273
- logger.debug(`Unknown event type: ${ev?.type}`);
274
- }
214
+ logger.debug(`Unknown event type: ${ev?.type}`);
275
215
  return;
276
216
  }
277
217
 
@@ -285,121 +225,36 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
285
225
  }
286
226
  }
287
227
 
288
- async onSimpleEventHandler(ev: ReolinkSimpleEvent) {
289
- this.forwardNativeEvent(ev);
290
- }
291
-
292
228
  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
- }
229
+ // Use base class implementation
230
+ return await super.ensureBaichuanClient();
342
231
  }
343
232
 
344
233
  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();
234
+ const logger = this.getBaichuanLogger();
235
+ const eventSource = this.storageSettings.values.eventSource || 'Native';
362
236
 
363
- // Verify connection is ready
364
- if (!api.client.isSocketConnected() || !api.client.loggedIn) {
365
- logger.warn('Cannot subscribe to events: connection not ready');
237
+ // Only subscribe if Native is selected
238
+ if (eventSource !== 'Native') {
239
+ await this.unsubscribeFromAllEvents();
366
240
  return;
367
241
  }
368
242
 
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
- }
243
+ // Use base class implementation
244
+ await super.subscribeToEvents();
245
+ logger.log('Subscribed to all events for NVR cameras');
379
246
  }
380
247
 
381
248
  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;
249
+ // Use base class implementation
250
+ await super.unsubscribeFromEvents();
396
251
  }
397
252
 
398
253
  /**
399
254
  * Reinitialize event subscriptions based on selected event source
400
255
  */
401
256
  private async reinitEventSubscriptions(): Promise<void> {
402
- const logger = this.getLogger();
257
+ const logger = this.getBaichuanLogger();
403
258
  const { eventSource } = this.storageSettings.values;
404
259
 
405
260
  // Unsubscribe from Native events if switching away
@@ -419,11 +274,9 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
419
274
  * Forward events from CGI source to cameras
420
275
  */
421
276
  private forwardCgiEvents(eventsRes: Record<number, EventsResponse>): void {
422
- const logger = this.getLogger();
277
+ const logger = this.getBaichuanLogger();
423
278
 
424
- if (this.storageSettings.values.debugEvents) {
425
- logger.debug(`CGI Events call result: ${JSON.stringify(eventsRes)}`);
426
- }
279
+ logger.debug(`CGI Events call result: ${JSON.stringify(eventsRes)}`);
427
280
 
428
281
  // Use channel map for efficient lookup
429
282
  for (const [channel, nativeId] of this.channelToNativeIdMap.entries()) {
@@ -437,7 +290,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
437
290
 
438
291
  async init() {
439
292
  const api = await this.ensureClient();
440
- const logger = this.getLogger();
293
+ const logger = this.getBaichuanLogger();
441
294
  await this.updateDeviceInfo();
442
295
 
443
296
  // Initialize event subscriptions based on selected source
@@ -462,9 +315,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
462
315
  const { hubData } = await api.getHubInfo();
463
316
  const { devicesData, channelsResponse, response } = await api.getDevicesInfo();
464
317
  logger.log('Hub info data fetched');
465
- if (this.storageSettings.values.debugEvents) {
466
- logger.log(`${JSON.stringify({ hubData, devicesData, channelsResponse, response })}`);
467
- }
318
+ logger.debug(`${JSON.stringify({ hubData, devicesData, channelsResponse, response })}`);
468
319
 
469
320
  await this.discoverDevices(true);
470
321
  }
@@ -479,9 +330,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
479
330
  // Always fetch battery info (not event-related)
480
331
  const { batteryInfoData, response } = await api.getAllChannelsBatteryInfo();
481
332
 
482
- if (this.storageSettings.values.debugEvents) {
483
- logger.debug(`Battery info call result: ${JSON.stringify({ batteryInfoData, response })}`);
484
- }
333
+ logger.debug(`Battery info call result: ${JSON.stringify({ batteryInfoData, response })}`);
485
334
 
486
335
  this.cameraNativeMap.forEach((camera) => {
487
336
  if (camera) {
@@ -498,7 +347,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
498
347
  }
499
348
  });
500
349
  } catch (e) {
501
- this.console.error('Error on events flow', e);
350
+ logger.error('Error on events flow', e);
502
351
  } finally {
503
352
  this.processing = false;
504
353
  }
@@ -517,7 +366,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
517
366
  deviceData,
518
367
  });
519
368
  } catch (e) {
520
- this.getLogger().warn('Failed to fetch device info', e);
369
+ this.getBaichuanLogger().warn('Failed to fetch device info', e);
521
370
  }
522
371
  }
523
372
 
@@ -571,7 +420,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
571
420
 
572
421
  async syncEntitiesFromRemote() {
573
422
  const api = await this.ensureClient();
574
- const logger = this.getLogger();
423
+ const logger = this.getBaichuanLogger();
575
424
 
576
425
  logger.log('Starting channels discovery using getDevicesInfo...');
577
426
 
@@ -683,7 +532,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
683
532
  await sdk.deviceManager.onDeviceDiscovered(actualDevice);
684
533
 
685
534
  const device = await this.getDevice(adopt.nativeId);
686
- this.console.log('Adopted device', entry, device?.name);
535
+ this.getBaichuanLogger().debug('Adopted device', entry, device?.name);
687
536
  const { username, password, ipAddress } = this.storageSettings.values;
688
537
 
689
538
  device.storageSettings.values.rtspChannel = entry.rtspChannel;
@@ -274,7 +274,7 @@ export class StreamManager {
274
274
  }
275
275
 
276
276
  private getLogger() {
277
- return this.opts.getLogger();
277
+ return this.opts.getLogger() as Console;
278
278
  }
279
279
 
280
280
  private async ensureNativeRfcServer(