@apocaliss92/scrypted-reolink-native 0.5.26 → 0.5.27
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 +49 -103
- 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.27",
|
|
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,
|
|
@@ -808,11 +807,6 @@ export class ReolinkCamera
|
|
|
808
807
|
private batteryUpdateTimer: NodeJS.Timeout | undefined;
|
|
809
808
|
private periodicStarted = false;
|
|
810
809
|
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;
|
|
816
810
|
|
|
817
811
|
// Cooldown timestamps to prevent alignAuxDevicesState from overwriting recently set states
|
|
818
812
|
// Camera can take 10+ seconds to reflect state changes in its API response
|
|
@@ -1563,6 +1557,14 @@ export class ReolinkCamera
|
|
|
1563
1557
|
// throw new Error("IP Address is required for TCP devices");
|
|
1564
1558
|
// }
|
|
1565
1559
|
|
|
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
|
+
|
|
1566
1568
|
return {
|
|
1567
1569
|
host: ipAddress,
|
|
1568
1570
|
username,
|
|
@@ -1573,6 +1575,9 @@ export class ReolinkCamera
|
|
|
1573
1575
|
udpDiscoveryMethod: discoveryMethod,
|
|
1574
1576
|
// NOTE: idleDisconnect is NOT set here - the library handles it internally
|
|
1575
1577
|
// based on the battery status detected during connection
|
|
1578
|
+
...(eligibleForEmailPush && this.nativeId
|
|
1579
|
+
? { emailPushCameraId: this.nativeId }
|
|
1580
|
+
: {}),
|
|
1576
1581
|
};
|
|
1577
1582
|
}
|
|
1578
1583
|
|
|
@@ -2118,6 +2123,17 @@ export class ReolinkCamera
|
|
|
2118
2123
|
this.processEvents({ motion, objects }).catch((e) => {
|
|
2119
2124
|
logger.warn("Error processing events", e?.message || String(e));
|
|
2120
2125
|
});
|
|
2126
|
+
|
|
2127
|
+
// Battery cams cache `lastPicture` aggressively because waking
|
|
2128
|
+
// the device for a snapshot is expensive. When motion fires the
|
|
2129
|
+
// cam is, by definition, awake right now — proactively refresh
|
|
2130
|
+
// the cache so the Snapshot mixin → MQTT image entity / HA
|
|
2131
|
+
// discovery serves the trigger frame on the next `takePicture`
|
|
2132
|
+
// call instead of the previous cached one. Wired cams don't
|
|
2133
|
+
// need this (Snapshot mixin always gets fresh data anyway).
|
|
2134
|
+
if (motion && this.isBattery && !this.multiFocalDevice) {
|
|
2135
|
+
void this.refreshSnapshotOnMotion(ev.timestamp ?? Date.now());
|
|
2136
|
+
}
|
|
2121
2137
|
} catch (e) {
|
|
2122
2138
|
logger.warn("Error in onSimpleEvent handler", e?.message || String(e));
|
|
2123
2139
|
}
|
|
@@ -2741,109 +2757,37 @@ export class ReolinkCamera
|
|
|
2741
2757
|
}
|
|
2742
2758
|
|
|
2743
2759
|
/**
|
|
2744
|
-
*
|
|
2745
|
-
*
|
|
2746
|
-
*
|
|
2747
|
-
*
|
|
2748
|
-
*
|
|
2749
|
-
* the
|
|
2750
|
-
*
|
|
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.
|
|
2760
|
+
* Refresh the `lastPicture` cache by fetching a live snapshot via
|
|
2761
|
+
* the Baichuan API. Used on motion events for battery cameras: the
|
|
2762
|
+
* cam is guaranteed awake at that moment (just sent native push or
|
|
2763
|
+
* SMTP), so the round-trip is cheap, and downstream consumers
|
|
2764
|
+
* (Snapshot mixin → MQTT image entity, HA discovery) reading
|
|
2765
|
+
* `takePicture()` on the back of the motion get the trigger frame
|
|
2766
|
+
* instead of the previous cached one. Fire-and-forget; per-camera
|
|
2767
|
+
* debounce so an event burst doesn't trigger overlapping fetches.
|
|
2830
2768
|
*/
|
|
2831
|
-
private
|
|
2832
|
-
|
|
2833
|
-
): Promise<void> {
|
|
2769
|
+
private lastMotionSnapshotAtMs = 0;
|
|
2770
|
+
private motionSnapshotInFlight = false;
|
|
2771
|
+
private async refreshSnapshotOnMotion(timestampMs: number): Promise<void> {
|
|
2772
|
+
if (this.motionSnapshotInFlight) return;
|
|
2773
|
+
if (timestampMs - this.lastMotionSnapshotAtMs < 5_000) return;
|
|
2774
|
+
this.motionSnapshotInFlight = true;
|
|
2775
|
+
this.lastMotionSnapshotAtMs = timestampMs;
|
|
2834
2776
|
const logger = this.getBaichuanLogger();
|
|
2835
2777
|
try {
|
|
2836
2778
|
const client = await this.ensureClient();
|
|
2837
2779
|
const mo = await this.takePictureInternal(client);
|
|
2838
|
-
this.lastPicture = { mo, atMs:
|
|
2780
|
+
this.lastPicture = { mo, atMs: timestampMs };
|
|
2839
2781
|
this.forceNewSnapshot = false;
|
|
2840
2782
|
logger.log(
|
|
2841
|
-
`
|
|
2783
|
+
`Motion snapshot refreshed — next takePicture will serve the trigger frame`,
|
|
2842
2784
|
);
|
|
2843
2785
|
} catch (e) {
|
|
2844
2786
|
logger.warn(
|
|
2845
|
-
`
|
|
2787
|
+
`Motion snapshot refresh failed: ${e?.message || String(e)}`,
|
|
2846
2788
|
);
|
|
2789
|
+
} finally {
|
|
2790
|
+
this.motionSnapshotInFlight = false;
|
|
2847
2791
|
}
|
|
2848
2792
|
}
|
|
2849
2793
|
|
|
@@ -3005,7 +2949,8 @@ export class ReolinkCamera
|
|
|
3005
2949
|
this.statusPollTimer && clearInterval(this.statusPollTimer);
|
|
3006
2950
|
this.sleepCheckTimer && clearInterval(this.sleepCheckTimer);
|
|
3007
2951
|
this.batteryUpdateTimer && clearInterval(this.batteryUpdateTimer);
|
|
3008
|
-
|
|
2952
|
+
// The lib's email-push auto-bridge is released by `api.close()`
|
|
2953
|
+
// inside `resetBaichuanClient` — no separate teardown needed.
|
|
3009
2954
|
this.resetBaichuanClient();
|
|
3010
2955
|
this.plugin.camerasMap.delete(this.id);
|
|
3011
2956
|
}
|
|
@@ -3675,10 +3620,11 @@ export class ReolinkCamera
|
|
|
3675
3620
|
|
|
3676
3621
|
this.startPeriodicTasks();
|
|
3677
3622
|
|
|
3678
|
-
//
|
|
3679
|
-
//
|
|
3680
|
-
//
|
|
3681
|
-
|
|
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.
|
|
3682
3628
|
|
|
3683
3629
|
await this.ensureClient();
|
|
3684
3630
|
|
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
|