@apocaliss92/scrypted-reolink-native 0.1.15 → 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 +55 -28
- package/src/connect.ts +38 -50
- package/src/main.ts +59 -16
- package/src/multifocal.ts +398 -0
- package/src/nvr.ts +49 -20
- 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,
|
|
@@ -263,6 +260,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
263
260
|
defaultValue: [],
|
|
264
261
|
choices: getDebugLogChoices(),
|
|
265
262
|
onPut: async (ov, value) => {
|
|
263
|
+
const logger = this.getBaichuanLogger();
|
|
266
264
|
const oldApiOptions = getApiRelevantDebugLogs(ov || []);
|
|
267
265
|
const newApiOptions = getApiRelevantDebugLogs(value || []);
|
|
268
266
|
|
|
@@ -288,7 +286,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
288
286
|
// Trigger reconnection
|
|
289
287
|
await this.ensureClient();
|
|
290
288
|
} catch (e) {
|
|
291
|
-
|
|
289
|
+
logger.warn('Failed to reset client after debug logs change', e);
|
|
292
290
|
}
|
|
293
291
|
}, 2000);
|
|
294
292
|
}
|
|
@@ -524,14 +522,16 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
524
522
|
resetBaichuanClient?(reason?: any): Promise<void>;
|
|
525
523
|
|
|
526
524
|
protected nvrDevice?: ReolinkNativeNvrDevice;
|
|
525
|
+
protected multiFocalDevice?: ReolinkNativeMultiFocalDevice;
|
|
527
526
|
thisDevice: Settings
|
|
528
527
|
|
|
529
528
|
constructor(nativeId: string, public plugin: ReolinkNativePlugin, public options: CommonCameraMixinOptions) {
|
|
530
529
|
super(nativeId);
|
|
531
|
-
this.protocol = !options.nvrDevice && options.type === 'battery' ? 'udp' : 'tcp';
|
|
530
|
+
this.protocol = !options.nvrDevice && !options.multiFocalDevice && options.type === 'battery' ? 'udp' : 'tcp';
|
|
532
531
|
|
|
533
532
|
// Store NVR device reference if provided
|
|
534
533
|
this.nvrDevice = options.nvrDevice;
|
|
534
|
+
this.multiFocalDevice = options.multiFocalDevice;
|
|
535
535
|
this.thisDevice = sdk.systemManager.getDeviceById<Settings>(this.id);
|
|
536
536
|
|
|
537
537
|
setTimeout(async () => {
|
|
@@ -691,6 +691,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
691
691
|
}
|
|
692
692
|
|
|
693
693
|
onSimpleEvent = (ev: ReolinkSimpleEvent) => {
|
|
694
|
+
const logger = this.getBaichuanLogger();
|
|
695
|
+
|
|
694
696
|
try {
|
|
695
697
|
const logger = this.getBaichuanLogger();
|
|
696
698
|
|
|
@@ -737,12 +739,12 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
737
739
|
});
|
|
738
740
|
}
|
|
739
741
|
catch (e) {
|
|
740
|
-
|
|
742
|
+
logger.warn('Error in onSimpleEvent handler', e);
|
|
741
743
|
}
|
|
742
744
|
}
|
|
743
745
|
|
|
744
746
|
async subscribeToEvents(): Promise<void> {
|
|
745
|
-
if (this.nvrDevice) {
|
|
747
|
+
if (this.nvrDevice || this.multiFocalDevice) {
|
|
746
748
|
return;
|
|
747
749
|
}
|
|
748
750
|
|
|
@@ -832,6 +834,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
832
834
|
|
|
833
835
|
// PanTiltZoom interface implementation
|
|
834
836
|
async ptzCommand(command: PanTiltZoomCommand): Promise<void> {
|
|
837
|
+
const logger = this.getBaichuanLogger();
|
|
838
|
+
|
|
835
839
|
const client = await this.ensureClient();
|
|
836
840
|
if (!client) {
|
|
837
841
|
return;
|
|
@@ -844,13 +848,13 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
844
848
|
if (preset !== undefined && preset !== null) {
|
|
845
849
|
const presetId = Number(preset);
|
|
846
850
|
if (!Number.isFinite(presetId)) {
|
|
847
|
-
|
|
851
|
+
logger.warn(`Invalid PTZ preset id: ${preset}`);
|
|
848
852
|
return;
|
|
849
853
|
}
|
|
850
854
|
if (this.ptzPresets) {
|
|
851
855
|
await this.ptzPresets.moveToPreset(presetId);
|
|
852
856
|
} else {
|
|
853
|
-
|
|
857
|
+
logger.warn('PTZ presets not available');
|
|
854
858
|
}
|
|
855
859
|
return;
|
|
856
860
|
}
|
|
@@ -885,14 +889,14 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
885
889
|
|
|
886
890
|
const step = Number(this.storageSettings.values.ptzZoomStep);
|
|
887
891
|
if (!Number.isFinite(step) || step <= 0) {
|
|
888
|
-
|
|
892
|
+
logger.warn('Invalid PTZ zoom step, using default 0.1');
|
|
889
893
|
return;
|
|
890
894
|
}
|
|
891
895
|
|
|
892
896
|
// Get current zoom factor and apply step
|
|
893
897
|
const info = await client.getZoomFocus(channel);
|
|
894
898
|
if (!info?.zoom) {
|
|
895
|
-
|
|
899
|
+
logger.warn('Zoom command requested but camera did not report zoom support.');
|
|
896
900
|
return;
|
|
897
901
|
}
|
|
898
902
|
|
|
@@ -1100,18 +1104,22 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1100
1104
|
}
|
|
1101
1105
|
|
|
1102
1106
|
async updateDeviceInfo(): Promise<void> {
|
|
1107
|
+
const logger = this.getBaichuanLogger();
|
|
1108
|
+
|
|
1103
1109
|
const { ipAddress, rtspChannel } = this.storageSettings.values;
|
|
1104
1110
|
try {
|
|
1105
1111
|
const api = await this.ensureClient();
|
|
1106
|
-
const deviceData = await api.getInfo(this.nvrDevice ? rtspChannel : undefined);
|
|
1112
|
+
const deviceData = await api.getInfo((this.nvrDevice || this.multiFocalDevice) ? rtspChannel : undefined);
|
|
1107
1113
|
|
|
1108
1114
|
await updateDeviceInfo({
|
|
1109
1115
|
device: this,
|
|
1110
1116
|
ipAddress,
|
|
1111
1117
|
deviceData,
|
|
1112
1118
|
});
|
|
1119
|
+
|
|
1120
|
+
logger.log(`Device info updated: ${JSON.stringify(deviceData)}`);
|
|
1113
1121
|
} catch (e) {
|
|
1114
|
-
|
|
1122
|
+
logger.warn('Failed to fetch device info', e);
|
|
1115
1123
|
}
|
|
1116
1124
|
}
|
|
1117
1125
|
|
|
@@ -1187,6 +1195,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1187
1195
|
* This should be called periodically for regular cameras and once when battery cameras wake up.
|
|
1188
1196
|
*/
|
|
1189
1197
|
async alignAuxDevicesState(): Promise<void> {
|
|
1198
|
+
const logger = this.getBaichuanLogger();
|
|
1199
|
+
|
|
1190
1200
|
const api = this.baichuanApi;
|
|
1191
1201
|
if (!api) return;
|
|
1192
1202
|
|
|
@@ -1200,7 +1210,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1200
1210
|
const sirenState = await api.getSiren(channel);
|
|
1201
1211
|
this.siren.on = sirenState.enabled;
|
|
1202
1212
|
} catch (e) {
|
|
1203
|
-
|
|
1213
|
+
logger.debug('Failed to align siren state', e);
|
|
1204
1214
|
}
|
|
1205
1215
|
}
|
|
1206
1216
|
|
|
@@ -1213,7 +1223,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1213
1223
|
this.floodlight.brightness = wl.brightness;
|
|
1214
1224
|
}
|
|
1215
1225
|
} catch (e) {
|
|
1216
|
-
|
|
1226
|
+
logger.debug('Failed to align floodlight state', e);
|
|
1217
1227
|
}
|
|
1218
1228
|
}
|
|
1219
1229
|
|
|
@@ -1237,16 +1247,18 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1237
1247
|
}
|
|
1238
1248
|
}
|
|
1239
1249
|
} catch (e) {
|
|
1240
|
-
|
|
1250
|
+
logger.debug('Failed to align PIR state', e);
|
|
1241
1251
|
}
|
|
1242
1252
|
}
|
|
1243
1253
|
} catch (e) {
|
|
1244
|
-
|
|
1254
|
+
logger.debug('Failed to align auxiliary devices state', e);
|
|
1245
1255
|
}
|
|
1246
1256
|
}
|
|
1247
1257
|
|
|
1248
1258
|
// Video stream helper methods
|
|
1249
1259
|
protected addRtspCredentials(rtspUrl: string): string {
|
|
1260
|
+
const logger = this.getBaichuanLogger();
|
|
1261
|
+
|
|
1250
1262
|
const { username, password } = this.storageSettings.values;
|
|
1251
1263
|
if (!username) {
|
|
1252
1264
|
return rtspUrl;
|
|
@@ -1271,7 +1283,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1271
1283
|
return url.toString();
|
|
1272
1284
|
} catch (e) {
|
|
1273
1285
|
// If URL parsing fails, return original URL
|
|
1274
|
-
|
|
1286
|
+
logger.warn('Failed to parse URL for credentials', e);
|
|
1275
1287
|
return rtspUrl;
|
|
1276
1288
|
}
|
|
1277
1289
|
}
|
|
@@ -1289,6 +1301,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1289
1301
|
}
|
|
1290
1302
|
|
|
1291
1303
|
protected async ensureNetPortCache(): Promise<void> {
|
|
1304
|
+
const logger = this.getBaichuanLogger();
|
|
1305
|
+
|
|
1292
1306
|
if (this.cachedNetPort) {
|
|
1293
1307
|
return;
|
|
1294
1308
|
}
|
|
@@ -1312,7 +1326,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1312
1326
|
} catch (e) {
|
|
1313
1327
|
// Only log if it's not a recoverable error to avoid spam
|
|
1314
1328
|
if (!this.isRecoverableBaichuanError?.(e)) {
|
|
1315
|
-
|
|
1329
|
+
logger.warn('Failed to get net port, using defaults', e);
|
|
1316
1330
|
}
|
|
1317
1331
|
// Use defaults if we can't get the ports
|
|
1318
1332
|
this.cachedNetPort = {
|
|
@@ -1368,7 +1382,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1368
1382
|
}
|
|
1369
1383
|
|
|
1370
1384
|
if (streams.length) {
|
|
1371
|
-
|
|
1385
|
+
logger.log('Fetched video stream options', { streams, netPort: this.cachedNetPort });
|
|
1372
1386
|
this.cachedVideoStreamOptions = streams;
|
|
1373
1387
|
return streams;
|
|
1374
1388
|
}
|
|
@@ -1443,6 +1457,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1443
1457
|
if (this.nvrDevice) {
|
|
1444
1458
|
return await this.nvrDevice.ensureBaichuanClient();
|
|
1445
1459
|
}
|
|
1460
|
+
if (this.multiFocalDevice) {
|
|
1461
|
+
return await this.multiFocalDevice.ensureBaichuanClient();
|
|
1462
|
+
}
|
|
1446
1463
|
|
|
1447
1464
|
// Use base class implementation
|
|
1448
1465
|
return await this.ensureBaichuanClient();
|
|
@@ -1497,14 +1514,14 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1497
1514
|
info: this.info,
|
|
1498
1515
|
};
|
|
1499
1516
|
|
|
1500
|
-
|
|
1517
|
+
logger.log(`Updating device interfaces: ${JSON.stringify(device)}`);
|
|
1501
1518
|
|
|
1502
1519
|
await sdk.deviceManager.onDeviceDiscovered(device);
|
|
1503
1520
|
} catch (e) {
|
|
1504
|
-
|
|
1521
|
+
logger.error('Failed to update device interfaces', e);
|
|
1505
1522
|
}
|
|
1506
1523
|
|
|
1507
|
-
|
|
1524
|
+
logger.log(`Refreshed device capabilities: ${JSON.stringify({ capabilities, abilities, support, presets })}`);
|
|
1508
1525
|
}
|
|
1509
1526
|
catch (e) {
|
|
1510
1527
|
logger.error('Failed to refresh abilities', e);
|
|
@@ -1566,7 +1583,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1566
1583
|
|
|
1567
1584
|
this.streamManager = new StreamManager({
|
|
1568
1585
|
createStreamClient: () => this.createStreamClient(),
|
|
1569
|
-
getLogger: () =>
|
|
1586
|
+
getLogger: () => logger as Console,
|
|
1570
1587
|
credentials: {
|
|
1571
1588
|
username,
|
|
1572
1589
|
password
|
|
@@ -1604,9 +1621,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1604
1621
|
logger.warn('Failed to subscribe to Baichuan events', e);
|
|
1605
1622
|
}
|
|
1606
1623
|
|
|
1607
|
-
const { isFromNvr } = this.storageSettings.values;
|
|
1608
1624
|
|
|
1609
|
-
if (
|
|
1625
|
+
if (this.nvrDevice) {
|
|
1610
1626
|
this.storageSettings.settings.username.hide = true;
|
|
1611
1627
|
this.storageSettings.settings.password.hide = true;
|
|
1612
1628
|
this.storageSettings.settings.ipAddress.hide = true;
|
|
@@ -1617,6 +1633,17 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1617
1633
|
this.storageSettings.settings.ipAddress.defaultValue = this.nvrDevice.storageSettings.values.ipAddress;
|
|
1618
1634
|
}
|
|
1619
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
|
+
|
|
1620
1647
|
await this.init();
|
|
1621
1648
|
this.initComplete = true;
|
|
1622
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
|
}
|