@apocaliss92/scrypted-reolink-native 0.1.3 → 0.1.5
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 +90 -85
- package/src/connect.ts +136 -0
- package/src/intercom.ts +1 -1
- package/src/main.ts +133 -80
- package/src/nvr.ts +367 -0
- package/src/presets.ts +6 -6
- package/src/stream-utils.ts +26 -25
- package/src/utils.ts +31 -2
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.5",
|
|
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[]> {
|