@apocaliss92/scrypted-reolink-native 0.5.24 → 0.5.26

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.5.24",
3
+ "version": "0.5.26",
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",
@@ -1,6 +1,5 @@
1
1
  import type {
2
2
  BaichuanClientOptions,
3
- EmailPushEvent,
4
3
  ReolinkBaichuanApi,
5
4
  ReolinkSimpleEvent,
6
5
  } from "@apocaliss92/nodelink-js" with { "resolution-mode": "import" };
@@ -23,26 +22,6 @@ export interface BaichuanConnectionCallbacks {
23
22
  onClose?: () => void | Promise<void>;
24
23
  onSimpleEvent?: (ev: ReolinkSimpleEvent) => void;
25
24
  getEventSubscriptionEnabled?: () => boolean;
26
- /**
27
- * When provided, the base class binds the camera's api to the global
28
- * email-push bus filtered on `cameraId === emailPushCameraId()`. The
29
- * lib's `subscribeEmailPushEvents` converts each matching event into
30
- * a `ReolinkSimpleEvent` and dispatches it through `onSimpleEvent`,
31
- * so the camera's existing motion / AI handler lights up for SMTP-
32
- * delivered events with no extra wiring. Standalone cameras only —
33
- * leave undefined on NVR children where email-push isn't meaningful.
34
- */
35
- emailPushCameraId?: () => string;
36
- /**
37
- * Optional. When the email-push event carries an image attachment
38
- * (typical for `attachmentType=picture` on motion), the camera
39
- * receives the full event so it can republish the snapshot —
40
- * usually by updating its `lastPicture` cache so subsequent
41
- * `takePicture()` calls return the fresh thumbnail without waking
42
- * the camera. Invoked AFTER the simple-event dispatch so any motion
43
- * listener has already fired.
44
- */
45
- onEmailPushEvent?: (event: EmailPushEvent) => void;
46
25
  }
47
26
 
48
27
  /**
@@ -207,7 +186,6 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
207
186
  private eventSubscriptionActive: boolean = false;
208
187
  private lastEventTime: number = 0;
209
188
  private currentWrappedEventHandler?: (ev: ReolinkSimpleEvent) => void;
210
- private currentEmailPushOff?: () => void;
211
189
  private subscribeToEventsPromise?: Promise<void>;
212
190
  private pingInterval?: NodeJS.Timeout;
213
191
  private autoRenewInterval?: NodeJS.Timeout;
@@ -904,33 +882,11 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
904
882
  // the library watchdog handles auto-recovery internally.
905
883
  await api.onSimpleEvent(this.currentWrappedEventHandler);
906
884
 
907
- // Bridge the global email-push bus into this api's onSimpleEvent
908
- // stream so the same wrapped handler above (and any other
909
- // listener) sees SMTP-delivered motion exactly like a native push.
910
- // Idempotent: if a previous off-handle is still around, release it.
911
- if (callbacks.emailPushCameraId) {
912
- if (this.currentEmailPushOff) {
913
- try {
914
- this.currentEmailPushOff();
915
- } catch {}
916
- this.currentEmailPushOff = undefined;
917
- }
918
- try {
919
- this.currentEmailPushOff = api.subscribeEmailPushEvents({
920
- cameraId: callbacks.emailPushCameraId(),
921
- channel: 0,
922
- ...(callbacks.onEmailPushEvent
923
- ? { onEvent: callbacks.onEmailPushEvent }
924
- : {}),
925
- });
926
- logger.debug("Bridged email-push bus to onSimpleEvent");
927
- } catch (e) {
928
- logger.warn(
929
- "Failed to bridge email-push events",
930
- e?.message || String(e),
931
- );
932
- }
933
- }
885
+ // NOTE: the email-push bus subscription used to live here, but
886
+ // it's now owned by `ReolinkCamera.subscribeToEmailPushBus()`
887
+ // because the bus is global and must outlive the Baichuan api —
888
+ // battery cams routinely drop the api between motions, which
889
+ // would silently drop SMTP events with an api-scoped bridge.
934
890
 
935
891
  this.eventSubscriptionActive = true;
936
892
  this.lastEventTime = Date.now(); // Initialize on subscription
@@ -961,12 +917,6 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
961
917
  // api.close() destroys the pool before the promise settles.
962
918
  await this.baichuanApi.offSimpleEvent(this.currentWrappedEventHandler);
963
919
  this.currentWrappedEventHandler = undefined;
964
- if (this.currentEmailPushOff) {
965
- try {
966
- this.currentEmailPushOff();
967
- } catch {}
968
- this.currentEmailPushOff = undefined;
969
- }
970
920
  logger.debug("Unsubscribed from Baichuan events");
971
921
  } catch (e) {
972
922
  logger.warn("Error unsubscribing from events", e?.message || String(e));
package/src/camera.ts CHANGED
@@ -808,6 +808,11 @@ export class ReolinkCamera
808
808
  private batteryUpdateTimer: NodeJS.Timeout | undefined;
809
809
  private periodicStarted = false;
810
810
  private statusPollTimer: NodeJS.Timeout | undefined;
811
+ // Off-handle for the global email-push bus subscription. Tied to
812
+ // the camera lifecycle (init → release), NOT to the Baichuan api
813
+ // lifetime — battery cams drop the api when they sleep, so an
814
+ // api-scoped subscription would be dead by the next motion email.
815
+ private emailPushBusOff?: () => void;
811
816
 
812
817
  // Cooldown timestamps to prevent alignAuxDevicesState from overwriting recently set states
813
818
  // Camera can take 10+ seconds to reflect state changes in its API response
@@ -1609,20 +1614,12 @@ export class ReolinkCamera
1609
1614
  onSimpleEvent: this.onSimpleEventBound,
1610
1615
  getEventSubscriptionEnabled: () =>
1611
1616
  this.isEventDispatchEnabled?.() ?? false,
1612
- // Bind this api to the global email-push bus so battery cameras
1613
- // delivering motion via SMTP land on the same `onSimpleEvent`
1614
- // stream as the native push. The Email Push Server device
1615
- // (singleton under the provider) maps `cam-<nativeId>@<domain>`
1616
- // RCPT addresses back to this `nativeId`.
1617
- emailPushCameraId: () => this.nativeId ?? "",
1618
- // When the e-mail carries the snapshot Reolink sends with
1619
- // `attachmentType=picture`, hand it to the camera's `lastPicture`
1620
- // cache. Downstream consumers (Snapshot mixin → MQTT image
1621
- // entity, Advanced Notifier, HA discovery) call `takePicture()`
1622
- // on the back of the motion event; for battery cams that returns
1623
- // the cached value, so dropping the fresh snapshot in here makes
1624
- // the trigger frame surface in MQTT/HA without waking the camera.
1625
- onEmailPushEvent: (event) => this.handleEmailPushSnapshot(event),
1617
+ // Email-push events are wired in `init()` directly against the
1618
+ // lib's global bus NOT through the api here, because for
1619
+ // battery cams the Baichuan api lifetime is much shorter than
1620
+ // the camera lifetime (api closes when the cam sleeps), so an
1621
+ // api-level subscription would be dead by the time the next
1622
+ // motion email arrives. See `subscribeToEmailPushBus()`.
1626
1623
  };
1627
1624
  }
1628
1625
 
@@ -2744,17 +2741,92 @@ export class ReolinkCamera
2744
2741
  }
2745
2742
 
2746
2743
  /**
2747
- * Called from `baichuan-base.subscribeToEvents` when a per-camera
2748
- * email-push event lands on the bus. We don't use the e-mail's image
2749
- * attachment Reolink firmwares vary on quality/encoding and we'd
2750
- * be reimplementing the snapshot pipeline. Instead we leverage the
2751
- * fact that the camera is *guaranteed awake* right now (it just
2752
- * pushed an e-mail) and call the live Baichuan snapshot API: it
2753
- * returns a fresh frame in milliseconds without forcing an extra
2754
- * wake-up cycle. The result is stashed in `lastPicture` so the
2755
- * Snapshot mixin → MQTT image entity (and any other consumer that
2756
- * calls `takePicture()` on the back of the motion event) serves the
2757
- * trigger frame instead of the previous cached one.
2744
+ * Subscribe to the lib's global email-push bus and translate each
2745
+ * matching event into a synthetic motion `ReolinkSimpleEvent` fed
2746
+ * to this camera's existing `onSimpleEvent` handler so the same
2747
+ * code path that flips `motionDetected` for native Baichuan push
2748
+ * fires for SMTP-delivered alerts too. Lives on the camera (not on
2749
+ * the api) so the subscription survives api closes for battery
2750
+ * cams. Idempotent: returns early if already subscribed.
2751
+ */
2752
+ private async subscribeToEmailPushBus(): Promise<void> {
2753
+ if (this.emailPushBusOff) return;
2754
+ if (this.isOnNvr || this.multiFocalDevice) return;
2755
+ const ownNativeId = this.nativeId ?? "";
2756
+ if (!ownNativeId) return;
2757
+ const logger = this.getBaichuanLogger();
2758
+ try {
2759
+ const { onEmailPushEvent, mapEmailPushInferredType } = await import(
2760
+ "@apocaliss92/nodelink-js"
2761
+ );
2762
+ this.emailPushBusOff = onEmailPushEvent((event) => {
2763
+ if (event.cameraId !== ownNativeId) return;
2764
+ logger.log(
2765
+ `E-mail Push event received (type=${event.inferredType}) → dispatching motion`,
2766
+ );
2767
+ try {
2768
+ this.onSimpleEvent({
2769
+ type: mapEmailPushInferredType(event.inferredType),
2770
+ channel: this.storageSettings.values.rtspChannel ?? 0,
2771
+ timestamp: event.receivedAtMs,
2772
+ });
2773
+ // Mirror the api-side fan-out: when the camera signalled an
2774
+ // AI sub-type (people / vehicle / …), also raise a generic
2775
+ // motion event so motion-only consumers light up.
2776
+ if (
2777
+ event.inferredType !== "motion" &&
2778
+ event.inferredType !== "doorbell" &&
2779
+ event.inferredType !== "other"
2780
+ ) {
2781
+ this.onSimpleEvent({
2782
+ type: "motion",
2783
+ channel: this.storageSettings.values.rtspChannel ?? 0,
2784
+ timestamp: event.receivedAtMs,
2785
+ });
2786
+ }
2787
+ } catch (e) {
2788
+ logger.warn(
2789
+ `E-mail Push: onSimpleEvent dispatch threw: ${e?.message || String(e)}`,
2790
+ );
2791
+ }
2792
+ // Fire-and-forget snapshot fetch; runs in parallel with the
2793
+ // motion dispatch so consumers reading `takePicture` right
2794
+ // after motion get the fresh frame.
2795
+ void this.handleEmailPushSnapshot(event).catch((e) => {
2796
+ logger.warn(
2797
+ `E-mail Push: snapshot fetch threw: ${e?.message || String(e)}`,
2798
+ );
2799
+ });
2800
+ });
2801
+ logger.log("E-mail Push: subscribed to global bus");
2802
+ } catch (e) {
2803
+ logger.warn(
2804
+ `E-mail Push: bus subscribe failed: ${e?.message || String(e)}`,
2805
+ );
2806
+ }
2807
+ }
2808
+
2809
+ private unsubscribeFromEmailPushBus(): void {
2810
+ if (this.emailPushBusOff) {
2811
+ try {
2812
+ this.emailPushBusOff();
2813
+ } catch {}
2814
+ this.emailPushBusOff = undefined;
2815
+ }
2816
+ }
2817
+
2818
+ /**
2819
+ * Called from `subscribeToEmailPushBus` for every matching event.
2820
+ * We don't use the e-mail's image attachment — Reolink firmwares
2821
+ * vary on quality/encoding and we'd be reimplementing the snapshot
2822
+ * pipeline. Instead we leverage the fact that the camera is
2823
+ * *guaranteed awake* right now (it just pushed an e-mail) and call
2824
+ * the live Baichuan snapshot API: it returns a fresh frame in
2825
+ * milliseconds without forcing an extra wake-up cycle. The result
2826
+ * is stashed in `lastPicture` so the Snapshot mixin → MQTT image
2827
+ * entity (and any other consumer that calls `takePicture()` on the
2828
+ * back of the motion event) serves the trigger frame instead of
2829
+ * the previous cached one.
2758
2830
  */
2759
2831
  private async handleEmailPushSnapshot(
2760
2832
  event: EmailPushEvent,
@@ -2933,6 +3005,7 @@ export class ReolinkCamera
2933
3005
  this.statusPollTimer && clearInterval(this.statusPollTimer);
2934
3006
  this.sleepCheckTimer && clearInterval(this.sleepCheckTimer);
2935
3007
  this.batteryUpdateTimer && clearInterval(this.batteryUpdateTimer);
3008
+ this.unsubscribeFromEmailPushBus();
2936
3009
  this.resetBaichuanClient();
2937
3010
  this.plugin.camerasMap.delete(this.id);
2938
3011
  }
@@ -3601,6 +3674,12 @@ export class ReolinkCamera
3601
3674
  }
3602
3675
 
3603
3676
  this.startPeriodicTasks();
3677
+
3678
+ // Subscribe BEFORE ensureClient so battery cams whose api fails to
3679
+ // connect on first try still pick up incoming SMTP motion events.
3680
+ // The subscription is idempotent — safe across re-inits.
3681
+ await this.subscribeToEmailPushBus();
3682
+
3604
3683
  await this.ensureClient();
3605
3684
 
3606
3685
  try {