@apocaliss92/scrypted-reolink-native 0.4.0 → 0.4.2
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/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/accessories/autotracking.ts +150 -0
- package/src/accessories/floodlight.ts +92 -0
- package/src/accessories/index.ts +7 -0
- package/src/accessories/motion-floodlight.ts +171 -0
- package/src/accessories/motion-siren.ts +165 -0
- package/src/accessories/pir-sensor.ts +138 -0
- package/src/accessories/siren.ts +63 -0
- package/src/camera.ts +166 -450
- package/src/multiFocal.ts +1 -3
- package/src/utils.ts +1 -0
package/src/camera.ts
CHANGED
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
StreamManagerOptions,
|
|
81
81
|
} from "./stream-utils";
|
|
82
82
|
import {
|
|
83
|
+
autotrackingSuffix,
|
|
83
84
|
floodlightSuffix,
|
|
84
85
|
getDeviceInterfaces,
|
|
85
86
|
getVideoClipWebhookUrls,
|
|
@@ -91,6 +92,14 @@ import {
|
|
|
91
92
|
sirenSuffix,
|
|
92
93
|
updateDeviceInfo,
|
|
93
94
|
} from "./utils";
|
|
95
|
+
import {
|
|
96
|
+
ReolinkCameraAutotracking,
|
|
97
|
+
ReolinkCameraFloodlight,
|
|
98
|
+
ReolinkCameraMotionFloodlight,
|
|
99
|
+
ReolinkCameraMotionSiren,
|
|
100
|
+
ReolinkCameraPirSensor,
|
|
101
|
+
ReolinkCameraSiren,
|
|
102
|
+
} from "./accessories";
|
|
94
103
|
|
|
95
104
|
export type CameraType =
|
|
96
105
|
| "battery"
|
|
@@ -104,410 +113,6 @@ export interface ReolinkCameraOptions {
|
|
|
104
113
|
multiFocalDevice?: ReolinkNativeMultiFocalDevice; // Optional reference to multi-focal device
|
|
105
114
|
}
|
|
106
115
|
|
|
107
|
-
// Motion-siren: controls motion detection alarm (MD alarm)
|
|
108
|
-
class ReolinkCameraMotionSiren extends ScryptedDeviceBase implements OnOff {
|
|
109
|
-
private logger: Console;
|
|
110
|
-
|
|
111
|
-
constructor(
|
|
112
|
-
public camera: ReolinkCamera,
|
|
113
|
-
nativeId: string,
|
|
114
|
-
) {
|
|
115
|
-
super(nativeId);
|
|
116
|
-
this.logger = camera.getBaichuanLogger();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async turnOff(): Promise<void> {
|
|
120
|
-
this.logger.log(`Motion-siren toggle: turnOff (device=${this.nativeId})`);
|
|
121
|
-
this.on = false;
|
|
122
|
-
try {
|
|
123
|
-
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
124
|
-
await this.camera.withBaichuanRetry(async () => {
|
|
125
|
-
const api = await this.camera.ensureClient();
|
|
126
|
-
await api.setMotionAlarm(channel, false);
|
|
127
|
-
const mdEnabled = await api.getMotionState(channel);
|
|
128
|
-
this.on = mdEnabled;
|
|
129
|
-
});
|
|
130
|
-
this.logger.log(
|
|
131
|
-
`Motion-siren toggle: turnOff ok (device=${this.nativeId})`,
|
|
132
|
-
);
|
|
133
|
-
} catch (e) {
|
|
134
|
-
this.logger.error(
|
|
135
|
-
`Motion-siren toggle: turnOff failed (device=${this.nativeId})`,
|
|
136
|
-
e?.message || String(e),
|
|
137
|
-
);
|
|
138
|
-
throw e;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async turnOn(): Promise<void> {
|
|
143
|
-
this.logger.log(`Motion-siren toggle: turnOn (device=${this.nativeId})`);
|
|
144
|
-
this.on = true;
|
|
145
|
-
try {
|
|
146
|
-
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
147
|
-
await this.camera.withBaichuanRetry(async () => {
|
|
148
|
-
const api = await this.camera.ensureClient();
|
|
149
|
-
await api.setMotionAlarm(channel, true);
|
|
150
|
-
const mdEnabled = await api.getMotionState(channel);
|
|
151
|
-
this.on = mdEnabled;
|
|
152
|
-
});
|
|
153
|
-
this.logger.log(
|
|
154
|
-
`Motion-siren toggle: turnOn ok (device=${this.nativeId})`,
|
|
155
|
-
);
|
|
156
|
-
} catch (e) {
|
|
157
|
-
this.logger.error(
|
|
158
|
-
`Motion-siren toggle: turnOn failed (device=${this.nativeId})`,
|
|
159
|
-
e?.message || String(e),
|
|
160
|
-
);
|
|
161
|
-
throw e;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Siren: direct control (not motion-based)
|
|
167
|
-
class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
|
|
168
|
-
private logger: Console;
|
|
169
|
-
|
|
170
|
-
constructor(
|
|
171
|
-
public camera: ReolinkCamera,
|
|
172
|
-
nativeId: string,
|
|
173
|
-
) {
|
|
174
|
-
super(nativeId);
|
|
175
|
-
this.logger = camera.getBaichuanLogger();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async turnOff(): Promise<void> {
|
|
179
|
-
this.logger.log(`Siren toggle: turnOff (device=${this.nativeId})`);
|
|
180
|
-
this.on = false;
|
|
181
|
-
try {
|
|
182
|
-
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
183
|
-
await this.camera.withBaichuanRetry(async () => {
|
|
184
|
-
const api = await this.camera.ensureClient();
|
|
185
|
-
await api.setSiren(channel, false);
|
|
186
|
-
const sirenState = await api.getSiren(channel);
|
|
187
|
-
this.on = sirenState.enabled;
|
|
188
|
-
});
|
|
189
|
-
this.logger.log(`Siren toggle: turnOff ok (device=${this.nativeId})`);
|
|
190
|
-
} catch (e) {
|
|
191
|
-
this.logger.error(
|
|
192
|
-
`Siren toggle: turnOff failed (device=${this.nativeId})`,
|
|
193
|
-
e?.message || String(e),
|
|
194
|
-
);
|
|
195
|
-
throw e;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async turnOn(): Promise<void> {
|
|
200
|
-
this.logger.log(`Siren toggle: turnOn (device=${this.nativeId})`);
|
|
201
|
-
this.on = true;
|
|
202
|
-
try {
|
|
203
|
-
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
204
|
-
await this.camera.withBaichuanRetry(async () => {
|
|
205
|
-
const api = await this.camera.ensureClient();
|
|
206
|
-
await api.setSiren(channel, true);
|
|
207
|
-
const sirenState = await api.getSiren(channel);
|
|
208
|
-
this.on = sirenState.enabled;
|
|
209
|
-
});
|
|
210
|
-
this.logger.log(`Siren toggle: turnOn ok (device=${this.nativeId})`);
|
|
211
|
-
} catch (e) {
|
|
212
|
-
this.logger.error(
|
|
213
|
-
`Siren toggle: turnOn failed (device=${this.nativeId})`,
|
|
214
|
-
e?.message || String(e),
|
|
215
|
-
);
|
|
216
|
-
throw e;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Motion-floodlight: controls motion detection light (MD light)
|
|
222
|
-
class ReolinkCameraMotionFloodlight
|
|
223
|
-
extends ScryptedDeviceBase
|
|
224
|
-
implements OnOff, Brightness
|
|
225
|
-
{
|
|
226
|
-
private logger: Console;
|
|
227
|
-
|
|
228
|
-
constructor(
|
|
229
|
-
public camera: ReolinkCamera,
|
|
230
|
-
nativeId: string,
|
|
231
|
-
) {
|
|
232
|
-
super(nativeId);
|
|
233
|
-
this.logger = camera.getBaichuanLogger();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
async setBrightness(brightness: number): Promise<void> {
|
|
237
|
-
this.logger.log(
|
|
238
|
-
`Motion-floodlight toggle: setBrightness (device=${this.nativeId} brightness=${brightness})`,
|
|
239
|
-
);
|
|
240
|
-
this.brightness = brightness;
|
|
241
|
-
try {
|
|
242
|
-
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
243
|
-
await this.camera.withBaichuanRetry(async () => {
|
|
244
|
-
const api = await this.camera.ensureClient();
|
|
245
|
-
await api.setWhiteLedState(channel, undefined, brightness);
|
|
246
|
-
const state = await api.getWhiteLedState(channel);
|
|
247
|
-
this.on = !!state.enabled;
|
|
248
|
-
if (state.brightness !== undefined) {
|
|
249
|
-
this.brightness = state.brightness;
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
this.logger.log(
|
|
253
|
-
`Motion-floodlight toggle: setBrightness ok (device=${this.nativeId} brightness=${brightness})`,
|
|
254
|
-
);
|
|
255
|
-
} catch (e) {
|
|
256
|
-
this.logger.warn(
|
|
257
|
-
`Motion-floodlight toggle: setBrightness failed (device=${this.nativeId} brightness=${brightness})`,
|
|
258
|
-
e?.message || String(e),
|
|
259
|
-
);
|
|
260
|
-
throw e;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async turnOff(): Promise<void> {
|
|
265
|
-
this.logger.log(
|
|
266
|
-
`Motion-floodlight toggle: turnOff (device=${this.nativeId})`,
|
|
267
|
-
);
|
|
268
|
-
this.on = false;
|
|
269
|
-
try {
|
|
270
|
-
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
271
|
-
await this.camera.withBaichuanRetry(async () => {
|
|
272
|
-
const api = await this.camera.ensureClient();
|
|
273
|
-
// For motion-floodlight, use setWhiteLedState with task (cmd 290)
|
|
274
|
-
// Get current state and modify enable field for task
|
|
275
|
-
const currentState = await api.getWhiteLedState(channel);
|
|
276
|
-
await api.setWhiteLedState(channel, false, currentState.brightness);
|
|
277
|
-
const state = await api.getWhiteLedState(channel);
|
|
278
|
-
this.on = !!state.enabled;
|
|
279
|
-
if (state.brightness !== undefined) {
|
|
280
|
-
this.brightness = state.brightness;
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
this.logger.log(
|
|
284
|
-
`Motion-floodlight toggle: turnOff ok (device=${this.nativeId})`,
|
|
285
|
-
);
|
|
286
|
-
} catch (e) {
|
|
287
|
-
this.logger.warn(
|
|
288
|
-
`Motion-floodlight toggle: turnOff failed (device=${this.nativeId})`,
|
|
289
|
-
e?.message || String(e),
|
|
290
|
-
);
|
|
291
|
-
throw e;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async turnOn(): Promise<void> {
|
|
296
|
-
this.logger.log(
|
|
297
|
-
`Motion-floodlight toggle: turnOn (device=${this.nativeId})`,
|
|
298
|
-
);
|
|
299
|
-
this.on = true;
|
|
300
|
-
try {
|
|
301
|
-
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
302
|
-
await this.camera.withBaichuanRetry(async () => {
|
|
303
|
-
const api = await this.camera.ensureClient();
|
|
304
|
-
// For motion-floodlight, use setWhiteLedState with task (cmd 290)
|
|
305
|
-
// Get current state and modify enable field for task
|
|
306
|
-
const currentState = await api.getWhiteLedState(channel);
|
|
307
|
-
await api.setWhiteLedState(channel, true, currentState.brightness);
|
|
308
|
-
const state = await api.getWhiteLedState(channel);
|
|
309
|
-
this.on = !!state.enabled;
|
|
310
|
-
if (state.brightness !== undefined) {
|
|
311
|
-
this.brightness = state.brightness;
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
this.logger.log(
|
|
315
|
-
`Motion-floodlight toggle: turnOn ok (device=${this.nativeId})`,
|
|
316
|
-
);
|
|
317
|
-
} catch (e) {
|
|
318
|
-
this.logger.warn(
|
|
319
|
-
`Motion-floodlight toggle: turnOn failed (device=${this.nativeId})`,
|
|
320
|
-
e?.message || String(e),
|
|
321
|
-
);
|
|
322
|
-
throw e;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Floodlight: direct control (not motion-based)
|
|
328
|
-
class ReolinkCameraFloodlight
|
|
329
|
-
extends ScryptedDeviceBase
|
|
330
|
-
implements OnOff, Brightness
|
|
331
|
-
{
|
|
332
|
-
private logger: Console;
|
|
333
|
-
|
|
334
|
-
constructor(
|
|
335
|
-
public camera: ReolinkCamera,
|
|
336
|
-
nativeId: string,
|
|
337
|
-
) {
|
|
338
|
-
super(nativeId);
|
|
339
|
-
this.logger = camera.getBaichuanLogger();
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async setBrightness(brightness: number): Promise<void> {
|
|
343
|
-
this.logger.log(
|
|
344
|
-
`Floodlight toggle: setBrightness (device=${this.nativeId} brightness=${brightness})`,
|
|
345
|
-
);
|
|
346
|
-
this.brightness = brightness;
|
|
347
|
-
try {
|
|
348
|
-
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
349
|
-
await this.camera.withBaichuanRetry(async () => {
|
|
350
|
-
const api = await this.camera.ensureClient();
|
|
351
|
-
const currentState = await api.getWhiteLedState(channel);
|
|
352
|
-
await api.setWhiteLedState(channel, currentState.enabled, brightness);
|
|
353
|
-
const state = await api.getWhiteLedState(channel);
|
|
354
|
-
this.on = !!state.enabled;
|
|
355
|
-
if (state.brightness !== undefined) {
|
|
356
|
-
this.brightness = state.brightness;
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
this.logger.log(
|
|
360
|
-
`Floodlight toggle: setBrightness ok (device=${this.nativeId} brightness=${brightness})`,
|
|
361
|
-
);
|
|
362
|
-
} catch (e) {
|
|
363
|
-
this.logger.warn(
|
|
364
|
-
`Floodlight toggle: setBrightness failed (device=${this.nativeId} brightness=${brightness})`,
|
|
365
|
-
e?.message || String(e),
|
|
366
|
-
);
|
|
367
|
-
throw e;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async turnOff(): Promise<void> {
|
|
372
|
-
this.logger.log(`Floodlight toggle: turnOff (device=${this.nativeId})`);
|
|
373
|
-
this.on = false;
|
|
374
|
-
try {
|
|
375
|
-
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
376
|
-
await this.camera.withBaichuanRetry(async () => {
|
|
377
|
-
const api = await this.camera.ensureClient();
|
|
378
|
-
await api.setWhiteLedState(channel, false);
|
|
379
|
-
const state = await api.getWhiteLedState(channel);
|
|
380
|
-
this.on = !!state.enabled;
|
|
381
|
-
if (state.brightness !== undefined) {
|
|
382
|
-
this.brightness = state.brightness;
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
this.logger.log(
|
|
386
|
-
`Floodlight toggle: turnOff ok (device=${this.nativeId})`,
|
|
387
|
-
);
|
|
388
|
-
} catch (e) {
|
|
389
|
-
this.logger.warn(
|
|
390
|
-
`Floodlight toggle: turnOff failed (device=${this.nativeId})`,
|
|
391
|
-
e?.message || String(e),
|
|
392
|
-
);
|
|
393
|
-
throw e;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async turnOn(): Promise<void> {
|
|
398
|
-
this.logger.log(`Floodlight toggle: turnOn (device=${this.nativeId})`);
|
|
399
|
-
this.on = true;
|
|
400
|
-
try {
|
|
401
|
-
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
402
|
-
await this.camera.withBaichuanRetry(async () => {
|
|
403
|
-
const api = await this.camera.ensureClient();
|
|
404
|
-
await api.setWhiteLedState(channel, true);
|
|
405
|
-
const state = await api.getWhiteLedState(channel);
|
|
406
|
-
this.on = !!state.enabled;
|
|
407
|
-
if (state.brightness !== undefined) {
|
|
408
|
-
this.brightness = state.brightness;
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
this.logger.log(`Floodlight toggle: turnOn ok (device=${this.nativeId})`);
|
|
412
|
-
} catch (e) {
|
|
413
|
-
this.logger.warn(
|
|
414
|
-
`Floodlight toggle: turnOn failed (device=${this.nativeId})`,
|
|
415
|
-
e?.message || String(e),
|
|
416
|
-
);
|
|
417
|
-
throw e;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
class ReolinkCameraPirSensor
|
|
423
|
-
extends ScryptedDeviceBase
|
|
424
|
-
implements OnOff, Settings
|
|
425
|
-
{
|
|
426
|
-
storageSettings = new StorageSettings(this, {
|
|
427
|
-
sensitive: {
|
|
428
|
-
title: "PIR Sensitivity",
|
|
429
|
-
description: "Detection sensitivity/threshold (higher = more sensitive)",
|
|
430
|
-
type: "number",
|
|
431
|
-
defaultValue: 50,
|
|
432
|
-
range: [0, 100],
|
|
433
|
-
},
|
|
434
|
-
reduceAlarm: {
|
|
435
|
-
title: "Reduce False Alarms",
|
|
436
|
-
description: "Enable reduction of false alarm rate",
|
|
437
|
-
type: "boolean",
|
|
438
|
-
defaultValue: false,
|
|
439
|
-
},
|
|
440
|
-
interval: {
|
|
441
|
-
title: "PIR Detection Interval",
|
|
442
|
-
description: "Detection interval in seconds",
|
|
443
|
-
type: "number",
|
|
444
|
-
defaultValue: 5,
|
|
445
|
-
range: [1, 60],
|
|
446
|
-
},
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
constructor(
|
|
450
|
-
public camera: ReolinkCamera,
|
|
451
|
-
nativeId: string,
|
|
452
|
-
) {
|
|
453
|
-
super(nativeId);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
async getSettings(): Promise<Setting[]> {
|
|
457
|
-
const settings = await this.storageSettings.getSettings();
|
|
458
|
-
return settings;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
async putSetting(key: string, value: SettingValue): Promise<void> {
|
|
462
|
-
await this.storageSettings.putSetting(key, value);
|
|
463
|
-
|
|
464
|
-
// Apply the new settings to the camera
|
|
465
|
-
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
466
|
-
const enabled = this.on ? 1 : 0;
|
|
467
|
-
const sensitive = this.storageSettings.values.sensitive;
|
|
468
|
-
const reduceAlarm = this.storageSettings.values.reduceAlarm ? 1 : 0;
|
|
469
|
-
const interval = this.storageSettings.values.interval;
|
|
470
|
-
|
|
471
|
-
await this.camera.withBaichuanRetry(async () => {
|
|
472
|
-
const api = await this.camera.ensureClient();
|
|
473
|
-
await api.setPirInfo(channel, {
|
|
474
|
-
enable: enabled,
|
|
475
|
-
sensitive: sensitive,
|
|
476
|
-
reduceAlarm: reduceAlarm,
|
|
477
|
-
interval: interval,
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
async turnOff(): Promise<void> {
|
|
483
|
-
this.on = false;
|
|
484
|
-
await this.updatePirSettings();
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
async turnOn(): Promise<void> {
|
|
488
|
-
this.on = true;
|
|
489
|
-
await this.updatePirSettings();
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
private async updatePirSettings(): Promise<void> {
|
|
493
|
-
const channel = this.camera.storageSettings.values.rtspChannel;
|
|
494
|
-
const enabled = this.on ? 1 : 0;
|
|
495
|
-
const sensitive = this.storageSettings.values.sensitive;
|
|
496
|
-
const reduceAlarm = this.storageSettings.values.reduceAlarm ? 1 : 0;
|
|
497
|
-
const interval = this.storageSettings.values.interval;
|
|
498
|
-
|
|
499
|
-
await this.camera.withBaichuanRetry(async () => {
|
|
500
|
-
const api = await this.camera.ensureClient();
|
|
501
|
-
await api.setPirInfo(channel, {
|
|
502
|
-
enable: enabled,
|
|
503
|
-
sensitive: sensitive,
|
|
504
|
-
reduceAlarm: reduceAlarm,
|
|
505
|
-
interval: interval,
|
|
506
|
-
});
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
116
|
export class ReolinkCamera
|
|
512
117
|
extends BaseBaichuanClass
|
|
513
118
|
implements
|
|
@@ -1107,6 +712,7 @@ export class ReolinkCamera
|
|
|
1107
712
|
motionFloodlight?: ReolinkCameraMotionFloodlight;
|
|
1108
713
|
floodlight?: ReolinkCameraFloodlight;
|
|
1109
714
|
pirSensor?: ReolinkCameraPirSensor;
|
|
715
|
+
autotracking?: ReolinkCameraAutotracking;
|
|
1110
716
|
|
|
1111
717
|
private lastPicture: { mo: MediaObject; atMs: number } | undefined;
|
|
1112
718
|
private takePictureInFlight: Promise<MediaObject> | undefined;
|
|
@@ -1145,6 +751,17 @@ export class ReolinkCamera
|
|
|
1145
751
|
private periodicStarted = false;
|
|
1146
752
|
private statusPollTimer: NodeJS.Timeout | undefined;
|
|
1147
753
|
|
|
754
|
+
// Cooldown timestamps to prevent alignAuxDevicesState from overwriting recently set states
|
|
755
|
+
// Camera can take 10+ seconds to reflect state changes in its API response
|
|
756
|
+
auxDeviceCooldowns: {
|
|
757
|
+
motionSiren?: number;
|
|
758
|
+
siren?: number;
|
|
759
|
+
motionFloodlight?: number;
|
|
760
|
+
floodlight?: number;
|
|
761
|
+
pir?: number;
|
|
762
|
+
autotracking?: number;
|
|
763
|
+
} = {};
|
|
764
|
+
|
|
1148
765
|
constructor(
|
|
1149
766
|
nativeId: string,
|
|
1150
767
|
public plugin: ReolinkNativePlugin,
|
|
@@ -2235,6 +1852,7 @@ export class ReolinkCamera
|
|
|
2235
1852
|
hasSiren: false,
|
|
2236
1853
|
hasFloodlight: false,
|
|
2237
1854
|
hasPir: false,
|
|
1855
|
+
hasAutotracking: false,
|
|
2238
1856
|
isDoorbell: false,
|
|
2239
1857
|
};
|
|
2240
1858
|
}
|
|
@@ -2818,11 +2436,26 @@ export class ReolinkCamera
|
|
|
2818
2436
|
const logger = this.getBaichuanLogger();
|
|
2819
2437
|
logger.debug(`Reporting devices: ${JSON.stringify(abilities)}`);
|
|
2820
2438
|
|
|
2821
|
-
const { hasSiren, hasFloodlight, hasPir } = abilities;
|
|
2439
|
+
const { hasSiren, hasFloodlight, hasPir, hasAutotracking } = abilities;
|
|
2440
|
+
|
|
2441
|
+
// Define native IDs for all sub-devices
|
|
2442
|
+
const motionSirenNativeId = `${this.nativeId}${motionSirenSuffix}`;
|
|
2443
|
+
const sirenNativeId = `${this.nativeId}${sirenSuffix}`;
|
|
2444
|
+
const motionFloodlightNativeId = `${this.nativeId}${motionFloodlightSuffix}`;
|
|
2445
|
+
const floodlightNativeId = `${this.nativeId}${floodlightSuffix}`;
|
|
2446
|
+
const pirNativeId = `${this.nativeId}${pirSuffix}`;
|
|
2447
|
+
const autotrackingNativeId = `${this.nativeId}${autotrackingSuffix}`;
|
|
2448
|
+
|
|
2449
|
+
// Helper to safely remove a device only if it exists
|
|
2450
|
+
const safeRemoveDevice = (nativeId: string) => {
|
|
2451
|
+
const existingIds = sdk.deviceManager.getNativeIds();
|
|
2452
|
+
if (existingIds.includes(nativeId)) {
|
|
2453
|
+
sdk.deviceManager.onDeviceRemoved(nativeId);
|
|
2454
|
+
}
|
|
2455
|
+
};
|
|
2822
2456
|
|
|
2823
2457
|
// Create motion-siren device (motion detection alarm)
|
|
2824
2458
|
if (hasSiren) {
|
|
2825
|
-
const motionSirenNativeId = `${this.nativeId}${motionSirenSuffix}`;
|
|
2826
2459
|
const device: Device = {
|
|
2827
2460
|
providerNativeId: this.nativeId,
|
|
2828
2461
|
name: `${this.name} Motion-Siren`,
|
|
@@ -2834,11 +2467,14 @@ export class ReolinkCamera
|
|
|
2834
2467
|
type: ScryptedDeviceType.Siren,
|
|
2835
2468
|
};
|
|
2836
2469
|
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2470
|
+
} else {
|
|
2471
|
+
// Remove motion-siren device if capability is no longer available
|
|
2472
|
+
safeRemoveDevice(motionSirenNativeId);
|
|
2473
|
+
this.motionSiren = undefined;
|
|
2837
2474
|
}
|
|
2838
2475
|
|
|
2839
2476
|
// Create siren device (direct control)
|
|
2840
2477
|
if (hasSiren) {
|
|
2841
|
-
const sirenNativeId = `${this.nativeId}${sirenSuffix}`;
|
|
2842
2478
|
const device: Device = {
|
|
2843
2479
|
providerNativeId: this.nativeId,
|
|
2844
2480
|
name: `${this.name} Siren`,
|
|
@@ -2850,11 +2486,14 @@ export class ReolinkCamera
|
|
|
2850
2486
|
type: ScryptedDeviceType.Siren,
|
|
2851
2487
|
};
|
|
2852
2488
|
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2489
|
+
} else {
|
|
2490
|
+
// Remove siren device if capability is no longer available
|
|
2491
|
+
safeRemoveDevice(sirenNativeId);
|
|
2492
|
+
this.siren = undefined;
|
|
2853
2493
|
}
|
|
2854
2494
|
|
|
2855
2495
|
// Create motion-floodlight device (motion detection light)
|
|
2856
2496
|
if (hasFloodlight) {
|
|
2857
|
-
const motionFloodlightNativeId = `${this.nativeId}${motionFloodlightSuffix}`;
|
|
2858
2497
|
const device: Device = {
|
|
2859
2498
|
providerNativeId: this.nativeId,
|
|
2860
2499
|
name: `${this.name} Motion-Floodlight`,
|
|
@@ -2870,11 +2509,14 @@ export class ReolinkCamera
|
|
|
2870
2509
|
type: ScryptedDeviceType.Light,
|
|
2871
2510
|
};
|
|
2872
2511
|
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2512
|
+
} else {
|
|
2513
|
+
// Remove motion-floodlight device if capability is no longer available
|
|
2514
|
+
safeRemoveDevice(motionFloodlightNativeId);
|
|
2515
|
+
this.motionFloodlight = undefined;
|
|
2873
2516
|
}
|
|
2874
2517
|
|
|
2875
2518
|
// Create floodlight device (direct control)
|
|
2876
2519
|
if (hasFloodlight) {
|
|
2877
|
-
const floodlightNativeId = `${this.nativeId}${floodlightSuffix}`;
|
|
2878
2520
|
const device: Device = {
|
|
2879
2521
|
providerNativeId: this.nativeId,
|
|
2880
2522
|
name: `${this.name} Floodlight`,
|
|
@@ -2890,10 +2532,13 @@ export class ReolinkCamera
|
|
|
2890
2532
|
type: ScryptedDeviceType.Light,
|
|
2891
2533
|
};
|
|
2892
2534
|
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2535
|
+
} else {
|
|
2536
|
+
// Remove floodlight device if capability is no longer available
|
|
2537
|
+
safeRemoveDevice(floodlightNativeId);
|
|
2538
|
+
this.floodlight = undefined;
|
|
2893
2539
|
}
|
|
2894
2540
|
|
|
2895
2541
|
if (hasPir) {
|
|
2896
|
-
const pirNativeId = `${this.nativeId}${pirSuffix}`;
|
|
2897
2542
|
const device: Device = {
|
|
2898
2543
|
providerNativeId: this.nativeId,
|
|
2899
2544
|
name: `${this.name} PIR`,
|
|
@@ -2905,6 +2550,29 @@ export class ReolinkCamera
|
|
|
2905
2550
|
type: ScryptedDeviceType.Switch,
|
|
2906
2551
|
};
|
|
2907
2552
|
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2553
|
+
} else {
|
|
2554
|
+
// Remove PIR device if capability is no longer available
|
|
2555
|
+
safeRemoveDevice(pirNativeId);
|
|
2556
|
+
this.pirSensor = undefined;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// Create autotracking device (PTZ auto-tracking/smart track)
|
|
2560
|
+
if (hasAutotracking) {
|
|
2561
|
+
const device: Device = {
|
|
2562
|
+
providerNativeId: this.nativeId,
|
|
2563
|
+
name: `${this.name} Autotracking`,
|
|
2564
|
+
nativeId: autotrackingNativeId,
|
|
2565
|
+
info: {
|
|
2566
|
+
...(this.info || {}),
|
|
2567
|
+
},
|
|
2568
|
+
interfaces: [ScryptedInterface.OnOff, ScryptedInterface.Settings],
|
|
2569
|
+
type: ScryptedDeviceType.Switch,
|
|
2570
|
+
};
|
|
2571
|
+
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2572
|
+
} else {
|
|
2573
|
+
// Remove autotracking device if capability is no longer available
|
|
2574
|
+
safeRemoveDevice(autotrackingNativeId);
|
|
2575
|
+
this.autotracking = undefined;
|
|
2908
2576
|
}
|
|
2909
2577
|
}
|
|
2910
2578
|
|
|
@@ -3080,6 +2748,9 @@ export class ReolinkCamera
|
|
|
3080
2748
|
} else if (nativeId.endsWith(pirSuffix)) {
|
|
3081
2749
|
this.pirSensor ||= new ReolinkCameraPirSensor(this, nativeId);
|
|
3082
2750
|
return this.pirSensor;
|
|
2751
|
+
} else if (nativeId.endsWith(autotrackingSuffix)) {
|
|
2752
|
+
this.autotracking ||= new ReolinkCameraAutotracking(this, nativeId);
|
|
2753
|
+
return this.autotracking;
|
|
3083
2754
|
}
|
|
3084
2755
|
}
|
|
3085
2756
|
|
|
@@ -3102,6 +2773,8 @@ export class ReolinkCamera
|
|
|
3102
2773
|
this.floodlight = undefined;
|
|
3103
2774
|
} else if (nativeId.endsWith(pirSuffix)) {
|
|
3104
2775
|
this.pirSensor = undefined;
|
|
2776
|
+
} else if (nativeId.endsWith(autotrackingSuffix)) {
|
|
2777
|
+
this.autotracking = undefined;
|
|
3105
2778
|
}
|
|
3106
2779
|
}
|
|
3107
2780
|
|
|
@@ -3131,8 +2804,10 @@ export class ReolinkCamera
|
|
|
3131
2804
|
}
|
|
3132
2805
|
|
|
3133
2806
|
/**
|
|
3134
|
-
* Aligns auxiliary device states (siren, floodlight, PIR) with current API state.
|
|
2807
|
+
* Aligns auxiliary device states (siren, floodlight, PIR, autotracking) with current API state.
|
|
2808
|
+
* Fetches only the states for capabilities the device has.
|
|
3135
2809
|
* This should be called periodically for regular cameras and once when battery cameras wake up.
|
|
2810
|
+
* Respects cooldown periods after manual state changes to avoid overwriting user changes.
|
|
3136
2811
|
*/
|
|
3137
2812
|
async alignAuxDevicesState(): Promise<void> {
|
|
3138
2813
|
const logger = this.getBaichuanLogger();
|
|
@@ -3140,78 +2815,105 @@ export class ReolinkCamera
|
|
|
3140
2815
|
const api = await this.ensureClient();
|
|
3141
2816
|
|
|
3142
2817
|
const channel = this.storageSettings.values.rtspChannel;
|
|
3143
|
-
const { hasSiren, hasFloodlight, hasPir } =
|
|
2818
|
+
const { hasSiren, hasFloodlight, hasPir, hasAutotracking } =
|
|
2819
|
+
await this.getAbilities();
|
|
3144
2820
|
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
2821
|
+
// Cooldown period: 15 seconds after a manual state change
|
|
2822
|
+
// Camera can take 10+ seconds to reflect state changes in its API response
|
|
2823
|
+
const COOLDOWN_MS = 15_000;
|
|
2824
|
+
const now = Date.now();
|
|
2825
|
+
|
|
2826
|
+
const isInCooldown = (timestamp: number | undefined): boolean =>
|
|
2827
|
+
timestamp !== undefined && now - timestamp < COOLDOWN_MS;
|
|
2828
|
+
|
|
2829
|
+
// Align motion-siren state
|
|
2830
|
+
if (hasSiren && this.motionSiren) {
|
|
2831
|
+
if (isInCooldown(this.auxDeviceCooldowns.motionSiren)) {
|
|
2832
|
+
logger.log(`[alignAuxDevicesState] Skipping motionSiren (in cooldown)`);
|
|
2833
|
+
} else {
|
|
3148
2834
|
try {
|
|
3149
|
-
const
|
|
3150
|
-
|
|
2835
|
+
const audioTask = await api.getSirenOnMotion(channel);
|
|
2836
|
+
const enabled = audioTask?.body?.AudioTask?.enable === 1;
|
|
2837
|
+
this.motionSiren.on = enabled;
|
|
3151
2838
|
} catch (e) {
|
|
3152
|
-
logger.
|
|
3153
|
-
"Failed to align
|
|
2839
|
+
logger.warn(
|
|
2840
|
+
"Failed to align motionSiren state",
|
|
3154
2841
|
e?.message || String(e),
|
|
3155
2842
|
);
|
|
3156
2843
|
}
|
|
3157
2844
|
}
|
|
2845
|
+
}
|
|
3158
2846
|
|
|
3159
|
-
|
|
3160
|
-
|
|
2847
|
+
// Align siren state (direct control)
|
|
2848
|
+
if (hasSiren && this.siren) {
|
|
2849
|
+
if (isInCooldown(this.auxDeviceCooldowns.siren)) {
|
|
2850
|
+
logger.log(`[alignAuxDevicesState] Skipping siren (in cooldown)`);
|
|
2851
|
+
} else {
|
|
3161
2852
|
try {
|
|
3162
2853
|
const sirenState = await api.getSiren(channel);
|
|
3163
2854
|
this.siren.on = sirenState.enabled;
|
|
3164
2855
|
} catch (e) {
|
|
3165
|
-
logger.
|
|
2856
|
+
logger.warn("Failed to align siren state", e?.message || String(e));
|
|
3166
2857
|
}
|
|
3167
2858
|
}
|
|
2859
|
+
}
|
|
3168
2860
|
|
|
3169
|
-
|
|
3170
|
-
|
|
2861
|
+
// Align motion-floodlight state
|
|
2862
|
+
if (hasFloodlight && this.motionFloodlight) {
|
|
2863
|
+
if (isInCooldown(this.auxDeviceCooldowns.motionFloodlight)) {
|
|
2864
|
+
logger.log(
|
|
2865
|
+
`[alignAuxDevicesState] Skipping motionFloodlight (in cooldown)`,
|
|
2866
|
+
);
|
|
2867
|
+
} else {
|
|
3171
2868
|
try {
|
|
3172
|
-
const
|
|
3173
|
-
this.motionFloodlight.on =
|
|
3174
|
-
if (
|
|
3175
|
-
this.motionFloodlight.brightness =
|
|
2869
|
+
const flState = await api.getFloodlightOnMotion(channel);
|
|
2870
|
+
this.motionFloodlight.on = flState.floodlightOnMotion;
|
|
2871
|
+
if (flState.brightness !== undefined) {
|
|
2872
|
+
this.motionFloodlight.brightness = flState.brightness;
|
|
3176
2873
|
}
|
|
3177
2874
|
} catch (e) {
|
|
3178
|
-
logger.
|
|
3179
|
-
"Failed to align
|
|
2875
|
+
logger.warn(
|
|
2876
|
+
"Failed to align motionFloodlight state",
|
|
3180
2877
|
e?.message || String(e),
|
|
3181
2878
|
);
|
|
3182
2879
|
}
|
|
3183
2880
|
}
|
|
2881
|
+
}
|
|
3184
2882
|
|
|
3185
|
-
|
|
3186
|
-
|
|
2883
|
+
// Align floodlight state (direct control)
|
|
2884
|
+
if (hasFloodlight && this.floodlight) {
|
|
2885
|
+
if (isInCooldown(this.auxDeviceCooldowns.floodlight)) {
|
|
2886
|
+
logger.log(`[alignAuxDevicesState] Skipping floodlight (in cooldown)`);
|
|
2887
|
+
} else {
|
|
3187
2888
|
try {
|
|
3188
|
-
const
|
|
3189
|
-
this.floodlight.on =
|
|
3190
|
-
if (
|
|
3191
|
-
this.floodlight.brightness =
|
|
2889
|
+
const ledState = await api.getWhiteLedState(channel);
|
|
2890
|
+
this.floodlight.on = ledState.enabled;
|
|
2891
|
+
if (ledState.brightness !== undefined) {
|
|
2892
|
+
this.floodlight.brightness = ledState.brightness;
|
|
3192
2893
|
}
|
|
3193
2894
|
} catch (e) {
|
|
3194
|
-
logger.
|
|
2895
|
+
logger.warn(
|
|
3195
2896
|
"Failed to align floodlight state",
|
|
3196
2897
|
e?.message || String(e),
|
|
3197
2898
|
);
|
|
3198
2899
|
}
|
|
3199
2900
|
}
|
|
2901
|
+
}
|
|
3200
2902
|
|
|
3201
|
-
|
|
3202
|
-
|
|
2903
|
+
// Align PIR state
|
|
2904
|
+
if (hasPir && this.pirSensor) {
|
|
2905
|
+
if (isInCooldown(this.auxDeviceCooldowns.pir)) {
|
|
2906
|
+
logger.log(`[alignAuxDevicesState] Skipping pir (in cooldown)`);
|
|
2907
|
+
} else {
|
|
3203
2908
|
try {
|
|
3204
2909
|
const pirState = await api.getPirInfo(channel);
|
|
3205
2910
|
this.pirSensor.on = pirState.enabled;
|
|
3206
|
-
|
|
3207
|
-
// Update storage settings with current values from API
|
|
3208
2911
|
if (pirState.state) {
|
|
3209
2912
|
if (pirState.state.sensitive !== undefined) {
|
|
3210
2913
|
this.pirSensor.storageSettings.values.sensitive =
|
|
3211
2914
|
pirState.state.sensitive;
|
|
3212
2915
|
}
|
|
3213
2916
|
if (pirState.state.reduceAlarm !== undefined) {
|
|
3214
|
-
// Convert number (0/1) to boolean
|
|
3215
2917
|
this.pirSensor.storageSettings.values.reduceAlarm =
|
|
3216
2918
|
!!pirState.state.reduceAlarm;
|
|
3217
2919
|
}
|
|
@@ -3221,14 +2923,28 @@ export class ReolinkCamera
|
|
|
3221
2923
|
}
|
|
3222
2924
|
}
|
|
3223
2925
|
} catch (e) {
|
|
3224
|
-
logger.
|
|
2926
|
+
logger.warn("Failed to align PIR state", e?.message || String(e));
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
// Align autotracking state
|
|
2932
|
+
if (hasAutotracking && this.autotracking) {
|
|
2933
|
+
if (isInCooldown(this.auxDeviceCooldowns.autotracking)) {
|
|
2934
|
+
logger.log(
|
|
2935
|
+
`[alignAuxDevicesState] Skipping autotracking (in cooldown)`,
|
|
2936
|
+
);
|
|
2937
|
+
} else {
|
|
2938
|
+
try {
|
|
2939
|
+
const autotracking = await api.getAutotracking(channel);
|
|
2940
|
+
this.autotracking.on = autotracking.enabled;
|
|
2941
|
+
} catch (e) {
|
|
2942
|
+
logger.warn(
|
|
2943
|
+
"Failed to align autotracking state",
|
|
2944
|
+
e?.message || String(e),
|
|
2945
|
+
);
|
|
3225
2946
|
}
|
|
3226
2947
|
}
|
|
3227
|
-
} catch (e) {
|
|
3228
|
-
logger.error(
|
|
3229
|
-
"Failed to align auxiliary devices state",
|
|
3230
|
-
e?.message || String(e),
|
|
3231
|
-
);
|
|
3232
2948
|
}
|
|
3233
2949
|
}
|
|
3234
2950
|
|
|
@@ -3682,8 +3398,8 @@ export class ReolinkCamera
|
|
|
3682
3398
|
!!this.multiFocalDevice;
|
|
3683
3399
|
this.storageSettings.settings.clearVideoclipsCache.hide =
|
|
3684
3400
|
!!this.multiFocalDevice;
|
|
3685
|
-
|
|
3686
|
-
|
|
3401
|
+
this.storageSettings.settings.videoclipSource.hide =
|
|
3402
|
+
!!this.multiFocalDevice;
|
|
3687
3403
|
this.storageSettings.settings.videoclipStreamType.hide =
|
|
3688
3404
|
!!this.multiFocalDevice;
|
|
3689
3405
|
this.storageSettings.settings.videoclipsRegularChecks.hide =
|