@apocaliss92/scrypted-reolink-native 0.1.3 → 0.1.5
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/.vscode/settings.json +1 -1
- package/README.md +12 -3
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +2 -2
- package/src/camera-battery.ts +72 -64
- package/src/camera.ts +11 -13
- package/src/common.ts +90 -85
- package/src/connect.ts +136 -0
- package/src/intercom.ts +1 -1
- package/src/main.ts +133 -80
- package/src/nvr.ts +367 -0
- package/src/presets.ts +6 -6
- package/src/stream-utils.ts +26 -25
- package/src/utils.ts +31 -2
package/src/main.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedNativeId, Setting } from "@scrypted/sdk";
|
|
1
|
+
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting } from "@scrypted/sdk";
|
|
2
2
|
import { ReolinkNativeCamera } from "./camera";
|
|
3
3
|
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
4
|
-
import {
|
|
4
|
+
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
5
|
+
import { autoDetectDeviceType, createBaichuanApi } from "./connect";
|
|
5
6
|
import { getDeviceInterfaces } from "./utils";
|
|
6
7
|
|
|
7
8
|
class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
|
8
|
-
devices = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
|
|
9
|
+
devices = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera | ReolinkNativeNvrDevice>();
|
|
9
10
|
|
|
10
11
|
getScryptedDeviceCreator(): string {
|
|
11
12
|
return 'Reolink Native camera';
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
async getDevice(nativeId: ScryptedNativeId): Promise<ReolinkNativeCamera | ReolinkNativeBatteryCamera> {
|
|
15
|
+
async getDevice(nativeId: ScryptedNativeId): Promise<ReolinkNativeCamera | ReolinkNativeBatteryCamera | ReolinkNativeNvrDevice> {
|
|
15
16
|
if (this.devices.has(nativeId)) {
|
|
16
|
-
return this.devices.get(nativeId)
|
|
17
|
+
return this.devices.get(nativeId)!;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const newCamera = this.createCamera(nativeId);
|
|
@@ -23,88 +24,143 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
23
24
|
|
|
24
25
|
async createDevice(settings: DeviceCreatorSettings, nativeId?: string): Promise<string> {
|
|
25
26
|
const ipAddress = settings.ip?.toString();
|
|
26
|
-
let info: DeviceInformation = {};
|
|
27
|
-
|
|
28
27
|
const username = settings.username?.toString();
|
|
29
28
|
const password = settings.password?.toString();
|
|
30
29
|
const uid = settings.uid?.toString();
|
|
31
|
-
const isBatteryCam = settings.isBatteryCam === true || settings.isBatteryCam?.toString() === 'true';
|
|
32
30
|
|
|
33
|
-
if (
|
|
34
|
-
throw new Error('
|
|
31
|
+
if (!ipAddress || !username || !password) {
|
|
32
|
+
throw new Error('IP address, username, and password are required');
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
// Auto-detect device type (camera, battery-cam, or nvr)
|
|
36
|
+
this.console.log(`[AutoDetect] Starting device type detection for ${ipAddress}...`);
|
|
37
|
+
const detection = await autoDetectDeviceType(
|
|
38
|
+
{
|
|
39
|
+
host: ipAddress,
|
|
40
|
+
username,
|
|
41
|
+
password,
|
|
42
|
+
uid,
|
|
43
|
+
logger: this.console,
|
|
44
|
+
},
|
|
45
|
+
this.console
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
this.console.log(`[AutoDetect] Detected device type: ${detection.type} (transport: ${detection.transport})`);
|
|
49
|
+
|
|
50
|
+
// Handle NVR case
|
|
51
|
+
if (detection.type === 'nvr') {
|
|
52
|
+
const deviceInfo = detection.deviceInfo || {};
|
|
53
|
+
const name = deviceInfo?.name || 'Reolink NVR';
|
|
54
|
+
const serialNumber = deviceInfo?.serialNumber || deviceInfo?.itemNo || `nvr-${Date.now()}`;
|
|
55
|
+
nativeId = `${serialNumber}-nvr`;
|
|
56
|
+
|
|
57
|
+
settings.newCamera ||= name;
|
|
58
|
+
|
|
59
|
+
await sdk.deviceManager.onDeviceDiscovered({
|
|
60
|
+
nativeId,
|
|
61
|
+
name,
|
|
62
|
+
interfaces: [
|
|
63
|
+
ScryptedInterface.Settings,
|
|
64
|
+
ScryptedInterface.DeviceDiscovery,
|
|
65
|
+
ScryptedInterface.DeviceProvider,
|
|
66
|
+
ScryptedInterface.Reboot,
|
|
67
|
+
],
|
|
68
|
+
type: ScryptedDeviceType.Builtin,
|
|
69
|
+
providerNativeId: this.nativeId,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const device = await this.getDevice(nativeId);
|
|
73
|
+
if (!(device instanceof ReolinkNativeNvrDevice)) {
|
|
74
|
+
throw new Error('Expected NVR device but got different type');
|
|
75
|
+
}
|
|
76
|
+
device.storageSettings.values.ipAddress = ipAddress;
|
|
77
|
+
device.storageSettings.values.username = username;
|
|
78
|
+
device.storageSettings.values.password = password;
|
|
51
79
|
|
|
52
|
-
|
|
80
|
+
return nativeId;
|
|
81
|
+
}
|
|
53
82
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
83
|
+
// For camera and battery-cam, create the device
|
|
84
|
+
const deviceInfo = detection.deviceInfo || {};
|
|
85
|
+
const name = deviceInfo?.name || 'Reolink Camera';
|
|
86
|
+
const serialNumber = deviceInfo?.serialNumber || deviceInfo?.itemNo || `unknown-${Date.now()}`;
|
|
87
|
+
|
|
88
|
+
// Create nativeId based on device type
|
|
89
|
+
if (detection.type === 'battery-cam') {
|
|
90
|
+
nativeId = `${serialNumber}-battery-cam`;
|
|
91
|
+
} else {
|
|
92
|
+
nativeId = `${serialNumber}-cam`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
settings.newCamera ||= name;
|
|
96
|
+
|
|
97
|
+
// Create API connection to get capabilities
|
|
98
|
+
const api = await createBaichuanApi({
|
|
99
|
+
inputs: {
|
|
100
|
+
host: ipAddress,
|
|
101
|
+
username,
|
|
102
|
+
password,
|
|
103
|
+
uid: detection.uid,
|
|
104
|
+
logger: this.console,
|
|
105
|
+
},
|
|
106
|
+
transport: detection.transport,
|
|
107
|
+
logger: this.console,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await api.login();
|
|
112
|
+
const rtspChannel = 0;
|
|
113
|
+
const { abilities, capabilities, objects, presets } = await api.getDeviceCapabilities(rtspChannel);
|
|
114
|
+
|
|
115
|
+
this.console.log(JSON.stringify({ abilities, capabilities, deviceInfo }));
|
|
116
|
+
|
|
117
|
+
const { interfaces, type } = getDeviceInterfaces({
|
|
118
|
+
capabilities,
|
|
119
|
+
logger: this.console,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await sdk.deviceManager.onDeviceDiscovered({
|
|
123
|
+
nativeId,
|
|
124
|
+
name,
|
|
125
|
+
interfaces,
|
|
126
|
+
type,
|
|
127
|
+
providerNativeId: this.nativeId,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const device = await this.getDevice(nativeId);
|
|
131
|
+
if (device instanceof ReolinkNativeNvrDevice) {
|
|
132
|
+
// NVR devices are handled separately above
|
|
133
|
+
throw new Error('NVR device should not reach this code path');
|
|
100
134
|
}
|
|
135
|
+
|
|
136
|
+
// Type guard: device is either ReolinkNativeCamera or ReolinkNativeBatteryCamera
|
|
137
|
+
device.info = deviceInfo;
|
|
138
|
+
device.classes = objects;
|
|
139
|
+
device.presets = presets;
|
|
140
|
+
device.storageSettings.values.username = username;
|
|
141
|
+
device.storageSettings.values.password = password;
|
|
142
|
+
device.storageSettings.values.rtspChannel = rtspChannel;
|
|
143
|
+
device.storageSettings.values.ipAddress = ipAddress;
|
|
144
|
+
device.storageSettings.values.capabilities = capabilities;
|
|
145
|
+
device.storageSettings.values.uid = detection.uid;
|
|
146
|
+
|
|
147
|
+
return nativeId;
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
this.console.error('Error adding Reolink device', e);
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await api.close();
|
|
101
155
|
}
|
|
102
156
|
}
|
|
103
157
|
|
|
104
158
|
async releaseDevice(id: string, nativeId: ScryptedNativeId): Promise<void> {
|
|
105
159
|
if (this.devices.has(nativeId)) {
|
|
106
160
|
const device = this.devices.get(nativeId);
|
|
107
|
-
|
|
161
|
+
if (device && 'release' in device && typeof device.release === 'function') {
|
|
162
|
+
await device.release();
|
|
163
|
+
}
|
|
108
164
|
this.devices.delete(nativeId);
|
|
109
165
|
}
|
|
110
166
|
}
|
|
@@ -116,12 +172,6 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
116
172
|
title: 'IP Address',
|
|
117
173
|
placeholder: '192.168.2.222',
|
|
118
174
|
},
|
|
119
|
-
{
|
|
120
|
-
key: 'isBatteryCam',
|
|
121
|
-
title: 'Is Battery Camera',
|
|
122
|
-
description: 'Enable for Reolink battery cameras. Uses UDP/BCUDP for discovery and streaming. Requires UID.',
|
|
123
|
-
type: 'boolean',
|
|
124
|
-
},
|
|
125
175
|
{
|
|
126
176
|
key: 'username',
|
|
127
177
|
title: 'Username',
|
|
@@ -134,7 +184,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
134
184
|
{
|
|
135
185
|
key: 'uid',
|
|
136
186
|
title: 'UID',
|
|
137
|
-
description: 'Reolink UID (required for battery cameras)',
|
|
187
|
+
description: 'Reolink UID (optional, required for battery cameras if TCP connection fails)',
|
|
138
188
|
}
|
|
139
189
|
]
|
|
140
190
|
}
|
|
@@ -142,8 +192,11 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
142
192
|
createCamera(nativeId: string) {
|
|
143
193
|
if (nativeId.endsWith('-battery-cam')) {
|
|
144
194
|
return new ReolinkNativeBatteryCamera(nativeId, this);
|
|
195
|
+
} else if (nativeId.endsWith('-nvr')) {
|
|
196
|
+
return new ReolinkNativeNvrDevice(nativeId, this);
|
|
197
|
+
} else {
|
|
198
|
+
return new ReolinkNativeCamera(nativeId, this);
|
|
145
199
|
}
|
|
146
|
-
return new ReolinkNativeCamera(nativeId, this);
|
|
147
200
|
}
|
|
148
201
|
}
|
|
149
202
|
|
package/src/nvr.ts
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import type { DeviceInfoResponse, DeviceInputData, ReolinkCgiApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
|
+
import sdk, { AdoptDevice, Device, DeviceDiscovery, DeviceProvider, DiscoveredDevice, Reboot, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
|
3
|
+
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
|
4
|
+
import { ReolinkNativeCamera } from "./camera";
|
|
5
|
+
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
6
|
+
import { normalizeUid } from "./connect";
|
|
7
|
+
import ReolinkNativePlugin from "./main";
|
|
8
|
+
import { getDeviceInterfaces, updateDeviceInfo } from "./utils";
|
|
9
|
+
|
|
10
|
+
export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settings, DeviceDiscovery, DeviceProvider, Reboot {
|
|
11
|
+
storageSettings = new StorageSettings(this, {
|
|
12
|
+
debugEvents: {
|
|
13
|
+
title: 'Debug Events',
|
|
14
|
+
type: 'boolean',
|
|
15
|
+
immediate: true,
|
|
16
|
+
},
|
|
17
|
+
ipAddress: {
|
|
18
|
+
title: 'IP address',
|
|
19
|
+
type: 'string',
|
|
20
|
+
onPut: async () => await this.reinit()
|
|
21
|
+
},
|
|
22
|
+
username: {
|
|
23
|
+
title: 'Username',
|
|
24
|
+
placeholder: 'admin',
|
|
25
|
+
defaultValue: 'admin',
|
|
26
|
+
type: 'string',
|
|
27
|
+
onPut: async () => await this.reinit()
|
|
28
|
+
},
|
|
29
|
+
password: {
|
|
30
|
+
title: 'Password',
|
|
31
|
+
type: 'password',
|
|
32
|
+
onPut: async () => await this.reinit()
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
plugin: ReolinkNativePlugin;
|
|
36
|
+
nvrApi: ReolinkCgiApi | undefined;
|
|
37
|
+
discoveredDevices = new Map<string, {
|
|
38
|
+
device: Device;
|
|
39
|
+
description: string;
|
|
40
|
+
rtspChannel: number;
|
|
41
|
+
deviceData: DeviceInfoResponse;
|
|
42
|
+
}>();
|
|
43
|
+
lastHubInfoCheck: number | undefined;
|
|
44
|
+
lastErrorsCheck: number | undefined;
|
|
45
|
+
lastDevicesStatusCheck: number | undefined;
|
|
46
|
+
cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
|
|
47
|
+
processing = false;
|
|
48
|
+
|
|
49
|
+
constructor(nativeId: string, plugin: ReolinkNativePlugin) {
|
|
50
|
+
super(nativeId);
|
|
51
|
+
this.plugin = plugin;
|
|
52
|
+
|
|
53
|
+
setTimeout(async () => {
|
|
54
|
+
await this.init();
|
|
55
|
+
}, 5000);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async reboot(): Promise<void> {
|
|
59
|
+
const api = await this.ensureClient();
|
|
60
|
+
await api.Reboot();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getLogger() {
|
|
64
|
+
return this.console;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async reinit() {
|
|
68
|
+
if (this.nvrApi) {
|
|
69
|
+
try {
|
|
70
|
+
await this.nvrApi.logout();
|
|
71
|
+
} catch {
|
|
72
|
+
// ignore
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.nvrApi = undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async ensureClient(): Promise<ReolinkCgiApi> {
|
|
79
|
+
if (this.nvrApi) {
|
|
80
|
+
return this.nvrApi;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { ipAddress, username, password } = this.storageSettings.values;
|
|
84
|
+
if (!ipAddress || !username || !password) {
|
|
85
|
+
throw new Error('Missing NVR credentials');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { ReolinkCgiApi } = await import("@apocaliss92/reolink-baichuan-js");
|
|
89
|
+
this.nvrApi = new ReolinkCgiApi({
|
|
90
|
+
host: ipAddress,
|
|
91
|
+
username,
|
|
92
|
+
password,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await this.nvrApi.login();
|
|
96
|
+
return this.nvrApi;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async init() {
|
|
100
|
+
const api = await this.ensureClient();
|
|
101
|
+
const logger = this.getLogger();
|
|
102
|
+
await this.updateDeviceInfo();
|
|
103
|
+
|
|
104
|
+
setInterval(async () => {
|
|
105
|
+
if (this.processing || !api) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.processing = true;
|
|
109
|
+
try {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
|
|
112
|
+
if (!this.lastErrorsCheck || (now - this.lastErrorsCheck > 60 * 1000)) {
|
|
113
|
+
this.lastErrorsCheck = now;
|
|
114
|
+
// Note: ReolinkCgiApi doesn't have checkErrors, skip for now
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!this.lastHubInfoCheck || now - this.lastHubInfoCheck > 1000 * 60 * 5) {
|
|
118
|
+
logger.log('Starting Hub info data fetch');
|
|
119
|
+
this.lastHubInfoCheck = now;
|
|
120
|
+
const { hubData } = await api.getHubInfo();
|
|
121
|
+
const { devicesData, channelsResponse, response } = await api.getDevicesInfo();
|
|
122
|
+
logger.log('Hub info data fetched');
|
|
123
|
+
if (this.storageSettings.values.debugEvents) {
|
|
124
|
+
logger.log(`${JSON.stringify({ hubData, devicesData, channelsResponse, response })}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await this.discoverDevices(true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const eventsRes = await api.getAllChannelsEvents();
|
|
131
|
+
|
|
132
|
+
if (this.storageSettings.values.debugEvents) {
|
|
133
|
+
logger.debug(`Events call result: ${JSON.stringify(eventsRes)}`);
|
|
134
|
+
}
|
|
135
|
+
this.cameraNativeMap.forEach((camera) => {
|
|
136
|
+
if (camera) {
|
|
137
|
+
const channel = camera.storageSettings.values.rtspChannel;
|
|
138
|
+
const cameraEventsData = eventsRes?.parsed[channel];
|
|
139
|
+
if (cameraEventsData) {
|
|
140
|
+
camera.processEvents(cameraEventsData);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const { batteryInfoData, response } = await api.getAllChannelsBatteryInfo();
|
|
146
|
+
|
|
147
|
+
if (this.storageSettings.values.debugEvents) {
|
|
148
|
+
logger.debug(`Battery info call result: ${JSON.stringify({ batteryInfoData, response })}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.cameraNativeMap.forEach((camera) => {
|
|
152
|
+
if (camera) {
|
|
153
|
+
const channel = camera.storageSettings.values.rtspChannel;
|
|
154
|
+
const cameraBatteryData = batteryInfoData[channel];
|
|
155
|
+
if (cameraBatteryData) {
|
|
156
|
+
(camera as ReolinkNativeBatteryCamera).updateSleepingState({
|
|
157
|
+
reason: 'NVR',
|
|
158
|
+
state: cameraBatteryData.sleeping ? 'sleeping' : 'awake',
|
|
159
|
+
idleMs: 0,
|
|
160
|
+
lastRxAtMs: 0,
|
|
161
|
+
}).catch(() => { });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
} catch (e) {
|
|
166
|
+
this.console.error('Error on events flow', e);
|
|
167
|
+
} finally {
|
|
168
|
+
this.processing = false;
|
|
169
|
+
}
|
|
170
|
+
}, 1000);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async updateDeviceInfo(): Promise<void> {
|
|
174
|
+
const { ipAddress } = this.storageSettings.values;
|
|
175
|
+
try {
|
|
176
|
+
const api = await this.ensureClient();
|
|
177
|
+
const deviceData = await api.getInfo();
|
|
178
|
+
|
|
179
|
+
await updateDeviceInfo({
|
|
180
|
+
device: this,
|
|
181
|
+
ipAddress,
|
|
182
|
+
deviceData,
|
|
183
|
+
});
|
|
184
|
+
} catch (e) {
|
|
185
|
+
this.getLogger().warn('Failed to fetch device info', e);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async getSettings(): Promise<Setting[]> {
|
|
190
|
+
const settings = await this.storageSettings.getSettings();
|
|
191
|
+
return settings;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async putSetting(key: string, value: SettingValue): Promise<void> {
|
|
195
|
+
return this.storageSettings.putSetting(key, value);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async releaseDevice(id: string, nativeId: string) {
|
|
199
|
+
this.cameraNativeMap.delete(nativeId);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async getDevice(nativeId: string): Promise<ReolinkNativeCamera | ReolinkNativeBatteryCamera> {
|
|
203
|
+
let device = this.cameraNativeMap.get(nativeId);
|
|
204
|
+
|
|
205
|
+
if (!device) {
|
|
206
|
+
if (nativeId.endsWith('-battery-cam')) {
|
|
207
|
+
device = new ReolinkNativeBatteryCamera(nativeId, this.plugin, this);
|
|
208
|
+
} else {
|
|
209
|
+
device = new ReolinkNativeCamera(nativeId, this.plugin, this);
|
|
210
|
+
}
|
|
211
|
+
this.cameraNativeMap.set(nativeId, device);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return device;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
buildNativeId(channel: number, serialNumber?: string, isBattery?: boolean): string {
|
|
218
|
+
const suffix = isBattery ? '-battery-cam' : '-cam';
|
|
219
|
+
if (serialNumber) {
|
|
220
|
+
return `${this.nativeId}-ch${channel}-${serialNumber}${suffix}`;
|
|
221
|
+
}
|
|
222
|
+
return `${this.nativeId}-ch${channel}${suffix}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
getCameraInterfaces() {
|
|
226
|
+
return [
|
|
227
|
+
ScryptedInterface.VideoCameraConfiguration,
|
|
228
|
+
ScryptedInterface.Camera,
|
|
229
|
+
ScryptedInterface.MotionSensor,
|
|
230
|
+
ScryptedInterface.VideoTextOverlays,
|
|
231
|
+
ScryptedInterface.VideoCamera,
|
|
232
|
+
ScryptedInterface.Settings,
|
|
233
|
+
ScryptedInterface.ObjectDetector,
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async syncEntitiesFromRemote() {
|
|
238
|
+
const api = await this.ensureClient();
|
|
239
|
+
const logger = this.getLogger();
|
|
240
|
+
|
|
241
|
+
logger.log('Starting channels discovery using getDevicesInfo...');
|
|
242
|
+
|
|
243
|
+
const { devicesData, channels } = await api.getDevicesInfo();
|
|
244
|
+
|
|
245
|
+
logger.log(`getDevicesInfo completed. Found ${channels.length} channels.`);
|
|
246
|
+
|
|
247
|
+
// Process each channel that was successfully discovered
|
|
248
|
+
for (const channel of channels) {
|
|
249
|
+
try {
|
|
250
|
+
const { channelStatus, channelInfo, abilities } = devicesData[channel];
|
|
251
|
+
const name = channelStatus?.name;
|
|
252
|
+
const uid = channelStatus?.uid;
|
|
253
|
+
const isBattery = !!(abilities?.battery?.ver ?? 0);
|
|
254
|
+
|
|
255
|
+
const nativeId = this.buildNativeId(channel, uid, isBattery);
|
|
256
|
+
const interfaces = [ScryptedInterface.VideoCamera];
|
|
257
|
+
if (isBattery) {
|
|
258
|
+
interfaces.push(ScryptedInterface.Battery);
|
|
259
|
+
}
|
|
260
|
+
const type = abilities.supportDoorbellLight ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera;
|
|
261
|
+
|
|
262
|
+
const device: Device = {
|
|
263
|
+
nativeId,
|
|
264
|
+
name,
|
|
265
|
+
providerNativeId: this.nativeId,
|
|
266
|
+
interfaces,
|
|
267
|
+
type,
|
|
268
|
+
info: {
|
|
269
|
+
manufacturer: 'Reolink',
|
|
270
|
+
model: channelInfo?.typeInfo,
|
|
271
|
+
serialNumber: uid,
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
if (sdk.deviceManager.getNativeIds().includes(nativeId)) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (this.discoveredDevices.has(nativeId)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.discoveredDevices.set(nativeId, {
|
|
284
|
+
device,
|
|
285
|
+
description: `${name} (Channel ${channel})`,
|
|
286
|
+
rtspChannel: channel,
|
|
287
|
+
deviceData: devicesData[channel],
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
logger.debug(`Discovered channel ${channel}: ${name}`);
|
|
291
|
+
} catch (e: any) {
|
|
292
|
+
logger.debug(`Error processing channel ${channel}: ${e?.message || String(e)}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
logger.log(`Channel discovery completed. Found ${this.discoveredDevices.size} devices.`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async discoverDevices(scan?: boolean): Promise<DiscoveredDevice[]> {
|
|
300
|
+
if (scan) {
|
|
301
|
+
await this.syncEntitiesFromRemote();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return [...this.discoveredDevices.values()].map(d => ({
|
|
305
|
+
...d.device,
|
|
306
|
+
description: d.description,
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async adoptDevice(adopt: AdoptDevice): Promise<string> {
|
|
311
|
+
const entry = this.discoveredDevices.get(adopt.nativeId);
|
|
312
|
+
|
|
313
|
+
if (!entry)
|
|
314
|
+
throw new Error('device not found');
|
|
315
|
+
|
|
316
|
+
await this.onDeviceEvent(ScryptedInterface.DeviceDiscovery, await this.discoverDevices());
|
|
317
|
+
|
|
318
|
+
const isBattery = entry.device.interfaces.includes(ScryptedInterface.Battery);
|
|
319
|
+
const { channelStatus } = entry.deviceData;
|
|
320
|
+
|
|
321
|
+
const { ReolinkBaichuanApi } = await import("@apocaliss92/reolink-baichuan-js");
|
|
322
|
+
const transport = 'tcp';
|
|
323
|
+
const uid = channelStatus?.uid;
|
|
324
|
+
const normalizedUid = isBattery && uid ? normalizeUid(uid) : undefined;
|
|
325
|
+
const baichuanApi = new ReolinkBaichuanApi({
|
|
326
|
+
host: this.storageSettings.values.ipAddress,
|
|
327
|
+
username: this.storageSettings.values.username,
|
|
328
|
+
password: this.storageSettings.values.password,
|
|
329
|
+
transport,
|
|
330
|
+
channel: entry.rtspChannel,
|
|
331
|
+
...(normalizedUid ? { uid: normalizedUid } : {}),
|
|
332
|
+
});
|
|
333
|
+
await baichuanApi.login();
|
|
334
|
+
const { capabilities, objects, presets } = await baichuanApi.getDeviceCapabilities(entry.rtspChannel);
|
|
335
|
+
const { interfaces, type } = getDeviceInterfaces({
|
|
336
|
+
capabilities,
|
|
337
|
+
logger: this.console,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const actualDevice: Device = {
|
|
341
|
+
...entry.device,
|
|
342
|
+
interfaces,
|
|
343
|
+
type
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
await sdk.deviceManager.onDeviceDiscovered(actualDevice);
|
|
347
|
+
|
|
348
|
+
const device = await this.getDevice(adopt.nativeId);
|
|
349
|
+
this.console.log('Adopted device', entry, device?.name);
|
|
350
|
+
const { username, password, ipAddress } = this.storageSettings.values;
|
|
351
|
+
|
|
352
|
+
device.storageSettings.values.rtspChannel = entry.rtspChannel;
|
|
353
|
+
device.classes = objects;
|
|
354
|
+
device.presets = presets;
|
|
355
|
+
device.storageSettings.values.username = username;
|
|
356
|
+
device.storageSettings.values.password = password;
|
|
357
|
+
device.storageSettings.values.rtspChannel = entry.rtspChannel;
|
|
358
|
+
device.storageSettings.values.ipAddress = ipAddress;
|
|
359
|
+
device.storageSettings.values.capabilities = capabilities;
|
|
360
|
+
device.storageSettings.values.uid = entry.deviceData.channelStatus.uid;
|
|
361
|
+
device.storageSettings.values.isFromNvr = true;
|
|
362
|
+
|
|
363
|
+
this.discoveredDevices.delete(adopt.nativeId);
|
|
364
|
+
return device?.id;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|