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