@apocaliss92/scrypted-reolink-native 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/common.ts +22 -10
- package/src/connect.ts +39 -51
- package/src/main.ts +59 -16
- package/src/multifocal.ts +449 -0
- package/src/nvr.ts +65 -15
- package/src/utils.ts +6 -1
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import type { DeviceInfoResponse, ReolinkBaichuanApi, ReolinkCgiApi, ReolinkSimpleEvent } 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 { BaseBaichuanClass, type BaichuanConnectionConfig, type BaichuanConnectionCallbacks } from "./baichuan-base";
|
|
5
|
+
import { ReolinkNativeCamera } from "./camera";
|
|
6
|
+
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
7
|
+
import { normalizeUid, type BaichuanTransport } from "./connect";
|
|
8
|
+
import ReolinkNativePlugin from "./main";
|
|
9
|
+
import { getDeviceInterfaces, updateDeviceInfo } from "./utils";
|
|
10
|
+
|
|
11
|
+
export class ReolinkNativeMultiFocalDevice extends BaseBaichuanClass implements Settings, DeviceDiscovery, 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
|
+
diagnosticsRun: {
|
|
43
|
+
subgroup: 'Diagnostics',
|
|
44
|
+
title: 'Run NVR Diagnostics',
|
|
45
|
+
description: 'Collect NVR diagnostics and display results in logs.',
|
|
46
|
+
type: 'button',
|
|
47
|
+
immediate: true,
|
|
48
|
+
onPut: async () => {
|
|
49
|
+
await this.runNvrDiagnostics();
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
plugin: ReolinkNativePlugin;
|
|
54
|
+
protected readonly protocol: BaichuanTransport;
|
|
55
|
+
discoveredDevices = new Map<string, {
|
|
56
|
+
device: Device;
|
|
57
|
+
description: string;
|
|
58
|
+
rtspChannel: number;
|
|
59
|
+
deviceData: DeviceInfoResponse;
|
|
60
|
+
}>();
|
|
61
|
+
cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
|
|
62
|
+
private channelToNativeIdMap = new Map<number, string>();
|
|
63
|
+
processing = false;
|
|
64
|
+
private initReinitTimeout: NodeJS.Timeout | undefined;
|
|
65
|
+
|
|
66
|
+
constructor(nativeId: string, plugin: ReolinkNativePlugin, transport: BaichuanTransport = 'tcp') {
|
|
67
|
+
super(nativeId);
|
|
68
|
+
this.plugin = plugin;
|
|
69
|
+
this.protocol = transport;
|
|
70
|
+
|
|
71
|
+
this.scheduleInit();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async reboot(): Promise<void> {
|
|
75
|
+
const api = await this.ensureBaichuanClient();
|
|
76
|
+
await api.reboot();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// BaseBaichuanClass abstract methods implementation
|
|
80
|
+
protected getConnectionConfig(): BaichuanConnectionConfig {
|
|
81
|
+
const { ipAddress, username, password, uid } = this.storageSettings.values;
|
|
82
|
+
if (!ipAddress || !username || !password) {
|
|
83
|
+
throw new Error('Missing device credentials');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const normalizedUid = this.protocol === 'udp' ? normalizeUid(uid) : undefined;
|
|
87
|
+
|
|
88
|
+
if (this.protocol === 'udp' && !normalizedUid) {
|
|
89
|
+
throw new Error('UID is required for UDP multi-focal devices (BCUDP)');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
host: ipAddress,
|
|
94
|
+
username,
|
|
95
|
+
password,
|
|
96
|
+
uid: normalizedUid,
|
|
97
|
+
transport: this.protocol,
|
|
98
|
+
logger: this.console,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
|
|
103
|
+
return {
|
|
104
|
+
onError: undefined, // Use default error handling
|
|
105
|
+
onClose: async () => {
|
|
106
|
+
// Reinit after cleanup
|
|
107
|
+
await this.reinit();
|
|
108
|
+
},
|
|
109
|
+
onSimpleEvent: (ev) => this.forwardNativeEvent(ev),
|
|
110
|
+
getEventSubscriptionEnabled: () => true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected isDebugEnabled(): boolean {
|
|
115
|
+
return this.storageSettings.values.debugEvents;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
protected getDeviceName(): string {
|
|
119
|
+
return this.name || 'Multi-Focal Device';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async reinit(): Promise<void> {
|
|
123
|
+
// Cancel any pending init/reinit
|
|
124
|
+
if (this.initReinitTimeout) {
|
|
125
|
+
clearTimeout(this.initReinitTimeout);
|
|
126
|
+
this.initReinitTimeout = undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Schedule reinit with debounce
|
|
130
|
+
this.scheduleInit(true);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private scheduleInit(isReinit: boolean = false): void {
|
|
134
|
+
// Cancel any pending init/reinit
|
|
135
|
+
if (this.initReinitTimeout) {
|
|
136
|
+
clearTimeout(this.initReinitTimeout);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.initReinitTimeout = setTimeout(async () => {
|
|
140
|
+
const logger = this.getBaichuanLogger();
|
|
141
|
+
if (isReinit) {
|
|
142
|
+
logger.log('Reinitializing multi-focal device...');
|
|
143
|
+
await this.cleanupBaichuanApi();
|
|
144
|
+
}
|
|
145
|
+
await this.init();
|
|
146
|
+
this.initReinitTimeout = undefined;
|
|
147
|
+
}, isReinit ? 500 : 2000);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async init(): Promise<void> {
|
|
151
|
+
const logger = this.getBaichuanLogger();
|
|
152
|
+
try {
|
|
153
|
+
// Update UID setting visibility based on transport
|
|
154
|
+
this.storageSettings.settings.uid.hide = this.protocol === 'tcp';
|
|
155
|
+
|
|
156
|
+
logger.debug('Initializing: ensuring Baichuan client...');
|
|
157
|
+
await this.ensureBaichuanClient();
|
|
158
|
+
|
|
159
|
+
logger.debug('Initializing: updating device info...');
|
|
160
|
+
await this.updateDeviceInfo();
|
|
161
|
+
|
|
162
|
+
logger.debug('Initializing: subscribing to events...');
|
|
163
|
+
await this.subscribeToEvents();
|
|
164
|
+
|
|
165
|
+
logger.debug('Initializing: discovering devices...');
|
|
166
|
+
await this.discoverDevices(true);
|
|
167
|
+
|
|
168
|
+
logger.log('Initialization completed successfully');
|
|
169
|
+
} catch (e) {
|
|
170
|
+
logger.error('Failed to initialize multi-focal device', e);
|
|
171
|
+
// Log more details about the error
|
|
172
|
+
if (e instanceof Error) {
|
|
173
|
+
logger.error(`Error message: ${e.message}`);
|
|
174
|
+
logger.error(`Error stack: ${e.stack}`);
|
|
175
|
+
} else {
|
|
176
|
+
logger.error(`Error details: ${JSON.stringify(e)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async updateDeviceInfo(): Promise<void> {
|
|
182
|
+
const logger = this.getBaichuanLogger();
|
|
183
|
+
try {
|
|
184
|
+
const api = await this.ensureBaichuanClient();
|
|
185
|
+
const deviceData = await api.getInfo();
|
|
186
|
+
|
|
187
|
+
await updateDeviceInfo({
|
|
188
|
+
device: this,
|
|
189
|
+
deviceData,
|
|
190
|
+
ipAddress: this.storageSettings.values.ipAddress,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
logger.log(`Device info updated: ${JSON.stringify(deviceData)}`);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
logger.warn('Failed to fetch device info', e);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async syncEntitiesFromRemote() {
|
|
200
|
+
const api = await this.ensureBaichuanClient();
|
|
201
|
+
const logger = this.getBaichuanLogger();
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const channelsInfo = await api.getNvrChannelsInfo();
|
|
205
|
+
const deviceInfo = await api.getInfo();
|
|
206
|
+
const { support } = await api.getDeviceCapabilities();
|
|
207
|
+
const channelNum = support?.channelNum ?? 1;
|
|
208
|
+
logger.log(`Sync entities from remote for ${channelNum} channels`);
|
|
209
|
+
const channels = Array.from({ length: channelNum }, (_, i) => i + 1);
|
|
210
|
+
|
|
211
|
+
logger.log(JSON.stringify({ channelsInfo, deviceInfo, channels }));
|
|
212
|
+
|
|
213
|
+
// for (const channel of channels) {
|
|
214
|
+
// try {
|
|
215
|
+
// const name = deviceInfo?.name || `Channel ${channel}`;
|
|
216
|
+
// const uid = deviceInfo?.uid;
|
|
217
|
+
// const isBattery = !!(abilities?.battery?.ver ?? 0);
|
|
218
|
+
|
|
219
|
+
// const nativeId = this.buildNativeId(channel, uid, isBattery);
|
|
220
|
+
// const interfaces = [ScryptedInterface.VideoCamera];
|
|
221
|
+
// if (isBattery) {
|
|
222
|
+
// interfaces.push(ScryptedInterface.Battery);
|
|
223
|
+
// }
|
|
224
|
+
// const type = abilities.supportDoorbellLight ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera;
|
|
225
|
+
|
|
226
|
+
// const device: Device = {
|
|
227
|
+
// nativeId,
|
|
228
|
+
// name,
|
|
229
|
+
// providerNativeId: this.nativeId,
|
|
230
|
+
// interfaces,
|
|
231
|
+
// type,
|
|
232
|
+
// info: {
|
|
233
|
+
// manufacturer: 'Reolink',
|
|
234
|
+
// model: channelInfo?.typeInfo,
|
|
235
|
+
// serialNumber: uid,
|
|
236
|
+
// }
|
|
237
|
+
// };
|
|
238
|
+
|
|
239
|
+
// this.channelToNativeIdMap.set(channel, nativeId);
|
|
240
|
+
|
|
241
|
+
// if (sdk.deviceManager.getNativeIds().includes(nativeId)) {
|
|
242
|
+
// continue;
|
|
243
|
+
// }
|
|
244
|
+
|
|
245
|
+
// if (this.discoveredDevices.has(nativeId)) {
|
|
246
|
+
// continue;
|
|
247
|
+
// }
|
|
248
|
+
|
|
249
|
+
// this.discoveredDevices.set(nativeId, {
|
|
250
|
+
// device,
|
|
251
|
+
// description: `${name} (Channel ${channel})`,
|
|
252
|
+
// rtspChannel: channel,
|
|
253
|
+
// deviceData: devicesData[channel],
|
|
254
|
+
// });
|
|
255
|
+
|
|
256
|
+
// logger.debug(`Discovered channel ${channel}: ${name}`);
|
|
257
|
+
// } catch (e: any) {
|
|
258
|
+
// logger.debug(`Error processing channel ${channel}: ${e?.message || String(e)}`);
|
|
259
|
+
// }
|
|
260
|
+
// }
|
|
261
|
+
|
|
262
|
+
// logger.log(`Channel discovery completed. ${JSON.stringify({ devicesData, channels })}`);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
logger.error('Failed to sync entities from remote', e);
|
|
265
|
+
if (e instanceof Error) {
|
|
266
|
+
logger.error(`Error in syncEntitiesFromRemote: ${e.message}`);
|
|
267
|
+
logger.error(`Stack: ${e.stack}`);
|
|
268
|
+
} else {
|
|
269
|
+
logger.error(`Error details: ${JSON.stringify(e)}`);
|
|
270
|
+
}
|
|
271
|
+
throw e;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async discoverDevices(scan?: boolean): Promise<DiscoveredDevice[]> {
|
|
276
|
+
if (scan) {
|
|
277
|
+
await this.syncEntitiesFromRemote();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return [...this.discoveredDevices.values()].map(d => ({
|
|
281
|
+
...d.device,
|
|
282
|
+
description: d.description,
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async adoptDevice(adopt: AdoptDevice): Promise<string> {
|
|
287
|
+
const entry = this.discoveredDevices.get(adopt.nativeId);
|
|
288
|
+
|
|
289
|
+
if (!entry)
|
|
290
|
+
throw new Error('device not found');
|
|
291
|
+
|
|
292
|
+
await this.onDeviceEvent(ScryptedInterface.DeviceDiscovery, await this.discoverDevices());
|
|
293
|
+
|
|
294
|
+
const isBattery = entry.device.interfaces.includes(ScryptedInterface.Battery);
|
|
295
|
+
const { channelStatus } = entry.deviceData;
|
|
296
|
+
|
|
297
|
+
const { ReolinkBaichuanApi } = await import("@apocaliss92/reolink-baichuan-js");
|
|
298
|
+
const transport = this.protocol;
|
|
299
|
+
const uid = channelStatus?.uid || this.storageSettings.values.uid;
|
|
300
|
+
// For battery cameras or UDP transport, use UID if available
|
|
301
|
+
const normalizedUid = (isBattery || transport === 'udp') && uid ? normalizeUid(uid) : undefined;
|
|
302
|
+
const baichuanApi = new ReolinkBaichuanApi({
|
|
303
|
+
host: this.storageSettings.values.ipAddress,
|
|
304
|
+
username: this.storageSettings.values.username,
|
|
305
|
+
password: this.storageSettings.values.password,
|
|
306
|
+
transport,
|
|
307
|
+
channel: entry.rtspChannel,
|
|
308
|
+
...(normalizedUid ? { uid: normalizedUid } : {}),
|
|
309
|
+
});
|
|
310
|
+
await baichuanApi.login();
|
|
311
|
+
const { capabilities, objects, presets } = await baichuanApi.getDeviceCapabilities(entry.rtspChannel);
|
|
312
|
+
const { interfaces, type } = getDeviceInterfaces({
|
|
313
|
+
capabilities,
|
|
314
|
+
logger: this.console,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const actualDevice: Device = {
|
|
318
|
+
...entry.device,
|
|
319
|
+
interfaces,
|
|
320
|
+
type
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
await sdk.deviceManager.onDeviceDiscovered(actualDevice);
|
|
324
|
+
|
|
325
|
+
const device = await this.getDevice(adopt.nativeId);
|
|
326
|
+
if (device instanceof ReolinkNativeCamera || device instanceof ReolinkNativeBatteryCamera) {
|
|
327
|
+
device.storageSettings.values.ipAddress = this.storageSettings.values.ipAddress;
|
|
328
|
+
device.storageSettings.values.username = this.storageSettings.values.username;
|
|
329
|
+
device.storageSettings.values.password = this.storageSettings.values.password;
|
|
330
|
+
device.storageSettings.values.rtspChannel = entry.rtspChannel;
|
|
331
|
+
// Set multiFocalDevice reference through options (similar to how NVR does it)
|
|
332
|
+
(device).options = { ...(device).options, multiFocalDevice: this, nvrDevice: undefined };
|
|
333
|
+
device.classes = objects;
|
|
334
|
+
device.presets = presets;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return adopt.nativeId;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async getDevice(nativeId: string): Promise<ReolinkNativeCamera | ReolinkNativeBatteryCamera> {
|
|
341
|
+
if (this.cameraNativeMap.has(nativeId)) {
|
|
342
|
+
return this.cameraNativeMap.get(nativeId)!;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const entry = this.discoveredDevices.get(nativeId);
|
|
346
|
+
if (!entry) {
|
|
347
|
+
throw new Error(`Device ${nativeId} not found`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const isBattery = entry.device.interfaces.includes(ScryptedInterface.Battery);
|
|
351
|
+
const cameraNativeId = entry.device.nativeId;
|
|
352
|
+
const camera = isBattery
|
|
353
|
+
? new ReolinkNativeBatteryCamera(cameraNativeId, this.plugin, { type: 'battery', multiFocalDevice: this, nvrDevice: undefined })
|
|
354
|
+
: new ReolinkNativeCamera(cameraNativeId, this.plugin, { type: 'regular', multiFocalDevice: this, nvrDevice: undefined });
|
|
355
|
+
|
|
356
|
+
this.cameraNativeMap.set(nativeId, camera);
|
|
357
|
+
return camera;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async getSettings(): Promise<Setting[]> {
|
|
361
|
+
const settings = await this.storageSettings.getSettings();
|
|
362
|
+
return settings;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async putSetting(key: string, value: SettingValue): Promise<void> {
|
|
366
|
+
return this.storageSettings.putSetting(key, value);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async releaseDevice(id: string, nativeId: string) {
|
|
370
|
+
this.cameraNativeMap.delete(nativeId);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
buildNativeId(channel: number, uid?: string, isBattery?: boolean): string {
|
|
374
|
+
const serialNumber = uid || this.storageSettings.values.ipAddress || 'unknown';
|
|
375
|
+
const suffix = isBattery ? '-battery-cam' : '-cam';
|
|
376
|
+
return `${serialNumber}-channel${channel}${suffix}`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
forwardNativeEvent(ev: ReolinkSimpleEvent): void {
|
|
380
|
+
if (this.processing) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const logger = this.getBaichuanLogger();
|
|
385
|
+
const channel = ev?.channel;
|
|
386
|
+
|
|
387
|
+
if (channel === undefined) {
|
|
388
|
+
logger.debug('Event missing channel, ignoring');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const nativeId = this.channelToNativeIdMap.get(channel);
|
|
393
|
+
if (!nativeId) {
|
|
394
|
+
logger.error(`No camera found for channel ${channel}, ignoring event`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const camera = this.cameraNativeMap.get(nativeId);
|
|
399
|
+
if (!camera) {
|
|
400
|
+
logger.debug(`Camera ${nativeId} not yet initialized, ignoring event`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Forward event to camera
|
|
405
|
+
if (camera.onSimpleEvent) {
|
|
406
|
+
camera.onSimpleEvent(ev);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async subscribeToAllEvents(): Promise<void> {
|
|
411
|
+
const logger = this.getBaichuanLogger();
|
|
412
|
+
logger.log('Subscribed to all events for multi-focal device cameras');
|
|
413
|
+
await this.subscribeToEvents();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private async runNvrDiagnostics(): Promise<void> {
|
|
417
|
+
const logger = this.getBaichuanLogger();
|
|
418
|
+
logger.log(`Starting NVR diagnostics...`);
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const { ipAddress, username, password } = this.storageSettings.values;
|
|
422
|
+
if (!ipAddress || !username || !password) {
|
|
423
|
+
throw new Error('Missing device credentials');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const { ReolinkCgiApi } = await import("@apocaliss92/reolink-baichuan-js");
|
|
427
|
+
const cgiApi = new ReolinkCgiApi({
|
|
428
|
+
host: ipAddress,
|
|
429
|
+
username,
|
|
430
|
+
password,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
await cgiApi.login();
|
|
434
|
+
|
|
435
|
+
const diagnostics = await cgiApi.collectNvrDiagnostics({
|
|
436
|
+
logger: this.console,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
logger.log(`NVR diagnostics completed successfully.`);
|
|
440
|
+
|
|
441
|
+
// Print diagnostics to console
|
|
442
|
+
cgiApi.printNvrDiagnostics(diagnostics, this.console);
|
|
443
|
+
} catch (e) {
|
|
444
|
+
logger.error('Failed to run NVR diagnostics', e);
|
|
445
|
+
throw e;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
package/src/nvr.ts
CHANGED
|
@@ -43,6 +43,16 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
43
43
|
type: 'password',
|
|
44
44
|
onPut: async () => await this.reinit()
|
|
45
45
|
},
|
|
46
|
+
diagnosticsRun: {
|
|
47
|
+
subgroup: 'Diagnostics',
|
|
48
|
+
title: 'Run NVR Diagnostics',
|
|
49
|
+
description: 'Collect NVR diagnostics and display results in logs.',
|
|
50
|
+
type: 'button',
|
|
51
|
+
immediate: true,
|
|
52
|
+
onPut: async () => {
|
|
53
|
+
await this.runNvrDiagnostics();
|
|
54
|
+
},
|
|
55
|
+
},
|
|
46
56
|
});
|
|
47
57
|
plugin: ReolinkNativePlugin;
|
|
48
58
|
nvrApi: ReolinkCgiApi | undefined;
|
|
@@ -59,14 +69,13 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
59
69
|
cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
|
|
60
70
|
private channelToNativeIdMap = new Map<number, string>();
|
|
61
71
|
processing = false;
|
|
72
|
+
private initReinitTimeout: NodeJS.Timeout | undefined;
|
|
62
73
|
|
|
63
74
|
constructor(nativeId: string, plugin: ReolinkNativePlugin) {
|
|
64
75
|
super(nativeId);
|
|
65
76
|
this.plugin = plugin;
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
await this.init();
|
|
69
|
-
}, 5000);
|
|
78
|
+
this.scheduleInit();
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
async reboot(): Promise<void> {
|
|
@@ -120,18 +129,40 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
async reinit() {
|
|
123
|
-
//
|
|
124
|
-
if (this.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
} catch {
|
|
128
|
-
// ignore
|
|
129
|
-
}
|
|
132
|
+
// Cancel any pending init/reinit
|
|
133
|
+
if (this.initReinitTimeout) {
|
|
134
|
+
clearTimeout(this.initReinitTimeout);
|
|
135
|
+
this.initReinitTimeout = undefined;
|
|
130
136
|
}
|
|
131
|
-
this.nvrApi = undefined;
|
|
132
137
|
|
|
133
|
-
//
|
|
134
|
-
|
|
138
|
+
// Schedule reinit with debounce
|
|
139
|
+
this.scheduleInit(true);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private scheduleInit(isReinit: boolean = false): void {
|
|
143
|
+
// Cancel any pending init/reinit
|
|
144
|
+
if (this.initReinitTimeout) {
|
|
145
|
+
clearTimeout(this.initReinitTimeout);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.initReinitTimeout = setTimeout(async () => {
|
|
149
|
+
if (isReinit) {
|
|
150
|
+
// Cleanup CGI API
|
|
151
|
+
if (this.nvrApi) {
|
|
152
|
+
try {
|
|
153
|
+
await this.nvrApi.logout();
|
|
154
|
+
} catch {
|
|
155
|
+
// ignore
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
this.nvrApi = undefined;
|
|
159
|
+
|
|
160
|
+
// Cleanup Baichuan API (this handles all listeners and connection)
|
|
161
|
+
await super.cleanupBaichuanApi();
|
|
162
|
+
}
|
|
163
|
+
await this.init();
|
|
164
|
+
this.initReinitTimeout = undefined;
|
|
165
|
+
}, isReinit ? 500 : 2000);
|
|
135
166
|
}
|
|
136
167
|
|
|
137
168
|
async ensureClient(): Promise<ReolinkCgiApi> {
|
|
@@ -177,7 +208,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
177
208
|
const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
|
|
178
209
|
|
|
179
210
|
if (!targetCamera) {
|
|
180
|
-
logger.
|
|
211
|
+
logger.info(`No camera found for channel ${channel}, ignoring event`);
|
|
181
212
|
return;
|
|
182
213
|
}
|
|
183
214
|
|
|
@@ -245,6 +276,26 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
245
276
|
logger.log('Subscribed to all events for NVR cameras');
|
|
246
277
|
}
|
|
247
278
|
|
|
279
|
+
private async runNvrDiagnostics(): Promise<void> {
|
|
280
|
+
const logger = this.getBaichuanLogger();
|
|
281
|
+
logger.log(`Starting NVR diagnostics...`);
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const cgiApi = await this.ensureClient();
|
|
285
|
+
|
|
286
|
+
const diagnostics = await cgiApi.collectNvrDiagnostics({
|
|
287
|
+
logger: this.console,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
logger.log(`NVR diagnostics completed successfully.`);
|
|
291
|
+
|
|
292
|
+
cgiApi.printNvrDiagnostics(diagnostics, this.console);
|
|
293
|
+
} catch (e) {
|
|
294
|
+
logger.error('Failed to run NVR diagnostics', e);
|
|
295
|
+
throw e;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
248
299
|
async unsubscribeFromAllEvents(): Promise<void> {
|
|
249
300
|
// Use base class implementation
|
|
250
301
|
await super.unsubscribeFromEvents();
|
|
@@ -544,7 +595,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
544
595
|
device.storageSettings.values.ipAddress = ipAddress;
|
|
545
596
|
device.storageSettings.values.capabilities = capabilities;
|
|
546
597
|
device.storageSettings.values.uid = entry.deviceData.channelStatus.uid;
|
|
547
|
-
device.storageSettings.values.isFromNvr = true;
|
|
548
598
|
|
|
549
599
|
this.discoveredDevices.delete(adopt.nativeId);
|
|
550
600
|
return device?.id;
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { DeviceCapabilities, ReolinkDeviceInfo } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
|
-
import { DeviceBase,
|
|
2
|
+
import { DeviceBase, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
|
|
3
|
+
|
|
4
|
+
export const nvrSuffix = `-nvr`;
|
|
5
|
+
export const batteryCameraSuffix = `-battery-cam`;
|
|
6
|
+
export const multifocalSuffix = `-multifocal`;
|
|
7
|
+
export const cameraSuffix = `-cam`;
|
|
3
8
|
|
|
4
9
|
export const getDeviceInterfaces = (props: {
|
|
5
10
|
capabilities: DeviceCapabilities,
|