@apocaliss92/scrypted-reolink-native 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/common.ts +55 -28
- package/src/connect.ts +38 -50
- package/src/main.ts +59 -16
- package/src/multifocal.ts +398 -0
- package/src/nvr.ts +49 -20
- package/src/utils.ts +6 -1
|
@@ -0,0 +1,398 @@
|
|
|
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
|
+
|
|
65
|
+
constructor(nativeId: string, plugin: ReolinkNativePlugin, transport: BaichuanTransport = 'tcp') {
|
|
66
|
+
super(nativeId);
|
|
67
|
+
this.plugin = plugin;
|
|
68
|
+
this.protocol = transport;
|
|
69
|
+
|
|
70
|
+
setTimeout(async () => {
|
|
71
|
+
await this.init();
|
|
72
|
+
}, 2000);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async reboot(): Promise<void> {
|
|
76
|
+
const api = await this.ensureBaichuanClient();
|
|
77
|
+
await api.reboot();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// BaseBaichuanClass abstract methods implementation
|
|
81
|
+
protected getConnectionConfig(): BaichuanConnectionConfig {
|
|
82
|
+
const { ipAddress, username, password, uid } = this.storageSettings.values;
|
|
83
|
+
if (!ipAddress || !username || !password) {
|
|
84
|
+
throw new Error('Missing device credentials');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const normalizedUid = this.protocol === 'udp' ? normalizeUid(uid) : undefined;
|
|
88
|
+
|
|
89
|
+
if (this.protocol === 'udp' && !normalizedUid) {
|
|
90
|
+
throw new Error('UID is required for UDP multi-focal devices (BCUDP)');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
host: ipAddress,
|
|
95
|
+
username,
|
|
96
|
+
password,
|
|
97
|
+
uid: normalizedUid,
|
|
98
|
+
transport: this.protocol,
|
|
99
|
+
logger: this.console,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
|
|
104
|
+
return {
|
|
105
|
+
onError: undefined, // Use default error handling
|
|
106
|
+
onClose: async () => {
|
|
107
|
+
// Reinit after cleanup
|
|
108
|
+
await this.reinit();
|
|
109
|
+
},
|
|
110
|
+
onSimpleEvent: (ev) => this.forwardNativeEvent(ev),
|
|
111
|
+
getEventSubscriptionEnabled: () => true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
protected isDebugEnabled(): boolean {
|
|
116
|
+
return this.storageSettings.values.debugEvents;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
protected getDeviceName(): string {
|
|
120
|
+
return this.name || 'Multi-Focal Device';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async reinit(): Promise<void> {
|
|
124
|
+
const logger = this.getBaichuanLogger();
|
|
125
|
+
logger.log('Reinitializing multi-focal device...');
|
|
126
|
+
await this.cleanupBaichuanApi();
|
|
127
|
+
await this.init();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async init(): Promise<void> {
|
|
131
|
+
const logger = this.getBaichuanLogger();
|
|
132
|
+
try {
|
|
133
|
+
// Update UID setting visibility based on transport
|
|
134
|
+
this.storageSettings.settings.uid.hide = this.protocol === 'tcp';
|
|
135
|
+
|
|
136
|
+
await this.ensureBaichuanClient();
|
|
137
|
+
await this.updateDeviceInfo();
|
|
138
|
+
await this.subscribeToEvents();
|
|
139
|
+
await this.discoverDevices(true);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
logger.error('Failed to initialize multi-focal device', e);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async updateDeviceInfo(): Promise<void> {
|
|
146
|
+
const logger = this.getBaichuanLogger();
|
|
147
|
+
try {
|
|
148
|
+
const api = await this.ensureBaichuanClient();
|
|
149
|
+
const deviceData = await api.getInfo();
|
|
150
|
+
|
|
151
|
+
await updateDeviceInfo({
|
|
152
|
+
device: this,
|
|
153
|
+
deviceData,
|
|
154
|
+
ipAddress: this.storageSettings.values.ipAddress,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
logger.log(`Device info updated: ${JSON.stringify(deviceData)}`);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
logger.warn('Failed to fetch device info', e);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async syncEntitiesFromRemote() {
|
|
164
|
+
const api = await this.ensureBaichuanClient();
|
|
165
|
+
const logger = this.getBaichuanLogger();
|
|
166
|
+
|
|
167
|
+
const { devicesData, channels } = await (api as any).getDevicesInfo();
|
|
168
|
+
logger.log(`Sync entities from remote for ${channels.length} channels`);
|
|
169
|
+
|
|
170
|
+
// Process each channel that was successfully discovered
|
|
171
|
+
for (const channel of channels) {
|
|
172
|
+
try {
|
|
173
|
+
const { channelStatus, channelInfo, abilities } = devicesData[channel];
|
|
174
|
+
const name = channelStatus?.name || `Channel ${channel}`;
|
|
175
|
+
const uid = channelStatus?.uid;
|
|
176
|
+
const isBattery = !!(abilities?.battery?.ver ?? 0);
|
|
177
|
+
|
|
178
|
+
const nativeId = this.buildNativeId(channel, uid, isBattery);
|
|
179
|
+
const interfaces = [ScryptedInterface.VideoCamera];
|
|
180
|
+
if (isBattery) {
|
|
181
|
+
interfaces.push(ScryptedInterface.Battery);
|
|
182
|
+
}
|
|
183
|
+
const type = abilities.supportDoorbellLight ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera;
|
|
184
|
+
|
|
185
|
+
const device: Device = {
|
|
186
|
+
nativeId,
|
|
187
|
+
name,
|
|
188
|
+
providerNativeId: this.nativeId,
|
|
189
|
+
interfaces,
|
|
190
|
+
type,
|
|
191
|
+
info: {
|
|
192
|
+
manufacturer: 'Reolink',
|
|
193
|
+
model: channelInfo?.typeInfo,
|
|
194
|
+
serialNumber: uid,
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
this.channelToNativeIdMap.set(channel, nativeId);
|
|
199
|
+
|
|
200
|
+
if (sdk.deviceManager.getNativeIds().includes(nativeId)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (this.discoveredDevices.has(nativeId)) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.discoveredDevices.set(nativeId, {
|
|
209
|
+
device,
|
|
210
|
+
description: `${name} (Channel ${channel})`,
|
|
211
|
+
rtspChannel: channel,
|
|
212
|
+
deviceData: devicesData[channel],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
logger.debug(`Discovered channel ${channel}: ${name}`);
|
|
216
|
+
} catch (e: any) {
|
|
217
|
+
logger.debug(`Error processing channel ${channel}: ${e?.message || String(e)}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
logger.log(`Channel discovery completed. ${JSON.stringify({ devicesData, channels })}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async discoverDevices(scan?: boolean): Promise<DiscoveredDevice[]> {
|
|
225
|
+
if (scan) {
|
|
226
|
+
await this.syncEntitiesFromRemote();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return [...this.discoveredDevices.values()].map(d => ({
|
|
230
|
+
...d.device,
|
|
231
|
+
description: d.description,
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async adoptDevice(adopt: AdoptDevice): Promise<string> {
|
|
236
|
+
const entry = this.discoveredDevices.get(adopt.nativeId);
|
|
237
|
+
|
|
238
|
+
if (!entry)
|
|
239
|
+
throw new Error('device not found');
|
|
240
|
+
|
|
241
|
+
await this.onDeviceEvent(ScryptedInterface.DeviceDiscovery, await this.discoverDevices());
|
|
242
|
+
|
|
243
|
+
const isBattery = entry.device.interfaces.includes(ScryptedInterface.Battery);
|
|
244
|
+
const { channelStatus } = entry.deviceData;
|
|
245
|
+
|
|
246
|
+
const { ReolinkBaichuanApi } = await import("@apocaliss92/reolink-baichuan-js");
|
|
247
|
+
const transport = this.protocol;
|
|
248
|
+
const uid = channelStatus?.uid || this.storageSettings.values.uid;
|
|
249
|
+
// For battery cameras or UDP transport, use UID if available
|
|
250
|
+
const normalizedUid = (isBattery || transport === 'udp') && uid ? normalizeUid(uid) : undefined;
|
|
251
|
+
const baichuanApi = new ReolinkBaichuanApi({
|
|
252
|
+
host: this.storageSettings.values.ipAddress,
|
|
253
|
+
username: this.storageSettings.values.username,
|
|
254
|
+
password: this.storageSettings.values.password,
|
|
255
|
+
transport,
|
|
256
|
+
channel: entry.rtspChannel,
|
|
257
|
+
...(normalizedUid ? { uid: normalizedUid } : {}),
|
|
258
|
+
});
|
|
259
|
+
await baichuanApi.login();
|
|
260
|
+
const { capabilities, objects, presets } = await baichuanApi.getDeviceCapabilities(entry.rtspChannel);
|
|
261
|
+
const { interfaces, type } = getDeviceInterfaces({
|
|
262
|
+
capabilities,
|
|
263
|
+
logger: this.console,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const actualDevice: Device = {
|
|
267
|
+
...entry.device,
|
|
268
|
+
interfaces,
|
|
269
|
+
type
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
await sdk.deviceManager.onDeviceDiscovered(actualDevice);
|
|
273
|
+
|
|
274
|
+
const device = await this.getDevice(adopt.nativeId);
|
|
275
|
+
if (device instanceof ReolinkNativeCamera || device instanceof ReolinkNativeBatteryCamera) {
|
|
276
|
+
device.storageSettings.values.ipAddress = this.storageSettings.values.ipAddress;
|
|
277
|
+
device.storageSettings.values.username = this.storageSettings.values.username;
|
|
278
|
+
device.storageSettings.values.password = this.storageSettings.values.password;
|
|
279
|
+
device.storageSettings.values.rtspChannel = entry.rtspChannel;
|
|
280
|
+
// Set multiFocalDevice reference through options (similar to how NVR does it)
|
|
281
|
+
(device as any).options = { ...(device as any).options, multiFocalDevice: this, nvrDevice: undefined };
|
|
282
|
+
device.classes = objects;
|
|
283
|
+
device.presets = presets;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return adopt.nativeId;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async getDevice(nativeId: string): Promise<ReolinkNativeCamera | ReolinkNativeBatteryCamera> {
|
|
290
|
+
if (this.cameraNativeMap.has(nativeId)) {
|
|
291
|
+
return this.cameraNativeMap.get(nativeId)!;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const entry = this.discoveredDevices.get(nativeId);
|
|
295
|
+
if (!entry) {
|
|
296
|
+
throw new Error(`Device ${nativeId} not found`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const isBattery = entry.device.interfaces.includes(ScryptedInterface.Battery);
|
|
300
|
+
const cameraNativeId = entry.device.nativeId;
|
|
301
|
+
const camera = isBattery
|
|
302
|
+
? new ReolinkNativeBatteryCamera(cameraNativeId, this.plugin, { type: 'battery', multiFocalDevice: this, nvrDevice: undefined })
|
|
303
|
+
: new ReolinkNativeCamera(cameraNativeId, this.plugin, { type: 'regular', multiFocalDevice: this, nvrDevice: undefined });
|
|
304
|
+
|
|
305
|
+
this.cameraNativeMap.set(nativeId, camera);
|
|
306
|
+
return camera;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async getSettings(): Promise<Setting[]> {
|
|
310
|
+
const settings = await this.storageSettings.getSettings();
|
|
311
|
+
return settings;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async putSetting(key: string, value: SettingValue): Promise<void> {
|
|
315
|
+
return this.storageSettings.putSetting(key, value);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async releaseDevice(id: string, nativeId: string) {
|
|
319
|
+
this.cameraNativeMap.delete(nativeId);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
buildNativeId(channel: number, uid?: string, isBattery?: boolean): string {
|
|
323
|
+
const serialNumber = uid || this.storageSettings.values.ipAddress || 'unknown';
|
|
324
|
+
const suffix = isBattery ? '-battery-cam' : '-cam';
|
|
325
|
+
return `${serialNumber}-channel${channel}${suffix}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
forwardNativeEvent(ev: ReolinkSimpleEvent): void {
|
|
329
|
+
if (this.processing) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const logger = this.getBaichuanLogger();
|
|
334
|
+
const channel = ev?.channel;
|
|
335
|
+
|
|
336
|
+
if (channel === undefined) {
|
|
337
|
+
logger.debug('Event missing channel, ignoring');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const nativeId = this.channelToNativeIdMap.get(channel);
|
|
342
|
+
if (!nativeId) {
|
|
343
|
+
logger.error(`No camera found for channel ${channel}, ignoring event`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const camera = this.cameraNativeMap.get(nativeId);
|
|
348
|
+
if (!camera) {
|
|
349
|
+
logger.debug(`Camera ${nativeId} not yet initialized, ignoring event`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Forward event to camera
|
|
354
|
+
if (camera.onSimpleEvent) {
|
|
355
|
+
camera.onSimpleEvent(ev);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async subscribeToAllEvents(): Promise<void> {
|
|
360
|
+
const logger = this.getBaichuanLogger();
|
|
361
|
+
logger.log('Subscribed to all events for multi-focal device cameras');
|
|
362
|
+
await this.subscribeToEvents();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private async runNvrDiagnostics(): Promise<void> {
|
|
366
|
+
const logger = this.getBaichuanLogger();
|
|
367
|
+
logger.log(`Starting NVR diagnostics...`);
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const { ipAddress, username, password } = this.storageSettings.values;
|
|
371
|
+
if (!ipAddress || !username || !password) {
|
|
372
|
+
throw new Error('Missing device credentials');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const { ReolinkCgiApi } = await import("@apocaliss92/reolink-baichuan-js");
|
|
376
|
+
const cgiApi = new ReolinkCgiApi({
|
|
377
|
+
host: ipAddress,
|
|
378
|
+
username,
|
|
379
|
+
password,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
await cgiApi.login();
|
|
383
|
+
|
|
384
|
+
const diagnostics = await cgiApi.collectNvrDiagnostics({
|
|
385
|
+
logger: this.console,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
logger.log(`NVR diagnostics completed successfully.`);
|
|
389
|
+
|
|
390
|
+
// Print diagnostics to console
|
|
391
|
+
cgiApi.printNvrDiagnostics(diagnostics, this.console);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
logger.error('Failed to run NVR diagnostics', e);
|
|
394
|
+
throw e;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
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;
|
|
@@ -53,7 +63,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
53
63
|
rtspChannel: number;
|
|
54
64
|
deviceData: DeviceInfoResponse;
|
|
55
65
|
}>();
|
|
56
|
-
|
|
66
|
+
lastNvrInfoCheck: number | undefined;
|
|
57
67
|
lastErrorsCheck: number | undefined;
|
|
58
68
|
lastDevicesStatusCheck: number | undefined;
|
|
59
69
|
cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
|
|
@@ -66,7 +76,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
66
76
|
|
|
67
77
|
setTimeout(async () => {
|
|
68
78
|
await this.init();
|
|
69
|
-
},
|
|
79
|
+
}, 2000);
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
async reboot(): Promise<void> {
|
|
@@ -169,7 +179,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
169
179
|
// Find camera for this channel
|
|
170
180
|
const channel = ev?.channel;
|
|
171
181
|
if (channel === undefined) {
|
|
172
|
-
logger.
|
|
182
|
+
logger.error('Event has no channel, ignoring');
|
|
173
183
|
return;
|
|
174
184
|
}
|
|
175
185
|
|
|
@@ -177,7 +187,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
177
187
|
const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
|
|
178
188
|
|
|
179
189
|
if (!targetCamera) {
|
|
180
|
-
logger.
|
|
190
|
+
logger.info(`No camera found for channel ${channel}, ignoring event`);
|
|
181
191
|
return;
|
|
182
192
|
}
|
|
183
193
|
|
|
@@ -211,7 +221,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
211
221
|
motion = true;
|
|
212
222
|
break;
|
|
213
223
|
default:
|
|
214
|
-
logger.
|
|
224
|
+
logger.error(`Unknown event type: ${ev?.type}`);
|
|
215
225
|
return;
|
|
216
226
|
}
|
|
217
227
|
|
|
@@ -245,6 +255,26 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
245
255
|
logger.log('Subscribed to all events for NVR cameras');
|
|
246
256
|
}
|
|
247
257
|
|
|
258
|
+
private async runNvrDiagnostics(): Promise<void> {
|
|
259
|
+
const logger = this.getBaichuanLogger();
|
|
260
|
+
logger.log(`Starting NVR diagnostics...`);
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const cgiApi = await this.ensureClient();
|
|
264
|
+
|
|
265
|
+
const diagnostics = await cgiApi.collectNvrDiagnostics({
|
|
266
|
+
logger: this.console,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
logger.log(`NVR diagnostics completed successfully.`);
|
|
270
|
+
|
|
271
|
+
cgiApi.printNvrDiagnostics(diagnostics, this.console);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
logger.error('Failed to run NVR diagnostics', e);
|
|
274
|
+
throw e;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
248
278
|
async unsubscribeFromAllEvents(): Promise<void> {
|
|
249
279
|
// Use base class implementation
|
|
250
280
|
await super.unsubscribeFromEvents();
|
|
@@ -309,13 +339,12 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
309
339
|
// Note: ReolinkCgiApi doesn't have checkErrors, skip for now
|
|
310
340
|
}
|
|
311
341
|
|
|
312
|
-
if (!this.
|
|
313
|
-
logger.log('Starting
|
|
314
|
-
this.
|
|
315
|
-
const {
|
|
342
|
+
if (!this.lastNvrInfoCheck || now - this.lastNvrInfoCheck > 1000 * 60 * 5) {
|
|
343
|
+
logger.log('Starting NVR info data fetch');
|
|
344
|
+
this.lastNvrInfoCheck = now;
|
|
345
|
+
const { nvrData } = await api.getNvrInfo();
|
|
316
346
|
const { devicesData, channelsResponse, response } = await api.getDevicesInfo();
|
|
317
|
-
logger.log(
|
|
318
|
-
logger.debug(`${JSON.stringify({ hubData, devicesData, channelsResponse, response })}`);
|
|
347
|
+
logger.log(`NVR info data fetched: ${JSON.stringify({ nvrData, devicesData, channelsResponse, response })}`);
|
|
319
348
|
|
|
320
349
|
await this.discoverDevices(true);
|
|
321
350
|
}
|
|
@@ -355,6 +384,8 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
355
384
|
}
|
|
356
385
|
|
|
357
386
|
async updateDeviceInfo(): Promise<void> {
|
|
387
|
+
const logger = this.getBaichuanLogger();
|
|
388
|
+
|
|
358
389
|
const { ipAddress } = this.storageSettings.values;
|
|
359
390
|
try {
|
|
360
391
|
const api = await this.ensureClient();
|
|
@@ -365,8 +396,10 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
365
396
|
ipAddress,
|
|
366
397
|
deviceData,
|
|
367
398
|
});
|
|
399
|
+
|
|
400
|
+
logger.log(`Device info updated: ${JSON.stringify(deviceData)}`);
|
|
368
401
|
} catch (e) {
|
|
369
|
-
|
|
402
|
+
logger.warn('Failed to fetch device info', e);
|
|
370
403
|
}
|
|
371
404
|
}
|
|
372
405
|
|
|
@@ -422,12 +455,8 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
422
455
|
const api = await this.ensureClient();
|
|
423
456
|
const logger = this.getBaichuanLogger();
|
|
424
457
|
|
|
425
|
-
logger.log('Starting channels discovery using getDevicesInfo...');
|
|
426
|
-
|
|
427
458
|
const { devicesData, channels } = await api.getDevicesInfo();
|
|
428
|
-
|
|
429
|
-
logger.log(`getDevicesInfo completed. Found ${channels.length} channels.`);
|
|
430
|
-
|
|
459
|
+
logger.log(`Sync entities from remote for ${channels.length} channels`);
|
|
431
460
|
// Process each channel that was successfully discovered
|
|
432
461
|
for (const channel of channels) {
|
|
433
462
|
try {
|
|
@@ -479,7 +508,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
479
508
|
}
|
|
480
509
|
}
|
|
481
510
|
|
|
482
|
-
logger.log(`Channel discovery completed.
|
|
511
|
+
logger.log(`Channel discovery completed. ${JSON.stringify({ devicesData, channels })}`);
|
|
483
512
|
}
|
|
484
513
|
|
|
485
514
|
async discoverDevices(scan?: boolean): Promise<DiscoveredDevice[]> {
|
|
@@ -532,7 +561,8 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
532
561
|
await sdk.deviceManager.onDeviceDiscovered(actualDevice);
|
|
533
562
|
|
|
534
563
|
const device = await this.getDevice(adopt.nativeId);
|
|
535
|
-
this.getBaichuanLogger()
|
|
564
|
+
const logger = this.getBaichuanLogger();
|
|
565
|
+
logger.log('Adopted device', entry, device?.name);
|
|
536
566
|
const { username, password, ipAddress } = this.storageSettings.values;
|
|
537
567
|
|
|
538
568
|
device.storageSettings.values.rtspChannel = entry.rtspChannel;
|
|
@@ -544,7 +574,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
544
574
|
device.storageSettings.values.ipAddress = ipAddress;
|
|
545
575
|
device.storageSettings.values.capabilities = capabilities;
|
|
546
576
|
device.storageSettings.values.uid = entry.deviceData.channelStatus.uid;
|
|
547
|
-
device.storageSettings.values.isFromNvr = true;
|
|
548
577
|
|
|
549
578
|
this.discoveredDevices.delete(adopt.nativeId);
|
|
550
579
|
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,
|