@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/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 } = await this.getAbilities();
2818
+ const { hasSiren, hasFloodlight, hasPir, hasAutotracking } =
2819
+ await this.getAbilities();
3144
2820
 
3145
- try {
3146
- // Align motion-siren state
3147
- if (hasSiren && this.motionSiren) {
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 mdEnabled = await api.getMotionState(channel);
3150
- this.motionSiren.on = mdEnabled;
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.error(
3153
- "Failed to align motion-siren state",
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
- // Align siren state (direct control)
3160
- if (hasSiren && this.siren) {
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.error("Failed to align siren state", e?.message || String(e));
2856
+ logger.warn("Failed to align siren state", e?.message || String(e));
3166
2857
  }
3167
2858
  }
2859
+ }
3168
2860
 
3169
- // Align motion-floodlight state
3170
- if (hasFloodlight && this.motionFloodlight) {
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 state = await api.getWhiteLedState(channel);
3173
- this.motionFloodlight.on = !!state.enabled;
3174
- if (state.brightness !== undefined) {
3175
- this.motionFloodlight.brightness = state.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.error(
3179
- "Failed to align motion-floodlight state",
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
- // Align floodlight state (direct control)
3186
- if (hasFloodlight && this.floodlight) {
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 state = await api.getWhiteLedState(channel);
3189
- this.floodlight.on = !!state.enabled;
3190
- if (state.brightness !== undefined) {
3191
- this.floodlight.brightness = state.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.error(
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
- // Align PIR state
3202
- if (hasPir && this.pirSensor) {
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.error("Failed to align PIR state", e?.message || String(e));
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
- // this.storageSettings.settings.videoclipSource.hide =
3686
- // !!this.multiFocalDevice;
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 =