@apocaliss92/scrypted-reolink-native 0.4.35 → 0.4.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apocaliss92/scrypted-reolink-native",
3
- "version": "0.4.35",
3
+ "version": "0.4.37",
4
4
  "description": "Use any reolink camera with Scrypted, even older/unsupported models without HTTP protocol support",
5
5
  "author": "@apocaliss92",
6
6
  "license": "Apache",
@@ -0,0 +1,131 @@
1
+ import {
2
+ OnOff,
3
+ ScryptedDeviceBase,
4
+ Setting,
5
+ Settings,
6
+ SettingValue,
7
+ } from "@scrypted/sdk";
8
+ import { StorageSettings } from "@scrypted/sdk/storage-settings";
9
+ import type { ReolinkCamera } from "../camera";
10
+
11
+ /**
12
+ * Chime: enable/disable the hardwired chime on the doorbell.
13
+ * turnOn() enables the chime; turnOff() disables (mutes) it.
14
+ * Additional parameters (chime type, timing) are configurable via Settings.
15
+ */
16
+ export class ReolinkCameraChime
17
+ extends ScryptedDeviceBase
18
+ implements OnOff, Settings
19
+ {
20
+ private get logger(): Console {
21
+ return this.camera.getBaichuanLogger();
22
+ }
23
+
24
+ storageSettings = new StorageSettings(this, {
25
+ chimeType: {
26
+ title: "Chime Type",
27
+ description:
28
+ 'Chime type string reported by the device (e.g. "dingdong", "single", "dual"). Leave empty to use the current device value.',
29
+ type: "string",
30
+ defaultValue: "",
31
+ },
32
+ time: {
33
+ title: "Chime Duration",
34
+ description:
35
+ "Chime timing/duration value (device-specific). Leave 0 to use the current device value.",
36
+ type: "number",
37
+ defaultValue: 0,
38
+ },
39
+ });
40
+
41
+ constructor(
42
+ public camera: ReolinkCamera,
43
+ nativeId: string,
44
+ ) {
45
+ super(nativeId);
46
+ }
47
+
48
+ async getSettings(): Promise<Setting[]> {
49
+ return this.storageSettings.getSettings();
50
+ }
51
+
52
+ async putSetting(key: string, value: SettingValue): Promise<void> {
53
+ await this.storageSettings.putSetting(key, value);
54
+ await this.applySettings();
55
+ }
56
+
57
+ private async applySettings(): Promise<void> {
58
+ const channel = this.camera.storageSettings.values.rtspChannel;
59
+ const chimeType = this.storageSettings.values.chimeType || undefined;
60
+ const time = this.storageSettings.values.time || undefined;
61
+ try {
62
+ await this.camera.withBaichuanRetry(async () => {
63
+ const api = await this.camera.ensureClient();
64
+ await api.setHardwiredChime(
65
+ { enabled: !!this.on, type: chimeType, time },
66
+ channel,
67
+ );
68
+ });
69
+ } catch (e: any) {
70
+ this.logger.error(
71
+ `Chime: applySettings failed (device=${this.nativeId})`,
72
+ e?.message || String(e),
73
+ );
74
+ throw e;
75
+ }
76
+ }
77
+
78
+ async turnOn(): Promise<void> {
79
+ const channel = this.camera.storageSettings.values.rtspChannel;
80
+ this.logger.log(`Chime: enable (device=${this.nativeId})`);
81
+ this.on = true;
82
+ this.camera.auxDeviceCooldowns.chime = Date.now();
83
+ try {
84
+ const chimeType = this.storageSettings.values.chimeType || undefined;
85
+ const time = this.storageSettings.values.time || undefined;
86
+ await this.camera.withBaichuanRetry(async () => {
87
+ const api = await this.camera.ensureClient();
88
+ const state = await api.setHardwiredChime(
89
+ { enabled: true, type: chimeType, time },
90
+ channel,
91
+ );
92
+ this.on = state.enabled;
93
+ });
94
+ this.logger.log(`Chime: enable ok (device=${this.nativeId})`);
95
+ } catch (e: any) {
96
+ this.on = false;
97
+ this.logger.error(
98
+ `Chime: enable failed (device=${this.nativeId})`,
99
+ e?.message || String(e),
100
+ );
101
+ throw e;
102
+ }
103
+ }
104
+
105
+ async turnOff(): Promise<void> {
106
+ const channel = this.camera.storageSettings.values.rtspChannel;
107
+ this.logger.log(`Chime: disable (device=${this.nativeId})`);
108
+ this.on = false;
109
+ this.camera.auxDeviceCooldowns.chime = Date.now();
110
+ try {
111
+ const chimeType = this.storageSettings.values.chimeType || undefined;
112
+ const time = this.storageSettings.values.time || undefined;
113
+ await this.camera.withBaichuanRetry(async () => {
114
+ const api = await this.camera.ensureClient();
115
+ const state = await api.setHardwiredChime(
116
+ { enabled: false, type: chimeType, time },
117
+ channel,
118
+ );
119
+ this.on = state.enabled;
120
+ });
121
+ this.logger.log(`Chime: disable ok (device=${this.nativeId})`);
122
+ } catch (e: any) {
123
+ this.on = true;
124
+ this.logger.error(
125
+ `Chime: disable failed (device=${this.nativeId})`,
126
+ e?.message || String(e),
127
+ );
128
+ throw e;
129
+ }
130
+ }
131
+ }
@@ -5,3 +5,4 @@ export { ReolinkCameraMotionFloodlight } from "./motion-floodlight";
5
5
  export { ReolinkCameraFloodlight } from "./floodlight";
6
6
  export { ReolinkCameraPirSensor } from "./pir-sensor";
7
7
  export { ReolinkCameraAutotracking } from "./autotracking";
8
+ export { ReolinkCameraChime } from "./chime";
package/src/camera.ts CHANGED
@@ -47,6 +47,7 @@ import path from "path";
47
47
  import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
48
48
  import {
49
49
  ReolinkCameraAutotracking,
50
+ ReolinkCameraChime,
50
51
  ReolinkCameraFloodlight,
51
52
  ReolinkCameraMotionFloodlight,
52
53
  ReolinkCameraMotionSiren,
@@ -76,6 +77,7 @@ import {
76
77
  } from "./stream-utils";
77
78
  import {
78
79
  autotrackingSuffix,
80
+ chimeSuffix,
79
81
  floodlightSuffix,
80
82
  getDeviceInterfaces,
81
83
  getVideoClipWebhookUrls,
@@ -658,6 +660,7 @@ export class ReolinkCamera
658
660
  floodlight?: ReolinkCameraFloodlight;
659
661
  pirSensor?: ReolinkCameraPirSensor;
660
662
  autotracking?: ReolinkCameraAutotracking;
663
+ chime?: ReolinkCameraChime;
661
664
 
662
665
  private lastPicture: { mo: MediaObject; atMs: number } | undefined;
663
666
  private takePictureInFlight: Promise<MediaObject> | undefined;
@@ -706,6 +709,7 @@ export class ReolinkCamera
706
709
  floodlight?: number;
707
710
  pir?: number;
708
711
  autotracking?: number;
712
+ chime?: number;
709
713
  } = {};
710
714
 
711
715
  constructor(
@@ -1693,6 +1697,7 @@ export class ReolinkCamera
1693
1697
  hasPir: false,
1694
1698
  hasAutotracking: false,
1695
1699
  isDoorbell: false,
1700
+ hasChime: false,
1696
1701
  };
1697
1702
  }
1698
1703
  }
@@ -2275,7 +2280,7 @@ export class ReolinkCamera
2275
2280
  const logger = this.getBaichuanLogger();
2276
2281
  logger.debug(`Reporting devices: ${JSON.stringify(abilities)}`);
2277
2282
 
2278
- const { hasSiren, hasFloodlight, hasPir, hasAutotracking } = abilities;
2283
+ const { hasSiren, hasFloodlight, hasPir, hasAutotracking, hasChime } = abilities;
2279
2284
 
2280
2285
  // Define native IDs for all sub-devices
2281
2286
  const motionSirenNativeId = `${this.nativeId}${motionSirenSuffix}`;
@@ -2284,6 +2289,7 @@ export class ReolinkCamera
2284
2289
  const floodlightNativeId = `${this.nativeId}${floodlightSuffix}`;
2285
2290
  const pirNativeId = `${this.nativeId}${pirSuffix}`;
2286
2291
  const autotrackingNativeId = `${this.nativeId}${autotrackingSuffix}`;
2292
+ const chimeNativeId = `${this.nativeId}${chimeSuffix}`;
2287
2293
 
2288
2294
  // Helper to safely remove a device only if it exists
2289
2295
  const safeRemoveDevice = (nativeId: string) => {
@@ -2413,6 +2419,24 @@ export class ReolinkCamera
2413
2419
  safeRemoveDevice(autotrackingNativeId);
2414
2420
  this.autotracking = undefined;
2415
2421
  }
2422
+
2423
+ // Create chime device (ring paired wireless chime)
2424
+ if (hasChime) {
2425
+ const device: Device = {
2426
+ providerNativeId: this.nativeId,
2427
+ name: `${this.name} Chime`,
2428
+ nativeId: chimeNativeId,
2429
+ info: {
2430
+ ...(this.info || {}),
2431
+ },
2432
+ interfaces: [ScryptedInterface.OnOff, ScryptedInterface.Settings],
2433
+ type: ScryptedDeviceType.Switch,
2434
+ };
2435
+ sdk.deviceManager.onDeviceDiscovered(device);
2436
+ } else {
2437
+ safeRemoveDevice(chimeNativeId);
2438
+ this.chime = undefined;
2439
+ }
2416
2440
  }
2417
2441
 
2418
2442
  async getSettings(): Promise<Setting[]> {
@@ -2573,6 +2597,9 @@ export class ReolinkCamera
2573
2597
  } else if (nativeId.endsWith(autotrackingSuffix)) {
2574
2598
  this.autotracking ||= new ReolinkCameraAutotracking(this, nativeId);
2575
2599
  return this.autotracking;
2600
+ } else if (nativeId.endsWith(chimeSuffix)) {
2601
+ this.chime ||= new ReolinkCameraChime(this, nativeId);
2602
+ return this.chime;
2576
2603
  }
2577
2604
  }
2578
2605
 
@@ -2597,6 +2624,8 @@ export class ReolinkCamera
2597
2624
  this.pirSensor = undefined;
2598
2625
  } else if (nativeId.endsWith(autotrackingSuffix)) {
2599
2626
  this.autotracking = undefined;
2627
+ } else if (nativeId.endsWith(chimeSuffix)) {
2628
+ this.chime = undefined;
2600
2629
  }
2601
2630
  }
2602
2631
 
@@ -2637,7 +2666,7 @@ export class ReolinkCamera
2637
2666
  const api = await this.ensureClient();
2638
2667
 
2639
2668
  const channel = this.storageSettings.values.rtspChannel;
2640
- const { hasSiren, hasFloodlight, hasPir, hasAutotracking } =
2669
+ const { hasSiren, hasFloodlight, hasPir, hasAutotracking, hasChime } =
2641
2670
  await this.getAbilities();
2642
2671
 
2643
2672
  // Cooldown period: 15 seconds after a manual state change
@@ -2768,6 +2797,29 @@ export class ReolinkCamera
2768
2797
  }
2769
2798
  }
2770
2799
  }
2800
+
2801
+ // Align chime state
2802
+ if (hasChime && this.chime) {
2803
+ if (isInCooldown(this.auxDeviceCooldowns.chime)) {
2804
+ logger.log(`[alignAuxDevicesState] Skipping chime (in cooldown)`);
2805
+ } else {
2806
+ try {
2807
+ const chimeState = await api.getHardwiredChime(channel);
2808
+ this.chime.on = chimeState.enabled;
2809
+ if (chimeState.type) {
2810
+ this.chime.storageSettings.values.chimeType = chimeState.type;
2811
+ }
2812
+ if (chimeState.time) {
2813
+ this.chime.storageSettings.values.time = chimeState.time;
2814
+ }
2815
+ } catch (e) {
2816
+ logger.warn(
2817
+ "Failed to align chime state",
2818
+ e?.message || String(e),
2819
+ );
2820
+ }
2821
+ }
2822
+ }
2771
2823
  }
2772
2824
 
2773
2825
  // Video stream helper methods
package/src/intercom.ts CHANGED
@@ -458,18 +458,28 @@ export class ReolinkBaichuanIntercom {
458
458
  : [];
459
459
 
460
460
  return [
461
+ "-hide_banner",
462
+
463
+ // Pre-input low-latency flags: these MUST be before -i to affect
464
+ // the input demuxer. Placing them after -i only affects output.
465
+ "-analyzeduration",
466
+ "0",
467
+ "-probesize",
468
+ "512",
469
+ "-fflags",
470
+ "nobuffer",
471
+ "-flags",
472
+ "low_delay",
473
+
461
474
  ...sanitizedArgs,
462
475
  "-i",
463
476
  url,
477
+
464
478
  // Ensure we only decode the first input's audio stream.
465
479
  "-map",
466
480
  "0:a:0?",
467
481
 
468
- // Low-latency decode settings.
469
- "-fflags",
470
- "nobuffer",
471
- "-flags",
472
- "low_delay",
482
+ // Output low-latency settings.
473
483
  "-flush_packets",
474
484
  "1",
475
485
 
package/src/utils.ts CHANGED
@@ -48,6 +48,7 @@ export const motionSirenSuffix = `-motion-siren`;
48
48
  export const motionFloodlightSuffix = `-motion-floodlight`;
49
49
  export const pirSuffix = `-pir`;
50
50
  export const autotrackingSuffix = `-autotracking`;
51
+ export const chimeSuffix = `-chime`;
51
52
 
52
53
  export const getDeviceInterfaces = (props: {
53
54
  capabilities: DeviceCapabilities;