@apocaliss92/nodelink-js 0.4.31 → 0.4.33

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/README.md CHANGED
@@ -49,14 +49,46 @@ Battery cameras (Argus, Go, …) can't reliably keep a TCP/ONVIF push subscripti
49
49
  - **Auto** — manager: **Email Push** tab in the camera modal → *Auto-configure*. Scrypted: open the camera's Settings → **E-mail Push** group → *Auto-configure from Email Push Server*. Both call `setupEmailPushToManager` under the hood.
50
50
  - **API** — `await api.setupEmailPushToManager({ managerHost, managerPort, recipientLocalPart, domain, authUsername, authPassword, triggerTypes, attachmentType }, channel)`. The lib auto-wraps a bare username as `<user@domain>` so MAIL FROM stays RFC 5321 compliant.
51
51
  - **Manual** — fill the Reolink app form: server = manager LAN IP, port = `2525`, sender = `authUsername`, password = `authPassword`, TLS off, receiver = `cam-<id>@<domain>`.
52
- 4. On motion, the camera sends an e-mail. The intake parses it, classifies the trigger (`MD` / `people` / `vehicle`), and emits an `EmailPushEvent` on the shared bus. Snapshots are kept in memory only (no disk persistence) they're forwarded to whatever per-event handler the consumer wires (MQTT image entities in the manager, `motionDetected` flip in the Scrypted plugin).
52
+ 4. On motion, the camera sends an e-mail. The intake parses it, classifies the trigger (`MD` / `people` / `vehicle`), and emits an `EmailPushEvent` on the shared bus. From there it lands on `api.onSimpleEvent` automaticallysee "Unified event stream" below.
53
53
 
54
54
  See [documentation/baichuan-api/email.md](./documentation/baichuan-api/email.md) for the full Baichuan API surface and [documentation/baichuan-api/time.md](./documentation/baichuan-api/time.md) for the related NTP / DST / system clock setters.
55
55
 
56
+ ### Unified event stream (since 0.4.32)
57
+
58
+ Construct the api with `emailPushCameraId` (and optionally `emailPushChannel`) and the library wires the SMTP bus into the api's internal `simpleEventListeners` for you. Every consumer registered via `api.onSimpleEvent(...)` then receives native Baichuan push **and** SMTP-delivered motion through the same stream — no separate `onEmailPushEvent` subscription needed. The bridge survives TCP transient disconnects (it's a pure JS fan-out, not a network operation) and is released automatically by `close()`.
59
+
60
+ ```ts
61
+ import { ReolinkBaichuanApi } from "@apocaliss92/nodelink-js";
62
+
63
+ const api = new ReolinkBaichuanApi({
64
+ host: "192.168.1.100",
65
+ username: "admin",
66
+ password: "secret",
67
+ transport: "udp",
68
+ uid: "REOLINK-UID-HERE",
69
+
70
+ // Auto-bridge SMTP motion into api.onSimpleEvent. Match the same
71
+ // cameraId your `createEmailPushServer({ cameraResolver })` returns
72
+ // (typically the camera's nativeId / stable identifier).
73
+ emailPushCameraId: "my-battery-cam",
74
+ emailPushChannel: 0, // optional, default 0
75
+ });
76
+
77
+ await api.login();
78
+ await api.onSimpleEvent((event) => {
79
+ // Fires for both native Baichuan push AND SMTP motion.
80
+ console.log(event.type, "on ch", event.channel, "@", event.timestamp);
81
+ });
82
+ ```
83
+
84
+ For single-owner consumers that already manage their own bridge (e.g. a custom resolver scheme), the lower-level `api.subscribeEmailPushEvents({ cameraId | match, channel })` is still exposed.
85
+
56
86
  **Library entry points**:
57
87
 
58
88
  - `createEmailPushServer({ config, cameraResolver, logger, loadTls? })` — factory returning `{ start, stop, restart, updateConfig, getStatus }`
59
- - `subscribeEmailPushEvents({ cameraId? | match?, channel? })` on a `ReolinkBaichuanApi` instance bridges per-camera SMTP events into the same `onSimpleEvent` stream native Baichuan push uses
89
+ - `new ReolinkBaichuanApi({ ..., emailPushCameraId, emailPushChannel? })` — auto-bridge into `onSimpleEvent` (recommended)
90
+ - `api.subscribeEmailPushEvents({ cameraId | match, channel? })` — manual per-api bridge with custom matcher
91
+ - `onEmailPushEvent(handler)` — raw global bus subscription (use when you need the full `EmailPushEvent` payload, not just the synthesised `ReolinkSimpleEvent`)
60
92
  - `getRecentEmailPushEvents(limit)` — bounded in-memory ring buffer of accepted deliveries
61
93
  - `setupEmailPushToManager(params, channel)` — orchestrator: `setEmail` + `setEmailTask` + optional `testEmail`
62
94
  - `getEmail`, `setEmail`, `testEmail`, `getEmailTask`, `setEmailTask` — low-level Baichuan accessors
@@ -11439,6 +11439,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11439
11439
  * Once closed, the API instance should not be reused.
11440
11440
  */
11441
11441
  _closed = false;
11442
+ /**
11443
+ * Off-handle for the auto-bridge between the global email-push bus
11444
+ * and this api's `simpleEventListeners`. Set in the constructor
11445
+ * when `emailPushCameraId` is provided; released in `close()`.
11446
+ * `undefined` means no bridge was requested for this api.
11447
+ */
11448
+ emailPushAutoBridgeOff;
11442
11449
  // ─────────────────────────────────────────────────────────────────────────────
11443
11450
  // SOCKET POOL - Tag-based socket management
11444
11451
  // ─────────────────────────────────────────────────────────────────────────────
@@ -11811,6 +11818,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11811
11818
  // check every 10s
11812
11819
  simpleEventWatchdogSilenceThresholdMs = 5 * 6e4;
11813
11820
  // 5 min without events
11821
+ /**
11822
+ * Whether the silence-based resubscribe path of the watchdog is
11823
+ * enabled. On UDP (battery cameras) silence is the *normal* state
11824
+ * while the device sleeps — firing `ensureSimpleEventSubscribed`
11825
+ * every 5 minutes wakes the camera on every tick, drains the
11826
+ * battery, and is observably wrong because the cam emits a
11827
+ * sleep/awake push when it actually wakes for motion.
11828
+ *
11829
+ * Defaults: `false` on UDP, `true` on TCP / `auto`. The subscription-
11830
+ * failed recovery path (Case 2) stays active regardless — it only
11831
+ * runs when the connection is alive, doesn't wake anyone, and is
11832
+ * useful on every transport when the initial subscribe call lost
11833
+ * the response packet.
11834
+ */
11835
+ eventWatchdogSilenceResubscribeEnabled = true;
11814
11836
  statePollingInterval;
11815
11837
  udpSleepInferenceInterval;
11816
11838
  udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
@@ -12768,6 +12790,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12768
12790
  } else {
12769
12791
  this.eventResubscribeEnabled = opts.transport !== "udp";
12770
12792
  }
12793
+ const explicitWatchdogResubscribe = opts.enableEventWatchdogSilenceResubscribe;
12794
+ if (typeof explicitWatchdogResubscribe === "boolean") {
12795
+ this.eventWatchdogSilenceResubscribeEnabled = explicitWatchdogResubscribe;
12796
+ } else {
12797
+ this.eventWatchdogSilenceResubscribeEnabled = opts.transport !== "udp";
12798
+ }
12771
12799
  const maxSessions = opts.maxDedicatedSessionsBeforeReboot;
12772
12800
  if (typeof maxSessions === "number" && Number.isFinite(maxSessions) && maxSessions > 0) {
12773
12801
  this.maxDedicatedSessionsBeforeReboot = Math.floor(maxSessions);
@@ -12781,6 +12809,12 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12781
12809
  this.rebootAfterConsecutiveEconnreset = Math.floor(econnresetThreshold);
12782
12810
  }
12783
12811
  this.setupGeneralClientListeners();
12812
+ if (opts.emailPushCameraId) {
12813
+ this.emailPushAutoBridgeOff = this.subscribeEmailPushEvents({
12814
+ cameraId: opts.emailPushCameraId,
12815
+ channel: opts.emailPushChannel ?? 0
12816
+ });
12817
+ }
12784
12818
  }
12785
12819
  /**
12786
12820
  * CGI forward: fetch RTSP URL for a channel via `GetRtspUrl`.
@@ -13584,6 +13618,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
13584
13618
  if (this.simpleEventSubscribed && this.simpleEventLastReceivedAt > 0) {
13585
13619
  const silence = now - this.simpleEventLastReceivedAt;
13586
13620
  if (silence < this.simpleEventWatchdogSilenceThresholdMs) return;
13621
+ if (!this.eventWatchdogSilenceResubscribeEnabled) {
13622
+ this.logger.debug?.(
13623
+ `[ReolinkBaichuanApi] event watchdog: silence-based resubscribe disabled (UDP / battery), skipping`,
13624
+ { host: this.host, silenceMs: silence }
13625
+ );
13626
+ this.simpleEventLastReceivedAt = now;
13627
+ return;
13628
+ }
13587
13629
  (this.logger.warn ?? this.logger.log).call(
13588
13630
  this.logger,
13589
13631
  `[ReolinkBaichuanApi] event watchdog: no events for ${Math.round(silence / 6e4)} min, forcing resubscribe`,
@@ -13904,6 +13946,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
13904
13946
  async close(options) {
13905
13947
  if (this._closed) return;
13906
13948
  this._closed = true;
13949
+ if (this.emailPushAutoBridgeOff) {
13950
+ try {
13951
+ this.emailPushAutoBridgeOff();
13952
+ } catch {
13953
+ }
13954
+ this.emailPushAutoBridgeOff = void 0;
13955
+ }
13907
13956
  if (this.sessionGuardIntervalTimer) {
13908
13957
  clearInterval(this.sessionGuardIntervalTimer);
13909
13958
  this.sessionGuardIntervalTimer = void 0;
@@ -24733,4 +24782,4 @@ export {
24733
24782
  isTcpFailureThatShouldFallbackToUdp,
24734
24783
  autoDetectDeviceType
24735
24784
  };
24736
- //# sourceMappingURL=chunk-XVFCEFM6.js.map
24785
+ //# sourceMappingURL=chunk-OZL6C2YJ.js.map