@apocaliss92/scrypted-reolink-native 0.1.23 → 0.1.25
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-battery.ts +10 -3
- package/src/camera.ts +9 -5
- package/src/common.ts +48 -38
- package/src/connect.ts +2 -2
- package/src/main.ts +4 -3
- package/src/multiFocal.ts +371 -0
- package/src/nvr.ts +0 -4
- package/src/multifocal.ts +0 -463
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/camera-battery.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
} from "./common";
|
|
10
10
|
import { DebugLogOption } from "./debug-options";
|
|
11
11
|
import type ReolinkNativePlugin from "./main";
|
|
12
|
+
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
13
|
+
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
12
14
|
|
|
13
15
|
export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
14
16
|
private lastPicture: { mo: MediaObject; atMs: number } | undefined;
|
|
@@ -28,10 +30,16 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
28
30
|
return debugLogs.includes(DebugLogOption.BatteryInfo);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
constructor(
|
|
33
|
+
constructor(
|
|
34
|
+
nativeId: string,
|
|
35
|
+
public plugin: ReolinkNativePlugin,
|
|
36
|
+
nvrDevice?: ReolinkNativeNvrDevice,
|
|
37
|
+
multiFocalDevice?: ReolinkNativeMultiFocalDevice
|
|
38
|
+
) {
|
|
32
39
|
super(nativeId, plugin, {
|
|
33
40
|
type: 'battery',
|
|
34
41
|
nvrDevice,
|
|
42
|
+
multiFocalDevice,
|
|
35
43
|
});
|
|
36
44
|
}
|
|
37
45
|
|
|
@@ -112,8 +120,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
|
112
120
|
|
|
113
121
|
logger.log('Starting periodic tasks for battery camera');
|
|
114
122
|
|
|
115
|
-
|
|
116
|
-
if (!this.nvrDevice) {
|
|
123
|
+
if (!this.nvrDevice && !this.multiFocalDevice) {
|
|
117
124
|
this.sleepCheckTimer = setInterval(async () => {
|
|
118
125
|
try {
|
|
119
126
|
const api = this.baichuanApi;
|
package/src/camera.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
} from "./common";
|
|
7
7
|
import { createBaichuanApi } from './connect';
|
|
8
8
|
import ReolinkNativePlugin from "./main";
|
|
9
|
+
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
10
|
+
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
9
11
|
|
|
10
12
|
export const moToB64 = async (mo: MediaObject) => {
|
|
11
13
|
const bufferImage = await sdk.mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
|
|
@@ -28,10 +30,16 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
|
|
|
28
30
|
private statusPollTimer: NodeJS.Timeout | undefined;
|
|
29
31
|
|
|
30
32
|
|
|
31
|
-
constructor(
|
|
33
|
+
constructor(
|
|
34
|
+
nativeId: string,
|
|
35
|
+
public plugin: ReolinkNativePlugin,
|
|
36
|
+
nvrDevice?: ReolinkNativeNvrDevice,
|
|
37
|
+
multiFocalDevice?: ReolinkNativeMultiFocalDevice
|
|
38
|
+
) {
|
|
32
39
|
super(nativeId, plugin, {
|
|
33
40
|
type: 'regular',
|
|
34
41
|
nvrDevice,
|
|
42
|
+
multiFocalDevice,
|
|
35
43
|
});
|
|
36
44
|
}
|
|
37
45
|
|
|
@@ -109,10 +117,6 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
|
|
|
109
117
|
return api;
|
|
110
118
|
}
|
|
111
119
|
|
|
112
|
-
getClient(): ReolinkBaichuanApi | undefined {
|
|
113
|
-
return this.baichuanApi;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
120
|
private passiveRefreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
117
121
|
|
|
118
122
|
async release() {
|
package/src/common.ts
CHANGED
|
@@ -8,7 +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 "./
|
|
11
|
+
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
12
12
|
import { ReolinkPtzPresets } from "./presets";
|
|
13
13
|
import {
|
|
14
14
|
createRfc4571MediaObjectFromStreamManager,
|
|
@@ -526,7 +526,11 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
526
526
|
protected multiFocalDevice?: ReolinkNativeMultiFocalDevice;
|
|
527
527
|
thisDevice: Settings
|
|
528
528
|
|
|
529
|
-
constructor(
|
|
529
|
+
constructor(
|
|
530
|
+
nativeId: string,
|
|
531
|
+
public plugin: ReolinkNativePlugin,
|
|
532
|
+
public options: CommonCameraMixinOptions
|
|
533
|
+
) {
|
|
530
534
|
super(nativeId);
|
|
531
535
|
this.protocol = !options.nvrDevice && !options.multiFocalDevice && options.type === 'battery' ? 'udp' : 'tcp';
|
|
532
536
|
|
|
@@ -644,12 +648,17 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
644
648
|
}
|
|
645
649
|
}
|
|
646
650
|
}
|
|
651
|
+
|
|
647
652
|
createStreamClient(): Promise<ReolinkBaichuanApi> {
|
|
648
653
|
throw new Error("Method not implemented.");
|
|
649
654
|
}
|
|
650
655
|
|
|
651
656
|
public getAbilities(): DeviceCapabilities {
|
|
652
|
-
|
|
657
|
+
if (this.options.multiFocalDevice) {
|
|
658
|
+
return this.options.multiFocalDevice.getInterfaces(this.storageSettings.values.rtspChannel).capabilities;
|
|
659
|
+
} else {
|
|
660
|
+
return this.storageSettings.values.capabilities;
|
|
661
|
+
}
|
|
653
662
|
}
|
|
654
663
|
|
|
655
664
|
getBaichuanDebugOptions(): any | undefined {
|
|
@@ -1107,6 +1116,11 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1107
1116
|
async updateDeviceInfo(): Promise<void> {
|
|
1108
1117
|
const logger = this.getBaichuanLogger();
|
|
1109
1118
|
|
|
1119
|
+
if (this.options.multiFocalDevice) {
|
|
1120
|
+
this.info = this.options.multiFocalDevice.info;
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1110
1124
|
const { ipAddress, rtspChannel } = this.storageSettings.values;
|
|
1111
1125
|
try {
|
|
1112
1126
|
const api = await this.ensureClient();
|
|
@@ -1357,21 +1371,16 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1357
1371
|
const vsos = await this.getVideoStreamOptions();
|
|
1358
1372
|
const selected = selectStreamOption(vsos, vso);
|
|
1359
1373
|
|
|
1360
|
-
// Check if this is a native stream (prefixed with "native_")
|
|
1361
|
-
|
|
1362
|
-
// If stream has RTSP/RTMP URL (not native), add credentials and create MediaStreamUrl
|
|
1363
1374
|
if (selected.url && (selected.container === 'rtsp' || selected.container === 'rtmp')) {
|
|
1364
1375
|
const urlWithCredentials = this.addRtspCredentials(selected.url);
|
|
1365
1376
|
const ret: MediaStreamUrl = {
|
|
1366
1377
|
container: selected.container,
|
|
1367
|
-
// url: selected.url,
|
|
1368
1378
|
url: urlWithCredentials,
|
|
1369
1379
|
mediaStreamOptions: selected,
|
|
1370
1380
|
};
|
|
1371
1381
|
return await this.createMediaObject(ret, ScryptedMimeTypes.MediaStreamUrl);
|
|
1372
1382
|
}
|
|
1373
1383
|
|
|
1374
|
-
// Use streamManager for native Baichuan streams (native_* or streams without URL)
|
|
1375
1384
|
if (!this.streamManager) {
|
|
1376
1385
|
throw new Error('StreamManager not initialized');
|
|
1377
1386
|
}
|
|
@@ -1406,9 +1415,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1406
1415
|
return await this.withBaichuanRetry(createStreamFn);
|
|
1407
1416
|
}
|
|
1408
1417
|
|
|
1409
|
-
// Client management
|
|
1410
1418
|
async ensureClient(): Promise<ReolinkBaichuanApi> {
|
|
1411
|
-
// If camera is connected to NVR, use NVR's shared Baichuan connection
|
|
1412
1419
|
if (this.nvrDevice) {
|
|
1413
1420
|
return await this.nvrDevice.ensureBaichuanClient();
|
|
1414
1421
|
}
|
|
@@ -1444,38 +1451,41 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1444
1451
|
const channel = this.storageSettings.values.rtspChannel;
|
|
1445
1452
|
|
|
1446
1453
|
try {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
this.storageSettings.values.capabilities = capabilities;
|
|
1454
|
-
this.ptzPresets.setCachedPtzPresets(presets);
|
|
1455
|
-
|
|
1456
|
-
try {
|
|
1457
|
-
const { interfaces, type } = getDeviceInterfaces({
|
|
1458
|
-
capabilities,
|
|
1459
|
-
logger: this.console,
|
|
1454
|
+
if (this.options.multiFocalDevice) {
|
|
1455
|
+
// do nothing for now
|
|
1456
|
+
} else {
|
|
1457
|
+
const { capabilities, abilities, support, presets, objects } = await this.withBaichuanRetry(async () => {
|
|
1458
|
+
const api = await this.ensureClient();
|
|
1459
|
+
return await api.getDeviceCapabilities(channel);
|
|
1460
1460
|
});
|
|
1461
|
+
this.classes = objects;
|
|
1462
|
+
this.presets = presets;
|
|
1463
|
+
this.ptzPresets.setCachedPtzPresets(presets);
|
|
1461
1464
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1465
|
+
try {
|
|
1466
|
+
const { interfaces, type } = getDeviceInterfaces({
|
|
1467
|
+
capabilities,
|
|
1468
|
+
logger: this.console,
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
const device: Device = {
|
|
1472
|
+
nativeId: this.nativeId,
|
|
1473
|
+
providerNativeId: this.nvrDevice?.nativeId ?? this.plugin?.nativeId,
|
|
1474
|
+
name: this.name,
|
|
1475
|
+
interfaces,
|
|
1476
|
+
type,
|
|
1477
|
+
info: this.info,
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
logger.log(`Updating device interfaces: ${JSON.stringify(device)}`);
|
|
1481
|
+
|
|
1482
|
+
await sdk.deviceManager.onDeviceDiscovered(device);
|
|
1483
|
+
} catch (e) {
|
|
1484
|
+
logger.error('Failed to update device interfaces', e);
|
|
1485
|
+
}
|
|
1472
1486
|
|
|
1473
|
-
|
|
1474
|
-
} catch (e) {
|
|
1475
|
-
logger.error('Failed to update device interfaces', e);
|
|
1487
|
+
logger.log(`Refreshed device capabilities: ${JSON.stringify({ capabilities, abilities, support, presets })}`);
|
|
1476
1488
|
}
|
|
1477
|
-
|
|
1478
|
-
logger.log(`Refreshed device capabilities: ${JSON.stringify({ capabilities, abilities, support, presets })}`);
|
|
1479
1489
|
}
|
|
1480
1490
|
catch (e) {
|
|
1481
1491
|
logger.error('Failed to refresh abilities', e);
|
package/src/connect.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { BaichuanClientOptions, ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
1
|
+
import type { BaichuanTransport as BaichuanTransportParent, BaichuanClientOptions, ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
2
|
|
|
3
|
-
export type BaichuanTransport =
|
|
3
|
+
export type BaichuanTransport = BaichuanTransportParent;
|
|
4
4
|
|
|
5
5
|
export type BaichuanConnectInputs = {
|
|
6
6
|
host: string;
|
package/src/main.ts
CHANGED
|
@@ -4,9 +4,10 @@ import { ReolinkNativeCamera } from "./camera";
|
|
|
4
4
|
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
5
5
|
import { CommonCameraMixin } from "./common";
|
|
6
6
|
import { createBaichuanApi } from "./connect";
|
|
7
|
-
import { ReolinkNativeMultiFocalDevice } from "./
|
|
7
|
+
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
8
8
|
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
9
9
|
import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces, multifocalSuffix, nvrSuffix } from "./utils";
|
|
10
|
+
import { BaichuanTransport } from "./connect";
|
|
10
11
|
|
|
11
12
|
class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
|
12
13
|
devices = new Map<string, BaseBaichuanClass>();
|
|
@@ -73,7 +74,6 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
73
74
|
name,
|
|
74
75
|
interfaces: [
|
|
75
76
|
ScryptedInterface.Settings,
|
|
76
|
-
ScryptedInterface.DeviceDiscovery,
|
|
77
77
|
ScryptedInterface.DeviceProvider,
|
|
78
78
|
ScryptedInterface.Reboot,
|
|
79
79
|
],
|
|
@@ -89,6 +89,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
89
89
|
device.storageSettings.values.username = username;
|
|
90
90
|
device.storageSettings.values.password = password;
|
|
91
91
|
device.storageSettings.values.uid = detection.uid || '';
|
|
92
|
+
device.storageSettings.values.protocol = detection.transport || 'tcp' as BaichuanTransport;
|
|
92
93
|
|
|
93
94
|
return nativeId;
|
|
94
95
|
}
|
|
@@ -237,7 +238,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
237
238
|
} else if (nativeId.endsWith(nvrSuffix)) {
|
|
238
239
|
return new ReolinkNativeNvrDevice(nativeId, this);
|
|
239
240
|
} else if (nativeId.endsWith(multifocalSuffix)) {
|
|
240
|
-
return new ReolinkNativeMultiFocalDevice(nativeId, this
|
|
241
|
+
return new ReolinkNativeMultiFocalDevice(nativeId, this);
|
|
241
242
|
} else {
|
|
242
243
|
return new ReolinkNativeCamera(nativeId, this);
|
|
243
244
|
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import type { DeviceCapabilities, DualLensChannelAnalysis, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
|
+
import sdk, { Device, DeviceProvider, Reboot, ScryptedDeviceType, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
|
3
|
+
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
|
4
|
+
import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
|
|
5
|
+
import { ReolinkNativeCamera } from "./camera";
|
|
6
|
+
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
7
|
+
import { normalizeUid } from "./connect";
|
|
8
|
+
import ReolinkNativePlugin from "./main";
|
|
9
|
+
import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces, updateDeviceInfo } from "./utils";
|
|
10
|
+
|
|
11
|
+
export class ReolinkNativeMultiFocalDevice extends BaseBaichuanClass implements Settings, DeviceProvider, Reboot {
|
|
12
|
+
storageSettings = new StorageSettings(this, {
|
|
13
|
+
debugEvents: {
|
|
14
|
+
title: 'Debug Events',
|
|
15
|
+
type: 'boolean',
|
|
16
|
+
immediate: true,
|
|
17
|
+
},
|
|
18
|
+
ipAddress: {
|
|
19
|
+
title: 'IP address',
|
|
20
|
+
type: 'string',
|
|
21
|
+
onPut: async () => await this.reinit()
|
|
22
|
+
},
|
|
23
|
+
username: {
|
|
24
|
+
title: 'Username',
|
|
25
|
+
placeholder: 'admin',
|
|
26
|
+
defaultValue: 'admin',
|
|
27
|
+
type: 'string',
|
|
28
|
+
onPut: async () => await this.reinit()
|
|
29
|
+
},
|
|
30
|
+
password: {
|
|
31
|
+
title: 'Password',
|
|
32
|
+
type: 'password',
|
|
33
|
+
onPut: async () => await this.reinit()
|
|
34
|
+
},
|
|
35
|
+
uid: {
|
|
36
|
+
title: 'UID',
|
|
37
|
+
description: 'Reolink UID (required for UDP/battery multi-focal devices)',
|
|
38
|
+
type: 'string',
|
|
39
|
+
hide: true,
|
|
40
|
+
onPut: async () => await this.reinit()
|
|
41
|
+
},
|
|
42
|
+
protocol: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
hide: true,
|
|
45
|
+
},
|
|
46
|
+
diagnosticsRun: {
|
|
47
|
+
subgroup: 'Diagnostics',
|
|
48
|
+
title: 'Run Diagnostics',
|
|
49
|
+
description: 'Collect diagnostics and display results in logs.',
|
|
50
|
+
type: 'button',
|
|
51
|
+
immediate: true,
|
|
52
|
+
onPut: async () => {
|
|
53
|
+
await this.runDiagnostics();
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
multifocalInfo: {
|
|
57
|
+
json: true,
|
|
58
|
+
hide: true,
|
|
59
|
+
},
|
|
60
|
+
capabilities: {
|
|
61
|
+
json: true,
|
|
62
|
+
hide: true,
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
plugin: ReolinkNativePlugin;
|
|
67
|
+
cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
|
|
68
|
+
private channelToNativeIdMap = new Map<number, string>();
|
|
69
|
+
private initReinitTimeout: NodeJS.Timeout | undefined;
|
|
70
|
+
isBattery: boolean;
|
|
71
|
+
|
|
72
|
+
constructor(nativeId: string, plugin: ReolinkNativePlugin) {
|
|
73
|
+
super(nativeId);
|
|
74
|
+
this.plugin = plugin;
|
|
75
|
+
|
|
76
|
+
this.isBattery = this.storageSettings.values.protocol === 'udp';
|
|
77
|
+
|
|
78
|
+
this.scheduleInit();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async reboot(): Promise<void> {
|
|
82
|
+
const api = await this.ensureBaichuanClient();
|
|
83
|
+
await api.reboot();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected getConnectionConfig(): BaichuanConnectionConfig {
|
|
87
|
+
const { ipAddress, username, password, uid } = this.storageSettings.values;
|
|
88
|
+
if (!ipAddress || !username || !password) {
|
|
89
|
+
throw new Error('Missing device credentials');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { protocol } = this.storageSettings.values;
|
|
93
|
+
|
|
94
|
+
const normalizedUid = this.isBattery ? normalizeUid(uid) : undefined;
|
|
95
|
+
|
|
96
|
+
if (protocol === 'udp' && !normalizedUid) {
|
|
97
|
+
throw new Error('UID is required for UDP multi-focal devices (BCUDP)');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
host: ipAddress,
|
|
102
|
+
username,
|
|
103
|
+
password,
|
|
104
|
+
uid: normalizedUid,
|
|
105
|
+
transport: protocol,
|
|
106
|
+
logger: this.console,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
|
|
111
|
+
return {
|
|
112
|
+
onError: undefined, // Use default error handling
|
|
113
|
+
onClose: async () => {
|
|
114
|
+
// Reinit after cleanup
|
|
115
|
+
await this.reinit();
|
|
116
|
+
if (!this.isBattery) {
|
|
117
|
+
setTimeout(async () => {
|
|
118
|
+
try {
|
|
119
|
+
await this.subscribeToEvents();
|
|
120
|
+
} catch (e) {
|
|
121
|
+
const logger = this.getBaichuanLogger();
|
|
122
|
+
logger.warn('Failed to resubscribe to events after reconnection', e);
|
|
123
|
+
}
|
|
124
|
+
}, 1000);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
onSimpleEvent: (ev) => this.forwardNativeEvent(ev),
|
|
128
|
+
getEventSubscriptionEnabled: () => true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protected async onBeforeCleanup(): Promise<void> {
|
|
133
|
+
await this.unsubscribeFromAllEvents();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
protected isDebugEnabled(): boolean {
|
|
137
|
+
return this.storageSettings.values.debugEvents;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
protected getDeviceName(): string {
|
|
141
|
+
return this.name || 'Multi-Focal Device';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async reinit(): Promise<void> {
|
|
145
|
+
// Cancel any pending init/reinit
|
|
146
|
+
if (this.initReinitTimeout) {
|
|
147
|
+
clearTimeout(this.initReinitTimeout);
|
|
148
|
+
this.initReinitTimeout = undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Schedule reinit with debounce
|
|
152
|
+
this.scheduleInit(true);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private scheduleInit(isReinit: boolean = false): void {
|
|
156
|
+
// Cancel any pending init/reinit
|
|
157
|
+
if (this.initReinitTimeout) {
|
|
158
|
+
clearTimeout(this.initReinitTimeout);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.initReinitTimeout = setTimeout(async () => {
|
|
162
|
+
const logger = this.getBaichuanLogger();
|
|
163
|
+
if (isReinit) {
|
|
164
|
+
logger.log('Reinitializing multi-focal device...');
|
|
165
|
+
await this.cleanupBaichuanApi();
|
|
166
|
+
}
|
|
167
|
+
await this.init();
|
|
168
|
+
this.initReinitTimeout = undefined;
|
|
169
|
+
}, isReinit ? 500 : 2000);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async init(): Promise<void> {
|
|
173
|
+
const logger = this.getBaichuanLogger();
|
|
174
|
+
try {
|
|
175
|
+
this.storageSettings.settings.uid.hide = !this.isBattery;
|
|
176
|
+
|
|
177
|
+
await this.ensureBaichuanClient();
|
|
178
|
+
await this.updateDeviceInfo();
|
|
179
|
+
await this.reportDevices();
|
|
180
|
+
await this.subscribeToEvents();
|
|
181
|
+
} catch (e) {
|
|
182
|
+
logger.error('Failed to initialize multi-focal device', e);
|
|
183
|
+
if (e instanceof Error) {
|
|
184
|
+
logger.error(`Error message: ${e.message}`);
|
|
185
|
+
logger.error(`Error stack: ${e.stack}`);
|
|
186
|
+
} else {
|
|
187
|
+
logger.error(`Error details: ${JSON.stringify(e)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async updateDeviceInfo(): Promise<void> {
|
|
193
|
+
const logger = this.getBaichuanLogger();
|
|
194
|
+
try {
|
|
195
|
+
const api = await this.ensureBaichuanClient();
|
|
196
|
+
const deviceData = await api.getInfo();
|
|
197
|
+
|
|
198
|
+
await updateDeviceInfo({
|
|
199
|
+
device: this,
|
|
200
|
+
deviceData,
|
|
201
|
+
ipAddress: this.storageSettings.values.ipAddress,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
logger.log(`Device info updated: ${JSON.stringify(deviceData)}`);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
logger.warn('Failed to fetch device info', e);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getInterfaces(channel: number) {
|
|
211
|
+
const logger = this.getBaichuanLogger();
|
|
212
|
+
const { capabilities: caps, multifocalInfo } = this.storageSettings.values;
|
|
213
|
+
const channelInfo = (multifocalInfo as DualLensChannelAnalysis).channels.find(c => c.channel === channel);
|
|
214
|
+
|
|
215
|
+
const capabilities: DeviceCapabilities = {
|
|
216
|
+
...caps,
|
|
217
|
+
hasPan: channelInfo.hasPan,
|
|
218
|
+
hasTilt: channelInfo.hasTilt,
|
|
219
|
+
hasZoom: channelInfo.hasZoom,
|
|
220
|
+
hasPresets: channelInfo.hasPresets,
|
|
221
|
+
hasIntercom: channelInfo.hasIntercom,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const { interfaces } = getDeviceInterfaces({
|
|
225
|
+
capabilities,
|
|
226
|
+
logger,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return { interfaces, capabilities };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async reportDevices(): Promise<void> {
|
|
233
|
+
const api = await this.ensureBaichuanClient();
|
|
234
|
+
const logger = this.getBaichuanLogger();
|
|
235
|
+
const { username, password, ipAddress, uid } = this.storageSettings.values;
|
|
236
|
+
|
|
237
|
+
const { capabilities, support, abilities, features, objects, presets } = await api.getDeviceCapabilities();
|
|
238
|
+
|
|
239
|
+
const multifocalInfo = await api.getDualLensChannelInfo();
|
|
240
|
+
logger.log(`Sync entities from remote for ${multifocalInfo.channels.length} channels`);
|
|
241
|
+
|
|
242
|
+
this.storageSettings.values.multifocalInfo = multifocalInfo;
|
|
243
|
+
this.storageSettings.values.capabilities = capabilities;
|
|
244
|
+
|
|
245
|
+
logger.debug(`Multichannel info: ${JSON.stringify({ multifocalInfo, capabilities, support, abilities, features, objects, presets })}`);
|
|
246
|
+
|
|
247
|
+
for (const channelInfo of multifocalInfo?.channels ?? []) {
|
|
248
|
+
const { channel, lensType } = channelInfo;
|
|
249
|
+
|
|
250
|
+
const name = `${this.name} - ${lensType}`;
|
|
251
|
+
const nativeId = this.buildNativeId(channel);
|
|
252
|
+
|
|
253
|
+
this.channelToNativeIdMap.set(channel, nativeId);
|
|
254
|
+
const { interfaces, capabilities: deviceCapabilities } = this.getInterfaces(channel);
|
|
255
|
+
|
|
256
|
+
const device: Device = {
|
|
257
|
+
providerNativeId: this.nativeId,
|
|
258
|
+
name,
|
|
259
|
+
nativeId,
|
|
260
|
+
info: {
|
|
261
|
+
...this.info,
|
|
262
|
+
metadata: {
|
|
263
|
+
channel,
|
|
264
|
+
lensType
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
interfaces,
|
|
268
|
+
type: ScryptedDeviceType.Camera,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
await sdk.deviceManager.onDeviceDiscovered(device);
|
|
272
|
+
|
|
273
|
+
const camera = await this.getDevice(nativeId);
|
|
274
|
+
|
|
275
|
+
camera.storageSettings.values.rtspChannel = channel;
|
|
276
|
+
camera.classes = objects;
|
|
277
|
+
camera.presets = presets;
|
|
278
|
+
camera.storageSettings.values.username = username;
|
|
279
|
+
camera.storageSettings.values.password = password;
|
|
280
|
+
camera.storageSettings.values.ipAddress = ipAddress;
|
|
281
|
+
camera.storageSettings.values.capabilities = deviceCapabilities;
|
|
282
|
+
if (this.isBattery) {
|
|
283
|
+
camera.storageSettings.values.uid = uid;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async getDevice(nativeId: string) {
|
|
289
|
+
let device = this.cameraNativeMap.get(nativeId);
|
|
290
|
+
if (!device) {
|
|
291
|
+
if (nativeId.endsWith(batteryCameraSuffix)) {
|
|
292
|
+
device = new ReolinkNativeBatteryCamera(nativeId, this.plugin, undefined, this);
|
|
293
|
+
} else {
|
|
294
|
+
device = new ReolinkNativeCamera(nativeId, this.plugin, undefined, this);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return device;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async getSettings(): Promise<Setting[]> {
|
|
302
|
+
const settings = await this.storageSettings.getSettings();
|
|
303
|
+
return settings;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async putSetting(key: string, value: SettingValue): Promise<void> {
|
|
307
|
+
return this.storageSettings.putSetting(key, value);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async releaseDevice(id: string, nativeId: string) {
|
|
311
|
+
this.cameraNativeMap.delete(nativeId);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
buildNativeId(channel: number): string {
|
|
315
|
+
const { protocol } = this.storageSettings.values;
|
|
316
|
+
return `${this.nativeId}-channel${channel}${protocol === "udp" ? batteryCameraSuffix : cameraSuffix}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
forwardNativeEvent(ev: ReolinkSimpleEvent): void {
|
|
320
|
+
const logger = this.getBaichuanLogger();
|
|
321
|
+
const channel = ev?.channel;
|
|
322
|
+
|
|
323
|
+
if (channel === undefined) {
|
|
324
|
+
logger.debug('Event missing channel, ignoring');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const nativeId = this.channelToNativeIdMap.get(channel);
|
|
329
|
+
if (!nativeId) {
|
|
330
|
+
logger.debug(`No camera found for channel ${channel}, ignoring event`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const camera = this.cameraNativeMap.get(nativeId);
|
|
335
|
+
if (!camera) {
|
|
336
|
+
logger.debug(`Camera ${nativeId} not yet initialized, ignoring event`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Forward event to camera
|
|
341
|
+
if (camera.onSimpleEvent) {
|
|
342
|
+
camera.onSimpleEvent(ev);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async unsubscribeFromAllEvents(): Promise<void> {
|
|
346
|
+
await super.unsubscribeFromEvents();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private async runDiagnostics(): Promise<void> {
|
|
350
|
+
const logger = this.getBaichuanLogger();
|
|
351
|
+
logger.log(`Starting Multifocal diagnostics...`);
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const { ipAddress, username, password } = this.storageSettings.values;
|
|
355
|
+
if (!ipAddress || !username || !password) {
|
|
356
|
+
throw new Error('Missing device credentials');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const api = await this.ensureBaichuanClient();
|
|
360
|
+
|
|
361
|
+
const multifocalDiagnostics = await api.collectMultifocalDiagnostics(logger);
|
|
362
|
+
|
|
363
|
+
logger.log(`NVR diagnostics completed successfully.`);
|
|
364
|
+
logger.log(JSON.stringify(multifocalDiagnostics));
|
|
365
|
+
} catch (e) {
|
|
366
|
+
logger.error('Failed to run NVR diagnostics', e);
|
|
367
|
+
throw e;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
package/src/nvr.ts
CHANGED
|
@@ -124,12 +124,10 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
protected async onBeforeCleanup(): Promise<void> {
|
|
127
|
-
// Unsubscribe from events if needed
|
|
128
127
|
await this.unsubscribeFromAllEvents();
|
|
129
128
|
}
|
|
130
129
|
|
|
131
130
|
async reinit() {
|
|
132
|
-
// Cancel any pending init/reinit
|
|
133
131
|
if (this.initReinitTimeout) {
|
|
134
132
|
clearTimeout(this.initReinitTimeout);
|
|
135
133
|
this.initReinitTimeout = undefined;
|
|
@@ -307,7 +305,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
307
305
|
if (eventSource !== 'Native') {
|
|
308
306
|
await this.unsubscribeFromAllEvents();
|
|
309
307
|
} else {
|
|
310
|
-
|
|
311
308
|
this.subscribeToAllEvents().catch((e) => {
|
|
312
309
|
logger.warn('Failed to subscribe to Native events', e);
|
|
313
310
|
});
|
|
@@ -343,7 +340,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
343
340
|
|
|
344
341
|
await this.updateDeviceInfo();
|
|
345
342
|
|
|
346
|
-
// Initialize event subscriptions based on selected source
|
|
347
343
|
await this.reinitEventSubscriptions();
|
|
348
344
|
|
|
349
345
|
setInterval(async () => {
|