@apocaliss92/scrypted-reolink-native 0.5.27 → 0.5.28

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.27",
3
+ "version": "0.5.28",
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/camera.ts CHANGED
@@ -807,6 +807,13 @@ export class ReolinkCamera
807
807
  private batteryUpdateTimer: NodeJS.Timeout | undefined;
808
808
  private periodicStarted = false;
809
809
  private statusPollTimer: NodeJS.Timeout | undefined;
810
+ // Off-handle for the global email-push bus subscription. Tied to
811
+ // the camera (Scrypted device) lifecycle, NOT to the Baichuan api
812
+ // — the plugin destroys the api on every idle_disconnect (battery
813
+ // cams every ~30s), and an api-scoped bridge would be dead in
814
+ // that window. The camera-scoped subscription stays alive across
815
+ // every reconnect cycle so SMTP motion always finds a listener.
816
+ private emailPushBusOff?: () => void;
810
817
 
811
818
  // Cooldown timestamps to prevent alignAuxDevicesState from overwriting recently set states
812
819
  // Camera can take 10+ seconds to reflect state changes in its API response
@@ -1557,13 +1564,14 @@ export class ReolinkCamera
1557
1564
  // throw new Error("IP Address is required for TCP devices");
1558
1565
  // }
1559
1566
 
1560
- // Enable the lib-level email-push auto-bridge on the api so SMTP
1561
- // motion lands on `api.onSimpleEvent` together with native push.
1562
- // Skipped for NVR children / multifocal lens children they
1563
- // share the parent's connection and the parent (camera/NVR) owns
1564
- // the mail path; running the bridge twice would dispatch the
1565
- // event to both ends and risk double-counting.
1566
- const eligibleForEmailPush = !this.isOnNvr && !this.multiFocalDevice;
1567
+ // The lib's `emailPushCameraId` auto-bridge would die every time
1568
+ // the plugin calls `cleanupBaichuanApi` (every idle_disconnect on
1569
+ // battery cams every ~30s of idle). In that window SMTP motion
1570
+ // events emitted on the global bus have no listener and are lost.
1571
+ // Instead, the camera subscribes to the bus directly in `init()`
1572
+ // via `subscribeToEmailPushBus()` that subscription is tied to
1573
+ // the camera (Scrypted device) lifecycle, not to the api object,
1574
+ // so it survives every api recreate / idle disconnect / wake.
1567
1575
 
1568
1576
  return {
1569
1577
  host: ipAddress,
@@ -1575,9 +1583,6 @@ export class ReolinkCamera
1575
1583
  udpDiscoveryMethod: discoveryMethod,
1576
1584
  // NOTE: idleDisconnect is NOT set here - the library handles it internally
1577
1585
  // based on the battery status detected during connection
1578
- ...(eligibleForEmailPush && this.nativeId
1579
- ? { emailPushCameraId: this.nativeId }
1580
- : {}),
1581
1586
  };
1582
1587
  }
1583
1588
 
@@ -2756,6 +2761,74 @@ export class ReolinkCamera
2756
2761
  }
2757
2762
  }
2758
2763
 
2764
+ /**
2765
+ * Subscribe to the lib's global email-push bus and translate every
2766
+ * matching SMTP delivery into a synthetic `ReolinkSimpleEvent` fed
2767
+ * to this camera's `onSimpleEvent` — same code path used by native
2768
+ * Baichuan push so `motionDetected` flips uniformly.
2769
+ *
2770
+ * Subscription lifetime is tied to the camera (init → release), NOT
2771
+ * to the Baichuan api: the plugin destroys the api on every
2772
+ * idle_disconnect (battery cams sleep every ~30s of idle), and a
2773
+ * lib-level api auto-bridge would be dead in that window. The
2774
+ * camera-level subscription stays alive across every reconnect so
2775
+ * SMTP motion always finds a listener and lands on `onSimpleEvent`.
2776
+ */
2777
+ private async subscribeToEmailPushBus(): Promise<void> {
2778
+ if (this.emailPushBusOff) return;
2779
+ if (this.isOnNvr || this.multiFocalDevice) return;
2780
+ const ownNativeId = this.nativeId ?? "";
2781
+ if (!ownNativeId) return;
2782
+ const logger = this.getBaichuanLogger();
2783
+ try {
2784
+ const { onEmailPushEvent, mapEmailPushInferredType } = await import(
2785
+ "@apocaliss92/nodelink-js"
2786
+ );
2787
+ this.emailPushBusOff = onEmailPushEvent((event) => {
2788
+ if (event.cameraId !== ownNativeId) return;
2789
+ const channel = this.storageSettings.values.rtspChannel ?? 0;
2790
+ const mapped = mapEmailPushInferredType(event.inferredType);
2791
+ logger.log(
2792
+ `E-mail Push event received (type=${event.inferredType}) → dispatching ${mapped}`,
2793
+ );
2794
+ try {
2795
+ this.onSimpleEvent({
2796
+ type: mapped,
2797
+ channel,
2798
+ timestamp: event.receivedAtMs,
2799
+ });
2800
+ // Fan out a generic motion for AI sub-types so motion-only
2801
+ // consumers still flip — mirrors lib's per-api behaviour.
2802
+ if (mapped !== "motion" && mapped !== "doorbell") {
2803
+ this.onSimpleEvent({
2804
+ type: "motion",
2805
+ channel,
2806
+ timestamp: event.receivedAtMs,
2807
+ });
2808
+ }
2809
+ } catch (e) {
2810
+ logger.warn(
2811
+ `E-mail Push: onSimpleEvent dispatch threw: ${e?.message || String(e)}`,
2812
+ );
2813
+ }
2814
+ });
2815
+ logger.log("E-mail Push: subscribed to global bus");
2816
+ } catch (e) {
2817
+ logger.warn(
2818
+ `E-mail Push: bus subscribe failed: ${e?.message || String(e)}`,
2819
+ );
2820
+ }
2821
+ }
2822
+
2823
+ private unsubscribeFromEmailPushBus(): void {
2824
+ if (this.emailPushBusOff) {
2825
+ try {
2826
+ this.emailPushBusOff();
2827
+ } catch {}
2828
+ this.emailPushBusOff = undefined;
2829
+ }
2830
+ }
2831
+
2759
2832
  /**
2760
2833
  * Refresh the `lastPicture` cache by fetching a live snapshot via
2761
2834
  * the Baichuan API. Used on motion events for battery cameras: the
@@ -2949,8 +3022,7 @@ export class ReolinkCamera
2949
3022
  this.statusPollTimer && clearInterval(this.statusPollTimer);
2950
3023
  this.sleepCheckTimer && clearInterval(this.sleepCheckTimer);
2951
3024
  this.batteryUpdateTimer && clearInterval(this.batteryUpdateTimer);
2952
- // The lib's email-push auto-bridge is released by `api.close()`
2953
- // inside `resetBaichuanClient` — no separate teardown needed.
3025
+ this.unsubscribeFromEmailPushBus();
2954
3026
  this.resetBaichuanClient();
2955
3027
  this.plugin.camerasMap.delete(this.id);
2956
3028
  }
@@ -3620,11 +3692,11 @@ export class ReolinkCamera
3620
3692
 
3621
3693
  this.startPeriodicTasks();
3622
3694
 
3623
- // NOTE: the email-push bus subscription used to live here, but
3624
- // the lib's api factory now auto-bridges SMTP motion into
3625
- // `api.onSimpleEvent` when `emailPushCameraId` is passed (see
3626
- // `getConnectionConfig()`), so there's nothing to wire up at the
3627
- // camera level — the next `ensureClient()` activates the bridge.
3695
+ // Subscribe to the lib's email-push bus BEFORE ensureClient so
3696
+ // SMTP motion is never lost during the api's connection window.
3697
+ // Lifetime is tied to the camera (released in `release()`) so it
3698
+ // survives every `cleanupBaichuanApi` cycle on battery cams.
3699
+ await this.subscribeToEmailPushBus();
3628
3700
 
3629
3701
  await this.ensureClient();
3630
3702