@apocaliss92/scrypted-reolink-native 0.5.19 → 0.5.22

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.
@@ -107,6 +107,8 @@ PERFORMANCE OF THIS SOFTWARE.
107
107
 
108
108
  /*! http://mths.be/fromcodepoint v0.1.0 by @mathias */
109
109
 
110
+ /*! https://mths.be/he v1.2.0 by @mathias | MIT license */
111
+
110
112
  /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
111
113
 
112
114
  /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
@@ -115,6 +117,8 @@ PERFORMANCE OF THIS SOFTWARE.
115
117
 
116
118
  /*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */
117
119
 
120
+ /*!*/
121
+
118
122
  /**
119
123
  * @preserve
120
124
  * Copyright 2015-2018 Igor Bezkrovnyi
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.5.19",
3
+ "version": "0.5.22",
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",
@@ -38,13 +38,13 @@
38
38
  "ScryptedDeviceCreator",
39
39
  "DeviceProvider",
40
40
  "DeviceCreator",
41
- "DeviceDiscovery",
41
+ "DeviceDiscoveryOff",
42
42
  "Settings",
43
43
  "HttpRequestHandler"
44
44
  ]
45
45
  },
46
46
  "dependencies": {
47
- "@apocaliss92/nodelink-js": "^0.4.26",
47
+ "@apocaliss92/nodelink-js": "^0.4.29",
48
48
  "@scrypted/common": "file:../../scrypted/common",
49
49
  "@scrypted/rtsp": "file:../../scrypted/plugins/rtsp",
50
50
  "@scrypted/sdk": "^0.3.118"
@@ -22,6 +22,16 @@ export interface BaichuanConnectionCallbacks {
22
22
  onClose?: () => void | Promise<void>;
23
23
  onSimpleEvent?: (ev: ReolinkSimpleEvent) => void;
24
24
  getEventSubscriptionEnabled?: () => boolean;
25
+ /**
26
+ * When provided, the base class binds the camera's api to the global
27
+ * email-push bus filtered on `cameraId === emailPushCameraId()`. The
28
+ * lib's `subscribeEmailPushEvents` converts each matching event into
29
+ * a `ReolinkSimpleEvent` and dispatches it through `onSimpleEvent`,
30
+ * so the camera's existing motion / AI handler lights up for SMTP-
31
+ * delivered events with no extra wiring. Standalone cameras only —
32
+ * leave undefined on NVR children where email-push isn't meaningful.
33
+ */
34
+ emailPushCameraId?: () => string;
25
35
  }
26
36
 
27
37
  /**
@@ -186,6 +196,7 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
186
196
  private eventSubscriptionActive: boolean = false;
187
197
  private lastEventTime: number = 0;
188
198
  private currentWrappedEventHandler?: (ev: ReolinkSimpleEvent) => void;
199
+ private currentEmailPushOff?: () => void;
189
200
  private subscribeToEventsPromise?: Promise<void>;
190
201
  private pingInterval?: NodeJS.Timeout;
191
202
  private autoRenewInterval?: NodeJS.Timeout;
@@ -866,7 +877,10 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
866
877
 
867
878
  // Subscribe to events with wrapper to track last event time
868
879
  try {
869
- const originalHandler = callbacks.onSimpleEvent;
880
+ // The outer `subscribeToEvents` already guards on `!callbacks.onSimpleEvent`
881
+ // and returns before reaching this point. The narrowed local keeps
882
+ // strict-null TS happy without changing the runtime contract.
883
+ const originalHandler = callbacks.onSimpleEvent!;
870
884
  // Create and store the wrapped handler so it can be properly removed later
871
885
  this.currentWrappedEventHandler = (ev: ReolinkSimpleEvent) => {
872
886
  // Update last event time
@@ -878,6 +892,32 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
878
892
  // onSimpleEvent no longer throws on initial subscribe failure;
879
893
  // the library watchdog handles auto-recovery internally.
880
894
  await api.onSimpleEvent(this.currentWrappedEventHandler);
895
+
896
+ // Bridge the global email-push bus into this api's onSimpleEvent
897
+ // stream so the same wrapped handler above (and any other
898
+ // listener) sees SMTP-delivered motion exactly like a native push.
899
+ // Idempotent: if a previous off-handle is still around, release it.
900
+ if (callbacks.emailPushCameraId) {
901
+ if (this.currentEmailPushOff) {
902
+ try {
903
+ this.currentEmailPushOff();
904
+ } catch {}
905
+ this.currentEmailPushOff = undefined;
906
+ }
907
+ try {
908
+ this.currentEmailPushOff = api.subscribeEmailPushEvents({
909
+ cameraId: callbacks.emailPushCameraId(),
910
+ channel: 0,
911
+ });
912
+ logger.debug("Bridged email-push bus to onSimpleEvent");
913
+ } catch (e) {
914
+ logger.warn(
915
+ "Failed to bridge email-push events",
916
+ e?.message || String(e),
917
+ );
918
+ }
919
+ }
920
+
881
921
  this.eventSubscriptionActive = true;
882
922
  this.lastEventTime = Date.now(); // Initialize on subscription
883
923
  logger.debug("Subscribed to Baichuan events (library watchdog handles auto-recovery)");
@@ -907,6 +947,12 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
907
947
  // api.close() destroys the pool before the promise settles.
908
948
  await this.baichuanApi.offSimpleEvent(this.currentWrappedEventHandler);
909
949
  this.currentWrappedEventHandler = undefined;
950
+ if (this.currentEmailPushOff) {
951
+ try {
952
+ this.currentEmailPushOff();
953
+ } catch {}
954
+ this.currentEmailPushOff = undefined;
955
+ }
910
956
  logger.debug("Unsubscribed from Baichuan events");
911
957
  } catch (e) {
912
958
  logger.warn("Error unsubscribing from events", e?.message || String(e));
package/src/camera.ts CHANGED
@@ -65,6 +65,7 @@ import {
65
65
  getApiRelevantDebugLogs,
66
66
  getDebugLogChoices,
67
67
  } from "./debug-options";
68
+ import { EMAIL_PUSH_SERVER_NATIVE_ID } from "./email-push-server-device";
68
69
  import ReolinkNativePlugin from "./main";
69
70
  import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
70
71
  import { ReolinkNativeNvrDevice } from "./nvr";
@@ -682,6 +683,77 @@ export class ReolinkCamera
682
683
  this.scheduleStreamManagerRestart("compositeDisableTranscode changed");
683
684
  },
684
685
  },
686
+ // ─── E-mail Push ─────────────────────────────────────────────
687
+ // Per-camera knobs that pair with the singleton
688
+ // `Reolink E-mail Push Server` device. Hidden by default and
689
+ // un-hidden in `init()` only for standalone cameras (NVR-attached
690
+ // children share the NVR's mail path and would silently fail).
691
+ emailPushCachedStatus: {
692
+ group: "E-mail Push",
693
+ title: "Camera-side SMTP target",
694
+ description:
695
+ "Last read of the camera's own SMTP config (smtpServer / recipient). Click Refresh to update — reading wakes battery cameras, so we don't auto-refresh.",
696
+ type: "string",
697
+ readonly: true,
698
+ defaultValue: "(not loaded yet — click Refresh status below)",
699
+ hide: true,
700
+ },
701
+ emailPushTriggers: {
702
+ group: "E-mail Push",
703
+ title: "Trigger events",
704
+ description:
705
+ "Event types whose schedule will be flipped to 24/7 ON when Auto-configure is applied. MD = generic motion.",
706
+ type: "string",
707
+ multiple: true,
708
+ combobox: true,
709
+ defaultValue: ["MD", "people", "vehicle"],
710
+ choices: ["MD", "people", "vehicle"],
711
+ hide: true,
712
+ },
713
+ emailPushAttachment: {
714
+ group: "E-mail Push",
715
+ title: "Attachment",
716
+ description:
717
+ "What the camera attaches when it sends an alert. `picture` is recommended — it lets the manager publish the snapshot to MQTT image entities.",
718
+ type: "string",
719
+ defaultValue: "picture",
720
+ choices: ["picture", "video", "none"],
721
+ immediate: true,
722
+ hide: true,
723
+ },
724
+ emailPushAutoConfigure: {
725
+ group: "E-mail Push",
726
+ title: "Auto-configure from Email Push Server",
727
+ description:
728
+ "Reads host/port/auth/domain from the singleton server device and pushes the matching SMTP + schedule to this camera. Open the Reolink E-mail Push Server device once first so it bootstraps.",
729
+ type: "button",
730
+ hide: true,
731
+ onPut: async () => {
732
+ await this.autoConfigureEmailPushFromServer();
733
+ },
734
+ },
735
+ emailPushTest: {
736
+ group: "E-mail Push",
737
+ title: "Send test e-mail",
738
+ description:
739
+ "Asks the camera to perform a live SMTP send against its currently saved target. Can take up to 60s; returns success/failure.",
740
+ type: "button",
741
+ hide: true,
742
+ onPut: async () => {
743
+ await this.runEmailPushTest();
744
+ },
745
+ },
746
+ emailPushRefreshStatus: {
747
+ group: "E-mail Push",
748
+ title: "Refresh status",
749
+ description:
750
+ "Reads the current camera-side SMTP config and updates the status row above. Wakes battery cameras.",
751
+ type: "button",
752
+ hide: true,
753
+ onPut: async () => {
754
+ await this.refreshEmailPushStatus();
755
+ },
756
+ },
685
757
  });
686
758
 
687
759
  ptzPresets = new ReolinkPtzPresets(this);
@@ -1536,6 +1608,12 @@ export class ReolinkCamera
1536
1608
  onSimpleEvent: this.onSimpleEventBound,
1537
1609
  getEventSubscriptionEnabled: () =>
1538
1610
  this.isEventDispatchEnabled?.() ?? false,
1611
+ // Bind this api to the global email-push bus so battery cameras
1612
+ // delivering motion via SMTP land on the same `onSimpleEvent`
1613
+ // stream as the native push. The Email Push Server device
1614
+ // (singleton under the provider) maps `cam-<nativeId>@<domain>`
1615
+ // RCPT addresses back to this `nativeId`.
1616
+ emailPushCameraId: () => this.nativeId ?? "",
1539
1617
  };
1540
1618
  }
1541
1619
 
@@ -1992,7 +2070,10 @@ export class ReolinkCamera
1992
2070
  case "battery":
1993
2071
  if (ev.battery) {
1994
2072
  this.updateBatteryInfo(ev.battery as any).catch((e) => {
1995
- logger.debug("Error updating battery from push", e?.message || String(e));
2073
+ logger.debug(
2074
+ "Error updating battery from push",
2075
+ e?.message || String(e),
2076
+ );
1996
2077
  });
1997
2078
  }
1998
2079
  return;
@@ -2544,6 +2625,115 @@ export class ReolinkCamera
2544
2625
  await this.storageSettings.putSetting(key, value);
2545
2626
  }
2546
2627
 
2628
+ /**
2629
+ * Push the singleton E-mail Push Server's manager-side SMTP config
2630
+ * down to this camera. Mirrors the per-camera select on the server
2631
+ * device but lives next to all other per-camera knobs so users can
2632
+ * stay on the camera page. NVR-attached / multifocal children skip
2633
+ * this — the StorageSettings `hide` flag is set accordingly in init.
2634
+ */
2635
+ async autoConfigureEmailPushFromServer(): Promise<void> {
2636
+ const logger = this.getBaichuanLogger();
2637
+ // Materialise the singleton — Scrypted only constructs it after
2638
+ // the first `getDevice` call, which may not have happened yet.
2639
+ await this.plugin.getDevice(EMAIL_PUSH_SERVER_NATIVE_ID);
2640
+ const server = this.plugin.emailPushServer;
2641
+ if (!server) {
2642
+ throw new Error(
2643
+ "Email Push Server not available. Open the 'Reolink E-mail Push Server' device once to initialise it.",
2644
+ );
2645
+ }
2646
+ const params = server.getManagerSetupParamsForCamera({
2647
+ id: this.id,
2648
+ nativeId: this.nativeId!,
2649
+ });
2650
+ if (!params) {
2651
+ throw new Error(
2652
+ "Email Push Server has no usable LAN address yet, or this camera is NVR-attached.",
2653
+ );
2654
+ }
2655
+ const triggers = this.storageSettings.values.emailPushTriggers ?? [
2656
+ "MD",
2657
+ "people",
2658
+ "vehicle",
2659
+ ];
2660
+ const attachment =
2661
+ (this.storageSettings.values.emailPushAttachment as
2662
+ | "picture"
2663
+ | "video"
2664
+ | "none") ?? "picture";
2665
+
2666
+ logger.log(
2667
+ `E-mail Push: configuring against ${params.managerHost}:${params.managerPort} (recipient=${params.recipientLocalPart}@${params.domain})`,
2668
+ );
2669
+ const api = await this.ensureClient();
2670
+ await api.setupEmailPushToManager(
2671
+ {
2672
+ ...params,
2673
+ attachmentType: attachment,
2674
+ triggerTypes: triggers,
2675
+ runTest: false,
2676
+ },
2677
+ this.storageSettings.values.rtspChannel ?? 0,
2678
+ );
2679
+ logger.log("E-mail Push: setup applied ✓");
2680
+ // Pull the camera's confirmation back into the status row.
2681
+ await this.refreshEmailPushStatus().catch((e) => {
2682
+ logger.warn(
2683
+ `E-mail Push: status refresh after auto-configure failed: ${e?.message || e}`,
2684
+ );
2685
+ });
2686
+ }
2687
+
2688
+ /**
2689
+ * Ask the camera to perform a live SMTP send against its current
2690
+ * (saved) target. Uses the lib's default 60s timeout — Reolink
2691
+ * firmwares can take a while when the manager is slow or DNS resolves
2692
+ * after a delay. Result is logged + reflected in the status row.
2693
+ */
2694
+ async runEmailPushTest(): Promise<void> {
2695
+ const logger = this.getBaichuanLogger();
2696
+ const api = await this.ensureClient();
2697
+ logger.log("E-mail Push: sending test e-mail…");
2698
+ const ok = await api.testEmail();
2699
+ if (ok) {
2700
+ logger.log("E-mail Push: test succeeded ✓");
2701
+ } else {
2702
+ logger.warn(
2703
+ "E-mail Push: test failed (camera reported 482 — check server reachable from camera + credentials).",
2704
+ );
2705
+ }
2706
+ const ts = new Date().toISOString();
2707
+ this.storageSettings.values.emailPushCachedStatus = ok
2708
+ ? `Test OK at ${ts}`
2709
+ : `Test FAILED at ${ts} (482 — server unreachable or auth wrong)`;
2710
+ }
2711
+
2712
+ /**
2713
+ * Read the camera's current SMTP config (cmdId=42) and render a
2714
+ * compact summary into the readonly status row. Wakes battery
2715
+ * cameras, so we never trigger this automatically — the user has to
2716
+ * click the button.
2717
+ */
2718
+ async refreshEmailPushStatus(): Promise<void> {
2719
+ const logger = this.getBaichuanLogger();
2720
+ try {
2721
+ const api = await this.ensureClient();
2722
+ const cfg = await api.getEmail();
2723
+ const ts = new Date().toISOString();
2724
+ const target = `${cfg.smtpServer || "(unset)"}:${cfg.smtpPort ?? "?"}`;
2725
+ const recipient = cfg.address1 || "(no recipient)";
2726
+ this.storageSettings.values.emailPushCachedStatus = `Target: ${target} · Recipient: ${recipient} · refreshed ${ts}`;
2727
+ logger.log(
2728
+ `E-mail Push: status refreshed (target=${target} recipient=${recipient})`,
2729
+ );
2730
+ } catch (e) {
2731
+ const msg = e instanceof Error ? e.message : String(e);
2732
+ this.storageSettings.values.emailPushCachedStatus = `Failed to read camera config: ${msg}`;
2733
+ logger.warn(`E-mail Push: refresh failed: ${msg}`);
2734
+ }
2735
+ }
2736
+
2547
2737
  async takePictureInternal(client: ReolinkBaichuanApi) {
2548
2738
  const { rtspChannel, variantType } = this.storageSettings.values;
2549
2739
  const logger = this.getBaichuanLogger();
@@ -2820,10 +3010,7 @@ export class ReolinkCamera
2820
3010
  this.siren.on = enabled || playing;
2821
3011
  }
2822
3012
  } catch (e) {
2823
- logger.warn(
2824
- "Failed to align siren state",
2825
- e?.message || String(e),
2826
- );
3013
+ logger.warn("Failed to align siren state", e?.message || String(e));
2827
3014
  }
2828
3015
  }
2829
3016
  }
@@ -3444,6 +3631,19 @@ export class ReolinkCamera
3444
3631
  const hideUid = !this.isBattery || this.isOnNvr || !!this.multiFocalDevice;
3445
3632
  this.storageSettings.settings.uid.hide = hideUid;
3446
3633
  this.storageSettings.settings.discoveryMethod.hide = hideUid;
3634
+
3635
+ // E-mail Push group: only standalone cameras get the per-camera
3636
+ // SMTP knobs. NVR children share the recorder's mail path and
3637
+ // multifocal lens-children share the parent's connection — neither
3638
+ // can independently target the singleton Email Push Server, so we
3639
+ // keep the group hidden to avoid misleading the user.
3640
+ const hideEmailPush = this.isOnNvr || !!this.multiFocalDevice;
3641
+ this.storageSettings.settings.emailPushCachedStatus.hide = hideEmailPush;
3642
+ this.storageSettings.settings.emailPushTriggers.hide = hideEmailPush;
3643
+ this.storageSettings.settings.emailPushAttachment.hide = hideEmailPush;
3644
+ this.storageSettings.settings.emailPushAutoConfigure.hide = hideEmailPush;
3645
+ this.storageSettings.settings.emailPushTest.hide = hideEmailPush;
3646
+ this.storageSettings.settings.emailPushRefreshStatus.hide = hideEmailPush;
3447
3647
  // Show UID and discovery method for UDP cameras (battery or UDP-only like Elite Floodlight WiFi)
3448
3648
  // Hide for NVR children or multifocal lenses (they use parent's connection)
3449
3649
  // const requiresUidSettings =