@apocaliss92/scrypted-reolink-native 0.1.8 → 0.1.10
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/baichuan-base.ts +478 -0
- package/src/camera-battery.ts +32 -32
- package/src/camera.ts +6 -9
- package/src/common.ts +107 -231
- package/src/connect.ts +1 -2
- package/src/debug-options.ts +1 -1
- package/src/intercom.ts +3 -3
- package/src/nvr.ts +179 -408
- package/src/stream-utils.ts +1 -1
- package/logs.txt +0 -7361
package/src/nvr.ts
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
|
-
import type { DeviceInfoResponse, DeviceInputData, ReolinkBaichuanApi, ReolinkCgiApi, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
1
|
+
import type { DeviceInfoResponse, DeviceInputData, EventsResponse, ReolinkBaichuanApi, ReolinkCgiApi, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
2
|
import sdk, { AdoptDevice, Device, DeviceDiscovery, DeviceProvider, DiscoveredDevice, Reboot, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
|
3
3
|
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
|
4
|
+
import { BaseBaichuanClass, type BaichuanConnectionConfig, type BaichuanConnectionCallbacks } from "./baichuan-base";
|
|
4
5
|
import { ReolinkNativeCamera } from "./camera";
|
|
5
6
|
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
6
7
|
import { normalizeUid } from "./connect";
|
|
7
8
|
import ReolinkNativePlugin from "./main";
|
|
8
9
|
import { getDeviceInterfaces, updateDeviceInfo } from "./utils";
|
|
9
10
|
|
|
10
|
-
export class ReolinkNativeNvrDevice extends
|
|
11
|
+
export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Settings, DeviceDiscovery, DeviceProvider, Reboot {
|
|
11
12
|
storageSettings = new StorageSettings(this, {
|
|
12
13
|
debugEvents: {
|
|
13
14
|
title: 'Debug Events',
|
|
14
15
|
type: 'boolean',
|
|
15
16
|
immediate: true,
|
|
16
17
|
},
|
|
18
|
+
eventSource: {
|
|
19
|
+
title: 'Event Source',
|
|
20
|
+
description: 'Select the source for camera events: Native (Baichuan) or CGI (HTTP polling)',
|
|
21
|
+
type: 'string',
|
|
22
|
+
choices: ['Native', 'CGI'],
|
|
23
|
+
defaultValue: 'Native',
|
|
24
|
+
immediate: true,
|
|
25
|
+
onPut: async () => {
|
|
26
|
+
await this.reinitEventSubscriptions();
|
|
27
|
+
}
|
|
28
|
+
},
|
|
17
29
|
ipAddress: {
|
|
18
30
|
title: 'IP address',
|
|
19
31
|
type: 'string',
|
|
@@ -34,8 +46,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
34
46
|
});
|
|
35
47
|
plugin: ReolinkNativePlugin;
|
|
36
48
|
nvrApi: ReolinkCgiApi | undefined;
|
|
37
|
-
baichuanApi
|
|
38
|
-
baichuanApiPromise: Promise<ReolinkBaichuanApi> | undefined;
|
|
49
|
+
// baichuanApi, ensureClientPromise, connectionTime inherited from BaseBaichuanClass
|
|
39
50
|
discoveredDevices = new Map<string, {
|
|
40
51
|
device: Device;
|
|
41
52
|
description: string;
|
|
@@ -46,15 +57,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
46
57
|
lastErrorsCheck: number | undefined;
|
|
47
58
|
lastDevicesStatusCheck: number | undefined;
|
|
48
59
|
cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
|
|
60
|
+
private channelToNativeIdMap = new Map<number, string>();
|
|
49
61
|
processing = false;
|
|
50
|
-
private eventSubscriptionActive = false;
|
|
51
|
-
private onSimpleEventHandler?: (ev: ReolinkSimpleEvent) => void;
|
|
52
|
-
private closeListener?: () => void;
|
|
53
|
-
private errorListener?: (err: unknown) => void;
|
|
54
|
-
private lastDisconnectTime: number = 0;
|
|
55
|
-
private lastErrorBeforeClose: { error: string; timestamp: number } | undefined;
|
|
56
|
-
private readonly reconnectBackoffMs: number = 2000; // 2 seconds minimum between reconnects
|
|
57
|
-
private resubscribeTimeout?: NodeJS.Timeout;
|
|
58
62
|
|
|
59
63
|
constructor(nativeId: string, plugin: ReolinkNativePlugin) {
|
|
60
64
|
super(nativeId);
|
|
@@ -70,11 +74,53 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
70
74
|
await api.Reboot();
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
// BaseBaichuanClass abstract methods implementation
|
|
78
|
+
protected getConnectionConfig(): BaichuanConnectionConfig {
|
|
79
|
+
const { ipAddress, username, password } = this.storageSettings.values;
|
|
80
|
+
if (!ipAddress || !username || !password) {
|
|
81
|
+
throw new Error('Missing NVR credentials');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
host: ipAddress,
|
|
86
|
+
username,
|
|
87
|
+
password,
|
|
88
|
+
transport: 'tcp',
|
|
89
|
+
logger: this.console,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
|
|
94
|
+
return {
|
|
95
|
+
onError: undefined, // Use default error handling
|
|
96
|
+
onClose: async () => {
|
|
97
|
+
// Reinit after cleanup
|
|
98
|
+
await this.reinit();
|
|
99
|
+
},
|
|
100
|
+
onSimpleEvent: (ev) => this.forwardNativeEvent(ev),
|
|
101
|
+
getEventSubscriptionEnabled: () => {
|
|
102
|
+
const eventSource = this.storageSettings.values.eventSource || 'Native';
|
|
103
|
+
return eventSource === 'Native';
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
protected isDebugEnabled(): boolean {
|
|
110
|
+
return this.storageSettings.values.debugEvents || false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
protected getDeviceName(): string {
|
|
114
|
+
return this.name || 'NVR';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
protected async onBeforeCleanup(): Promise<void> {
|
|
118
|
+
// Unsubscribe from events if needed
|
|
119
|
+
await this.unsubscribeFromAllEvents();
|
|
75
120
|
}
|
|
76
121
|
|
|
77
122
|
async reinit() {
|
|
123
|
+
// Cleanup CGI API
|
|
78
124
|
if (this.nvrApi) {
|
|
79
125
|
try {
|
|
80
126
|
await this.nvrApi.logout();
|
|
@@ -84,48 +130,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
84
130
|
}
|
|
85
131
|
this.nvrApi = undefined;
|
|
86
132
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
clearTimeout(this.resubscribeTimeout);
|
|
90
|
-
this.resubscribeTimeout = undefined;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Unsubscribe from events first
|
|
94
|
-
await this.unsubscribeFromAllEvents();
|
|
95
|
-
|
|
96
|
-
if (this.baichuanApi) {
|
|
97
|
-
// Remove close listener
|
|
98
|
-
if (this.closeListener) {
|
|
99
|
-
try {
|
|
100
|
-
this.baichuanApi.client.off("close", this.closeListener);
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
// ignore
|
|
104
|
-
}
|
|
105
|
-
this.closeListener = undefined;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Remove error listener
|
|
109
|
-
if (this.errorListener) {
|
|
110
|
-
try {
|
|
111
|
-
this.baichuanApi.client.off("error", this.errorListener);
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// ignore
|
|
115
|
-
}
|
|
116
|
-
this.errorListener = undefined;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
if (this.baichuanApi.client.isSocketConnected()) {
|
|
121
|
-
await this.baichuanApi.close();
|
|
122
|
-
}
|
|
123
|
-
} catch {
|
|
124
|
-
// ignore
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
this.baichuanApi = undefined;
|
|
128
|
-
this.baichuanApiPromise = undefined;
|
|
133
|
+
// Cleanup Baichuan API (this handles all listeners and connection)
|
|
134
|
+
await super.cleanupBaichuanApi();
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
async ensureClient(): Promise<ReolinkCgiApi> {
|
|
@@ -149,372 +155,146 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
149
155
|
return this.nvrApi;
|
|
150
156
|
}
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (this.baichuanApi && this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
|
|
155
|
-
return this.baichuanApi;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Prevent concurrent login storms
|
|
159
|
-
if (this.baichuanApiPromise) return await this.baichuanApiPromise;
|
|
158
|
+
private forwardNativeEvent(ev: ReolinkSimpleEvent): void {
|
|
159
|
+
const logger = this.getBaichuanLogger();
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
161
|
+
const eventSource = this.storageSettings.values.eventSource || 'Native';
|
|
162
|
+
if (eventSource !== 'Native') {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
166
165
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// Remove close listener from old client
|
|
170
|
-
if (this.closeListener) {
|
|
171
|
-
try {
|
|
172
|
-
this.baichuanApi.client.off("close", this.closeListener);
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
// ignore
|
|
176
|
-
}
|
|
177
|
-
this.closeListener = undefined;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Remove error listener from old client
|
|
181
|
-
if (this.errorListener) {
|
|
182
|
-
try {
|
|
183
|
-
this.baichuanApi.client.off("error", this.errorListener);
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
// ignore
|
|
187
|
-
}
|
|
188
|
-
this.errorListener = undefined;
|
|
189
|
-
}
|
|
166
|
+
try {
|
|
167
|
+
logger.debug(`Baichuan event: ${JSON.stringify(ev)}`);
|
|
190
168
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
catch {
|
|
197
|
-
// ignore
|
|
198
|
-
}
|
|
169
|
+
// Find camera for this channel
|
|
170
|
+
const channel = ev?.channel;
|
|
171
|
+
if (channel === undefined) {
|
|
172
|
+
logger.debug('Event has no channel, ignoring');
|
|
173
|
+
return;
|
|
199
174
|
}
|
|
200
175
|
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
this.baichuanApi = new ReolinkBaichuanApi({
|
|
204
|
-
host: ipAddress,
|
|
205
|
-
username,
|
|
206
|
-
password,
|
|
207
|
-
transport: 'tcp',
|
|
208
|
-
logger: this.getLogger(),
|
|
209
|
-
// rebootAfterDisconnectionsPerMinute: 5,
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
await this.baichuanApi.login();
|
|
176
|
+
const nativeId = this.channelToNativeIdMap.get(channel);
|
|
177
|
+
const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
|
|
213
178
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
179
|
+
if (!targetCamera) {
|
|
180
|
+
logger.debug(`No camera found for channel ${channel}, ignoring event`);
|
|
181
|
+
return;
|
|
217
182
|
}
|
|
218
183
|
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const msg = (err as any)?.message || (err as any)?.toString?.() || String(err);
|
|
223
|
-
|
|
224
|
-
// Store last error before close
|
|
225
|
-
this.lastErrorBeforeClose = {
|
|
226
|
-
error: msg,
|
|
227
|
-
timestamp: Date.now()
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
// Only log if it's not a recoverable error to avoid spam
|
|
231
|
-
if (typeof msg === 'string' && (
|
|
232
|
-
msg.includes('Baichuan socket closed') ||
|
|
233
|
-
msg.includes('Baichuan UDP stream closed') ||
|
|
234
|
-
msg.includes('Not running')
|
|
235
|
-
)) {
|
|
236
|
-
// Log even recoverable errors for debugging
|
|
237
|
-
logger.debug(`[NVR BaichuanClient] error (recoverable): ${msg}`);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
logger.error(`[NVR BaichuanClient] error: ${msg}`);
|
|
241
|
-
};
|
|
242
|
-
this.baichuanApi.client.on("error", this.errorListener);
|
|
184
|
+
// Convert event to camera's processEvents format
|
|
185
|
+
const objects: string[] = [];
|
|
186
|
+
let motion = false;
|
|
243
187
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
// Log detailed information about the close
|
|
252
|
-
const errorInfo = this.lastErrorBeforeClose
|
|
253
|
-
? ` (last error: ${this.lastErrorBeforeClose.error} at ${new Date(this.lastErrorBeforeClose.timestamp).toISOString()}, ${now - this.lastErrorBeforeClose.timestamp}ms before close)`
|
|
254
|
-
: '';
|
|
255
|
-
|
|
256
|
-
logger.log(`[NVR BaichuanClient] Socket closed, resetting client state (last disconnect ${timeSinceLastDisconnect}ms ago)${errorInfo}`);
|
|
257
|
-
|
|
258
|
-
// Log connection state before close
|
|
259
|
-
try {
|
|
260
|
-
const wasConnected = this.baichuanApi?.client.isSocketConnected();
|
|
261
|
-
const wasLoggedIn = this.baichuanApi?.client.loggedIn;
|
|
262
|
-
logger.log(`[NVR BaichuanClient] Connection state before close: connected=${wasConnected}, loggedIn=${wasLoggedIn}`);
|
|
263
|
-
|
|
264
|
-
// Try to get last message info if available
|
|
265
|
-
const client = this.baichuanApi?.client as any;
|
|
266
|
-
if (client?.lastRx || client?.lastTx) {
|
|
267
|
-
logger.log(`[NVR BaichuanClient] Last message info: lastRx=${JSON.stringify(client.lastRx)}, lastTx=${JSON.stringify(client.lastTx)}`);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
catch (e) {
|
|
271
|
-
logger.debug(`[NVR BaichuanClient] Could not get connection state: ${e}`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Clear any pending resubscribe timeout
|
|
275
|
-
if (this.resubscribeTimeout) {
|
|
276
|
-
clearTimeout(this.resubscribeTimeout);
|
|
277
|
-
this.resubscribeTimeout = undefined;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const wasSubscribed = this.eventSubscriptionActive;
|
|
281
|
-
const api = this.baichuanApi; // Save reference before clearing
|
|
282
|
-
|
|
283
|
-
// Reset state
|
|
284
|
-
this.baichuanApi = undefined;
|
|
285
|
-
this.baichuanApiPromise = undefined;
|
|
286
|
-
this.eventSubscriptionActive = false;
|
|
287
|
-
this.onSimpleEventHandler = undefined;
|
|
288
|
-
|
|
289
|
-
// Remove event handler from closed client
|
|
290
|
-
if (api && this.onSimpleEventHandler) {
|
|
188
|
+
switch (ev?.type) {
|
|
189
|
+
case 'motion':
|
|
190
|
+
motion = true;
|
|
191
|
+
break;
|
|
192
|
+
case 'doorbell':
|
|
193
|
+
// Handle doorbell if camera supports it
|
|
291
194
|
try {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
// ignore
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Remove close listener (it will be re-added on next connection)
|
|
300
|
-
if (api && this.closeListener) {
|
|
301
|
-
try {
|
|
302
|
-
api.client.off("close", this.closeListener);
|
|
303
|
-
}
|
|
304
|
-
catch {
|
|
305
|
-
// ignore
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Remove error listener
|
|
310
|
-
if (api && this.errorListener) {
|
|
311
|
-
try {
|
|
312
|
-
api.client.off("error", this.errorListener);
|
|
195
|
+
if (typeof (targetCamera as any).handleDoorbellEvent === 'function') {
|
|
196
|
+
(targetCamera as any).handleDoorbellEvent();
|
|
197
|
+
}
|
|
313
198
|
}
|
|
314
|
-
catch {
|
|
315
|
-
|
|
199
|
+
catch (e) {
|
|
200
|
+
logger.warn(`Error handling doorbell event for camera channel ${channel}`, e);
|
|
316
201
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
logger.warn('Failed to resubscribe to events after reconnection', e);
|
|
333
|
-
}
|
|
334
|
-
}, this.reconnectBackoffMs); // Wait for backoff period before resubscribing
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
this.baichuanApi.client.on("close", this.closeListener);
|
|
338
|
-
|
|
339
|
-
return this.baichuanApi;
|
|
340
|
-
})();
|
|
202
|
+
motion = true;
|
|
203
|
+
break;
|
|
204
|
+
case 'people':
|
|
205
|
+
case 'vehicle':
|
|
206
|
+
case 'animal':
|
|
207
|
+
case 'face':
|
|
208
|
+
case 'package':
|
|
209
|
+
case 'other':
|
|
210
|
+
objects.push(ev.type);
|
|
211
|
+
motion = true;
|
|
212
|
+
break;
|
|
213
|
+
default:
|
|
214
|
+
logger.debug(`Unknown event type: ${ev?.type}`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
341
217
|
|
|
342
|
-
|
|
343
|
-
|
|
218
|
+
// Process events on the target camera
|
|
219
|
+
targetCamera.processEvents({ motion, objects }).catch((e) => {
|
|
220
|
+
logger.warn(`Error processing events for camera channel ${channel}`, e);
|
|
221
|
+
});
|
|
344
222
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
this.baichuanApiPromise = undefined;
|
|
223
|
+
catch (e) {
|
|
224
|
+
logger.warn('Error in NVR Native event forwarder', e);
|
|
348
225
|
}
|
|
349
226
|
}
|
|
350
227
|
|
|
351
|
-
async
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// if (this.lastDisconnectTime > 0) {
|
|
356
|
-
// const timeSinceDisconnect = Date.now() - this.lastDisconnectTime;
|
|
357
|
-
// if (timeSinceDisconnect < this.reconnectBackoffMs) {
|
|
358
|
-
// const waitTime = this.reconnectBackoffMs - timeSinceDisconnect;
|
|
359
|
-
// logger.log(`[NVR] Waiting ${waitTime}ms before subscribing to events (backoff)`);
|
|
360
|
-
// await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
361
|
-
// }
|
|
362
|
-
// }
|
|
363
|
-
|
|
364
|
-
// If already subscribed, return
|
|
365
|
-
if (this.eventSubscriptionActive && this.onSimpleEventHandler && this.baichuanApi) {
|
|
366
|
-
// Verify connection is still valid
|
|
367
|
-
if (this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
|
|
368
|
-
logger.log('Event subscription already active');
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
// Connection is invalid, unsubscribe first
|
|
372
|
-
try {
|
|
373
|
-
this.baichuanApi.offSimpleEvent(this.onSimpleEventHandler);
|
|
374
|
-
}
|
|
375
|
-
catch {
|
|
376
|
-
// ignore
|
|
377
|
-
}
|
|
378
|
-
this.eventSubscriptionActive = false;
|
|
379
|
-
this.onSimpleEventHandler = undefined;
|
|
380
|
-
}
|
|
228
|
+
async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
|
|
229
|
+
// Use base class implementation
|
|
230
|
+
return await super.ensureBaichuanClient();
|
|
231
|
+
}
|
|
381
232
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
this.baichuanApi.offSimpleEvent(this.onSimpleEventHandler);
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
// ignore
|
|
389
|
-
}
|
|
390
|
-
}
|
|
233
|
+
async subscribeToAllEvents(): Promise<void> {
|
|
234
|
+
const logger = this.getBaichuanLogger();
|
|
235
|
+
const eventSource = this.storageSettings.values.eventSource || 'Native';
|
|
391
236
|
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// Verify connection is ready
|
|
396
|
-
if (!api.client.isSocketConnected() || !api.client.loggedIn) {
|
|
397
|
-
logger.warn('Cannot subscribe to events: connection not ready');
|
|
237
|
+
// Only subscribe if Native is selected
|
|
238
|
+
if (eventSource !== 'Native') {
|
|
239
|
+
await this.unsubscribeFromAllEvents();
|
|
398
240
|
return;
|
|
399
241
|
}
|
|
400
242
|
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
logger.log(`NVR Baichuan event: ${JSON.stringify(ev)}`);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Find camera for this channel
|
|
409
|
-
const channel = ev?.channel;
|
|
410
|
-
if (channel === undefined) {
|
|
411
|
-
if (this.storageSettings.values.debugEvents) {
|
|
412
|
-
logger.debug('Event has no channel, ignoring');
|
|
413
|
-
}
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Find camera with matching channel
|
|
418
|
-
let targetCamera: ReolinkNativeCamera | ReolinkNativeBatteryCamera | undefined;
|
|
419
|
-
for (const camera of this.cameraNativeMap.values()) {
|
|
420
|
-
if (camera && camera.storageSettings.values.rtspChannel === channel) {
|
|
421
|
-
targetCamera = camera;
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
243
|
+
// Use base class implementation
|
|
244
|
+
await super.subscribeToEvents();
|
|
245
|
+
logger.log('Subscribed to all events for NVR cameras');
|
|
246
|
+
}
|
|
425
247
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
248
|
+
async unsubscribeFromAllEvents(): Promise<void> {
|
|
249
|
+
// Use base class implementation
|
|
250
|
+
await super.unsubscribeFromEvents();
|
|
251
|
+
}
|
|
432
252
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
motion = true;
|
|
440
|
-
break;
|
|
441
|
-
case 'doorbell':
|
|
442
|
-
// Handle doorbell if camera supports it
|
|
443
|
-
try {
|
|
444
|
-
if (typeof (targetCamera as any).handleDoorbellEvent === 'function') {
|
|
445
|
-
(targetCamera as any).handleDoorbellEvent();
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
catch (e) {
|
|
449
|
-
logger.warn(`Error handling doorbell event for camera channel ${channel}`, e);
|
|
450
|
-
}
|
|
451
|
-
motion = true;
|
|
452
|
-
break;
|
|
453
|
-
case 'people':
|
|
454
|
-
case 'vehicle':
|
|
455
|
-
case 'animal':
|
|
456
|
-
case 'face':
|
|
457
|
-
case 'package':
|
|
458
|
-
case 'other':
|
|
459
|
-
objects.push(ev.type);
|
|
460
|
-
motion = true;
|
|
461
|
-
break;
|
|
462
|
-
default:
|
|
463
|
-
if (this.storageSettings.values.debugEvents) {
|
|
464
|
-
logger.debug(`Unknown event type: ${ev?.type}`);
|
|
465
|
-
}
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
253
|
+
/**
|
|
254
|
+
* Reinitialize event subscriptions based on selected event source
|
|
255
|
+
*/
|
|
256
|
+
private async reinitEventSubscriptions(): Promise<void> {
|
|
257
|
+
const logger = this.getBaichuanLogger();
|
|
258
|
+
const { eventSource } = this.storageSettings.values;
|
|
468
259
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
catch (e) {
|
|
475
|
-
logger.warn('Error in NVR onSimpleEvent handler', e);
|
|
476
|
-
}
|
|
477
|
-
};
|
|
260
|
+
// Unsubscribe from Native events if switching away
|
|
261
|
+
if (eventSource !== 'Native') {
|
|
262
|
+
await this.unsubscribeFromAllEvents();
|
|
263
|
+
} else {
|
|
478
264
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
this.eventSubscriptionActive = true;
|
|
483
|
-
logger.log('Subscribed to all events for NVR cameras');
|
|
484
|
-
}
|
|
485
|
-
catch (e) {
|
|
486
|
-
logger.warn('Failed to subscribe to events', e);
|
|
487
|
-
this.eventSubscriptionActive = false;
|
|
488
|
-
this.onSimpleEventHandler = undefined;
|
|
265
|
+
this.subscribeToAllEvents().catch((e) => {
|
|
266
|
+
logger.warn('Failed to subscribe to Native events', e);
|
|
267
|
+
});
|
|
489
268
|
}
|
|
269
|
+
|
|
270
|
+
logger.log(`Event source set to: ${eventSource}`);
|
|
490
271
|
}
|
|
491
272
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Forward events from CGI source to cameras
|
|
275
|
+
*/
|
|
276
|
+
private forwardCgiEvents(eventsRes: Record<number, EventsResponse>): void {
|
|
277
|
+
const logger = this.getBaichuanLogger();
|
|
278
|
+
|
|
279
|
+
logger.debug(`CGI Events call result: ${JSON.stringify(eventsRes)}`);
|
|
280
|
+
|
|
281
|
+
// Use channel map for efficient lookup
|
|
282
|
+
for (const [channel, nativeId] of this.channelToNativeIdMap.entries()) {
|
|
283
|
+
const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
|
|
284
|
+
const cameraEventsData = eventsRes[channel];
|
|
285
|
+
if (cameraEventsData && targetCamera) {
|
|
286
|
+
targetCamera.processEvents(cameraEventsData);
|
|
502
287
|
}
|
|
503
288
|
}
|
|
504
|
-
|
|
505
|
-
this.eventSubscriptionActive = false;
|
|
506
|
-
this.onSimpleEventHandler = undefined;
|
|
507
289
|
}
|
|
508
290
|
|
|
509
291
|
async init() {
|
|
510
292
|
const api = await this.ensureClient();
|
|
511
|
-
const logger = this.
|
|
293
|
+
const logger = this.getBaichuanLogger();
|
|
512
294
|
await this.updateDeviceInfo();
|
|
513
295
|
|
|
514
|
-
//
|
|
515
|
-
this.
|
|
516
|
-
logger.warn('Failed to subscribe to events during init', e);
|
|
517
|
-
});
|
|
296
|
+
// Initialize event subscriptions based on selected source
|
|
297
|
+
await this.reinitEventSubscriptions();
|
|
518
298
|
|
|
519
299
|
setInterval(async () => {
|
|
520
300
|
if (this.processing || !api) {
|
|
@@ -535,33 +315,22 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
535
315
|
const { hubData } = await api.getHubInfo();
|
|
536
316
|
const { devicesData, channelsResponse, response } = await api.getDevicesInfo();
|
|
537
317
|
logger.log('Hub info data fetched');
|
|
538
|
-
|
|
539
|
-
logger.log(`${JSON.stringify({ hubData, devicesData, channelsResponse, response })}`);
|
|
540
|
-
}
|
|
318
|
+
logger.debug(`${JSON.stringify({ hubData, devicesData, channelsResponse, response })}`);
|
|
541
319
|
|
|
542
320
|
await this.discoverDevices(true);
|
|
543
321
|
}
|
|
544
322
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (
|
|
548
|
-
|
|
323
|
+
// Only fetch and forward CGI events if CGI is selected as event source
|
|
324
|
+
const { eventSource } = this.storageSettings.values;
|
|
325
|
+
if (eventSource === 'CGI') {
|
|
326
|
+
const eventsRes = await api.getAllChannelsEvents();
|
|
327
|
+
this.forwardCgiEvents(eventsRes.parsed);
|
|
549
328
|
}
|
|
550
|
-
this.cameraNativeMap.forEach((camera) => {
|
|
551
|
-
if (camera) {
|
|
552
|
-
const channel = camera.storageSettings.values.rtspChannel;
|
|
553
|
-
const cameraEventsData = eventsRes?.parsed[channel];
|
|
554
|
-
if (cameraEventsData) {
|
|
555
|
-
camera.processEvents(cameraEventsData);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
329
|
|
|
330
|
+
// Always fetch battery info (not event-related)
|
|
560
331
|
const { batteryInfoData, response } = await api.getAllChannelsBatteryInfo();
|
|
561
332
|
|
|
562
|
-
|
|
563
|
-
logger.debug(`Battery info call result: ${JSON.stringify({ batteryInfoData, response })}`);
|
|
564
|
-
}
|
|
333
|
+
logger.debug(`Battery info call result: ${JSON.stringify({ batteryInfoData, response })}`);
|
|
565
334
|
|
|
566
335
|
this.cameraNativeMap.forEach((camera) => {
|
|
567
336
|
if (camera) {
|
|
@@ -578,7 +347,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
578
347
|
}
|
|
579
348
|
});
|
|
580
349
|
} catch (e) {
|
|
581
|
-
|
|
350
|
+
logger.error('Error on events flow', e);
|
|
582
351
|
} finally {
|
|
583
352
|
this.processing = false;
|
|
584
353
|
}
|
|
@@ -597,7 +366,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
597
366
|
deviceData,
|
|
598
367
|
});
|
|
599
368
|
} catch (e) {
|
|
600
|
-
this.
|
|
369
|
+
this.getBaichuanLogger().warn('Failed to fetch device info', e);
|
|
601
370
|
}
|
|
602
371
|
}
|
|
603
372
|
|
|
@@ -651,7 +420,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
651
420
|
|
|
652
421
|
async syncEntitiesFromRemote() {
|
|
653
422
|
const api = await this.ensureClient();
|
|
654
|
-
const logger = this.
|
|
423
|
+
const logger = this.getBaichuanLogger();
|
|
655
424
|
|
|
656
425
|
logger.log('Starting channels discovery using getDevicesInfo...');
|
|
657
426
|
|
|
@@ -687,6 +456,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
687
456
|
}
|
|
688
457
|
};
|
|
689
458
|
|
|
459
|
+
this.channelToNativeIdMap.set(channel, nativeId);
|
|
460
|
+
|
|
690
461
|
if (sdk.deviceManager.getNativeIds().includes(nativeId)) {
|
|
691
462
|
continue;
|
|
692
463
|
}
|
|
@@ -761,7 +532,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
761
532
|
await sdk.deviceManager.onDeviceDiscovered(actualDevice);
|
|
762
533
|
|
|
763
534
|
const device = await this.getDevice(adopt.nativeId);
|
|
764
|
-
this.
|
|
535
|
+
this.getBaichuanLogger().debug('Adopted device', entry, device?.name);
|
|
765
536
|
const { username, password, ipAddress } = this.storageSettings.values;
|
|
766
537
|
|
|
767
538
|
device.storageSettings.values.rtspChannel = entry.rtspChannel;
|