@apocaliss92/scrypted-reolink-native 0.1.11 → 0.1.13

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.11",
3
+ "version": "0.1.13",
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",
@@ -180,6 +180,26 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
180
180
  }
181
181
  }
182
182
 
183
+ async checkRecordingAction(newBatteryLevel: number) {
184
+ const nvrDeviceId = this.plugin.nvrDeviceId;
185
+ if (nvrDeviceId && this.mixins.includes(nvrDeviceId)) {
186
+ const logger = this.getBaichuanLogger();
187
+
188
+ const settings = await this.thisDevice.getSettings();
189
+ const isRecording = !settings.find(setting => setting.key === 'recording:privacyMode')?.value;
190
+ const { lowThresholdBatteryRecording, highThresholdBatteryRecording } = this.storageSettings.values;
191
+
192
+ if (isRecording && newBatteryLevel < lowThresholdBatteryRecording) {
193
+ logger.log(`Recording is enabled, but battery level is below low threshold (${newBatteryLevel}% < ${lowThresholdBatteryRecording}%), disabling recording`);
194
+ await this.thisDevice.putSetting('recording:privacyMode', true);
195
+ } else if (!isRecording && newBatteryLevel > highThresholdBatteryRecording) {
196
+ logger.log(`Recording is disabled, but battery level is above high threshold (${newBatteryLevel}% > ${highThresholdBatteryRecording}%), enabling recording`);
197
+ await this.thisDevice.putSetting('recording:privacyMode', false);
198
+ }
199
+
200
+ }
201
+ }
202
+
183
203
  async updateBatteryInfo() {
184
204
  const api = await this.ensureClient();
185
205
  const channel = this.storageSettings.values.rtspChannel;
@@ -194,6 +214,8 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
194
214
  this.batteryLevel = batteryInfo.batteryPercent;
195
215
  this.lastBatteryLevel = batteryInfo.batteryPercent;
196
216
 
217
+ let shouldCheckRecordingAction = true;
218
+
197
219
  // Log only if battery level changed
198
220
  if (oldLevel !== undefined && oldLevel !== batteryInfo.batteryPercent) {
199
221
  if (batteryInfo.chargeStatus !== undefined) {
@@ -211,6 +233,12 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
211
233
  } else {
212
234
  this.getBaichuanLogger().log(`Battery level set: ${batteryInfo.batteryPercent}%`);
213
235
  }
236
+ } else {
237
+ shouldCheckRecordingAction = false;
238
+ }
239
+
240
+ if (shouldCheckRecordingAction) {
241
+ await this.checkRecordingAction(batteryInfo.batteryPercent);
214
242
  }
215
243
  }
216
244
  }
package/src/camera.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DebugOptions, ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
1
+ import type { ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
2
  import sdk, { MediaObject, ObjectsDetected, RequestPictureOptions, ResponsePictureOptions, ScryptedInterface, Setting } from "@scrypted/sdk";
3
3
  import { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
4
4
  import {
@@ -6,10 +6,6 @@ import {
6
6
  } from "./common";
7
7
  import { createBaichuanApi } from './connect';
8
8
  import ReolinkNativePlugin from "./main";
9
- import {
10
- buildVideoStreamOptionsFromRtspRtmp,
11
- fetchVideoStreamOptionsFromApi
12
- } from './stream-utils';
13
9
 
14
10
  export const moToB64 = async (mo: MediaObject) => {
15
11
  const bufferImage = await sdk.mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
package/src/common.ts CHANGED
@@ -1,16 +1,16 @@
1
- import type { DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
1
+ import type { DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, ReolinkSimpleEvent, StreamSamplingSelection } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
2
  import sdk, { BinarySensor, Brightness, Camera, Device, DeviceProvider, Intercom, MediaObject, MediaStreamUrl, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, RequestMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoTextOverlay, VideoTextOverlays } from "@scrypted/sdk";
3
3
  import { StorageSettings } from "@scrypted/sdk/storage-settings";
4
4
  import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
5
- import { BaseBaichuanClass, type BaichuanConnectionConfig, type BaichuanConnectionCallbacks } from "./baichuan-base";
6
- import { createBaichuanApi, normalizeUid, type BaichuanTransport } from "./connect";
5
+ import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
6
+ import { normalizeUid, type BaichuanTransport } from "./connect";
7
7
  import { convertDebugLogsToApiOptions, DebugLogDisplayNames, DebugLogOption, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
8
8
  import { ReolinkBaichuanIntercom } from "./intercom";
9
9
  import ReolinkNativePlugin from "./main";
10
10
  import { ReolinkNativeNvrDevice } from "./nvr";
11
11
  import { ReolinkPtzPresets } from "./presets";
12
12
  import {
13
- buildVideoStreamOptionsFromRtspRtmp,
13
+ buildVideoStreamOptions,
14
14
  createRfc4571MediaObjectFromStreamManager,
15
15
  expectedVideoTypeFromUrlMediaStreamOptions,
16
16
  isNativeStreamId,
@@ -19,6 +19,7 @@ import {
19
19
  StreamManager
20
20
  } from "./stream-utils";
21
21
  import { getDeviceInterfaces, updateDeviceInfo } from "./utils";
22
+ import path from 'path';
22
23
 
23
24
  export type CameraType = 'battery' | 'regular';
24
25
 
@@ -238,22 +239,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
238
239
  type: 'boolean',
239
240
  hide: true,
240
241
  },
241
- // snapshotCacheMinutes: {
242
- // title: "Snapshot Cache Minutes",
243
- // subgroup: 'Advanced',
244
- // description: "Return a cached snapshot if taken within the last N minutes.",
245
- // type: "number",
246
- // defaultValue: 60,
247
- // hide: true,
248
- // },
249
- batteryUpdateIntervalMinutes: {
250
- title: "Battery Update Interval (minutes)",
251
- subgroup: 'Advanced',
252
- description: "How often to wake up the camera and update battery status and snapshot (default: 60 minutes).",
253
- type: "number",
254
- defaultValue: 60,
255
- hide: true,
256
- },
257
242
  // Regular camera specific
258
243
  dispatchEvents: {
259
244
  subgroup: 'Advanced',
@@ -330,7 +315,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
330
315
  },
331
316
  // PTZ Presets
332
317
  presets: {
333
- group: 'PTZ',
318
+ subgroup: 'PTZ',
334
319
  title: 'Presets to enable',
335
320
  description: 'PTZ Presets in the format "id=name". Where id is the PTZ Preset identifier and name is a friendly name.',
336
321
  multiple: true,
@@ -358,11 +343,11 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
358
343
  description: 'How long a PTZ command moves before sending stop. Higher = more movement per click.',
359
344
  type: 'number',
360
345
  defaultValue: 300,
361
- group: 'PTZ',
346
+ subgroup: 'PTZ',
362
347
  hide: true,
363
348
  },
364
349
  ptzZoomStep: {
365
- group: 'PTZ',
350
+ subgroup: 'PTZ',
366
351
  title: 'PTZ Zoom Step',
367
352
  description: 'How much to change zoom per zoom command (in zoom factor units, where 1.0 is normal).',
368
353
  type: 'number',
@@ -370,7 +355,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
370
355
  hide: true,
371
356
  },
372
357
  ptzCreatePreset: {
373
- group: 'PTZ',
358
+ subgroup: 'PTZ',
374
359
  title: 'Create Preset',
375
360
  description: 'Enter a name and press Save to create a new PTZ preset at the current position.',
376
361
  type: 'string',
@@ -407,7 +392,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
407
392
  },
408
393
  },
409
394
  ptzSelectedPreset: {
410
- group: 'PTZ',
395
+ subgroup: 'PTZ',
411
396
  title: 'Selected Preset',
412
397
  description: 'Select the preset to update or delete. Format: "id=name".',
413
398
  type: 'string',
@@ -416,7 +401,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
416
401
  hide: true,
417
402
  },
418
403
  ptzUpdateSelectedPreset: {
419
- group: 'PTZ',
404
+ subgroup: 'PTZ',
420
405
  title: 'Update Selected Preset Position',
421
406
  description: 'Overwrite the selected preset with the current PTZ position.',
422
407
  type: 'button',
@@ -439,7 +424,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
439
424
  },
440
425
  },
441
426
  ptzDeleteSelectedPreset: {
442
- group: 'PTZ',
427
+ subgroup: 'PTZ',
443
428
  title: 'Delete Selected Preset',
444
429
  description: 'Delete the selected preset (firmware dependent).',
445
430
  type: 'button',
@@ -463,6 +448,47 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
463
448
  logger.log(`PTZ presets: delete ok (presetId=${presetId})`);
464
449
  },
465
450
  },
451
+ batteryUpdateIntervalMinutes: {
452
+ title: "Battery Update Interval (minutes)",
453
+ subgroup: 'Advanced',
454
+ description: "How often to wake up the camera and update battery status and snapshot (default: 60 minutes).",
455
+ type: "number",
456
+ defaultValue: 60,
457
+ hide: true,
458
+ },
459
+ lowThresholdBatteryRecording: {
460
+ title: "Low Threshold Battery Recording (%)",
461
+ subgroup: 'Recording',
462
+ description: "Battery level threshold below which recording is disabled (default: 15%).",
463
+ type: "number",
464
+ defaultValue: 15,
465
+ hide: true,
466
+ },
467
+ highThresholdBatteryRecording: {
468
+ title: "High Threshold Battery Recording (%)",
469
+ subgroup: 'Recording',
470
+ description: "Battery level threshold above which recording is enabled (default: 35%).",
471
+ type: "number",
472
+ defaultValue: 35,
473
+ hide: true,
474
+ },
475
+ diagnosticsOutputPath: {
476
+ title: "Diagnostics Output Path",
477
+ subgroup: 'Diagnostics',
478
+ description: "Directory where diagnostics files will be saved (default: plugin volume).",
479
+ type: "string",
480
+ defaultValue: path.join(process.env.SCRYPTED_PLUGIN_VOLUME, 'diagnostics', this.name),
481
+ },
482
+ diagnosticsRun: {
483
+ subgroup: 'Diagnostics',
484
+ title: 'Run Diagnostics',
485
+ description: 'Run all diagnostics and save results to the output path.',
486
+ type: 'button',
487
+ immediate: true,
488
+ onPut: async () => {
489
+ await this.runDiagnostics();
490
+ },
491
+ },
466
492
  });
467
493
 
468
494
  ptzPresets = new ReolinkPtzPresets(this);
@@ -497,15 +523,16 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
497
523
  initComplete?: boolean;
498
524
  resetBaichuanClient?(reason?: any): Promise<void>;
499
525
 
500
- protected nvrDevice?: any; // Optional reference to NVR device
526
+ protected nvrDevice?: ReolinkNativeNvrDevice;
527
+ thisDevice: Settings
501
528
 
502
529
  constructor(nativeId: string, public plugin: ReolinkNativePlugin, public options: CommonCameraMixinOptions) {
503
530
  super(nativeId);
504
- // Set protocol based on camera type
505
531
  this.protocol = !options.nvrDevice && options.type === 'battery' ? 'udp' : 'tcp';
506
532
 
507
533
  // Store NVR device reference if provided
508
534
  this.nvrDevice = options.nvrDevice;
535
+ this.thisDevice = sdk.systemManager.getDeviceById<Settings>(this.id);
509
536
 
510
537
  setTimeout(async () => {
511
538
  await this.parentInit();
@@ -554,6 +581,42 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
554
581
  return this.name || 'Camera';
555
582
  }
556
583
 
584
+ private async runDiagnostics(): Promise<void> {
585
+ const logger = this.getBaichuanLogger();
586
+ const outputPath = this.storageSettings.values.diagnosticsOutputPath || process.env.SCRYPTED_PLUGIN_VOLUME || "";
587
+
588
+ if (!outputPath) {
589
+ throw new Error('Diagnostics output path is required');
590
+ }
591
+
592
+ const channel = this.storageSettings.values.rtspChannel || 0;
593
+ const durationSeconds = 15;
594
+ const selection: StreamSamplingSelection = {
595
+ kinds: ['native'],
596
+ profiles: ['main', 'ext', 'sub'],
597
+ };
598
+
599
+ logger.log(`Starting diagnostics with parameters: outDir=${outputPath}, channel=${channel}, durationSeconds=${durationSeconds}, selection=${JSON.stringify(selection)}`);
600
+
601
+ try {
602
+ const api = await this.ensureClient();
603
+
604
+ const result = await api.runAllDiagnosticsConsecutively({
605
+ outDir: outputPath,
606
+ channel,
607
+ durationSeconds,
608
+ selection,
609
+ });
610
+
611
+ logger.log(`Diagnostics completed successfully. Output directory: ${result.runDir}`);
612
+ logger.log(`Diagnostics file: ${result.diagnosticsPath}`);
613
+ logger.log(`Streams directory: ${result.streamsDir}`);
614
+ } catch (e) {
615
+ logger.error('Failed to run diagnostics', e);
616
+ throw e;
617
+ }
618
+ }
619
+
557
620
  protected async onBeforeCleanup(): Promise<void> {
558
621
  // Unsubscribe from events if needed
559
622
  if (this.onSimpleEvent && this.baichuanApi) {
@@ -1261,7 +1324,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1261
1324
 
1262
1325
  const client = await this.ensureClient();
1263
1326
 
1264
- const { ipAddress, rtspChannel, isFromNvr } = this.storageSettings.values;
1327
+ const { ipAddress, rtspChannel } = this.storageSettings.values;
1265
1328
 
1266
1329
  try {
1267
1330
  await this.ensureNetPortCache();
@@ -1272,7 +1335,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1272
1335
  }
1273
1336
 
1274
1337
  try {
1275
- streams = await buildVideoStreamOptionsFromRtspRtmp(
1338
+ streams = await buildVideoStreamOptions(
1276
1339
  {
1277
1340
  client,
1278
1341
  ipAddress,
@@ -1501,6 +1564,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1501
1564
  // this.storageSettings.settings.snapshotCacheMinutes.hide = !isBattery;
1502
1565
  this.storageSettings.settings.uid.hide = !isBattery;
1503
1566
  this.storageSettings.settings.batteryUpdateIntervalMinutes.hide = !isBattery;
1567
+ this.storageSettings.settings.lowThresholdBatteryRecording.hide = !isBattery;
1568
+ this.storageSettings.settings.highThresholdBatteryRecording.hide = !isBattery;
1504
1569
 
1505
1570
  if (isBattery && !this.storageSettings.values.mixinsSetup) {
1506
1571
  try {
package/src/main.ts CHANGED
@@ -7,6 +7,14 @@ import { getDeviceInterfaces } from "./utils";
7
7
 
8
8
  class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
9
9
  devices = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera | ReolinkNativeNvrDevice>();
10
+ nvrDeviceId: string;
11
+
12
+ constructor(nativeId: string) {
13
+ super(nativeId);
14
+
15
+ const nvrDevice = sdk.systemManager.getDeviceByName('Scrypted NVR');
16
+ this.nvrDeviceId = nvrDevice?.id;
17
+ }
10
18
 
11
19
  getScryptedDeviceCreator(): string {
12
20
  return 'Reolink Native camera';
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  ReolinkBaichuanApi,
3
- StreamProfile,
4
3
  ScryptedRfc4571TcpServer,
4
+ StreamProfile,
5
5
  VideoType,
6
6
  } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
7
7
 
@@ -97,7 +97,7 @@ export async function fetchVideoStreamOptionsFromApi(
97
97
  return streams;
98
98
  }
99
99
 
100
- export async function buildVideoStreamOptionsFromRtspRtmp(
100
+ export async function buildVideoStreamOptions(
101
101
  props: {
102
102
  client: ReolinkBaichuanApi,
103
103
  ipAddress: string,