@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/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.3",
4
- "description": "Reolink Native plugin for Scrypted",
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": {
@@ -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.getRtspChannel();
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.sleepCheckTimer = setInterval(() => {
105
- this.checkSleepingState().catch(() => { });
106
- }, 5_000);
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
- private async checkSleepingState(): Promise<void> {
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.getRtspChannel();
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
- const batteryInfo = await api.getBatteryInfo(channel);
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
- return this.withBaichuanRetry(async () => {
212
- try {
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
- } catch (e) {
219
- this.getLogger().error('Error taking snapshot', e);
220
- throw e;
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[]> {