@apocaliss92/scrypted-reolink-native 0.5.19 → 0.5.21
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/main.nodejs.js.LICENSE.txt +4 -0
- package/dist/plugin.zip +0 -0
- package/package.json +3 -3
- package/src/baichuan-base.ts +47 -1
- package/src/camera.ts +205 -5
- package/src/email-push-server-device.ts +583 -0
- package/src/main.ts +228 -188
|
@@ -107,6 +107,8 @@ PERFORMANCE OF THIS SOFTWARE.
|
|
|
107
107
|
|
|
108
108
|
/*! http://mths.be/fromcodepoint v0.1.0 by @mathias */
|
|
109
109
|
|
|
110
|
+
/*! https://mths.be/he v1.2.0 by @mathias | MIT license */
|
|
111
|
+
|
|
110
112
|
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
|
111
113
|
|
|
112
114
|
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
@@ -115,6 +117,8 @@ PERFORMANCE OF THIS SOFTWARE.
|
|
|
115
117
|
|
|
116
118
|
/*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> */
|
|
117
119
|
|
|
120
|
+
/*!*/
|
|
121
|
+
|
|
118
122
|
/**
|
|
119
123
|
* @preserve
|
|
120
124
|
* Copyright 2015-2018 Igor Bezkrovnyi
|
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.21",
|
|
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",
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
"ScryptedDeviceCreator",
|
|
39
39
|
"DeviceProvider",
|
|
40
40
|
"DeviceCreator",
|
|
41
|
-
"
|
|
41
|
+
"DeviceDiscoveryOff",
|
|
42
42
|
"Settings",
|
|
43
43
|
"HttpRequestHandler"
|
|
44
44
|
]
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@apocaliss92/nodelink-js": "^0.4.
|
|
47
|
+
"@apocaliss92/nodelink-js": "^0.4.28",
|
|
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
|
@@ -22,6 +22,16 @@ export interface BaichuanConnectionCallbacks {
|
|
|
22
22
|
onClose?: () => void | Promise<void>;
|
|
23
23
|
onSimpleEvent?: (ev: ReolinkSimpleEvent) => void;
|
|
24
24
|
getEventSubscriptionEnabled?: () => boolean;
|
|
25
|
+
/**
|
|
26
|
+
* When provided, the base class binds the camera's api to the global
|
|
27
|
+
* email-push bus filtered on `cameraId === emailPushCameraId()`. The
|
|
28
|
+
* lib's `subscribeEmailPushEvents` converts each matching event into
|
|
29
|
+
* a `ReolinkSimpleEvent` and dispatches it through `onSimpleEvent`,
|
|
30
|
+
* so the camera's existing motion / AI handler lights up for SMTP-
|
|
31
|
+
* delivered events with no extra wiring. Standalone cameras only —
|
|
32
|
+
* leave undefined on NVR children where email-push isn't meaningful.
|
|
33
|
+
*/
|
|
34
|
+
emailPushCameraId?: () => string;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
/**
|
|
@@ -186,6 +196,7 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
186
196
|
private eventSubscriptionActive: boolean = false;
|
|
187
197
|
private lastEventTime: number = 0;
|
|
188
198
|
private currentWrappedEventHandler?: (ev: ReolinkSimpleEvent) => void;
|
|
199
|
+
private currentEmailPushOff?: () => void;
|
|
189
200
|
private subscribeToEventsPromise?: Promise<void>;
|
|
190
201
|
private pingInterval?: NodeJS.Timeout;
|
|
191
202
|
private autoRenewInterval?: NodeJS.Timeout;
|
|
@@ -866,7 +877,10 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
866
877
|
|
|
867
878
|
// Subscribe to events with wrapper to track last event time
|
|
868
879
|
try {
|
|
869
|
-
|
|
880
|
+
// The outer `subscribeToEvents` already guards on `!callbacks.onSimpleEvent`
|
|
881
|
+
// and returns before reaching this point. The narrowed local keeps
|
|
882
|
+
// strict-null TS happy without changing the runtime contract.
|
|
883
|
+
const originalHandler = callbacks.onSimpleEvent!;
|
|
870
884
|
// Create and store the wrapped handler so it can be properly removed later
|
|
871
885
|
this.currentWrappedEventHandler = (ev: ReolinkSimpleEvent) => {
|
|
872
886
|
// Update last event time
|
|
@@ -878,6 +892,32 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
878
892
|
// onSimpleEvent no longer throws on initial subscribe failure;
|
|
879
893
|
// the library watchdog handles auto-recovery internally.
|
|
880
894
|
await api.onSimpleEvent(this.currentWrappedEventHandler);
|
|
895
|
+
|
|
896
|
+
// Bridge the global email-push bus into this api's onSimpleEvent
|
|
897
|
+
// stream so the same wrapped handler above (and any other
|
|
898
|
+
// listener) sees SMTP-delivered motion exactly like a native push.
|
|
899
|
+
// Idempotent: if a previous off-handle is still around, release it.
|
|
900
|
+
if (callbacks.emailPushCameraId) {
|
|
901
|
+
if (this.currentEmailPushOff) {
|
|
902
|
+
try {
|
|
903
|
+
this.currentEmailPushOff();
|
|
904
|
+
} catch {}
|
|
905
|
+
this.currentEmailPushOff = undefined;
|
|
906
|
+
}
|
|
907
|
+
try {
|
|
908
|
+
this.currentEmailPushOff = api.subscribeEmailPushEvents({
|
|
909
|
+
cameraId: callbacks.emailPushCameraId(),
|
|
910
|
+
channel: 0,
|
|
911
|
+
});
|
|
912
|
+
logger.debug("Bridged email-push bus to onSimpleEvent");
|
|
913
|
+
} catch (e) {
|
|
914
|
+
logger.warn(
|
|
915
|
+
"Failed to bridge email-push events",
|
|
916
|
+
e?.message || String(e),
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
881
921
|
this.eventSubscriptionActive = true;
|
|
882
922
|
this.lastEventTime = Date.now(); // Initialize on subscription
|
|
883
923
|
logger.debug("Subscribed to Baichuan events (library watchdog handles auto-recovery)");
|
|
@@ -907,6 +947,12 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
|
|
|
907
947
|
// api.close() destroys the pool before the promise settles.
|
|
908
948
|
await this.baichuanApi.offSimpleEvent(this.currentWrappedEventHandler);
|
|
909
949
|
this.currentWrappedEventHandler = undefined;
|
|
950
|
+
if (this.currentEmailPushOff) {
|
|
951
|
+
try {
|
|
952
|
+
this.currentEmailPushOff();
|
|
953
|
+
} catch {}
|
|
954
|
+
this.currentEmailPushOff = undefined;
|
|
955
|
+
}
|
|
910
956
|
logger.debug("Unsubscribed from Baichuan events");
|
|
911
957
|
} catch (e) {
|
|
912
958
|
logger.warn("Error unsubscribing from events", e?.message || String(e));
|
package/src/camera.ts
CHANGED
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
getApiRelevantDebugLogs,
|
|
66
66
|
getDebugLogChoices,
|
|
67
67
|
} from "./debug-options";
|
|
68
|
+
import { EMAIL_PUSH_SERVER_NATIVE_ID } from "./email-push-server-device";
|
|
68
69
|
import ReolinkNativePlugin from "./main";
|
|
69
70
|
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
70
71
|
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
@@ -682,6 +683,77 @@ export class ReolinkCamera
|
|
|
682
683
|
this.scheduleStreamManagerRestart("compositeDisableTranscode changed");
|
|
683
684
|
},
|
|
684
685
|
},
|
|
686
|
+
// ─── E-mail Push ─────────────────────────────────────────────
|
|
687
|
+
// Per-camera knobs that pair with the singleton
|
|
688
|
+
// `Reolink E-mail Push Server` device. Hidden by default and
|
|
689
|
+
// un-hidden in `init()` only for standalone cameras (NVR-attached
|
|
690
|
+
// children share the NVR's mail path and would silently fail).
|
|
691
|
+
emailPushCachedStatus: {
|
|
692
|
+
group: "E-mail Push",
|
|
693
|
+
title: "Camera-side SMTP target",
|
|
694
|
+
description:
|
|
695
|
+
"Last read of the camera's own SMTP config (smtpServer / recipient). Click Refresh to update — reading wakes battery cameras, so we don't auto-refresh.",
|
|
696
|
+
type: "string",
|
|
697
|
+
readonly: true,
|
|
698
|
+
defaultValue: "(not loaded yet — click Refresh status below)",
|
|
699
|
+
hide: true,
|
|
700
|
+
},
|
|
701
|
+
emailPushTriggers: {
|
|
702
|
+
group: "E-mail Push",
|
|
703
|
+
title: "Trigger events",
|
|
704
|
+
description:
|
|
705
|
+
"Event types whose schedule will be flipped to 24/7 ON when Auto-configure is applied. MD = generic motion.",
|
|
706
|
+
type: "string",
|
|
707
|
+
multiple: true,
|
|
708
|
+
combobox: true,
|
|
709
|
+
defaultValue: ["MD", "people", "vehicle"],
|
|
710
|
+
choices: ["MD", "people", "vehicle"],
|
|
711
|
+
hide: true,
|
|
712
|
+
},
|
|
713
|
+
emailPushAttachment: {
|
|
714
|
+
group: "E-mail Push",
|
|
715
|
+
title: "Attachment",
|
|
716
|
+
description:
|
|
717
|
+
"What the camera attaches when it sends an alert. `picture` is recommended — it lets the manager publish the snapshot to MQTT image entities.",
|
|
718
|
+
type: "string",
|
|
719
|
+
defaultValue: "picture",
|
|
720
|
+
choices: ["picture", "video", "none"],
|
|
721
|
+
immediate: true,
|
|
722
|
+
hide: true,
|
|
723
|
+
},
|
|
724
|
+
emailPushAutoConfigure: {
|
|
725
|
+
group: "E-mail Push",
|
|
726
|
+
title: "Auto-configure from Email Push Server",
|
|
727
|
+
description:
|
|
728
|
+
"Reads host/port/auth/domain from the singleton server device and pushes the matching SMTP + schedule to this camera. Open the Reolink E-mail Push Server device once first so it bootstraps.",
|
|
729
|
+
type: "button",
|
|
730
|
+
hide: true,
|
|
731
|
+
onPut: async () => {
|
|
732
|
+
await this.autoConfigureEmailPushFromServer();
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
emailPushTest: {
|
|
736
|
+
group: "E-mail Push",
|
|
737
|
+
title: "Send test e-mail",
|
|
738
|
+
description:
|
|
739
|
+
"Asks the camera to perform a live SMTP send against its currently saved target. Can take up to 60s; returns success/failure.",
|
|
740
|
+
type: "button",
|
|
741
|
+
hide: true,
|
|
742
|
+
onPut: async () => {
|
|
743
|
+
await this.runEmailPushTest();
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
emailPushRefreshStatus: {
|
|
747
|
+
group: "E-mail Push",
|
|
748
|
+
title: "Refresh status",
|
|
749
|
+
description:
|
|
750
|
+
"Reads the current camera-side SMTP config and updates the status row above. Wakes battery cameras.",
|
|
751
|
+
type: "button",
|
|
752
|
+
hide: true,
|
|
753
|
+
onPut: async () => {
|
|
754
|
+
await this.refreshEmailPushStatus();
|
|
755
|
+
},
|
|
756
|
+
},
|
|
685
757
|
});
|
|
686
758
|
|
|
687
759
|
ptzPresets = new ReolinkPtzPresets(this);
|
|
@@ -1536,6 +1608,12 @@ export class ReolinkCamera
|
|
|
1536
1608
|
onSimpleEvent: this.onSimpleEventBound,
|
|
1537
1609
|
getEventSubscriptionEnabled: () =>
|
|
1538
1610
|
this.isEventDispatchEnabled?.() ?? false,
|
|
1611
|
+
// Bind this api to the global email-push bus so battery cameras
|
|
1612
|
+
// delivering motion via SMTP land on the same `onSimpleEvent`
|
|
1613
|
+
// stream as the native push. The Email Push Server device
|
|
1614
|
+
// (singleton under the provider) maps `cam-<nativeId>@<domain>`
|
|
1615
|
+
// RCPT addresses back to this `nativeId`.
|
|
1616
|
+
emailPushCameraId: () => this.nativeId ?? "",
|
|
1539
1617
|
};
|
|
1540
1618
|
}
|
|
1541
1619
|
|
|
@@ -1992,7 +2070,10 @@ export class ReolinkCamera
|
|
|
1992
2070
|
case "battery":
|
|
1993
2071
|
if (ev.battery) {
|
|
1994
2072
|
this.updateBatteryInfo(ev.battery as any).catch((e) => {
|
|
1995
|
-
logger.debug(
|
|
2073
|
+
logger.debug(
|
|
2074
|
+
"Error updating battery from push",
|
|
2075
|
+
e?.message || String(e),
|
|
2076
|
+
);
|
|
1996
2077
|
});
|
|
1997
2078
|
}
|
|
1998
2079
|
return;
|
|
@@ -2544,6 +2625,115 @@ export class ReolinkCamera
|
|
|
2544
2625
|
await this.storageSettings.putSetting(key, value);
|
|
2545
2626
|
}
|
|
2546
2627
|
|
|
2628
|
+
/**
|
|
2629
|
+
* Push the singleton E-mail Push Server's manager-side SMTP config
|
|
2630
|
+
* down to this camera. Mirrors the per-camera select on the server
|
|
2631
|
+
* device but lives next to all other per-camera knobs so users can
|
|
2632
|
+
* stay on the camera page. NVR-attached / multifocal children skip
|
|
2633
|
+
* this — the StorageSettings `hide` flag is set accordingly in init.
|
|
2634
|
+
*/
|
|
2635
|
+
async autoConfigureEmailPushFromServer(): Promise<void> {
|
|
2636
|
+
const logger = this.getBaichuanLogger();
|
|
2637
|
+
// Materialise the singleton — Scrypted only constructs it after
|
|
2638
|
+
// the first `getDevice` call, which may not have happened yet.
|
|
2639
|
+
await this.plugin.getDevice(EMAIL_PUSH_SERVER_NATIVE_ID);
|
|
2640
|
+
const server = this.plugin.emailPushServer;
|
|
2641
|
+
if (!server) {
|
|
2642
|
+
throw new Error(
|
|
2643
|
+
"Email Push Server not available. Open the 'Reolink E-mail Push Server' device once to initialise it.",
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
const params = server.getManagerSetupParamsForCamera({
|
|
2647
|
+
id: this.id,
|
|
2648
|
+
nativeId: this.nativeId!,
|
|
2649
|
+
});
|
|
2650
|
+
if (!params) {
|
|
2651
|
+
throw new Error(
|
|
2652
|
+
"Email Push Server has no usable LAN address yet, or this camera is NVR-attached.",
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
const triggers = this.storageSettings.values.emailPushTriggers ?? [
|
|
2656
|
+
"MD",
|
|
2657
|
+
"people",
|
|
2658
|
+
"vehicle",
|
|
2659
|
+
];
|
|
2660
|
+
const attachment =
|
|
2661
|
+
(this.storageSettings.values.emailPushAttachment as
|
|
2662
|
+
| "picture"
|
|
2663
|
+
| "video"
|
|
2664
|
+
| "none") ?? "picture";
|
|
2665
|
+
|
|
2666
|
+
logger.log(
|
|
2667
|
+
`E-mail Push: configuring against ${params.managerHost}:${params.managerPort} (recipient=${params.recipientLocalPart}@${params.domain})`,
|
|
2668
|
+
);
|
|
2669
|
+
const api = await this.ensureClient();
|
|
2670
|
+
await api.setupEmailPushToManager(
|
|
2671
|
+
{
|
|
2672
|
+
...params,
|
|
2673
|
+
attachmentType: attachment,
|
|
2674
|
+
triggerTypes: triggers,
|
|
2675
|
+
runTest: false,
|
|
2676
|
+
},
|
|
2677
|
+
this.storageSettings.values.rtspChannel ?? 0,
|
|
2678
|
+
);
|
|
2679
|
+
logger.log("E-mail Push: setup applied ✓");
|
|
2680
|
+
// Pull the camera's confirmation back into the status row.
|
|
2681
|
+
await this.refreshEmailPushStatus().catch((e) => {
|
|
2682
|
+
logger.warn(
|
|
2683
|
+
`E-mail Push: status refresh after auto-configure failed: ${e?.message || e}`,
|
|
2684
|
+
);
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
/**
|
|
2689
|
+
* Ask the camera to perform a live SMTP send against its current
|
|
2690
|
+
* (saved) target. Uses the lib's default 60s timeout — Reolink
|
|
2691
|
+
* firmwares can take a while when the manager is slow or DNS resolves
|
|
2692
|
+
* after a delay. Result is logged + reflected in the status row.
|
|
2693
|
+
*/
|
|
2694
|
+
async runEmailPushTest(): Promise<void> {
|
|
2695
|
+
const logger = this.getBaichuanLogger();
|
|
2696
|
+
const api = await this.ensureClient();
|
|
2697
|
+
logger.log("E-mail Push: sending test e-mail…");
|
|
2698
|
+
const ok = await api.testEmail();
|
|
2699
|
+
if (ok) {
|
|
2700
|
+
logger.log("E-mail Push: test succeeded ✓");
|
|
2701
|
+
} else {
|
|
2702
|
+
logger.warn(
|
|
2703
|
+
"E-mail Push: test failed (camera reported 482 — check server reachable from camera + credentials).",
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2706
|
+
const ts = new Date().toISOString();
|
|
2707
|
+
this.storageSettings.values.emailPushCachedStatus = ok
|
|
2708
|
+
? `Test OK at ${ts}`
|
|
2709
|
+
: `Test FAILED at ${ts} (482 — server unreachable or auth wrong)`;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
/**
|
|
2713
|
+
* Read the camera's current SMTP config (cmdId=42) and render a
|
|
2714
|
+
* compact summary into the readonly status row. Wakes battery
|
|
2715
|
+
* cameras, so we never trigger this automatically — the user has to
|
|
2716
|
+
* click the button.
|
|
2717
|
+
*/
|
|
2718
|
+
async refreshEmailPushStatus(): Promise<void> {
|
|
2719
|
+
const logger = this.getBaichuanLogger();
|
|
2720
|
+
try {
|
|
2721
|
+
const api = await this.ensureClient();
|
|
2722
|
+
const cfg = await api.getEmail();
|
|
2723
|
+
const ts = new Date().toISOString();
|
|
2724
|
+
const target = `${cfg.smtpServer || "(unset)"}:${cfg.smtpPort ?? "?"}`;
|
|
2725
|
+
const recipient = cfg.address1 || "(no recipient)";
|
|
2726
|
+
this.storageSettings.values.emailPushCachedStatus = `Target: ${target} · Recipient: ${recipient} · refreshed ${ts}`;
|
|
2727
|
+
logger.log(
|
|
2728
|
+
`E-mail Push: status refreshed (target=${target} recipient=${recipient})`,
|
|
2729
|
+
);
|
|
2730
|
+
} catch (e) {
|
|
2731
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2732
|
+
this.storageSettings.values.emailPushCachedStatus = `Failed to read camera config: ${msg}`;
|
|
2733
|
+
logger.warn(`E-mail Push: refresh failed: ${msg}`);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2547
2737
|
async takePictureInternal(client: ReolinkBaichuanApi) {
|
|
2548
2738
|
const { rtspChannel, variantType } = this.storageSettings.values;
|
|
2549
2739
|
const logger = this.getBaichuanLogger();
|
|
@@ -2820,10 +3010,7 @@ export class ReolinkCamera
|
|
|
2820
3010
|
this.siren.on = enabled || playing;
|
|
2821
3011
|
}
|
|
2822
3012
|
} catch (e) {
|
|
2823
|
-
logger.warn(
|
|
2824
|
-
"Failed to align siren state",
|
|
2825
|
-
e?.message || String(e),
|
|
2826
|
-
);
|
|
3013
|
+
logger.warn("Failed to align siren state", e?.message || String(e));
|
|
2827
3014
|
}
|
|
2828
3015
|
}
|
|
2829
3016
|
}
|
|
@@ -3444,6 +3631,19 @@ export class ReolinkCamera
|
|
|
3444
3631
|
const hideUid = !this.isBattery || this.isOnNvr || !!this.multiFocalDevice;
|
|
3445
3632
|
this.storageSettings.settings.uid.hide = hideUid;
|
|
3446
3633
|
this.storageSettings.settings.discoveryMethod.hide = hideUid;
|
|
3634
|
+
|
|
3635
|
+
// E-mail Push group: only standalone cameras get the per-camera
|
|
3636
|
+
// SMTP knobs. NVR children share the recorder's mail path and
|
|
3637
|
+
// multifocal lens-children share the parent's connection — neither
|
|
3638
|
+
// can independently target the singleton Email Push Server, so we
|
|
3639
|
+
// keep the group hidden to avoid misleading the user.
|
|
3640
|
+
const hideEmailPush = this.isOnNvr || !!this.multiFocalDevice;
|
|
3641
|
+
this.storageSettings.settings.emailPushCachedStatus.hide = hideEmailPush;
|
|
3642
|
+
this.storageSettings.settings.emailPushTriggers.hide = hideEmailPush;
|
|
3643
|
+
this.storageSettings.settings.emailPushAttachment.hide = hideEmailPush;
|
|
3644
|
+
this.storageSettings.settings.emailPushAutoConfigure.hide = hideEmailPush;
|
|
3645
|
+
this.storageSettings.settings.emailPushTest.hide = hideEmailPush;
|
|
3646
|
+
this.storageSettings.settings.emailPushRefreshStatus.hide = hideEmailPush;
|
|
3447
3647
|
// Show UID and discovery method for UDP cameras (battery or UDP-only like Elite Floodlight WiFi)
|
|
3448
3648
|
// Hide for NVR children or multifocal lenses (they use parent's connection)
|
|
3449
3649
|
// const requiresUidSettings =
|