@apocaliss92/scrypted-reolink-native 0.4.32 → 0.4.34
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/camera.ts +1 -118
- package/src/intercom-mixin.ts +302 -0
- package/src/intercom-provider.ts +130 -0
- package/src/intercom.ts +31 -35
- package/src/main.ts +80 -2
- package/src/utils.ts +0 -4
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/camera.ts
CHANGED
|
@@ -16,7 +16,6 @@ import sdk, {
|
|
|
16
16
|
ChargeState,
|
|
17
17
|
Device,
|
|
18
18
|
DeviceProvider,
|
|
19
|
-
Intercom,
|
|
20
19
|
MediaObject,
|
|
21
20
|
MediaStreamUrl,
|
|
22
21
|
ObjectDetectionTypes,
|
|
@@ -65,7 +64,6 @@ import {
|
|
|
65
64
|
getApiRelevantDebugLogs,
|
|
66
65
|
getDebugLogChoices,
|
|
67
66
|
} from "./debug-options";
|
|
68
|
-
import { ReolinkBaichuanIntercom } from "./intercom";
|
|
69
67
|
import ReolinkNativePlugin from "./main";
|
|
70
68
|
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
71
69
|
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
@@ -115,7 +113,6 @@ export class ReolinkCamera
|
|
|
115
113
|
PanTiltZoom,
|
|
116
114
|
VideoTextOverlays,
|
|
117
115
|
BinarySensor,
|
|
118
|
-
Intercom,
|
|
119
116
|
Reboot,
|
|
120
117
|
VideoClips
|
|
121
118
|
{
|
|
@@ -280,30 +277,6 @@ export class ReolinkCamera
|
|
|
280
277
|
json: true,
|
|
281
278
|
defaultValue: [],
|
|
282
279
|
},
|
|
283
|
-
intercomBlocksPerPayload: {
|
|
284
|
-
group: "Intercom",
|
|
285
|
-
title: "Blocks Per Payload",
|
|
286
|
-
description:
|
|
287
|
-
"Lower reduces latency (more packets). Typical: 1-4. Requires restarting talk session to take effect.",
|
|
288
|
-
type: "number",
|
|
289
|
-
defaultValue: 1,
|
|
290
|
-
},
|
|
291
|
-
intercomMaxBacklogMs: {
|
|
292
|
-
group: "Intercom",
|
|
293
|
-
title: "Max Backlog (ms)",
|
|
294
|
-
description:
|
|
295
|
-
"Maximum PCM backlog before dropping old audio to cap latency. Higher improves stability on slow systems but increases latency. Typical: 80-250. Requires restarting talk session to take effect.",
|
|
296
|
-
type: "number",
|
|
297
|
-
defaultValue: 120,
|
|
298
|
-
},
|
|
299
|
-
intercomGain: {
|
|
300
|
-
group: "Intercom",
|
|
301
|
-
title: "Gain",
|
|
302
|
-
description:
|
|
303
|
-
"Output gain multiplier applied before encoding. 1.0 = normal, 2.0 ≈ +6dB, 0.5 ≈ -6dB. Requires restarting talk session to take effect.",
|
|
304
|
-
type: "number",
|
|
305
|
-
defaultValue: 1.0,
|
|
306
|
-
},
|
|
307
280
|
// PTZ Presets
|
|
308
281
|
presets: {
|
|
309
282
|
group: "PTZ",
|
|
@@ -459,24 +432,6 @@ export class ReolinkCamera
|
|
|
459
432
|
defaultValue: 60,
|
|
460
433
|
hide: true,
|
|
461
434
|
},
|
|
462
|
-
lowThresholdBatteryRecording: {
|
|
463
|
-
title: "Low Threshold Battery Recording (%)",
|
|
464
|
-
subgroup: "Recording",
|
|
465
|
-
description:
|
|
466
|
-
"Battery level threshold below which recording is disabled (default: 15%).",
|
|
467
|
-
type: "number",
|
|
468
|
-
defaultValue: 15,
|
|
469
|
-
hide: true,
|
|
470
|
-
},
|
|
471
|
-
highThresholdBatteryRecording: {
|
|
472
|
-
title: "High Threshold Battery Recording (%)",
|
|
473
|
-
subgroup: "Recording",
|
|
474
|
-
description:
|
|
475
|
-
"Battery level threshold above which recording is enabled (default: 35%).",
|
|
476
|
-
type: "number",
|
|
477
|
-
defaultValue: 35,
|
|
478
|
-
hide: true,
|
|
479
|
-
},
|
|
480
435
|
diagnosticsOutputPath: {
|
|
481
436
|
title: "Diagnostics Output Path",
|
|
482
437
|
subgroup: "Diagnostics",
|
|
@@ -696,7 +651,6 @@ export class ReolinkCamera
|
|
|
696
651
|
classes: string[] = [];
|
|
697
652
|
presets: PtzPreset[] = [];
|
|
698
653
|
streamManager?: StreamManager;
|
|
699
|
-
intercom?: ReolinkBaichuanIntercom;
|
|
700
654
|
|
|
701
655
|
motionSiren?: ReolinkCameraMotionSiren;
|
|
702
656
|
siren?: ReolinkCameraSiren;
|
|
@@ -2570,23 +2524,6 @@ export class ReolinkCamera
|
|
|
2570
2524
|
return [];
|
|
2571
2525
|
}
|
|
2572
2526
|
|
|
2573
|
-
// Intercom interface methods
|
|
2574
|
-
async startIntercom(media: MediaObject): Promise<void> {
|
|
2575
|
-
if (this.intercom) {
|
|
2576
|
-
await this.intercom.start(media);
|
|
2577
|
-
} else {
|
|
2578
|
-
throw new Error("Intercom not initialized");
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
async stopIntercom(): Promise<void> {
|
|
2583
|
-
if (this.intercom) {
|
|
2584
|
-
return await this.intercom.stop();
|
|
2585
|
-
} else {
|
|
2586
|
-
throw new Error("Intercom not initialized");
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
2527
|
async updateDeviceInfo(): Promise<void> {
|
|
2591
2528
|
const logger = this.getBaichuanLogger();
|
|
2592
2529
|
|
|
@@ -3301,10 +3238,6 @@ export class ReolinkCamera
|
|
|
3301
3238
|
|
|
3302
3239
|
this.storageSettings.settings.batteryUpdateIntervalMinutes.hide =
|
|
3303
3240
|
!this.isBattery;
|
|
3304
|
-
this.storageSettings.settings.lowThresholdBatteryRecording.hide =
|
|
3305
|
-
!this.isBattery;
|
|
3306
|
-
this.storageSettings.settings.highThresholdBatteryRecording.hide =
|
|
3307
|
-
!this.isBattery;
|
|
3308
3241
|
|
|
3309
3242
|
// Show PIP settings only for multifocal devices
|
|
3310
3243
|
this.storageSettings.settings.pipPosition.hide = !this.isMultiFocal;
|
|
@@ -3362,11 +3295,7 @@ export class ReolinkCamera
|
|
|
3362
3295
|
);
|
|
3363
3296
|
}
|
|
3364
3297
|
|
|
3365
|
-
const {
|
|
3366
|
-
|
|
3367
|
-
if (hasIntercom) {
|
|
3368
|
-
this.intercom = new ReolinkBaichuanIntercom(this);
|
|
3369
|
-
}
|
|
3298
|
+
const { hasPtz } = await this.getAbilities();
|
|
3370
3299
|
|
|
3371
3300
|
if (hasPtz && !this.multiFocalDevice) {
|
|
3372
3301
|
const choices = (this.presets || []).map(
|
|
@@ -3532,44 +3461,6 @@ export class ReolinkCamera
|
|
|
3532
3461
|
}
|
|
3533
3462
|
}
|
|
3534
3463
|
|
|
3535
|
-
async checkRecordingAction(newBatteryLevel: number) {
|
|
3536
|
-
const nvrDeviceId = this.plugin.nvrDeviceId;
|
|
3537
|
-
if (nvrDeviceId && this.mixins.includes(nvrDeviceId)) {
|
|
3538
|
-
const logger = this.getBaichuanLogger();
|
|
3539
|
-
|
|
3540
|
-
const settings = await this.thisDevice.getSettings();
|
|
3541
|
-
const isPrivacyEnabled =
|
|
3542
|
-
settings.find((s) => s.key === "prebuffer:privacyMode")?.value ||
|
|
3543
|
-
settings.find((s) => s.key === "recording:privacyMode")?.value ||
|
|
3544
|
-
settings.find((s) => s.key === "snapshot:privacyMode")?.value;
|
|
3545
|
-
const { lowThresholdBatteryRecording, highThresholdBatteryRecording } =
|
|
3546
|
-
this.storageSettings.values;
|
|
3547
|
-
|
|
3548
|
-
if (!isPrivacyEnabled && newBatteryLevel < lowThresholdBatteryRecording) {
|
|
3549
|
-
logger.log(
|
|
3550
|
-
`Battery level is below low threshold (${newBatteryLevel}% < ${lowThresholdBatteryRecording}%), enabling privacy mode`,
|
|
3551
|
-
);
|
|
3552
|
-
await Promise.all([
|
|
3553
|
-
this.thisDevice.putSetting("prebuffer:privacyMode", true),
|
|
3554
|
-
this.thisDevice.putSetting("recording:privacyMode", true),
|
|
3555
|
-
this.thisDevice.putSetting("snapshot:privacyMode", true),
|
|
3556
|
-
]);
|
|
3557
|
-
} else if (
|
|
3558
|
-
isPrivacyEnabled &&
|
|
3559
|
-
newBatteryLevel > highThresholdBatteryRecording
|
|
3560
|
-
) {
|
|
3561
|
-
logger.log(
|
|
3562
|
-
`Battery level is above high threshold (${newBatteryLevel}% > ${highThresholdBatteryRecording}%), disabling privacy mode`,
|
|
3563
|
-
);
|
|
3564
|
-
await Promise.all([
|
|
3565
|
-
this.thisDevice.putSetting("prebuffer:privacyMode", false),
|
|
3566
|
-
this.thisDevice.putSetting("recording:privacyMode", false),
|
|
3567
|
-
this.thisDevice.putSetting("snapshot:privacyMode", false),
|
|
3568
|
-
]);
|
|
3569
|
-
}
|
|
3570
|
-
}
|
|
3571
|
-
}
|
|
3572
|
-
|
|
3573
3464
|
async updateBatteryInfo(batteryInfoParent?: BatteryInfo) {
|
|
3574
3465
|
const api = await this.ensureClient();
|
|
3575
3466
|
const channel = this.storageSettings.values.rtspChannel;
|
|
@@ -3603,8 +3494,6 @@ export class ReolinkCamera
|
|
|
3603
3494
|
this.chargeState = newChargeState;
|
|
3604
3495
|
}
|
|
3605
3496
|
|
|
3606
|
-
let shouldCheckRecordingAction = true;
|
|
3607
|
-
|
|
3608
3497
|
// Log only if battery level changed
|
|
3609
3498
|
if (oldLevel !== batteryInfo.batteryPercent) {
|
|
3610
3499
|
if (batteryInfo.adapterStatus !== undefined) {
|
|
@@ -3625,8 +3514,6 @@ export class ReolinkCamera
|
|
|
3625
3514
|
} else {
|
|
3626
3515
|
logger.log(`Battery level set: ${batteryInfo.batteryPercent}%`);
|
|
3627
3516
|
}
|
|
3628
|
-
} else {
|
|
3629
|
-
shouldCheckRecordingAction = false;
|
|
3630
3517
|
}
|
|
3631
3518
|
|
|
3632
3519
|
// Forward battery/charge state changes to Scrypted (plugin, HomeKit, etc.)
|
|
@@ -3641,10 +3528,6 @@ export class ReolinkCamera
|
|
|
3641
3528
|
void this.onDeviceEvent(ScryptedInterface.Charger, undefined);
|
|
3642
3529
|
}
|
|
3643
3530
|
}
|
|
3644
|
-
|
|
3645
|
-
if (shouldCheckRecordingAction) {
|
|
3646
|
-
await this.checkRecordingAction(batteryInfo.batteryPercent);
|
|
3647
|
-
}
|
|
3648
3531
|
}
|
|
3649
3532
|
|
|
3650
3533
|
return batteryInfo;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import type { ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with {
|
|
2
|
+
"resolution-mode": "import",
|
|
3
|
+
};
|
|
4
|
+
import sdk, {
|
|
5
|
+
Intercom,
|
|
6
|
+
MediaObject,
|
|
7
|
+
Setting,
|
|
8
|
+
Settings,
|
|
9
|
+
SettingValue,
|
|
10
|
+
} from "@scrypted/sdk";
|
|
11
|
+
import {
|
|
12
|
+
SettingsMixinDeviceBase,
|
|
13
|
+
SettingsMixinDeviceOptions,
|
|
14
|
+
} from "@scrypted/sdk/settings-mixin";
|
|
15
|
+
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
|
16
|
+
import type { BaichuanTransport } from "./connect";
|
|
17
|
+
import type { ReolinkNativeIntercom } from "./intercom-provider";
|
|
18
|
+
import { ReolinkBaichuanIntercom, type IntercomHost } from "./intercom";
|
|
19
|
+
import type { ReolinkCamera } from "./camera";
|
|
20
|
+
|
|
21
|
+
export class ReolinkNativeIntercomMixin
|
|
22
|
+
extends SettingsMixinDeviceBase<any>
|
|
23
|
+
implements Intercom, Settings
|
|
24
|
+
{
|
|
25
|
+
private intercomEngine?: ReolinkBaichuanIntercom;
|
|
26
|
+
|
|
27
|
+
storageSettings = new StorageSettings(this, {
|
|
28
|
+
// Connection settings (hidden when attached to an internal Reolink Native camera)
|
|
29
|
+
intercomIpAddress: {
|
|
30
|
+
group: "Connection",
|
|
31
|
+
title: "IP Address",
|
|
32
|
+
description: "Camera IP address for Baichuan connection",
|
|
33
|
+
type: "string",
|
|
34
|
+
},
|
|
35
|
+
intercomUsername: {
|
|
36
|
+
group: "Connection",
|
|
37
|
+
title: "Username",
|
|
38
|
+
type: "string",
|
|
39
|
+
defaultValue: "admin",
|
|
40
|
+
},
|
|
41
|
+
intercomPassword: {
|
|
42
|
+
group: "Connection",
|
|
43
|
+
title: "Password",
|
|
44
|
+
type: "password",
|
|
45
|
+
},
|
|
46
|
+
intercomUid: {
|
|
47
|
+
group: "Connection",
|
|
48
|
+
title: "UID",
|
|
49
|
+
description: "Required for battery/UDP cameras",
|
|
50
|
+
type: "string",
|
|
51
|
+
},
|
|
52
|
+
intercomChannel: {
|
|
53
|
+
group: "Connection",
|
|
54
|
+
title: "Channel",
|
|
55
|
+
description: "RTSP channel number (0-based)",
|
|
56
|
+
type: "number",
|
|
57
|
+
defaultValue: 0,
|
|
58
|
+
},
|
|
59
|
+
intercomTransport: {
|
|
60
|
+
group: "Connection",
|
|
61
|
+
title: "Transport",
|
|
62
|
+
description: "Connection transport protocol",
|
|
63
|
+
type: "string",
|
|
64
|
+
choices: ["tcp", "udp"],
|
|
65
|
+
defaultValue: "tcp",
|
|
66
|
+
},
|
|
67
|
+
// Audio pipeline settings (always visible)
|
|
68
|
+
intercomBlocksPerPayload: {
|
|
69
|
+
group: "Audio",
|
|
70
|
+
title: "Blocks Per Payload",
|
|
71
|
+
description:
|
|
72
|
+
"Lower reduces latency (more packets). Typical: 1-4. Requires restarting talk session to take effect.",
|
|
73
|
+
type: "number",
|
|
74
|
+
defaultValue: 1,
|
|
75
|
+
},
|
|
76
|
+
intercomMaxBacklogMs: {
|
|
77
|
+
group: "Audio",
|
|
78
|
+
title: "Max Backlog (ms)",
|
|
79
|
+
description:
|
|
80
|
+
"Maximum PCM backlog before dropping old audio to cap latency. Higher improves stability on slow systems but increases latency. Typical: 80-250. Requires restarting talk session to take effect.",
|
|
81
|
+
type: "number",
|
|
82
|
+
defaultValue: 120,
|
|
83
|
+
},
|
|
84
|
+
intercomGain: {
|
|
85
|
+
group: "Audio",
|
|
86
|
+
title: "Gain",
|
|
87
|
+
description:
|
|
88
|
+
"Output gain multiplier applied before encoding. 1.0 = normal, 2.0 ≈ +6dB, 0.5 ≈ -6dB. Requires restarting talk session to take effect.",
|
|
89
|
+
type: "number",
|
|
90
|
+
defaultValue: 1.0,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
constructor(
|
|
95
|
+
options: SettingsMixinDeviceOptions<any>,
|
|
96
|
+
public provider: ReolinkNativeIntercom,
|
|
97
|
+
) {
|
|
98
|
+
super(options);
|
|
99
|
+
this.provider.currentMixinsMap[this.id] = this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private getInternalCamera(): ReolinkCamera | undefined {
|
|
103
|
+
return this.provider.plugin?.camerasMap?.get(this.id);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private isReolinkPluginCamera(): boolean {
|
|
107
|
+
try {
|
|
108
|
+
const device = sdk.systemManager.getDeviceById(this.id);
|
|
109
|
+
return device?.pluginId === "@scrypted/reolink";
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async getReolinkPluginCredentials(): Promise<
|
|
116
|
+
{ host?: string; username?: string; password?: string } | undefined
|
|
117
|
+
> {
|
|
118
|
+
if (!this.isReolinkPluginCamera()) return undefined;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const settings: Setting[] = await this.mixinDevice.getSettings();
|
|
122
|
+
const map = new Map(
|
|
123
|
+
settings.map((s: Setting) => [s.key, s.value?.toString()]),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Non-NVR cameras use "ip", NVR child cameras use "ipAddress"
|
|
127
|
+
const host = map.get("ip") || map.get("ipAddress");
|
|
128
|
+
const username = map.get("username");
|
|
129
|
+
const password = map.get("password");
|
|
130
|
+
|
|
131
|
+
return { host, username, password };
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private buildHost(): IntercomHost {
|
|
138
|
+
const self = this;
|
|
139
|
+
const internalCamera = this.getInternalCamera();
|
|
140
|
+
|
|
141
|
+
if (internalCamera) {
|
|
142
|
+
return {
|
|
143
|
+
get blocksPerPayload() {
|
|
144
|
+
return Math.max(
|
|
145
|
+
1,
|
|
146
|
+
Math.min(
|
|
147
|
+
8,
|
|
148
|
+
self.storageSettings.values.intercomBlocksPerPayload ?? 1,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
get outputGain() {
|
|
153
|
+
const v = Number(self.storageSettings.values.intercomGain);
|
|
154
|
+
return Number.isFinite(v) ? Math.max(0.1, Math.min(10, v)) : 1.0;
|
|
155
|
+
},
|
|
156
|
+
get maxBacklogMs() {
|
|
157
|
+
const v = Number(self.storageSettings.values.intercomMaxBacklogMs);
|
|
158
|
+
return Number.isFinite(v) ? Math.max(20, Math.min(5000, v)) : 120;
|
|
159
|
+
},
|
|
160
|
+
get channel() {
|
|
161
|
+
return internalCamera.storageSettings.values.rtspChannel;
|
|
162
|
+
},
|
|
163
|
+
get isBatteryCamera() {
|
|
164
|
+
return internalCamera.isBattery;
|
|
165
|
+
},
|
|
166
|
+
get deviceId() {
|
|
167
|
+
return internalCamera.nativeId;
|
|
168
|
+
},
|
|
169
|
+
get logger() {
|
|
170
|
+
return internalCamera.getBaichuanLogger();
|
|
171
|
+
},
|
|
172
|
+
ensureApi: () => internalCamera.ensureBaichuanClient(),
|
|
173
|
+
withRetry: (fn) => internalCamera.withBaichuanRetry(fn),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// External path: use shared client from plugin registry
|
|
178
|
+
return {
|
|
179
|
+
get blocksPerPayload() {
|
|
180
|
+
return Math.max(
|
|
181
|
+
1,
|
|
182
|
+
Math.min(
|
|
183
|
+
8,
|
|
184
|
+
self.storageSettings.values.intercomBlocksPerPayload ?? 1,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
get outputGain() {
|
|
189
|
+
const v = Number(self.storageSettings.values.intercomGain);
|
|
190
|
+
return Number.isFinite(v) ? Math.max(0.1, Math.min(10, v)) : 1.0;
|
|
191
|
+
},
|
|
192
|
+
get maxBacklogMs() {
|
|
193
|
+
const v = Number(self.storageSettings.values.intercomMaxBacklogMs);
|
|
194
|
+
return Number.isFinite(v) ? Math.max(20, Math.min(5000, v)) : 120;
|
|
195
|
+
},
|
|
196
|
+
get channel() {
|
|
197
|
+
return self.storageSettings.values.intercomChannel ?? 0;
|
|
198
|
+
},
|
|
199
|
+
get isBatteryCamera() {
|
|
200
|
+
return self.storageSettings.values.intercomTransport === "udp";
|
|
201
|
+
},
|
|
202
|
+
get deviceId() {
|
|
203
|
+
return self.id;
|
|
204
|
+
},
|
|
205
|
+
get logger() {
|
|
206
|
+
return self.console;
|
|
207
|
+
},
|
|
208
|
+
ensureApi: () => self.ensureExternalApi(),
|
|
209
|
+
withRetry: (fn) => fn(),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async ensureExternalApi(): Promise<ReolinkBaichuanApi> {
|
|
214
|
+
let { intercomIpAddress, intercomUsername, intercomPassword,
|
|
215
|
+
intercomUid, intercomTransport } =
|
|
216
|
+
this.storageSettings.values;
|
|
217
|
+
|
|
218
|
+
// Auto-detect credentials from @scrypted/reolink camera settings
|
|
219
|
+
if (!intercomIpAddress || !intercomUsername || !intercomPassword) {
|
|
220
|
+
const creds = await this.getReolinkPluginCredentials();
|
|
221
|
+
if (creds) {
|
|
222
|
+
intercomIpAddress ||= creds.host;
|
|
223
|
+
intercomUsername ||= creds.username;
|
|
224
|
+
intercomPassword ||= creds.password;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!intercomIpAddress || !intercomUsername || !intercomPassword) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
"Intercom connection settings incomplete: IP, username, and password are required",
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const transport: BaichuanTransport =
|
|
235
|
+
intercomTransport === "udp" ? "udp" : "tcp";
|
|
236
|
+
|
|
237
|
+
return await this.provider.plugin.acquireExternalClient(this.id, {
|
|
238
|
+
host: intercomIpAddress,
|
|
239
|
+
username: intercomUsername,
|
|
240
|
+
password: intercomPassword,
|
|
241
|
+
uid: intercomUid,
|
|
242
|
+
transport,
|
|
243
|
+
logger: this.console,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async startIntercom(media: MediaObject): Promise<void> {
|
|
248
|
+
const host = this.buildHost();
|
|
249
|
+
this.intercomEngine = new ReolinkBaichuanIntercom(host);
|
|
250
|
+
await this.intercomEngine.start(media);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async stopIntercom(): Promise<void> {
|
|
254
|
+
if (this.intercomEngine) {
|
|
255
|
+
await this.intercomEngine.stop();
|
|
256
|
+
this.intercomEngine = undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async getMixinSettings(): Promise<Setting[]> {
|
|
261
|
+
const isInternal = !!this.getInternalCamera();
|
|
262
|
+
const isReolinkPlugin = this.isReolinkPluginCamera();
|
|
263
|
+
const hideConnection = isInternal || isReolinkPlugin;
|
|
264
|
+
const settings = await this.storageSettings.getSettings();
|
|
265
|
+
|
|
266
|
+
const connectionKeys = new Set([
|
|
267
|
+
"intercomIpAddress",
|
|
268
|
+
"intercomUsername",
|
|
269
|
+
"intercomPassword",
|
|
270
|
+
"intercomUid",
|
|
271
|
+
"intercomChannel",
|
|
272
|
+
"intercomTransport",
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
for (const setting of settings) {
|
|
276
|
+
if (connectionKeys.has(setting.key)) {
|
|
277
|
+
(setting as any).hide = hideConnection;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return settings;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async putMixinSetting(
|
|
285
|
+
key: string,
|
|
286
|
+
value: SettingValue,
|
|
287
|
+
): Promise<void> {
|
|
288
|
+
await this.storageSettings.putSetting(key, value);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async release(): Promise<void> {
|
|
292
|
+
if (this.intercomEngine) {
|
|
293
|
+
await this.intercomEngine.stop();
|
|
294
|
+
this.intercomEngine = undefined;
|
|
295
|
+
}
|
|
296
|
+
// Release external client if not internal
|
|
297
|
+
if (!this.getInternalCamera()) {
|
|
298
|
+
await this.provider.plugin?.releaseExternalClient(this.id);
|
|
299
|
+
}
|
|
300
|
+
delete this.provider.currentMixinsMap[this.id];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import sdk, {
|
|
2
|
+
MixinProvider,
|
|
3
|
+
ScryptedDevice,
|
|
4
|
+
ScryptedDeviceBase,
|
|
5
|
+
ScryptedDeviceType,
|
|
6
|
+
ScryptedInterface,
|
|
7
|
+
Setting,
|
|
8
|
+
Settings,
|
|
9
|
+
SettingValue,
|
|
10
|
+
WritableDeviceState,
|
|
11
|
+
} from "@scrypted/sdk";
|
|
12
|
+
import type ReolinkNativePlugin from "./main";
|
|
13
|
+
import { ReolinkNativeIntercomMixin } from "./intercom-mixin";
|
|
14
|
+
|
|
15
|
+
export const INTERCOM_PROVIDER_NATIVE_ID = "reolink-native-intercom";
|
|
16
|
+
|
|
17
|
+
const AUTO_INCLUDE_TOKEN = "v1";
|
|
18
|
+
|
|
19
|
+
export class ReolinkNativeIntercom
|
|
20
|
+
extends ScryptedDeviceBase
|
|
21
|
+
implements MixinProvider, Settings
|
|
22
|
+
{
|
|
23
|
+
currentMixinsMap: Record<string, ReolinkNativeIntercomMixin> = {};
|
|
24
|
+
plugin: ReolinkNativePlugin;
|
|
25
|
+
private hasEnabledMixin: Record<string, string> = {};
|
|
26
|
+
private pluginsComponent: Promise<any>;
|
|
27
|
+
|
|
28
|
+
constructor(nativeId: string) {
|
|
29
|
+
super(nativeId);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
this.hasEnabledMixin = JSON.parse(
|
|
33
|
+
this.storage.getItem("hasEnabledMixin") || "{}",
|
|
34
|
+
);
|
|
35
|
+
} catch {
|
|
36
|
+
this.hasEnabledMixin = {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.pluginsComponent = sdk.systemManager.getComponent("plugins");
|
|
40
|
+
|
|
41
|
+
// Watch for new device descriptors to auto-enable on newly added devices
|
|
42
|
+
sdk.systemManager.listen((eventSource, eventDetails) => {
|
|
43
|
+
if (
|
|
44
|
+
eventDetails.eventInterface !== ScryptedInterface.ScryptedDevice ||
|
|
45
|
+
eventDetails.property
|
|
46
|
+
)
|
|
47
|
+
return;
|
|
48
|
+
this.maybeEnableMixin(eventSource);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Check all existing devices on startup
|
|
52
|
+
process.nextTick(() => {
|
|
53
|
+
for (const id of Object.keys(sdk.systemManager.getSystemState())) {
|
|
54
|
+
const device = sdk.systemManager.getDeviceById(id);
|
|
55
|
+
this.maybeEnableMixin(device);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private async maybeEnableMixin(device: ScryptedDevice) {
|
|
61
|
+
if (!device || device.mixins?.includes(this.id)) return;
|
|
62
|
+
|
|
63
|
+
// Already auto-enabled once with this token
|
|
64
|
+
if (this.hasEnabledMixin[device.id] === AUTO_INCLUDE_TOKEN) return;
|
|
65
|
+
|
|
66
|
+
const match = await this.canMixin(device.type, device.interfaces);
|
|
67
|
+
if (!match) return;
|
|
68
|
+
|
|
69
|
+
// Only auto-enable for cameras provided by our own plugin
|
|
70
|
+
if (!this.plugin?.camerasMap?.has(device.id)) return;
|
|
71
|
+
|
|
72
|
+
this.console.log(`Auto-enabling intercom mixin for ${device.name}`);
|
|
73
|
+
const mixins = (device.mixins || []).slice();
|
|
74
|
+
mixins.push(this.id);
|
|
75
|
+
const plugins = await this.pluginsComponent;
|
|
76
|
+
await plugins.setMixins(device.id, mixins);
|
|
77
|
+
|
|
78
|
+
this.hasEnabledMixin[device.id] = AUTO_INCLUDE_TOKEN;
|
|
79
|
+
this.storage.setItem(
|
|
80
|
+
"hasEnabledMixin",
|
|
81
|
+
JSON.stringify(this.hasEnabledMixin),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async canMixin(
|
|
86
|
+
type: ScryptedDeviceType,
|
|
87
|
+
interfaces: string[],
|
|
88
|
+
): Promise<string[] | null> {
|
|
89
|
+
if (
|
|
90
|
+
(type === ScryptedDeviceType.Camera ||
|
|
91
|
+
type === ScryptedDeviceType.Doorbell) &&
|
|
92
|
+
interfaces.includes(ScryptedInterface.VideoCamera)
|
|
93
|
+
) {
|
|
94
|
+
return [ScryptedInterface.Intercom, ScryptedInterface.Settings];
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getMixin(
|
|
100
|
+
mixinDevice: any,
|
|
101
|
+
mixinDeviceInterfaces: ScryptedInterface[],
|
|
102
|
+
mixinDeviceState: WritableDeviceState,
|
|
103
|
+
): Promise<any> {
|
|
104
|
+
return new ReolinkNativeIntercomMixin(
|
|
105
|
+
{
|
|
106
|
+
mixinDevice,
|
|
107
|
+
mixinDeviceInterfaces,
|
|
108
|
+
mixinDeviceState,
|
|
109
|
+
mixinProviderNativeId: this.nativeId,
|
|
110
|
+
group: "Reolink Native Intercom",
|
|
111
|
+
groupKey: "reolinkNativeIntercom",
|
|
112
|
+
},
|
|
113
|
+
this,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
|
|
118
|
+
const mixin = this.currentMixinsMap[id];
|
|
119
|
+
if (mixin) {
|
|
120
|
+
await mixin.release();
|
|
121
|
+
delete this.currentMixinsMap[id];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async getSettings(): Promise<Setting[]> {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async putSetting(key: string, value: SettingValue): Promise<void> {}
|
|
130
|
+
}
|