@apocaliss92/scrypted-reolink-native 0.0.1 → 0.0.3
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 +97 -100
- package/src/connect.ts +146 -0
- package/src/main.ts +18 -6
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/camera.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { ReolinkBaichuanIntercom } from "./intercom";
|
|
|
7
7
|
import ReolinkNativePlugin from "./main";
|
|
8
8
|
import { ReolinkPtzPresets } from "./presets";
|
|
9
9
|
import { parseStreamProfileFromId, StreamManager } from './stream-utils';
|
|
10
|
+
import { connectBaichuanWithTcpUdpFallback, createBaichuanApi, maskUid } from './connect';
|
|
10
11
|
|
|
11
12
|
export const moToB64 = async (mo: MediaObject) => {
|
|
12
13
|
const bufferImage = await sdk.mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
|
|
@@ -164,8 +165,7 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
|
|
|
164
165
|
floodlight: ReolinkCameraFloodlight;
|
|
165
166
|
pirSensor: ReolinkCameraPirSensor;
|
|
166
167
|
private baichuanApi: ReolinkBaichuanApi | undefined;
|
|
167
|
-
private
|
|
168
|
-
private refreshDeviceStatePromise: Promise<void> | undefined;
|
|
168
|
+
private refreshingState = false;
|
|
169
169
|
|
|
170
170
|
private subscribedToEvents = false;
|
|
171
171
|
private onSimpleEvent: ((ev: ReolinkSimpleEvent) => void) | undefined;
|
|
@@ -179,6 +179,8 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
|
|
|
179
179
|
private lastSnapshotTaken: number | undefined;
|
|
180
180
|
private streamManager: StreamManager;
|
|
181
181
|
|
|
182
|
+
private udpFallbackAlerted = false;
|
|
183
|
+
|
|
182
184
|
private dispatchEventsApplyTimer: NodeJS.Timeout | undefined;
|
|
183
185
|
private dispatchEventsApplySeq = 0;
|
|
184
186
|
|
|
@@ -195,6 +197,11 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
|
|
|
195
197
|
title: 'IP Address',
|
|
196
198
|
type: 'string',
|
|
197
199
|
},
|
|
200
|
+
uid: {
|
|
201
|
+
title: 'UID',
|
|
202
|
+
description: 'Reolink UID (required for battery cameras / BCUDP).',
|
|
203
|
+
type: 'string',
|
|
204
|
+
},
|
|
198
205
|
username: {
|
|
199
206
|
type: 'string',
|
|
200
207
|
title: 'Username',
|
|
@@ -462,7 +469,6 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
|
|
|
462
469
|
}
|
|
463
470
|
}
|
|
464
471
|
this.baichuanApi = undefined;
|
|
465
|
-
this.baichuanInitPromise = undefined;
|
|
466
472
|
this.subscribedToEvents = false;
|
|
467
473
|
this.eventsApi = undefined;
|
|
468
474
|
}
|
|
@@ -494,9 +500,6 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
|
|
|
494
500
|
async init() {
|
|
495
501
|
const logger = this.getLogger();
|
|
496
502
|
|
|
497
|
-
// Migrate older boolean value to the new multi-select format.
|
|
498
|
-
this.migrateDispatchEventsSetting();
|
|
499
|
-
|
|
500
503
|
// Initialize Baichuan API
|
|
501
504
|
await this.ensureClient();
|
|
502
505
|
|
|
@@ -507,21 +510,6 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
|
|
|
507
510
|
this.updateDeviceInfo();
|
|
508
511
|
this.updatePtzCaps();
|
|
509
512
|
|
|
510
|
-
const interfaces = await this.getDeviceInterfaces();
|
|
511
|
-
|
|
512
|
-
const device: Device = {
|
|
513
|
-
nativeId: this.nativeId,
|
|
514
|
-
providerNativeId: this.plugin.nativeId,
|
|
515
|
-
name: this.name,
|
|
516
|
-
interfaces,
|
|
517
|
-
type: this.type as ScryptedDeviceType,
|
|
518
|
-
info: this.info,
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
logger.log(`Updating device interfaces: ${JSON.stringify(interfaces)}`);
|
|
522
|
-
|
|
523
|
-
await sdk.deviceManager.onDeviceDiscovered(device);
|
|
524
|
-
|
|
525
513
|
// Start event subscription after discovery.
|
|
526
514
|
try {
|
|
527
515
|
if (this.isEventDispatchEnabled()) {
|
|
@@ -544,65 +532,67 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
|
|
|
544
532
|
}
|
|
545
533
|
|
|
546
534
|
async ensureClient(): Promise<ReolinkBaichuanApi> {
|
|
547
|
-
if (this.baichuanInitPromise) {
|
|
548
|
-
return this.baichuanInitPromise;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
535
|
if (this.baichuanApi && this.baichuanApi.client.loggedIn) {
|
|
552
536
|
return this.baichuanApi;
|
|
553
537
|
}
|
|
554
538
|
|
|
555
|
-
const { ipAddress, username, password } = this.storageSettings.values;
|
|
539
|
+
const { ipAddress, username, password, uid } = this.storageSettings.values;
|
|
556
540
|
|
|
557
541
|
if (!ipAddress || !username || !password) {
|
|
558
542
|
throw new Error('Missing camera credentials');
|
|
559
543
|
}
|
|
560
544
|
|
|
561
|
-
this.
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
545
|
+
if (this.baichuanApi) {
|
|
546
|
+
await this.baichuanApi.close();
|
|
547
|
+
}
|
|
565
548
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
549
|
+
const debugOptions = this.getBaichuanDebugOptions();
|
|
550
|
+
const { api } = await connectBaichuanWithTcpUdpFallback(
|
|
551
|
+
{
|
|
569
552
|
host: ipAddress,
|
|
570
553
|
username,
|
|
571
554
|
password,
|
|
555
|
+
uid,
|
|
572
556
|
logger: this.console,
|
|
573
557
|
...(debugOptions ? { debugOptions } : {}),
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
558
|
+
},
|
|
559
|
+
({ uid: normalizedUid, uidMissing }) => {
|
|
560
|
+
const uidMsg = !uidMissing && normalizedUid ? `UID ${maskUid(normalizedUid)}` : 'UID MISSING';
|
|
561
|
+
if (!this.udpFallbackAlerted) {
|
|
562
|
+
this.udpFallbackAlerted = true;
|
|
563
|
+
this.log.a(
|
|
564
|
+
`Baichuan TCP failed for camera ${this.name} (${ipAddress}). This appears to be a battery camera: UID is required and UDP/BCUDP will be used (${uidMsg}).`,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
);
|
|
579
569
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
finally {
|
|
584
|
-
// If login failed, allow future retries.
|
|
585
|
-
if (!this.baichuanApi?.client?.loggedIn) {
|
|
586
|
-
this.baichuanInitPromise = undefined;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
570
|
+
this.baichuanApi = api;
|
|
571
|
+
return api;
|
|
589
572
|
}
|
|
590
573
|
|
|
591
574
|
private async createStreamClient(): Promise<ReolinkBaichuanApi> {
|
|
592
|
-
|
|
575
|
+
// Ensure the main client is initialized first so we know if this device needs UDP.
|
|
576
|
+
const primary = await this.ensureClient();
|
|
577
|
+
const transport = primary.client.getTransport();
|
|
578
|
+
|
|
579
|
+
const { ipAddress, username, password, uid } = this.storageSettings.values;
|
|
593
580
|
if (!ipAddress || !username || !password) {
|
|
594
581
|
throw new Error('Missing camera credentials');
|
|
595
582
|
}
|
|
596
583
|
|
|
597
|
-
const { ReolinkBaichuanApi } = await import('@apocaliss92/reolink-baichuan-js');
|
|
598
584
|
const debugOptions = this.getBaichuanDebugOptions();
|
|
599
|
-
const api =
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
585
|
+
const api = await createBaichuanApi(
|
|
586
|
+
{
|
|
587
|
+
host: ipAddress,
|
|
588
|
+
username,
|
|
589
|
+
password,
|
|
590
|
+
uid,
|
|
591
|
+
logger: this.console,
|
|
592
|
+
...(debugOptions ? { debugOptions } : {}),
|
|
593
|
+
},
|
|
594
|
+
transport,
|
|
595
|
+
);
|
|
606
596
|
await api.login();
|
|
607
597
|
return api;
|
|
608
598
|
}
|
|
@@ -612,30 +602,52 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
|
|
|
612
602
|
}
|
|
613
603
|
|
|
614
604
|
private async refreshDeviceState(): Promise<void> {
|
|
615
|
-
if (this.
|
|
605
|
+
if (this.refreshingState) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
this.refreshingState = true;
|
|
616
609
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const channel = this.getRtspChannel();
|
|
610
|
+
const logger = this.getLogger();
|
|
611
|
+
const api = await this.ensureClient();
|
|
612
|
+
const channel = this.getRtspChannel();
|
|
621
613
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
614
|
+
try {
|
|
615
|
+
const { capabilities, abilities, support, presets } = await api.getDeviceCapabilities(channel);
|
|
616
|
+
this.storageSettings.values.capabilities = capabilities;
|
|
617
|
+
this.ptzPresets.setCachedPtzPresets(presets);
|
|
618
|
+
this.console.log(`Refreshed device capabilities: ${JSON.stringify({ capabilities, abilities, support, presets })}`);
|
|
619
|
+
}
|
|
620
|
+
catch (e) {
|
|
621
|
+
logger.error('Failed to refresh abilities', e);
|
|
622
|
+
}
|
|
631
623
|
|
|
632
|
-
|
|
633
|
-
await this.refreshAuxDevicesStatus()
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
|
|
624
|
+
try {
|
|
625
|
+
await this.refreshAuxDevicesStatus();
|
|
626
|
+
}
|
|
627
|
+
catch (e) {
|
|
628
|
+
logger.error('Failed to refresh device status', e);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
const interfaces = await this.getDeviceInterfaces();
|
|
633
|
+
|
|
634
|
+
const device: Device = {
|
|
635
|
+
nativeId: this.nativeId,
|
|
636
|
+
providerNativeId: this.plugin.nativeId,
|
|
637
|
+
name: this.name,
|
|
638
|
+
interfaces,
|
|
639
|
+
type: this.type as ScryptedDeviceType,
|
|
640
|
+
info: this.info,
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
logger.log(`Updating device interfaces: ${JSON.stringify(interfaces)}`);
|
|
637
644
|
|
|
638
|
-
|
|
645
|
+
await sdk.deviceManager.onDeviceDiscovered(device);
|
|
646
|
+
} catch (e) {
|
|
647
|
+
logger.error('Failed to update device interfaces', e);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
this.refreshingState = false;
|
|
639
651
|
}
|
|
640
652
|
|
|
641
653
|
private async ensureBaichuanEventSubscription(): Promise<void> {
|
|
@@ -1088,6 +1100,11 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
|
|
|
1088
1100
|
return Boolean(capabilities?.hasBattery);
|
|
1089
1101
|
}
|
|
1090
1102
|
|
|
1103
|
+
hasIntercom() {
|
|
1104
|
+
const capabilities = this.getAbilities();
|
|
1105
|
+
return Boolean(capabilities?.hasIntercom);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1091
1108
|
getPtzCapabilities() {
|
|
1092
1109
|
const capabilities = this.getAbilities();
|
|
1093
1110
|
const hasZoom = Boolean(capabilities?.hasZoom);
|
|
@@ -1120,33 +1137,20 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
|
|
|
1120
1137
|
];
|
|
1121
1138
|
|
|
1122
1139
|
try {
|
|
1123
|
-
// Expose Intercom if the camera supports Baichuan talkback.
|
|
1124
|
-
try {
|
|
1125
|
-
const api = this.getClient();
|
|
1126
|
-
if (api) {
|
|
1127
|
-
const ability = await api.getTalkAbility(this.getRtspChannel());
|
|
1128
|
-
if (Array.isArray((ability as any)?.audioConfigList) && (ability as any).audioConfigList.length > 0) {
|
|
1129
|
-
interfaces.push(ScryptedInterface.Intercom);
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
catch {
|
|
1134
|
-
// ignore: camera likely doesn't support talkback
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
1140
|
const { hasPtz } = this.getPtzCapabilities();
|
|
1138
1141
|
|
|
1139
1142
|
if (hasPtz) {
|
|
1140
1143
|
interfaces.push(ScryptedInterface.PanTiltZoom);
|
|
1141
1144
|
}
|
|
1142
|
-
|
|
1143
|
-
interfaces.push(ScryptedInterface.ObjectDetector);
|
|
1144
|
-
}
|
|
1145
|
+
interfaces.push(ScryptedInterface.ObjectDetector);
|
|
1145
1146
|
if (this.hasSiren() || this.hasFloodlight() || this.hasPirEvents())
|
|
1146
1147
|
interfaces.push(ScryptedInterface.DeviceProvider);
|
|
1147
1148
|
if (this.hasBattery()) {
|
|
1148
1149
|
interfaces.push(ScryptedInterface.Battery, ScryptedInterface.Sleep);
|
|
1149
1150
|
}
|
|
1151
|
+
if (this.hasIntercom()) {
|
|
1152
|
+
interfaces.push(ScryptedInterface.Intercom);
|
|
1153
|
+
}
|
|
1150
1154
|
} catch (e) {
|
|
1151
1155
|
this.getLogger().error('Error getting device interfaces', e);
|
|
1152
1156
|
}
|
|
@@ -1283,13 +1287,6 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
|
|
|
1283
1287
|
return this.getDispatchEventsSelection().has('objects');
|
|
1284
1288
|
}
|
|
1285
1289
|
|
|
1286
|
-
private migrateDispatchEventsSetting(): void {
|
|
1287
|
-
const cur = (this.storageSettings.values as any).dispatchEvents;
|
|
1288
|
-
if (typeof cur === 'boolean') {
|
|
1289
|
-
(this.storageSettings.values as any).dispatchEvents = cur ? ['motion', 'objects'] : [];
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
1290
|
private scheduleApplyEventDispatchSettings(): void {
|
|
1294
1291
|
// Debounce to avoid rapid apply loops while editing multi-select.
|
|
1295
1292
|
this.dispatchEventsApplySeq++;
|
package/src/connect.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { BaichuanClientOptions, ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
|
+
|
|
3
|
+
export type BaichuanTransport = "tcp" | "udp";
|
|
4
|
+
|
|
5
|
+
export type BaichuanConnectInputs = {
|
|
6
|
+
host: string;
|
|
7
|
+
username: string;
|
|
8
|
+
password: string;
|
|
9
|
+
uid?: string;
|
|
10
|
+
logger?: Console;
|
|
11
|
+
debugOptions?: unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function normalizeUid(uid?: string): string | undefined {
|
|
15
|
+
const v = uid?.trim();
|
|
16
|
+
return v ? v : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function maskUid(uid: string): string {
|
|
20
|
+
const v = uid.trim();
|
|
21
|
+
if (v.length <= 8) return v;
|
|
22
|
+
return `${v.slice(0, 4)}…${v.slice(-4)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isTcpFailureThatShouldFallbackToUdp(e: unknown): boolean {
|
|
26
|
+
const message = (e as any)?.message || (e as any)?.toString?.() || "";
|
|
27
|
+
if (typeof message !== "string") return false;
|
|
28
|
+
|
|
29
|
+
// Fallback only on transport/connection style failures.
|
|
30
|
+
// Wrong credentials won't be fixed by switching to UDP.
|
|
31
|
+
return (
|
|
32
|
+
message.includes("ECONNREFUSED") ||
|
|
33
|
+
message.includes("ETIMEDOUT") ||
|
|
34
|
+
message.includes("EHOSTUNREACH") ||
|
|
35
|
+
message.includes("ENETUNREACH") ||
|
|
36
|
+
message.includes("socket hang up") ||
|
|
37
|
+
message.includes("TCP connection timeout") ||
|
|
38
|
+
message.includes("Baichuan socket closed")
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function createBaichuanApi(inputs: BaichuanConnectInputs, transport: BaichuanTransport): Promise<ReolinkBaichuanApi> {
|
|
43
|
+
const { ReolinkBaichuanApi } = await import("@apocaliss92/reolink-baichuan-js");
|
|
44
|
+
|
|
45
|
+
const base: BaichuanClientOptions = {
|
|
46
|
+
host: inputs.host,
|
|
47
|
+
username: inputs.username,
|
|
48
|
+
password: inputs.password,
|
|
49
|
+
logger: inputs.logger,
|
|
50
|
+
...(inputs.debugOptions ? { debugOptions: inputs.debugOptions } : {}),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const attachErrorHandler = (api: ReolinkBaichuanApi) => {
|
|
54
|
+
// Critical: BaichuanClient emits 'error'. If nobody listens, Node treats it as an
|
|
55
|
+
// uncaught exception. Ensure we always have a listener.
|
|
56
|
+
try {
|
|
57
|
+
api.client.on("error", (err: unknown) => {
|
|
58
|
+
const logger = inputs.logger ?? console;
|
|
59
|
+
const msg = (err as any)?.message || (err as any)?.toString?.() || String(err);
|
|
60
|
+
logger.error(`[BaichuanClient] error (${transport}) ${inputs.host}: ${msg}`);
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (transport === "tcp") {
|
|
68
|
+
const api = new ReolinkBaichuanApi({
|
|
69
|
+
...base,
|
|
70
|
+
keepAliveInterval: 10000,
|
|
71
|
+
tcpSocketKeepAlive: true,
|
|
72
|
+
transport: "tcp",
|
|
73
|
+
});
|
|
74
|
+
attachErrorHandler(api);
|
|
75
|
+
return api;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const uid = normalizeUid(inputs.uid);
|
|
79
|
+
if (!uid) {
|
|
80
|
+
throw new Error("UID is required for battery cameras (BCUDP)");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const api = new ReolinkBaichuanApi({
|
|
84
|
+
...base,
|
|
85
|
+
transport: "udp",
|
|
86
|
+
udp: {
|
|
87
|
+
mode: "uid",
|
|
88
|
+
uid,
|
|
89
|
+
host: inputs.host,
|
|
90
|
+
broadcast: false,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
attachErrorHandler(api);
|
|
94
|
+
return api;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type UdpFallbackInfo = {
|
|
98
|
+
host: string;
|
|
99
|
+
uid?: string;
|
|
100
|
+
uidMissing: boolean;
|
|
101
|
+
tcpError: unknown;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export async function connectBaichuanWithTcpUdpFallback(
|
|
105
|
+
inputs: BaichuanConnectInputs,
|
|
106
|
+
onUdpFallback?: (info: UdpFallbackInfo) => void,
|
|
107
|
+
): Promise<{ api: ReolinkBaichuanApi; transport: BaichuanTransport }> {
|
|
108
|
+
let tcpApi: ReolinkBaichuanApi | undefined;
|
|
109
|
+
try {
|
|
110
|
+
tcpApi = await createBaichuanApi(inputs, "tcp");
|
|
111
|
+
await tcpApi.login();
|
|
112
|
+
return { api: tcpApi, transport: "tcp" };
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
try {
|
|
116
|
+
await tcpApi?.close();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// ignore
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!isTcpFailureThatShouldFallbackToUdp(e)) {
|
|
123
|
+
throw e;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const uid = normalizeUid(inputs.uid);
|
|
127
|
+
const uidMissing = !uid;
|
|
128
|
+
|
|
129
|
+
onUdpFallback?.({
|
|
130
|
+
host: inputs.host,
|
|
131
|
+
uid,
|
|
132
|
+
uidMissing,
|
|
133
|
+
tcpError: e,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (uidMissing) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Baichuan TCP failed and this camera likely requires UDP/BCUDP. Set the Reolink UID in settings to continue (ip=${inputs.host}).`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const udpApi = await createBaichuanApi(inputs, "udp");
|
|
143
|
+
await udpApi.login();
|
|
144
|
+
return { api: udpApi, transport: "udp" };
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting } from "@scrypted/sdk";
|
|
2
2
|
import { ReolinkNativeCamera } from "./camera";
|
|
3
|
+
import { connectBaichuanWithTcpUdpFallback, maskUid } from "./connect";
|
|
3
4
|
|
|
4
5
|
class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
|
5
6
|
devices = new Map<string, ReolinkNativeCamera>();
|
|
@@ -35,14 +36,24 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
35
36
|
|
|
36
37
|
const username = settings.username?.toString();
|
|
37
38
|
const password = settings.password?.toString();
|
|
39
|
+
const uid = settings.uid?.toString();
|
|
38
40
|
|
|
39
41
|
if (ipAddress && username && password) {
|
|
40
|
-
const {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const { api } = await connectBaichuanWithTcpUdpFallback(
|
|
43
|
+
{
|
|
44
|
+
host: ipAddress,
|
|
45
|
+
username,
|
|
46
|
+
password,
|
|
47
|
+
uid,
|
|
48
|
+
logger: this.console,
|
|
49
|
+
},
|
|
50
|
+
({ uid: normalizedUid, uidMissing }) => {
|
|
51
|
+
const uidMsg = !uidMissing && normalizedUid ? `UID ${maskUid(normalizedUid)}` : 'UID MISSING';
|
|
52
|
+
this.console.log(
|
|
53
|
+
`Baichuan TCP failed during discovery for ${ipAddress}; falling back to UDP/BCUDP (${uidMsg}).`,
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
);
|
|
46
57
|
|
|
47
58
|
try {
|
|
48
59
|
const deviceInfo = await api.getInfo();
|
|
@@ -70,6 +81,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
70
81
|
device.storageSettings.values.password = password;
|
|
71
82
|
device.storageSettings.values.rtspChannel = rtspChannel;
|
|
72
83
|
device.storageSettings.values.ipAddress = ipAddress;
|
|
84
|
+
if (uid) device.storageSettings.values.uid = uid;
|
|
73
85
|
device.storageSettings.values.capabilities = capabilities;
|
|
74
86
|
device.updateDeviceInfo();
|
|
75
87
|
|