@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/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/baichuan-base.ts +5 -55
- package/src/camera.ts +104 -25
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/baichuan-base.ts
CHANGED
|
@@ -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
|
-
//
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
|
|
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
|
-
//
|
|
1613
|
-
//
|
|
1614
|
-
//
|
|
1615
|
-
// (
|
|
1616
|
-
//
|
|
1617
|
-
|
|
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
|
-
*
|
|
2748
|
-
*
|
|
2749
|
-
*
|
|
2750
|
-
*
|
|
2751
|
-
*
|
|
2752
|
-
*
|
|
2753
|
-
*
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
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 {
|