@apocaliss92/scrypted-reolink-native 0.5.19 → 0.5.21

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/main.ts CHANGED
@@ -11,16 +11,16 @@ if (typeof globalThis.File === "undefined") {
11
11
  };
12
12
  }
13
13
 
14
- import type { AutoDetectMode, ReolinkBaichuanApi, DiscoveredDevice as LibDiscoveredDevice, AutodiscoveryClient } from "@apocaliss92/nodelink-js" with {
15
- "resolution-mode": "import"
14
+ import type {
15
+ AutoDetectMode,
16
+ ReolinkBaichuanApi,
17
+ } from "@apocaliss92/nodelink-js" with {
18
+ "resolution-mode": "import",
16
19
  };
17
20
  import sdk, {
18
- AdoptDevice,
19
21
  DeviceCreator,
20
22
  DeviceCreatorSettings,
21
- DeviceDiscovery,
22
23
  DeviceProvider,
23
- DiscoveredDevice,
24
24
  HttpRequest,
25
25
  HttpResponse,
26
26
  ScryptedDeviceBase,
@@ -28,15 +28,23 @@ import sdk, {
28
28
  ScryptedInterface,
29
29
  ScryptedNativeId,
30
30
  Setting,
31
- Settings
31
+ Settings,
32
32
  } from "@scrypted/sdk";
33
33
  import { randomBytes } from "crypto";
34
34
  import { BaseBaichuanClass } from "./baichuan-base";
35
35
  import { ReolinkCamera } from "./camera";
36
- import { createBaichuanApi, normalizeUid, type BaichuanTransport } from "./connect";
37
36
  import {
38
- ReolinkNativeIntercom,
37
+ createBaichuanApi,
38
+ normalizeUid,
39
+ type BaichuanTransport,
40
+ } from "./connect";
41
+ import {
42
+ EMAIL_PUSH_SERVER_NATIVE_ID,
43
+ EmailPushServerDevice,
44
+ } from "./email-push-server-device";
45
+ import {
39
46
  INTERCOM_PROVIDER_NATIVE_ID,
47
+ ReolinkNativeIntercom,
40
48
  } from "./intercom-provider";
41
49
  import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
42
50
  import { ReolinkNativeNvrDevice } from "./nvr";
@@ -54,14 +62,22 @@ import {
54
62
 
55
63
  class ReolinkNativePlugin
56
64
  extends ScryptedDeviceBase
57
- implements DeviceProvider, DeviceCreator, DeviceDiscovery, Settings {
65
+ implements DeviceProvider, DeviceCreator, Settings
66
+ {
58
67
  devices = new Map<string, BaseBaichuanClass>();
59
68
  camerasMap = new Map<string, ReolinkCamera>();
60
69
  nvrDeviceId: string;
61
70
  private intercomProvider?: ReolinkNativeIntercom;
62
- private networkDiscoveredDevices = new Map<string, { discovered: DiscoveredDevice; libDevice: LibDiscoveredDevice }>();
63
- private discoverDevicesPromise: Promise<DiscoveredDevice[]> | undefined;
64
- private autodiscoveryClient: AutodiscoveryClient | undefined;
71
+ // Public so `ReolinkCamera` can call the device's
72
+ // `getManagerSetupParamsForCamera()` when the user clicks
73
+ // "Auto-configure from Email Push Server" on the camera page.
74
+ // May be undefined until `getDevice(EMAIL_PUSH_SERVER_NATIVE_ID)`
75
+ // is invoked at least once — callers materialise it via
76
+ // `await plugin.getDevice(EMAIL_PUSH_SERVER_NATIVE_ID)`.
77
+ emailPushServer?: EmailPushServerDevice;
78
+ // private networkDiscoveredDevices = new Map<string, { discovered: DiscoveredDevice; libDevice: LibDiscoveredDevice }>();
79
+ // private discoverDevicesPromise: Promise<DiscoveredDevice[]> | undefined;
80
+ // private autodiscoveryClient: AutodiscoveryClient | undefined;
65
81
 
66
82
  // Shared Baichuan API connections for external devices (non-Reolink Native cameras).
67
83
  // Keyed by Scrypted device ID. Multiple mixins on the same device share one connection.
@@ -85,58 +101,66 @@ class ReolinkNativePlugin
85
101
  providerNativeId: this.nativeId,
86
102
  });
87
103
 
88
- // Start background autodiscovery
89
- this.startBackgroundDiscovery();
90
- }
91
-
92
- private async startBackgroundDiscovery() {
93
- const { AutodiscoveryClient: AutodiscoveryClientClass } = await import("@apocaliss92/nodelink-js");
94
-
95
- // Stop previous client if settings changed
96
- this.autodiscoveryClient?.stop();
97
-
98
- const methods: string[] = JSON.parse(this.storage.getItem("discoveryMethods") || '["arp","onvif"]');
99
- const subnetsRaw = this.storage.getItem("discoverySubnets") || "";
100
- const subnets = subnetsRaw.split(",").map((s: string) => s.trim()).filter(Boolean);
101
-
102
- this.autodiscoveryClient = new AutodiscoveryClientClass({
103
- enableHttpScanning: methods.includes("http"),
104
- enableUdpDiscovery: methods.includes("udp"),
105
- enableTcpPortScan: methods.includes("tcp"),
106
- enableArpLookup: methods.includes("arp"),
107
- enableDhcpListener: methods.includes("dhcp"),
108
- enableOnvifDiscovery: methods.includes("onvif"),
109
- logger: this.console,
110
- httpProbeTimeoutMs: 3000,
111
- udpBroadcastTimeoutMs: 5000,
112
- tcpProbeTimeoutMs: 1500,
113
- dhcpListenerTimeoutMs: 10000,
114
- scanIntervalMs: 120_000, // 2 minutes
115
- autoStart: true,
116
- ...(subnets.length > 0 ? { networkCidr: subnets[0] } : {}),
117
- onDeviceDiscovered: (libDevice) => {
118
- this.handleBackgroundDiscovery(libDevice);
119
- },
104
+ sdk.deviceManager.onDeviceDiscovered({
105
+ nativeId: EMAIL_PUSH_SERVER_NATIVE_ID,
106
+ name: "Reolink E-mail Push Server",
107
+ interfaces: [ScryptedInterface.Settings, ScryptedInterface.Online],
108
+ type: ScryptedDeviceType.API,
109
+ providerNativeId: this.nativeId,
120
110
  });
121
- }
122
-
123
- private handleBackgroundDiscovery(libDevice: LibDiscoveredDevice) {
124
- const existingIps = this.getExistingDeviceIps();
125
- if (existingIps.has(libDevice.host)) return;
126
-
127
- const nativeId = `discovered-${libDevice.host}`;
128
- if (this.networkDiscoveredDevices.has(nativeId)) return;
129
-
130
- this.addToDiscoveryList(libDevice);
131
- this.console.log(`[Discovery] Background: new device ${libDevice.host} (${libDevice.name || libDevice.model || "unknown"})`);
132
111
 
133
- // Notify Scrypted of updated discovery list
134
- this.onDeviceEvent(
135
- ScryptedInterface.DeviceDiscovery,
136
- [...this.networkDiscoveredDevices.values()].map((d) => d.discovered),
137
- );
112
+ // Start background autodiscovery
113
+ // this.startBackgroundDiscovery();
138
114
  }
139
115
 
116
+ // private async startBackgroundDiscovery() {
117
+ // const { AutodiscoveryClient: AutodiscoveryClientClass } = await import("@apocaliss92/nodelink-js");
118
+
119
+ // // Stop previous client if settings changed
120
+ // this.autodiscoveryClient?.stop();
121
+
122
+ // const methods: string[] = JSON.parse(this.storage.getItem("discoveryMethods") || '["arp","onvif"]');
123
+ // const subnetsRaw = this.storage.getItem("discoverySubnets") || "";
124
+ // const subnets = subnetsRaw.split(",").map((s: string) => s.trim()).filter(Boolean);
125
+
126
+ // this.autodiscoveryClient = new AutodiscoveryClientClass({
127
+ // enableHttpScanning: methods.includes("http"),
128
+ // enableUdpDiscovery: methods.includes("udp"),
129
+ // enableTcpPortScan: methods.includes("tcp"),
130
+ // enableArpLookup: methods.includes("arp"),
131
+ // enableDhcpListener: methods.includes("dhcp"),
132
+ // enableOnvifDiscovery: methods.includes("onvif"),
133
+ // logger: this.console,
134
+ // httpProbeTimeoutMs: 3000,
135
+ // udpBroadcastTimeoutMs: 5000,
136
+ // tcpProbeTimeoutMs: 1500,
137
+ // dhcpListenerTimeoutMs: 10000,
138
+ // scanIntervalMs: 120_000, // 2 minutes
139
+ // autoStart: true,
140
+ // ...(subnets.length > 0 ? { networkCidr: subnets[0] } : {}),
141
+ // onDeviceDiscovered: (libDevice) => {
142
+ // this.handleBackgroundDiscovery(libDevice);
143
+ // },
144
+ // });
145
+ // }
146
+
147
+ // private handleBackgroundDiscovery(libDevice: LibDiscoveredDevice) {
148
+ // const existingIps = this.getExistingDeviceIps();
149
+ // if (existingIps.has(libDevice.host)) return;
150
+
151
+ // const nativeId = `discovered-${libDevice.host}`;
152
+ // if (this.networkDiscoveredDevices.has(nativeId)) return;
153
+
154
+ // this.addToDiscoveryList(libDevice);
155
+ // this.console.log(`[Discovery] Background: new device ${libDevice.host} (${libDevice.name || libDevice.model || "unknown"})`);
156
+
157
+ // // Notify Scrypted of updated discovery list
158
+ // this.onDeviceEvent(
159
+ // ScryptedInterface.DeviceDiscovery,
160
+ // [...this.networkDiscoveredDevices.values()].map((d) => d.discovered),
161
+ // );
162
+ // }
163
+
140
164
  async acquireExternalClient(
141
165
  deviceId: string,
142
166
  config: {
@@ -198,6 +222,20 @@ class ReolinkNativePlugin
198
222
  return this.intercomProvider;
199
223
  }
200
224
 
225
+ if (nativeId === EMAIL_PUSH_SERVER_NATIVE_ID) {
226
+ if (!this.emailPushServer) {
227
+ this.emailPushServer = new EmailPushServerDevice(nativeId);
228
+ this.emailPushServer.plugin = this;
229
+ // Fire-and-forget: starts the SMTP server if enabled in storage.
230
+ void this.emailPushServer.start().catch((e: unknown) => {
231
+ this.console.error(
232
+ `Email push server start failed: ${e instanceof Error ? e.message : e}`,
233
+ );
234
+ });
235
+ }
236
+ return this.emailPushServer;
237
+ }
238
+
201
239
  if (this.devices.has(nativeId)) {
202
240
  return this.devices.get(nativeId)!;
203
241
  }
@@ -229,8 +267,7 @@ class ReolinkNativePlugin
229
267
  this.console.log(
230
268
  `[AutoDetect] Starting device type detection for ${ipAddress}...${forceType ? ` (forcing type: ${forceType})` : ""}`,
231
269
  );
232
- const { autoDetectDeviceType } =
233
- await import("@apocaliss92/nodelink-js");
270
+ const { autoDetectDeviceType } = await import("@apocaliss92/nodelink-js");
234
271
  // 'Auto', 'NVR', 'Battery Camera', 'Regular Camera
235
272
  const mode: AutoDetectMode =
236
273
  forceType === "Auto"
@@ -426,8 +463,8 @@ class ReolinkNativePlugin
426
463
  private getExistingDeviceIps(): Set<string> {
427
464
  const ips = new Set<string>();
428
465
  for (const [, device] of this.devices) {
429
- const ip = (device as any).storageSettings?.values?.ipAddress;
430
- if (ip) ips.add(ip);
466
+ const ip = (device as any).storageSettings?.values?.ipAddress;
467
+ if (ip) ips.add(ip);
431
468
  }
432
469
  // Also check storage for devices not yet instantiated
433
470
  const nativeIds = sdk.deviceManager.getNativeIds().filter((nid) => !!nid);
@@ -443,133 +480,136 @@ class ReolinkNativePlugin
443
480
  return ips;
444
481
  }
445
482
 
446
- async discoverDevices(scan?: boolean): Promise<DiscoveredDevice[]> {
447
- if (scan) {
448
- if (this.discoverDevicesPromise) {
449
- return await this.discoverDevicesPromise;
450
- }
451
- this.discoverDevicesPromise = (async () => {
452
- try {
453
- // Trigger immediate scan on the background client
454
- if (this.autodiscoveryClient) {
455
- await this.autodiscoveryClient.scanNow();
456
- }
457
-
458
- // Rebuild the discovery list from the client's cumulative results
459
- this.rebuildDiscoveryList();
460
-
461
- return [...this.networkDiscoveredDevices.values()].map((d) => d.discovered);
462
- } catch (e: any) {
463
- this.console.error(`[Discovery] Scan failed: ${e?.message || String(e)}`);
464
- return [];
465
- } finally {
466
- this.discoverDevicesPromise = undefined;
467
- }
468
- })();
469
- return await this.discoverDevicesPromise;
470
- }
471
-
472
- // No scan: return cached results
473
- return [...this.networkDiscoveredDevices.values()].map((d) => d.discovered);
474
- }
475
-
476
- private rebuildDiscoveryList() {
477
- const existingIps = this.getExistingDeviceIps();
478
- const allDevices = this.autodiscoveryClient?.getDiscoveredDevices() ?? [];
479
- this.networkDiscoveredDevices.clear();
480
-
481
- for (const libDevice of allDevices) {
482
- const alreadyAdded = existingIps.has(libDevice.host);
483
- this.console.log(`[Discovery] Found ${libDevice.host} (${libDevice.name || libDevice.model || "unknown"})${alreadyAdded ? " — already added, skipping" : ""}`);
484
- if (alreadyAdded) continue;
485
- this.addToDiscoveryList(libDevice);
486
- }
487
-
488
- this.console.log(`[Discovery] ${this.networkDiscoveredDevices.size} new device(s), ${existingIps.size} already added`);
489
- }
490
-
491
- private addToDiscoveryList(libDevice: LibDiscoveredDevice) {
492
- const nativeId = `discovered-${libDevice.host}`;
493
- const name = libDevice.name || libDevice.model || `Reolink ${libDevice.host}`;
494
- const descParts: string[] = [libDevice.host];
495
- if (libDevice.model) descParts.push(libDevice.model);
496
- if (libDevice.uid) descParts.push(`UID: ${libDevice.uid}`);
497
-
498
- const discovered: DiscoveredDevice = {
499
- nativeId,
500
- name,
501
- description: descParts.join(" — "),
502
- type: ScryptedDeviceType.Camera,
503
- settings: [
504
- { key: "username", title: "Username", value: "admin" },
505
- { key: "password", title: "Password", type: "password" },
506
- ],
507
- };
508
-
509
- if (libDevice.firmwareVersion || libDevice.model) {
510
- discovered.info = {
511
- manufacturer: "Reolink",
512
- ...(libDevice.model ? { model: libDevice.model } : {}),
513
- ...(libDevice.firmwareVersion ? { firmware: libDevice.firmwareVersion } : {}),
514
- };
515
- }
516
-
517
- this.networkDiscoveredDevices.set(nativeId, { discovered, libDevice });
518
- }
519
-
520
- async adoptDevice(adopt: AdoptDevice): Promise<string> {
521
- const entry = this.networkDiscoveredDevices.get(adopt.nativeId);
522
- if (!entry) throw new Error("Device not found in discovery cache");
523
-
524
- const { libDevice } = entry;
525
- const username = adopt.settings?.username?.toString() || "admin";
526
- const password = adopt.settings?.password?.toString();
527
- if (!password) throw new Error("Password is required");
528
-
529
- // Delegate to createDevice which handles auto-detection, capabilities, etc.
530
- const nativeId = await this.createDevice({
531
- ip: libDevice.host,
532
- username,
533
- password,
534
- uid: libDevice.uid || "",
535
- deviceType: "Auto",
536
- });
537
-
538
- // Remove from discovered cache
539
- this.networkDiscoveredDevices.delete(adopt.nativeId);
540
-
541
- // Notify Scrypted of updated discovery list
542
- await this.onDeviceEvent(
543
- ScryptedInterface.DeviceDiscovery,
544
- [...this.networkDiscoveredDevices.values()].map((d) => d.discovered),
545
- );
546
-
547
- return nativeId;
548
- }
483
+ // async discoverDevices(scan?: boolean): Promise<DiscoveredDevice[]> {
484
+ // if (scan) {
485
+ // if (this.discoverDevicesPromise) {
486
+ // return await this.discoverDevicesPromise;
487
+ // }
488
+ // this.discoverDevicesPromise = (async () => {
489
+ // try {
490
+ // // Trigger immediate scan on the background client
491
+ // if (this.autodiscoveryClient) {
492
+ // await this.autodiscoveryClient.scanNow();
493
+ // }
494
+
495
+ // // Rebuild the discovery list from the client's cumulative results
496
+ // this.rebuildDiscoveryList();
497
+
498
+ // return [...this.networkDiscoveredDevices.values()].map((d) => d.discovered);
499
+ // } catch (e: any) {
500
+ // this.console.error(`[Discovery] Scan failed: ${e?.message || String(e)}`);
501
+ // return [];
502
+ // } finally {
503
+ // this.discoverDevicesPromise = undefined;
504
+ // }
505
+ // })();
506
+ // return await this.discoverDevicesPromise;
507
+ // }
508
+
509
+ // // No scan: return cached results
510
+ // return [...this.networkDiscoveredDevices.values()].map((d) => d.discovered);
511
+ // }
512
+
513
+ // private rebuildDiscoveryList() {
514
+ // const existingIps = this.getExistingDeviceIps();
515
+ // const allDevices = this.autodiscoveryClient?.getDiscoveredDevices() ?? [];
516
+ // this.networkDiscoveredDevices.clear();
517
+
518
+ // for (const libDevice of allDevices) {
519
+ // const alreadyAdded = existingIps.has(libDevice.host);
520
+ // this.console.log(`[Discovery] Found ${libDevice.host} (${libDevice.name || libDevice.model || "unknown"})${alreadyAdded ? " — already added, skipping" : ""}`);
521
+ // if (alreadyAdded) continue;
522
+ // this.addToDiscoveryList(libDevice);
523
+ // }
524
+
525
+ // this.console.log(`[Discovery] ${this.networkDiscoveredDevices.size} new device(s), ${existingIps.size} already added`);
526
+ // }
527
+
528
+ // private addToDiscoveryList(libDevice: LibDiscoveredDevice) {
529
+ // const nativeId = `discovered-${libDevice.host}`;
530
+ // const name = libDevice.name || libDevice.model || `Reolink ${libDevice.host}`;
531
+ // const descParts: string[] = [libDevice.host];
532
+ // if (libDevice.model) descParts.push(libDevice.model);
533
+ // if (libDevice.uid) descParts.push(`UID: ${libDevice.uid}`);
534
+
535
+ // const discovered: DiscoveredDevice = {
536
+ // nativeId,
537
+ // name,
538
+ // description: descParts.join(" — "),
539
+ // type: ScryptedDeviceType.Camera,
540
+ // settings: [
541
+ // { key: "username", title: "Username", value: "admin" },
542
+ // { key: "password", title: "Password", type: "password" },
543
+ // ],
544
+ // };
545
+
546
+ // if (libDevice.firmwareVersion || libDevice.model) {
547
+ // discovered.info = {
548
+ // manufacturer: "Reolink",
549
+ // ...(libDevice.model ? { model: libDevice.model } : {}),
550
+ // ...(libDevice.firmwareVersion ? { firmware: libDevice.firmwareVersion } : {}),
551
+ // };
552
+ // }
553
+
554
+ // this.networkDiscoveredDevices.set(nativeId, { discovered, libDevice });
555
+ // }
556
+
557
+ // async adoptDevice(adopt: AdoptDevice): Promise<string> {
558
+ // const entry = this.networkDiscoveredDevices.get(adopt.nativeId);
559
+ // if (!entry) throw new Error("Device not found in discovery cache");
560
+
561
+ // const { libDevice } = entry;
562
+ // const username = adopt.settings?.username?.toString() || "admin";
563
+ // const password = adopt.settings?.password?.toString();
564
+ // if (!password) throw new Error("Password is required");
565
+
566
+ // // Delegate to createDevice which handles auto-detection, capabilities, etc.
567
+ // const nativeId = await this.createDevice({
568
+ // ip: libDevice.host,
569
+ // username,
570
+ // password,
571
+ // uid: libDevice.uid || "",
572
+ // deviceType: "Auto",
573
+ // });
574
+
575
+ // // Remove from discovered cache
576
+ // this.networkDiscoveredDevices.delete(adopt.nativeId);
577
+
578
+ // // Notify Scrypted of updated discovery list
579
+ // await this.onDeviceEvent(
580
+ // ScryptedInterface.DeviceDiscovery,
581
+ // [...this.networkDiscoveredDevices.values()].map((d) => d.discovered),
582
+ // );
583
+
584
+ // return nativeId;
585
+ // }
549
586
 
550
587
  async getSettings(): Promise<Setting[]> {
551
588
  return [
552
- {
553
- key: "discoveryMethods",
554
- title: "Discovery Methods",
555
- description: "ARP: reads ARP table for Reolink MAC prefixes (fast, like Home Assistant). ONVIF: WS-Discovery multicast (most Reolink cameras). DHCP: passive listener on port 67 (requires root).",
556
- type: "string",
557
- choices: ["arp", "onvif", "dhcp"],
558
- multiple: true,
559
- value: JSON.parse(this.storage.getItem("discoveryMethods") || '["arp","onvif"]'),
560
- },
561
- {
562
- key: "discoverySubnets",
563
- title: "Discovery Subnets",
564
- description: "Comma-separated list of subnets to scan in CIDR notation (e.g. 192.168.1.0/24, 10.0.0.0/24). Leave empty to auto-detect the local network. Used for HTTP and TCP scanning; ARP/UDP/DHCP methods don't need this.",
565
- type: "string",
566
- placeholder: "192.168.1.0/24, 10.0.0.0/24",
567
- value: this.storage.getItem("discoverySubnets") || "",
568
- },
589
+ // {
590
+ // key: "discoveryMethods",
591
+ // title: "Discovery Methods",
592
+ // description: "ARP: reads ARP table for Reolink MAC prefixes (fast, like Home Assistant). ONVIF: WS-Discovery multicast (most Reolink cameras). DHCP: passive listener on port 67 (requires root).",
593
+ // type: "string",
594
+ // choices: ["arp", "onvif", "dhcp"],
595
+ // multiple: true,
596
+ // value: JSON.parse(this.storage.getItem("discoveryMethods") || '["arp","onvif"]'),
597
+ // },
598
+ // {
599
+ // key: "discoverySubnets",
600
+ // title: "Discovery Subnets",
601
+ // description: "Comma-separated list of subnets to scan in CIDR notation (e.g. 192.168.1.0/24, 10.0.0.0/24). Leave empty to auto-detect the local network. Used for HTTP and TCP scanning; ARP/UDP/DHCP methods don't need this.",
602
+ // type: "string",
603
+ // placeholder: "192.168.1.0/24, 10.0.0.0/24",
604
+ // value: this.storage.getItem("discoverySubnets") || "",
605
+ // },
569
606
  ];
570
607
  }
571
608
 
572
- async putSetting(key: string, value: string | number | boolean | string[]): Promise<void> {
609
+ async putSetting(
610
+ key: string,
611
+ value: string | number | boolean | string[],
612
+ ): Promise<void> {
573
613
  if (Array.isArray(value)) {
574
614
  this.storage.setItem(key, JSON.stringify(value));
575
615
  } else {
@@ -577,9 +617,9 @@ class ReolinkNativePlugin
577
617
  }
578
618
 
579
619
  // Restart background discovery when discovery settings change
580
- if (key === "discoveryMethods" || key === "discoverySubnets") {
581
- this.startBackgroundDiscovery();
582
- }
620
+ // if (key === "discoveryMethods" || key === "discoverySubnets") {
621
+ // this.startBackgroundDiscovery();
622
+ // }
583
623
  }
584
624
 
585
625
  async getCreateDeviceSettings(): Promise<Setting[]> {