@apocaliss92/scrypted-reolink-native 0.3.17 → 0.4.1
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/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/accessories/autotracking.ts +150 -0
- package/src/accessories/floodlight.ts +92 -0
- package/src/accessories/index.ts +7 -0
- package/src/accessories/motion-floodlight.ts +171 -0
- package/src/accessories/motion-siren.ts +165 -0
- package/src/accessories/pir-sensor.ts +138 -0
- package/src/accessories/siren.ts +63 -0
- package/src/baichuan-base.ts +863 -792
- package/src/camera.ts +3726 -2911
- package/src/intercom.ts +496 -476
- package/src/main.ts +378 -409
- package/src/multiFocal.ts +300 -270
- package/src/nvr.ts +588 -477
- package/src/stream-utils.ts +478 -427
- package/src/utils.ts +385 -1009
package/src/nvr.ts
CHANGED
|
@@ -1,528 +1,639 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ReolinkBaichuanApi,
|
|
3
|
+
ReolinkBaichuanDeviceSummary,
|
|
4
|
+
ReolinkSimpleEvent,
|
|
5
|
+
} from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
6
|
+
import sdk, {
|
|
7
|
+
AdoptDevice,
|
|
8
|
+
Device,
|
|
9
|
+
DeviceDiscovery,
|
|
10
|
+
DeviceProvider,
|
|
11
|
+
DiscoveredDevice,
|
|
12
|
+
Reboot,
|
|
13
|
+
ScryptedDeviceType,
|
|
14
|
+
ScryptedInterface,
|
|
15
|
+
Setting,
|
|
16
|
+
Settings,
|
|
17
|
+
SettingValue,
|
|
18
|
+
} from "@scrypted/sdk";
|
|
3
19
|
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
|
4
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
BaseBaichuanClass,
|
|
22
|
+
type BaichuanConnectionCallbacks,
|
|
23
|
+
type BaichuanConnectionConfig,
|
|
24
|
+
} from "./baichuan-base";
|
|
5
25
|
import { ReolinkCamera } from "./camera";
|
|
6
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
convertDebugLogsToApiOptions,
|
|
28
|
+
getApiRelevantDebugLogs,
|
|
29
|
+
getDebugLogChoices,
|
|
30
|
+
} from "./debug-options";
|
|
7
31
|
import ReolinkNativePlugin from "./main";
|
|
8
32
|
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
private debugLogsResetTimeout: NodeJS.Timeout | undefined;
|
|
110
|
-
|
|
111
|
-
constructor(nativeId: string, plugin: ReolinkNativePlugin) {
|
|
112
|
-
super(nativeId, "tcp");
|
|
113
|
-
this.plugin = plugin;
|
|
114
|
-
|
|
115
|
-
this.scheduleInit();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async reboot(): Promise<void> {
|
|
119
|
-
const api = await this.ensureBaichuanClient();
|
|
120
|
-
await api.reboot();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
protected getConnectionConfig(): BaichuanConnectionConfig {
|
|
124
|
-
const { ipAddress, username, password } = this.storageSettings.values;
|
|
125
|
-
if (!ipAddress || !username || !password) {
|
|
126
|
-
throw new Error('Missing NVR credentials');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const debugOptions = this.getBaichuanDebugOptions();
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
host: ipAddress,
|
|
133
|
-
username,
|
|
134
|
-
password,
|
|
135
|
-
transport: 'tcp',
|
|
136
|
-
debugOptions,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
protected getStreamClientInputs(): BaichuanConnectionConfig {
|
|
141
|
-
const { ipAddress, username, password } = this.storageSettings.values;
|
|
142
|
-
if (!ipAddress || !username || !password) {
|
|
143
|
-
throw new Error('Missing NVR credentials');
|
|
33
|
+
import {
|
|
34
|
+
batteryCameraSuffix,
|
|
35
|
+
batteryMultifocalSuffix,
|
|
36
|
+
cameraSuffix,
|
|
37
|
+
getDeviceInterfaces,
|
|
38
|
+
multifocalSuffix,
|
|
39
|
+
updateDeviceInfo,
|
|
40
|
+
} from "./utils";
|
|
41
|
+
|
|
42
|
+
export class ReolinkNativeNvrDevice
|
|
43
|
+
extends BaseBaichuanClass
|
|
44
|
+
implements Settings, DeviceDiscovery, DeviceProvider, Reboot
|
|
45
|
+
{
|
|
46
|
+
private readonly onSimpleEventBound = (ev: ReolinkSimpleEvent) =>
|
|
47
|
+
this.onSimpleEvent(ev);
|
|
48
|
+
|
|
49
|
+
storageSettings = new StorageSettings(this, {
|
|
50
|
+
debugLogs: {
|
|
51
|
+
title: "Debug Events",
|
|
52
|
+
type: "boolean",
|
|
53
|
+
immediate: true,
|
|
54
|
+
},
|
|
55
|
+
// eventSource: {
|
|
56
|
+
// title: 'Event Source',
|
|
57
|
+
// description: 'Select the source for camera events: Native (Baichuan) or CGI (HTTP polling)',
|
|
58
|
+
// type: 'string',
|
|
59
|
+
// choices: ['Native', 'CGI'],
|
|
60
|
+
// defaultValue: 'Native',
|
|
61
|
+
// immediate: true,
|
|
62
|
+
// onPut: async () => {
|
|
63
|
+
// await this.reinitEventSubscriptions();
|
|
64
|
+
// }
|
|
65
|
+
// },
|
|
66
|
+
ipAddress: {
|
|
67
|
+
title: "IP address",
|
|
68
|
+
type: "string",
|
|
69
|
+
onPut: async () => await this.reinit(),
|
|
70
|
+
},
|
|
71
|
+
username: {
|
|
72
|
+
title: "Username",
|
|
73
|
+
placeholder: "admin",
|
|
74
|
+
defaultValue: "admin",
|
|
75
|
+
type: "string",
|
|
76
|
+
onPut: async () => await this.reinit(),
|
|
77
|
+
},
|
|
78
|
+
password: {
|
|
79
|
+
title: "Password",
|
|
80
|
+
type: "password",
|
|
81
|
+
onPut: async () => await this.reinit(),
|
|
82
|
+
},
|
|
83
|
+
diagnosticsRun: {
|
|
84
|
+
subgroup: "Advanced",
|
|
85
|
+
title: "Run Diagnostics",
|
|
86
|
+
description: "Collect NVR diagnostics and display results in logs.",
|
|
87
|
+
type: "button",
|
|
88
|
+
immediate: true,
|
|
89
|
+
onPut: async () => {
|
|
90
|
+
await this.runNvrDiagnostics();
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
socketApiDebugLogs: {
|
|
94
|
+
subgroup: "Advanced",
|
|
95
|
+
title: "Socket API Debug Logs",
|
|
96
|
+
description: "Enable specific debug logs.",
|
|
97
|
+
multiple: true,
|
|
98
|
+
combobox: true,
|
|
99
|
+
immediate: true,
|
|
100
|
+
defaultValue: [],
|
|
101
|
+
choices: getDebugLogChoices(),
|
|
102
|
+
onPut: async (ov, value) => {
|
|
103
|
+
const logger = this.getBaichuanLogger();
|
|
104
|
+
const oldApiOptions = getApiRelevantDebugLogs(ov || []);
|
|
105
|
+
const newApiOptions = getApiRelevantDebugLogs(value || []);
|
|
106
|
+
|
|
107
|
+
const oldSel = new Set(oldApiOptions);
|
|
108
|
+
const newSel = new Set(newApiOptions);
|
|
109
|
+
|
|
110
|
+
const changed =
|
|
111
|
+
oldSel.size !== newSel.size ||
|
|
112
|
+
Array.from(oldSel).some((k) => !newSel.has(k));
|
|
113
|
+
if (changed) {
|
|
114
|
+
// Clear any existing timeout
|
|
115
|
+
if (this.debugLogsResetTimeout) {
|
|
116
|
+
clearTimeout(this.debugLogsResetTimeout);
|
|
117
|
+
this.debugLogsResetTimeout = undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.debugLogsResetTimeout = setTimeout(async () => {
|
|
121
|
+
this.debugLogsResetTimeout = undefined;
|
|
122
|
+
try {
|
|
123
|
+
this.baichuanApi = undefined;
|
|
124
|
+
this.ensureClientPromise = undefined;
|
|
125
|
+
await this.ensureBaichuanClient();
|
|
126
|
+
} catch (e) {
|
|
127
|
+
logger.warn(
|
|
128
|
+
"Failed to reset client after debug logs change",
|
|
129
|
+
e?.message || String(e),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}, 2000);
|
|
144
133
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
userSessions: {
|
|
137
|
+
title: "Active User Sessions",
|
|
138
|
+
subgroup: "Sessions",
|
|
139
|
+
description:
|
|
140
|
+
"List of currently active user sessions connected to the device via Baichuan socket. Click 'Refresh Sessions' to update.",
|
|
141
|
+
type: "string",
|
|
142
|
+
multiple: true,
|
|
143
|
+
combobox: false,
|
|
144
|
+
readonly: true,
|
|
145
|
+
hide: false,
|
|
146
|
+
defaultValue: [],
|
|
147
|
+
},
|
|
148
|
+
refreshUserSessions: {
|
|
149
|
+
title: "Refresh Sessions",
|
|
150
|
+
subgroup: "Sessions",
|
|
151
|
+
description: "Refresh the list of active user sessions from the device.",
|
|
152
|
+
type: "button",
|
|
153
|
+
immediate: true,
|
|
154
|
+
hide: false,
|
|
155
|
+
onPut: async () => {
|
|
156
|
+
await this.refreshUserSessionsList();
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
plugin: ReolinkNativePlugin;
|
|
161
|
+
discoveredDevices = new Map<
|
|
162
|
+
string,
|
|
163
|
+
{
|
|
164
|
+
device: Device;
|
|
165
|
+
description: string;
|
|
166
|
+
rtspChannel: number;
|
|
167
|
+
deviceData: ReolinkBaichuanDeviceSummary;
|
|
174
168
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
>();
|
|
170
|
+
cameraNativeMap = new Map<string, ReolinkCamera>();
|
|
171
|
+
private channelToNativeIdMap = new Map<number, string>();
|
|
172
|
+
private discoverDevicesPromise: Promise<DiscoveredDevice[]> | undefined;
|
|
173
|
+
processing = false;
|
|
174
|
+
private initReinitTimeout: NodeJS.Timeout | undefined;
|
|
175
|
+
private debugLogsResetTimeout: NodeJS.Timeout | undefined;
|
|
176
|
+
|
|
177
|
+
constructor(nativeId: string, plugin: ReolinkNativePlugin) {
|
|
178
|
+
super(nativeId, "tcp");
|
|
179
|
+
this.plugin = plugin;
|
|
180
|
+
|
|
181
|
+
this.scheduleInit();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async reboot(): Promise<void> {
|
|
185
|
+
const api = await this.ensureBaichuanClient();
|
|
186
|
+
await api.reboot();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
protected getConnectionConfig(): BaichuanConnectionConfig {
|
|
190
|
+
const { ipAddress, username, password } = this.storageSettings.values;
|
|
191
|
+
if (!ipAddress || !username || !password) {
|
|
192
|
+
throw new Error("Missing NVR credentials");
|
|
178
193
|
}
|
|
179
194
|
|
|
180
|
-
|
|
181
|
-
|
|
195
|
+
const debugOptions = this.getBaichuanDebugOptions();
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
host: ipAddress,
|
|
199
|
+
username,
|
|
200
|
+
password,
|
|
201
|
+
transport: "tcp",
|
|
202
|
+
debugOptions,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getBaichuanDebugOptions(): any | undefined {
|
|
207
|
+
const socketDebugLogs =
|
|
208
|
+
this.storageSettings.values.socketApiDebugLogs || [];
|
|
209
|
+
return convertDebugLogsToApiOptions(socketDebugLogs);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
|
|
213
|
+
return {
|
|
214
|
+
onClose: async () => {
|
|
215
|
+
await this.reinit();
|
|
216
|
+
},
|
|
217
|
+
onSimpleEvent: this.onSimpleEventBound,
|
|
218
|
+
getEventSubscriptionEnabled: () => true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
protected isDebugEnabled(): boolean {
|
|
223
|
+
return this.storageSettings.values.debugLogs || false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
protected getDeviceName(): string {
|
|
227
|
+
return this.name || "NVR";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
protected async onBeforeCleanup(): Promise<void> {
|
|
231
|
+
await this.unsubscribeFromEvents();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async reinit() {
|
|
235
|
+
if (this.initReinitTimeout) {
|
|
236
|
+
clearTimeout(this.initReinitTimeout);
|
|
237
|
+
this.initReinitTimeout = undefined;
|
|
182
238
|
}
|
|
183
239
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
clearTimeout(this.initReinitTimeout);
|
|
187
|
-
this.initReinitTimeout = undefined;
|
|
188
|
-
}
|
|
240
|
+
this.scheduleInit(true);
|
|
241
|
+
}
|
|
189
242
|
|
|
190
|
-
|
|
243
|
+
private scheduleInit(isReinit: boolean = false): void {
|
|
244
|
+
// Cancel any pending init/reinit
|
|
245
|
+
if (this.initReinitTimeout) {
|
|
246
|
+
clearTimeout(this.initReinitTimeout);
|
|
191
247
|
}
|
|
192
248
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (
|
|
196
|
-
|
|
249
|
+
this.initReinitTimeout = setTimeout(
|
|
250
|
+
async () => {
|
|
251
|
+
if (isReinit) {
|
|
252
|
+
await super.cleanupBaichuanApi();
|
|
197
253
|
}
|
|
198
|
-
|
|
199
|
-
this.initReinitTimeout =
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
254
|
+
await this.init();
|
|
255
|
+
this.initReinitTimeout = undefined;
|
|
256
|
+
},
|
|
257
|
+
isReinit ? 500 : 2000,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
onSimpleEvent(ev: ReolinkSimpleEvent) {
|
|
262
|
+
const logger = this.getBaichuanLogger();
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
logger.debug(`Baichuan event on nvr: ${JSON.stringify(ev)}`);
|
|
266
|
+
|
|
267
|
+
const channel = ev?.channel;
|
|
268
|
+
if (channel === undefined) {
|
|
269
|
+
logger.error("Event has no channel, ignoring");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const nativeId = this.channelToNativeIdMap.get(channel);
|
|
274
|
+
const targetDevice = nativeId
|
|
275
|
+
? this.cameraNativeMap.get(nativeId)
|
|
276
|
+
: undefined;
|
|
277
|
+
|
|
278
|
+
if (!targetDevice) {
|
|
279
|
+
logger.debug(
|
|
280
|
+
`No device found for channel ${channel} (nativeId: ${nativeId}), ignoring event`,
|
|
281
|
+
);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
targetDevice.onSimpleEvent(ev);
|
|
286
|
+
} catch (e) {
|
|
287
|
+
logger.warn(
|
|
288
|
+
"Error in NVR Native event forwarder",
|
|
289
|
+
e?.message || String(e),
|
|
290
|
+
);
|
|
206
291
|
}
|
|
292
|
+
}
|
|
207
293
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
logger.debug(`Baichuan event on nvr: ${JSON.stringify(ev)}`);
|
|
294
|
+
async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
|
|
295
|
+
return await super.ensureBaichuanClient();
|
|
296
|
+
}
|
|
213
297
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
298
|
+
async ensureClient(): Promise<ReolinkBaichuanApi> {
|
|
299
|
+
return await this.ensureBaichuanClient();
|
|
300
|
+
}
|
|
219
301
|
|
|
220
|
-
|
|
221
|
-
|
|
302
|
+
private async runNvrDiagnostics(): Promise<void> {
|
|
303
|
+
const logger = this.getBaichuanLogger();
|
|
304
|
+
logger.log(`Starting NVR diagnostics...`);
|
|
222
305
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
306
|
+
try {
|
|
307
|
+
const api = await this.ensureBaichuanClient();
|
|
227
308
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
309
|
+
await api.collectNvrDiagnostics({
|
|
310
|
+
logger: this.console,
|
|
311
|
+
});
|
|
312
|
+
} catch (e) {
|
|
313
|
+
logger.error("Failed to run NVR diagnostics", e?.message || String(e));
|
|
314
|
+
throw e;
|
|
233
315
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async init() {
|
|
319
|
+
await this.ensureBaichuanClient();
|
|
320
|
+
await this.subscribeToEvents();
|
|
321
|
+
await this.discoverDevices(true);
|
|
322
|
+
|
|
323
|
+
await this.updateDeviceInfo();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async updateDeviceInfo(): Promise<void> {
|
|
327
|
+
const logger = this.getBaichuanLogger();
|
|
328
|
+
|
|
329
|
+
const { ipAddress } = this.storageSettings.values;
|
|
330
|
+
try {
|
|
331
|
+
const api = await this.ensureBaichuanClient();
|
|
332
|
+
const deviceData = await api.getInfo();
|
|
333
|
+
|
|
334
|
+
await updateDeviceInfo({
|
|
335
|
+
device: this,
|
|
336
|
+
ipAddress,
|
|
337
|
+
deviceData,
|
|
338
|
+
logger,
|
|
339
|
+
});
|
|
340
|
+
} catch (e) {
|
|
341
|
+
logger.warn("Failed to fetch device info", e?.message || String(e));
|
|
237
342
|
}
|
|
343
|
+
}
|
|
238
344
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
345
|
+
async getSettings(): Promise<Setting[]> {
|
|
346
|
+
const settings = await this.storageSettings.getSettings();
|
|
347
|
+
return settings;
|
|
348
|
+
}
|
|
242
349
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
350
|
+
async putSetting(key: string, value: SettingValue): Promise<void> {
|
|
351
|
+
return this.storageSettings.putSetting(key, value);
|
|
352
|
+
}
|
|
246
353
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
await api.collectNvrDiagnostics({
|
|
251
|
-
logger: this.console,
|
|
252
|
-
});
|
|
253
|
-
} catch (e) {
|
|
254
|
-
logger.error('Failed to run NVR diagnostics', e?.message || String(e));
|
|
255
|
-
throw e;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
354
|
+
async releaseDevice(id: string, nativeId: string) {
|
|
355
|
+
this.cameraNativeMap.delete(nativeId);
|
|
356
|
+
}
|
|
258
357
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
await this.subscribeToEvents();
|
|
262
|
-
await this.discoverDevices(true);
|
|
358
|
+
async getDevice(nativeId: string): Promise<ReolinkCamera> {
|
|
359
|
+
let device = this.cameraNativeMap.get(nativeId);
|
|
263
360
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
361
|
+
if (!device) {
|
|
362
|
+
if (nativeId.endsWith(batteryCameraSuffix)) {
|
|
363
|
+
device = new ReolinkCamera(nativeId, this.plugin, {
|
|
364
|
+
type: "battery",
|
|
365
|
+
nvrDevice: this,
|
|
366
|
+
});
|
|
367
|
+
} else if (nativeId.endsWith(batteryMultifocalSuffix)) {
|
|
368
|
+
device = new ReolinkNativeMultiFocalDevice(
|
|
369
|
+
nativeId,
|
|
370
|
+
this.plugin,
|
|
371
|
+
"multi-focal-battery",
|
|
372
|
+
this,
|
|
373
|
+
);
|
|
374
|
+
} else if (nativeId.endsWith(multifocalSuffix)) {
|
|
375
|
+
device = new ReolinkNativeMultiFocalDevice(
|
|
376
|
+
nativeId,
|
|
377
|
+
this.plugin,
|
|
378
|
+
"multi-focal",
|
|
379
|
+
this,
|
|
380
|
+
);
|
|
381
|
+
} else {
|
|
382
|
+
device = new ReolinkCamera(nativeId, this.plugin, {
|
|
383
|
+
type: "regular",
|
|
384
|
+
nvrDevice: this,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
285
387
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
388
|
+
if (device) {
|
|
389
|
+
this.cameraNativeMap.set(nativeId, device);
|
|
390
|
+
}
|
|
289
391
|
}
|
|
290
392
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
393
|
+
return device;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
buildNativeId(props: {
|
|
397
|
+
identifier?: string;
|
|
398
|
+
isBattery?: boolean;
|
|
399
|
+
isMultifocal?: boolean;
|
|
400
|
+
}): string {
|
|
401
|
+
const { identifier, isBattery, isMultifocal } = props;
|
|
402
|
+
|
|
403
|
+
const suffix = isBattery
|
|
404
|
+
? isMultifocal
|
|
405
|
+
? batteryMultifocalSuffix
|
|
406
|
+
: batteryCameraSuffix
|
|
407
|
+
: isMultifocal
|
|
408
|
+
? multifocalSuffix
|
|
409
|
+
: cameraSuffix;
|
|
410
|
+
|
|
411
|
+
return `${this.nativeId}-${identifier}${suffix}`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
getCameraInterfaces() {
|
|
415
|
+
return [
|
|
416
|
+
ScryptedInterface.VideoCameraConfiguration,
|
|
417
|
+
ScryptedInterface.Camera,
|
|
418
|
+
ScryptedInterface.MotionSensor,
|
|
419
|
+
ScryptedInterface.VideoTextOverlays,
|
|
420
|
+
ScryptedInterface.VideoCamera,
|
|
421
|
+
ScryptedInterface.Settings,
|
|
422
|
+
ScryptedInterface.ObjectDetector,
|
|
423
|
+
];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async syncEntitiesFromRemote() {
|
|
427
|
+
const logger = this.getBaichuanLogger();
|
|
428
|
+
// const { ipAddress } = this.storageSettings.values;
|
|
429
|
+
|
|
430
|
+
const api = await this.ensureBaichuanClient();
|
|
431
|
+
const { devices, channels } = await api.getNvrChannelsSummary({
|
|
432
|
+
source: "cgi",
|
|
433
|
+
});
|
|
294
434
|
|
|
295
|
-
|
|
296
|
-
|
|
435
|
+
if (!channels.length) {
|
|
436
|
+
logger.debug(
|
|
437
|
+
`No channels found, ${JSON.stringify({ channels, devices })}`,
|
|
438
|
+
);
|
|
439
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
440
|
+
await this.syncEntitiesFromRemote();
|
|
441
|
+
return;
|
|
297
442
|
}
|
|
298
443
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
444
|
+
logger.log(`Sync entities from remote for ${channels.length} channels`);
|
|
445
|
+
|
|
446
|
+
for (const deviceData of devices) {
|
|
447
|
+
const {
|
|
448
|
+
isBattery,
|
|
449
|
+
serialNumber,
|
|
450
|
+
name,
|
|
451
|
+
model,
|
|
452
|
+
isDoorbell,
|
|
453
|
+
uid,
|
|
454
|
+
channel,
|
|
455
|
+
isMultifocal,
|
|
456
|
+
} = deviceData;
|
|
457
|
+
const identifier = uid || name || `channel-${channel}`;
|
|
458
|
+
// const identifier = uid || mac || (ip !== ipAddress ? ip : undefined) || name || randomBytes(4).toString('hex');
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
const nativeId = this.buildNativeId({
|
|
462
|
+
isBattery,
|
|
463
|
+
isMultifocal,
|
|
464
|
+
identifier,
|
|
465
|
+
});
|
|
312
466
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
467
|
+
// Check if device already exists in cameraNativeMap with a different nativeId format
|
|
468
|
+
// (e.g., with chN- prefix). If so, use that nativeId for the mapping.
|
|
469
|
+
let actualNativeId = nativeId;
|
|
470
|
+
const existingDevice = Array.from(this.cameraNativeMap.entries()).find(
|
|
471
|
+
([id, camera]) => {
|
|
472
|
+
// Check if the camera matches by channel or UID
|
|
473
|
+
const cameraChannel = camera.storageSettings.values.rtspChannel;
|
|
474
|
+
const cameraUid = camera.storageSettings.values.uid;
|
|
475
|
+
return cameraChannel === channel || (uid && cameraUid === uid);
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
if (existingDevice) {
|
|
480
|
+
actualNativeId = existingDevice[0];
|
|
481
|
+
logger.debug(
|
|
482
|
+
`[syncEntities] Using existing nativeId for channel ${channel}: ${actualNativeId} (instead of ${nativeId})`,
|
|
483
|
+
);
|
|
316
484
|
}
|
|
317
485
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
buildNativeId(props: {
|
|
322
|
-
identifier?: string, isBattery?: boolean, isMultifocal?: boolean
|
|
323
|
-
}): string {
|
|
324
|
-
const { identifier, isBattery, isMultifocal } = props;
|
|
325
|
-
|
|
326
|
-
const suffix = isBattery ?
|
|
327
|
-
(isMultifocal ? batteryMultifocalSuffix : batteryCameraSuffix) :
|
|
328
|
-
(isMultifocal ? multifocalSuffix : cameraSuffix)
|
|
329
|
-
|
|
330
|
-
return `${this.nativeId}-${identifier}${suffix}`;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
getCameraInterfaces() {
|
|
334
|
-
return [
|
|
335
|
-
ScryptedInterface.VideoCameraConfiguration,
|
|
336
|
-
ScryptedInterface.Camera,
|
|
337
|
-
ScryptedInterface.MotionSensor,
|
|
338
|
-
ScryptedInterface.VideoTextOverlays,
|
|
339
|
-
ScryptedInterface.VideoCamera,
|
|
340
|
-
ScryptedInterface.Settings,
|
|
341
|
-
ScryptedInterface.ObjectDetector,
|
|
342
|
-
];
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async syncEntitiesFromRemote() {
|
|
346
|
-
const logger = this.getBaichuanLogger();
|
|
347
|
-
// const { ipAddress } = this.storageSettings.values;
|
|
348
|
-
|
|
349
|
-
const api = await this.ensureBaichuanClient();
|
|
350
|
-
const { devices, channels } = await api.getNvrChannelsSummary({ source: "cgi" });
|
|
351
|
-
|
|
352
|
-
if (!channels.length) {
|
|
353
|
-
logger.debug(`No channels found, ${JSON.stringify({ channels, devices })}`);
|
|
354
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
355
|
-
await this.syncEntitiesFromRemote();
|
|
356
|
-
return;
|
|
486
|
+
const interfaces = [ScryptedInterface.VideoCamera];
|
|
487
|
+
if (isBattery) {
|
|
488
|
+
interfaces.push(ScryptedInterface.Battery);
|
|
357
489
|
}
|
|
490
|
+
const type = isDoorbell
|
|
491
|
+
? ScryptedDeviceType.Doorbell
|
|
492
|
+
: ScryptedDeviceType.Camera;
|
|
493
|
+
|
|
494
|
+
const device: Device = {
|
|
495
|
+
nativeId,
|
|
496
|
+
name,
|
|
497
|
+
providerNativeId: this.nativeId,
|
|
498
|
+
interfaces,
|
|
499
|
+
type,
|
|
500
|
+
info: {
|
|
501
|
+
manufacturer: "Reolink",
|
|
502
|
+
model,
|
|
503
|
+
serialNumber,
|
|
504
|
+
},
|
|
505
|
+
};
|
|
358
506
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
507
|
+
this.channelToNativeIdMap.set(channel, actualNativeId);
|
|
508
|
+
|
|
509
|
+
const allNativeIds = sdk.deviceManager
|
|
510
|
+
.getNativeIds()
|
|
511
|
+
.filter((nid) => !!nid);
|
|
512
|
+
|
|
513
|
+
if (
|
|
514
|
+
allNativeIds.some(
|
|
515
|
+
(nid) =>
|
|
516
|
+
nid.includes(uid) ||
|
|
517
|
+
nid.includes(`channel-${channel}`) ||
|
|
518
|
+
// nid.includes(mac) ||
|
|
519
|
+
// nid.includes(ip) ||
|
|
520
|
+
nid.includes(name) ||
|
|
521
|
+
nid === nativeId,
|
|
522
|
+
)
|
|
523
|
+
) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
365
526
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
isBattery,
|
|
369
|
-
isMultifocal,
|
|
370
|
-
identifier,
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// Check if device already exists in cameraNativeMap with a different nativeId format
|
|
374
|
-
// (e.g., with chN- prefix). If so, use that nativeId for the mapping.
|
|
375
|
-
let actualNativeId = nativeId;
|
|
376
|
-
const existingDevice = Array.from(this.cameraNativeMap.entries()).find(([id, camera]) => {
|
|
377
|
-
// Check if the camera matches by channel or UID
|
|
378
|
-
const cameraChannel = camera.storageSettings.values.rtspChannel;
|
|
379
|
-
const cameraUid = camera.storageSettings.values.uid;
|
|
380
|
-
return cameraChannel === channel || (uid && cameraUid === uid);
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
if (existingDevice) {
|
|
384
|
-
actualNativeId = existingDevice[0];
|
|
385
|
-
logger.debug(`[syncEntities] Using existing nativeId for channel ${channel}: ${actualNativeId} (instead of ${nativeId})`);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const interfaces = [ScryptedInterface.VideoCamera];
|
|
389
|
-
if (isBattery) {
|
|
390
|
-
interfaces.push(ScryptedInterface.Battery);
|
|
391
|
-
}
|
|
392
|
-
const type = isDoorbell ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera;
|
|
393
|
-
|
|
394
|
-
const device: Device = {
|
|
395
|
-
nativeId,
|
|
396
|
-
name,
|
|
397
|
-
providerNativeId: this.nativeId,
|
|
398
|
-
interfaces,
|
|
399
|
-
type,
|
|
400
|
-
info: {
|
|
401
|
-
manufacturer: 'Reolink',
|
|
402
|
-
model,
|
|
403
|
-
serialNumber,
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
this.channelToNativeIdMap.set(channel, actualNativeId);
|
|
408
|
-
|
|
409
|
-
const allNativeIds = sdk.deviceManager.getNativeIds().filter(nid => !!nid);
|
|
410
|
-
|
|
411
|
-
if (
|
|
412
|
-
allNativeIds.some(
|
|
413
|
-
nid => nid.includes(uid) ||
|
|
414
|
-
nid.includes(`channel-${channel}`) ||
|
|
415
|
-
// nid.includes(mac) ||
|
|
416
|
-
// nid.includes(ip) ||
|
|
417
|
-
nid.includes(name) ||
|
|
418
|
-
nid === nativeId)
|
|
419
|
-
) {
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (this.discoveredDevices.has(nativeId)) {
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
this.discoveredDevices.set(nativeId, {
|
|
428
|
-
device,
|
|
429
|
-
description: `${name} (Channel ${channel})`,
|
|
430
|
-
rtspChannel: channel,
|
|
431
|
-
deviceData,
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
logger.debug(`Discovered channel ${channel}: ${name}`);
|
|
435
|
-
} catch (e: any) {
|
|
436
|
-
logger.debug(`Error processing channel ${channel}: ${e?.message || String(e)}`);
|
|
437
|
-
}
|
|
527
|
+
if (this.discoveredDevices.has(nativeId)) {
|
|
528
|
+
continue;
|
|
438
529
|
}
|
|
439
530
|
|
|
440
|
-
|
|
531
|
+
this.discoveredDevices.set(nativeId, {
|
|
532
|
+
device,
|
|
533
|
+
description: `${name} (Channel ${channel})`,
|
|
534
|
+
rtspChannel: channel,
|
|
535
|
+
deviceData,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
logger.debug(`Discovered channel ${channel}: ${name}`);
|
|
539
|
+
} catch (e: any) {
|
|
540
|
+
logger.debug(
|
|
541
|
+
`Error processing channel ${channel}: ${e?.message || String(e)}`,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
441
544
|
}
|
|
442
545
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
546
|
+
logger.debug(
|
|
547
|
+
`Channel discovery completed. ${JSON.stringify({ devices, channels })}`,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
448
550
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return [...this.discoveredDevices.values()].map(d => ({
|
|
455
|
-
...d.device,
|
|
456
|
-
description: d.description,
|
|
457
|
-
}));
|
|
458
|
-
} finally {
|
|
459
|
-
this.discoverDevicesPromise = undefined;
|
|
460
|
-
}
|
|
461
|
-
})();
|
|
462
|
-
return await this.discoverDevicesPromise;
|
|
463
|
-
}
|
|
551
|
+
async discoverDevices(scan?: boolean): Promise<DiscoveredDevice[]> {
|
|
552
|
+
// If a discovery is already in progress, return that promise
|
|
553
|
+
if (this.discoverDevicesPromise) {
|
|
554
|
+
return await this.discoverDevicesPromise;
|
|
555
|
+
}
|
|
464
556
|
|
|
465
|
-
|
|
466
|
-
|
|
557
|
+
// If scan is requested, start a new discovery
|
|
558
|
+
if (scan) {
|
|
559
|
+
this.discoverDevicesPromise = (async () => {
|
|
560
|
+
try {
|
|
561
|
+
await this.syncEntitiesFromRemote();
|
|
562
|
+
return [...this.discoveredDevices.values()].map((d) => ({
|
|
467
563
|
...d.device,
|
|
468
564
|
description: d.description,
|
|
469
|
-
|
|
565
|
+
}));
|
|
566
|
+
} finally {
|
|
567
|
+
this.discoverDevicesPromise = undefined;
|
|
568
|
+
}
|
|
569
|
+
})();
|
|
570
|
+
return await this.discoverDevicesPromise;
|
|
470
571
|
}
|
|
471
572
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
573
|
+
// If no scan requested, return cached devices immediately
|
|
574
|
+
return [...this.discoveredDevices.values()].map((d) => ({
|
|
575
|
+
...d.device,
|
|
576
|
+
description: d.description,
|
|
577
|
+
}));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async adoptDevice(adopt: AdoptDevice): Promise<string> {
|
|
581
|
+
const entry = this.discoveredDevices.get(adopt.nativeId);
|
|
582
|
+
|
|
583
|
+
if (!entry) throw new Error("device not found");
|
|
584
|
+
|
|
585
|
+
await this.onDeviceEvent(
|
|
586
|
+
ScryptedInterface.DeviceDiscovery,
|
|
587
|
+
await this.discoverDevices(),
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
const { uid } = entry.deviceData;
|
|
591
|
+
|
|
592
|
+
const { ReolinkBaichuanApi } =
|
|
593
|
+
await import("@apocaliss92/reolink-baichuan-js");
|
|
594
|
+
const transport = "tcp";
|
|
595
|
+
const baichuanApi = new ReolinkBaichuanApi({
|
|
596
|
+
host: this.storageSettings.values.ipAddress,
|
|
597
|
+
username: this.storageSettings.values.username,
|
|
598
|
+
password: this.storageSettings.values.password,
|
|
599
|
+
transport,
|
|
600
|
+
channel: entry.rtspChannel,
|
|
601
|
+
uid,
|
|
602
|
+
});
|
|
603
|
+
await baichuanApi.login();
|
|
604
|
+
const { capabilities, objects, presets } =
|
|
605
|
+
await baichuanApi.getDeviceCapabilities(entry.rtspChannel);
|
|
606
|
+
const { interfaces, type } = getDeviceInterfaces({
|
|
607
|
+
capabilities,
|
|
608
|
+
logger: this.getBaichuanLogger(),
|
|
609
|
+
});
|
|
507
610
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
611
|
+
const actualDevice: Device = {
|
|
612
|
+
...entry.device,
|
|
613
|
+
providerNativeId: this.nativeId,
|
|
614
|
+
interfaces,
|
|
615
|
+
type,
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
await sdk.deviceManager.onDeviceDiscovered(actualDevice);
|
|
619
|
+
|
|
620
|
+
const device = await this.getDevice(adopt.nativeId);
|
|
621
|
+
const logger = this.getBaichuanLogger();
|
|
622
|
+
logger.log("Adopted device", device?.name, JSON.stringify(actualDevice));
|
|
623
|
+
const { username, password, ipAddress } = this.storageSettings.values;
|
|
624
|
+
|
|
625
|
+
device.storageSettings.values.rtspChannel = entry.rtspChannel;
|
|
626
|
+
device.classes = objects;
|
|
627
|
+
device.presets = presets;
|
|
628
|
+
device.storageSettings.values.username = username;
|
|
629
|
+
device.storageSettings.values.password = password;
|
|
630
|
+
device.storageSettings.values.rtspChannel = entry.rtspChannel;
|
|
631
|
+
device.storageSettings.values.ipAddress = ipAddress;
|
|
632
|
+
device.storageSettings.values.uid = uid;
|
|
633
|
+
|
|
634
|
+
device.cachedCapabilities = capabilities;
|
|
635
|
+
|
|
636
|
+
this.discoveredDevices.delete(adopt.nativeId);
|
|
637
|
+
return device?.id;
|
|
638
|
+
}
|
|
527
639
|
}
|
|
528
|
-
|