@apocaliss92/scrypted-reolink-native 0.1.36 → 0.1.38
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/baichuan-base.ts +3 -2
- package/src/camera-battery.ts +16 -1
- package/src/common.ts +58 -91
- package/src/connect.ts +2 -0
- package/src/main.ts +1 -0
- package/src/nvr.ts +123 -126
- package/src/utils.ts +47 -0
package/src/nvr.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EventsResponse, ReolinkBaichuanApi, ReolinkBaichuanDeviceSummary, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
2
|
import sdk, { AdoptDevice, Device, DeviceDiscovery, DeviceProvider, DiscoveredDevice, Reboot, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
|
3
3
|
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
|
4
4
|
import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
|
|
5
5
|
import { ReolinkNativeCamera } from "./camera";
|
|
6
6
|
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
7
7
|
import { normalizeUid } from "./connect";
|
|
8
|
+
import { convertDebugLogsToApiOptions, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
|
|
8
9
|
import ReolinkNativePlugin from "./main";
|
|
9
10
|
import { getDeviceInterfaces, updateDeviceInfo } from "./utils";
|
|
10
11
|
|
|
@@ -44,8 +45,8 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
44
45
|
onPut: async () => await this.reinit()
|
|
45
46
|
},
|
|
46
47
|
diagnosticsRun: {
|
|
47
|
-
subgroup: '
|
|
48
|
-
title: 'Run
|
|
48
|
+
subgroup: 'Advanced',
|
|
49
|
+
title: 'Run Diagnostics',
|
|
49
50
|
description: 'Collect NVR diagnostics and display results in logs.',
|
|
50
51
|
type: 'button',
|
|
51
52
|
immediate: true,
|
|
@@ -53,15 +54,54 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
53
54
|
await this.runNvrDiagnostics();
|
|
54
55
|
},
|
|
55
56
|
},
|
|
57
|
+
socketApiDebugLogs: {
|
|
58
|
+
subgroup: 'Advanced',
|
|
59
|
+
title: 'Socket API Debug Logs',
|
|
60
|
+
description: 'Enable specific debug logs.',
|
|
61
|
+
multiple: true,
|
|
62
|
+
combobox: true,
|
|
63
|
+
immediate: true,
|
|
64
|
+
defaultValue: [],
|
|
65
|
+
choices: getDebugLogChoices(),
|
|
66
|
+
onPut: async (ov, value) => {
|
|
67
|
+
const logger = this.getBaichuanLogger();
|
|
68
|
+
const oldApiOptions = getApiRelevantDebugLogs(ov || []);
|
|
69
|
+
const newApiOptions = getApiRelevantDebugLogs(value || []);
|
|
70
|
+
|
|
71
|
+
const oldSel = new Set(oldApiOptions);
|
|
72
|
+
const newSel = new Set(newApiOptions);
|
|
73
|
+
|
|
74
|
+
const changed = oldSel.size !== newSel.size || Array.from(oldSel).some((k) => !newSel.has(k));
|
|
75
|
+
if (changed) {
|
|
76
|
+
// Clear any existing timeout
|
|
77
|
+
if (this.debugLogsResetTimeout) {
|
|
78
|
+
clearTimeout(this.debugLogsResetTimeout);
|
|
79
|
+
this.debugLogsResetTimeout = undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Defer reset by 2 seconds to allow settings to settle
|
|
83
|
+
this.debugLogsResetTimeout = setTimeout(async () => {
|
|
84
|
+
this.debugLogsResetTimeout = undefined;
|
|
85
|
+
try {
|
|
86
|
+
// Force reconnection with new debug options
|
|
87
|
+
this.baichuanApi = undefined;
|
|
88
|
+
this.ensureClientPromise = undefined;
|
|
89
|
+
// Trigger reconnection
|
|
90
|
+
await this.ensureBaichuanClient();
|
|
91
|
+
} catch (e) {
|
|
92
|
+
logger.warn('Failed to reset client after debug logs change', e);
|
|
93
|
+
}
|
|
94
|
+
}, 2000);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
},
|
|
56
98
|
});
|
|
57
99
|
plugin: ReolinkNativePlugin;
|
|
58
|
-
nvrApi: ReolinkCgiApi | undefined;
|
|
59
|
-
// baichuanApi, ensureClientPromise, connectionTime inherited from BaseBaichuanClass
|
|
60
100
|
discoveredDevices = new Map<string, {
|
|
61
101
|
device: Device;
|
|
62
102
|
description: string;
|
|
63
103
|
rtspChannel: number;
|
|
64
|
-
deviceData:
|
|
104
|
+
deviceData: ReolinkBaichuanDeviceSummary;
|
|
65
105
|
}>();
|
|
66
106
|
lastNvrInfoCheck: number | undefined;
|
|
67
107
|
lastErrorsCheck: number | undefined;
|
|
@@ -71,6 +111,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
71
111
|
private discoverDevicesPromise: Promise<DiscoveredDevice[]> | undefined;
|
|
72
112
|
processing = false;
|
|
73
113
|
private initReinitTimeout: NodeJS.Timeout | undefined;
|
|
114
|
+
private debugLogsResetTimeout: NodeJS.Timeout | undefined;
|
|
74
115
|
|
|
75
116
|
constructor(nativeId: string, plugin: ReolinkNativePlugin) {
|
|
76
117
|
super(nativeId);
|
|
@@ -84,27 +125,31 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
84
125
|
await api.reboot();
|
|
85
126
|
}
|
|
86
127
|
|
|
87
|
-
// BaseBaichuanClass abstract methods implementation
|
|
88
128
|
protected getConnectionConfig(): BaichuanConnectionConfig {
|
|
89
129
|
const { ipAddress, username, password } = this.storageSettings.values;
|
|
90
130
|
if (!ipAddress || !username || !password) {
|
|
91
131
|
throw new Error('Missing NVR credentials');
|
|
92
132
|
}
|
|
93
133
|
|
|
134
|
+
const debugOptions = this.getBaichuanDebugOptions();
|
|
135
|
+
|
|
94
136
|
return {
|
|
95
137
|
host: ipAddress,
|
|
96
138
|
username,
|
|
97
139
|
password,
|
|
98
140
|
transport: 'tcp',
|
|
99
|
-
|
|
141
|
+
debugOptions,
|
|
100
142
|
};
|
|
101
143
|
}
|
|
102
144
|
|
|
145
|
+
getBaichuanDebugOptions(): any | undefined {
|
|
146
|
+
const socketDebugLogs = this.storageSettings.values.socketApiDebugLogs || [];
|
|
147
|
+
return convertDebugLogsToApiOptions(socketDebugLogs);
|
|
148
|
+
}
|
|
149
|
+
|
|
103
150
|
protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
|
|
104
151
|
return {
|
|
105
|
-
onError: undefined, // Use default error handling
|
|
106
152
|
onClose: async () => {
|
|
107
|
-
// Reinit after cleanup
|
|
108
153
|
await this.reinit();
|
|
109
154
|
},
|
|
110
155
|
onSimpleEvent: (ev) => this.forwardNativeEvent(ev),
|
|
@@ -146,17 +191,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
146
191
|
|
|
147
192
|
this.initReinitTimeout = setTimeout(async () => {
|
|
148
193
|
if (isReinit) {
|
|
149
|
-
// Cleanup CGI API
|
|
150
|
-
if (this.nvrApi) {
|
|
151
|
-
try {
|
|
152
|
-
await this.nvrApi.logout();
|
|
153
|
-
} catch {
|
|
154
|
-
// ignore
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
this.nvrApi = undefined;
|
|
158
|
-
|
|
159
|
-
// Cleanup Baichuan API (this handles all listeners and connection)
|
|
160
194
|
await super.cleanupBaichuanApi();
|
|
161
195
|
}
|
|
162
196
|
await this.init();
|
|
@@ -164,52 +198,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
164
198
|
}, isReinit ? 500 : 2000);
|
|
165
199
|
}
|
|
166
200
|
|
|
167
|
-
async ensureClient(): Promise<ReolinkCgiApi> {
|
|
168
|
-
if (this.nvrApi) {
|
|
169
|
-
return this.nvrApi;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const { ipAddress, username, password } = this.storageSettings.values;
|
|
173
|
-
if (!ipAddress || !username || !password) {
|
|
174
|
-
throw new Error('Missing NVR credentials');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const { ReolinkCgiApi } = await import("@apocaliss92/reolink-baichuan-js");
|
|
178
|
-
const logger = this.getBaichuanLogger();
|
|
179
|
-
this.nvrApi = new ReolinkCgiApi({
|
|
180
|
-
host: ipAddress,
|
|
181
|
-
username,
|
|
182
|
-
password,
|
|
183
|
-
logger,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
await this.nvrApi.login();
|
|
187
|
-
return this.nvrApi;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* List enriched VOD files (with proper parsing and detection info)
|
|
192
|
-
* This uses the library's enrichVodFile which handles all parsing correctly
|
|
193
|
-
*/
|
|
194
|
-
async listEnrichedVodFiles(params: {
|
|
195
|
-
channel: number;
|
|
196
|
-
start: Date;
|
|
197
|
-
end: Date;
|
|
198
|
-
streamType?: "main" | "sub";
|
|
199
|
-
autoSearchByDay?: boolean;
|
|
200
|
-
bypassCache?: boolean;
|
|
201
|
-
}): Promise<Array<EnrichedRecordingFile>> {
|
|
202
|
-
const api = await this.ensureClient();
|
|
203
|
-
return await api.listEnrichedVodFiles({
|
|
204
|
-
channel: params.channel,
|
|
205
|
-
start: params.start,
|
|
206
|
-
end: params.end,
|
|
207
|
-
streamType: params.streamType,
|
|
208
|
-
autoSearchByDay: params.autoSearchByDay,
|
|
209
|
-
bypassCache: params.bypassCache,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
201
|
private forwardNativeEvent(ev: ReolinkSimpleEvent): void {
|
|
214
202
|
const logger = this.getBaichuanLogger();
|
|
215
203
|
|
|
@@ -232,13 +220,15 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
232
220
|
const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
|
|
233
221
|
|
|
234
222
|
if (!targetCamera) {
|
|
235
|
-
logger.debug(`No camera found for channel ${channel}, ignoring event`);
|
|
223
|
+
logger.debug(`No camera found for channel ${channel} (nativeId: ${nativeId}), ignoring event`);
|
|
236
224
|
return;
|
|
237
225
|
}
|
|
238
226
|
|
|
239
227
|
// Convert event to camera's processEvents format
|
|
240
228
|
const objects: string[] = [];
|
|
241
229
|
let motion = false;
|
|
230
|
+
let isSleepingEvent = false;
|
|
231
|
+
let isOnlineEvent = false;
|
|
242
232
|
|
|
243
233
|
switch (ev?.type) {
|
|
244
234
|
case 'motion':
|
|
@@ -263,15 +253,34 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
263
253
|
objects.push(ev.type);
|
|
264
254
|
motion = true;
|
|
265
255
|
break;
|
|
256
|
+
case 'awake':
|
|
257
|
+
case 'sleeping':
|
|
258
|
+
isSleepingEvent = true;
|
|
259
|
+
break;
|
|
260
|
+
case 'offline':
|
|
261
|
+
case 'online':
|
|
262
|
+
isOnlineEvent = true;
|
|
263
|
+
break;
|
|
266
264
|
default:
|
|
267
265
|
logger.error(`Unknown event type: ${ev?.type}`);
|
|
268
266
|
return;
|
|
269
267
|
}
|
|
270
268
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
269
|
+
if (isSleepingEvent) {
|
|
270
|
+
(targetCamera as ReolinkNativeBatteryCamera).updateSleepingState({
|
|
271
|
+
reason: 'NVR',
|
|
272
|
+
state: ev.type === 'sleeping' ? 'sleeping' : 'awake',
|
|
273
|
+
}).catch(() => { });
|
|
274
|
+
} if (isSleepingEvent) {
|
|
275
|
+
(targetCamera as ReolinkNativeBatteryCamera).updateOnlineState(
|
|
276
|
+
ev.type === 'online' ? true : false
|
|
277
|
+
).catch(() => { });
|
|
278
|
+
} else {
|
|
279
|
+
// Process events on the target camera
|
|
280
|
+
targetCamera.processEvents({ motion, objects }).catch((e) => {
|
|
281
|
+
logger.warn(`Error processing events for camera channel ${channel}`, e);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
275
284
|
}
|
|
276
285
|
catch (e) {
|
|
277
286
|
logger.warn('Error in NVR Native event forwarder', e);
|
|
@@ -298,15 +307,11 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
298
307
|
logger.log(`Starting NVR diagnostics...`);
|
|
299
308
|
|
|
300
309
|
try {
|
|
301
|
-
const
|
|
310
|
+
const api = await this.ensureBaichuanClient();
|
|
302
311
|
|
|
303
|
-
|
|
312
|
+
await api.collectNvrDiagnostics({
|
|
304
313
|
logger: this.console,
|
|
305
314
|
});
|
|
306
|
-
|
|
307
|
-
logger.log(`NVR diagnostics completed successfully.`);
|
|
308
|
-
|
|
309
|
-
cgiApi.printNvrDiagnostics(diagnostics, this.console);
|
|
310
315
|
} catch (e) {
|
|
311
316
|
logger.error('Failed to run NVR diagnostics', e);
|
|
312
317
|
throw e;
|
|
@@ -357,9 +362,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
357
362
|
|
|
358
363
|
async init() {
|
|
359
364
|
const logger = this.getBaichuanLogger();
|
|
360
|
-
|
|
361
|
-
// Ensure both APIs are ready before proceeding
|
|
362
|
-
const api = await this.ensureClient();
|
|
363
365
|
await this.ensureBaichuanClient();
|
|
364
366
|
|
|
365
367
|
await this.updateDeviceInfo();
|
|
@@ -367,7 +369,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
367
369
|
await this.reinitEventSubscriptions();
|
|
368
370
|
|
|
369
371
|
setInterval(async () => {
|
|
370
|
-
if (this.processing
|
|
372
|
+
if (this.processing) {
|
|
371
373
|
return;
|
|
372
374
|
}
|
|
373
375
|
this.processing = true;
|
|
@@ -381,39 +383,41 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
381
383
|
|
|
382
384
|
if (!this.lastNvrInfoCheck || now - this.lastNvrInfoCheck > 1000 * 60 * 5) {
|
|
383
385
|
this.lastNvrInfoCheck = now;
|
|
384
|
-
const { nvrData } = await api.getNvrInfo();
|
|
385
|
-
const { devicesData, channelsResponse, response } = await api.getDevicesInfo();
|
|
386
|
-
logger.log(`NVR info data fetched`);
|
|
387
|
-
logger.debug(`${JSON.stringify({ nvrData, devicesData, channelsResponse, response })}`);
|
|
386
|
+
// const { nvrData } = await api.getNvrInfo();
|
|
387
|
+
// const { devicesData, channelsResponse, response } = await api.getDevicesInfo();
|
|
388
|
+
// logger.log(`NVR info data fetched`);
|
|
389
|
+
// logger.debug(`${JSON.stringify({ nvrData, devicesData, channelsResponse, response })}`);
|
|
388
390
|
|
|
389
391
|
await this.discoverDevices(true);
|
|
390
392
|
}
|
|
391
393
|
|
|
392
|
-
|
|
394
|
+
const api = await this.ensureBaichuanClient();
|
|
395
|
+
|
|
393
396
|
const { eventSource } = this.storageSettings.values;
|
|
397
|
+
|
|
394
398
|
if (eventSource === 'CGI') {
|
|
395
399
|
const eventsRes = await api.getAllChannelsEvents();
|
|
396
400
|
this.forwardCgiEvents(eventsRes.parsed);
|
|
397
|
-
}
|
|
398
401
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
402
|
+
const { batteryInfoData, response } = await api.getAllChannelsBatteryInfo();
|
|
403
|
+
|
|
404
|
+
logger.debug(`Battery info call result: ${JSON.stringify({ batteryInfoData, response })}`);
|
|
405
|
+
|
|
406
|
+
this.cameraNativeMap.forEach((camera) => {
|
|
407
|
+
if (camera) {
|
|
408
|
+
const channel = camera.storageSettings.values.rtspChannel;
|
|
409
|
+
const cameraBatteryData = batteryInfoData[channel];
|
|
410
|
+
if (cameraBatteryData) {
|
|
411
|
+
(camera as ReolinkNativeBatteryCamera).updateSleepingState({
|
|
412
|
+
reason: 'NVR',
|
|
413
|
+
state: cameraBatteryData.sleeping ? 'sleeping' : 'awake',
|
|
414
|
+
idleMs: 0,
|
|
415
|
+
lastRxAtMs: 0,
|
|
416
|
+
}).catch(() => { });
|
|
417
|
+
}
|
|
414
418
|
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
417
421
|
} catch (e) {
|
|
418
422
|
logger.error('Error on events flow', e);
|
|
419
423
|
} finally {
|
|
@@ -427,7 +431,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
427
431
|
|
|
428
432
|
const { ipAddress } = this.storageSettings.values;
|
|
429
433
|
try {
|
|
430
|
-
const api = await this.
|
|
434
|
+
const api = await this.ensureBaichuanClient();
|
|
431
435
|
const deviceData = await api.getInfo();
|
|
432
436
|
|
|
433
437
|
await updateDeviceInfo({
|
|
@@ -492,33 +496,27 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
492
496
|
async syncEntitiesFromRemote() {
|
|
493
497
|
const logger = this.getBaichuanLogger();
|
|
494
498
|
|
|
495
|
-
const
|
|
496
|
-
const {
|
|
497
|
-
|
|
498
|
-
// const api = await this.ensureBaichuanClient();
|
|
499
|
-
// const devicesMap = api.getDevicesInfo();
|
|
500
|
-
// const deviceEntries = Object.entries(devicesMap);
|
|
499
|
+
const api = await this.ensureBaichuanClient();
|
|
500
|
+
const { devices, channels } = await api.getDevicesInfo();
|
|
501
|
+
logger.log(devices, channels);
|
|
501
502
|
|
|
502
503
|
if (!channels.length) {
|
|
503
|
-
logger.debug(`No channels found, ${JSON.stringify({ channels,
|
|
504
|
+
logger.debug(`No channels found, ${JSON.stringify({ channels, devices })}`);
|
|
504
505
|
return;
|
|
505
506
|
}
|
|
506
507
|
|
|
507
508
|
logger.log(`Sync entities from remote for ${channels.length} channels`);
|
|
508
509
|
|
|
509
|
-
for (const
|
|
510
|
-
|
|
511
|
-
const { channelStatus, channelInfo, abilities } = devicesData[channel];
|
|
512
|
-
const name = channelStatus?.name;
|
|
513
|
-
const uid = channelStatus?.uid;
|
|
514
|
-
const isBattery = !!(abilities?.battery?.ver ?? 0);
|
|
510
|
+
for (const deviceData of devices) {
|
|
511
|
+
const { isBattery, name, model, isDoorbell, uid, channel } = deviceData
|
|
515
512
|
|
|
513
|
+
try {
|
|
516
514
|
const nativeId = this.buildNativeId(channel, uid, isBattery);
|
|
517
515
|
const interfaces = [ScryptedInterface.VideoCamera];
|
|
518
516
|
if (isBattery) {
|
|
519
517
|
interfaces.push(ScryptedInterface.Battery);
|
|
520
518
|
}
|
|
521
|
-
const type =
|
|
519
|
+
const type = isDoorbell ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera;
|
|
522
520
|
|
|
523
521
|
const device: Device = {
|
|
524
522
|
nativeId,
|
|
@@ -528,7 +526,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
528
526
|
type,
|
|
529
527
|
info: {
|
|
530
528
|
manufacturer: 'Reolink',
|
|
531
|
-
model
|
|
529
|
+
model,
|
|
532
530
|
serialNumber: uid,
|
|
533
531
|
}
|
|
534
532
|
};
|
|
@@ -547,7 +545,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
547
545
|
device,
|
|
548
546
|
description: `${name} (Channel ${channel})`,
|
|
549
547
|
rtspChannel: channel,
|
|
550
|
-
deviceData
|
|
548
|
+
deviceData,
|
|
551
549
|
});
|
|
552
550
|
|
|
553
551
|
logger.debug(`Discovered channel ${channel}: ${name}`);
|
|
@@ -556,7 +554,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
556
554
|
}
|
|
557
555
|
}
|
|
558
556
|
|
|
559
|
-
logger.debug(`Channel discovery completed. ${JSON.stringify({
|
|
557
|
+
logger.debug(`Channel discovery completed. ${JSON.stringify({ devices, channels })}`);
|
|
560
558
|
}
|
|
561
559
|
|
|
562
560
|
async discoverDevices(scan?: boolean): Promise<DiscoveredDevice[]> {
|
|
@@ -597,11 +595,10 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
597
595
|
await this.onDeviceEvent(ScryptedInterface.DeviceDiscovery, await this.discoverDevices());
|
|
598
596
|
|
|
599
597
|
const isBattery = entry.device.interfaces.includes(ScryptedInterface.Battery);
|
|
600
|
-
const {
|
|
598
|
+
const { uid } = entry.deviceData;
|
|
601
599
|
|
|
602
600
|
const { ReolinkBaichuanApi } = await import("@apocaliss92/reolink-baichuan-js");
|
|
603
601
|
const transport = 'tcp';
|
|
604
|
-
const uid = channelStatus?.uid;
|
|
605
602
|
const normalizedUid = isBattery && uid ? normalizeUid(uid) : undefined;
|
|
606
603
|
const baichuanApi = new ReolinkBaichuanApi({
|
|
607
604
|
host: this.storageSettings.values.ipAddress,
|
|
@@ -640,7 +637,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
640
637
|
device.storageSettings.values.rtspChannel = entry.rtspChannel;
|
|
641
638
|
device.storageSettings.values.ipAddress = ipAddress;
|
|
642
639
|
device.storageSettings.values.capabilities = capabilities;
|
|
643
|
-
device.storageSettings.values.uid =
|
|
640
|
+
device.storageSettings.values.uid = uid;
|
|
644
641
|
|
|
645
642
|
this.discoveredDevices.delete(adopt.nativeId);
|
|
646
643
|
return device?.id;
|
package/src/utils.ts
CHANGED
|
@@ -329,6 +329,53 @@ export async function recordingFileToVideoClip(
|
|
|
329
329
|
};
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Convert an array of RecordingFile or EnrichedRecordingFile to VideoClip array
|
|
334
|
+
* Uses recordingFileToVideoClip for each recording
|
|
335
|
+
* Handles both NVR (EnrichedRecordingFile) and device standalone (RecordingFile) cases
|
|
336
|
+
*/
|
|
337
|
+
export async function recordingsToVideoClips(
|
|
338
|
+
recordings: (RecordingFile | EnrichedRecordingFile)[],
|
|
339
|
+
options: {
|
|
340
|
+
/** Fallback start date if recording doesn't have one */
|
|
341
|
+
fallbackStart: Date;
|
|
342
|
+
/** API instance to get playback URLs (optional, for device standalone recordings) */
|
|
343
|
+
api?: ReolinkBaichuanApi;
|
|
344
|
+
/** Logger for debug messages */
|
|
345
|
+
logger?: Console;
|
|
346
|
+
/** Plugin instance for generating webhook URLs */
|
|
347
|
+
plugin?: ScryptedDeviceBase;
|
|
348
|
+
/** Device ID for webhook URLs */
|
|
349
|
+
deviceId?: string;
|
|
350
|
+
/** Use webhook URLs instead of direct RTMP URLs */
|
|
351
|
+
useWebhook?: boolean;
|
|
352
|
+
/** Maximum number of clips to return (optional) */
|
|
353
|
+
count?: number;
|
|
354
|
+
}
|
|
355
|
+
): Promise<VideoClip[]> {
|
|
356
|
+
const { fallbackStart, api, logger, plugin, deviceId, useWebhook, count } = options;
|
|
357
|
+
const clips: VideoClip[] = [];
|
|
358
|
+
|
|
359
|
+
for (const rec of recordings) {
|
|
360
|
+
try {
|
|
361
|
+
const clip = await recordingFileToVideoClip(rec, {
|
|
362
|
+
fallbackStart,
|
|
363
|
+
api,
|
|
364
|
+
logger,
|
|
365
|
+
plugin,
|
|
366
|
+
deviceId,
|
|
367
|
+
useWebhook,
|
|
368
|
+
});
|
|
369
|
+
clips.push(clip);
|
|
370
|
+
} catch (e) {
|
|
371
|
+
logger?.warn(`Failed to convert recording to video clip: fileName=${rec.fileName}`, e);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Apply count limit if specified
|
|
376
|
+
return count ? clips.slice(0, count) : clips;
|
|
377
|
+
}
|
|
378
|
+
|
|
332
379
|
/**
|
|
333
380
|
* Generate webhook URLs for video clips
|
|
334
381
|
*/
|