@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/.vscode/settings.json +1 -1
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/baichuan-base.ts +1 -1
- package/src/camera-battery.ts +44 -12
- package/src/common.ts +31 -12
- package/src/main.ts +8 -0
- package/src/stream-utils.ts +18 -7
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/baichuan-base.ts
CHANGED
|
@@ -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().
|
|
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
|
}
|
package/src/camera-battery.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
377
|
+
subgroup: 'PTZ',
|
|
362
378
|
hide: true,
|
|
363
379
|
},
|
|
364
380
|
ptzZoomStep: {
|
|
365
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
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
|
-
|
|
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';
|
package/src/stream-utils.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
}
|