@apocaliss92/scrypted-reolink-native 0.5.26 → 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/README.md +1 -0
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +2 -2
- package/src/baichuan-base.ts +10 -0
- package/src/camera.ts +69 -51
- package/src/connect.ts +29 -0
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.
|
|
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",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
]
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@apocaliss92/nodelink-js": "^0.4.
|
|
47
|
+
"@apocaliss92/nodelink-js": "^0.4.32",
|
|
48
48
|
"@scrypted/common": "file:../../scrypted/common",
|
|
49
49
|
"@scrypted/rtsp": "file:../../scrypted/plugins/rtsp",
|
|
50
50
|
"@scrypted/sdk": "^0.3.118"
|
package/src/baichuan-base.ts
CHANGED
|
@@ -15,6 +15,13 @@ export interface BaichuanConnectionConfig {
|
|
|
15
15
|
transport: BaichuanTransport;
|
|
16
16
|
debugOptions?: any;
|
|
17
17
|
udpDiscoveryMethod?: BaichuanClientOptions["udpDiscoveryMethod"];
|
|
18
|
+
/**
|
|
19
|
+
* When set, the api auto-bridges the global email-push bus into
|
|
20
|
+
* its own `simpleEventListeners` so SMTP motion lands on the same
|
|
21
|
+
* `onSimpleEvent` stream as native Baichuan push. Standalone
|
|
22
|
+
* (non-NVR-child, non-multifocal-lens) cameras only.
|
|
23
|
+
*/
|
|
24
|
+
emailPushCameraId?: string;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export interface BaichuanConnectionCallbacks {
|
|
@@ -349,6 +356,9 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
349
356
|
logger,
|
|
350
357
|
debugOptions: config.debugOptions,
|
|
351
358
|
udpDiscoveryMethod: config.udpDiscoveryMethod,
|
|
359
|
+
...(config.emailPushCameraId
|
|
360
|
+
? { emailPushCameraId: config.emailPushCameraId }
|
|
361
|
+
: {}),
|
|
352
362
|
},
|
|
353
363
|
transport: config.transport,
|
|
354
364
|
});
|
package/src/camera.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BatteryInfo,
|
|
3
3
|
DeviceCapabilities,
|
|
4
|
-
EmailPushEvent,
|
|
5
4
|
NativeVideoStreamVariant,
|
|
6
5
|
PtzCommand,
|
|
7
6
|
PtzPreset,
|
|
@@ -809,9 +808,11 @@ export class ReolinkCamera
|
|
|
809
808
|
private periodicStarted = false;
|
|
810
809
|
private statusPollTimer: NodeJS.Timeout | undefined;
|
|
811
810
|
// Off-handle for the global email-push bus subscription. Tied to
|
|
812
|
-
// the camera
|
|
813
|
-
//
|
|
814
|
-
// api-scoped
|
|
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.
|
|
815
816
|
private emailPushBusOff?: () => void;
|
|
816
817
|
|
|
817
818
|
// Cooldown timestamps to prevent alignAuxDevicesState from overwriting recently set states
|
|
@@ -1563,6 +1564,15 @@ export class ReolinkCamera
|
|
|
1563
1564
|
// throw new Error("IP Address is required for TCP devices");
|
|
1564
1565
|
// }
|
|
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.
|
|
1575
|
+
|
|
1566
1576
|
return {
|
|
1567
1577
|
host: ipAddress,
|
|
1568
1578
|
username,
|
|
@@ -2118,6 +2128,17 @@ export class ReolinkCamera
|
|
|
2118
2128
|
this.processEvents({ motion, objects }).catch((e) => {
|
|
2119
2129
|
logger.warn("Error processing events", e?.message || String(e));
|
|
2120
2130
|
});
|
|
2131
|
+
|
|
2132
|
+
// Battery cams cache `lastPicture` aggressively because waking
|
|
2133
|
+
// the device for a snapshot is expensive. When motion fires the
|
|
2134
|
+
// cam is, by definition, awake right now — proactively refresh
|
|
2135
|
+
// the cache so the Snapshot mixin → MQTT image entity / HA
|
|
2136
|
+
// discovery serves the trigger frame on the next `takePicture`
|
|
2137
|
+
// call instead of the previous cached one. Wired cams don't
|
|
2138
|
+
// need this (Snapshot mixin always gets fresh data anyway).
|
|
2139
|
+
if (motion && this.isBattery && !this.multiFocalDevice) {
|
|
2140
|
+
void this.refreshSnapshotOnMotion(ev.timestamp ?? Date.now());
|
|
2141
|
+
}
|
|
2121
2142
|
} catch (e) {
|
|
2122
2143
|
logger.warn("Error in onSimpleEvent handler", e?.message || String(e));
|
|
2123
2144
|
}
|
|
@@ -2741,13 +2762,17 @@ export class ReolinkCamera
|
|
|
2741
2762
|
}
|
|
2742
2763
|
|
|
2743
2764
|
/**
|
|
2744
|
-
* Subscribe to the lib's global email-push bus and translate
|
|
2745
|
-
* matching
|
|
2746
|
-
* to this camera's
|
|
2747
|
-
*
|
|
2748
|
-
*
|
|
2749
|
-
*
|
|
2750
|
-
*
|
|
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`.
|
|
2751
2776
|
*/
|
|
2752
2777
|
private async subscribeToEmailPushBus(): Promise<void> {
|
|
2753
2778
|
if (this.emailPushBusOff) return;
|
|
@@ -2761,26 +2786,23 @@ export class ReolinkCamera
|
|
|
2761
2786
|
);
|
|
2762
2787
|
this.emailPushBusOff = onEmailPushEvent((event) => {
|
|
2763
2788
|
if (event.cameraId !== ownNativeId) return;
|
|
2789
|
+
const channel = this.storageSettings.values.rtspChannel ?? 0;
|
|
2790
|
+
const mapped = mapEmailPushInferredType(event.inferredType);
|
|
2764
2791
|
logger.log(
|
|
2765
|
-
`E-mail Push event received (type=${event.inferredType}) → dispatching
|
|
2792
|
+
`E-mail Push event received (type=${event.inferredType}) → dispatching ${mapped}`,
|
|
2766
2793
|
);
|
|
2767
2794
|
try {
|
|
2768
2795
|
this.onSimpleEvent({
|
|
2769
|
-
type:
|
|
2770
|
-
channel
|
|
2796
|
+
type: mapped,
|
|
2797
|
+
channel,
|
|
2771
2798
|
timestamp: event.receivedAtMs,
|
|
2772
2799
|
});
|
|
2773
|
-
//
|
|
2774
|
-
//
|
|
2775
|
-
|
|
2776
|
-
if (
|
|
2777
|
-
event.inferredType !== "motion" &&
|
|
2778
|
-
event.inferredType !== "doorbell" &&
|
|
2779
|
-
event.inferredType !== "other"
|
|
2780
|
-
) {
|
|
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") {
|
|
2781
2803
|
this.onSimpleEvent({
|
|
2782
2804
|
type: "motion",
|
|
2783
|
-
channel
|
|
2805
|
+
channel,
|
|
2784
2806
|
timestamp: event.receivedAtMs,
|
|
2785
2807
|
});
|
|
2786
2808
|
}
|
|
@@ -2789,14 +2811,6 @@ export class ReolinkCamera
|
|
|
2789
2811
|
`E-mail Push: onSimpleEvent dispatch threw: ${e?.message || String(e)}`,
|
|
2790
2812
|
);
|
|
2791
2813
|
}
|
|
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
2814
|
});
|
|
2801
2815
|
logger.log("E-mail Push: subscribed to global bus");
|
|
2802
2816
|
} catch (e) {
|
|
@@ -2816,34 +2830,37 @@ export class ReolinkCamera
|
|
|
2816
2830
|
}
|
|
2817
2831
|
|
|
2818
2832
|
/**
|
|
2819
|
-
*
|
|
2820
|
-
*
|
|
2821
|
-
*
|
|
2822
|
-
*
|
|
2823
|
-
*
|
|
2824
|
-
* the
|
|
2825
|
-
*
|
|
2826
|
-
*
|
|
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.
|
|
2833
|
+
* Refresh the `lastPicture` cache by fetching a live snapshot via
|
|
2834
|
+
* the Baichuan API. Used on motion events for battery cameras: the
|
|
2835
|
+
* cam is guaranteed awake at that moment (just sent native push or
|
|
2836
|
+
* SMTP), so the round-trip is cheap, and downstream consumers
|
|
2837
|
+
* (Snapshot mixin → MQTT image entity, HA discovery) reading
|
|
2838
|
+
* `takePicture()` on the back of the motion get the trigger frame
|
|
2839
|
+
* instead of the previous cached one. Fire-and-forget; per-camera
|
|
2840
|
+
* debounce so an event burst doesn't trigger overlapping fetches.
|
|
2830
2841
|
*/
|
|
2831
|
-
private
|
|
2832
|
-
|
|
2833
|
-
): Promise<void> {
|
|
2842
|
+
private lastMotionSnapshotAtMs = 0;
|
|
2843
|
+
private motionSnapshotInFlight = false;
|
|
2844
|
+
private async refreshSnapshotOnMotion(timestampMs: number): Promise<void> {
|
|
2845
|
+
if (this.motionSnapshotInFlight) return;
|
|
2846
|
+
if (timestampMs - this.lastMotionSnapshotAtMs < 5_000) return;
|
|
2847
|
+
this.motionSnapshotInFlight = true;
|
|
2848
|
+
this.lastMotionSnapshotAtMs = timestampMs;
|
|
2834
2849
|
const logger = this.getBaichuanLogger();
|
|
2835
2850
|
try {
|
|
2836
2851
|
const client = await this.ensureClient();
|
|
2837
2852
|
const mo = await this.takePictureInternal(client);
|
|
2838
|
-
this.lastPicture = { mo, atMs:
|
|
2853
|
+
this.lastPicture = { mo, atMs: timestampMs };
|
|
2839
2854
|
this.forceNewSnapshot = false;
|
|
2840
2855
|
logger.log(
|
|
2841
|
-
`
|
|
2856
|
+
`Motion snapshot refreshed — next takePicture will serve the trigger frame`,
|
|
2842
2857
|
);
|
|
2843
2858
|
} catch (e) {
|
|
2844
2859
|
logger.warn(
|
|
2845
|
-
`
|
|
2860
|
+
`Motion snapshot refresh failed: ${e?.message || String(e)}`,
|
|
2846
2861
|
);
|
|
2862
|
+
} finally {
|
|
2863
|
+
this.motionSnapshotInFlight = false;
|
|
2847
2864
|
}
|
|
2848
2865
|
}
|
|
2849
2866
|
|
|
@@ -3675,9 +3692,10 @@ export class ReolinkCamera
|
|
|
3675
3692
|
|
|
3676
3693
|
this.startPeriodicTasks();
|
|
3677
3694
|
|
|
3678
|
-
// Subscribe
|
|
3679
|
-
//
|
|
3680
|
-
//
|
|
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.
|
|
3681
3699
|
await this.subscribeToEmailPushBus();
|
|
3682
3700
|
|
|
3683
3701
|
await this.ensureClient();
|
package/src/connect.ts
CHANGED
|
@@ -14,6 +14,18 @@ export type BaichuanConnectInputs = {
|
|
|
14
14
|
logger?: Console;
|
|
15
15
|
debugOptions?: BaichuanClientOptions["debugOptions"];
|
|
16
16
|
udpDiscoveryMethod?: BaichuanClientOptions["udpDiscoveryMethod"];
|
|
17
|
+
/**
|
|
18
|
+
* When set, the lib api auto-bridges the global email-push bus into
|
|
19
|
+
* its own `simpleEventListeners`, so SMTP motion lands on the same
|
|
20
|
+
* `onSimpleEvent` stream as native Baichuan push. Bridge survives
|
|
21
|
+
* TCP transient disconnects (it's a pure JS fan-out). For Reolink
|
|
22
|
+
* cameras this should be the camera's plugin-side nativeId — the
|
|
23
|
+
* `EmailPushServerDevice` resolves `cam-<nativeId>@<domain>` against
|
|
24
|
+
* the same string, so the round-trip is symmetric.
|
|
25
|
+
*/
|
|
26
|
+
emailPushCameraId?: string;
|
|
27
|
+
/** Channel reported on the synthesised event. Default 0. */
|
|
28
|
+
emailPushChannel?: number;
|
|
17
29
|
};
|
|
18
30
|
|
|
19
31
|
export function normalizeUid(uid?: string): string | undefined {
|
|
@@ -38,6 +50,21 @@ export async function createBaichuanApi(props: {
|
|
|
38
50
|
debugOptions: inputs.debugOptions ?? {},
|
|
39
51
|
};
|
|
40
52
|
|
|
53
|
+
// The lib's auto-bridge fields belong on the second positional arg
|
|
54
|
+
// of `new ReolinkBaichuanApi(opts)` alongside `nativeOnly` etc.; we
|
|
55
|
+
// build a small `extras` bag and spread it at construction time
|
|
56
|
+
// below for both tcp + udp paths.
|
|
57
|
+
const extras: {
|
|
58
|
+
emailPushCameraId?: string;
|
|
59
|
+
emailPushChannel?: number;
|
|
60
|
+
} = {};
|
|
61
|
+
if (inputs.emailPushCameraId) {
|
|
62
|
+
extras.emailPushCameraId = inputs.emailPushCameraId;
|
|
63
|
+
if (inputs.emailPushChannel !== undefined) {
|
|
64
|
+
extras.emailPushChannel = inputs.emailPushChannel;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
const attachErrorHandler = (api: ReolinkBaichuanApi) => {
|
|
42
69
|
// Critical: BaichuanClient emits 'error'. If nobody listens, Node treats it as an
|
|
43
70
|
// uncaught exception. Ensure we always have a listener.
|
|
@@ -75,6 +102,7 @@ export async function createBaichuanApi(props: {
|
|
|
75
102
|
if (transport === "tcp") {
|
|
76
103
|
const api = new ReolinkBaichuanApi({
|
|
77
104
|
...base,
|
|
105
|
+
...extras,
|
|
78
106
|
transport: "tcp",
|
|
79
107
|
});
|
|
80
108
|
attachErrorHandler(api);
|
|
@@ -88,6 +116,7 @@ export async function createBaichuanApi(props: {
|
|
|
88
116
|
|
|
89
117
|
const api = new ReolinkBaichuanApi({
|
|
90
118
|
...base,
|
|
119
|
+
...extras,
|
|
91
120
|
transport: "udp",
|
|
92
121
|
uid,
|
|
93
122
|
// NOTE: idleDisconnect is NOT set here - the library handles it internally
|