@apocaliss92/scrypted-reolink-native 0.4.0 → 0.4.1
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 +158 -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,18 @@ 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}`;
|
|
2822
2448
|
|
|
2823
2449
|
// Create motion-siren device (motion detection alarm)
|
|
2824
2450
|
if (hasSiren) {
|
|
2825
|
-
const motionSirenNativeId = `${this.nativeId}${motionSirenSuffix}`;
|
|
2826
2451
|
const device: Device = {
|
|
2827
2452
|
providerNativeId: this.nativeId,
|
|
2828
2453
|
name: `${this.name} Motion-Siren`,
|
|
@@ -2834,11 +2459,14 @@ export class ReolinkCamera
|
|
|
2834
2459
|
type: ScryptedDeviceType.Siren,
|
|
2835
2460
|
};
|
|
2836
2461
|
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2462
|
+
} else {
|
|
2463
|
+
// Remove motion-siren device if capability is no longer available
|
|
2464
|
+
sdk.deviceManager.onDeviceRemoved(motionSirenNativeId);
|
|
2465
|
+
this.motionSiren = undefined;
|
|
2837
2466
|
}
|
|
2838
2467
|
|
|
2839
2468
|
// Create siren device (direct control)
|
|
2840
2469
|
if (hasSiren) {
|
|
2841
|
-
const sirenNativeId = `${this.nativeId}${sirenSuffix}`;
|
|
2842
2470
|
const device: Device = {
|
|
2843
2471
|
providerNativeId: this.nativeId,
|
|
2844
2472
|
name: `${this.name} Siren`,
|
|
@@ -2850,11 +2478,14 @@ export class ReolinkCamera
|
|
|
2850
2478
|
type: ScryptedDeviceType.Siren,
|
|
2851
2479
|
};
|
|
2852
2480
|
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2481
|
+
} else {
|
|
2482
|
+
// Remove siren device if capability is no longer available
|
|
2483
|
+
sdk.deviceManager.onDeviceRemoved(sirenNativeId);
|
|
2484
|
+
this.siren = undefined;
|
|
2853
2485
|
}
|
|
2854
2486
|
|
|
2855
2487
|
// Create motion-floodlight device (motion detection light)
|
|
2856
2488
|
if (hasFloodlight) {
|
|
2857
|
-
const motionFloodlightNativeId = `${this.nativeId}${motionFloodlightSuffix}`;
|
|
2858
2489
|
const device: Device = {
|
|
2859
2490
|
providerNativeId: this.nativeId,
|
|
2860
2491
|
name: `${this.name} Motion-Floodlight`,
|
|
@@ -2870,11 +2501,14 @@ export class ReolinkCamera
|
|
|
2870
2501
|
type: ScryptedDeviceType.Light,
|
|
2871
2502
|
};
|
|
2872
2503
|
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2504
|
+
} else {
|
|
2505
|
+
// Remove motion-floodlight device if capability is no longer available
|
|
2506
|
+
sdk.deviceManager.onDeviceRemoved(motionFloodlightNativeId);
|
|
2507
|
+
this.motionFloodlight = undefined;
|
|
2873
2508
|
}
|
|
2874
2509
|
|
|
2875
2510
|
// Create floodlight device (direct control)
|
|
2876
2511
|
if (hasFloodlight) {
|
|
2877
|
-
const floodlightNativeId = `${this.nativeId}${floodlightSuffix}`;
|
|
2878
2512
|
const device: Device = {
|
|
2879
2513
|
providerNativeId: this.nativeId,
|
|
2880
2514
|
name: `${this.name} Floodlight`,
|
|
@@ -2890,10 +2524,13 @@ export class ReolinkCamera
|
|
|
2890
2524
|
type: ScryptedDeviceType.Light,
|
|
2891
2525
|
};
|
|
2892
2526
|
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2527
|
+
} else {
|
|
2528
|
+
// Remove floodlight device if capability is no longer available
|
|
2529
|
+
sdk.deviceManager.onDeviceRemoved(floodlightNativeId);
|
|
2530
|
+
this.floodlight = undefined;
|
|
2893
2531
|
}
|
|
2894
2532
|
|
|
2895
2533
|
if (hasPir) {
|
|
2896
|
-
const pirNativeId = `${this.nativeId}${pirSuffix}`;
|
|
2897
2534
|
const device: Device = {
|
|
2898
2535
|
providerNativeId: this.nativeId,
|
|
2899
2536
|
name: `${this.name} PIR`,
|
|
@@ -2905,6 +2542,29 @@ export class ReolinkCamera
|
|
|
2905
2542
|
type: ScryptedDeviceType.Switch,
|
|
2906
2543
|
};
|
|
2907
2544
|
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2545
|
+
} else {
|
|
2546
|
+
// Remove PIR device if capability is no longer available
|
|
2547
|
+
sdk.deviceManager.onDeviceRemoved(pirNativeId);
|
|
2548
|
+
this.pirSensor = undefined;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// Create autotracking device (PTZ auto-tracking/smart track)
|
|
2552
|
+
if (hasAutotracking) {
|
|
2553
|
+
const device: Device = {
|
|
2554
|
+
providerNativeId: this.nativeId,
|
|
2555
|
+
name: `${this.name} Autotracking`,
|
|
2556
|
+
nativeId: autotrackingNativeId,
|
|
2557
|
+
info: {
|
|
2558
|
+
...(this.info || {}),
|
|
2559
|
+
},
|
|
2560
|
+
interfaces: [ScryptedInterface.OnOff, ScryptedInterface.Settings],
|
|
2561
|
+
type: ScryptedDeviceType.Switch,
|
|
2562
|
+
};
|
|
2563
|
+
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2564
|
+
} else {
|
|
2565
|
+
// Remove autotracking device if capability is no longer available
|
|
2566
|
+
sdk.deviceManager.onDeviceRemoved(autotrackingNativeId);
|
|
2567
|
+
this.autotracking = undefined;
|
|
2908
2568
|
}
|
|
2909
2569
|
}
|
|
2910
2570
|
|
|
@@ -3080,6 +2740,9 @@ export class ReolinkCamera
|
|
|
3080
2740
|
} else if (nativeId.endsWith(pirSuffix)) {
|
|
3081
2741
|
this.pirSensor ||= new ReolinkCameraPirSensor(this, nativeId);
|
|
3082
2742
|
return this.pirSensor;
|
|
2743
|
+
} else if (nativeId.endsWith(autotrackingSuffix)) {
|
|
2744
|
+
this.autotracking ||= new ReolinkCameraAutotracking(this, nativeId);
|
|
2745
|
+
return this.autotracking;
|
|
3083
2746
|
}
|
|
3084
2747
|
}
|
|
3085
2748
|
|
|
@@ -3102,6 +2765,8 @@ export class ReolinkCamera
|
|
|
3102
2765
|
this.floodlight = undefined;
|
|
3103
2766
|
} else if (nativeId.endsWith(pirSuffix)) {
|
|
3104
2767
|
this.pirSensor = undefined;
|
|
2768
|
+
} else if (nativeId.endsWith(autotrackingSuffix)) {
|
|
2769
|
+
this.autotracking = undefined;
|
|
3105
2770
|
}
|
|
3106
2771
|
}
|
|
3107
2772
|
|
|
@@ -3131,8 +2796,10 @@ export class ReolinkCamera
|
|
|
3131
2796
|
}
|
|
3132
2797
|
|
|
3133
2798
|
/**
|
|
3134
|
-
* Aligns auxiliary device states (siren, floodlight, PIR) with current API state.
|
|
2799
|
+
* Aligns auxiliary device states (siren, floodlight, PIR, autotracking) with current API state.
|
|
2800
|
+
* Fetches only the states for capabilities the device has.
|
|
3135
2801
|
* This should be called periodically for regular cameras and once when battery cameras wake up.
|
|
2802
|
+
* Respects cooldown periods after manual state changes to avoid overwriting user changes.
|
|
3136
2803
|
*/
|
|
3137
2804
|
async alignAuxDevicesState(): Promise<void> {
|
|
3138
2805
|
const logger = this.getBaichuanLogger();
|
|
@@ -3140,78 +2807,105 @@ export class ReolinkCamera
|
|
|
3140
2807
|
const api = await this.ensureClient();
|
|
3141
2808
|
|
|
3142
2809
|
const channel = this.storageSettings.values.rtspChannel;
|
|
3143
|
-
const { hasSiren, hasFloodlight, hasPir } =
|
|
2810
|
+
const { hasSiren, hasFloodlight, hasPir, hasAutotracking } =
|
|
2811
|
+
await this.getAbilities();
|
|
3144
2812
|
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
2813
|
+
// Cooldown period: 15 seconds after a manual state change
|
|
2814
|
+
// Camera can take 10+ seconds to reflect state changes in its API response
|
|
2815
|
+
const COOLDOWN_MS = 15_000;
|
|
2816
|
+
const now = Date.now();
|
|
2817
|
+
|
|
2818
|
+
const isInCooldown = (timestamp: number | undefined): boolean =>
|
|
2819
|
+
timestamp !== undefined && now - timestamp < COOLDOWN_MS;
|
|
2820
|
+
|
|
2821
|
+
// Align motion-siren state
|
|
2822
|
+
if (hasSiren && this.motionSiren) {
|
|
2823
|
+
if (isInCooldown(this.auxDeviceCooldowns.motionSiren)) {
|
|
2824
|
+
logger.log(`[alignAuxDevicesState] Skipping motionSiren (in cooldown)`);
|
|
2825
|
+
} else {
|
|
3148
2826
|
try {
|
|
3149
|
-
const
|
|
3150
|
-
|
|
2827
|
+
const audioTask = await api.getSirenOnMotion(channel);
|
|
2828
|
+
const enabled = audioTask?.body?.AudioTask?.enable === 1;
|
|
2829
|
+
this.motionSiren.on = enabled;
|
|
3151
2830
|
} catch (e) {
|
|
3152
|
-
logger.
|
|
3153
|
-
"Failed to align
|
|
2831
|
+
logger.warn(
|
|
2832
|
+
"Failed to align motionSiren state",
|
|
3154
2833
|
e?.message || String(e),
|
|
3155
2834
|
);
|
|
3156
2835
|
}
|
|
3157
2836
|
}
|
|
2837
|
+
}
|
|
3158
2838
|
|
|
3159
|
-
|
|
3160
|
-
|
|
2839
|
+
// Align siren state (direct control)
|
|
2840
|
+
if (hasSiren && this.siren) {
|
|
2841
|
+
if (isInCooldown(this.auxDeviceCooldowns.siren)) {
|
|
2842
|
+
logger.log(`[alignAuxDevicesState] Skipping siren (in cooldown)`);
|
|
2843
|
+
} else {
|
|
3161
2844
|
try {
|
|
3162
2845
|
const sirenState = await api.getSiren(channel);
|
|
3163
2846
|
this.siren.on = sirenState.enabled;
|
|
3164
2847
|
} catch (e) {
|
|
3165
|
-
logger.
|
|
2848
|
+
logger.warn("Failed to align siren state", e?.message || String(e));
|
|
3166
2849
|
}
|
|
3167
2850
|
}
|
|
2851
|
+
}
|
|
3168
2852
|
|
|
3169
|
-
|
|
3170
|
-
|
|
2853
|
+
// Align motion-floodlight state
|
|
2854
|
+
if (hasFloodlight && this.motionFloodlight) {
|
|
2855
|
+
if (isInCooldown(this.auxDeviceCooldowns.motionFloodlight)) {
|
|
2856
|
+
logger.log(
|
|
2857
|
+
`[alignAuxDevicesState] Skipping motionFloodlight (in cooldown)`,
|
|
2858
|
+
);
|
|
2859
|
+
} else {
|
|
3171
2860
|
try {
|
|
3172
|
-
const
|
|
3173
|
-
this.motionFloodlight.on =
|
|
3174
|
-
if (
|
|
3175
|
-
this.motionFloodlight.brightness =
|
|
2861
|
+
const flState = await api.getFloodlightOnMotion(channel);
|
|
2862
|
+
this.motionFloodlight.on = flState.floodlightOnMotion;
|
|
2863
|
+
if (flState.brightness !== undefined) {
|
|
2864
|
+
this.motionFloodlight.brightness = flState.brightness;
|
|
3176
2865
|
}
|
|
3177
2866
|
} catch (e) {
|
|
3178
|
-
logger.
|
|
3179
|
-
"Failed to align
|
|
2867
|
+
logger.warn(
|
|
2868
|
+
"Failed to align motionFloodlight state",
|
|
3180
2869
|
e?.message || String(e),
|
|
3181
2870
|
);
|
|
3182
2871
|
}
|
|
3183
2872
|
}
|
|
2873
|
+
}
|
|
3184
2874
|
|
|
3185
|
-
|
|
3186
|
-
|
|
2875
|
+
// Align floodlight state (direct control)
|
|
2876
|
+
if (hasFloodlight && this.floodlight) {
|
|
2877
|
+
if (isInCooldown(this.auxDeviceCooldowns.floodlight)) {
|
|
2878
|
+
logger.log(`[alignAuxDevicesState] Skipping floodlight (in cooldown)`);
|
|
2879
|
+
} else {
|
|
3187
2880
|
try {
|
|
3188
|
-
const
|
|
3189
|
-
this.floodlight.on =
|
|
3190
|
-
if (
|
|
3191
|
-
this.floodlight.brightness =
|
|
2881
|
+
const ledState = await api.getWhiteLedState(channel);
|
|
2882
|
+
this.floodlight.on = ledState.enabled;
|
|
2883
|
+
if (ledState.brightness !== undefined) {
|
|
2884
|
+
this.floodlight.brightness = ledState.brightness;
|
|
3192
2885
|
}
|
|
3193
2886
|
} catch (e) {
|
|
3194
|
-
logger.
|
|
2887
|
+
logger.warn(
|
|
3195
2888
|
"Failed to align floodlight state",
|
|
3196
2889
|
e?.message || String(e),
|
|
3197
2890
|
);
|
|
3198
2891
|
}
|
|
3199
2892
|
}
|
|
2893
|
+
}
|
|
3200
2894
|
|
|
3201
|
-
|
|
3202
|
-
|
|
2895
|
+
// Align PIR state
|
|
2896
|
+
if (hasPir && this.pirSensor) {
|
|
2897
|
+
if (isInCooldown(this.auxDeviceCooldowns.pir)) {
|
|
2898
|
+
logger.log(`[alignAuxDevicesState] Skipping pir (in cooldown)`);
|
|
2899
|
+
} else {
|
|
3203
2900
|
try {
|
|
3204
2901
|
const pirState = await api.getPirInfo(channel);
|
|
3205
2902
|
this.pirSensor.on = pirState.enabled;
|
|
3206
|
-
|
|
3207
|
-
// Update storage settings with current values from API
|
|
3208
2903
|
if (pirState.state) {
|
|
3209
2904
|
if (pirState.state.sensitive !== undefined) {
|
|
3210
2905
|
this.pirSensor.storageSettings.values.sensitive =
|
|
3211
2906
|
pirState.state.sensitive;
|
|
3212
2907
|
}
|
|
3213
2908
|
if (pirState.state.reduceAlarm !== undefined) {
|
|
3214
|
-
// Convert number (0/1) to boolean
|
|
3215
2909
|
this.pirSensor.storageSettings.values.reduceAlarm =
|
|
3216
2910
|
!!pirState.state.reduceAlarm;
|
|
3217
2911
|
}
|
|
@@ -3221,14 +2915,28 @@ export class ReolinkCamera
|
|
|
3221
2915
|
}
|
|
3222
2916
|
}
|
|
3223
2917
|
} catch (e) {
|
|
3224
|
-
logger.
|
|
2918
|
+
logger.warn("Failed to align PIR state", e?.message || String(e));
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// Align autotracking state
|
|
2924
|
+
if (hasAutotracking && this.autotracking) {
|
|
2925
|
+
if (isInCooldown(this.auxDeviceCooldowns.autotracking)) {
|
|
2926
|
+
logger.log(
|
|
2927
|
+
`[alignAuxDevicesState] Skipping autotracking (in cooldown)`,
|
|
2928
|
+
);
|
|
2929
|
+
} else {
|
|
2930
|
+
try {
|
|
2931
|
+
const autotracking = await api.getAutotracking(channel);
|
|
2932
|
+
this.autotracking.on = autotracking.enabled;
|
|
2933
|
+
} catch (e) {
|
|
2934
|
+
logger.warn(
|
|
2935
|
+
"Failed to align autotracking state",
|
|
2936
|
+
e?.message || String(e),
|
|
2937
|
+
);
|
|
3225
2938
|
}
|
|
3226
2939
|
}
|
|
3227
|
-
} catch (e) {
|
|
3228
|
-
logger.error(
|
|
3229
|
-
"Failed to align auxiliary devices state",
|
|
3230
|
-
e?.message || String(e),
|
|
3231
|
-
);
|
|
3232
2940
|
}
|
|
3233
2941
|
}
|
|
3234
2942
|
|
|
@@ -3682,8 +3390,8 @@ export class ReolinkCamera
|
|
|
3682
3390
|
!!this.multiFocalDevice;
|
|
3683
3391
|
this.storageSettings.settings.clearVideoclipsCache.hide =
|
|
3684
3392
|
!!this.multiFocalDevice;
|
|
3685
|
-
|
|
3686
|
-
|
|
3393
|
+
this.storageSettings.settings.videoclipSource.hide =
|
|
3394
|
+
!!this.multiFocalDevice;
|
|
3687
3395
|
this.storageSettings.settings.videoclipStreamType.hide =
|
|
3688
3396
|
!!this.multiFocalDevice;
|
|
3689
3397
|
this.storageSettings.settings.videoclipsRegularChecks.hide =
|