@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/README.md +14 -0
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/accessories/chime.ts +131 -0
- package/src/accessories/index.ts +1 -0
- package/src/camera.ts +54 -2
- package/src/intercom.ts +15 -5
- package/src/utils.ts +1 -0
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/accessories/index.ts
CHANGED
|
@@ -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
|
-
//
|
|
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;
|