@apocaliss92/scrypted-reolink-native 0.1.36 → 0.1.38

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,10 +1,11 @@
1
- import type { DeviceInfoResponse, EnrichedRecordingFile, EventsResponse, ReolinkBaichuanApi, ReolinkCgiApi, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
1
+ import type { EventsResponse, ReolinkBaichuanApi, ReolinkBaichuanDeviceSummary, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
2
  import sdk, { AdoptDevice, Device, DeviceDiscovery, DeviceProvider, DiscoveredDevice, Reboot, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
3
3
  import { StorageSettings } from "@scrypted/sdk/storage-settings";
4
4
  import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
5
5
  import { ReolinkNativeCamera } from "./camera";
6
6
  import { ReolinkNativeBatteryCamera } from "./camera-battery";
7
7
  import { normalizeUid } from "./connect";
8
+ import { convertDebugLogsToApiOptions, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
8
9
  import ReolinkNativePlugin from "./main";
9
10
  import { getDeviceInterfaces, updateDeviceInfo } from "./utils";
10
11
 
@@ -44,8 +45,8 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
44
45
  onPut: async () => await this.reinit()
45
46
  },
46
47
  diagnosticsRun: {
47
- subgroup: 'Diagnostics',
48
- title: 'Run NVR Diagnostics',
48
+ subgroup: 'Advanced',
49
+ title: 'Run Diagnostics',
49
50
  description: 'Collect NVR diagnostics and display results in logs.',
50
51
  type: 'button',
51
52
  immediate: true,
@@ -53,15 +54,54 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
53
54
  await this.runNvrDiagnostics();
54
55
  },
55
56
  },
57
+ socketApiDebugLogs: {
58
+ subgroup: 'Advanced',
59
+ title: 'Socket API Debug Logs',
60
+ description: 'Enable specific debug logs.',
61
+ multiple: true,
62
+ combobox: true,
63
+ immediate: true,
64
+ defaultValue: [],
65
+ choices: getDebugLogChoices(),
66
+ onPut: async (ov, value) => {
67
+ const logger = this.getBaichuanLogger();
68
+ const oldApiOptions = getApiRelevantDebugLogs(ov || []);
69
+ const newApiOptions = getApiRelevantDebugLogs(value || []);
70
+
71
+ const oldSel = new Set(oldApiOptions);
72
+ const newSel = new Set(newApiOptions);
73
+
74
+ const changed = oldSel.size !== newSel.size || Array.from(oldSel).some((k) => !newSel.has(k));
75
+ if (changed) {
76
+ // Clear any existing timeout
77
+ if (this.debugLogsResetTimeout) {
78
+ clearTimeout(this.debugLogsResetTimeout);
79
+ this.debugLogsResetTimeout = undefined;
80
+ }
81
+
82
+ // Defer reset by 2 seconds to allow settings to settle
83
+ this.debugLogsResetTimeout = setTimeout(async () => {
84
+ this.debugLogsResetTimeout = undefined;
85
+ try {
86
+ // Force reconnection with new debug options
87
+ this.baichuanApi = undefined;
88
+ this.ensureClientPromise = undefined;
89
+ // Trigger reconnection
90
+ await this.ensureBaichuanClient();
91
+ } catch (e) {
92
+ logger.warn('Failed to reset client after debug logs change', e);
93
+ }
94
+ }, 2000);
95
+ }
96
+ },
97
+ },
56
98
  });
57
99
  plugin: ReolinkNativePlugin;
58
- nvrApi: ReolinkCgiApi | undefined;
59
- // baichuanApi, ensureClientPromise, connectionTime inherited from BaseBaichuanClass
60
100
  discoveredDevices = new Map<string, {
61
101
  device: Device;
62
102
  description: string;
63
103
  rtspChannel: number;
64
- deviceData: DeviceInfoResponse;
104
+ deviceData: ReolinkBaichuanDeviceSummary;
65
105
  }>();
66
106
  lastNvrInfoCheck: number | undefined;
67
107
  lastErrorsCheck: number | undefined;
@@ -71,6 +111,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
71
111
  private discoverDevicesPromise: Promise<DiscoveredDevice[]> | undefined;
72
112
  processing = false;
73
113
  private initReinitTimeout: NodeJS.Timeout | undefined;
114
+ private debugLogsResetTimeout: NodeJS.Timeout | undefined;
74
115
 
75
116
  constructor(nativeId: string, plugin: ReolinkNativePlugin) {
76
117
  super(nativeId);
@@ -84,27 +125,31 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
84
125
  await api.reboot();
85
126
  }
86
127
 
87
- // BaseBaichuanClass abstract methods implementation
88
128
  protected getConnectionConfig(): BaichuanConnectionConfig {
89
129
  const { ipAddress, username, password } = this.storageSettings.values;
90
130
  if (!ipAddress || !username || !password) {
91
131
  throw new Error('Missing NVR credentials');
92
132
  }
93
133
 
134
+ const debugOptions = this.getBaichuanDebugOptions();
135
+
94
136
  return {
95
137
  host: ipAddress,
96
138
  username,
97
139
  password,
98
140
  transport: 'tcp',
99
- logger: this.console,
141
+ debugOptions,
100
142
  };
101
143
  }
102
144
 
145
+ getBaichuanDebugOptions(): any | undefined {
146
+ const socketDebugLogs = this.storageSettings.values.socketApiDebugLogs || [];
147
+ return convertDebugLogsToApiOptions(socketDebugLogs);
148
+ }
149
+
103
150
  protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
104
151
  return {
105
- onError: undefined, // Use default error handling
106
152
  onClose: async () => {
107
- // Reinit after cleanup
108
153
  await this.reinit();
109
154
  },
110
155
  onSimpleEvent: (ev) => this.forwardNativeEvent(ev),
@@ -146,17 +191,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
146
191
 
147
192
  this.initReinitTimeout = setTimeout(async () => {
148
193
  if (isReinit) {
149
- // Cleanup CGI API
150
- if (this.nvrApi) {
151
- try {
152
- await this.nvrApi.logout();
153
- } catch {
154
- // ignore
155
- }
156
- }
157
- this.nvrApi = undefined;
158
-
159
- // Cleanup Baichuan API (this handles all listeners and connection)
160
194
  await super.cleanupBaichuanApi();
161
195
  }
162
196
  await this.init();
@@ -164,52 +198,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
164
198
  }, isReinit ? 500 : 2000);
165
199
  }
166
200
 
167
- async ensureClient(): Promise<ReolinkCgiApi> {
168
- if (this.nvrApi) {
169
- return this.nvrApi;
170
- }
171
-
172
- const { ipAddress, username, password } = this.storageSettings.values;
173
- if (!ipAddress || !username || !password) {
174
- throw new Error('Missing NVR credentials');
175
- }
176
-
177
- const { ReolinkCgiApi } = await import("@apocaliss92/reolink-baichuan-js");
178
- const logger = this.getBaichuanLogger();
179
- this.nvrApi = new ReolinkCgiApi({
180
- host: ipAddress,
181
- username,
182
- password,
183
- logger,
184
- });
185
-
186
- await this.nvrApi.login();
187
- return this.nvrApi;
188
- }
189
-
190
- /**
191
- * List enriched VOD files (with proper parsing and detection info)
192
- * This uses the library's enrichVodFile which handles all parsing correctly
193
- */
194
- async listEnrichedVodFiles(params: {
195
- channel: number;
196
- start: Date;
197
- end: Date;
198
- streamType?: "main" | "sub";
199
- autoSearchByDay?: boolean;
200
- bypassCache?: boolean;
201
- }): Promise<Array<EnrichedRecordingFile>> {
202
- const api = await this.ensureClient();
203
- return await api.listEnrichedVodFiles({
204
- channel: params.channel,
205
- start: params.start,
206
- end: params.end,
207
- streamType: params.streamType,
208
- autoSearchByDay: params.autoSearchByDay,
209
- bypassCache: params.bypassCache,
210
- });
211
- }
212
-
213
201
  private forwardNativeEvent(ev: ReolinkSimpleEvent): void {
214
202
  const logger = this.getBaichuanLogger();
215
203
 
@@ -232,13 +220,15 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
232
220
  const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
233
221
 
234
222
  if (!targetCamera) {
235
- logger.debug(`No camera found for channel ${channel}, ignoring event`);
223
+ logger.debug(`No camera found for channel ${channel} (nativeId: ${nativeId}), ignoring event`);
236
224
  return;
237
225
  }
238
226
 
239
227
  // Convert event to camera's processEvents format
240
228
  const objects: string[] = [];
241
229
  let motion = false;
230
+ let isSleepingEvent = false;
231
+ let isOnlineEvent = false;
242
232
 
243
233
  switch (ev?.type) {
244
234
  case 'motion':
@@ -263,15 +253,34 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
263
253
  objects.push(ev.type);
264
254
  motion = true;
265
255
  break;
256
+ case 'awake':
257
+ case 'sleeping':
258
+ isSleepingEvent = true;
259
+ break;
260
+ case 'offline':
261
+ case 'online':
262
+ isOnlineEvent = true;
263
+ break;
266
264
  default:
267
265
  logger.error(`Unknown event type: ${ev?.type}`);
268
266
  return;
269
267
  }
270
268
 
271
- // Process events on the target camera
272
- targetCamera.processEvents({ motion, objects }).catch((e) => {
273
- logger.warn(`Error processing events for camera channel ${channel}`, e);
274
- });
269
+ if (isSleepingEvent) {
270
+ (targetCamera as ReolinkNativeBatteryCamera).updateSleepingState({
271
+ reason: 'NVR',
272
+ state: ev.type === 'sleeping' ? 'sleeping' : 'awake',
273
+ }).catch(() => { });
274
+ } if (isSleepingEvent) {
275
+ (targetCamera as ReolinkNativeBatteryCamera).updateOnlineState(
276
+ ev.type === 'online' ? true : false
277
+ ).catch(() => { });
278
+ } else {
279
+ // Process events on the target camera
280
+ targetCamera.processEvents({ motion, objects }).catch((e) => {
281
+ logger.warn(`Error processing events for camera channel ${channel}`, e);
282
+ });
283
+ }
275
284
  }
276
285
  catch (e) {
277
286
  logger.warn('Error in NVR Native event forwarder', e);
@@ -298,15 +307,11 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
298
307
  logger.log(`Starting NVR diagnostics...`);
299
308
 
300
309
  try {
301
- const cgiApi = await this.ensureClient();
310
+ const api = await this.ensureBaichuanClient();
302
311
 
303
- const diagnostics = await cgiApi.collectNvrDiagnostics({
312
+ await api.collectNvrDiagnostics({
304
313
  logger: this.console,
305
314
  });
306
-
307
- logger.log(`NVR diagnostics completed successfully.`);
308
-
309
- cgiApi.printNvrDiagnostics(diagnostics, this.console);
310
315
  } catch (e) {
311
316
  logger.error('Failed to run NVR diagnostics', e);
312
317
  throw e;
@@ -357,9 +362,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
357
362
 
358
363
  async init() {
359
364
  const logger = this.getBaichuanLogger();
360
-
361
- // Ensure both APIs are ready before proceeding
362
- const api = await this.ensureClient();
363
365
  await this.ensureBaichuanClient();
364
366
 
365
367
  await this.updateDeviceInfo();
@@ -367,7 +369,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
367
369
  await this.reinitEventSubscriptions();
368
370
 
369
371
  setInterval(async () => {
370
- if (this.processing || !api) {
372
+ if (this.processing) {
371
373
  return;
372
374
  }
373
375
  this.processing = true;
@@ -381,39 +383,41 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
381
383
 
382
384
  if (!this.lastNvrInfoCheck || now - this.lastNvrInfoCheck > 1000 * 60 * 5) {
383
385
  this.lastNvrInfoCheck = now;
384
- const { nvrData } = await api.getNvrInfo();
385
- const { devicesData, channelsResponse, response } = await api.getDevicesInfo();
386
- logger.log(`NVR info data fetched`);
387
- logger.debug(`${JSON.stringify({ nvrData, devicesData, channelsResponse, response })}`);
386
+ // const { nvrData } = await api.getNvrInfo();
387
+ // const { devicesData, channelsResponse, response } = await api.getDevicesInfo();
388
+ // logger.log(`NVR info data fetched`);
389
+ // logger.debug(`${JSON.stringify({ nvrData, devicesData, channelsResponse, response })}`);
388
390
 
389
391
  await this.discoverDevices(true);
390
392
  }
391
393
 
392
- // Only fetch and forward CGI events if CGI is selected as event source
394
+ const api = await this.ensureBaichuanClient();
395
+
393
396
  const { eventSource } = this.storageSettings.values;
397
+
394
398
  if (eventSource === 'CGI') {
395
399
  const eventsRes = await api.getAllChannelsEvents();
396
400
  this.forwardCgiEvents(eventsRes.parsed);
397
- }
398
401
 
399
- const { batteryInfoData, response } = await api.getAllChannelsBatteryInfo();
400
-
401
- logger.debug(`Battery info call result: ${JSON.stringify({ batteryInfoData, response })}`);
402
-
403
- this.cameraNativeMap.forEach((camera) => {
404
- if (camera) {
405
- const channel = camera.storageSettings.values.rtspChannel;
406
- const cameraBatteryData = batteryInfoData[channel];
407
- if (cameraBatteryData) {
408
- (camera as ReolinkNativeBatteryCamera).updateSleepingState({
409
- reason: 'NVR',
410
- state: cameraBatteryData.sleeping ? 'sleeping' : 'awake',
411
- idleMs: 0,
412
- lastRxAtMs: 0,
413
- }).catch(() => { });
402
+ const { batteryInfoData, response } = await api.getAllChannelsBatteryInfo();
403
+
404
+ logger.debug(`Battery info call result: ${JSON.stringify({ batteryInfoData, response })}`);
405
+
406
+ this.cameraNativeMap.forEach((camera) => {
407
+ if (camera) {
408
+ const channel = camera.storageSettings.values.rtspChannel;
409
+ const cameraBatteryData = batteryInfoData[channel];
410
+ if (cameraBatteryData) {
411
+ (camera as ReolinkNativeBatteryCamera).updateSleepingState({
412
+ reason: 'NVR',
413
+ state: cameraBatteryData.sleeping ? 'sleeping' : 'awake',
414
+ idleMs: 0,
415
+ lastRxAtMs: 0,
416
+ }).catch(() => { });
417
+ }
414
418
  }
415
- }
416
- });
419
+ });
420
+ }
417
421
  } catch (e) {
418
422
  logger.error('Error on events flow', e);
419
423
  } finally {
@@ -427,7 +431,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
427
431
 
428
432
  const { ipAddress } = this.storageSettings.values;
429
433
  try {
430
- const api = await this.ensureClient();
434
+ const api = await this.ensureBaichuanClient();
431
435
  const deviceData = await api.getInfo();
432
436
 
433
437
  await updateDeviceInfo({
@@ -492,33 +496,27 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
492
496
  async syncEntitiesFromRemote() {
493
497
  const logger = this.getBaichuanLogger();
494
498
 
495
- const cgiApi = await this.ensureClient();
496
- const { devicesData, channels } = await cgiApi.getDevicesInfo();
497
-
498
- // const api = await this.ensureBaichuanClient();
499
- // const devicesMap = api.getDevicesInfo();
500
- // const deviceEntries = Object.entries(devicesMap);
499
+ const api = await this.ensureBaichuanClient();
500
+ const { devices, channels } = await api.getDevicesInfo();
501
+ logger.log(devices, channels);
501
502
 
502
503
  if (!channels.length) {
503
- logger.debug(`No channels found, ${JSON.stringify({ channels, devicesData })}`);
504
+ logger.debug(`No channels found, ${JSON.stringify({ channels, devices })}`);
504
505
  return;
505
506
  }
506
507
 
507
508
  logger.log(`Sync entities from remote for ${channels.length} channels`);
508
509
 
509
- for (const channel of channels) {
510
- try {
511
- const { channelStatus, channelInfo, abilities } = devicesData[channel];
512
- const name = channelStatus?.name;
513
- const uid = channelStatus?.uid;
514
- const isBattery = !!(abilities?.battery?.ver ?? 0);
510
+ for (const deviceData of devices) {
511
+ const { isBattery, name, model, isDoorbell, uid, channel } = deviceData
515
512
 
513
+ try {
516
514
  const nativeId = this.buildNativeId(channel, uid, isBattery);
517
515
  const interfaces = [ScryptedInterface.VideoCamera];
518
516
  if (isBattery) {
519
517
  interfaces.push(ScryptedInterface.Battery);
520
518
  }
521
- const type = abilities.supportDoorbellLight ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera;
519
+ const type = isDoorbell ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera;
522
520
 
523
521
  const device: Device = {
524
522
  nativeId,
@@ -528,7 +526,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
528
526
  type,
529
527
  info: {
530
528
  manufacturer: 'Reolink',
531
- model: channelInfo?.typeInfo,
529
+ model,
532
530
  serialNumber: uid,
533
531
  }
534
532
  };
@@ -547,7 +545,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
547
545
  device,
548
546
  description: `${name} (Channel ${channel})`,
549
547
  rtspChannel: channel,
550
- deviceData: devicesData[channel],
548
+ deviceData,
551
549
  });
552
550
 
553
551
  logger.debug(`Discovered channel ${channel}: ${name}`);
@@ -556,7 +554,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
556
554
  }
557
555
  }
558
556
 
559
- logger.debug(`Channel discovery completed. ${JSON.stringify({ devicesData, channels })}`);
557
+ logger.debug(`Channel discovery completed. ${JSON.stringify({ devices, channels })}`);
560
558
  }
561
559
 
562
560
  async discoverDevices(scan?: boolean): Promise<DiscoveredDevice[]> {
@@ -597,11 +595,10 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
597
595
  await this.onDeviceEvent(ScryptedInterface.DeviceDiscovery, await this.discoverDevices());
598
596
 
599
597
  const isBattery = entry.device.interfaces.includes(ScryptedInterface.Battery);
600
- const { channelStatus } = entry.deviceData;
598
+ const { uid } = entry.deviceData;
601
599
 
602
600
  const { ReolinkBaichuanApi } = await import("@apocaliss92/reolink-baichuan-js");
603
601
  const transport = 'tcp';
604
- const uid = channelStatus?.uid;
605
602
  const normalizedUid = isBattery && uid ? normalizeUid(uid) : undefined;
606
603
  const baichuanApi = new ReolinkBaichuanApi({
607
604
  host: this.storageSettings.values.ipAddress,
@@ -640,7 +637,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
640
637
  device.storageSettings.values.rtspChannel = entry.rtspChannel;
641
638
  device.storageSettings.values.ipAddress = ipAddress;
642
639
  device.storageSettings.values.capabilities = capabilities;
643
- device.storageSettings.values.uid = entry.deviceData.channelStatus.uid;
640
+ device.storageSettings.values.uid = uid;
644
641
 
645
642
  this.discoveredDevices.delete(adopt.nativeId);
646
643
  return device?.id;
package/src/utils.ts CHANGED
@@ -329,6 +329,53 @@ export async function recordingFileToVideoClip(
329
329
  };
330
330
  }
331
331
 
332
+ /**
333
+ * Convert an array of RecordingFile or EnrichedRecordingFile to VideoClip array
334
+ * Uses recordingFileToVideoClip for each recording
335
+ * Handles both NVR (EnrichedRecordingFile) and device standalone (RecordingFile) cases
336
+ */
337
+ export async function recordingsToVideoClips(
338
+ recordings: (RecordingFile | EnrichedRecordingFile)[],
339
+ options: {
340
+ /** Fallback start date if recording doesn't have one */
341
+ fallbackStart: Date;
342
+ /** API instance to get playback URLs (optional, for device standalone recordings) */
343
+ api?: ReolinkBaichuanApi;
344
+ /** Logger for debug messages */
345
+ logger?: Console;
346
+ /** Plugin instance for generating webhook URLs */
347
+ plugin?: ScryptedDeviceBase;
348
+ /** Device ID for webhook URLs */
349
+ deviceId?: string;
350
+ /** Use webhook URLs instead of direct RTMP URLs */
351
+ useWebhook?: boolean;
352
+ /** Maximum number of clips to return (optional) */
353
+ count?: number;
354
+ }
355
+ ): Promise<VideoClip[]> {
356
+ const { fallbackStart, api, logger, plugin, deviceId, useWebhook, count } = options;
357
+ const clips: VideoClip[] = [];
358
+
359
+ for (const rec of recordings) {
360
+ try {
361
+ const clip = await recordingFileToVideoClip(rec, {
362
+ fallbackStart,
363
+ api,
364
+ logger,
365
+ plugin,
366
+ deviceId,
367
+ useWebhook,
368
+ });
369
+ clips.push(clip);
370
+ } catch (e) {
371
+ logger?.warn(`Failed to convert recording to video clip: fileName=${rec.fileName}`, e);
372
+ }
373
+ }
374
+
375
+ // Apply count limit if specified
376
+ return count ? clips.slice(0, count) : clips;
377
+ }
378
+
332
379
  /**
333
380
  * Generate webhook URLs for video clips
334
381
  */