@apocaliss92/scrypted-reolink-native 0.3.17 → 0.4.0

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/src/nvr.ts CHANGED
@@ -1,528 +1,639 @@
1
- import type { ReolinkBaichuanApi, ReolinkBaichuanDeviceSummary, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
- import sdk, { AdoptDevice, Device, DeviceDiscovery, DeviceProvider, DiscoveredDevice, Reboot, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
1
+ import type {
2
+ ReolinkBaichuanApi,
3
+ ReolinkBaichuanDeviceSummary,
4
+ ReolinkSimpleEvent,
5
+ } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
6
+ import sdk, {
7
+ AdoptDevice,
8
+ Device,
9
+ DeviceDiscovery,
10
+ DeviceProvider,
11
+ DiscoveredDevice,
12
+ Reboot,
13
+ ScryptedDeviceType,
14
+ ScryptedInterface,
15
+ Setting,
16
+ Settings,
17
+ SettingValue,
18
+ } from "@scrypted/sdk";
3
19
  import { StorageSettings } from "@scrypted/sdk/storage-settings";
4
- import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
20
+ import {
21
+ BaseBaichuanClass,
22
+ type BaichuanConnectionCallbacks,
23
+ type BaichuanConnectionConfig,
24
+ } from "./baichuan-base";
5
25
  import { ReolinkCamera } from "./camera";
6
- import { convertDebugLogsToApiOptions, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
26
+ import {
27
+ convertDebugLogsToApiOptions,
28
+ getApiRelevantDebugLogs,
29
+ getDebugLogChoices,
30
+ } from "./debug-options";
7
31
  import ReolinkNativePlugin from "./main";
8
32
  import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
9
- import { batteryCameraSuffix, batteryMultifocalSuffix, cameraSuffix, getDeviceInterfaces, multifocalSuffix, updateDeviceInfo } from "./utils";
10
-
11
- export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Settings, DeviceDiscovery, DeviceProvider, Reboot {
12
- private readonly onSimpleEventBound = (ev: ReolinkSimpleEvent) => this.onSimpleEvent(ev);
13
-
14
- storageSettings = new StorageSettings(this, {
15
- debugLogs: {
16
- title: 'Debug Events',
17
- type: 'boolean',
18
- immediate: true,
19
- },
20
- // eventSource: {
21
- // title: 'Event Source',
22
- // description: 'Select the source for camera events: Native (Baichuan) or CGI (HTTP polling)',
23
- // type: 'string',
24
- // choices: ['Native', 'CGI'],
25
- // defaultValue: 'Native',
26
- // immediate: true,
27
- // onPut: async () => {
28
- // await this.reinitEventSubscriptions();
29
- // }
30
- // },
31
- ipAddress: {
32
- title: 'IP address',
33
- type: 'string',
34
- onPut: async () => await this.reinit()
35
- },
36
- username: {
37
- title: 'Username',
38
- placeholder: 'admin',
39
- defaultValue: 'admin',
40
- type: 'string',
41
- onPut: async () => await this.reinit()
42
- },
43
- password: {
44
- title: 'Password',
45
- type: 'password',
46
- onPut: async () => await this.reinit()
47
- },
48
- diagnosticsRun: {
49
- subgroup: 'Advanced',
50
- title: 'Run Diagnostics',
51
- description: 'Collect NVR diagnostics and display results in logs.',
52
- type: 'button',
53
- immediate: true,
54
- onPut: async () => {
55
- await this.runNvrDiagnostics();
56
- },
57
- },
58
- socketApiDebugLogs: {
59
- subgroup: 'Advanced',
60
- title: 'Socket API Debug Logs',
61
- description: 'Enable specific debug logs.',
62
- multiple: true,
63
- combobox: true,
64
- immediate: true,
65
- defaultValue: [],
66
- choices: getDebugLogChoices(),
67
- onPut: async (ov, value) => {
68
- const logger = this.getBaichuanLogger();
69
- const oldApiOptions = getApiRelevantDebugLogs(ov || []);
70
- const newApiOptions = getApiRelevantDebugLogs(value || []);
71
-
72
- const oldSel = new Set(oldApiOptions);
73
- const newSel = new Set(newApiOptions);
74
-
75
- const changed = oldSel.size !== newSel.size || Array.from(oldSel).some((k) => !newSel.has(k));
76
- if (changed) {
77
- // Clear any existing timeout
78
- if (this.debugLogsResetTimeout) {
79
- clearTimeout(this.debugLogsResetTimeout);
80
- this.debugLogsResetTimeout = undefined;
81
- }
82
-
83
- this.debugLogsResetTimeout = setTimeout(async () => {
84
- this.debugLogsResetTimeout = undefined;
85
- try {
86
- this.baichuanApi = undefined;
87
- this.ensureClientPromise = undefined;
88
- await this.ensureBaichuanClient();
89
- } catch (e) {
90
- logger.warn('Failed to reset client after debug logs change', e?.message || String(e));
91
- }
92
- }, 2000);
93
- }
94
- },
95
- },
96
- });
97
- plugin: ReolinkNativePlugin;
98
- discoveredDevices = new Map<string, {
99
- device: Device;
100
- description: string;
101
- rtspChannel: number;
102
- deviceData: ReolinkBaichuanDeviceSummary;
103
- }>();
104
- cameraNativeMap = new Map<string, ReolinkCamera>();
105
- private channelToNativeIdMap = new Map<number, string>();
106
- private discoverDevicesPromise: Promise<DiscoveredDevice[]> | undefined;
107
- processing = false;
108
- private initReinitTimeout: NodeJS.Timeout | undefined;
109
- private debugLogsResetTimeout: NodeJS.Timeout | undefined;
110
-
111
- constructor(nativeId: string, plugin: ReolinkNativePlugin) {
112
- super(nativeId, "tcp");
113
- this.plugin = plugin;
114
-
115
- this.scheduleInit();
116
- }
117
-
118
- async reboot(): Promise<void> {
119
- const api = await this.ensureBaichuanClient();
120
- await api.reboot();
121
- }
122
-
123
- protected getConnectionConfig(): BaichuanConnectionConfig {
124
- const { ipAddress, username, password } = this.storageSettings.values;
125
- if (!ipAddress || !username || !password) {
126
- throw new Error('Missing NVR credentials');
127
- }
128
-
129
- const debugOptions = this.getBaichuanDebugOptions();
130
-
131
- return {
132
- host: ipAddress,
133
- username,
134
- password,
135
- transport: 'tcp',
136
- debugOptions,
137
- };
138
- }
139
-
140
- protected getStreamClientInputs(): BaichuanConnectionConfig {
141
- const { ipAddress, username, password } = this.storageSettings.values;
142
- if (!ipAddress || !username || !password) {
143
- throw new Error('Missing NVR credentials');
33
+ import {
34
+ batteryCameraSuffix,
35
+ batteryMultifocalSuffix,
36
+ cameraSuffix,
37
+ getDeviceInterfaces,
38
+ multifocalSuffix,
39
+ updateDeviceInfo,
40
+ } from "./utils";
41
+
42
+ export class ReolinkNativeNvrDevice
43
+ extends BaseBaichuanClass
44
+ implements Settings, DeviceDiscovery, DeviceProvider, Reboot
45
+ {
46
+ private readonly onSimpleEventBound = (ev: ReolinkSimpleEvent) =>
47
+ this.onSimpleEvent(ev);
48
+
49
+ storageSettings = new StorageSettings(this, {
50
+ debugLogs: {
51
+ title: "Debug Events",
52
+ type: "boolean",
53
+ immediate: true,
54
+ },
55
+ // eventSource: {
56
+ // title: 'Event Source',
57
+ // description: 'Select the source for camera events: Native (Baichuan) or CGI (HTTP polling)',
58
+ // type: 'string',
59
+ // choices: ['Native', 'CGI'],
60
+ // defaultValue: 'Native',
61
+ // immediate: true,
62
+ // onPut: async () => {
63
+ // await this.reinitEventSubscriptions();
64
+ // }
65
+ // },
66
+ ipAddress: {
67
+ title: "IP address",
68
+ type: "string",
69
+ onPut: async () => await this.reinit(),
70
+ },
71
+ username: {
72
+ title: "Username",
73
+ placeholder: "admin",
74
+ defaultValue: "admin",
75
+ type: "string",
76
+ onPut: async () => await this.reinit(),
77
+ },
78
+ password: {
79
+ title: "Password",
80
+ type: "password",
81
+ onPut: async () => await this.reinit(),
82
+ },
83
+ diagnosticsRun: {
84
+ subgroup: "Advanced",
85
+ title: "Run Diagnostics",
86
+ description: "Collect NVR diagnostics and display results in logs.",
87
+ type: "button",
88
+ immediate: true,
89
+ onPut: async () => {
90
+ await this.runNvrDiagnostics();
91
+ },
92
+ },
93
+ socketApiDebugLogs: {
94
+ subgroup: "Advanced",
95
+ title: "Socket API Debug Logs",
96
+ description: "Enable specific debug logs.",
97
+ multiple: true,
98
+ combobox: true,
99
+ immediate: true,
100
+ defaultValue: [],
101
+ choices: getDebugLogChoices(),
102
+ onPut: async (ov, value) => {
103
+ const logger = this.getBaichuanLogger();
104
+ const oldApiOptions = getApiRelevantDebugLogs(ov || []);
105
+ const newApiOptions = getApiRelevantDebugLogs(value || []);
106
+
107
+ const oldSel = new Set(oldApiOptions);
108
+ const newSel = new Set(newApiOptions);
109
+
110
+ const changed =
111
+ oldSel.size !== newSel.size ||
112
+ Array.from(oldSel).some((k) => !newSel.has(k));
113
+ if (changed) {
114
+ // Clear any existing timeout
115
+ if (this.debugLogsResetTimeout) {
116
+ clearTimeout(this.debugLogsResetTimeout);
117
+ this.debugLogsResetTimeout = undefined;
118
+ }
119
+
120
+ this.debugLogsResetTimeout = setTimeout(async () => {
121
+ this.debugLogsResetTimeout = undefined;
122
+ try {
123
+ this.baichuanApi = undefined;
124
+ this.ensureClientPromise = undefined;
125
+ await this.ensureBaichuanClient();
126
+ } catch (e) {
127
+ logger.warn(
128
+ "Failed to reset client after debug logs change",
129
+ e?.message || String(e),
130
+ );
131
+ }
132
+ }, 2000);
144
133
  }
145
-
146
- const debugOptions = this.getBaichuanDebugOptions();
147
-
148
- return {
149
- host: ipAddress,
150
- username,
151
- password,
152
- transport: 'tcp',
153
- debugOptions,
154
- };
155
- }
156
-
157
- getBaichuanDebugOptions(): any | undefined {
158
- const socketDebugLogs = this.storageSettings.values.socketApiDebugLogs || [];
159
- return convertDebugLogsToApiOptions(socketDebugLogs);
160
- }
161
-
162
- protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
163
- return {
164
- onClose: async () => {
165
- await this.reinit();
166
- },
167
- onSimpleEvent: this.onSimpleEventBound,
168
- getEventSubscriptionEnabled: () => true,
169
- };
170
- }
171
-
172
- protected isDebugEnabled(): boolean {
173
- return this.storageSettings.values.debugLogs || false;
134
+ },
135
+ },
136
+ userSessions: {
137
+ title: "Active User Sessions",
138
+ subgroup: "Sessions",
139
+ description:
140
+ "List of currently active user sessions connected to the device via Baichuan socket. Click 'Refresh Sessions' to update.",
141
+ type: "string",
142
+ multiple: true,
143
+ combobox: false,
144
+ readonly: true,
145
+ hide: false,
146
+ defaultValue: [],
147
+ },
148
+ refreshUserSessions: {
149
+ title: "Refresh Sessions",
150
+ subgroup: "Sessions",
151
+ description: "Refresh the list of active user sessions from the device.",
152
+ type: "button",
153
+ immediate: true,
154
+ hide: false,
155
+ onPut: async () => {
156
+ await this.refreshUserSessionsList();
157
+ },
158
+ },
159
+ });
160
+ plugin: ReolinkNativePlugin;
161
+ discoveredDevices = new Map<
162
+ string,
163
+ {
164
+ device: Device;
165
+ description: string;
166
+ rtspChannel: number;
167
+ deviceData: ReolinkBaichuanDeviceSummary;
174
168
  }
175
-
176
- protected getDeviceName(): string {
177
- return this.name || 'NVR';
169
+ >();
170
+ cameraNativeMap = new Map<string, ReolinkCamera>();
171
+ private channelToNativeIdMap = new Map<number, string>();
172
+ private discoverDevicesPromise: Promise<DiscoveredDevice[]> | undefined;
173
+ processing = false;
174
+ private initReinitTimeout: NodeJS.Timeout | undefined;
175
+ private debugLogsResetTimeout: NodeJS.Timeout | undefined;
176
+
177
+ constructor(nativeId: string, plugin: ReolinkNativePlugin) {
178
+ super(nativeId, "tcp");
179
+ this.plugin = plugin;
180
+
181
+ this.scheduleInit();
182
+ }
183
+
184
+ async reboot(): Promise<void> {
185
+ const api = await this.ensureBaichuanClient();
186
+ await api.reboot();
187
+ }
188
+
189
+ protected getConnectionConfig(): BaichuanConnectionConfig {
190
+ const { ipAddress, username, password } = this.storageSettings.values;
191
+ if (!ipAddress || !username || !password) {
192
+ throw new Error("Missing NVR credentials");
178
193
  }
179
194
 
180
- protected async onBeforeCleanup(): Promise<void> {
181
- await this.unsubscribeFromEvents();
195
+ const debugOptions = this.getBaichuanDebugOptions();
196
+
197
+ return {
198
+ host: ipAddress,
199
+ username,
200
+ password,
201
+ transport: "tcp",
202
+ debugOptions,
203
+ };
204
+ }
205
+
206
+ getBaichuanDebugOptions(): any | undefined {
207
+ const socketDebugLogs =
208
+ this.storageSettings.values.socketApiDebugLogs || [];
209
+ return convertDebugLogsToApiOptions(socketDebugLogs);
210
+ }
211
+
212
+ protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
213
+ return {
214
+ onClose: async () => {
215
+ await this.reinit();
216
+ },
217
+ onSimpleEvent: this.onSimpleEventBound,
218
+ getEventSubscriptionEnabled: () => true,
219
+ };
220
+ }
221
+
222
+ protected isDebugEnabled(): boolean {
223
+ return this.storageSettings.values.debugLogs || false;
224
+ }
225
+
226
+ protected getDeviceName(): string {
227
+ return this.name || "NVR";
228
+ }
229
+
230
+ protected async onBeforeCleanup(): Promise<void> {
231
+ await this.unsubscribeFromEvents();
232
+ }
233
+
234
+ async reinit() {
235
+ if (this.initReinitTimeout) {
236
+ clearTimeout(this.initReinitTimeout);
237
+ this.initReinitTimeout = undefined;
182
238
  }
183
239
 
184
- async reinit() {
185
- if (this.initReinitTimeout) {
186
- clearTimeout(this.initReinitTimeout);
187
- this.initReinitTimeout = undefined;
188
- }
240
+ this.scheduleInit(true);
241
+ }
189
242
 
190
- this.scheduleInit(true);
243
+ private scheduleInit(isReinit: boolean = false): void {
244
+ // Cancel any pending init/reinit
245
+ if (this.initReinitTimeout) {
246
+ clearTimeout(this.initReinitTimeout);
191
247
  }
192
248
 
193
- private scheduleInit(isReinit: boolean = false): void {
194
- // Cancel any pending init/reinit
195
- if (this.initReinitTimeout) {
196
- clearTimeout(this.initReinitTimeout);
249
+ this.initReinitTimeout = setTimeout(
250
+ async () => {
251
+ if (isReinit) {
252
+ await super.cleanupBaichuanApi();
197
253
  }
198
-
199
- this.initReinitTimeout = setTimeout(async () => {
200
- if (isReinit) {
201
- await super.cleanupBaichuanApi();
202
- }
203
- await this.init();
204
- this.initReinitTimeout = undefined;
205
- }, isReinit ? 500 : 2000);
254
+ await this.init();
255
+ this.initReinitTimeout = undefined;
256
+ },
257
+ isReinit ? 500 : 2000,
258
+ );
259
+ }
260
+
261
+ onSimpleEvent(ev: ReolinkSimpleEvent) {
262
+ const logger = this.getBaichuanLogger();
263
+
264
+ try {
265
+ logger.debug(`Baichuan event on nvr: ${JSON.stringify(ev)}`);
266
+
267
+ const channel = ev?.channel;
268
+ if (channel === undefined) {
269
+ logger.error("Event has no channel, ignoring");
270
+ return;
271
+ }
272
+
273
+ const nativeId = this.channelToNativeIdMap.get(channel);
274
+ const targetDevice = nativeId
275
+ ? this.cameraNativeMap.get(nativeId)
276
+ : undefined;
277
+
278
+ if (!targetDevice) {
279
+ logger.debug(
280
+ `No device found for channel ${channel} (nativeId: ${nativeId}), ignoring event`,
281
+ );
282
+ return;
283
+ }
284
+
285
+ targetDevice.onSimpleEvent(ev);
286
+ } catch (e) {
287
+ logger.warn(
288
+ "Error in NVR Native event forwarder",
289
+ e?.message || String(e),
290
+ );
206
291
  }
292
+ }
207
293
 
208
- onSimpleEvent(ev: ReolinkSimpleEvent) {
209
- const logger = this.getBaichuanLogger();
210
-
211
- try {
212
- logger.debug(`Baichuan event on nvr: ${JSON.stringify(ev)}`);
294
+ async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
295
+ return await super.ensureBaichuanClient();
296
+ }
213
297
 
214
- const channel = ev?.channel;
215
- if (channel === undefined) {
216
- logger.error('Event has no channel, ignoring');
217
- return;
218
- }
298
+ async ensureClient(): Promise<ReolinkBaichuanApi> {
299
+ return await this.ensureBaichuanClient();
300
+ }
219
301
 
220
- const nativeId = this.channelToNativeIdMap.get(channel);
221
- const targetDevice = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
302
+ private async runNvrDiagnostics(): Promise<void> {
303
+ const logger = this.getBaichuanLogger();
304
+ logger.log(`Starting NVR diagnostics...`);
222
305
 
223
- if (!targetDevice) {
224
- logger.debug(`No device found for channel ${channel} (nativeId: ${nativeId}), ignoring event`);
225
- return;
226
- }
306
+ try {
307
+ const api = await this.ensureBaichuanClient();
227
308
 
228
- targetDevice.onSimpleEvent(ev);
229
- }
230
- catch (e) {
231
- logger.warn('Error in NVR Native event forwarder', e?.message || String(e));
232
- }
309
+ await api.collectNvrDiagnostics({
310
+ logger: this.console,
311
+ });
312
+ } catch (e) {
313
+ logger.error("Failed to run NVR diagnostics", e?.message || String(e));
314
+ throw e;
233
315
  }
234
-
235
- async ensureBaichuanClient(): Promise<ReolinkBaichuanApi> {
236
- return await super.ensureBaichuanClient();
316
+ }
317
+
318
+ async init() {
319
+ await this.ensureBaichuanClient();
320
+ await this.subscribeToEvents();
321
+ await this.discoverDevices(true);
322
+
323
+ await this.updateDeviceInfo();
324
+ }
325
+
326
+ async updateDeviceInfo(): Promise<void> {
327
+ const logger = this.getBaichuanLogger();
328
+
329
+ const { ipAddress } = this.storageSettings.values;
330
+ try {
331
+ const api = await this.ensureBaichuanClient();
332
+ const deviceData = await api.getInfo();
333
+
334
+ await updateDeviceInfo({
335
+ device: this,
336
+ ipAddress,
337
+ deviceData,
338
+ logger,
339
+ });
340
+ } catch (e) {
341
+ logger.warn("Failed to fetch device info", e?.message || String(e));
237
342
  }
343
+ }
238
344
 
239
- async ensureClient(): Promise<ReolinkBaichuanApi> {
240
- return await this.ensureBaichuanClient();
241
- }
345
+ async getSettings(): Promise<Setting[]> {
346
+ const settings = await this.storageSettings.getSettings();
347
+ return settings;
348
+ }
242
349
 
243
- private async runNvrDiagnostics(): Promise<void> {
244
- const logger = this.getBaichuanLogger();
245
- logger.log(`Starting NVR diagnostics...`);
350
+ async putSetting(key: string, value: SettingValue): Promise<void> {
351
+ return this.storageSettings.putSetting(key, value);
352
+ }
246
353
 
247
- try {
248
- const api = await this.ensureBaichuanClient();
249
-
250
- await api.collectNvrDiagnostics({
251
- logger: this.console,
252
- });
253
- } catch (e) {
254
- logger.error('Failed to run NVR diagnostics', e?.message || String(e));
255
- throw e;
256
- }
257
- }
354
+ async releaseDevice(id: string, nativeId: string) {
355
+ this.cameraNativeMap.delete(nativeId);
356
+ }
258
357
 
259
- async init() {
260
- await this.ensureBaichuanClient();
261
- await this.subscribeToEvents();
262
- await this.discoverDevices(true);
358
+ async getDevice(nativeId: string): Promise<ReolinkCamera> {
359
+ let device = this.cameraNativeMap.get(nativeId);
263
360
 
264
- await this.updateDeviceInfo();
265
- }
266
-
267
- async updateDeviceInfo(): Promise<void> {
268
- const logger = this.getBaichuanLogger();
269
-
270
- const { ipAddress } = this.storageSettings.values;
271
- try {
272
- const api = await this.ensureBaichuanClient();
273
- const deviceData = await api.getInfo();
274
-
275
- await updateDeviceInfo({
276
- device: this,
277
- ipAddress,
278
- deviceData,
279
- logger
280
- });
281
- } catch (e) {
282
- logger.warn('Failed to fetch device info', e?.message || String(e));
283
- }
284
- }
361
+ if (!device) {
362
+ if (nativeId.endsWith(batteryCameraSuffix)) {
363
+ device = new ReolinkCamera(nativeId, this.plugin, {
364
+ type: "battery",
365
+ nvrDevice: this,
366
+ });
367
+ } else if (nativeId.endsWith(batteryMultifocalSuffix)) {
368
+ device = new ReolinkNativeMultiFocalDevice(
369
+ nativeId,
370
+ this.plugin,
371
+ "multi-focal-battery",
372
+ this,
373
+ );
374
+ } else if (nativeId.endsWith(multifocalSuffix)) {
375
+ device = new ReolinkNativeMultiFocalDevice(
376
+ nativeId,
377
+ this.plugin,
378
+ "multi-focal",
379
+ this,
380
+ );
381
+ } else {
382
+ device = new ReolinkCamera(nativeId, this.plugin, {
383
+ type: "regular",
384
+ nvrDevice: this,
385
+ });
386
+ }
285
387
 
286
- async getSettings(): Promise<Setting[]> {
287
- const settings = await this.storageSettings.getSettings();
288
- return settings;
388
+ if (device) {
389
+ this.cameraNativeMap.set(nativeId, device);
390
+ }
289
391
  }
290
392
 
291
- async putSetting(key: string, value: SettingValue): Promise<void> {
292
- return this.storageSettings.putSetting(key, value);
293
- }
393
+ return device;
394
+ }
395
+
396
+ buildNativeId(props: {
397
+ identifier?: string;
398
+ isBattery?: boolean;
399
+ isMultifocal?: boolean;
400
+ }): string {
401
+ const { identifier, isBattery, isMultifocal } = props;
402
+
403
+ const suffix = isBattery
404
+ ? isMultifocal
405
+ ? batteryMultifocalSuffix
406
+ : batteryCameraSuffix
407
+ : isMultifocal
408
+ ? multifocalSuffix
409
+ : cameraSuffix;
410
+
411
+ return `${this.nativeId}-${identifier}${suffix}`;
412
+ }
413
+
414
+ getCameraInterfaces() {
415
+ return [
416
+ ScryptedInterface.VideoCameraConfiguration,
417
+ ScryptedInterface.Camera,
418
+ ScryptedInterface.MotionSensor,
419
+ ScryptedInterface.VideoTextOverlays,
420
+ ScryptedInterface.VideoCamera,
421
+ ScryptedInterface.Settings,
422
+ ScryptedInterface.ObjectDetector,
423
+ ];
424
+ }
425
+
426
+ async syncEntitiesFromRemote() {
427
+ const logger = this.getBaichuanLogger();
428
+ // const { ipAddress } = this.storageSettings.values;
429
+
430
+ const api = await this.ensureBaichuanClient();
431
+ const { devices, channels } = await api.getNvrChannelsSummary({
432
+ source: "cgi",
433
+ });
294
434
 
295
- async releaseDevice(id: string, nativeId: string) {
296
- this.cameraNativeMap.delete(nativeId);
435
+ if (!channels.length) {
436
+ logger.debug(
437
+ `No channels found, ${JSON.stringify({ channels, devices })}`,
438
+ );
439
+ await new Promise((resolve) => setTimeout(resolve, 1000));
440
+ await this.syncEntitiesFromRemote();
441
+ return;
297
442
  }
298
443
 
299
- async getDevice(nativeId: string): Promise<ReolinkCamera> {
300
- let device = this.cameraNativeMap.get(nativeId);
301
-
302
- if (!device) {
303
- if (nativeId.endsWith(batteryCameraSuffix)) {
304
- device = new ReolinkCamera(nativeId, this.plugin, { type: 'battery', nvrDevice: this });
305
- } else if (nativeId.endsWith(batteryMultifocalSuffix)) {
306
- device = new ReolinkNativeMultiFocalDevice(nativeId, this.plugin, "multi-focal-battery", this);
307
- } else if (nativeId.endsWith(multifocalSuffix)) {
308
- device = new ReolinkNativeMultiFocalDevice(nativeId, this.plugin, "multi-focal", this);
309
- } else {
310
- device = new ReolinkCamera(nativeId, this.plugin, { type: 'regular', nvrDevice: this });
311
- }
444
+ logger.log(`Sync entities from remote for ${channels.length} channels`);
445
+
446
+ for (const deviceData of devices) {
447
+ const {
448
+ isBattery,
449
+ serialNumber,
450
+ name,
451
+ model,
452
+ isDoorbell,
453
+ uid,
454
+ channel,
455
+ isMultifocal,
456
+ } = deviceData;
457
+ const identifier = uid || name || `channel-${channel}`;
458
+ // const identifier = uid || mac || (ip !== ipAddress ? ip : undefined) || name || randomBytes(4).toString('hex');
459
+
460
+ try {
461
+ const nativeId = this.buildNativeId({
462
+ isBattery,
463
+ isMultifocal,
464
+ identifier,
465
+ });
312
466
 
313
- if (device) {
314
- this.cameraNativeMap.set(nativeId, device);
315
- }
467
+ // Check if device already exists in cameraNativeMap with a different nativeId format
468
+ // (e.g., with chN- prefix). If so, use that nativeId for the mapping.
469
+ let actualNativeId = nativeId;
470
+ const existingDevice = Array.from(this.cameraNativeMap.entries()).find(
471
+ ([id, camera]) => {
472
+ // Check if the camera matches by channel or UID
473
+ const cameraChannel = camera.storageSettings.values.rtspChannel;
474
+ const cameraUid = camera.storageSettings.values.uid;
475
+ return cameraChannel === channel || (uid && cameraUid === uid);
476
+ },
477
+ );
478
+
479
+ if (existingDevice) {
480
+ actualNativeId = existingDevice[0];
481
+ logger.debug(
482
+ `[syncEntities] Using existing nativeId for channel ${channel}: ${actualNativeId} (instead of ${nativeId})`,
483
+ );
316
484
  }
317
485
 
318
- return device;
319
- }
320
-
321
- buildNativeId(props: {
322
- identifier?: string, isBattery?: boolean, isMultifocal?: boolean
323
- }): string {
324
- const { identifier, isBattery, isMultifocal } = props;
325
-
326
- const suffix = isBattery ?
327
- (isMultifocal ? batteryMultifocalSuffix : batteryCameraSuffix) :
328
- (isMultifocal ? multifocalSuffix : cameraSuffix)
329
-
330
- return `${this.nativeId}-${identifier}${suffix}`;
331
- }
332
-
333
- getCameraInterfaces() {
334
- return [
335
- ScryptedInterface.VideoCameraConfiguration,
336
- ScryptedInterface.Camera,
337
- ScryptedInterface.MotionSensor,
338
- ScryptedInterface.VideoTextOverlays,
339
- ScryptedInterface.VideoCamera,
340
- ScryptedInterface.Settings,
341
- ScryptedInterface.ObjectDetector,
342
- ];
343
- }
344
-
345
- async syncEntitiesFromRemote() {
346
- const logger = this.getBaichuanLogger();
347
- // const { ipAddress } = this.storageSettings.values;
348
-
349
- const api = await this.ensureBaichuanClient();
350
- const { devices, channels } = await api.getNvrChannelsSummary({ source: "cgi" });
351
-
352
- if (!channels.length) {
353
- logger.debug(`No channels found, ${JSON.stringify({ channels, devices })}`);
354
- await new Promise(resolve => setTimeout(resolve, 1000));
355
- await this.syncEntitiesFromRemote();
356
- return;
486
+ const interfaces = [ScryptedInterface.VideoCamera];
487
+ if (isBattery) {
488
+ interfaces.push(ScryptedInterface.Battery);
357
489
  }
490
+ const type = isDoorbell
491
+ ? ScryptedDeviceType.Doorbell
492
+ : ScryptedDeviceType.Camera;
493
+
494
+ const device: Device = {
495
+ nativeId,
496
+ name,
497
+ providerNativeId: this.nativeId,
498
+ interfaces,
499
+ type,
500
+ info: {
501
+ manufacturer: "Reolink",
502
+ model,
503
+ serialNumber,
504
+ },
505
+ };
358
506
 
359
- logger.log(`Sync entities from remote for ${channels.length} channels`);
360
-
361
- for (const deviceData of devices) {
362
- const { isBattery, serialNumber, name, model, isDoorbell, uid, channel, isMultifocal } = deviceData;
363
- const identifier = uid || name || `channel-${channel}`;
364
- // const identifier = uid || mac || (ip !== ipAddress ? ip : undefined) || name || randomBytes(4).toString('hex');
507
+ this.channelToNativeIdMap.set(channel, actualNativeId);
508
+
509
+ const allNativeIds = sdk.deviceManager
510
+ .getNativeIds()
511
+ .filter((nid) => !!nid);
512
+
513
+ if (
514
+ allNativeIds.some(
515
+ (nid) =>
516
+ nid.includes(uid) ||
517
+ nid.includes(`channel-${channel}`) ||
518
+ // nid.includes(mac) ||
519
+ // nid.includes(ip) ||
520
+ nid.includes(name) ||
521
+ nid === nativeId,
522
+ )
523
+ ) {
524
+ continue;
525
+ }
365
526
 
366
- try {
367
- const nativeId = this.buildNativeId({
368
- isBattery,
369
- isMultifocal,
370
- identifier,
371
- });
372
-
373
- // Check if device already exists in cameraNativeMap with a different nativeId format
374
- // (e.g., with chN- prefix). If so, use that nativeId for the mapping.
375
- let actualNativeId = nativeId;
376
- const existingDevice = Array.from(this.cameraNativeMap.entries()).find(([id, camera]) => {
377
- // Check if the camera matches by channel or UID
378
- const cameraChannel = camera.storageSettings.values.rtspChannel;
379
- const cameraUid = camera.storageSettings.values.uid;
380
- return cameraChannel === channel || (uid && cameraUid === uid);
381
- });
382
-
383
- if (existingDevice) {
384
- actualNativeId = existingDevice[0];
385
- logger.debug(`[syncEntities] Using existing nativeId for channel ${channel}: ${actualNativeId} (instead of ${nativeId})`);
386
- }
387
-
388
- const interfaces = [ScryptedInterface.VideoCamera];
389
- if (isBattery) {
390
- interfaces.push(ScryptedInterface.Battery);
391
- }
392
- const type = isDoorbell ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera;
393
-
394
- const device: Device = {
395
- nativeId,
396
- name,
397
- providerNativeId: this.nativeId,
398
- interfaces,
399
- type,
400
- info: {
401
- manufacturer: 'Reolink',
402
- model,
403
- serialNumber,
404
- }
405
- };
406
-
407
- this.channelToNativeIdMap.set(channel, actualNativeId);
408
-
409
- const allNativeIds = sdk.deviceManager.getNativeIds().filter(nid => !!nid);
410
-
411
- if (
412
- allNativeIds.some(
413
- nid => nid.includes(uid) ||
414
- nid.includes(`channel-${channel}`) ||
415
- // nid.includes(mac) ||
416
- // nid.includes(ip) ||
417
- nid.includes(name) ||
418
- nid === nativeId)
419
- ) {
420
- continue;
421
- }
422
-
423
- if (this.discoveredDevices.has(nativeId)) {
424
- continue;
425
- }
426
-
427
- this.discoveredDevices.set(nativeId, {
428
- device,
429
- description: `${name} (Channel ${channel})`,
430
- rtspChannel: channel,
431
- deviceData,
432
- });
433
-
434
- logger.debug(`Discovered channel ${channel}: ${name}`);
435
- } catch (e: any) {
436
- logger.debug(`Error processing channel ${channel}: ${e?.message || String(e)}`);
437
- }
527
+ if (this.discoveredDevices.has(nativeId)) {
528
+ continue;
438
529
  }
439
530
 
440
- logger.debug(`Channel discovery completed. ${JSON.stringify({ devices, channels })}`);
531
+ this.discoveredDevices.set(nativeId, {
532
+ device,
533
+ description: `${name} (Channel ${channel})`,
534
+ rtspChannel: channel,
535
+ deviceData,
536
+ });
537
+
538
+ logger.debug(`Discovered channel ${channel}: ${name}`);
539
+ } catch (e: any) {
540
+ logger.debug(
541
+ `Error processing channel ${channel}: ${e?.message || String(e)}`,
542
+ );
543
+ }
441
544
  }
442
545
 
443
- async discoverDevices(scan?: boolean): Promise<DiscoveredDevice[]> {
444
- // If a discovery is already in progress, return that promise
445
- if (this.discoverDevicesPromise) {
446
- return await this.discoverDevicesPromise;
447
- }
546
+ logger.debug(
547
+ `Channel discovery completed. ${JSON.stringify({ devices, channels })}`,
548
+ );
549
+ }
448
550
 
449
- // If scan is requested, start a new discovery
450
- if (scan) {
451
- this.discoverDevicesPromise = (async () => {
452
- try {
453
- await this.syncEntitiesFromRemote();
454
- return [...this.discoveredDevices.values()].map(d => ({
455
- ...d.device,
456
- description: d.description,
457
- }));
458
- } finally {
459
- this.discoverDevicesPromise = undefined;
460
- }
461
- })();
462
- return await this.discoverDevicesPromise;
463
- }
551
+ async discoverDevices(scan?: boolean): Promise<DiscoveredDevice[]> {
552
+ // If a discovery is already in progress, return that promise
553
+ if (this.discoverDevicesPromise) {
554
+ return await this.discoverDevicesPromise;
555
+ }
464
556
 
465
- // If no scan requested, return cached devices immediately
466
- return [...this.discoveredDevices.values()].map(d => ({
557
+ // If scan is requested, start a new discovery
558
+ if (scan) {
559
+ this.discoverDevicesPromise = (async () => {
560
+ try {
561
+ await this.syncEntitiesFromRemote();
562
+ return [...this.discoveredDevices.values()].map((d) => ({
467
563
  ...d.device,
468
564
  description: d.description,
469
- }));
565
+ }));
566
+ } finally {
567
+ this.discoverDevicesPromise = undefined;
568
+ }
569
+ })();
570
+ return await this.discoverDevicesPromise;
470
571
  }
471
572
 
472
- async adoptDevice(adopt: AdoptDevice): Promise<string> {
473
- const entry = this.discoveredDevices.get(adopt.nativeId);
474
-
475
- if (!entry)
476
- throw new Error('device not found');
477
-
478
- await this.onDeviceEvent(ScryptedInterface.DeviceDiscovery, await this.discoverDevices());
479
-
480
- const { uid } = entry.deviceData;
481
-
482
- const { ReolinkBaichuanApi } = await import("@apocaliss92/reolink-baichuan-js");
483
- const transport = 'tcp';
484
- const baichuanApi = new ReolinkBaichuanApi({
485
- host: this.storageSettings.values.ipAddress,
486
- username: this.storageSettings.values.username,
487
- password: this.storageSettings.values.password,
488
- transport,
489
- channel: entry.rtspChannel,
490
- uid,
491
- });
492
- await baichuanApi.login();
493
- const { capabilities, objects, presets } = await baichuanApi.getDeviceCapabilities(entry.rtspChannel);
494
- const { interfaces, type } = getDeviceInterfaces({
495
- capabilities,
496
- logger: this.getBaichuanLogger(),
497
- });
498
-
499
- const actualDevice: Device = {
500
- ...entry.device,
501
- providerNativeId: this.nativeId,
502
- interfaces,
503
- type
504
- };
505
-
506
- await sdk.deviceManager.onDeviceDiscovered(actualDevice);
573
+ // If no scan requested, return cached devices immediately
574
+ return [...this.discoveredDevices.values()].map((d) => ({
575
+ ...d.device,
576
+ description: d.description,
577
+ }));
578
+ }
579
+
580
+ async adoptDevice(adopt: AdoptDevice): Promise<string> {
581
+ const entry = this.discoveredDevices.get(adopt.nativeId);
582
+
583
+ if (!entry) throw new Error("device not found");
584
+
585
+ await this.onDeviceEvent(
586
+ ScryptedInterface.DeviceDiscovery,
587
+ await this.discoverDevices(),
588
+ );
589
+
590
+ const { uid } = entry.deviceData;
591
+
592
+ const { ReolinkBaichuanApi } =
593
+ await import("@apocaliss92/reolink-baichuan-js");
594
+ const transport = "tcp";
595
+ const baichuanApi = new ReolinkBaichuanApi({
596
+ host: this.storageSettings.values.ipAddress,
597
+ username: this.storageSettings.values.username,
598
+ password: this.storageSettings.values.password,
599
+ transport,
600
+ channel: entry.rtspChannel,
601
+ uid,
602
+ });
603
+ await baichuanApi.login();
604
+ const { capabilities, objects, presets } =
605
+ await baichuanApi.getDeviceCapabilities(entry.rtspChannel);
606
+ const { interfaces, type } = getDeviceInterfaces({
607
+ capabilities,
608
+ logger: this.getBaichuanLogger(),
609
+ });
507
610
 
508
- const device = await this.getDevice(adopt.nativeId);
509
- const logger = this.getBaichuanLogger();
510
- logger.log('Adopted device', device?.name, JSON.stringify(actualDevice));
511
- const { username, password, ipAddress } = this.storageSettings.values;
512
-
513
- device.storageSettings.values.rtspChannel = entry.rtspChannel;
514
- device.classes = objects;
515
- device.presets = presets;
516
- device.storageSettings.values.username = username;
517
- device.storageSettings.values.password = password;
518
- device.storageSettings.values.rtspChannel = entry.rtspChannel;
519
- device.storageSettings.values.ipAddress = ipAddress;
520
- device.storageSettings.values.uid = uid;
521
-
522
- device.cachedCapabilities = capabilities;
523
-
524
- this.discoveredDevices.delete(adopt.nativeId);
525
- return device?.id;
526
- }
611
+ const actualDevice: Device = {
612
+ ...entry.device,
613
+ providerNativeId: this.nativeId,
614
+ interfaces,
615
+ type,
616
+ };
617
+
618
+ await sdk.deviceManager.onDeviceDiscovered(actualDevice);
619
+
620
+ const device = await this.getDevice(adopt.nativeId);
621
+ const logger = this.getBaichuanLogger();
622
+ logger.log("Adopted device", device?.name, JSON.stringify(actualDevice));
623
+ const { username, password, ipAddress } = this.storageSettings.values;
624
+
625
+ device.storageSettings.values.rtspChannel = entry.rtspChannel;
626
+ device.classes = objects;
627
+ device.presets = presets;
628
+ device.storageSettings.values.username = username;
629
+ device.storageSettings.values.password = password;
630
+ device.storageSettings.values.rtspChannel = entry.rtspChannel;
631
+ device.storageSettings.values.ipAddress = ipAddress;
632
+ device.storageSettings.values.uid = uid;
633
+
634
+ device.cachedCapabilities = capabilities;
635
+
636
+ this.discoveredDevices.delete(adopt.nativeId);
637
+ return device?.id;
638
+ }
527
639
  }
528
-