@apocaliss92/scrypted-reolink-native 0.1.2 → 0.1.4
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/README.md +12 -3
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +2 -2
- package/src/camera-battery.ts +72 -64
- package/src/camera.ts +11 -13
- package/src/common.ts +82 -65
- package/src/connect.ts +136 -2
- package/src/intercom.ts +1 -1
- package/src/main.ts +135 -80
- package/src/nvr.ts +364 -0
- package/src/presets.ts +6 -6
- package/src/stream-utils.ts +26 -25
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apocaliss92/scrypted-reolink-native",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.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",
|
|
7
7
|
"scripts": {
|
package/src/camera-battery.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
1
|
+
import type { ReolinkBaichuanApi, SleepStatus } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
2
|
import sdk, {
|
|
3
3
|
type MediaObject,
|
|
4
4
|
RequestPictureOptions,
|
|
@@ -28,16 +28,18 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
28
28
|
return debugLogs.includes(DebugLogOption.BatteryInfo);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
constructor(nativeId: string, public plugin: ReolinkNativePlugin) {
|
|
31
|
+
constructor(nativeId: string, public plugin: ReolinkNativePlugin, nvrDevice?: any) {
|
|
32
32
|
super(nativeId, plugin, {
|
|
33
33
|
type: 'battery',
|
|
34
|
+
nvrDevice,
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
async takePicture(options?: RequestPictureOptions): Promise<MediaObject> {
|
|
38
|
-
const { snapshotCacheMinutes = 5 } = this.storageSettings.values;
|
|
39
|
-
const cacheMs = snapshotCacheMinutes * 60_000;
|
|
40
|
-
if (!this.forceNewSnapshot && cacheMs > 0 && this.lastPicture && Date.now() - this.lastPicture.atMs < cacheMs) {
|
|
39
|
+
// const { snapshotCacheMinutes = 5 } = this.storageSettings.values;
|
|
40
|
+
// const cacheMs = snapshotCacheMinutes * 60_000;
|
|
41
|
+
// if (!this.forceNewSnapshot && cacheMs > 0 && this.lastPicture && Date.now() - this.lastPicture.atMs < cacheMs) {
|
|
42
|
+
if (!this.forceNewSnapshot && this.lastPicture) {
|
|
41
43
|
this.console.log(`Returning cached snapshot, taken at ${new Date(this.lastPicture.atMs).toLocaleString()}`);
|
|
42
44
|
return this.lastPicture.mo;
|
|
43
45
|
}
|
|
@@ -50,7 +52,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
50
52
|
this.forceNewSnapshot = false;
|
|
51
53
|
|
|
52
54
|
this.takePictureInFlight = (async () => {
|
|
53
|
-
const channel = this.
|
|
55
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
54
56
|
const snapshotBuffer = await this.withBaichuanClient(async (api) => {
|
|
55
57
|
return await api.getSnapshot(channel);
|
|
56
58
|
});
|
|
@@ -75,6 +77,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
75
77
|
async init(): Promise<void> {
|
|
76
78
|
this.startPeriodicTasks();
|
|
77
79
|
await this.alignAuxDevicesState();
|
|
80
|
+
await this.updateBatteryInfo();
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
async release(): Promise<void> {
|
|
@@ -101,9 +104,27 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
101
104
|
this.console.log('Starting periodic tasks for battery camera');
|
|
102
105
|
|
|
103
106
|
// Check sleeping state every 5 seconds (non-blocking)
|
|
104
|
-
this.
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
+
if (!this.nvrDevice) {
|
|
108
|
+
this.sleepCheckTimer = setInterval(async () => {
|
|
109
|
+
try {
|
|
110
|
+
const api = this.baichuanApi;
|
|
111
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
112
|
+
|
|
113
|
+
if (!api) {
|
|
114
|
+
if (!this.sleeping) {
|
|
115
|
+
this.console.log('Camera is sleeping: no active Baichuan client');
|
|
116
|
+
this.sleeping = true;
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const sleepStatus = api.getSleepStatus({ channel });
|
|
122
|
+
await this.updateSleepingState(sleepStatus);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
this.console.warn('Error checking sleeping state:', e);
|
|
125
|
+
}
|
|
126
|
+
}, 5_000);
|
|
127
|
+
}
|
|
107
128
|
|
|
108
129
|
// Update battery and snapshot every N minutes
|
|
109
130
|
const { batteryUpdateIntervalMinutes = 10 } = this.storageSettings.values;
|
|
@@ -115,30 +136,13 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
115
136
|
this.console.log(`Periodic tasks started: sleep check every 5s, battery update every ${batteryUpdateIntervalMinutes} minutes`);
|
|
116
137
|
}
|
|
117
138
|
|
|
118
|
-
|
|
139
|
+
async updateSleepingState(sleepStatus: SleepStatus): Promise<void> {
|
|
119
140
|
try {
|
|
120
|
-
// IMPORTANT: do not call ensureClient() here.
|
|
121
|
-
// If the camera is asleep or disconnected, ensureClient() may reconnect/login and wake it.
|
|
122
|
-
const api = this.baichuanApi;
|
|
123
|
-
const channel = this.getRtspChannel();
|
|
124
|
-
|
|
125
|
-
// If there is no existing client, assume sleeping/idle.
|
|
126
|
-
if (!api) {
|
|
127
|
-
if (!this.sleeping) {
|
|
128
|
-
this.console.log('Camera is sleeping: no active Baichuan client');
|
|
129
|
-
this.sleeping = true;
|
|
130
|
-
}
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Passive sleep detection (no request sent to camera)
|
|
135
|
-
const sleepStatus = api.getSleepStatus({ channel });
|
|
136
141
|
if (this.isBatteryInfoLoggingEnabled()) {
|
|
137
142
|
this.console.log('getSleepStatus result:', JSON.stringify(sleepStatus));
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
if (sleepStatus.state === 'sleeping') {
|
|
141
|
-
// Camera is sleeping
|
|
142
146
|
if (!this.sleeping) {
|
|
143
147
|
this.console.log(`Camera is sleeping: ${sleepStatus.reason}`);
|
|
144
148
|
this.sleeping = true;
|
|
@@ -151,15 +155,12 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
151
155
|
this.sleeping = false;
|
|
152
156
|
}
|
|
153
157
|
|
|
154
|
-
// When camera wakes up (transition from sleeping to awake), align auxiliary devices state and force snapshot (once)
|
|
155
158
|
if (wasSleeping) {
|
|
156
159
|
this.alignAuxDevicesState().catch(() => { });
|
|
157
160
|
if (this.forceNewSnapshot) {
|
|
158
161
|
this.takePicture().catch(() => { });
|
|
159
162
|
}
|
|
160
163
|
}
|
|
161
|
-
// NOTE: We don't call getBatteryInfo() here anymore to avoid timeouts.
|
|
162
|
-
// Battery updates are handled by updateBatteryAndSnapshot() which properly wakes the camera.
|
|
163
164
|
} else {
|
|
164
165
|
// Unknown state
|
|
165
166
|
this.console.debug(`Sleep status unknown: ${sleepStatus.reason}`);
|
|
@@ -170,6 +171,41 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
async updateBatteryInfo() {
|
|
175
|
+
const api = await this.ensureClient();
|
|
176
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
177
|
+
|
|
178
|
+
const batteryInfo = await api.getBatteryInfo(channel);
|
|
179
|
+
if (this.isBatteryInfoLoggingEnabled()) {
|
|
180
|
+
this.console.log('getBatteryInfo result:', JSON.stringify(batteryInfo));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (batteryInfo.batteryPercent !== undefined) {
|
|
184
|
+
const oldLevel = this.lastBatteryLevel;
|
|
185
|
+
this.batteryLevel = batteryInfo.batteryPercent;
|
|
186
|
+
this.lastBatteryLevel = batteryInfo.batteryPercent;
|
|
187
|
+
|
|
188
|
+
// Log only if battery level changed
|
|
189
|
+
if (oldLevel !== undefined && oldLevel !== batteryInfo.batteryPercent) {
|
|
190
|
+
if (batteryInfo.chargeStatus !== undefined) {
|
|
191
|
+
// chargeStatus: "0"=charging, "1"=discharging, "2"=full
|
|
192
|
+
const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
|
|
193
|
+
this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}% (charging: ${charging})`);
|
|
194
|
+
} else {
|
|
195
|
+
this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}%`);
|
|
196
|
+
}
|
|
197
|
+
} else if (oldLevel === undefined) {
|
|
198
|
+
// First time setting battery level
|
|
199
|
+
if (batteryInfo.chargeStatus !== undefined) {
|
|
200
|
+
const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
|
|
201
|
+
this.console.log(`Battery level set: ${batteryInfo.batteryPercent}% (charging: ${charging})`);
|
|
202
|
+
} else {
|
|
203
|
+
this.console.log(`Battery level set: ${batteryInfo.batteryPercent}%`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
173
209
|
private async updateBatteryAndSnapshot(): Promise<void> {
|
|
174
210
|
// Prevent multiple simultaneous calls
|
|
175
211
|
if (this.batteryUpdateInProgress) {
|
|
@@ -179,7 +215,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
179
215
|
|
|
180
216
|
this.batteryUpdateInProgress = true;
|
|
181
217
|
try {
|
|
182
|
-
const channel = this.
|
|
218
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
183
219
|
const updateIntervalMinutes = this.storageSettings.values.batteryUpdateIntervalMinutes ?? 10;
|
|
184
220
|
this.console.log(`Force battery update interval started (every ${updateIntervalMinutes} minutes)`);
|
|
185
221
|
|
|
@@ -192,7 +228,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
192
228
|
|
|
193
229
|
// Check current sleep status
|
|
194
230
|
let sleepStatus = api.getSleepStatus({ channel });
|
|
195
|
-
|
|
231
|
+
|
|
196
232
|
// If camera is sleeping, wake it up
|
|
197
233
|
if (sleepStatus.state === 'sleeping') {
|
|
198
234
|
this.console.log('Camera is sleeping, waking up for periodic update...');
|
|
@@ -208,7 +244,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
208
244
|
const wakeTimeoutMs = 30000; // 30 seconds max
|
|
209
245
|
const startWakePoll = Date.now();
|
|
210
246
|
let awake = false;
|
|
211
|
-
|
|
247
|
+
|
|
212
248
|
while (Date.now() - startWakePoll < wakeTimeoutMs) {
|
|
213
249
|
await new Promise(resolve => setTimeout(resolve, 1000)); // Check every second
|
|
214
250
|
sleepStatus = api.getSleepStatus({ channel });
|
|
@@ -231,35 +267,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
231
267
|
// Now that camera is awake, update all states
|
|
232
268
|
// 1. Update battery info
|
|
233
269
|
try {
|
|
234
|
-
|
|
235
|
-
if (this.isBatteryInfoLoggingEnabled()) {
|
|
236
|
-
this.console.log('getBatteryInfo result:', JSON.stringify(batteryInfo));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (batteryInfo.batteryPercent !== undefined) {
|
|
240
|
-
const oldLevel = this.lastBatteryLevel;
|
|
241
|
-
this.batteryLevel = batteryInfo.batteryPercent;
|
|
242
|
-
this.lastBatteryLevel = batteryInfo.batteryPercent;
|
|
243
|
-
|
|
244
|
-
// Log only if battery level changed
|
|
245
|
-
if (oldLevel !== undefined && oldLevel !== batteryInfo.batteryPercent) {
|
|
246
|
-
if (batteryInfo.chargeStatus !== undefined) {
|
|
247
|
-
// chargeStatus: "0"=charging, "1"=discharging, "2"=full
|
|
248
|
-
const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
|
|
249
|
-
this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}% (charging: ${charging})`);
|
|
250
|
-
} else {
|
|
251
|
-
this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}%`);
|
|
252
|
-
}
|
|
253
|
-
} else if (oldLevel === undefined) {
|
|
254
|
-
// First time setting battery level
|
|
255
|
-
if (batteryInfo.chargeStatus !== undefined) {
|
|
256
|
-
const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
|
|
257
|
-
this.console.log(`Battery level set: ${batteryInfo.batteryPercent}% (charging: ${charging})`);
|
|
258
|
-
} else {
|
|
259
|
-
this.console.log(`Battery level set: ${batteryInfo.batteryPercent}%`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
270
|
+
await this.updateBatteryInfo();
|
|
263
271
|
} catch (e) {
|
|
264
272
|
this.console.warn('Failed to get battery info during periodic update:', e);
|
|
265
273
|
}
|
|
@@ -289,14 +297,14 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
289
297
|
async resetBaichuanClient(reason?: any): Promise<void> {
|
|
290
298
|
try {
|
|
291
299
|
this.unsubscribedToEvents?.();
|
|
292
|
-
|
|
300
|
+
|
|
293
301
|
// Close all stream servers before closing the main connection
|
|
294
302
|
// This ensures streams are properly cleaned up when using shared connection
|
|
295
303
|
if (this.streamManager) {
|
|
296
304
|
const reasonStr = reason?.message || reason?.toString?.() || 'connection reset';
|
|
297
305
|
await this.streamManager.closeAllStreams(reasonStr);
|
|
298
306
|
}
|
|
299
|
-
|
|
307
|
+
|
|
300
308
|
await this.baichuanApi?.close();
|
|
301
309
|
}
|
|
302
310
|
catch (e) {
|
package/src/camera.ts
CHANGED
|
@@ -32,9 +32,10 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
|
|
|
32
32
|
private statusPollTimer: NodeJS.Timeout | undefined;
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
constructor(nativeId: string, public plugin: ReolinkNativePlugin) {
|
|
35
|
+
constructor(nativeId: string, public plugin: ReolinkNativePlugin, nvrDevice?: any) {
|
|
36
36
|
super(nativeId, plugin, {
|
|
37
37
|
type: 'regular',
|
|
38
|
+
nvrDevice,
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -101,17 +102,14 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
|
|
|
101
102
|
|
|
102
103
|
async createStreamClient(): Promise<ReolinkBaichuanApi> {
|
|
103
104
|
const { ipAddress, username, password } = this.storageSettings.values;
|
|
104
|
-
if (!ipAddress || !username || !password) {
|
|
105
|
-
throw new Error('Missing camera credentials');
|
|
106
|
-
}
|
|
107
105
|
|
|
108
106
|
const debugOptions = this.getBaichuanDebugOptions();
|
|
109
107
|
const api = await createBaichuanApi(
|
|
110
108
|
{
|
|
111
109
|
inputs: {
|
|
112
110
|
host: ipAddress,
|
|
113
|
-
username,
|
|
114
|
-
password,
|
|
111
|
+
username: username,
|
|
112
|
+
password: password,
|
|
115
113
|
logger: this.console,
|
|
116
114
|
debugOptions
|
|
117
115
|
},
|
|
@@ -208,18 +206,18 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
|
|
|
208
206
|
}
|
|
209
207
|
|
|
210
208
|
async takePicture(options?: RequestPictureOptions) {
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
try {
|
|
210
|
+
return this.withBaichuanRetry(async () => {
|
|
213
211
|
const client = await this.ensureClient();
|
|
214
212
|
const snapshotBuffer = await client.getSnapshot();
|
|
215
213
|
const mo = await this.createMediaObject(snapshotBuffer, 'image/jpeg');
|
|
216
214
|
|
|
217
215
|
return mo;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
216
|
+
});
|
|
217
|
+
} catch (e) {
|
|
218
|
+
this.getLogger().error('Error taking snapshot', e);
|
|
219
|
+
throw e;
|
|
220
|
+
}
|
|
223
221
|
}
|
|
224
222
|
|
|
225
223
|
async getPictureOptions(): Promise<ResponsePictureOptions[]> {
|
package/src/common.ts
CHANGED
|
@@ -18,11 +18,13 @@ import {
|
|
|
18
18
|
StreamManager,
|
|
19
19
|
} from "./stream-utils";
|
|
20
20
|
import { getDeviceInterfaces } from "./utils";
|
|
21
|
+
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
21
22
|
|
|
22
23
|
export type CameraType = 'battery' | 'regular';
|
|
23
24
|
|
|
24
25
|
export interface CommonCameraMixinOptions {
|
|
25
26
|
type: CameraType;
|
|
27
|
+
nvrDevice?: ReolinkNativeNvrDevice; // Optional reference to NVR device
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
|
|
@@ -138,7 +140,7 @@ class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff, Settin
|
|
|
138
140
|
await this.storageSettings.putSetting(key, value);
|
|
139
141
|
|
|
140
142
|
// Apply the new settings to the camera
|
|
141
|
-
const channel = this.camera.
|
|
143
|
+
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
142
144
|
const enabled = this.on ? 1 : 0;
|
|
143
145
|
const sensitive = this.storageSettings.values.sensitive;
|
|
144
146
|
const reduceAlarm = this.storageSettings.values.reduceAlarm ? 1 : 0;
|
|
@@ -166,7 +168,7 @@ class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff, Settin
|
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
private async updatePirSettings(): Promise<void> {
|
|
169
|
-
const channel = this.camera.
|
|
171
|
+
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
170
172
|
const enabled = this.on ? 1 : 0;
|
|
171
173
|
const sensitive = this.storageSettings.values.sensitive;
|
|
172
174
|
const reduceAlarm = this.storageSettings.values.reduceAlarm ? 1 : 0;
|
|
@@ -227,24 +229,29 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
227
229
|
await this.credentialsChanged();
|
|
228
230
|
}
|
|
229
231
|
},
|
|
230
|
-
|
|
232
|
+
isFromNvr: {
|
|
231
233
|
type: 'boolean',
|
|
232
234
|
hide: true,
|
|
235
|
+
defaultValue: false,
|
|
233
236
|
},
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
subgroup: 'Advanced',
|
|
237
|
-
description: "Return a cached snapshot if taken within the last N minutes.",
|
|
238
|
-
type: "number",
|
|
239
|
-
defaultValue: 5,
|
|
237
|
+
mixinsSetup: {
|
|
238
|
+
type: 'boolean',
|
|
240
239
|
hide: true,
|
|
241
240
|
},
|
|
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
|
+
// },
|
|
242
249
|
batteryUpdateIntervalMinutes: {
|
|
243
250
|
title: "Battery Update Interval (minutes)",
|
|
244
251
|
subgroup: 'Advanced',
|
|
245
|
-
description: "How often to wake up the camera and update battery status and snapshot (default:
|
|
252
|
+
description: "How often to wake up the camera and update battery status and snapshot (default: 60 minutes).",
|
|
246
253
|
type: "number",
|
|
247
|
-
defaultValue:
|
|
254
|
+
defaultValue: 60,
|
|
248
255
|
hide: true,
|
|
249
256
|
},
|
|
250
257
|
// Regular camera specific
|
|
@@ -493,21 +500,15 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
493
500
|
initComplete?: boolean;
|
|
494
501
|
resetBaichuanClient?(reason?: any): Promise<void>;
|
|
495
502
|
|
|
503
|
+
protected nvrDevice?: any; // Optional reference to NVR device
|
|
504
|
+
|
|
496
505
|
constructor(nativeId: string, public plugin: ReolinkNativePlugin, public options: CommonCameraMixinOptions) {
|
|
497
506
|
super(nativeId);
|
|
498
507
|
// Set protocol based on camera type
|
|
499
|
-
this.protocol = options.type === 'battery' ? 'udp' : 'tcp';
|
|
508
|
+
this.protocol = !options.nvrDevice && options.type === 'battery' ? 'udp' : 'tcp';
|
|
500
509
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
getLogger: () => this.getLogger(),
|
|
504
|
-
credentials: {
|
|
505
|
-
username: this.storageSettings.values.username || '',
|
|
506
|
-
password: this.storageSettings.values.password || '',
|
|
507
|
-
},
|
|
508
|
-
// For battery cameras, we use a shared connection
|
|
509
|
-
sharedConnection: options.type === 'battery',
|
|
510
|
-
});
|
|
510
|
+
// Store NVR device reference if provided
|
|
511
|
+
this.nvrDevice = options.nvrDevice;
|
|
511
512
|
|
|
512
513
|
setTimeout(async () => {
|
|
513
514
|
await this.parentInit();
|
|
@@ -517,12 +518,6 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
517
518
|
throw new Error("Method not implemented.");
|
|
518
519
|
}
|
|
519
520
|
|
|
520
|
-
// Common method implementations
|
|
521
|
-
public getRtspChannel(): number {
|
|
522
|
-
const channel = this.storageSettings.values.rtspChannel;
|
|
523
|
-
return channel !== undefined ? Number(channel) : 0;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
521
|
public getAbilities(): DeviceCapabilities {
|
|
527
522
|
return this.storageSettings.values.capabilities;
|
|
528
523
|
}
|
|
@@ -589,7 +584,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
589
584
|
return;
|
|
590
585
|
}
|
|
591
586
|
|
|
592
|
-
const channel = this.
|
|
587
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
593
588
|
if (ev?.channel !== undefined && ev.channel !== channel) {
|
|
594
589
|
if (this.isEventLogsEnabled()) {
|
|
595
590
|
logger.debug(`Event channel ${ev.channel} does not match camera channel ${channel}, ignoring`);
|
|
@@ -637,6 +632,10 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
637
632
|
}
|
|
638
633
|
|
|
639
634
|
async subscribeToEvents(): Promise<void> {
|
|
635
|
+
if (this.nvrDevice) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
640
639
|
const logger = this.getLogger();
|
|
641
640
|
const selection = Array.from(this.getDispatchEventsSelection?.() ?? new Set()).sort();
|
|
642
641
|
const enabled = selection.length > 0;
|
|
@@ -676,7 +675,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
676
675
|
// VideoTextOverlays interface implementation
|
|
677
676
|
async getVideoTextOverlays(): Promise<Record<string, VideoTextOverlay>> {
|
|
678
677
|
const client = await this.ensureClient();
|
|
679
|
-
const channel = this.
|
|
678
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
680
679
|
|
|
681
680
|
let osd = this.storageSettings.values.cachedOsd;
|
|
682
681
|
|
|
@@ -698,7 +697,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
698
697
|
|
|
699
698
|
async setVideoTextOverlay(id: 'osdChannel' | 'osdTime', value: VideoTextOverlay): Promise<void> {
|
|
700
699
|
const client = await this.ensureClient();
|
|
701
|
-
const channel = this.
|
|
700
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
702
701
|
|
|
703
702
|
const osd = await client.getOsd(channel);
|
|
704
703
|
|
|
@@ -728,7 +727,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
728
727
|
return;
|
|
729
728
|
}
|
|
730
729
|
|
|
731
|
-
const channel = this.
|
|
730
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
732
731
|
|
|
733
732
|
// Preset navigation.
|
|
734
733
|
const preset = command.preset;
|
|
@@ -959,9 +958,10 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
959
958
|
});
|
|
960
959
|
}
|
|
961
960
|
|
|
962
|
-
// Settings methods
|
|
963
961
|
async getSettings(): Promise<Setting[]> {
|
|
964
|
-
|
|
962
|
+
const settings = await this.storageSettings.getSettings();
|
|
963
|
+
|
|
964
|
+
return settings;
|
|
965
965
|
}
|
|
966
966
|
|
|
967
967
|
async putSetting(key: string, value: string): Promise<void> {
|
|
@@ -994,7 +994,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
994
994
|
const ip = this.storageSettings.values.ipAddress;
|
|
995
995
|
try {
|
|
996
996
|
const api = await this.ensureClient();
|
|
997
|
-
const deviceData = await api.getInfo();
|
|
997
|
+
const deviceData = await api.getInfo(this.storageSettings.values.rtspChannel);
|
|
998
998
|
const info = this.info || {};
|
|
999
999
|
info.ip = ip;
|
|
1000
1000
|
|
|
@@ -1002,7 +1002,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1002
1002
|
info.firmware = deviceData?.firmwareVersion || deviceData?.firmVer;
|
|
1003
1003
|
info.version = deviceData?.hardwareVersion || deviceData?.boardInfo;
|
|
1004
1004
|
info.model = deviceData?.type || deviceData?.typeInfo;
|
|
1005
|
-
info.manufacturer = 'Reolink
|
|
1005
|
+
info.manufacturer = 'Reolink';
|
|
1006
1006
|
info.managementUrl = `http://${ip}`;
|
|
1007
1007
|
this.info = info;
|
|
1008
1008
|
} catch (e) {
|
|
@@ -1041,7 +1041,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1041
1041
|
}
|
|
1042
1042
|
|
|
1043
1043
|
async setSirenEnabled(enabled: boolean): Promise<void> {
|
|
1044
|
-
const channel = this.
|
|
1044
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
1045
1045
|
|
|
1046
1046
|
await this.withBaichuanRetry(async () => {
|
|
1047
1047
|
const api = await this.ensureClient();
|
|
@@ -1050,7 +1050,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1050
1050
|
}
|
|
1051
1051
|
|
|
1052
1052
|
async setFloodlightState(on?: boolean, brightness?: number): Promise<void> {
|
|
1053
|
-
const channel = this.
|
|
1053
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
1054
1054
|
|
|
1055
1055
|
await this.withBaichuanRetry(async () => {
|
|
1056
1056
|
const api = await this.ensureClient();
|
|
@@ -1059,7 +1059,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1059
1059
|
}
|
|
1060
1060
|
|
|
1061
1061
|
async setPirEnabled(enabled: boolean): Promise<void> {
|
|
1062
|
-
const channel = this.
|
|
1062
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
1063
1063
|
|
|
1064
1064
|
// Get current PIR settings from the sensor if available
|
|
1065
1065
|
let sensitive: number | undefined;
|
|
@@ -1091,7 +1091,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1091
1091
|
const api = this.baichuanApi;
|
|
1092
1092
|
if (!api) return;
|
|
1093
1093
|
|
|
1094
|
-
const channel = this.
|
|
1094
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
1095
1095
|
const { hasSiren, hasFloodlight, hasPir } = this.getAbilities();
|
|
1096
1096
|
|
|
1097
1097
|
try {
|
|
@@ -1234,16 +1234,13 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1234
1234
|
return [];
|
|
1235
1235
|
}
|
|
1236
1236
|
|
|
1237
|
-
// while (this.fetchingStreams) {
|
|
1238
|
-
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1239
|
-
// }
|
|
1240
1237
|
this.fetchingStreams = true;
|
|
1241
1238
|
|
|
1242
1239
|
let streams: UrlMediaStreamOptions[] = [];
|
|
1243
1240
|
|
|
1244
1241
|
const client = await this.ensureClient();
|
|
1245
1242
|
|
|
1246
|
-
const { ipAddress,
|
|
1243
|
+
const { ipAddress, rtspChannel, isFromNvr } = this.storageSettings.values;
|
|
1247
1244
|
|
|
1248
1245
|
try {
|
|
1249
1246
|
await this.ensureNetPortCache();
|
|
@@ -1255,12 +1252,14 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1255
1252
|
|
|
1256
1253
|
try {
|
|
1257
1254
|
streams = await buildVideoStreamOptionsFromRtspRtmp(
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1255
|
+
{
|
|
1256
|
+
client,
|
|
1257
|
+
ipAddress,
|
|
1258
|
+
cachedNetPort: this.cachedNetPort,
|
|
1259
|
+
isFromNvr,
|
|
1260
|
+
rtspChannel,
|
|
1261
|
+
logger,
|
|
1262
|
+
},
|
|
1264
1263
|
);
|
|
1265
1264
|
} catch (e) {
|
|
1266
1265
|
if (!this.isRecoverableBaichuanError?.(e)) {
|
|
@@ -1269,12 +1268,8 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1269
1268
|
this.cachedNetPort = undefined;
|
|
1270
1269
|
}
|
|
1271
1270
|
|
|
1272
|
-
|
|
1273
|
-
const nativeStreams = await fetchVideoStreamOptionsFromApi(client, rtspChannel, this.getLogger());
|
|
1274
|
-
streams = [...streams, ...nativeStreams];
|
|
1275
|
-
|
|
1276
1271
|
if (streams.length) {
|
|
1277
|
-
logger.log('Fetched video stream options', streams);
|
|
1272
|
+
logger.log('Fetched video stream options', { streams, netPort: this.cachedNetPort });
|
|
1278
1273
|
this.cachedVideoStreamOptions = streams;
|
|
1279
1274
|
return streams;
|
|
1280
1275
|
}
|
|
@@ -1308,7 +1303,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1308
1303
|
}
|
|
1309
1304
|
|
|
1310
1305
|
const profile = parseStreamProfileFromId(selected.id) || 'main';
|
|
1311
|
-
const channel = this.
|
|
1306
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
1312
1307
|
const streamKey = `${channel}_${profile}`;
|
|
1313
1308
|
const expectedVideoType = expectedVideoTypeFromUrlMediaStreamOptions(selected);
|
|
1314
1309
|
|
|
@@ -1356,10 +1351,6 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1356
1351
|
this.ensureClientPromise = (async () => {
|
|
1357
1352
|
const { ipAddress, username, password, uid } = this.storageSettings.values;
|
|
1358
1353
|
|
|
1359
|
-
if (!ipAddress || !username || !password) {
|
|
1360
|
-
throw new Error('Missing camera credentials');
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
1354
|
// Only tear down previous session if it exists and is not connected
|
|
1364
1355
|
if (this.baichuanApi) {
|
|
1365
1356
|
const isConnected = this.baichuanApi.client.isSocketConnected();
|
|
@@ -1409,8 +1400,8 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1409
1400
|
{
|
|
1410
1401
|
inputs: {
|
|
1411
1402
|
host: ipAddress,
|
|
1412
|
-
username,
|
|
1413
|
-
password,
|
|
1403
|
+
username: username,
|
|
1404
|
+
password: password,
|
|
1414
1405
|
uid: normalizedUid,
|
|
1415
1406
|
logger: this.console,
|
|
1416
1407
|
debugOptions,
|
|
@@ -1472,7 +1463,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1472
1463
|
this.refreshingState = true;
|
|
1473
1464
|
|
|
1474
1465
|
const logger = this.getLogger();
|
|
1475
|
-
const channel = this.
|
|
1466
|
+
const channel = this.storageSettings.values.rtspChannel;
|
|
1476
1467
|
|
|
1477
1468
|
try {
|
|
1478
1469
|
const { capabilities, abilities, support, presets, objects } = await this.withBaichuanRetry(async () => {
|
|
@@ -1492,7 +1483,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1492
1483
|
|
|
1493
1484
|
const device: Device = {
|
|
1494
1485
|
nativeId: this.nativeId,
|
|
1495
|
-
providerNativeId: this.plugin?.nativeId,
|
|
1486
|
+
providerNativeId: this.nvrDevice?.nativeId ?? this.plugin?.nativeId,
|
|
1496
1487
|
name: this.name,
|
|
1497
1488
|
interfaces,
|
|
1498
1489
|
type,
|
|
@@ -1564,8 +1555,21 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1564
1555
|
}
|
|
1565
1556
|
|
|
1566
1557
|
const isBattery = this.options.type === 'battery';
|
|
1558
|
+
const { username, password } = this.storageSettings.values;
|
|
1567
1559
|
|
|
1568
|
-
this.
|
|
1560
|
+
this.streamManager = new StreamManager({
|
|
1561
|
+
createStreamClient: () => this.createStreamClient(),
|
|
1562
|
+
getLogger: () => this.getLogger(),
|
|
1563
|
+
credentials: {
|
|
1564
|
+
username,
|
|
1565
|
+
password
|
|
1566
|
+
},
|
|
1567
|
+
// For battery cameras, we use a shared connection
|
|
1568
|
+
sharedConnection: isBattery,
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
// this.storageSettings.settings.snapshotCacheMinutes.hide = !isBattery;
|
|
1569
1573
|
this.storageSettings.settings.uid.hide = !isBattery;
|
|
1570
1574
|
this.storageSettings.settings.batteryUpdateIntervalMinutes.hide = !isBattery;
|
|
1571
1575
|
|
|
@@ -1591,6 +1595,19 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1591
1595
|
logger.warn('Failed to subscribe to Baichuan events', e);
|
|
1592
1596
|
}
|
|
1593
1597
|
|
|
1598
|
+
const { isFromNvr } = this.storageSettings.values;
|
|
1599
|
+
|
|
1600
|
+
if (isFromNvr && this.nvrDevice) {
|
|
1601
|
+
this.storageSettings.settings.username.hide = true;
|
|
1602
|
+
this.storageSettings.settings.password.hide = true;
|
|
1603
|
+
this.storageSettings.settings.ipAddress.hide = true;
|
|
1604
|
+
this.storageSettings.settings.uid.hide = true;
|
|
1605
|
+
|
|
1606
|
+
this.storageSettings.settings.username.defaultValue = this.nvrDevice.storageSettings.values.username;
|
|
1607
|
+
this.storageSettings.settings.password.defaultValue = this.nvrDevice.storageSettings.values.password;
|
|
1608
|
+
this.storageSettings.settings.ipAddress.defaultValue = this.nvrDevice.storageSettings.values.ipAddress;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1594
1611
|
await this.init();
|
|
1595
1612
|
this.initComplete = true;
|
|
1596
1613
|
}
|