@apocaliss92/scrypted-reolink-native 0.1.10 → 0.1.12

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.10",
3
+ "version": "0.1.12",
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",
@@ -35,7 +35,7 @@ export class BaichuanLogger implements Console {
35
35
  }
36
36
 
37
37
  private formatMessage(level: string, ...args: any[]): string {
38
- const timestamp = new Date().toISOString();
38
+ const timestamp = new Date().toLocaleString();
39
39
  const prefix = `[${this.deviceName}] [${timestamp}] [${level}]`;
40
40
  return `${prefix} ${args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ')}`;
41
41
  }
@@ -36,16 +36,19 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
36
36
  }
37
37
 
38
38
  async takePicture(options?: RequestPictureOptions): Promise<MediaObject> {
39
+ const logger = this.getBaichuanLogger();
39
40
  // Allow new snapshot if:
40
41
  // 1. forceNewSnapshot is true, OR
41
42
  // 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));
43
+ // const minSnapshotIntervalMs = 10_000; // 10 seconds
44
+ // const now = Date.now();
45
+ const shouldTakeNewSnapshot = this.forceNewSnapshot;
46
+ // const now = Date.now();
47
+ // const shouldTakeNewSnapshot = this.forceNewSnapshot ||
48
+ // (!this.sleeping && this.lastPicture && (now - this.lastPicture.atMs >= minSnapshotIntervalMs));
46
49
 
47
50
  if (!shouldTakeNewSnapshot && this.lastPicture) {
48
- this.getBaichuanLogger().debug(`Returning cached snapshot, taken at ${new Date(this.lastPicture.atMs).toLocaleString()}`);
51
+ logger.debug(`Returning cached snapshot, taken at ${new Date(this.lastPicture.atMs).toLocaleString()}`);
49
52
  return this.lastPicture.mo;
50
53
  }
51
54
 
@@ -53,7 +56,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
53
56
  return await this.takePictureInFlight;
54
57
  }
55
58
 
56
- this.getBaichuanLogger().log(`Taking new snapshot from camera (forceNewSnapshot: ${this.forceNewSnapshot})`);
59
+ logger.log(`Taking new snapshot from camera (forceNewSnapshot: ${this.forceNewSnapshot})`);
57
60
  this.forceNewSnapshot = false;
58
61
 
59
62
  this.takePictureInFlight = (async () => {
@@ -63,7 +66,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
63
66
  });
64
67
  const mo = await sdk.mediaManager.createMediaObject(snapshotBuffer, 'image/jpeg');
65
68
  this.lastPicture = { mo, atMs: Date.now() };
66
- this.getBaichuanLogger().log(`Snapshot taken at ${new Date(this.lastPicture.atMs).toLocaleString()}`);
69
+ logger.log(`Snapshot taken at ${new Date(this.lastPicture.atMs).toLocaleString()}`);
67
70
  return mo;
68
71
  })();
69
72
 
@@ -104,9 +107,10 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
104
107
 
105
108
  private startPeriodicTasks(): void {
106
109
  if (this.periodicStarted) return;
110
+ const logger = this.getBaichuanLogger();
107
111
  this.periodicStarted = true;
108
112
 
109
- this.getBaichuanLogger().log('Starting periodic tasks for battery camera');
113
+ logger.log('Starting periodic tasks for battery camera');
110
114
 
111
115
  // Check sleeping state every 5 seconds (non-blocking)
112
116
  if (!this.nvrDevice) {
@@ -117,7 +121,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
117
121
 
118
122
  if (!api) {
119
123
  if (!this.sleeping) {
120
- this.getBaichuanLogger().log('Camera is sleeping: no active Baichuan client');
124
+ logger.log('Camera is sleeping: no active Baichuan client');
121
125
  this.sleeping = true;
122
126
  }
123
127
  return;
@@ -126,7 +130,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
126
130
  const sleepStatus = api.getSleepStatus({ channel });
127
131
  await this.updateSleepingState(sleepStatus);
128
132
  } catch (e) {
129
- this.getBaichuanLogger().warn('Error checking sleeping state:', e);
133
+ logger.warn('Error checking sleeping state:', e);
130
134
  }
131
135
  }, 5_000);
132
136
  }
@@ -138,7 +142,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
138
142
  this.updateBatteryAndSnapshot().catch(() => { });
139
143
  }, updateIntervalMs);
140
144
 
141
- this.getBaichuanLogger().log(`Periodic tasks started: sleep check every 5s, battery update every ${batteryUpdateIntervalMinutes} minutes`);
145
+ logger.log(`Periodic tasks started: sleep check every 5s, battery update every ${batteryUpdateIntervalMinutes} minutes`);
142
146
  }
143
147
 
144
148
  async updateSleepingState(sleepStatus: SleepStatus): Promise<void> {
@@ -176,13 +180,33 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
176
180
  }
177
181
  }
178
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
+
179
203
  async updateBatteryInfo() {
180
204
  const api = await this.ensureClient();
181
205
  const channel = this.storageSettings.values.rtspChannel;
182
206
 
183
207
  const batteryInfo = await api.getBatteryInfo(channel);
184
208
  if (this.isBatteryInfoLoggingEnabled()) {
185
- this.getBaichuanLogger().debug('getBatteryInfo result:', JSON.stringify(batteryInfo));
209
+ this.getBaichuanLogger().debug('getBatteryInfo result:', JSON.stringify(batteryInfo));
186
210
  }
187
211
 
188
212
  if (batteryInfo.batteryPercent !== undefined) {
@@ -190,6 +214,8 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
190
214
  this.batteryLevel = batteryInfo.batteryPercent;
191
215
  this.lastBatteryLevel = batteryInfo.batteryPercent;
192
216
 
217
+ let shouldCheckRecordingAction = true;
218
+
193
219
  // Log only if battery level changed
194
220
  if (oldLevel !== undefined && oldLevel !== batteryInfo.batteryPercent) {
195
221
  if (batteryInfo.chargeStatus !== undefined) {
@@ -207,6 +233,12 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
207
233
  } else {
208
234
  this.getBaichuanLogger().log(`Battery level set: ${batteryInfo.batteryPercent}%`);
209
235
  }
236
+ } else {
237
+ shouldCheckRecordingAction = false;
238
+ }
239
+
240
+ if (shouldCheckRecordingAction) {
241
+ await this.checkRecordingAction(batteryInfo.batteryPercent);
210
242
  }
211
243
  }
212
244
  }
package/src/common.ts CHANGED
@@ -2,8 +2,8 @@ import type { DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, Reo
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";
@@ -254,6 +254,22 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
254
254
  defaultValue: 60,
255
255
  hide: true,
256
256
  },
257
+ lowThresholdBatteryRecording: {
258
+ title: "Low Threshold Battery Recording (%)",
259
+ subgroup: 'Recording',
260
+ description: "Battery level threshold below which recording is disabled (default: 15%).",
261
+ type: "number",
262
+ defaultValue: 15,
263
+ hide: true,
264
+ },
265
+ highThresholdBatteryRecording: {
266
+ title: "High Threshold Battery Recording (%)",
267
+ subgroup: 'Recording',
268
+ description: "Battery level threshold above which recording is enabled (default: 35%).",
269
+ type: "number",
270
+ defaultValue: 35,
271
+ hide: true,
272
+ },
257
273
  // Regular camera specific
258
274
  dispatchEvents: {
259
275
  subgroup: 'Advanced',
@@ -330,7 +346,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
330
346
  },
331
347
  // PTZ Presets
332
348
  presets: {
333
- group: 'PTZ',
349
+ subgroup: 'PTZ',
334
350
  title: 'Presets to enable',
335
351
  description: 'PTZ Presets in the format "id=name". Where id is the PTZ Preset identifier and name is a friendly name.',
336
352
  multiple: true,
@@ -358,11 +374,11 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
358
374
  description: 'How long a PTZ command moves before sending stop. Higher = more movement per click.',
359
375
  type: 'number',
360
376
  defaultValue: 300,
361
- group: 'PTZ',
377
+ subgroup: 'PTZ',
362
378
  hide: true,
363
379
  },
364
380
  ptzZoomStep: {
365
- group: 'PTZ',
381
+ subgroup: 'PTZ',
366
382
  title: 'PTZ Zoom Step',
367
383
  description: 'How much to change zoom per zoom command (in zoom factor units, where 1.0 is normal).',
368
384
  type: 'number',
@@ -370,7 +386,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
370
386
  hide: true,
371
387
  },
372
388
  ptzCreatePreset: {
373
- group: 'PTZ',
389
+ subgroup: 'PTZ',
374
390
  title: 'Create Preset',
375
391
  description: 'Enter a name and press Save to create a new PTZ preset at the current position.',
376
392
  type: 'string',
@@ -407,7 +423,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
407
423
  },
408
424
  },
409
425
  ptzSelectedPreset: {
410
- group: 'PTZ',
426
+ subgroup: 'PTZ',
411
427
  title: 'Selected Preset',
412
428
  description: 'Select the preset to update or delete. Format: "id=name".',
413
429
  type: 'string',
@@ -416,7 +432,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
416
432
  hide: true,
417
433
  },
418
434
  ptzUpdateSelectedPreset: {
419
- group: 'PTZ',
435
+ subgroup: 'PTZ',
420
436
  title: 'Update Selected Preset Position',
421
437
  description: 'Overwrite the selected preset with the current PTZ position.',
422
438
  type: 'button',
@@ -439,7 +455,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
439
455
  },
440
456
  },
441
457
  ptzDeleteSelectedPreset: {
442
- group: 'PTZ',
458
+ subgroup: 'PTZ',
443
459
  title: 'Delete Selected Preset',
444
460
  description: 'Delete the selected preset (firmware dependent).',
445
461
  type: 'button',
@@ -497,15 +513,16 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
497
513
  initComplete?: boolean;
498
514
  resetBaichuanClient?(reason?: any): Promise<void>;
499
515
 
500
- protected nvrDevice?: any; // Optional reference to NVR device
516
+ protected nvrDevice?: ReolinkNativeNvrDevice;
517
+ thisDevice: Settings
501
518
 
502
519
  constructor(nativeId: string, public plugin: ReolinkNativePlugin, public options: CommonCameraMixinOptions) {
503
520
  super(nativeId);
504
- // Set protocol based on camera type
505
521
  this.protocol = !options.nvrDevice && options.type === 'battery' ? 'udp' : 'tcp';
506
522
 
507
523
  // Store NVR device reference if provided
508
524
  this.nvrDevice = options.nvrDevice;
525
+ this.thisDevice = sdk.systemManager.getDeviceById<Settings>(this.id);
509
526
 
510
527
  setTimeout(async () => {
511
528
  await this.parentInit();
@@ -1277,7 +1294,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1277
1294
  client,
1278
1295
  ipAddress,
1279
1296
  cachedNetPort: this.cachedNetPort,
1280
- isFromNvr,
1297
+ nvrDevice: this.nvrDevice,
1281
1298
  rtspChannel,
1282
1299
  logger,
1283
1300
  },
@@ -1501,6 +1518,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1501
1518
  // this.storageSettings.settings.snapshotCacheMinutes.hide = !isBattery;
1502
1519
  this.storageSettings.settings.uid.hide = !isBattery;
1503
1520
  this.storageSettings.settings.batteryUpdateIntervalMinutes.hide = !isBattery;
1521
+ this.storageSettings.settings.lowThresholdBatteryRecording.hide = !isBattery;
1522
+ this.storageSettings.settings.highThresholdBatteryRecording.hide = !isBattery;
1504
1523
 
1505
1524
  if (isBattery && !this.storageSettings.values.mixinsSetup) {
1506
1525
  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';
@@ -12,6 +12,7 @@ import sdk, {
12
12
  } from "@scrypted/sdk";
13
13
 
14
14
  import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
15
+ import { ReolinkNativeNvrDevice } from "./nvr";
15
16
 
16
17
  export interface StreamManagerOptions {
17
18
  /**
@@ -101,12 +102,12 @@ export async function buildVideoStreamOptionsFromRtspRtmp(
101
102
  client: ReolinkBaichuanApi,
102
103
  ipAddress: string,
103
104
  cachedNetPort: { rtsp?: { port?: number; enable?: number }; rtmp?: { port?: number; enable?: number } },
104
- isFromNvr: boolean,
105
+ nvrDevice?: ReolinkNativeNvrDevice,
105
106
  rtspChannel: number,
106
107
  logger: Console
107
108
  },
108
109
  ): Promise<UrlMediaStreamOptions[]> {
109
- const { client, ipAddress, cachedNetPort, rtspChannel, logger } = props;
110
+ const { client, ipAddress, cachedNetPort, rtspChannel, logger, nvrDevice } = props;
110
111
  const rtspStreams: UrlMediaStreamOptions[] = [];
111
112
  const rtmpStreams: UrlMediaStreamOptions[] = [];
112
113
 
@@ -179,11 +180,21 @@ export async function buildVideoStreamOptionsFromRtspRtmp(
179
180
 
180
181
  const nativeStreams = await fetchVideoStreamOptionsFromApi(client, rtspChannel, logger);
181
182
 
182
- const streams: UrlMediaStreamOptions[] = [
183
- ...rtspStreams,
184
- ...rtmpStreams,
185
- ...nativeStreams,
186
- ];
183
+ let streams: UrlMediaStreamOptions[] = [];
184
+
185
+ if (nvrDevice && nvrDevice.info.model === 'HOMEHUB') {
186
+ streams = [
187
+ ...nativeStreams,
188
+ ...rtspStreams,
189
+ ...rtmpStreams,
190
+ ];
191
+ } else {
192
+ streams = [
193
+ ...rtspStreams,
194
+ ...rtmpStreams,
195
+ ...nativeStreams,
196
+ ];
197
+ }
187
198
 
188
199
  return streams;
189
200
  }