@apocaliss92/scrypted-reolink-native 0.1.16 → 0.1.17
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/common.ts +22 -10
- package/src/connect.ts +38 -50
- package/src/main.ts +59 -16
- package/src/multifocal.ts +398 -0
- package/src/nvr.ts +32 -3
- package/src/utils.ts +6 -1
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/common.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { convertDebugLogsToApiOptions, DebugLogDisplayNames, DebugLogOption, get
|
|
|
8
8
|
import { ReolinkBaichuanIntercom } from "./intercom";
|
|
9
9
|
import ReolinkNativePlugin from "./main";
|
|
10
10
|
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
11
|
+
import { ReolinkNativeMultiFocalDevice } from "./multifocal";
|
|
11
12
|
import { ReolinkPtzPresets } from "./presets";
|
|
12
13
|
import {
|
|
13
14
|
buildVideoStreamOptions,
|
|
@@ -26,6 +27,7 @@ export type CameraType = 'battery' | 'regular';
|
|
|
26
27
|
export interface CommonCameraMixinOptions {
|
|
27
28
|
type: CameraType;
|
|
28
29
|
nvrDevice?: ReolinkNativeNvrDevice; // Optional reference to NVR device
|
|
30
|
+
multiFocalDevice?: ReolinkNativeMultiFocalDevice; // Optional reference to multi-focal device
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
|
|
@@ -230,11 +232,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
230
232
|
await this.credentialsChanged();
|
|
231
233
|
}
|
|
232
234
|
},
|
|
233
|
-
isFromNvr: {
|
|
234
|
-
type: 'boolean',
|
|
235
|
-
hide: true,
|
|
236
|
-
defaultValue: false,
|
|
237
|
-
},
|
|
238
235
|
mixinsSetup: {
|
|
239
236
|
type: 'boolean',
|
|
240
237
|
hide: true,
|
|
@@ -525,14 +522,16 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
525
522
|
resetBaichuanClient?(reason?: any): Promise<void>;
|
|
526
523
|
|
|
527
524
|
protected nvrDevice?: ReolinkNativeNvrDevice;
|
|
525
|
+
protected multiFocalDevice?: ReolinkNativeMultiFocalDevice;
|
|
528
526
|
thisDevice: Settings
|
|
529
527
|
|
|
530
528
|
constructor(nativeId: string, public plugin: ReolinkNativePlugin, public options: CommonCameraMixinOptions) {
|
|
531
529
|
super(nativeId);
|
|
532
|
-
this.protocol = !options.nvrDevice && options.type === 'battery' ? 'udp' : 'tcp';
|
|
530
|
+
this.protocol = !options.nvrDevice && !options.multiFocalDevice && options.type === 'battery' ? 'udp' : 'tcp';
|
|
533
531
|
|
|
534
532
|
// Store NVR device reference if provided
|
|
535
533
|
this.nvrDevice = options.nvrDevice;
|
|
534
|
+
this.multiFocalDevice = options.multiFocalDevice;
|
|
536
535
|
this.thisDevice = sdk.systemManager.getDeviceById<Settings>(this.id);
|
|
537
536
|
|
|
538
537
|
setTimeout(async () => {
|
|
@@ -745,7 +744,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
745
744
|
}
|
|
746
745
|
|
|
747
746
|
async subscribeToEvents(): Promise<void> {
|
|
748
|
-
if (this.nvrDevice) {
|
|
747
|
+
if (this.nvrDevice || this.multiFocalDevice) {
|
|
749
748
|
return;
|
|
750
749
|
}
|
|
751
750
|
|
|
@@ -1110,7 +1109,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1110
1109
|
const { ipAddress, rtspChannel } = this.storageSettings.values;
|
|
1111
1110
|
try {
|
|
1112
1111
|
const api = await this.ensureClient();
|
|
1113
|
-
const deviceData = await api.getInfo(this.nvrDevice ? rtspChannel : undefined);
|
|
1112
|
+
const deviceData = await api.getInfo((this.nvrDevice || this.multiFocalDevice) ? rtspChannel : undefined);
|
|
1114
1113
|
|
|
1115
1114
|
await updateDeviceInfo({
|
|
1116
1115
|
device: this,
|
|
@@ -1458,6 +1457,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1458
1457
|
if (this.nvrDevice) {
|
|
1459
1458
|
return await this.nvrDevice.ensureBaichuanClient();
|
|
1460
1459
|
}
|
|
1460
|
+
if (this.multiFocalDevice) {
|
|
1461
|
+
return await this.multiFocalDevice.ensureBaichuanClient();
|
|
1462
|
+
}
|
|
1461
1463
|
|
|
1462
1464
|
// Use base class implementation
|
|
1463
1465
|
return await this.ensureBaichuanClient();
|
|
@@ -1619,9 +1621,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1619
1621
|
logger.warn('Failed to subscribe to Baichuan events', e);
|
|
1620
1622
|
}
|
|
1621
1623
|
|
|
1622
|
-
const { isFromNvr } = this.storageSettings.values;
|
|
1623
1624
|
|
|
1624
|
-
if (
|
|
1625
|
+
if (this.nvrDevice) {
|
|
1625
1626
|
this.storageSettings.settings.username.hide = true;
|
|
1626
1627
|
this.storageSettings.settings.password.hide = true;
|
|
1627
1628
|
this.storageSettings.settings.ipAddress.hide = true;
|
|
@@ -1632,6 +1633,17 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1632
1633
|
this.storageSettings.settings.ipAddress.defaultValue = this.nvrDevice.storageSettings.values.ipAddress;
|
|
1633
1634
|
}
|
|
1634
1635
|
|
|
1636
|
+
if (this.multiFocalDevice) {
|
|
1637
|
+
this.storageSettings.settings.username.hide = true;
|
|
1638
|
+
this.storageSettings.settings.password.hide = true;
|
|
1639
|
+
this.storageSettings.settings.ipAddress.hide = true;
|
|
1640
|
+
this.storageSettings.settings.uid.hide = true;
|
|
1641
|
+
|
|
1642
|
+
this.storageSettings.settings.username.defaultValue = this.multiFocalDevice.storageSettings.values.username;
|
|
1643
|
+
this.storageSettings.settings.password.defaultValue = this.multiFocalDevice.storageSettings.values.password;
|
|
1644
|
+
this.storageSettings.settings.ipAddress.defaultValue = this.multiFocalDevice.storageSettings.values.ipAddress;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1635
1647
|
await this.init();
|
|
1636
1648
|
this.initComplete = true;
|
|
1637
1649
|
}
|
package/src/connect.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BaichuanClientOptions, ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
1
|
+
import type { BaichuanClientOptions, ReolinkBaichuanApi, ReolinkDeviceInfo } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
2
|
|
|
3
3
|
export type BaichuanTransport = "tcp" | "udp";
|
|
4
4
|
|
|
@@ -116,13 +116,13 @@ export type UdpFallbackInfo = {
|
|
|
116
116
|
tcpError: unknown;
|
|
117
117
|
};
|
|
118
118
|
|
|
119
|
-
export type DeviceType = 'camera' | 'battery-cam' | 'nvr';
|
|
119
|
+
export type DeviceType = 'camera' | 'battery-cam' | 'nvr' | 'multifocal';
|
|
120
120
|
|
|
121
121
|
export type AutoDetectResult = {
|
|
122
122
|
type: DeviceType;
|
|
123
123
|
transport: BaichuanTransport;
|
|
124
|
-
uid
|
|
125
|
-
deviceInfo?:
|
|
124
|
+
uid: string;
|
|
125
|
+
deviceInfo?: ReolinkDeviceInfo;
|
|
126
126
|
channelNum?: number;
|
|
127
127
|
};
|
|
128
128
|
|
|
@@ -134,7 +134,7 @@ async function pingHost(host: string, timeoutMs: number = 3000): Promise<boolean
|
|
|
134
134
|
const { exec } = require('child_process');
|
|
135
135
|
const platform = process.platform;
|
|
136
136
|
const pingCmd = platform === 'win32' ? `ping -n 1 -w ${timeoutMs} ${host}` : `ping -c 1 -W ${Math.floor(timeoutMs / 1000)} ${host}`;
|
|
137
|
-
|
|
137
|
+
|
|
138
138
|
exec(pingCmd, (error: any) => {
|
|
139
139
|
resolve(!error);
|
|
140
140
|
});
|
|
@@ -173,13 +173,26 @@ export async function autoDetectDeviceType(
|
|
|
173
173
|
});
|
|
174
174
|
await tcpApi.login();
|
|
175
175
|
|
|
176
|
-
// Get device info to check
|
|
176
|
+
// Get device info to check device type
|
|
177
177
|
const deviceInfo = await tcpApi.getInfo();
|
|
178
178
|
const { support } = await tcpApi.getDeviceCapabilities(0);
|
|
179
179
|
const channelNum = support?.channelNum ?? 1;
|
|
180
180
|
|
|
181
181
|
logger.log(`[AutoDetect] TCP connection successful. channelNum=${channelNum}`);
|
|
182
182
|
|
|
183
|
+
// Multi-focal devices have 2 or 3 channels
|
|
184
|
+
if (channelNum === 2 || channelNum === 3) {
|
|
185
|
+
logger.log(`[AutoDetect] Detected multi-focal device (${channelNum} channels, channelNum=${channelNum})`);
|
|
186
|
+
await tcpApi.close();
|
|
187
|
+
return {
|
|
188
|
+
type: 'multifocal',
|
|
189
|
+
transport: 'tcp',
|
|
190
|
+
uid: uid || '',
|
|
191
|
+
deviceInfo,
|
|
192
|
+
channelNum,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
183
196
|
// If channelNum > 1, it's likely an NVR
|
|
184
197
|
if (channelNum > 1) {
|
|
185
198
|
logger.log(`[AutoDetect] Detected NVR (${channelNum} channels)`);
|
|
@@ -187,6 +200,7 @@ export async function autoDetectDeviceType(
|
|
|
187
200
|
return {
|
|
188
201
|
type: 'nvr',
|
|
189
202
|
transport: 'tcp',
|
|
203
|
+
uid: uid || '',
|
|
190
204
|
deviceInfo,
|
|
191
205
|
channelNum,
|
|
192
206
|
};
|
|
@@ -198,6 +212,7 @@ export async function autoDetectDeviceType(
|
|
|
198
212
|
return {
|
|
199
213
|
type: 'camera',
|
|
200
214
|
transport: 'tcp',
|
|
215
|
+
uid: uid || '',
|
|
201
216
|
deviceInfo,
|
|
202
217
|
channelNum: 1,
|
|
203
218
|
};
|
|
@@ -233,6 +248,23 @@ export async function autoDetectDeviceType(
|
|
|
233
248
|
await udpApi.login();
|
|
234
249
|
|
|
235
250
|
const deviceInfo = await udpApi.getInfo();
|
|
251
|
+
const { support } = await udpApi.getDeviceCapabilities(0);
|
|
252
|
+
const channelNum = support?.channelNum ?? 1;
|
|
253
|
+
|
|
254
|
+
// Multi-focal devices can also be UDP (battery multi-focal cameras)
|
|
255
|
+
if (channelNum === 2 || channelNum === 3) {
|
|
256
|
+
logger.log(`[AutoDetect] UDP connection successful. Detected multi-focal device (${channelNum} channels).`);
|
|
257
|
+
await udpApi.close();
|
|
258
|
+
return {
|
|
259
|
+
type: 'multifocal',
|
|
260
|
+
transport: 'udp',
|
|
261
|
+
uid: normalizedUid,
|
|
262
|
+
deviceInfo,
|
|
263
|
+
channelNum,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Regular battery camera
|
|
236
268
|
logger.log(`[AutoDetect] UDP connection successful. Detected battery camera.`);
|
|
237
269
|
await udpApi.close();
|
|
238
270
|
|
|
@@ -251,47 +283,3 @@ export async function autoDetectDeviceType(
|
|
|
251
283
|
}
|
|
252
284
|
}
|
|
253
285
|
}
|
|
254
|
-
|
|
255
|
-
// export async function connectBaichuanWithTcpUdpFallback(
|
|
256
|
-
// inputs: BaichuanConnectInputs,
|
|
257
|
-
// onUdpFallback?: (info: UdpFallbackInfo) => void,
|
|
258
|
-
// ): Promise<{ api: ReolinkBaichuanApi; transport: BaichuanTransport }> {
|
|
259
|
-
// let tcpApi: ReolinkBaichuanApi | undefined;
|
|
260
|
-
// try {
|
|
261
|
-
// tcpApi = await createBaichuanApi(inputs, "tcp");
|
|
262
|
-
// await tcpApi.login();
|
|
263
|
-
// return { api: tcpApi, transport: "tcp" };
|
|
264
|
-
// }
|
|
265
|
-
// catch (e) {
|
|
266
|
-
// try {
|
|
267
|
-
// await tcpApi?.close();
|
|
268
|
-
// }
|
|
269
|
-
// catch {
|
|
270
|
-
// // ignore
|
|
271
|
-
// }
|
|
272
|
-
|
|
273
|
-
// if (!isTcpFailureThatShouldFallbackToUdp(e)) {
|
|
274
|
-
// throw e;
|
|
275
|
-
// }
|
|
276
|
-
|
|
277
|
-
// const uid = normalizeUid(inputs.uid);
|
|
278
|
-
// const uidMissing = !uid;
|
|
279
|
-
|
|
280
|
-
// onUdpFallback?.({
|
|
281
|
-
// host: inputs.host,
|
|
282
|
-
// uid,
|
|
283
|
-
// uidMissing,
|
|
284
|
-
// tcpError: e,
|
|
285
|
-
// });
|
|
286
|
-
|
|
287
|
-
// if (uidMissing) {
|
|
288
|
-
// throw new Error(
|
|
289
|
-
// `Baichuan TCP failed and this camera likely requires UDP/BCUDP. Set the Reolink UID in settings to continue (ip=${inputs.host}).`,
|
|
290
|
-
// );
|
|
291
|
-
// }
|
|
292
|
-
|
|
293
|
-
// const udpApi = await createBaichuanApi(inputs, "udp");
|
|
294
|
-
// await udpApi.login();
|
|
295
|
-
// return { api: udpApi, transport: "udp" };
|
|
296
|
-
// }
|
|
297
|
-
// }
|
package/src/main.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting } from "@scrypted/sdk";
|
|
1
|
+
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings } from "@scrypted/sdk";
|
|
2
2
|
import { ReolinkNativeCamera } from "./camera";
|
|
3
3
|
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
4
4
|
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { ReolinkNativeMultiFocalDevice } from "./multifocal";
|
|
6
|
+
import { autoDetectDeviceType, createBaichuanApi, type BaichuanTransport } from "./connect";
|
|
7
|
+
import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces, multifocalSuffix, nvrSuffix } from "./utils";
|
|
8
|
+
import { BaseBaichuanClass } from "./baichuan-base";
|
|
9
|
+
import { CommonCameraMixin } from "./common";
|
|
7
10
|
|
|
8
11
|
class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
|
9
|
-
devices = new Map<string,
|
|
12
|
+
devices = new Map<string, BaseBaichuanClass>();
|
|
10
13
|
nvrDeviceId: string;
|
|
11
14
|
|
|
12
15
|
constructor(nativeId: string) {
|
|
@@ -20,7 +23,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
20
23
|
return 'Reolink Native camera';
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
async getDevice(nativeId: ScryptedNativeId): Promise<
|
|
26
|
+
async getDevice(nativeId: ScryptedNativeId): Promise<BaseBaichuanClass> {
|
|
24
27
|
if (this.devices.has(nativeId)) {
|
|
25
28
|
return this.devices.get(nativeId)!;
|
|
26
29
|
}
|
|
@@ -55,12 +58,51 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
55
58
|
|
|
56
59
|
this.console.log(`[AutoDetect] Detected device type: ${detection.type} (transport: ${detection.transport})`);
|
|
57
60
|
|
|
61
|
+
// Handle multi-focal device case
|
|
62
|
+
if (detection.type === 'multifocal') {
|
|
63
|
+
const deviceInfo = detection.deviceInfo || {};
|
|
64
|
+
const name = deviceInfo.name || 'Reolink Multi-Focal';
|
|
65
|
+
const serialNumber = deviceInfo.serialNumber || deviceInfo.itemNo || `multifocal-${Date.now()}`;
|
|
66
|
+
nativeId = `${serialNumber}${multifocalSuffix}`;
|
|
67
|
+
|
|
68
|
+
settings.newCamera ||= name;
|
|
69
|
+
|
|
70
|
+
await sdk.deviceManager.onDeviceDiscovered({
|
|
71
|
+
nativeId,
|
|
72
|
+
name,
|
|
73
|
+
interfaces: [
|
|
74
|
+
ScryptedInterface.Settings,
|
|
75
|
+
ScryptedInterface.DeviceDiscovery,
|
|
76
|
+
ScryptedInterface.DeviceProvider,
|
|
77
|
+
ScryptedInterface.Reboot,
|
|
78
|
+
],
|
|
79
|
+
type: ScryptedDeviceType.DeviceProvider,
|
|
80
|
+
providerNativeId: this.nativeId,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const device = await this.getDevice(nativeId);
|
|
84
|
+
if (!(device instanceof ReolinkNativeMultiFocalDevice)) {
|
|
85
|
+
throw new Error('Expected multi-focal device but got different type');
|
|
86
|
+
}
|
|
87
|
+
device.storageSettings.values.ipAddress = ipAddress;
|
|
88
|
+
device.storageSettings.values.username = username;
|
|
89
|
+
device.storageSettings.values.password = password;
|
|
90
|
+
device.storageSettings.values.uid = detection.uid || '';
|
|
91
|
+
|
|
92
|
+
// Update the protocol based on detection result
|
|
93
|
+
// Note: This requires updating the protocol property, but it's readonly
|
|
94
|
+
// The transport is already set in the constructor during createDevice
|
|
95
|
+
// For now, we'll rely on the constructor parameter
|
|
96
|
+
|
|
97
|
+
return nativeId;
|
|
98
|
+
}
|
|
99
|
+
|
|
58
100
|
// Handle NVR case
|
|
59
101
|
if (detection.type === 'nvr') {
|
|
60
102
|
const deviceInfo = detection.deviceInfo || {};
|
|
61
103
|
const name = deviceInfo?.name || 'Reolink NVR';
|
|
62
104
|
const serialNumber = deviceInfo?.serialNumber || deviceInfo?.itemNo || `nvr-${Date.now()}`;
|
|
63
|
-
nativeId = `${serialNumber}
|
|
105
|
+
nativeId = `${serialNumber}${nvrSuffix}`;
|
|
64
106
|
|
|
65
107
|
settings.newCamera ||= name;
|
|
66
108
|
|
|
@@ -95,9 +137,9 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
95
137
|
|
|
96
138
|
// Create nativeId based on device type
|
|
97
139
|
if (detection.type === 'battery-cam') {
|
|
98
|
-
nativeId = `${serialNumber}
|
|
140
|
+
nativeId = `${serialNumber}${batteryCameraSuffix}`;
|
|
99
141
|
} else {
|
|
100
|
-
nativeId = `${serialNumber}
|
|
142
|
+
nativeId = `${serialNumber}${cameraSuffix}`;
|
|
101
143
|
}
|
|
102
144
|
|
|
103
145
|
settings.newCamera ||= name;
|
|
@@ -135,13 +177,8 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
135
177
|
providerNativeId: this.nativeId,
|
|
136
178
|
});
|
|
137
179
|
|
|
138
|
-
const device = await this.getDevice(nativeId);
|
|
139
|
-
if (device instanceof ReolinkNativeNvrDevice) {
|
|
140
|
-
// NVR devices are handled separately above
|
|
141
|
-
throw new Error('NVR device should not reach this code path');
|
|
142
|
-
}
|
|
180
|
+
const device = await this.getDevice(nativeId) as CommonCameraMixin;
|
|
143
181
|
|
|
144
|
-
// Type guard: device is either ReolinkNativeCamera or ReolinkNativeBatteryCamera
|
|
145
182
|
device.info = deviceInfo;
|
|
146
183
|
device.classes = objects;
|
|
147
184
|
device.presets = presets;
|
|
@@ -198,10 +235,16 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
198
235
|
}
|
|
199
236
|
|
|
200
237
|
createCamera(nativeId: string) {
|
|
201
|
-
if (nativeId.endsWith(
|
|
238
|
+
if (nativeId.endsWith(batteryCameraSuffix)) {
|
|
202
239
|
return new ReolinkNativeBatteryCamera(nativeId, this);
|
|
203
|
-
} else if (nativeId.endsWith(
|
|
240
|
+
} else if (nativeId.endsWith(nvrSuffix)) {
|
|
204
241
|
return new ReolinkNativeNvrDevice(nativeId, this);
|
|
242
|
+
} else if (nativeId.endsWith(multifocalSuffix)) {
|
|
243
|
+
// Get transport from device settings if available, otherwise default to TCP
|
|
244
|
+
// The transport is determined during autoDetect and should be stored
|
|
245
|
+
// For now, we'll try to infer from UID presence (if UID is set, likely UDP)
|
|
246
|
+
// Default to TCP for now - the transport will be set correctly during createDevice
|
|
247
|
+
return new ReolinkNativeMultiFocalDevice(nativeId, this, 'tcp');
|
|
205
248
|
} else {
|
|
206
249
|
return new ReolinkNativeCamera(nativeId, this);
|
|
207
250
|
}
|