@apocaliss92/scrypted-reolink-native 0.1.8 → 0.1.9
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 +0 -1
- package/src/nvr.ts +248 -326
- package/logs.txt +0 -7361
package/src/nvr.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
4
|
import { ReolinkNativeCamera } from "./camera";
|
|
@@ -14,6 +14,17 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
14
14
|
type: 'boolean',
|
|
15
15
|
immediate: true,
|
|
16
16
|
},
|
|
17
|
+
eventSource: {
|
|
18
|
+
title: 'Event Source',
|
|
19
|
+
description: 'Select the source for camera events: Native (Baichuan) or CGI (HTTP polling)',
|
|
20
|
+
type: 'string',
|
|
21
|
+
choices: ['Native', 'CGI'],
|
|
22
|
+
defaultValue: 'Native',
|
|
23
|
+
immediate: true,
|
|
24
|
+
onPut: async () => {
|
|
25
|
+
await this.reinitEventSubscriptions();
|
|
26
|
+
}
|
|
27
|
+
},
|
|
17
28
|
ipAddress: {
|
|
18
29
|
title: 'IP address',
|
|
19
30
|
type: 'string',
|
|
@@ -46,15 +57,11 @@ 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
62
|
private eventSubscriptionActive = false;
|
|
51
|
-
private onSimpleEventHandler?: (ev: ReolinkSimpleEvent) => void;
|
|
52
|
-
private closeListener?: () => void;
|
|
53
63
|
private errorListener?: (err: unknown) => void;
|
|
54
|
-
private
|
|
55
|
-
private lastErrorBeforeClose: { error: string; timestamp: number } | undefined;
|
|
56
|
-
private readonly reconnectBackoffMs: number = 2000; // 2 seconds minimum between reconnects
|
|
57
|
-
private resubscribeTimeout?: NodeJS.Timeout;
|
|
64
|
+
private closeListener?: () => void;
|
|
58
65
|
|
|
59
66
|
constructor(nativeId: string, plugin: ReolinkNativePlugin) {
|
|
60
67
|
super(nativeId);
|
|
@@ -74,58 +81,109 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
74
81
|
return this.console;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Centralized cleanup method for Baichuan API
|
|
86
|
+
* Removes all listeners, closes connection, and resets state
|
|
87
|
+
*/
|
|
88
|
+
private async cleanupBaichuanApi(): Promise<void> {
|
|
89
|
+
if (!this.baichuanApi) {
|
|
90
|
+
return;
|
|
84
91
|
}
|
|
85
|
-
this.nvrApi = undefined;
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
if (this.resubscribeTimeout) {
|
|
89
|
-
clearTimeout(this.resubscribeTimeout);
|
|
90
|
-
this.resubscribeTimeout = undefined;
|
|
91
|
-
}
|
|
93
|
+
const api = this.baichuanApi;
|
|
92
94
|
|
|
93
95
|
// Unsubscribe from events first
|
|
94
96
|
await this.unsubscribeFromAllEvents();
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
// Remove all listeners
|
|
99
|
+
try {
|
|
100
|
+
api.client.off("close", this.closeListener);
|
|
101
|
+
} catch {
|
|
102
|
+
// ignore
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
api.client.off("error", this.errorListener);
|
|
107
|
+
} catch {
|
|
108
|
+
// ignore
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Close connection if still connected
|
|
112
|
+
try {
|
|
113
|
+
if (api.client.isSocketConnected()) {
|
|
114
|
+
await api.close();
|
|
106
115
|
}
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
} catch {
|
|
117
|
+
// ignore
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Reset state
|
|
121
|
+
this.baichuanApi = undefined;
|
|
122
|
+
this.baichuanApiPromise = undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Attach error and close listeners to Baichuan API
|
|
127
|
+
*/
|
|
128
|
+
private attachBaichuanListeners(api: ReolinkBaichuanApi): void {
|
|
129
|
+
const logger = this.getLogger();
|
|
130
|
+
|
|
131
|
+
// Error listener
|
|
132
|
+
this.errorListener = (err: unknown) => {
|
|
133
|
+
const msg = (err as any)?.message || (err as any)?.toString?.() || String(err);
|
|
134
|
+
|
|
135
|
+
// Only log if it's not a recoverable error to avoid spam
|
|
136
|
+
if (typeof msg === 'string' && (
|
|
137
|
+
msg.includes('Baichuan socket closed') ||
|
|
138
|
+
msg.includes('Baichuan UDP stream closed') ||
|
|
139
|
+
msg.includes('Not running')
|
|
140
|
+
)) {
|
|
141
|
+
logger.debug(`[NVR BaichuanClient] error (recoverable): ${msg}`);
|
|
142
|
+
return;
|
|
117
143
|
}
|
|
144
|
+
logger.error(`[NVR BaichuanClient] error: ${msg}`);
|
|
145
|
+
};
|
|
118
146
|
|
|
147
|
+
// Close listener
|
|
148
|
+
this.closeListener = async () => {
|
|
119
149
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
150
|
+
const wasConnected = api.client.isSocketConnected();
|
|
151
|
+
const wasLoggedIn = api.client.loggedIn;
|
|
152
|
+
logger.log(`[NVR BaichuanClient] Connection state before close: connected=${wasConnected}, loggedIn=${wasLoggedIn}`);
|
|
153
|
+
|
|
154
|
+
// Try to get last message info if available
|
|
155
|
+
const client = api.client as any;
|
|
156
|
+
if (client?.lastRx || client?.lastTx) {
|
|
157
|
+
logger.log(`[NVR BaichuanClient] Last message info: lastRx=${JSON.stringify(client.lastRx)}, lastTx=${JSON.stringify(client.lastTx)}`);
|
|
122
158
|
}
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
logger.debug(`[NVR BaichuanClient] Could not get connection state: ${e}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Cleanup and reinit
|
|
165
|
+
await this.cleanupBaichuanApi();
|
|
166
|
+
await this.reinit();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Attach listeners
|
|
170
|
+
api.client.on("error", this.errorListener);
|
|
171
|
+
api.client.on("close", this.closeListener);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async reinit() {
|
|
175
|
+
// Cleanup CGI API
|
|
176
|
+
if (this.nvrApi) {
|
|
177
|
+
try {
|
|
178
|
+
await this.nvrApi.logout();
|
|
123
179
|
} catch {
|
|
124
180
|
// ignore
|
|
125
181
|
}
|
|
126
182
|
}
|
|
127
|
-
this.
|
|
128
|
-
|
|
183
|
+
this.nvrApi = undefined;
|
|
184
|
+
|
|
185
|
+
// Cleanup Baichuan API (this handles all listeners and connection)
|
|
186
|
+
await this.cleanupBaichuanApi();
|
|
129
187
|
}
|
|
130
188
|
|
|
131
189
|
async ensureClient(): Promise<ReolinkCgiApi> {
|
|
@@ -149,6 +207,88 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
149
207
|
return this.nvrApi;
|
|
150
208
|
}
|
|
151
209
|
|
|
210
|
+
private forwardNativeEvent(ev: ReolinkSimpleEvent): void {
|
|
211
|
+
const logger = this.getLogger();
|
|
212
|
+
|
|
213
|
+
const eventSource = this.storageSettings.values.eventSource || 'Native';
|
|
214
|
+
if (eventSource !== 'Native') {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
if (this.storageSettings.values.debugEvents) {
|
|
220
|
+
logger.log(`NVR Baichuan event: ${JSON.stringify(ev)}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Find camera for this channel
|
|
224
|
+
const channel = ev?.channel;
|
|
225
|
+
if (channel === undefined) {
|
|
226
|
+
if (this.storageSettings.values.debugEvents) {
|
|
227
|
+
logger.debug('Event has no channel, ignoring');
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const nativeId = this.channelToNativeIdMap.get(channel);
|
|
233
|
+
const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
|
|
234
|
+
|
|
235
|
+
if (!targetCamera) {
|
|
236
|
+
if (this.storageSettings.values.debugEvents) {
|
|
237
|
+
logger.debug(`No camera found for channel ${channel}, ignoring event`);
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Convert event to camera's processEvents format
|
|
243
|
+
const objects: string[] = [];
|
|
244
|
+
let motion = false;
|
|
245
|
+
|
|
246
|
+
switch (ev?.type) {
|
|
247
|
+
case 'motion':
|
|
248
|
+
motion = true;
|
|
249
|
+
break;
|
|
250
|
+
case 'doorbell':
|
|
251
|
+
// Handle doorbell if camera supports it
|
|
252
|
+
try {
|
|
253
|
+
if (typeof (targetCamera as any).handleDoorbellEvent === 'function') {
|
|
254
|
+
(targetCamera as any).handleDoorbellEvent();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
logger.warn(`Error handling doorbell event for camera channel ${channel}`, e);
|
|
259
|
+
}
|
|
260
|
+
motion = true;
|
|
261
|
+
break;
|
|
262
|
+
case 'people':
|
|
263
|
+
case 'vehicle':
|
|
264
|
+
case 'animal':
|
|
265
|
+
case 'face':
|
|
266
|
+
case 'package':
|
|
267
|
+
case 'other':
|
|
268
|
+
objects.push(ev.type);
|
|
269
|
+
motion = true;
|
|
270
|
+
break;
|
|
271
|
+
default:
|
|
272
|
+
if (this.storageSettings.values.debugEvents) {
|
|
273
|
+
logger.debug(`Unknown event type: ${ev?.type}`);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Process events on the target camera
|
|
279
|
+
targetCamera.processEvents({ motion, objects }).catch((e) => {
|
|
280
|
+
logger.warn(`Error processing events for camera channel ${channel}`, e);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
catch (e) {
|
|
284
|
+
logger.warn('Error in NVR Native event forwarder', e);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async onSimpleEventHandler(ev: ReolinkSimpleEvent) {
|
|
289
|
+
this.forwardNativeEvent(ev);
|
|
290
|
+
}
|
|
291
|
+
|
|
152
292
|
async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
|
|
153
293
|
// Reuse existing client if socket is still connected and logged in
|
|
154
294
|
if (this.baichuanApi && this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
|
|
@@ -166,36 +306,7 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
166
306
|
|
|
167
307
|
// Clean up old client if exists
|
|
168
308
|
if (this.baichuanApi) {
|
|
169
|
-
|
|
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
|
-
}
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
if (this.baichuanApi.client.isSocketConnected()) {
|
|
193
|
-
await this.baichuanApi.close();
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
// ignore
|
|
198
|
-
}
|
|
309
|
+
await this.cleanupBaichuanApi();
|
|
199
310
|
}
|
|
200
311
|
|
|
201
312
|
// Create new Baichuan client
|
|
@@ -206,7 +317,6 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
206
317
|
password,
|
|
207
318
|
transport: 'tcp',
|
|
208
319
|
logger: this.getLogger(),
|
|
209
|
-
// rebootAfterDisconnectionsPerMinute: 5,
|
|
210
320
|
});
|
|
211
321
|
|
|
212
322
|
await this.baichuanApi.login();
|
|
@@ -216,125 +326,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
216
326
|
throw new Error('Socket not connected after login');
|
|
217
327
|
}
|
|
218
328
|
|
|
219
|
-
//
|
|
220
|
-
this.
|
|
221
|
-
const logger = this.getLogger();
|
|
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);
|
|
243
|
-
|
|
244
|
-
// Listen for socket disconnection to reset client state
|
|
245
|
-
this.closeListener = () => {
|
|
246
|
-
const logger = this.getLogger();
|
|
247
|
-
const now = Date.now();
|
|
248
|
-
const timeSinceLastDisconnect = now - this.lastDisconnectTime;
|
|
249
|
-
this.lastDisconnectTime = now;
|
|
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) {
|
|
291
|
-
try {
|
|
292
|
-
api.offSimpleEvent(this.onSimpleEventHandler);
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
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);
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
// ignore
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
this.closeListener = undefined;
|
|
320
|
-
this.errorListener = undefined;
|
|
321
|
-
this.lastErrorBeforeClose = undefined;
|
|
322
|
-
|
|
323
|
-
// Try to resubscribe when connection is restored (async, don't block)
|
|
324
|
-
// Only if we had an active subscription and enough time has passed
|
|
325
|
-
if (wasSubscribed && timeSinceLastDisconnect >= this.reconnectBackoffMs) {
|
|
326
|
-
this.resubscribeTimeout = setTimeout(async () => {
|
|
327
|
-
this.resubscribeTimeout = undefined;
|
|
328
|
-
try {
|
|
329
|
-
await this.subscribeToAllEvents();
|
|
330
|
-
}
|
|
331
|
-
catch (e) {
|
|
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);
|
|
329
|
+
// Attach listeners (error and close)
|
|
330
|
+
this.attachBaichuanListeners(this.baichuanApi);
|
|
338
331
|
|
|
339
332
|
return this.baichuanApi;
|
|
340
333
|
})();
|
|
@@ -350,132 +343,29 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
350
343
|
|
|
351
344
|
async subscribeToAllEvents(): Promise<void> {
|
|
352
345
|
const logger = this.getLogger();
|
|
353
|
-
|
|
354
|
-
//
|
|
355
|
-
|
|
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
|
|
346
|
+
|
|
347
|
+
// If already subscribed and connection is valid, return
|
|
348
|
+
if (this.eventSubscriptionActive && this.baichuanApi) {
|
|
367
349
|
if (this.baichuanApi.client.isSocketConnected() && this.baichuanApi.client.loggedIn) {
|
|
368
350
|
logger.log('Event subscription already active');
|
|
369
351
|
return;
|
|
370
352
|
}
|
|
371
|
-
// Connection is invalid,
|
|
372
|
-
try {
|
|
373
|
-
this.baichuanApi.offSimpleEvent(this.onSimpleEventHandler);
|
|
374
|
-
}
|
|
375
|
-
catch {
|
|
376
|
-
// ignore
|
|
377
|
-
}
|
|
353
|
+
// Connection is invalid, reset subscription state
|
|
378
354
|
this.eventSubscriptionActive = false;
|
|
379
|
-
this.onSimpleEventHandler = undefined;
|
|
380
355
|
}
|
|
381
356
|
|
|
382
|
-
// Unsubscribe first if handler exists
|
|
383
|
-
|
|
384
|
-
try {
|
|
385
|
-
this.baichuanApi.offSimpleEvent(this.onSimpleEventHandler);
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
// ignore
|
|
389
|
-
}
|
|
390
|
-
}
|
|
357
|
+
// Unsubscribe first if handler exists (idempotent)
|
|
358
|
+
await this.unsubscribeFromAllEvents();
|
|
391
359
|
|
|
392
360
|
// Get Baichuan client connection
|
|
393
361
|
const api = await this.ensureBaichuanClient();
|
|
394
|
-
|
|
362
|
+
|
|
395
363
|
// Verify connection is ready
|
|
396
364
|
if (!api.client.isSocketConnected() || !api.client.loggedIn) {
|
|
397
365
|
logger.warn('Cannot subscribe to events: connection not ready');
|
|
398
366
|
return;
|
|
399
367
|
}
|
|
400
368
|
|
|
401
|
-
// Create event handler that distributes events to cameras
|
|
402
|
-
this.onSimpleEventHandler = (ev: ReolinkSimpleEvent) => {
|
|
403
|
-
try {
|
|
404
|
-
if (this.storageSettings.values.debugEvents) {
|
|
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
|
-
}
|
|
425
|
-
|
|
426
|
-
if (!targetCamera) {
|
|
427
|
-
if (this.storageSettings.values.debugEvents) {
|
|
428
|
-
logger.debug(`No camera found for channel ${channel}, ignoring event`);
|
|
429
|
-
}
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Convert event to camera's processEvents format
|
|
434
|
-
const objects: string[] = [];
|
|
435
|
-
let motion = false;
|
|
436
|
-
|
|
437
|
-
switch (ev?.type) {
|
|
438
|
-
case 'motion':
|
|
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
|
-
}
|
|
468
|
-
|
|
469
|
-
// Process events on the target camera
|
|
470
|
-
targetCamera.processEvents({ motion, objects }).catch((e) => {
|
|
471
|
-
logger.warn(`Error processing events for camera channel ${channel}`, e);
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
catch (e) {
|
|
475
|
-
logger.warn('Error in NVR onSimpleEvent handler', e);
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
|
-
|
|
479
369
|
// Subscribe to events
|
|
480
370
|
try {
|
|
481
371
|
await api.onSimpleEvent(this.onSimpleEventHandler);
|
|
@@ -485,14 +375,14 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
485
375
|
catch (e) {
|
|
486
376
|
logger.warn('Failed to subscribe to events', e);
|
|
487
377
|
this.eventSubscriptionActive = false;
|
|
488
|
-
this.onSimpleEventHandler = undefined;
|
|
489
378
|
}
|
|
490
379
|
}
|
|
491
380
|
|
|
492
381
|
async unsubscribeFromAllEvents(): Promise<void> {
|
|
493
382
|
const logger = this.getLogger();
|
|
494
|
-
|
|
495
|
-
if
|
|
383
|
+
|
|
384
|
+
// Only unsubscribe if we have an active subscription
|
|
385
|
+
if (this.eventSubscriptionActive && this.baichuanApi) {
|
|
496
386
|
try {
|
|
497
387
|
this.baichuanApi.offSimpleEvent(this.onSimpleEventHandler);
|
|
498
388
|
logger.log('Unsubscribed from all events');
|
|
@@ -501,9 +391,48 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
501
391
|
logger.warn('Error unsubscribing from events', e);
|
|
502
392
|
}
|
|
503
393
|
}
|
|
504
|
-
|
|
394
|
+
|
|
505
395
|
this.eventSubscriptionActive = false;
|
|
506
|
-
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Reinitialize event subscriptions based on selected event source
|
|
400
|
+
*/
|
|
401
|
+
private async reinitEventSubscriptions(): Promise<void> {
|
|
402
|
+
const logger = this.getLogger();
|
|
403
|
+
const { eventSource } = this.storageSettings.values;
|
|
404
|
+
|
|
405
|
+
// Unsubscribe from Native events if switching away
|
|
406
|
+
if (eventSource !== 'Native') {
|
|
407
|
+
await this.unsubscribeFromAllEvents();
|
|
408
|
+
} else {
|
|
409
|
+
|
|
410
|
+
this.subscribeToAllEvents().catch((e) => {
|
|
411
|
+
logger.warn('Failed to subscribe to Native events', e);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
logger.log(`Event source set to: ${eventSource}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Forward events from CGI source to cameras
|
|
420
|
+
*/
|
|
421
|
+
private forwardCgiEvents(eventsRes: Record<number, EventsResponse>): void {
|
|
422
|
+
const logger = this.getLogger();
|
|
423
|
+
|
|
424
|
+
if (this.storageSettings.values.debugEvents) {
|
|
425
|
+
logger.debug(`CGI Events call result: ${JSON.stringify(eventsRes)}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Use channel map for efficient lookup
|
|
429
|
+
for (const [channel, nativeId] of this.channelToNativeIdMap.entries()) {
|
|
430
|
+
const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
|
|
431
|
+
const cameraEventsData = eventsRes[channel];
|
|
432
|
+
if (cameraEventsData && targetCamera) {
|
|
433
|
+
targetCamera.processEvents(cameraEventsData);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
507
436
|
}
|
|
508
437
|
|
|
509
438
|
async init() {
|
|
@@ -511,10 +440,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
511
440
|
const logger = this.getLogger();
|
|
512
441
|
await this.updateDeviceInfo();
|
|
513
442
|
|
|
514
|
-
//
|
|
515
|
-
this.
|
|
516
|
-
logger.warn('Failed to subscribe to events during init', e);
|
|
517
|
-
});
|
|
443
|
+
// Initialize event subscriptions based on selected source
|
|
444
|
+
await this.reinitEventSubscriptions();
|
|
518
445
|
|
|
519
446
|
setInterval(async () => {
|
|
520
447
|
if (this.processing || !api) {
|
|
@@ -542,21 +469,14 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
542
469
|
await this.discoverDevices(true);
|
|
543
470
|
}
|
|
544
471
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (
|
|
548
|
-
|
|
472
|
+
// Only fetch and forward CGI events if CGI is selected as event source
|
|
473
|
+
const { eventSource } = this.storageSettings.values;
|
|
474
|
+
if (eventSource === 'CGI') {
|
|
475
|
+
const eventsRes = await api.getAllChannelsEvents();
|
|
476
|
+
this.forwardCgiEvents(eventsRes.parsed);
|
|
549
477
|
}
|
|
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
478
|
|
|
479
|
+
// Always fetch battery info (not event-related)
|
|
560
480
|
const { batteryInfoData, response } = await api.getAllChannelsBatteryInfo();
|
|
561
481
|
|
|
562
482
|
if (this.storageSettings.values.debugEvents) {
|
|
@@ -687,6 +607,8 @@ export class ReolinkNativeNvrDevice extends ScryptedDeviceBase implements Settin
|
|
|
687
607
|
}
|
|
688
608
|
};
|
|
689
609
|
|
|
610
|
+
this.channelToNativeIdMap.set(channel, nativeId);
|
|
611
|
+
|
|
690
612
|
if (sdk.deviceManager.getNativeIds().includes(nativeId)) {
|
|
691
613
|
continue;
|
|
692
614
|
}
|