@apocaliss92/scrypted-reolink-native 0.5.25 → 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 +15 -55
- package/src/camera.ts +57 -32
- 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
|
@@ -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" };
|
|
@@ -16,6 +15,13 @@ export interface BaichuanConnectionConfig {
|
|
|
16
15
|
transport: BaichuanTransport;
|
|
17
16
|
debugOptions?: any;
|
|
18
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;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export interface BaichuanConnectionCallbacks {
|
|
@@ -23,26 +29,6 @@ export interface BaichuanConnectionCallbacks {
|
|
|
23
29
|
onClose?: () => void | Promise<void>;
|
|
24
30
|
onSimpleEvent?: (ev: ReolinkSimpleEvent) => void;
|
|
25
31
|
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
32
|
}
|
|
47
33
|
|
|
48
34
|
/**
|
|
@@ -207,7 +193,6 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
207
193
|
private eventSubscriptionActive: boolean = false;
|
|
208
194
|
private lastEventTime: number = 0;
|
|
209
195
|
private currentWrappedEventHandler?: (ev: ReolinkSimpleEvent) => void;
|
|
210
|
-
private currentEmailPushOff?: () => void;
|
|
211
196
|
private subscribeToEventsPromise?: Promise<void>;
|
|
212
197
|
private pingInterval?: NodeJS.Timeout;
|
|
213
198
|
private autoRenewInterval?: NodeJS.Timeout;
|
|
@@ -371,6 +356,9 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
371
356
|
logger,
|
|
372
357
|
debugOptions: config.debugOptions,
|
|
373
358
|
udpDiscoveryMethod: config.udpDiscoveryMethod,
|
|
359
|
+
...(config.emailPushCameraId
|
|
360
|
+
? { emailPushCameraId: config.emailPushCameraId }
|
|
361
|
+
: {}),
|
|
374
362
|
},
|
|
375
363
|
transport: config.transport,
|
|
376
364
|
});
|
|
@@ -904,33 +892,11 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
904
892
|
// the library watchdog handles auto-recovery internally.
|
|
905
893
|
await api.onSimpleEvent(this.currentWrappedEventHandler);
|
|
906
894
|
|
|
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
|
-
}
|
|
895
|
+
// NOTE: the email-push bus subscription used to live here, but
|
|
896
|
+
// it's now owned by `ReolinkCamera.subscribeToEmailPushBus()`
|
|
897
|
+
// because the bus is global and must outlive the Baichuan api —
|
|
898
|
+
// battery cams routinely drop the api between motions, which
|
|
899
|
+
// would silently drop SMTP events with an api-scoped bridge.
|
|
934
900
|
|
|
935
901
|
this.eventSubscriptionActive = true;
|
|
936
902
|
this.lastEventTime = Date.now(); // Initialize on subscription
|
|
@@ -961,12 +927,6 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
961
927
|
// api.close() destroys the pool before the promise settles.
|
|
962
928
|
await this.baichuanApi.offSimpleEvent(this.currentWrappedEventHandler);
|
|
963
929
|
this.currentWrappedEventHandler = undefined;
|
|
964
|
-
if (this.currentEmailPushOff) {
|
|
965
|
-
try {
|
|
966
|
-
this.currentEmailPushOff();
|
|
967
|
-
} catch {}
|
|
968
|
-
this.currentEmailPushOff = undefined;
|
|
969
|
-
}
|
|
970
930
|
logger.debug("Unsubscribed from Baichuan events");
|
|
971
931
|
} catch (e) {
|
|
972
932
|
logger.warn("Error unsubscribing from events", e?.message || String(e));
|
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,
|
|
@@ -1558,6 +1557,14 @@ export class ReolinkCamera
|
|
|
1558
1557
|
// throw new Error("IP Address is required for TCP devices");
|
|
1559
1558
|
// }
|
|
1560
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
|
+
|
|
1561
1568
|
return {
|
|
1562
1569
|
host: ipAddress,
|
|
1563
1570
|
username,
|
|
@@ -1568,6 +1575,9 @@ export class ReolinkCamera
|
|
|
1568
1575
|
udpDiscoveryMethod: discoveryMethod,
|
|
1569
1576
|
// NOTE: idleDisconnect is NOT set here - the library handles it internally
|
|
1570
1577
|
// based on the battery status detected during connection
|
|
1578
|
+
...(eligibleForEmailPush && this.nativeId
|
|
1579
|
+
? { emailPushCameraId: this.nativeId }
|
|
1580
|
+
: {}),
|
|
1571
1581
|
};
|
|
1572
1582
|
}
|
|
1573
1583
|
|
|
@@ -1609,20 +1619,12 @@ export class ReolinkCamera
|
|
|
1609
1619
|
onSimpleEvent: this.onSimpleEventBound,
|
|
1610
1620
|
getEventSubscriptionEnabled: () =>
|
|
1611
1621
|
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),
|
|
1622
|
+
// Email-push events are wired in `init()` directly against the
|
|
1623
|
+
// lib's global bus — NOT through the api here, because for
|
|
1624
|
+
// battery cams the Baichuan api lifetime is much shorter than
|
|
1625
|
+
// the camera lifetime (api closes when the cam sleeps), so an
|
|
1626
|
+
// api-level subscription would be dead by the time the next
|
|
1627
|
+
// motion email arrives. See `subscribeToEmailPushBus()`.
|
|
1626
1628
|
};
|
|
1627
1629
|
}
|
|
1628
1630
|
|
|
@@ -2121,6 +2123,17 @@ export class ReolinkCamera
|
|
|
2121
2123
|
this.processEvents({ motion, objects }).catch((e) => {
|
|
2122
2124
|
logger.warn("Error processing events", e?.message || String(e));
|
|
2123
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
|
+
}
|
|
2124
2137
|
} catch (e) {
|
|
2125
2138
|
logger.warn("Error in onSimpleEvent handler", e?.message || String(e));
|
|
2126
2139
|
}
|
|
@@ -2744,34 +2757,37 @@ export class ReolinkCamera
|
|
|
2744
2757
|
}
|
|
2745
2758
|
|
|
2746
2759
|
/**
|
|
2747
|
-
*
|
|
2748
|
-
*
|
|
2749
|
-
*
|
|
2750
|
-
*
|
|
2751
|
-
*
|
|
2752
|
-
*
|
|
2753
|
-
*
|
|
2754
|
-
*
|
|
2755
|
-
* Snapshot mixin → MQTT image entity (and any other consumer that
|
|
2756
|
-
* calls `takePicture()` on the back of the motion event) serves the
|
|
2757
|
-
* trigger frame instead of 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.
|
|
2758
2768
|
*/
|
|
2759
|
-
private
|
|
2760
|
-
|
|
2761
|
-
): 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;
|
|
2762
2776
|
const logger = this.getBaichuanLogger();
|
|
2763
2777
|
try {
|
|
2764
2778
|
const client = await this.ensureClient();
|
|
2765
2779
|
const mo = await this.takePictureInternal(client);
|
|
2766
|
-
this.lastPicture = { mo, atMs:
|
|
2780
|
+
this.lastPicture = { mo, atMs: timestampMs };
|
|
2767
2781
|
this.forceNewSnapshot = false;
|
|
2768
2782
|
logger.log(
|
|
2769
|
-
`
|
|
2783
|
+
`Motion snapshot refreshed — next takePicture will serve the trigger frame`,
|
|
2770
2784
|
);
|
|
2771
2785
|
} catch (e) {
|
|
2772
2786
|
logger.warn(
|
|
2773
|
-
`
|
|
2787
|
+
`Motion snapshot refresh failed: ${e?.message || String(e)}`,
|
|
2774
2788
|
);
|
|
2789
|
+
} finally {
|
|
2790
|
+
this.motionSnapshotInFlight = false;
|
|
2775
2791
|
}
|
|
2776
2792
|
}
|
|
2777
2793
|
|
|
@@ -2933,6 +2949,8 @@ export class ReolinkCamera
|
|
|
2933
2949
|
this.statusPollTimer && clearInterval(this.statusPollTimer);
|
|
2934
2950
|
this.sleepCheckTimer && clearInterval(this.sleepCheckTimer);
|
|
2935
2951
|
this.batteryUpdateTimer && clearInterval(this.batteryUpdateTimer);
|
|
2952
|
+
// The lib's email-push auto-bridge is released by `api.close()`
|
|
2953
|
+
// inside `resetBaichuanClient` — no separate teardown needed.
|
|
2936
2954
|
this.resetBaichuanClient();
|
|
2937
2955
|
this.plugin.camerasMap.delete(this.id);
|
|
2938
2956
|
}
|
|
@@ -3601,6 +3619,13 @@ export class ReolinkCamera
|
|
|
3601
3619
|
}
|
|
3602
3620
|
|
|
3603
3621
|
this.startPeriodicTasks();
|
|
3622
|
+
|
|
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.
|
|
3628
|
+
|
|
3604
3629
|
await this.ensureClient();
|
|
3605
3630
|
|
|
3606
3631
|
try {
|
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
|