@apocaliss92/scrypted-reolink-native 0.1.16 → 0.1.18

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/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apocaliss92/scrypted-reolink-native",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Use any reolink camera with Scrypted, even older/unsupported models without HTTP protocol support",
5
5
  "author": "@apocaliss92",
6
6
  "license": "Apache",
package/src/common.ts CHANGED
@@ -8,6 +8,7 @@ import { convertDebugLogsToApiOptions, DebugLogDisplayNames, DebugLogOption, get
8
8
  import { ReolinkBaichuanIntercom } from "./intercom";
9
9
  import ReolinkNativePlugin from "./main";
10
10
  import { ReolinkNativeNvrDevice } from "./nvr";
11
+ import { ReolinkNativeMultiFocalDevice } from "./multifocal";
11
12
  import { ReolinkPtzPresets } from "./presets";
12
13
  import {
13
14
  buildVideoStreamOptions,
@@ -26,6 +27,7 @@ export type CameraType = 'battery' | 'regular';
26
27
  export interface CommonCameraMixinOptions {
27
28
  type: CameraType;
28
29
  nvrDevice?: ReolinkNativeNvrDevice; // Optional reference to NVR device
30
+ multiFocalDevice?: ReolinkNativeMultiFocalDevice; // Optional reference to multi-focal device
29
31
  }
30
32
 
31
33
  class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
@@ -230,11 +232,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
230
232
  await this.credentialsChanged();
231
233
  }
232
234
  },
233
- isFromNvr: {
234
- type: 'boolean',
235
- hide: true,
236
- defaultValue: false,
237
- },
238
235
  mixinsSetup: {
239
236
  type: 'boolean',
240
237
  hide: true,
@@ -525,14 +522,16 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
525
522
  resetBaichuanClient?(reason?: any): Promise<void>;
526
523
 
527
524
  protected nvrDevice?: ReolinkNativeNvrDevice;
525
+ protected multiFocalDevice?: ReolinkNativeMultiFocalDevice;
528
526
  thisDevice: Settings
529
527
 
530
528
  constructor(nativeId: string, public plugin: ReolinkNativePlugin, public options: CommonCameraMixinOptions) {
531
529
  super(nativeId);
532
- this.protocol = !options.nvrDevice && options.type === 'battery' ? 'udp' : 'tcp';
530
+ this.protocol = !options.nvrDevice && !options.multiFocalDevice && options.type === 'battery' ? 'udp' : 'tcp';
533
531
 
534
532
  // Store NVR device reference if provided
535
533
  this.nvrDevice = options.nvrDevice;
534
+ this.multiFocalDevice = options.multiFocalDevice;
536
535
  this.thisDevice = sdk.systemManager.getDeviceById<Settings>(this.id);
537
536
 
538
537
  setTimeout(async () => {
@@ -745,7 +744,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
745
744
  }
746
745
 
747
746
  async subscribeToEvents(): Promise<void> {
748
- if (this.nvrDevice) {
747
+ if (this.nvrDevice || this.multiFocalDevice) {
749
748
  return;
750
749
  }
751
750
 
@@ -1110,7 +1109,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1110
1109
  const { ipAddress, rtspChannel } = this.storageSettings.values;
1111
1110
  try {
1112
1111
  const api = await this.ensureClient();
1113
- const deviceData = await api.getInfo(this.nvrDevice ? rtspChannel : undefined);
1112
+ const deviceData = await api.getInfo((this.nvrDevice || this.multiFocalDevice) ? rtspChannel : undefined);
1114
1113
 
1115
1114
  await updateDeviceInfo({
1116
1115
  device: this,
@@ -1458,6 +1457,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1458
1457
  if (this.nvrDevice) {
1459
1458
  return await this.nvrDevice.ensureBaichuanClient();
1460
1459
  }
1460
+ if (this.multiFocalDevice) {
1461
+ return await this.multiFocalDevice.ensureBaichuanClient();
1462
+ }
1461
1463
 
1462
1464
  // Use base class implementation
1463
1465
  return await this.ensureBaichuanClient();
@@ -1619,9 +1621,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1619
1621
  logger.warn('Failed to subscribe to Baichuan events', e);
1620
1622
  }
1621
1623
 
1622
- const { isFromNvr } = this.storageSettings.values;
1623
1624
 
1624
- if (isFromNvr && this.nvrDevice) {
1625
+ if (this.nvrDevice) {
1625
1626
  this.storageSettings.settings.username.hide = true;
1626
1627
  this.storageSettings.settings.password.hide = true;
1627
1628
  this.storageSettings.settings.ipAddress.hide = true;
@@ -1632,6 +1633,17 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1632
1633
  this.storageSettings.settings.ipAddress.defaultValue = this.nvrDevice.storageSettings.values.ipAddress;
1633
1634
  }
1634
1635
 
1636
+ if (this.multiFocalDevice) {
1637
+ this.storageSettings.settings.username.hide = true;
1638
+ this.storageSettings.settings.password.hide = true;
1639
+ this.storageSettings.settings.ipAddress.hide = true;
1640
+ this.storageSettings.settings.uid.hide = true;
1641
+
1642
+ this.storageSettings.settings.username.defaultValue = this.multiFocalDevice.storageSettings.values.username;
1643
+ this.storageSettings.settings.password.defaultValue = this.multiFocalDevice.storageSettings.values.password;
1644
+ this.storageSettings.settings.ipAddress.defaultValue = this.multiFocalDevice.storageSettings.values.ipAddress;
1645
+ }
1646
+
1635
1647
  await this.init();
1636
1648
  this.initComplete = true;
1637
1649
  }
package/src/connect.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BaichuanClientOptions, ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
1
+ import type { BaichuanClientOptions, ReolinkBaichuanApi, ReolinkDeviceInfo } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
2
 
3
3
  export type BaichuanTransport = "tcp" | "udp";
4
4
 
@@ -116,13 +116,13 @@ export type UdpFallbackInfo = {
116
116
  tcpError: unknown;
117
117
  };
118
118
 
119
- export type DeviceType = 'camera' | 'battery-cam' | 'nvr';
119
+ export type DeviceType = 'camera' | 'battery-cam' | 'nvr' | 'multifocal';
120
120
 
121
121
  export type AutoDetectResult = {
122
122
  type: DeviceType;
123
123
  transport: BaichuanTransport;
124
- uid?: string;
125
- deviceInfo?: Record<string, string>;
124
+ uid: string;
125
+ deviceInfo?: ReolinkDeviceInfo;
126
126
  channelNum?: number;
127
127
  };
128
128
 
@@ -134,7 +134,7 @@ async function pingHost(host: string, timeoutMs: number = 3000): Promise<boolean
134
134
  const { exec } = require('child_process');
135
135
  const platform = process.platform;
136
136
  const pingCmd = platform === 'win32' ? `ping -n 1 -w ${timeoutMs} ${host}` : `ping -c 1 -W ${Math.floor(timeoutMs / 1000)} ${host}`;
137
-
137
+
138
138
  exec(pingCmd, (error: any) => {
139
139
  resolve(!error);
140
140
  });
@@ -173,13 +173,26 @@ export async function autoDetectDeviceType(
173
173
  });
174
174
  await tcpApi.login();
175
175
 
176
- // Get device info to check if it's an NVR
176
+ // Get device info to check device type
177
177
  const deviceInfo = await tcpApi.getInfo();
178
- const { support } = await tcpApi.getDeviceCapabilities(0);
178
+ const { support } = await tcpApi.getDeviceCapabilities();
179
179
  const channelNum = support?.channelNum ?? 1;
180
180
 
181
181
  logger.log(`[AutoDetect] TCP connection successful. channelNum=${channelNum}`);
182
182
 
183
+ // Multi-focal devices have 2 or 3 channels
184
+ if (channelNum === 2 || channelNum === 3) {
185
+ logger.log(`[AutoDetect] Detected multi-focal device (${channelNum} channels, channelNum=${channelNum})`);
186
+ await tcpApi.close();
187
+ return {
188
+ type: 'multifocal',
189
+ transport: 'tcp',
190
+ uid: uid || '',
191
+ deviceInfo,
192
+ channelNum,
193
+ };
194
+ }
195
+
183
196
  // If channelNum > 1, it's likely an NVR
184
197
  if (channelNum > 1) {
185
198
  logger.log(`[AutoDetect] Detected NVR (${channelNum} channels)`);
@@ -187,6 +200,7 @@ export async function autoDetectDeviceType(
187
200
  return {
188
201
  type: 'nvr',
189
202
  transport: 'tcp',
203
+ uid: uid || '',
190
204
  deviceInfo,
191
205
  channelNum,
192
206
  };
@@ -198,6 +212,7 @@ export async function autoDetectDeviceType(
198
212
  return {
199
213
  type: 'camera',
200
214
  transport: 'tcp',
215
+ uid: uid || '',
201
216
  deviceInfo,
202
217
  channelNum: 1,
203
218
  };
@@ -233,6 +248,23 @@ export async function autoDetectDeviceType(
233
248
  await udpApi.login();
234
249
 
235
250
  const deviceInfo = await udpApi.getInfo();
251
+ const { support } = await udpApi.getDeviceCapabilities();
252
+ const channelNum = support?.channelNum ?? 1;
253
+
254
+ // Multi-focal devices can also be UDP (battery multi-focal cameras)
255
+ if (channelNum === 2 || channelNum === 3) {
256
+ logger.log(`[AutoDetect] UDP connection successful. Detected multi-focal device (${channelNum} channels).`);
257
+ await udpApi.close();
258
+ return {
259
+ type: 'multifocal',
260
+ transport: 'udp',
261
+ uid: normalizedUid,
262
+ deviceInfo,
263
+ channelNum,
264
+ };
265
+ }
266
+
267
+ // Regular battery camera
236
268
  logger.log(`[AutoDetect] UDP connection successful. Detected battery camera.`);
237
269
  await udpApi.close();
238
270
 
@@ -251,47 +283,3 @@ export async function autoDetectDeviceType(
251
283
  }
252
284
  }
253
285
  }
254
-
255
- // export async function connectBaichuanWithTcpUdpFallback(
256
- // inputs: BaichuanConnectInputs,
257
- // onUdpFallback?: (info: UdpFallbackInfo) => void,
258
- // ): Promise<{ api: ReolinkBaichuanApi; transport: BaichuanTransport }> {
259
- // let tcpApi: ReolinkBaichuanApi | undefined;
260
- // try {
261
- // tcpApi = await createBaichuanApi(inputs, "tcp");
262
- // await tcpApi.login();
263
- // return { api: tcpApi, transport: "tcp" };
264
- // }
265
- // catch (e) {
266
- // try {
267
- // await tcpApi?.close();
268
- // }
269
- // catch {
270
- // // ignore
271
- // }
272
-
273
- // if (!isTcpFailureThatShouldFallbackToUdp(e)) {
274
- // throw e;
275
- // }
276
-
277
- // const uid = normalizeUid(inputs.uid);
278
- // const uidMissing = !uid;
279
-
280
- // onUdpFallback?.({
281
- // host: inputs.host,
282
- // uid,
283
- // uidMissing,
284
- // tcpError: e,
285
- // });
286
-
287
- // if (uidMissing) {
288
- // throw new Error(
289
- // `Baichuan TCP failed and this camera likely requires UDP/BCUDP. Set the Reolink UID in settings to continue (ip=${inputs.host}).`,
290
- // );
291
- // }
292
-
293
- // const udpApi = await createBaichuanApi(inputs, "udp");
294
- // await udpApi.login();
295
- // return { api: udpApi, transport: "udp" };
296
- // }
297
- // }
package/src/main.ts CHANGED
@@ -1,12 +1,15 @@
1
- import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting } from "@scrypted/sdk";
1
+ import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings } from "@scrypted/sdk";
2
2
  import { ReolinkNativeCamera } from "./camera";
3
3
  import { ReolinkNativeBatteryCamera } from "./camera-battery";
4
4
  import { ReolinkNativeNvrDevice } from "./nvr";
5
- import { autoDetectDeviceType, createBaichuanApi } from "./connect";
6
- import { getDeviceInterfaces } from "./utils";
5
+ import { ReolinkNativeMultiFocalDevice } from "./multifocal";
6
+ import { autoDetectDeviceType, createBaichuanApi, type BaichuanTransport } from "./connect";
7
+ import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces, multifocalSuffix, nvrSuffix } from "./utils";
8
+ import { BaseBaichuanClass } from "./baichuan-base";
9
+ import { CommonCameraMixin } from "./common";
7
10
 
8
11
  class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
9
- devices = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera | ReolinkNativeNvrDevice>();
12
+ devices = new Map<string, BaseBaichuanClass>();
10
13
  nvrDeviceId: string;
11
14
 
12
15
  constructor(nativeId: string) {
@@ -20,7 +23,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
20
23
  return 'Reolink Native camera';
21
24
  }
22
25
 
23
- async getDevice(nativeId: ScryptedNativeId): Promise<ReolinkNativeCamera | ReolinkNativeBatteryCamera | ReolinkNativeNvrDevice> {
26
+ async getDevice(nativeId: ScryptedNativeId): Promise<BaseBaichuanClass> {
24
27
  if (this.devices.has(nativeId)) {
25
28
  return this.devices.get(nativeId)!;
26
29
  }
@@ -55,12 +58,51 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
55
58
 
56
59
  this.console.log(`[AutoDetect] Detected device type: ${detection.type} (transport: ${detection.transport})`);
57
60
 
61
+ // Handle multi-focal device case
62
+ if (detection.type === 'multifocal') {
63
+ const deviceInfo = detection.deviceInfo || {};
64
+ const name = deviceInfo.name || 'Reolink Multi-Focal';
65
+ const serialNumber = deviceInfo.serialNumber || deviceInfo.itemNo || `multifocal-${Date.now()}`;
66
+ nativeId = `${serialNumber}${multifocalSuffix}`;
67
+
68
+ settings.newCamera ||= name;
69
+
70
+ await sdk.deviceManager.onDeviceDiscovered({
71
+ nativeId,
72
+ name,
73
+ interfaces: [
74
+ ScryptedInterface.Settings,
75
+ ScryptedInterface.DeviceDiscovery,
76
+ ScryptedInterface.DeviceProvider,
77
+ ScryptedInterface.Reboot,
78
+ ],
79
+ type: ScryptedDeviceType.DeviceProvider,
80
+ providerNativeId: this.nativeId,
81
+ });
82
+
83
+ const device = await this.getDevice(nativeId);
84
+ if (!(device instanceof ReolinkNativeMultiFocalDevice)) {
85
+ throw new Error('Expected multi-focal device but got different type');
86
+ }
87
+ device.storageSettings.values.ipAddress = ipAddress;
88
+ device.storageSettings.values.username = username;
89
+ device.storageSettings.values.password = password;
90
+ device.storageSettings.values.uid = detection.uid || '';
91
+
92
+ // Update the protocol based on detection result
93
+ // Note: This requires updating the protocol property, but it's readonly
94
+ // The transport is already set in the constructor during createDevice
95
+ // For now, we'll rely on the constructor parameter
96
+
97
+ return nativeId;
98
+ }
99
+
58
100
  // Handle NVR case
59
101
  if (detection.type === 'nvr') {
60
102
  const deviceInfo = detection.deviceInfo || {};
61
103
  const name = deviceInfo?.name || 'Reolink NVR';
62
104
  const serialNumber = deviceInfo?.serialNumber || deviceInfo?.itemNo || `nvr-${Date.now()}`;
63
- nativeId = `${serialNumber}-nvr`;
105
+ nativeId = `${serialNumber}${nvrSuffix}`;
64
106
 
65
107
  settings.newCamera ||= name;
66
108
 
@@ -95,9 +137,9 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
95
137
 
96
138
  // Create nativeId based on device type
97
139
  if (detection.type === 'battery-cam') {
98
- nativeId = `${serialNumber}-battery-cam`;
140
+ nativeId = `${serialNumber}${batteryCameraSuffix}`;
99
141
  } else {
100
- nativeId = `${serialNumber}-cam`;
142
+ nativeId = `${serialNumber}${cameraSuffix}`;
101
143
  }
102
144
 
103
145
  settings.newCamera ||= name;
@@ -135,13 +177,8 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
135
177
  providerNativeId: this.nativeId,
136
178
  });
137
179
 
138
- const device = await this.getDevice(nativeId);
139
- if (device instanceof ReolinkNativeNvrDevice) {
140
- // NVR devices are handled separately above
141
- throw new Error('NVR device should not reach this code path');
142
- }
180
+ const device = await this.getDevice(nativeId) as CommonCameraMixin;
143
181
 
144
- // Type guard: device is either ReolinkNativeCamera or ReolinkNativeBatteryCamera
145
182
  device.info = deviceInfo;
146
183
  device.classes = objects;
147
184
  device.presets = presets;
@@ -198,10 +235,16 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
198
235
  }
199
236
 
200
237
  createCamera(nativeId: string) {
201
- if (nativeId.endsWith('-battery-cam')) {
238
+ if (nativeId.endsWith(batteryCameraSuffix)) {
202
239
  return new ReolinkNativeBatteryCamera(nativeId, this);
203
- } else if (nativeId.endsWith('-nvr')) {
240
+ } else if (nativeId.endsWith(nvrSuffix)) {
204
241
  return new ReolinkNativeNvrDevice(nativeId, this);
242
+ } else if (nativeId.endsWith(multifocalSuffix)) {
243
+ // Get transport from device settings if available, otherwise default to TCP
244
+ // The transport is determined during autoDetect and should be stored
245
+ // For now, we'll try to infer from UID presence (if UID is set, likely UDP)
246
+ // Default to TCP for now - the transport will be set correctly during createDevice
247
+ return new ReolinkNativeMultiFocalDevice(nativeId, this, 'tcp');
205
248
  } else {
206
249
  return new ReolinkNativeCamera(nativeId, this);
207
250
  }