@apocaliss92/scrypted-reolink-native 0.4.8 → 0.4.10
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/baichuan-base.ts +70 -3
- package/src/camera.ts +40 -12
- package/src/connect.ts +77 -67
- package/src/main.ts +35 -5
- package/src/nvr.ts +4 -0
- package/src/utils.ts +2 -0
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/baichuan-base.ts
CHANGED
|
@@ -199,6 +199,24 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
199
199
|
*/
|
|
200
200
|
protected abstract getConnectionCallbacks(): BaichuanConnectionCallbacks;
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Check if this is an NVR/Hub device (multiple channels).
|
|
204
|
+
* Override in subclasses to return true for NVR devices.
|
|
205
|
+
* This is used for socket pooling to allocate separate sockets per channel.
|
|
206
|
+
*/
|
|
207
|
+
protected isNvrDevice(): boolean {
|
|
208
|
+
return false; // Default: standalone camera
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if this is a battery-powered device.
|
|
213
|
+
* Override in subclasses to return true for battery cameras.
|
|
214
|
+
* This is used to enable idle disconnect to preserve battery life.
|
|
215
|
+
*/
|
|
216
|
+
protected isBatteryDevice(): boolean {
|
|
217
|
+
return false; // Default: AC-powered
|
|
218
|
+
}
|
|
219
|
+
|
|
202
220
|
/**
|
|
203
221
|
* Check if debug logging is enabled
|
|
204
222
|
*/
|
|
@@ -236,8 +254,16 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
236
254
|
* Ensure Baichuan client is connected and ready
|
|
237
255
|
*/
|
|
238
256
|
async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
|
|
257
|
+
const logger = this.getBaichuanLogger();
|
|
258
|
+
const caller = new Error().stack?.split("\n")[2]?.trim() ?? "unknown";
|
|
259
|
+
|
|
239
260
|
// Prevent concurrent login storms - check promise first
|
|
240
|
-
if (this.ensureClientPromise)
|
|
261
|
+
if (this.ensureClientPromise) {
|
|
262
|
+
logger.debug(
|
|
263
|
+
`ensureBaichuanClient: waiting on existing promise (caller: ${caller})`,
|
|
264
|
+
);
|
|
265
|
+
return await this.ensureClientPromise;
|
|
266
|
+
}
|
|
241
267
|
|
|
242
268
|
// Reuse existing client if socket is still connected and logged in
|
|
243
269
|
// Check this AFTER checking the promise to avoid race conditions
|
|
@@ -247,18 +273,22 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
247
273
|
|
|
248
274
|
// Only reuse if both conditions are true
|
|
249
275
|
if (isConnected && isLoggedIn) {
|
|
276
|
+
logger.debug(
|
|
277
|
+
`ensureBaichuanClient: reusing existing client (caller: ${caller})`,
|
|
278
|
+
);
|
|
250
279
|
return this.baichuanApi;
|
|
251
280
|
}
|
|
252
281
|
|
|
253
282
|
// If socket is not connected or not logged in, cleanup the stale client
|
|
254
283
|
// This prevents leaking connections when the socket appears connected but isn't
|
|
255
|
-
const logger = this.getBaichuanLogger();
|
|
256
284
|
logger.log(
|
|
257
|
-
`Stale client detected: connected=${isConnected}, loggedIn=${isLoggedIn}, cleaning up`,
|
|
285
|
+
`Stale client detected: connected=${isConnected}, loggedIn=${isLoggedIn}, cleaning up (caller: ${caller})`,
|
|
258
286
|
);
|
|
259
287
|
await this.cleanupBaichuanApi();
|
|
260
288
|
}
|
|
261
289
|
|
|
290
|
+
logger.log(`ensureBaichuanClient: creating NEW client (caller: ${caller})`);
|
|
291
|
+
|
|
262
292
|
// IMPORTANT: Assign the promise BEFORE the backoff to prevent parallel reconnections
|
|
263
293
|
this.ensureClientPromise = (async () => {
|
|
264
294
|
// Apply backoff to avoid aggressive reconnection after disconnection
|
|
@@ -299,6 +329,16 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
299
329
|
|
|
300
330
|
await api.login();
|
|
301
331
|
|
|
332
|
+
// Set NVR flag BEFORE any streaming to ensure correct socket pooling
|
|
333
|
+
// NVR devices need separate sockets per channel
|
|
334
|
+
api.setIsNvr(this.isNvrDevice());
|
|
335
|
+
|
|
336
|
+
// Enable idle disconnect for battery cameras to preserve battery life
|
|
337
|
+
// AC-powered cameras (including UDP cameras like Elite Floodlight WiFi) don't need it
|
|
338
|
+
if (this.isBatteryDevice()) {
|
|
339
|
+
api.setIdleDisconnect(true);
|
|
340
|
+
}
|
|
341
|
+
|
|
302
342
|
// Verify socket is connected before returning
|
|
303
343
|
if (!api.client.isSocketConnected()) {
|
|
304
344
|
throw new Error("Socket not connected after login");
|
|
@@ -657,8 +697,15 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
657
697
|
`No events received in the last ${Math.round(timeSinceLastEvent / 60_000)} minutes, restarting event listener`,
|
|
658
698
|
);
|
|
659
699
|
// Restart event subscription
|
|
700
|
+
logger.debug(
|
|
701
|
+
"Restarting event listener: calling unsubscribeFromEvents...",
|
|
702
|
+
);
|
|
660
703
|
await this.unsubscribeFromEvents(true);
|
|
704
|
+
logger.debug(
|
|
705
|
+
"Restarting event listener: calling subscribeToEvents...",
|
|
706
|
+
);
|
|
661
707
|
await this.subscribeToEvents(true);
|
|
708
|
+
logger.debug("Restarting event listener: done");
|
|
662
709
|
} else if (this.lastEventTime === 0) {
|
|
663
710
|
// If lastEventTime is 0, it means we just subscribed but haven't received any events yet
|
|
664
711
|
// Wait a bit longer before considering it a problem
|
|
@@ -667,8 +714,17 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
667
714
|
logger.log(
|
|
668
715
|
`No events received since subscription (${Math.round(timeSinceSubscription / 60_000)} minutes ago), restarting event listener`,
|
|
669
716
|
);
|
|
717
|
+
logger.debug(
|
|
718
|
+
"Restarting event listener (no events since sub): calling unsubscribeFromEvents...",
|
|
719
|
+
);
|
|
670
720
|
await this.unsubscribeFromEvents(true);
|
|
721
|
+
logger.debug(
|
|
722
|
+
"Restarting event listener (no events since sub): calling subscribeToEvents...",
|
|
723
|
+
);
|
|
671
724
|
await this.subscribeToEvents(true);
|
|
725
|
+
logger.debug(
|
|
726
|
+
"Restarting event listener (no events since sub): done",
|
|
727
|
+
);
|
|
672
728
|
}
|
|
673
729
|
}
|
|
674
730
|
} catch (e) {
|
|
@@ -694,6 +750,13 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
694
750
|
async subscribeToEvents(silent: boolean = false): Promise<void> {
|
|
695
751
|
const logger = this.getBaichuanLogger();
|
|
696
752
|
const callbacks = this.getConnectionCallbacks();
|
|
753
|
+
const existingClientInfo = this.baichuanApi
|
|
754
|
+
? `connected=${this.baichuanApi.client.isSocketConnected()}, loggedIn=${this.baichuanApi.client.loggedIn}`
|
|
755
|
+
: "no client";
|
|
756
|
+
|
|
757
|
+
logger.debug(
|
|
758
|
+
`subscribeToEvents() called: silent=${silent}, existingClient=[${existingClientInfo}], eventSubscriptionActive=${this.eventSubscriptionActive}`,
|
|
759
|
+
);
|
|
697
760
|
|
|
698
761
|
if (!callbacks.onSimpleEvent) {
|
|
699
762
|
return;
|
|
@@ -716,7 +779,11 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
716
779
|
await this.unsubscribeFromEvents(silent);
|
|
717
780
|
|
|
718
781
|
// Get Baichuan client connection
|
|
782
|
+
logger.debug("subscribeToEvents: calling ensureBaichuanClient...");
|
|
719
783
|
const api = await this.ensureBaichuanClient();
|
|
784
|
+
logger.debug(
|
|
785
|
+
`subscribeToEvents: ensureBaichuanClient returned, reused=${api === this.baichuanApi}`,
|
|
786
|
+
);
|
|
720
787
|
|
|
721
788
|
// Verify connection is ready
|
|
722
789
|
if (!api.client.isSocketConnected() || !api.client.loggedIn) {
|
package/src/camera.ts
CHANGED
|
@@ -104,8 +104,10 @@ import {
|
|
|
104
104
|
export type CameraType =
|
|
105
105
|
| "battery"
|
|
106
106
|
| "regular"
|
|
107
|
+
| "udp" // UDP camera without battery (e.g., Elite Floodlight WiFi)
|
|
107
108
|
| "multi-focal"
|
|
108
|
-
| "multi-focal-battery"
|
|
109
|
+
| "multi-focal-battery"
|
|
110
|
+
| "multi-focal-udp"; // UDP multifocal without battery
|
|
109
111
|
|
|
110
112
|
export interface ReolinkCameraOptions {
|
|
111
113
|
type: CameraType;
|
|
@@ -738,6 +740,7 @@ export class ReolinkCamera
|
|
|
738
740
|
protected multiFocalDevice?: ReolinkNativeMultiFocalDevice;
|
|
739
741
|
thisDevice: Settings;
|
|
740
742
|
isBattery: boolean;
|
|
743
|
+
isUdpCamera: boolean; // UDP camera without battery (e.g., Elite Floodlight WiFi)
|
|
741
744
|
isMultiFocal: boolean;
|
|
742
745
|
isOnNvr: boolean;
|
|
743
746
|
protocol: BaichuanTransport;
|
|
@@ -769,7 +772,12 @@ export class ReolinkCamera
|
|
|
769
772
|
) {
|
|
770
773
|
const isBattery =
|
|
771
774
|
options.type === "battery" || options.type === "multi-focal-battery";
|
|
772
|
-
|
|
775
|
+
// UDP cameras include: battery cameras, NVR children, and UDP-only cameras (e.g., Elite Floodlight WiFi)
|
|
776
|
+
const isUdpCamera =
|
|
777
|
+
options.type === "udp" || options.type === "multi-focal-udp";
|
|
778
|
+
const transport = isBattery || isUdpCamera ? "udp" : "tcp";
|
|
779
|
+
// const transport =
|
|
780
|
+
// isBattery || isUdpCamera || !!options.nvrDevice ? "udp" : "tcp";
|
|
773
781
|
super(nativeId, transport);
|
|
774
782
|
this.plugin.camerasMap.set(this.id, this);
|
|
775
783
|
|
|
@@ -779,8 +787,11 @@ export class ReolinkCamera
|
|
|
779
787
|
this.thisDevice = sdk.systemManager.getDeviceById<Settings>(this.id);
|
|
780
788
|
|
|
781
789
|
this.isBattery = isBattery;
|
|
790
|
+
this.isUdpCamera = isUdpCamera; // UDP camera without battery (e.g., Elite Floodlight WiFi)
|
|
782
791
|
this.isMultiFocal =
|
|
783
|
-
options.type === "multi-focal" ||
|
|
792
|
+
options.type === "multi-focal" ||
|
|
793
|
+
options.type === "multi-focal-battery" ||
|
|
794
|
+
options.type === "multi-focal-udp";
|
|
784
795
|
this.isOnNvr = !!this.nvrDevice || !!this.multiFocalDevice?.nvrDevice;
|
|
785
796
|
this.protocol = transport;
|
|
786
797
|
|
|
@@ -1475,17 +1486,21 @@ export class ReolinkCamera
|
|
|
1475
1486
|
const { ipAddress, username, password, uid, discoveryMethod } =
|
|
1476
1487
|
this.storageSettings.values;
|
|
1477
1488
|
const debugOptions = this.getBaichuanDebugOptions();
|
|
1478
|
-
const normalizedUid = this.isBattery ? normalizeUid(uid) : undefined;
|
|
1479
1489
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1490
|
+
// UDP cameras (battery or UDP-only like Elite Floodlight WiFi) require UID
|
|
1491
|
+
// const requiresUid = this.isBattery || this.isUdpCamera;
|
|
1492
|
+
// const normalizedUid = requiresUid ? normalizeUid(uid) : undefined;
|
|
1493
|
+
const normalizedUid = normalizeUid(uid);
|
|
1494
|
+
|
|
1495
|
+
// if (requiresUid && !normalizedUid) {
|
|
1496
|
+
// throw new Error("UID is required for UDP cameras (BCUDP)");
|
|
1497
|
+
// }
|
|
1483
1498
|
|
|
1484
1499
|
// Prevent accidental connections to localhost (Node will default host=127.0.0.1 when host is undefined).
|
|
1485
1500
|
// This shows up as connect ECONNREFUSED 127.0.0.1:9000 and will never recover with socket resets.
|
|
1486
|
-
if (!
|
|
1487
|
-
|
|
1488
|
-
}
|
|
1501
|
+
// if (!requiresUid && !ipAddress) {
|
|
1502
|
+
// throw new Error("IP Address is required for TCP devices");
|
|
1503
|
+
// }
|
|
1489
1504
|
|
|
1490
1505
|
return {
|
|
1491
1506
|
host: ipAddress,
|
|
@@ -1494,8 +1509,9 @@ export class ReolinkCamera
|
|
|
1494
1509
|
uid: normalizedUid,
|
|
1495
1510
|
transport: this.protocol,
|
|
1496
1511
|
debugOptions,
|
|
1497
|
-
udpDiscoveryMethod:
|
|
1498
|
-
|
|
1512
|
+
udpDiscoveryMethod: discoveryMethod,
|
|
1513
|
+
// NOTE: idleDisconnect is NOT set here - the library handles it internally
|
|
1514
|
+
// based on the battery status detected during connection
|
|
1499
1515
|
};
|
|
1500
1516
|
}
|
|
1501
1517
|
|
|
@@ -1530,6 +1546,10 @@ export class ReolinkCamera
|
|
|
1530
1546
|
return this.storageSettings.values.debugLogs;
|
|
1531
1547
|
}
|
|
1532
1548
|
|
|
1549
|
+
protected isBatteryDevice(): boolean {
|
|
1550
|
+
return this.isBattery;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1533
1553
|
protected getDeviceName(): string {
|
|
1534
1554
|
return this.name || "Camera";
|
|
1535
1555
|
}
|
|
@@ -3287,6 +3307,14 @@ export class ReolinkCamera
|
|
|
3287
3307
|
const hideUid = !this.isBattery || this.isOnNvr || !!this.multiFocalDevice;
|
|
3288
3308
|
this.storageSettings.settings.uid.hide = hideUid;
|
|
3289
3309
|
this.storageSettings.settings.discoveryMethod.hide = hideUid;
|
|
3310
|
+
// Show UID and discovery method for UDP cameras (battery or UDP-only like Elite Floodlight WiFi)
|
|
3311
|
+
// Hide for NVR children or multifocal lenses (they use parent's connection)
|
|
3312
|
+
// const requiresUidSettings =
|
|
3313
|
+
// (this.isBattery || this.isUdpCamera) &&
|
|
3314
|
+
// !this.isOnNvr &&
|
|
3315
|
+
// !this.multiFocalDevice;
|
|
3316
|
+
// this.storageSettings.settings.uid.hide = !requiresUidSettings;
|
|
3317
|
+
// this.storageSettings.settings.discoveryMethod.hide = !requiresUidSettings;
|
|
3290
3318
|
|
|
3291
3319
|
if (this.isBattery && !this.storageSettings.values.mixinsSetup) {
|
|
3292
3320
|
try {
|
package/src/connect.ts
CHANGED
|
@@ -1,89 +1,99 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BaichuanTransport as BaichuanTransportParent,
|
|
3
|
+
BaichuanClientOptions,
|
|
4
|
+
ReolinkBaichuanApi,
|
|
5
|
+
} from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
6
|
|
|
3
7
|
export type BaichuanTransport = BaichuanTransportParent;
|
|
4
8
|
|
|
5
9
|
export type BaichuanConnectInputs = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
host: string;
|
|
11
|
+
username: string;
|
|
12
|
+
password: string;
|
|
13
|
+
uid?: string;
|
|
14
|
+
logger?: Console;
|
|
15
|
+
debugOptions?: BaichuanClientOptions["debugOptions"];
|
|
16
|
+
udpDiscoveryMethod?: BaichuanClientOptions["udpDiscoveryMethod"];
|
|
13
17
|
};
|
|
14
18
|
|
|
15
19
|
export function normalizeUid(uid?: string): string | undefined {
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
const v = uid?.trim();
|
|
21
|
+
return v ? v : undefined;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
export async function createBaichuanApi(props: {
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
inputs: BaichuanConnectInputs;
|
|
26
|
+
transport: BaichuanTransport;
|
|
23
27
|
}): Promise<ReolinkBaichuanApi> {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
const { inputs, transport } = props;
|
|
29
|
+
const { logger } = inputs;
|
|
30
|
+
const { ReolinkBaichuanApi } =
|
|
31
|
+
await import("@apocaliss92/reolink-baichuan-js");
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
const base: BaichuanClientOptions = {
|
|
34
|
+
host: inputs.host,
|
|
35
|
+
username: inputs.username,
|
|
36
|
+
password: inputs.password,
|
|
37
|
+
logger,
|
|
38
|
+
debugOptions: inputs.debugOptions ?? {},
|
|
39
|
+
};
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Handle 'close' event to prevent unhandled rejections from pending promises
|
|
57
|
-
api.client.on("close", () => {
|
|
58
|
-
// Socket closed - pending promises will be rejected, but we've already handled errors above
|
|
59
|
-
// This handler prevents the close event from causing issues
|
|
60
|
-
});
|
|
61
|
-
} catch {
|
|
62
|
-
// ignore
|
|
41
|
+
const attachErrorHandler = (api: ReolinkBaichuanApi) => {
|
|
42
|
+
// Critical: BaichuanClient emits 'error'. If nobody listens, Node treats it as an
|
|
43
|
+
// uncaught exception. Ensure we always have a listener.
|
|
44
|
+
try {
|
|
45
|
+
api.client.on("error", (err: unknown) => {
|
|
46
|
+
if (!logger) return;
|
|
47
|
+
const msg =
|
|
48
|
+
(err as any)?.message || (err as any)?.toString?.() || String(err);
|
|
49
|
+
// Only log if it's not a recoverable error to avoid spam
|
|
50
|
+
if (
|
|
51
|
+
typeof msg === "string" &&
|
|
52
|
+
(msg.includes("Baichuan socket closed") ||
|
|
53
|
+
msg.includes("Baichuan UDP stream closed") ||
|
|
54
|
+
msg.includes("Not running"))
|
|
55
|
+
) {
|
|
56
|
+
// Silently ignore recoverable socket close errors and "Not running" errors
|
|
57
|
+
// "Not running" is common for UDP/battery cameras when sleeping or during initialization
|
|
58
|
+
return;
|
|
63
59
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
...base,
|
|
69
|
-
transport: "tcp",
|
|
70
|
-
});
|
|
71
|
-
attachErrorHandler(api);
|
|
72
|
-
return api;
|
|
73
|
-
}
|
|
60
|
+
logger.error(
|
|
61
|
+
`[BaichuanClient] error (${transport}) ${inputs.host}: ${msg}`,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
74
64
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
65
|
+
// Handle 'close' event to prevent unhandled rejections from pending promises
|
|
66
|
+
api.client.on("close", () => {
|
|
67
|
+
// Socket closed - pending promises will be rejected, but we've already handled errors above
|
|
68
|
+
// This handler prevents the close event from causing issues
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
// ignore
|
|
78
72
|
}
|
|
73
|
+
};
|
|
79
74
|
|
|
75
|
+
if (transport === "tcp") {
|
|
80
76
|
const api = new ReolinkBaichuanApi({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
uid,
|
|
84
|
-
idleDisconnect: true,
|
|
85
|
-
udpDiscoveryMethod: inputs.udpDiscoveryMethod,
|
|
77
|
+
...base,
|
|
78
|
+
transport: "tcp",
|
|
86
79
|
});
|
|
87
80
|
attachErrorHandler(api);
|
|
88
81
|
return api;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const uid = normalizeUid(inputs.uid);
|
|
85
|
+
if (!uid) {
|
|
86
|
+
throw new Error("UID is required for UDP cameras (BCUDP)");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const api = new ReolinkBaichuanApi({
|
|
90
|
+
...base,
|
|
91
|
+
transport: "udp",
|
|
92
|
+
uid,
|
|
93
|
+
// NOTE: idleDisconnect is NOT set here - the library handles it internally
|
|
94
|
+
// based on the battery status detected during connection/autodetect
|
|
95
|
+
udpDiscoveryMethod: inputs.udpDiscoveryMethod,
|
|
96
|
+
});
|
|
97
|
+
attachErrorHandler(api);
|
|
98
|
+
return api;
|
|
89
99
|
}
|
package/src/main.ts
CHANGED
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
handleVideoClipRequest,
|
|
38
38
|
multifocalSuffix,
|
|
39
39
|
nvrSuffix,
|
|
40
|
+
udpCameraSuffix,
|
|
41
|
+
udpMultifocalSuffix,
|
|
40
42
|
} from "./utils";
|
|
41
43
|
import { randomBytes } from "crypto";
|
|
42
44
|
import { ReolinkCamera } from "./camera";
|
|
@@ -135,14 +137,25 @@ class ReolinkNativePlugin
|
|
|
135
137
|
|
|
136
138
|
// Handle multi-focal device case
|
|
137
139
|
if (detection.type === "multifocal") {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
settings.newCamera ||= name;
|
|
140
|
+
// Determine suffix based on transport and battery capability
|
|
141
|
+
// UDP transport does NOT always mean battery (e.g., some floodlight cameras use UDP but are AC-powered)
|
|
142
|
+
const isUdp = detection.transport === "udp";
|
|
142
143
|
|
|
144
|
+
// Get capabilities to check if device has battery
|
|
143
145
|
const { capabilities, objects, presets } =
|
|
144
146
|
await detectedApi.getDeviceCapabilities();
|
|
145
147
|
|
|
148
|
+
const hasBattery = capabilities?.hasBattery === true;
|
|
149
|
+
|
|
150
|
+
// Choose suffix based on transport and battery
|
|
151
|
+
if (isUdp) {
|
|
152
|
+
nativeId = `${identifier}${hasBattery ? batteryMultifocalSuffix : udpMultifocalSuffix}`;
|
|
153
|
+
} else {
|
|
154
|
+
nativeId = `${identifier}${multifocalSuffix}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
settings.newCamera ||= name;
|
|
158
|
+
|
|
146
159
|
const { interfaces } = getDeviceInterfaces({
|
|
147
160
|
capabilities,
|
|
148
161
|
logger: this.console,
|
|
@@ -201,10 +214,17 @@ class ReolinkNativePlugin
|
|
|
201
214
|
return nativeId;
|
|
202
215
|
}
|
|
203
216
|
|
|
204
|
-
// Create nativeId based on device type
|
|
217
|
+
// Create nativeId based on device type and transport
|
|
218
|
+
// UDP transport does NOT always mean battery (e.g., Elite Floodlight WiFi uses UDP but is AC-powered)
|
|
219
|
+
const isUdp = detection.transport === "udp";
|
|
205
220
|
if (detection.type === "battery-cam") {
|
|
221
|
+
// Battery camera (always UDP)
|
|
206
222
|
nativeId = `${identifier}${batteryCameraSuffix}`;
|
|
223
|
+
} else if (isUdp) {
|
|
224
|
+
// UDP camera without battery (e.g., Elite Floodlight WiFi)
|
|
225
|
+
nativeId = `${identifier}${udpCameraSuffix}`;
|
|
207
226
|
} else {
|
|
227
|
+
// Regular TCP camera
|
|
208
228
|
nativeId = `${identifier}${cameraSuffix}`;
|
|
209
229
|
}
|
|
210
230
|
|
|
@@ -315,8 +335,18 @@ class ReolinkNativePlugin
|
|
|
315
335
|
this,
|
|
316
336
|
"multi-focal-battery",
|
|
317
337
|
);
|
|
338
|
+
} else if (nativeId.endsWith(udpMultifocalSuffix)) {
|
|
339
|
+
// UDP multifocal without battery
|
|
340
|
+
return new ReolinkNativeMultiFocalDevice(
|
|
341
|
+
nativeId,
|
|
342
|
+
this,
|
|
343
|
+
"multi-focal-udp",
|
|
344
|
+
);
|
|
318
345
|
} else if (nativeId.endsWith(multifocalSuffix)) {
|
|
319
346
|
return new ReolinkNativeMultiFocalDevice(nativeId, this, "multi-focal");
|
|
347
|
+
} else if (nativeId.endsWith(udpCameraSuffix)) {
|
|
348
|
+
// UDP camera without battery (e.g., Elite Floodlight WiFi)
|
|
349
|
+
return new ReolinkCamera(nativeId, this, { type: "udp" });
|
|
320
350
|
} else {
|
|
321
351
|
return new ReolinkCamera(nativeId, this, { type: "regular" });
|
|
322
352
|
}
|
package/src/nvr.ts
CHANGED
|
@@ -218,6 +218,10 @@ export class ReolinkNativeNvrDevice
|
|
|
218
218
|
};
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
protected isNvrDevice(): boolean {
|
|
222
|
+
return true; // NVR/Hub always returns true
|
|
223
|
+
}
|
|
224
|
+
|
|
221
225
|
protected isDebugEnabled(): boolean {
|
|
222
226
|
return this.storageSettings.values.debugLogs || false;
|
|
223
227
|
}
|
package/src/utils.ts
CHANGED
|
@@ -37,8 +37,10 @@ export type OperationChannelMap = Partial<Record<OperationChannelType, number>>;
|
|
|
37
37
|
|
|
38
38
|
export const nvrSuffix = `-nvr`;
|
|
39
39
|
export const batteryCameraSuffix = `-battery-cam`;
|
|
40
|
+
export const udpCameraSuffix = `-udp-cam`; // UDP camera without battery (e.g., Elite Floodlight WiFi)
|
|
40
41
|
export const multifocalSuffix = `-multifocal`;
|
|
41
42
|
export const batteryMultifocalSuffix = `-battery-multifocal`;
|
|
43
|
+
export const udpMultifocalSuffix = `-udp-multifocal`; // UDP multifocal without battery
|
|
42
44
|
export const cameraSuffix = `-cam`;
|
|
43
45
|
export const sirenSuffix = `-siren`;
|
|
44
46
|
export const floodlightSuffix = `-floodlight`;
|