@apocaliss92/scrypted-reolink-native 0.5.27 → 0.5.29
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/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/baichuan-base.ts +22 -11
- package/src/camera.ts +89 -17
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/baichuan-base.ts
CHANGED
|
@@ -486,19 +486,30 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
486
486
|
this.lastDisconnectTime = now;
|
|
487
487
|
|
|
488
488
|
logger.log(
|
|
489
|
-
`Socket closed
|
|
489
|
+
`Socket closed${timeSinceLastDisconnect != null ? ` (last disconnect ${timeSinceLastDisconnect}ms ago)` : " (first disconnect)"} — api object kept alive, will reconnect on next use`,
|
|
490
490
|
);
|
|
491
491
|
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
492
|
+
// The api object itself stays alive across socket disconnects:
|
|
493
|
+
// - It's NOT explicitly closed (no `_closed = true`), so simple
|
|
494
|
+
// event listeners, the email-push auto-bridge (when used),
|
|
495
|
+
// recordings cache, sleep inference state, etc. are preserved
|
|
496
|
+
// for the next reconnect cycle.
|
|
497
|
+
// - The next `ensureBaichuanClient` call sees `api.isReady=false`
|
|
498
|
+
// AND `api.isClosed=false` and routes through
|
|
499
|
+
// `api.ensureConnected()`, which reuses the same api with a
|
|
500
|
+
// freshly reconnected socket pool — see the
|
|
501
|
+
// `socket disconnected but API still valid` branch above.
|
|
502
|
+
// - If the lib reconnect fails repeatedly, the same caller
|
|
503
|
+
// falls back to `cleanupBaichuanApi()` + full recreate (the
|
|
504
|
+
// catch around `ensureConnected()` in ensureBaichuanClient).
|
|
505
|
+
//
|
|
506
|
+
// Calling `cleanupBaichuanApi()` here would defeat all of that:
|
|
507
|
+
// it sets `_closed=true` (next ensureBaichuanClient destroys +
|
|
508
|
+
// recreates from scratch), and worse it tears down any consumer
|
|
509
|
+
// registered against `api.simpleEventListeners` — like the lib
|
|
510
|
+
// email-push auto-bridge — for the entire gap until the next
|
|
511
|
+
// `ensureBaichuanClient` call (which on battery cams can be
|
|
512
|
+
// minutes). We deliberately don't.
|
|
502
513
|
|
|
503
514
|
// Call custom close handler if provided
|
|
504
515
|
if (callbacks.onClose) {
|
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
|
-
//
|
|
1561
|
-
//
|
|
1562
|
-
//
|
|
1563
|
-
//
|
|
1564
|
-
// the
|
|
1565
|
-
//
|
|
1566
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
3624
|
-
//
|
|
3625
|
-
//
|
|
3626
|
-
// `
|
|
3627
|
-
|
|
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
|
|