@camstack/addon-provider-reolink 1.2.218 → 1.2.219
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/CHANGELOG.md +6 -0
- package/dist/addon.js +1140 -802
- package/dist/addon.mjs +1149 -812
- package/dist/{esm-DsdWoSZo.mjs → esm-CO4o0BWI.mjs} +3 -3
- package/i18n/en.json +0 -11
- package/i18n/it.json +0 -11
- package/package.json +6 -3
package/dist/addon.mjs
CHANGED
|
@@ -2,9 +2,8 @@ import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM, r as __
|
|
|
2
2
|
import { A as normalizeDebugOptions, C as eventTraceLog, D as getH265NalType, E as extractVpsFromAnnexB, F as traceLog, M as splitAnnexBToNalPayloads, N as splitAnnexBToNalPayloads2, O as isH265Irap, P as talkTraceLog, S as deriveAesKey, T as extractSpsFromAnnexB, _ as bcHeaderHasPayloadOffset, a as BC_MAGIC, b as convertToAnnexB2, c as BaichuanVideoStream, f as __require$1, g as bcEncrypt, h as bcDecrypt, i as BC_CLASS_MODERN_24, j as recordingsTraceLog, k as md5StrModern, l as BcMediaAnnexBDecoder, m as aesEncrypt, n as BC_CLASS_LEGACY, o as BC_MAGIC_REV, p as aesDecrypt, r as BC_CLASS_MODERN_20, t as BC_CLASS_FILE_DOWNLOAD, v as bcHeaderIsKnown20, w as extractPpsFromAnnexB, x as debugLog, y as convertToAnnexB } from "./chunk-LXENEW4N-QHQAg67o.mjs";
|
|
3
3
|
import { EventEmitter } from "events";
|
|
4
4
|
import * as fs2 from "fs";
|
|
5
|
-
import fs from "fs";
|
|
6
5
|
import * as path2 from "path";
|
|
7
|
-
import
|
|
6
|
+
import { dirname, join } from "path";
|
|
8
7
|
import * as crypto$7 from "crypto";
|
|
9
8
|
import crypto$1, { createHash, randomUUID } from "crypto";
|
|
10
9
|
import { execFile, spawn } from "child_process";
|
|
@@ -14,7 +13,7 @@ import { createHash as createHash$1, randomBytes } from "node:crypto";
|
|
|
14
13
|
import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
15
14
|
import { join as join$1 } from "node:path";
|
|
16
15
|
import { PassThrough } from "stream";
|
|
17
|
-
import {
|
|
16
|
+
import { promisify } from "util";
|
|
18
17
|
import * as dgram2 from "dgram";
|
|
19
18
|
import dgram from "dgram";
|
|
20
19
|
import dns from "dns/promises";
|
|
@@ -22101,7 +22100,7 @@ var require_dist$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
22101
22100
|
//#endregion
|
|
22102
22101
|
//#region ../../node_modules/yazl/index.js
|
|
22103
22102
|
var require_yazl = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
22104
|
-
var fs$
|
|
22103
|
+
var fs$1 = __require("fs");
|
|
22105
22104
|
var Transform$10 = __require("stream").Transform;
|
|
22106
22105
|
var PassThrough$3 = __require("stream").PassThrough;
|
|
22107
22106
|
var zlib$1 = __require("zlib");
|
|
@@ -22130,14 +22129,14 @@ var require_yazl = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
22130
22129
|
if (shouldIgnoreAdding(self)) return;
|
|
22131
22130
|
var entry = new Entry(metadataPath, false, options);
|
|
22132
22131
|
self.entries.push(entry);
|
|
22133
|
-
fs$
|
|
22132
|
+
fs$1.stat(realPath, function(err, stats) {
|
|
22134
22133
|
if (err) return self.emit("error", err);
|
|
22135
22134
|
if (!stats.isFile()) return self.emit("error", /* @__PURE__ */ new Error("not a file: " + realPath));
|
|
22136
22135
|
entry.uncompressedSize = stats.size;
|
|
22137
22136
|
if (options.mtime == null) entry.setLastModDate(stats.mtime);
|
|
22138
22137
|
if (options.mode == null) entry.setFileAttributesMode(stats.mode);
|
|
22139
22138
|
entry.setFileDataPumpFunction(function() {
|
|
22140
|
-
var readStream = fs$
|
|
22139
|
+
var readStream = fs$1.createReadStream(realPath);
|
|
22141
22140
|
entry.state = Entry.FILE_DATA_IN_PROGRESS;
|
|
22142
22141
|
readStream.on("error", function(err) {
|
|
22143
22142
|
self.emit("error", err);
|
|
@@ -29163,7 +29162,7 @@ var require_lz4 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
29163
29162
|
};
|
|
29164
29163
|
}));
|
|
29165
29164
|
//#endregion
|
|
29166
|
-
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-
|
|
29165
|
+
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-DBCXE3EJ.js
|
|
29167
29166
|
var import_fxp = require_fxp();
|
|
29168
29167
|
var import_lz4 = /* @__PURE__ */ __toESM(require_lz4(), 1);
|
|
29169
29168
|
function bcHeaderLen(messageClass, buf, bodyLen) {
|
|
@@ -31240,6 +31239,13 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter {
|
|
|
31240
31239
|
* others wait on the same promise.
|
|
31241
31240
|
*/
|
|
31242
31241
|
tcpConnectPromise;
|
|
31242
|
+
/**
|
|
31243
|
+
* Bound for a single TCP handshake. Without this (and without rejecting on
|
|
31244
|
+
* `close`), a socket destroyed mid-connect left `tcpConnectPromise` pending
|
|
31245
|
+
* forever — every later `sendXml`/`connect` that joined the same promise
|
|
31246
|
+
* hung past its request timeout (which only started AFTER connect resolved).
|
|
31247
|
+
*/
|
|
31248
|
+
static TCP_CONNECT_TIMEOUT_MS = 1e4;
|
|
31243
31249
|
msgNum = 0;
|
|
31244
31250
|
loggedIn = false;
|
|
31245
31251
|
subscribed = false;
|
|
@@ -31835,6 +31841,26 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter {
|
|
|
31835
31841
|
}
|
|
31836
31842
|
}
|
|
31837
31843
|
}
|
|
31844
|
+
/**
|
|
31845
|
+
* Connect, but fail if the handshake does not settle within `timeoutMs`.
|
|
31846
|
+
* Returns remaining ms so callers can apply the rest of their budget to the
|
|
31847
|
+
* request itself (request timers used to start only AFTER connect resolved,
|
|
31848
|
+
* so a wedged `tcpConnectPromise` made every send hang forever).
|
|
31849
|
+
*/
|
|
31850
|
+
async connectWithDeadline(timeoutMs, label) {
|
|
31851
|
+
const started = Date.now();
|
|
31852
|
+
let timer;
|
|
31853
|
+
try {
|
|
31854
|
+
await Promise.race([this.connect(), new Promise((_, reject) => {
|
|
31855
|
+
timer = setTimeout(() => {
|
|
31856
|
+
reject(/* @__PURE__ */ new Error(`Baichuan connect timeout after ${timeoutMs}ms (${label})`));
|
|
31857
|
+
}, Math.max(1, timeoutMs));
|
|
31858
|
+
})]);
|
|
31859
|
+
} finally {
|
|
31860
|
+
if (timer) clearTimeout(timer);
|
|
31861
|
+
}
|
|
31862
|
+
return Math.max(1, timeoutMs - (Date.now() - started));
|
|
31863
|
+
}
|
|
31838
31864
|
async connect() {
|
|
31839
31865
|
const desired = this.opts.transport ?? "tcp";
|
|
31840
31866
|
if (desired === "tcp") {
|
|
@@ -31968,8 +31994,30 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter {
|
|
|
31968
31994
|
this.emit("error", err);
|
|
31969
31995
|
});
|
|
31970
31996
|
await new Promise((resolve, reject) => {
|
|
31971
|
-
|
|
31972
|
-
|
|
31997
|
+
let settled = false;
|
|
31998
|
+
const settle = (fn) => {
|
|
31999
|
+
if (settled) return;
|
|
32000
|
+
settled = true;
|
|
32001
|
+
clearTimeout(timer);
|
|
32002
|
+
sock.off("connect", onConnect);
|
|
32003
|
+
sock.off("error", onError);
|
|
32004
|
+
sock.off("close", onClose);
|
|
32005
|
+
fn();
|
|
32006
|
+
};
|
|
32007
|
+
const onConnect = () => settle(() => resolve());
|
|
32008
|
+
const onError = (e) => settle(() => reject(e));
|
|
32009
|
+
const onClose = () => settle(() => reject(/* @__PURE__ */ new Error("Baichuan TCP connect aborted: socket closed")));
|
|
32010
|
+
const timer = setTimeout(() => {
|
|
32011
|
+
settle(() => {
|
|
32012
|
+
try {
|
|
32013
|
+
if (!sock.destroyed) sock.destroy();
|
|
32014
|
+
} catch {}
|
|
32015
|
+
reject(/* @__PURE__ */ new Error(`TCP connection timeout after ${_BaichuanClient.TCP_CONNECT_TIMEOUT_MS}ms`));
|
|
32016
|
+
});
|
|
32017
|
+
}, _BaichuanClient.TCP_CONNECT_TIMEOUT_MS);
|
|
32018
|
+
sock.once("connect", onConnect);
|
|
32019
|
+
sock.once("error", onError);
|
|
32020
|
+
sock.once("close", onClose);
|
|
31973
32021
|
});
|
|
31974
32022
|
const sid = this.socketSessionId;
|
|
31975
32023
|
const localAddress = sock.localAddress;
|
|
@@ -32880,7 +32928,8 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter {
|
|
|
32880
32928
|
async sendXml(params) {
|
|
32881
32929
|
const internal = params.internal === true;
|
|
32882
32930
|
if (!internal) this.touchUserActivity(`sendXml cmdId=${params.cmdId}`);
|
|
32883
|
-
|
|
32931
|
+
const timeoutMs = params.timeoutMs ?? 1e4;
|
|
32932
|
+
const remainingMs = await this.connectWithDeadline(timeoutMs, `sendXml cmdId=${params.cmdId}`);
|
|
32884
32933
|
if (!internal) this.kickIdleDisconnectTimer();
|
|
32885
32934
|
const channel = params.channel ?? this.opts.channel ?? 0;
|
|
32886
32935
|
const channelId = params.channelIdOverride ?? (params.channel == null ? this.hostChannelId : channel + 1);
|
|
@@ -32905,12 +32954,11 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter {
|
|
|
32905
32954
|
const enc = params.encryption ?? this.enc;
|
|
32906
32955
|
const bodyBytes = this.encodeBodyXml(extXml, payloadXml, channelId, enc);
|
|
32907
32956
|
const wire = Buffer.concat([header, bodyBytes]);
|
|
32908
|
-
const timeoutMs = params.timeoutMs ?? 1e4;
|
|
32909
32957
|
const framePromise = new Promise((resolve, reject) => {
|
|
32910
32958
|
const t = setTimeout(() => {
|
|
32911
32959
|
this.pending.delete(pendingKey);
|
|
32912
32960
|
reject(/* @__PURE__ */ new Error(`Baichuan timeout cmdId=${cmdId} msgNum=${msgNum}`));
|
|
32913
|
-
},
|
|
32961
|
+
}, remainingMs);
|
|
32914
32962
|
this.pending.set(pendingKey, {
|
|
32915
32963
|
resolve: (f) => {
|
|
32916
32964
|
clearTimeout(t);
|
|
@@ -32967,7 +33015,8 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter {
|
|
|
32967
33015
|
async sendFrame(params) {
|
|
32968
33016
|
const internal = params.internal === true;
|
|
32969
33017
|
if (!internal) this.touchUserActivity(`sendFrame cmdId=${params.cmdId}`);
|
|
32970
|
-
|
|
33018
|
+
const timeoutMs = params.timeoutMs ?? 1e4;
|
|
33019
|
+
const remainingMs = await this.connectWithDeadline(timeoutMs, `sendFrame cmdId=${params.cmdId}`);
|
|
32971
33020
|
if (!internal) this.kickIdleDisconnectTimer();
|
|
32972
33021
|
const channel = params.channel ?? this.opts.channel ?? 0;
|
|
32973
33022
|
const channelId = params.channelIdOverride ?? (params.channel == null ? this.hostChannelId : channel + 1);
|
|
@@ -32992,12 +33041,11 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter {
|
|
|
32992
33041
|
const enc = params.encryption ?? this.enc;
|
|
32993
33042
|
const bodyBytes = this.encodeBodyXml(extXml, payloadXml, channelId, enc);
|
|
32994
33043
|
const wire = Buffer.concat([header, bodyBytes]);
|
|
32995
|
-
const timeoutMs = params.timeoutMs ?? 1e4;
|
|
32996
33044
|
const framePromise = new Promise((resolve, reject) => {
|
|
32997
33045
|
const t = setTimeout(() => {
|
|
32998
33046
|
this.pending.delete(pendingKey);
|
|
32999
33047
|
reject(/* @__PURE__ */ new Error(`Baichuan timeout cmdId=${cmdId} msgNum=${msgNum}`));
|
|
33000
|
-
},
|
|
33048
|
+
}, remainingMs);
|
|
33001
33049
|
this.pending.set(pendingKey, {
|
|
33002
33050
|
resolve: (f) => {
|
|
33003
33051
|
clearTimeout(t);
|
|
@@ -34191,7 +34239,7 @@ var ALWAYS_ON_DEFAULTS = {
|
|
|
34191
34239
|
};
|
|
34192
34240
|
var jimpPromise;
|
|
34193
34241
|
function loadJimp() {
|
|
34194
|
-
jimpPromise ??= Promise.all([import("./esm-
|
|
34242
|
+
jimpPromise ??= Promise.all([import("./esm-CO4o0BWI.mjs"), import("./fonts-CJf_QFtH.mjs")]).then(([jimp, fonts]) => ({
|
|
34195
34243
|
jimp,
|
|
34196
34244
|
fonts
|
|
34197
34245
|
}));
|
|
@@ -37607,10 +37655,18 @@ var mapToSimpleEvent = (event) => {
|
|
|
37607
37655
|
}
|
|
37608
37656
|
return null;
|
|
37609
37657
|
};
|
|
37610
|
-
var shouldAcceptSleepPushSource = (source, preference,
|
|
37658
|
+
var shouldAcceptSleepPushSource = (source, preference, options) => {
|
|
37611
37659
|
if (source !== "email" && source !== "baichuanWebhook") return true;
|
|
37660
|
+
const opts = typeof options === "boolean" ? {
|
|
37661
|
+
webhookSupported: options,
|
|
37662
|
+
webhookDeliverySeen: options
|
|
37663
|
+
} : options;
|
|
37612
37664
|
const pref = preference === "webhook" || preference === "email" ? preference : "auto";
|
|
37613
|
-
|
|
37665
|
+
if (pref === "email") return source === "email";
|
|
37666
|
+
if (pref === "webhook") return source === "baichuanWebhook";
|
|
37667
|
+
if (!opts.webhookSupported) return source === "email";
|
|
37668
|
+
if (opts.webhookDeliverySeen) return source === "baichuanWebhook";
|
|
37669
|
+
return true;
|
|
37614
37670
|
};
|
|
37615
37671
|
var DEFAULT_SHELTER_CANVAS = {
|
|
37616
37672
|
width: 540,
|
|
@@ -38958,19 +39014,10 @@ function parsePtzGuardXml(xml) {
|
|
|
38958
39014
|
};
|
|
38959
39015
|
}
|
|
38960
39016
|
var emitter = new EventEmitter();
|
|
38961
|
-
var lastEventByCamera = /* @__PURE__ */ new Map();
|
|
38962
|
-
var MAX_GLOBAL_EVENTS = 300;
|
|
38963
|
-
var globalRecentEvents = [];
|
|
38964
39017
|
function onEmailPushEvent(handler) {
|
|
38965
39018
|
emitter.on("event", handler);
|
|
38966
39019
|
return () => emitter.off("event", handler);
|
|
38967
39020
|
}
|
|
38968
|
-
function emitEmailPushEvent(event) {
|
|
38969
|
-
lastEventByCamera.set(event.cameraId, event);
|
|
38970
|
-
globalRecentEvents.unshift(event);
|
|
38971
|
-
if (globalRecentEvents.length > MAX_GLOBAL_EVENTS) globalRecentEvents.length = MAX_GLOBAL_EVENTS;
|
|
38972
|
-
emitter.emit("event", event);
|
|
38973
|
-
}
|
|
38974
39021
|
function mapEmailPushInferredType(inferred) {
|
|
38975
39022
|
switch (inferred) {
|
|
38976
39023
|
case "motion":
|
|
@@ -38983,6 +39030,52 @@ function mapEmailPushInferredType(inferred) {
|
|
|
38983
39030
|
default: return "motion";
|
|
38984
39031
|
}
|
|
38985
39032
|
}
|
|
39033
|
+
var AI_OBJECT_MAP = {
|
|
39034
|
+
people: "people",
|
|
39035
|
+
person: "people",
|
|
39036
|
+
human: "people",
|
|
39037
|
+
vehicle: "vehicle",
|
|
39038
|
+
car: "vehicle",
|
|
39039
|
+
dog_cat: "animal",
|
|
39040
|
+
dog: "animal",
|
|
39041
|
+
cat: "animal",
|
|
39042
|
+
pet: "animal",
|
|
39043
|
+
face: "face",
|
|
39044
|
+
package: "package"
|
|
39045
|
+
};
|
|
39046
|
+
function xmlTag(xml, tag2) {
|
|
39047
|
+
return (new RegExp(`<${tag2}>([^<]*)</${tag2}>`, "i").exec(xml)?.[1] ?? "").trim().toLowerCase();
|
|
39048
|
+
}
|
|
39049
|
+
function splitCsv(value) {
|
|
39050
|
+
return value.split(",").map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0);
|
|
39051
|
+
}
|
|
39052
|
+
function mapBaichuanWebhookAlarmXmlToSimpleEvents(xml) {
|
|
39053
|
+
if (!xml || !/<AlarmEvent\b/i.test(xml)) return [];
|
|
39054
|
+
const out = /* @__PURE__ */ new Set();
|
|
39055
|
+
for (const match of xml.matchAll(/<AlarmEvent\b[^>]*>([\s\S]*?)<\/AlarmEvent>/gi)) {
|
|
39056
|
+
const block = match[1] ?? "";
|
|
39057
|
+
const status = splitCsv(xmlTag(block, "status"));
|
|
39058
|
+
const ai = splitCsv(xmlTag(block, "AItype") || xmlTag(block, "aiType") || xmlTag(block, "aitype"));
|
|
39059
|
+
if (status.includes("visitor")) {
|
|
39060
|
+
out.add("doorbell");
|
|
39061
|
+
continue;
|
|
39062
|
+
}
|
|
39063
|
+
const objects = [];
|
|
39064
|
+
for (const token of ai) {
|
|
39065
|
+
if (token === "none" || token === "other") continue;
|
|
39066
|
+
const mapped = AI_OBJECT_MAP[token];
|
|
39067
|
+
if (mapped) objects.push(mapped);
|
|
39068
|
+
}
|
|
39069
|
+
if (objects.length > 0) {
|
|
39070
|
+
for (const t of objects) out.add(t);
|
|
39071
|
+
continue;
|
|
39072
|
+
}
|
|
39073
|
+
const statusIndicatesMotion = status.some((s) => s !== "none" && s !== "visitor" && s !== "dn" && s !== "daynight");
|
|
39074
|
+
const aiIndicatesMotion = ai.some((a) => a !== "none");
|
|
39075
|
+
if (statusIndicatesMotion || aiIndicatesMotion) out.add("motion");
|
|
39076
|
+
}
|
|
39077
|
+
return [...out];
|
|
39078
|
+
}
|
|
38986
39079
|
var emitter2 = new EventEmitter();
|
|
38987
39080
|
var lastEventByCamera2 = /* @__PURE__ */ new Map();
|
|
38988
39081
|
var MAX_GLOBAL_EVENTS2 = 300;
|
|
@@ -39007,6 +39100,11 @@ function mapBaichuanWebhookToSimpleEvents(event) {
|
|
|
39007
39100
|
if (reason === "pir") return ["motion", "awake"];
|
|
39008
39101
|
return ["awake"];
|
|
39009
39102
|
}
|
|
39103
|
+
function mapBaichuanWebhookParsedToSimpleEvents(parsed) {
|
|
39104
|
+
if (parsed.kind === "event") return mapBaichuanWebhookToSimpleEvents(parsed);
|
|
39105
|
+
if (parsed.cmdId === 33) return mapBaichuanWebhookAlarmXmlToSimpleEvents(parsed.xml);
|
|
39106
|
+
return [];
|
|
39107
|
+
}
|
|
39010
39108
|
var parser = new import_fxp.XMLParser({
|
|
39011
39109
|
ignoreAttributes: false,
|
|
39012
39110
|
attributeNamePrefix: "@_",
|
|
@@ -40905,7 +41003,7 @@ var parseSirenStatusListPushXml = (xml) => {
|
|
|
40905
41003
|
};
|
|
40906
41004
|
var jimpPromise2;
|
|
40907
41005
|
function loadJimp2() {
|
|
40908
|
-
jimpPromise2 ??= import("./esm-
|
|
41006
|
+
jimpPromise2 ??= import("./esm-CO4o0BWI.mjs");
|
|
40909
41007
|
return jimpPromise2;
|
|
40910
41008
|
}
|
|
40911
41009
|
var DUAL_LENS_DUAL_MOTION_MODELS = /* @__PURE__ */ new Set([
|
|
@@ -42584,13 +42682,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
42584
42682
|
const matches = "match" in params ? params.match : (event) => event.cameraId === params.cameraId;
|
|
42585
42683
|
return onBaichuanWebhookEvent((event) => {
|
|
42586
42684
|
if (!matches(event)) return;
|
|
42587
|
-
const types =
|
|
42685
|
+
const types = mapBaichuanWebhookParsedToSimpleEvents(event.raw);
|
|
42588
42686
|
for (const type of types) this.dispatchSimpleEvent({
|
|
42589
42687
|
type,
|
|
42590
42688
|
channel,
|
|
42591
42689
|
timestamp: event.receivedAtMs,
|
|
42592
42690
|
source: "baichuanWebhook"
|
|
42593
42691
|
});
|
|
42692
|
+
if (types.some((t) => t !== "motion" && t !== "doorbell" && t !== "awake" && t !== "sleeping" && t !== "other") && !types.includes("motion")) this.dispatchSimpleEvent({
|
|
42693
|
+
type: "motion",
|
|
42694
|
+
channel,
|
|
42695
|
+
timestamp: event.receivedAtMs,
|
|
42696
|
+
source: "baichuanWebhook"
|
|
42697
|
+
});
|
|
42594
42698
|
if (onEvent) try {
|
|
42595
42699
|
onEvent(event);
|
|
42596
42700
|
} catch (err) {
|
|
@@ -44669,7 +44773,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
44669
44773
|
const uid = await this.ensureUidForRecordings(channel, void 0);
|
|
44670
44774
|
const payloadXml = buildFileInfoListReplayByNameXml({
|
|
44671
44775
|
channel,
|
|
44672
|
-
xmlChannelId:
|
|
44776
|
+
xmlChannelId: channel,
|
|
44673
44777
|
name: params.fileName,
|
|
44674
44778
|
uid,
|
|
44675
44779
|
streamType
|
|
@@ -44725,6 +44829,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
44725
44829
|
});
|
|
44726
44830
|
await dedicatedClient.sendXml({
|
|
44727
44831
|
cmdId: 7,
|
|
44832
|
+
extensionXml: "",
|
|
44728
44833
|
channel,
|
|
44729
44834
|
payloadXml: stopXml,
|
|
44730
44835
|
messageClass: BC_CLASS_MODERN_24,
|
|
@@ -44777,7 +44882,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
44777
44882
|
const headerChannelIdOverride = this.resolveHeaderChannelIdForLogicalChannel(channel);
|
|
44778
44883
|
const payloadXml = buildFileInfoListReplayByIdXml({
|
|
44779
44884
|
channel,
|
|
44780
|
-
xmlChannelId:
|
|
44885
|
+
xmlChannelId: channel,
|
|
44781
44886
|
id: params.fileName,
|
|
44782
44887
|
...uid ? { uid } : {},
|
|
44783
44888
|
streamType
|
|
@@ -44834,6 +44939,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
44834
44939
|
});
|
|
44835
44940
|
await dedicatedClient.sendXml({
|
|
44836
44941
|
cmdId: 7,
|
|
44942
|
+
extensionXml: "",
|
|
44837
44943
|
channel,
|
|
44838
44944
|
payloadXml: stopXml,
|
|
44839
44945
|
messageClass: BC_CLASS_MODERN_24,
|
|
@@ -45764,13 +45870,13 @@ ${stderr}`));
|
|
|
45764
45870
|
if (isNvr) uid = await this.ensureUidForRecordings(channel, params.uid);
|
|
45765
45871
|
const payloadXml = ident.includes("/") ? buildFileInfoListReplayByIdXml({
|
|
45766
45872
|
channel,
|
|
45767
|
-
xmlChannelId:
|
|
45873
|
+
xmlChannelId: channel,
|
|
45768
45874
|
id: ident,
|
|
45769
45875
|
...uid ? { uid } : {},
|
|
45770
45876
|
streamType
|
|
45771
45877
|
}) : buildFileInfoListReplayByNameXml({
|
|
45772
45878
|
channel,
|
|
45773
|
-
xmlChannelId:
|
|
45879
|
+
xmlChannelId: channel,
|
|
45774
45880
|
name: ident,
|
|
45775
45881
|
...uid ? { uid } : {},
|
|
45776
45882
|
streamType
|
|
@@ -45937,6 +46043,7 @@ ${stderr}`));
|
|
|
45937
46043
|
try {
|
|
45938
46044
|
await this.client.sendXml({
|
|
45939
46045
|
cmdId: 7,
|
|
46046
|
+
extensionXml: "",
|
|
45940
46047
|
channel: params.channel,
|
|
45941
46048
|
payloadXml: buildFileInfoListStopXml({
|
|
45942
46049
|
channel: params.channel,
|
|
@@ -56991,7 +57098,7 @@ var require_fetch = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
56991
57098
|
var require_shared = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
56992
57099
|
var urllib = require_url();
|
|
56993
57100
|
var util = __require("util");
|
|
56994
|
-
var fs
|
|
57101
|
+
var fs = __require("fs");
|
|
56995
57102
|
var nmfetch = require_fetch();
|
|
56996
57103
|
var errors = require_errors();
|
|
56997
57104
|
var dns$1 = __require("dns");
|
|
@@ -57361,7 +57468,7 @@ var require_shared = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
57361
57468
|
err.code = errors.EFILEACCESS;
|
|
57362
57469
|
callback(err);
|
|
57363
57470
|
});
|
|
57364
|
-
return resolveStream(fs
|
|
57471
|
+
return resolveStream(fs.createReadStream(content.path), callback);
|
|
57365
57472
|
}
|
|
57366
57473
|
}
|
|
57367
57474
|
if (typeof data[key].content === "string" && ![
|
|
@@ -98114,10 +98221,8 @@ var require_mailparser = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
98114
98221
|
simpleParser: require_simple_parser()
|
|
98115
98222
|
};
|
|
98116
98223
|
}));
|
|
98117
|
-
|
|
98118
|
-
|
|
98119
|
-
var import_smtp_server = require_smtp_server();
|
|
98120
|
-
var import_mailparser = require_mailparser();
|
|
98224
|
+
require_smtp_server();
|
|
98225
|
+
require_mailparser();
|
|
98121
98226
|
function buildRfc4571Sdp(video, audio) {
|
|
98122
98227
|
let out = "v=0\r\n";
|
|
98123
98228
|
out += "o=- 0 0 IN IP4 0.0.0.0\r\n";
|
|
@@ -100525,271 +100630,18 @@ function resolveAlawmulaw() {
|
|
|
100525
100630
|
throw new Error("alawmulaw: unable to resolve mulaw/alaw codecs from module exports");
|
|
100526
100631
|
}
|
|
100527
100632
|
var { mulaw, alaw } = resolveAlawmulaw();
|
|
100528
|
-
|
|
100529
|
-
const
|
|
100530
|
-
const keyPath = path.join(params.dir, "key.pem");
|
|
100531
|
-
const warn = params.warn ?? ((m) => console.warn(`[email-push-tls] ${m}`));
|
|
100532
|
-
if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) {
|
|
100533
|
-
warn(`Email-push TLS requested but ${certPath} or ${keyPath} is missing. Place a PEM cert+key under that directory to enable STARTTLS, or disable TLS in the consumer config to silence this warning.`);
|
|
100534
|
-
return;
|
|
100535
|
-
}
|
|
100536
|
-
return {
|
|
100537
|
-
cert: fs.readFileSync(certPath),
|
|
100538
|
-
key: fs.readFileSync(keyPath)
|
|
100539
|
-
};
|
|
100540
|
-
}
|
|
100541
|
-
async function defaultLoadTls(dir, warn) {
|
|
100542
|
-
return loadEmailPushTls({
|
|
100543
|
-
dir,
|
|
100544
|
-
warn
|
|
100545
|
-
});
|
|
100546
|
-
}
|
|
100547
|
-
function classifyMessage(parsed) {
|
|
100548
|
-
const haystack = `${parsed.subject ?? ""} ${parsed.text ?? ""}`.toLowerCase();
|
|
100633
|
+
function classifyEmailPushMessage(input) {
|
|
100634
|
+
const haystack = `${(input.subject ?? "").toLowerCase()} ${(input.text ?? "").toLowerCase()}`;
|
|
100549
100635
|
if (/person|people|human/.test(haystack)) return "people";
|
|
100550
100636
|
if (/vehicle|car|truck/.test(haystack)) return "vehicle";
|
|
100551
100637
|
if (/dog[_\s-]?cat|pet|animal/.test(haystack)) return "animal";
|
|
100552
100638
|
if (/face/.test(haystack)) return "face";
|
|
100553
100639
|
if (/package|parcel/.test(haystack)) return "package";
|
|
100554
|
-
if (/
|
|
100640
|
+
if (/ring(?:ing)?\s+button|doorbell\s+(?:press|ring|button)|visitor\s+at|someone\s+rang/.test(haystack)) return "doorbell";
|
|
100555
100641
|
if (/motion|alarm|alert|detect/.test(haystack)) return "motion";
|
|
100642
|
+
if (/\bdoorbell\b/.test(haystack) && !/\bfrom\s+[^\n]*doorbell/.test(haystack)) return "doorbell";
|
|
100556
100643
|
return "other";
|
|
100557
100644
|
}
|
|
100558
|
-
function createEmailPushServer(params) {
|
|
100559
|
-
const log = {
|
|
100560
|
-
debug: params.logger?.debug ?? (() => {}),
|
|
100561
|
-
info: params.logger?.info ?? (() => {}),
|
|
100562
|
-
warn: params.logger?.warn ?? (() => {}),
|
|
100563
|
-
error: params.logger?.error ?? (() => {})
|
|
100564
|
-
};
|
|
100565
|
-
let config = params.config;
|
|
100566
|
-
let server;
|
|
100567
|
-
let status = buildInitialStatus(config);
|
|
100568
|
-
function parseRecipient(rcpt) {
|
|
100569
|
-
const at = rcpt.lastIndexOf("@");
|
|
100570
|
-
if (at <= 0 || at === rcpt.length - 1) return void 0;
|
|
100571
|
-
return {
|
|
100572
|
-
local: rcpt.slice(0, at),
|
|
100573
|
-
domain: rcpt.slice(at + 1).toLowerCase()
|
|
100574
|
-
};
|
|
100575
|
-
}
|
|
100576
|
-
function resolveCameraIdFromRecipient(rcpt) {
|
|
100577
|
-
const parsed = parseRecipient(rcpt);
|
|
100578
|
-
if (!parsed) return void 0;
|
|
100579
|
-
if (parsed.domain !== config.domain.toLowerCase()) return void 0;
|
|
100580
|
-
const match = parsed.local.match(/^cam-(.+)$/);
|
|
100581
|
-
if (!match || !match[1]) return void 0;
|
|
100582
|
-
return params.cameraResolver(match[1]);
|
|
100583
|
-
}
|
|
100584
|
-
async function handleIncomingMessage(cameraId, recipient, raw) {
|
|
100585
|
-
let parsed;
|
|
100586
|
-
try {
|
|
100587
|
-
parsed = await (0, import_mailparser.simpleParser)(raw);
|
|
100588
|
-
} catch (err) {
|
|
100589
|
-
log.warn(`Failed to parse mail for ${cameraId}: ${err instanceof Error ? err.message : err}`);
|
|
100590
|
-
status.messagesRejected++;
|
|
100591
|
-
return;
|
|
100592
|
-
}
|
|
100593
|
-
const receivedAtMs = Date.now();
|
|
100594
|
-
const event = {
|
|
100595
|
-
cameraId,
|
|
100596
|
-
recipient,
|
|
100597
|
-
inferredType: classifyMessage(parsed),
|
|
100598
|
-
receivedAtMs,
|
|
100599
|
-
subject: parsed.subject ?? "",
|
|
100600
|
-
from: typeof parsed.from === "object" && parsed.from !== null && "text" in parsed.from ? String(parsed.from.text) : "",
|
|
100601
|
-
bodyExcerpt: (parsed.text ?? "").slice(0, 500)
|
|
100602
|
-
};
|
|
100603
|
-
status.messagesAccepted++;
|
|
100604
|
-
log.info(`Email push for camera=${cameraId} type=${event.inferredType} subject="${event.subject.slice(0, 80)}"`);
|
|
100605
|
-
emitEmailPushEvent(event);
|
|
100606
|
-
}
|
|
100607
|
-
async function start() {
|
|
100608
|
-
if (server) {
|
|
100609
|
-
log.debug("startEmailPushServer called but server already running");
|
|
100610
|
-
return;
|
|
100611
|
-
}
|
|
100612
|
-
const tlsOptions = config.tls ? await (params.loadTls ?? defaultLoadTls)(config.tlsDir ?? "./email-push-tls", (m) => log.warn(m)) : void 0;
|
|
100613
|
-
status = buildInitialStatus(config);
|
|
100614
|
-
const next = new import_smtp_server.SMTPServer({
|
|
100615
|
-
authOptional: !config.requireAuth,
|
|
100616
|
-
disabledCommands: config.requireAuth ? [] : ["AUTH"],
|
|
100617
|
-
allowInsecureAuth: !config.tls,
|
|
100618
|
-
size: config.maxMessageBytes,
|
|
100619
|
-
maxClients: 50,
|
|
100620
|
-
socketTimeout: 6e4,
|
|
100621
|
-
closeTimeout: 3e4,
|
|
100622
|
-
logger: {
|
|
100623
|
-
info: (...args) => log.info(formatSmtpLogArgs(args)),
|
|
100624
|
-
debug: (...args) => log.debug(formatSmtpLogArgs(args)),
|
|
100625
|
-
error: (...args) => log.warn(formatSmtpLogArgs(args))
|
|
100626
|
-
},
|
|
100627
|
-
...tlsOptions ? {
|
|
100628
|
-
secure: false,
|
|
100629
|
-
...tlsOptions
|
|
100630
|
-
} : {},
|
|
100631
|
-
onConnect(session, callback) {
|
|
100632
|
-
log.info(`SMTP connect from ${session.remoteAddress} (sessionId=${session.id})`);
|
|
100633
|
-
callback();
|
|
100634
|
-
},
|
|
100635
|
-
onClose(session) {
|
|
100636
|
-
log.debug(`SMTP close ${session.remoteAddress} (sessionId=${session.id})`);
|
|
100637
|
-
},
|
|
100638
|
-
onMailFrom(address, session, callback) {
|
|
100639
|
-
log.info(`SMTP MAIL FROM ${address.address} (sessionId=${session.id})`);
|
|
100640
|
-
callback();
|
|
100641
|
-
},
|
|
100642
|
-
onAuth(auth, session, callback) {
|
|
100643
|
-
const expectedUser = config.authUsername;
|
|
100644
|
-
const expectedPass = config.authPassword;
|
|
100645
|
-
if (!expectedUser || !expectedPass) {
|
|
100646
|
-
log.warn(`SMTP AUTH rejected from ${session.remoteAddress} (sessionId=${session.id}): server has no credentials configured`);
|
|
100647
|
-
return callback(/* @__PURE__ */ new Error("Email push auth not configured"));
|
|
100648
|
-
}
|
|
100649
|
-
const stripDomain = (u) => {
|
|
100650
|
-
const at = u.lastIndexOf("@");
|
|
100651
|
-
if (at < 0) return u;
|
|
100652
|
-
const local = u.slice(0, at);
|
|
100653
|
-
return u.slice(at + 1).toLowerCase() === config.domain.toLowerCase() ? local : u;
|
|
100654
|
-
};
|
|
100655
|
-
if (stripDomain(auth.username ?? "") === stripDomain(expectedUser) && auth.password === expectedPass) {
|
|
100656
|
-
log.info(`SMTP AUTH ok method=${auth.method} user=${auth.username} (sessionId=${session.id})`);
|
|
100657
|
-
return callback(null, { user: auth.username });
|
|
100658
|
-
}
|
|
100659
|
-
log.warn(`SMTP AUTH FAIL method=${auth.method} from=${session.remoteAddress} triedUser="${auth.username}" expectedUser="${expectedUser}" triedPasswordLen=${auth.password?.length ?? 0} (sessionId=${session.id})`);
|
|
100660
|
-
return callback(/* @__PURE__ */ new Error("Invalid username or password"));
|
|
100661
|
-
},
|
|
100662
|
-
onRcptTo(address, session, callback) {
|
|
100663
|
-
const cameraId = resolveCameraIdFromRecipient(address.address);
|
|
100664
|
-
if (!cameraId) {
|
|
100665
|
-
status.messagesRejected++;
|
|
100666
|
-
log.warn(`SMTP RCPT TO rejected ${address.address} \u2014 unknown recipient (sessionId=${session.id})`);
|
|
100667
|
-
return callback(/* @__PURE__ */ new Error(`Unknown recipient ${address.address} (not registered)`));
|
|
100668
|
-
}
|
|
100669
|
-
log.info(`SMTP RCPT TO ${address.address} \u2192 camera=${cameraId} (sessionId=${session.id})`);
|
|
100670
|
-
callback();
|
|
100671
|
-
},
|
|
100672
|
-
onData(stream, session, callback) {
|
|
100673
|
-
const startedAt = Date.now();
|
|
100674
|
-
log.debug(`SMTP DATA start (sessionId=${session.id} from=${session.envelope.mailFrom ? session.envelope.mailFrom.address : "?"})`);
|
|
100675
|
-
const chunks = [];
|
|
100676
|
-
stream.on("data", (chunk) => chunks.push(chunk));
|
|
100677
|
-
stream.on("end", () => {
|
|
100678
|
-
const recipients = session.envelope.rcptTo?.map((r) => r.address) ?? [];
|
|
100679
|
-
const buffer = Buffer.concat(chunks);
|
|
100680
|
-
log.info(`SMTP DATA received ${buffer.length}B in ${Date.now() - startedAt}ms for ${recipients.length} recipient(s) (sessionId=${session.id})`);
|
|
100681
|
-
const matched = recipients.map((r) => ({
|
|
100682
|
-
recipient: r,
|
|
100683
|
-
cameraId: resolveCameraIdFromRecipient(r)
|
|
100684
|
-
})).filter((m) => Boolean(m.cameraId));
|
|
100685
|
-
if (matched.length === 0) {
|
|
100686
|
-
status.messagesRejected++;
|
|
100687
|
-
log.warn(`SMTP DATA dropped \u2014 no recognised recipients in [${recipients.join(", ")}] (sessionId=${session.id})`);
|
|
100688
|
-
return callback(/* @__PURE__ */ new Error("No recognised recipients"));
|
|
100689
|
-
}
|
|
100690
|
-
Promise.all(matched.map((m) => handleIncomingMessage(m.cameraId, m.recipient, buffer))).then(() => callback()).catch((err) => {
|
|
100691
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
100692
|
-
log.error(`Email push pipeline error (sessionId=${session.id}): ${msg}`);
|
|
100693
|
-
status.lastErrorMessage = msg;
|
|
100694
|
-
callback(new Error(msg));
|
|
100695
|
-
});
|
|
100696
|
-
});
|
|
100697
|
-
stream.on("error", (err) => {
|
|
100698
|
-
log.warn(`SMTP stream error (sessionId=${session.id}): ${err.message}`);
|
|
100699
|
-
callback(err);
|
|
100700
|
-
});
|
|
100701
|
-
}
|
|
100702
|
-
});
|
|
100703
|
-
next.on("error", (err) => {
|
|
100704
|
-
status.lastErrorMessage = err.message;
|
|
100705
|
-
log.error(`Email push server error: ${err.code ?? ""} ${err.message}`);
|
|
100706
|
-
const netServer = next.server;
|
|
100707
|
-
if (server === next && netServer && netServer.listening === false) {
|
|
100708
|
-
status.running = false;
|
|
100709
|
-
setTimeout(() => {
|
|
100710
|
-
if (server !== next) return;
|
|
100711
|
-
try {
|
|
100712
|
-
next.listen(config.port, config.bindHost, () => {
|
|
100713
|
-
status.running = true;
|
|
100714
|
-
log.warn(`Email push SMTP re-listened on ${config.bindHost}:${config.port} after error`);
|
|
100715
|
-
});
|
|
100716
|
-
} catch (e) {
|
|
100717
|
-
log.error(`Email push re-listen failed: ${e.message}`);
|
|
100718
|
-
}
|
|
100719
|
-
}, 5e3).unref?.();
|
|
100720
|
-
}
|
|
100721
|
-
});
|
|
100722
|
-
await new Promise((resolve, reject) => {
|
|
100723
|
-
next.listen(config.port, config.bindHost, () => {
|
|
100724
|
-
status.running = true;
|
|
100725
|
-
status.startedAtMs = Date.now();
|
|
100726
|
-
log.info(`Email push SMTP listening on ${config.bindHost}:${config.port} (domain=${config.domain}, auth=${config.requireAuth}, tls=${config.tls})`);
|
|
100727
|
-
resolve();
|
|
100728
|
-
});
|
|
100729
|
-
next.once("error", reject);
|
|
100730
|
-
});
|
|
100731
|
-
server = next;
|
|
100732
|
-
}
|
|
100733
|
-
async function stop() {
|
|
100734
|
-
if (!server) return;
|
|
100735
|
-
const active = server;
|
|
100736
|
-
server = void 0;
|
|
100737
|
-
await new Promise((resolve) => {
|
|
100738
|
-
active.close(() => resolve());
|
|
100739
|
-
});
|
|
100740
|
-
status.running = false;
|
|
100741
|
-
log.info("Email push SMTP server stopped");
|
|
100742
|
-
}
|
|
100743
|
-
async function restart() {
|
|
100744
|
-
await stop();
|
|
100745
|
-
await start();
|
|
100746
|
-
}
|
|
100747
|
-
return {
|
|
100748
|
-
start,
|
|
100749
|
-
stop,
|
|
100750
|
-
restart,
|
|
100751
|
-
getStatus() {
|
|
100752
|
-
return { ...status };
|
|
100753
|
-
},
|
|
100754
|
-
updateConfig(next) {
|
|
100755
|
-
config = next;
|
|
100756
|
-
}
|
|
100757
|
-
};
|
|
100758
|
-
}
|
|
100759
|
-
function formatSmtpLogArgs(args) {
|
|
100760
|
-
if (args.length === 0) return "[smtp]";
|
|
100761
|
-
const [first, ...rest] = args;
|
|
100762
|
-
let tag = "";
|
|
100763
|
-
let formatArgs = args;
|
|
100764
|
-
if (first && typeof first === "object" && !Array.isArray(first)) {
|
|
100765
|
-
const meta = first;
|
|
100766
|
-
const tnx = typeof meta.tnx === "string" ? meta.tnx : void 0;
|
|
100767
|
-
const cid = typeof meta.cid === "string" ? meta.cid : void 0;
|
|
100768
|
-
const sid = typeof meta.sid === "string" ? meta.sid : void 0;
|
|
100769
|
-
const bits = [];
|
|
100770
|
-
if (tnx) bits.push(tnx);
|
|
100771
|
-
if (cid) bits.push(`cid=${cid}`);
|
|
100772
|
-
if (sid) bits.push(`sid=${sid}`);
|
|
100773
|
-
if (bits.length > 0) tag = ` [${bits.join(" ")}]`;
|
|
100774
|
-
formatArgs = rest;
|
|
100775
|
-
}
|
|
100776
|
-
return `[smtp]${tag} ${format(...formatArgs)}`;
|
|
100777
|
-
}
|
|
100778
|
-
function buildInitialStatus(config) {
|
|
100779
|
-
return {
|
|
100780
|
-
enabled: true,
|
|
100781
|
-
running: false,
|
|
100782
|
-
port: config.port,
|
|
100783
|
-
bindHost: config.bindHost,
|
|
100784
|
-
domain: config.domain,
|
|
100785
|
-
requireAuth: config.requireAuth,
|
|
100786
|
-
tls: config.tls,
|
|
100787
|
-
messagesAccepted: 0,
|
|
100788
|
-
messagesRejected: 0,
|
|
100789
|
-
startedAtMs: void 0,
|
|
100790
|
-
lastErrorMessage: void 0
|
|
100791
|
-
};
|
|
100792
|
-
}
|
|
100793
100645
|
function asRecord(value) {
|
|
100794
100646
|
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
100795
100647
|
}
|
|
@@ -101806,13 +101658,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
101806
101658
|
return EventCategory;
|
|
101807
101659
|
}({});
|
|
101808
101660
|
//#endregion
|
|
101809
|
-
//#region ../types/dist/device-type-
|
|
101661
|
+
//#region ../types/dist/device-type-CfHw_quR.mjs
|
|
101810
101662
|
/**
|
|
101811
101663
|
import { errMsg } from '@camstack/types'
|
|
101812
101664
|
* Extract a human-readable message from an unknown error value.
|
|
101813
101665
|
* Replaces the ubiquitous `errMsg(err)` pattern.
|
|
101814
101666
|
*/
|
|
101815
|
-
function errMsg$
|
|
101667
|
+
function errMsg$3(err) {
|
|
101816
101668
|
if (err instanceof Error) return err.message;
|
|
101817
101669
|
if (typeof err === "string") return err;
|
|
101818
101670
|
return String(err);
|
|
@@ -101995,6 +101847,9 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
|
101995
101847
|
DeviceFeature["CoverPositionable"] = "cover-positionable";
|
|
101996
101848
|
/** Cover exposes slat-tilt control. Gates the tilt slider UI. */
|
|
101997
101849
|
DeviceFeature["CoverTilt"] = "cover-tilt";
|
|
101850
|
+
/** Cover has a real stop: `stop` halts the travel in place. Gates the stop
|
|
101851
|
+
* control in exporters; a provider whose `stop` is a no-op never declares it. */
|
|
101852
|
+
DeviceFeature["CoverStoppable"] = "cover-stoppable";
|
|
101998
101853
|
/** Valve exposes intermediate position control (0..100). Gates the
|
|
101999
101854
|
* position slider / drag surface UI. */
|
|
102000
101855
|
DeviceFeature["ValvePositionable"] = "valve-positionable";
|
|
@@ -106860,7 +106715,7 @@ var ZodIssueCode = {
|
|
|
106860
106715
|
var ZodFirstPartyTypeKind;
|
|
106861
106716
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
106862
106717
|
//#endregion
|
|
106863
|
-
//#region ../types/dist/sleep-
|
|
106718
|
+
//#region ../types/dist/sleep-QsoHs66J.mjs
|
|
106864
106719
|
/**
|
|
106865
106720
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
106866
106721
|
* window to float samples (D455).
|
|
@@ -106977,7 +106832,7 @@ function isKeychainStoredField(field) {
|
|
|
106977
106832
|
var SECRET_CLEAR = { $secretClear: true };
|
|
106978
106833
|
/** The `'***'` token D634 redaction writes for non-admins. */
|
|
106979
106834
|
var LEGACY_DEVICE_REDACTION = "***";
|
|
106980
|
-
function isRecord$
|
|
106835
|
+
function isRecord$7(value) {
|
|
106981
106836
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
106982
106837
|
}
|
|
106983
106838
|
function makeSecretMarker(ref) {
|
|
@@ -106988,16 +106843,16 @@ function makeSecretMarker(ref) {
|
|
|
106988
106843
|
}
|
|
106989
106844
|
/** A stored marker: an object whose `$secret` is a non-empty string. */
|
|
106990
106845
|
function isSecretMarker(value) {
|
|
106991
|
-
if (!isRecord$
|
|
106846
|
+
if (!isRecord$7(value)) return false;
|
|
106992
106847
|
const ref = value["$secret"];
|
|
106993
106848
|
return typeof ref === "string" && ref.length > 0;
|
|
106994
106849
|
}
|
|
106995
106850
|
/** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
|
|
106996
106851
|
function isSecretMarkerLike(value) {
|
|
106997
|
-
return isRecord$
|
|
106852
|
+
return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
|
|
106998
106853
|
}
|
|
106999
106854
|
function isSecretClear(value) {
|
|
107000
|
-
return isRecord$
|
|
106855
|
+
return isRecord$7(value) && value["$secretClear"] === true;
|
|
107001
106856
|
}
|
|
107002
106857
|
/**
|
|
107003
106858
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
@@ -107380,6 +107235,53 @@ function emitReadiness(bus, params) {
|
|
|
107380
107235
|
ts
|
|
107381
107236
|
}));
|
|
107382
107237
|
}
|
|
107238
|
+
/** Why a catalogue entry is not a runnable body of its declared kind. */
|
|
107239
|
+
var AddonMigrationDefinitionError = class extends Error {
|
|
107240
|
+
id;
|
|
107241
|
+
constructor(id, message) {
|
|
107242
|
+
super(`${id}: ${message}`);
|
|
107243
|
+
this.id = id;
|
|
107244
|
+
this.name = "AddonMigrationDefinitionError";
|
|
107245
|
+
}
|
|
107246
|
+
};
|
|
107247
|
+
/**
|
|
107248
|
+
* Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
|
|
107249
|
+
* an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
|
|
107250
|
+
* body). A throwing body never escapes `execute`: it is reported as a failed result.
|
|
107251
|
+
*/
|
|
107252
|
+
function defineAddonMigrations(catalogue, ports) {
|
|
107253
|
+
return catalogue.map((entry) => {
|
|
107254
|
+
const { meta } = entry;
|
|
107255
|
+
if ("plan" in entry) {
|
|
107256
|
+
if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
|
|
107257
|
+
return {
|
|
107258
|
+
meta,
|
|
107259
|
+
execute: () => guarded(async () => ({
|
|
107260
|
+
kind: "plan",
|
|
107261
|
+
plan: await entry.plan(ports)
|
|
107262
|
+
}))
|
|
107263
|
+
};
|
|
107264
|
+
}
|
|
107265
|
+
if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
|
|
107266
|
+
return {
|
|
107267
|
+
meta,
|
|
107268
|
+
execute: () => guarded(async () => ({
|
|
107269
|
+
kind: "procedure",
|
|
107270
|
+
result: await entry.procedure(ports)
|
|
107271
|
+
}))
|
|
107272
|
+
};
|
|
107273
|
+
});
|
|
107274
|
+
}
|
|
107275
|
+
async function guarded(work) {
|
|
107276
|
+
try {
|
|
107277
|
+
return await work();
|
|
107278
|
+
} catch (err) {
|
|
107279
|
+
return {
|
|
107280
|
+
kind: "failed",
|
|
107281
|
+
error: errMsg$3(err)
|
|
107282
|
+
};
|
|
107283
|
+
}
|
|
107284
|
+
}
|
|
107383
107285
|
var NOT_RUN = {
|
|
107384
107286
|
declared: false,
|
|
107385
107287
|
snapshot: null,
|
|
@@ -107407,7 +107309,7 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
107407
107309
|
metas: migrations.map((m) => m.meta)
|
|
107408
107310
|
});
|
|
107409
107311
|
} catch (err) {
|
|
107410
|
-
log.error("the migration engine refused or could not take this addon catalogue — none ran", { meta: { error: errMsg$
|
|
107312
|
+
log.error("the migration engine refused or could not take this addon catalogue — none ran", { meta: { error: errMsg$3(err) } });
|
|
107411
107313
|
return {
|
|
107412
107314
|
...NOT_RUN,
|
|
107413
107315
|
blockOwner: migrations.filter((m) => m.meta.onFailure === "block-owner").map((m) => m.meta.id)
|
|
@@ -107460,7 +107362,7 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
107460
107362
|
} catch (err) {
|
|
107461
107363
|
log.error("migration could not be completed with the engine", {
|
|
107462
107364
|
tags,
|
|
107463
|
-
meta: { error: errMsg$
|
|
107365
|
+
meta: { error: errMsg$3(err) }
|
|
107464
107366
|
});
|
|
107465
107367
|
failed.push(step.id);
|
|
107466
107368
|
}
|
|
@@ -107703,7 +107605,7 @@ var KeychainWriteRefusedError = class extends Error {
|
|
|
107703
107605
|
}
|
|
107704
107606
|
};
|
|
107705
107607
|
var KEEP = { kind: "keep" };
|
|
107706
|
-
function isRecord$
|
|
107608
|
+
function isRecord$6(value) {
|
|
107707
107609
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
107708
107610
|
}
|
|
107709
107611
|
function defaultMintId() {
|
|
@@ -107716,7 +107618,7 @@ function collectSecretMarkerRefs(value, out = []) {
|
|
|
107716
107618
|
return out;
|
|
107717
107619
|
}
|
|
107718
107620
|
if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
|
|
107719
|
-
else if (isRecord$
|
|
107621
|
+
else if (isRecord$6(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
|
|
107720
107622
|
return out;
|
|
107721
107623
|
}
|
|
107722
107624
|
async function storedValueEquals(secrets, marker, candidate) {
|
|
@@ -107771,7 +107673,7 @@ function rowId(row, idKey) {
|
|
|
107771
107673
|
return null;
|
|
107772
107674
|
}
|
|
107773
107675
|
async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
107774
|
-
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$
|
|
107676
|
+
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$6) : [];
|
|
107775
107677
|
if (!Array.isArray(incoming)) {
|
|
107776
107678
|
ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
|
|
107777
107679
|
return {
|
|
@@ -107787,7 +107689,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
|
107787
107689
|
const kept = /* @__PURE__ */ new Set();
|
|
107788
107690
|
const rows = [];
|
|
107789
107691
|
for (const row of incoming) {
|
|
107790
|
-
if (!isRecord$
|
|
107692
|
+
if (!isRecord$6(row)) {
|
|
107791
107693
|
rows.push(row);
|
|
107792
107694
|
continue;
|
|
107793
107695
|
}
|
|
@@ -107812,7 +107714,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
107812
107714
|
case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
|
|
107813
107715
|
case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
|
|
107814
107716
|
case "object": {
|
|
107815
|
-
if (!isRecord$
|
|
107717
|
+
if (!isRecord$6(incoming)) {
|
|
107816
107718
|
if (incoming === void 0) return KEEP;
|
|
107817
107719
|
ctx.deletes.push(...collectSecretMarkerRefs(stored));
|
|
107818
107720
|
return {
|
|
@@ -107820,7 +107722,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
107820
107722
|
value: incoming
|
|
107821
107723
|
};
|
|
107822
107724
|
}
|
|
107823
|
-
const storedObj = isRecord$
|
|
107725
|
+
const storedObj = isRecord$6(stored) ? stored : {};
|
|
107824
107726
|
return {
|
|
107825
107727
|
kind: "value",
|
|
107826
107728
|
value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
|
|
@@ -107943,7 +107845,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
|
|
|
107943
107845
|
value: items
|
|
107944
107846
|
};
|
|
107945
107847
|
}
|
|
107946
|
-
if (isRecord$
|
|
107848
|
+
if (isRecord$6(v)) {
|
|
107947
107849
|
const out = {};
|
|
107948
107850
|
for (const [k, child] of Object.entries(v)) {
|
|
107949
107851
|
const s = substitute(child);
|
|
@@ -107975,7 +107877,7 @@ var SecretOverwriteRefusedError = class extends Error {
|
|
|
107975
107877
|
this.name = "SecretOverwriteRefusedError";
|
|
107976
107878
|
}
|
|
107977
107879
|
};
|
|
107978
|
-
function isRecord$
|
|
107880
|
+
function isRecord$5(value) {
|
|
107979
107881
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
107980
107882
|
}
|
|
107981
107883
|
function resolvedValue(pass, marker) {
|
|
@@ -108004,7 +107906,7 @@ function inArray(pass, incoming, stored) {
|
|
|
108004
107906
|
return incoming.map((item, index) => {
|
|
108005
107907
|
const twin = stored[index];
|
|
108006
107908
|
if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
|
|
108007
|
-
if (isRecord$
|
|
107909
|
+
if (isRecord$5(item) && isRecord$5(twin)) return inArrayRow(pass, item, twin);
|
|
108008
107910
|
if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
|
|
108009
107911
|
return item;
|
|
108010
107912
|
});
|
|
@@ -108015,7 +107917,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
108015
107917
|
const twin = stored[key];
|
|
108016
107918
|
if (isSecretMarker(twin)) {
|
|
108017
107919
|
if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
|
|
108018
|
-
} else if (isRecord$
|
|
107920
|
+
} else if (isRecord$5(value) && isRecord$5(twin)) out[key] = inArrayRow(pass, value, twin);
|
|
108019
107921
|
else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
|
|
108020
107922
|
}
|
|
108021
107923
|
return out;
|
|
@@ -108023,7 +107925,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
108023
107925
|
async function walk(pass, incoming, stored) {
|
|
108024
107926
|
if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
|
|
108025
107927
|
if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
|
|
108026
|
-
if (!isRecord$
|
|
107928
|
+
if (!isRecord$5(incoming) || !isRecord$5(stored)) return incoming;
|
|
108027
107929
|
const out = { ...incoming };
|
|
108028
107930
|
for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
|
|
108029
107931
|
return out;
|
|
@@ -108054,7 +107956,7 @@ async function restoreUnchangedSecrets(input) {
|
|
|
108054
107956
|
function isJsonEqual(a, b) {
|
|
108055
107957
|
if (Object.is(a, b)) return true;
|
|
108056
107958
|
if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
|
|
108057
|
-
if (isRecord$
|
|
107959
|
+
if (isRecord$5(a) && isRecord$5(b)) {
|
|
108058
107960
|
const keys = Object.keys(a);
|
|
108059
107961
|
if (keys.length !== Object.keys(b).length) return false;
|
|
108060
107962
|
return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
|
|
@@ -108078,7 +107980,7 @@ function markerKeysOf(blob, owner) {
|
|
|
108078
107980
|
}
|
|
108079
107981
|
function nodeFromStored(value, owner) {
|
|
108080
107982
|
if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
|
|
108081
|
-
if (!isRecord$
|
|
107983
|
+
if (!isRecord$5(value)) return null;
|
|
108082
107984
|
const children = /* @__PURE__ */ new Map();
|
|
108083
107985
|
for (const [key, child] of Object.entries(value)) {
|
|
108084
107986
|
const node = nodeFromStored(child, owner);
|
|
@@ -108697,6 +108599,44 @@ var BaseAddon = class {
|
|
|
108697
108599
|
return null;
|
|
108698
108600
|
}
|
|
108699
108601
|
/**
|
|
108602
|
+
* D986 scoped settings: the form shape of the CLUSTER-wide settings one capability this addon
|
|
108603
|
+
* provides owns, or `null` when it has none. `cap` is a capability name the addon's manifest
|
|
108604
|
+
* declares. Stored as `addon-settings` rows `<addonId>.<field>`. The keys of this schema, of
|
|
108605
|
+
* {@link nodeSettingsSchema} and of {@link deviceDefaultsSchema} are disjoint, and a key never
|
|
108606
|
+
* contains `@` (guard `check-scoped-settings-declared`). SHAPE only: values are read by scope.
|
|
108607
|
+
*/
|
|
108608
|
+
async clusterSettingsSchema(_cap) {
|
|
108609
|
+
return null;
|
|
108610
|
+
}
|
|
108611
|
+
/**
|
|
108612
|
+
* D986: the form shape of the settings one capability owns PER NODE, or `null`. Stored as
|
|
108613
|
+
* `addon-settings` rows `<addonId>.<field>@<nodeId>` carrying `nodeId` in the row data, so a
|
|
108614
|
+
* forgotten node's rows go in one central delete.
|
|
108615
|
+
*/
|
|
108616
|
+
async nodeSettingsSchema(_cap) {
|
|
108617
|
+
return null;
|
|
108618
|
+
}
|
|
108619
|
+
/**
|
|
108620
|
+
* D986: the form shape of the FLEET DEFAULTS of the per-device settings one capability owns,
|
|
108621
|
+
* or `null`. Stored as `addon-settings` rows `<addonId>.<field>`; a device resolves its value
|
|
108622
|
+
* as device row, then this default, then the schema default (`resolveDeviceSettings`).
|
|
108623
|
+
*/
|
|
108624
|
+
async deviceDefaultsSchema(_cap) {
|
|
108625
|
+
return null;
|
|
108626
|
+
}
|
|
108627
|
+
/**
|
|
108628
|
+
* The host's read of the scoped declarations: the schema one capability declares for `scope`,
|
|
108629
|
+
* or `null`. Beside {@link getIntegrationFormSchema}, the same shape: the hook is protected, the
|
|
108630
|
+
* host asks this method.
|
|
108631
|
+
*/
|
|
108632
|
+
async getScopedSettingsSchema(scope, cap) {
|
|
108633
|
+
switch (scope) {
|
|
108634
|
+
case "cluster": return this.clusterSettingsSchema(cap);
|
|
108635
|
+
case "node": return this.nodeSettingsSchema(cap);
|
|
108636
|
+
case "device-defaults": return this.deviceDefaultsSchema(cap);
|
|
108637
|
+
}
|
|
108638
|
+
}
|
|
108639
|
+
/**
|
|
108700
108640
|
* What the create wizard and the integration edit page render for one instance: the addon's
|
|
108701
108641
|
* per-instance schema when it declares one, else its global schema (what they always showed).
|
|
108702
108642
|
*/
|
|
@@ -109490,6 +109430,21 @@ function normalizeAddonInitResult(result) {
|
|
|
109490
109430
|
if (Array.isArray(result)) return { providers: result };
|
|
109491
109431
|
return result;
|
|
109492
109432
|
}
|
|
109433
|
+
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
109434
|
+
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
109435
|
+
var RECORD_DATABASES = ["settings", "events"];
|
|
109436
|
+
/** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
|
|
109437
|
+
var RECORD_CLASSES = [
|
|
109438
|
+
"config",
|
|
109439
|
+
"ledger",
|
|
109440
|
+
"audit",
|
|
109441
|
+
"mirror",
|
|
109442
|
+
"ephemeral"
|
|
109443
|
+
];
|
|
109444
|
+
var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
109445
|
+
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
109446
|
+
var RECORD_BATCH_MAX = 1e3;
|
|
109447
|
+
var RECORD_QUERY_MAX = 1e3;
|
|
109493
109448
|
/**
|
|
109494
109449
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
109495
109450
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -109514,21 +109469,6 @@ var PeerBytesTicketSchema = object({
|
|
|
109514
109469
|
*/
|
|
109515
109470
|
declaredBytes: number().int().nonnegative().nullable()
|
|
109516
109471
|
});
|
|
109517
|
-
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
109518
|
-
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
109519
|
-
var RECORD_DATABASES = ["settings", "events"];
|
|
109520
|
-
/** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
|
|
109521
|
-
var RECORD_CLASSES = [
|
|
109522
|
-
"config",
|
|
109523
|
-
"ledger",
|
|
109524
|
-
"audit",
|
|
109525
|
-
"mirror",
|
|
109526
|
-
"ephemeral"
|
|
109527
|
-
];
|
|
109528
|
-
var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
109529
|
-
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
109530
|
-
var RECORD_BATCH_MAX = 1e3;
|
|
109531
|
-
var RECORD_QUERY_MAX = 1e3;
|
|
109532
109472
|
function defOf(schema) {
|
|
109533
109473
|
const zodBag = Reflect.get(schema, "_zod");
|
|
109534
109474
|
if (typeof zodBag !== "object" || zodBag === null) return {};
|
|
@@ -110140,6 +110080,81 @@ var ProfileRtspEntrySchema = object({
|
|
|
110140
110080
|
codec: string().optional(),
|
|
110141
110081
|
resolution: CamStreamResolutionSchema.optional()
|
|
110142
110082
|
});
|
|
110083
|
+
/** Every debug channel's name starts with this; the ring denies it by default. */
|
|
110084
|
+
var DEBUG_CHANNEL_PREFIX = "debug.";
|
|
110085
|
+
function isDebugChannelName(name) {
|
|
110086
|
+
return name.startsWith(DEBUG_CHANNEL_PREFIX);
|
|
110087
|
+
}
|
|
110088
|
+
/**
|
|
110089
|
+
* Who is watching what, in THIS process. In-memory, moved off the log path.
|
|
110090
|
+
*
|
|
110091
|
+
* `active` is a plain boolean FIELD, read first: with no watch anywhere, a
|
|
110092
|
+
* `ctx.debug` call is one load and one branch.
|
|
110093
|
+
*/
|
|
110094
|
+
var DebugInterest = class {
|
|
110095
|
+
active = false;
|
|
110096
|
+
windows = /* @__PURE__ */ new Map();
|
|
110097
|
+
/**
|
|
110098
|
+
* Replace the interest with the FULL set of windows. Non-debug windows are
|
|
110099
|
+
* ignored and an absent one is dropped, so a lost disarm cannot leave a
|
|
110100
|
+
* channel running.
|
|
110101
|
+
*/
|
|
110102
|
+
apply(windows, nowMs) {
|
|
110103
|
+
this.windows.clear();
|
|
110104
|
+
for (const window of windows) {
|
|
110105
|
+
if (!isDebugChannelName(window.channel) || window.armedUntilMs <= nowMs) continue;
|
|
110106
|
+
const ids = window.deviceIds;
|
|
110107
|
+
this.windows.set(window.channel, {
|
|
110108
|
+
untilMs: window.armedUntilMs,
|
|
110109
|
+
devices: ids === null || ids.length === 0 ? null : new Set(ids)
|
|
110110
|
+
});
|
|
110111
|
+
}
|
|
110112
|
+
this.active = this.windows.size > 0;
|
|
110113
|
+
}
|
|
110114
|
+
/** Drop what has lapsed. Called on a timer, never from a log path. */
|
|
110115
|
+
tick(nowMs) {
|
|
110116
|
+
const closed = [];
|
|
110117
|
+
for (const [name, window] of this.windows) if (window.untilMs <= nowMs) {
|
|
110118
|
+
this.windows.delete(name);
|
|
110119
|
+
closed.push(name);
|
|
110120
|
+
}
|
|
110121
|
+
this.active = this.windows.size > 0;
|
|
110122
|
+
return closed;
|
|
110123
|
+
}
|
|
110124
|
+
/** `channel` is the full name, `debug.<channel>`. */
|
|
110125
|
+
wants(channel, deviceId) {
|
|
110126
|
+
if (!this.active) return false;
|
|
110127
|
+
const window = this.windows.get(channel);
|
|
110128
|
+
if (window === void 0) return false;
|
|
110129
|
+
return window.devices === null || window.devices.has(deviceId);
|
|
110130
|
+
}
|
|
110131
|
+
};
|
|
110132
|
+
var interestInstance = null;
|
|
110133
|
+
function getDebugInterest() {
|
|
110134
|
+
interestInstance ??= new DebugInterest();
|
|
110135
|
+
return interestInstance;
|
|
110136
|
+
}
|
|
110137
|
+
/**
|
|
110138
|
+
* Build `ctx.debug`. The thunk runs ONLY when somebody watches this
|
|
110139
|
+
* (channel, device) pair, and the line carries `tags: { deviceId }` plus the
|
|
110140
|
+
* `debug.<channel>` name as `logChannel`.
|
|
110141
|
+
*/
|
|
110142
|
+
function createDebugEmitter(logger, interest = getDebugInterest()) {
|
|
110143
|
+
return (channel, deviceId, thunk) => {
|
|
110144
|
+
if (!interest.active) return;
|
|
110145
|
+
const name = `${DEBUG_CHANNEL_PREFIX}${channel}`;
|
|
110146
|
+
if (!interest.wants(name, deviceId)) return;
|
|
110147
|
+
const line = thunk();
|
|
110148
|
+
const tags = {
|
|
110149
|
+
deviceId,
|
|
110150
|
+
logChannel: name
|
|
110151
|
+
};
|
|
110152
|
+
logger.info(line.message, line.meta === void 0 ? { tags } : {
|
|
110153
|
+
tags,
|
|
110154
|
+
meta: line.meta
|
|
110155
|
+
});
|
|
110156
|
+
};
|
|
110157
|
+
}
|
|
110143
110158
|
/**
|
|
110144
110159
|
* The model weight formats a catalog entry can ship. Schema-free on purpose:
|
|
110145
110160
|
* `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
|
|
@@ -110415,6 +110430,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
110415
110430
|
"humidity-sensor",
|
|
110416
110431
|
"image",
|
|
110417
110432
|
"image-settings",
|
|
110433
|
+
"integration-instances",
|
|
110418
110434
|
"integrations",
|
|
110419
110435
|
"intercom",
|
|
110420
110436
|
"ir-transmitter",
|
|
@@ -112386,7 +112402,7 @@ function sleep(ms) {
|
|
|
112386
112402
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
112387
112403
|
}
|
|
112388
112404
|
//#endregion
|
|
112389
|
-
//#region ../types/dist/composition-
|
|
112405
|
+
//#region ../types/dist/composition-Dh3RfMx5.mjs
|
|
112390
112406
|
/**
|
|
112391
112407
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
112392
112408
|
*
|
|
@@ -113600,6 +113616,94 @@ var NodeColorIdSchema = _enum([
|
|
|
113600
113616
|
"node-g",
|
|
113601
113617
|
"node-h"
|
|
113602
113618
|
]);
|
|
113619
|
+
//#endregion
|
|
113620
|
+
//#region ../types/dist/index.mjs
|
|
113621
|
+
/**
|
|
113622
|
+
* The `smtp-inbound` broker kind (D999): a mailbox a device can send mail to.
|
|
113623
|
+
*
|
|
113624
|
+
* The `broker` cap carries these as `Record<string, unknown>` on the wire; the
|
|
113625
|
+
* provider and its consumers parse them with the schemas below at BOTH ends, so
|
|
113626
|
+
* the shape is declared once.
|
|
113627
|
+
*/
|
|
113628
|
+
/** The broker kind tag. */
|
|
113629
|
+
var SMTP_INBOUND_KIND = "smtp-inbound";
|
|
113630
|
+
/** The id the first mailbox is minted under (adopt-id), so a consumer can find it again. */
|
|
113631
|
+
var SMTP_INBOUND_DEFAULT_ID = "smtp-inbound-default";
|
|
113632
|
+
var SMTP_INBOUND_DEFAULT_PORT = 2525;
|
|
113633
|
+
var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
|
|
113634
|
+
var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
|
|
113635
|
+
/** The text excerpt a message carries on the bus. */
|
|
113636
|
+
var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
|
|
113637
|
+
var PortSchema = number().int().min(1).max(65535);
|
|
113638
|
+
/** A recipient local-part: what stands before the `@`. */
|
|
113639
|
+
var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
|
|
113640
|
+
object({
|
|
113641
|
+
port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
|
|
113642
|
+
bindHost: string().min(1).default("0.0.0.0"),
|
|
113643
|
+
domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
|
|
113644
|
+
requireAuth: boolean().default(true),
|
|
113645
|
+
username: string().default(""),
|
|
113646
|
+
password: string().default("").meta({ secret: true }),
|
|
113647
|
+
starttls: boolean().default(false),
|
|
113648
|
+
maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
|
|
113649
|
+
});
|
|
113650
|
+
/**
|
|
113651
|
+
* What `getBrokerConfig` answers: what a device needs to send mail here.
|
|
113652
|
+
* `host` is null when the `internal` role of the network palette is not set;
|
|
113653
|
+
* `hostReason` then says where to set it.
|
|
113654
|
+
*/
|
|
113655
|
+
var SmtpInboundConfigSchema = object({
|
|
113656
|
+
host: string().min(1).nullable(),
|
|
113657
|
+
hostReason: string().nullable(),
|
|
113658
|
+
port: PortSchema,
|
|
113659
|
+
domain: string().min(1),
|
|
113660
|
+
requireAuth: boolean(),
|
|
113661
|
+
username: string(),
|
|
113662
|
+
password: string(),
|
|
113663
|
+
starttls: boolean()
|
|
113664
|
+
});
|
|
113665
|
+
object({
|
|
113666
|
+
recipients: array(SmtpRecipientSchema).min(1),
|
|
113667
|
+
/**
|
|
113668
|
+
* Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
|
|
113669
|
+
* recipients (the way a consumer that restarted, or lost a device, declares its set again);
|
|
113670
|
+
* a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
|
|
113671
|
+
* Absent, the subscriber is anonymous and unique.
|
|
113672
|
+
*/
|
|
113673
|
+
subscriberId: string().min(1).max(64).optional()
|
|
113674
|
+
});
|
|
113675
|
+
var SmtpInboundAttachmentSchema = object({
|
|
113676
|
+
filename: string(),
|
|
113677
|
+
contentType: string(),
|
|
113678
|
+
bytes: number().int().nonnegative()
|
|
113679
|
+
});
|
|
113680
|
+
/** The payload of one `broker.message` (the key is the recipient local-part). */
|
|
113681
|
+
var SmtpInboundMessageSchema = object({
|
|
113682
|
+
/** Monotonic per mailbox; a gap tells a consumer it missed an event. */
|
|
113683
|
+
seq: number().int().positive(),
|
|
113684
|
+
messageId: string(),
|
|
113685
|
+
receivedAt: number().int().nonnegative(),
|
|
113686
|
+
from: string(),
|
|
113687
|
+
to: string(),
|
|
113688
|
+
subject: string(),
|
|
113689
|
+
text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
|
|
113690
|
+
attachments: array(SmtpInboundAttachmentSchema)
|
|
113691
|
+
});
|
|
113692
|
+
/** What `getState('since:<seq>')` answers: every retained message after `seq`. */
|
|
113693
|
+
var SmtpInboundSinceSchema = object({
|
|
113694
|
+
/** The newest seq issued so far (0 before any mail). */
|
|
113695
|
+
lastSeq: number().int().nonnegative(),
|
|
113696
|
+
/** The oldest seq still retained; messages below it are gone. */
|
|
113697
|
+
oldestSeq: number().int().nonnegative(),
|
|
113698
|
+
messages: array(object({
|
|
113699
|
+
key: string(),
|
|
113700
|
+
message: SmtpInboundMessageSchema
|
|
113701
|
+
}))
|
|
113702
|
+
});
|
|
113703
|
+
/** The key a consumer passes to `getState` to replay the ring, and its parser. */
|
|
113704
|
+
function smtpSinceKey(seq) {
|
|
113705
|
+
return `since:${seq}`;
|
|
113706
|
+
}
|
|
113603
113707
|
new Set([
|
|
113604
113708
|
"track",
|
|
113605
113709
|
"face",
|
|
@@ -116765,81 +116869,6 @@ var TIMEZONES = [
|
|
|
116765
116869
|
function findTimezone(id) {
|
|
116766
116870
|
return TIMEZONES.find((tz) => tz.id === id);
|
|
116767
116871
|
}
|
|
116768
|
-
/** Every debug channel's name starts with this; the ring denies it by default. */
|
|
116769
|
-
var DEBUG_CHANNEL_PREFIX = "debug.";
|
|
116770
|
-
function isDebugChannelName(name) {
|
|
116771
|
-
return name.startsWith(DEBUG_CHANNEL_PREFIX);
|
|
116772
|
-
}
|
|
116773
|
-
/**
|
|
116774
|
-
* Who is watching what, in THIS process. In-memory, moved off the log path.
|
|
116775
|
-
*
|
|
116776
|
-
* `active` is a plain boolean FIELD, read first: with no watch anywhere, a
|
|
116777
|
-
* `ctx.debug` call is one load and one branch.
|
|
116778
|
-
*/
|
|
116779
|
-
var DebugInterest = class {
|
|
116780
|
-
active = false;
|
|
116781
|
-
windows = /* @__PURE__ */ new Map();
|
|
116782
|
-
/**
|
|
116783
|
-
* Replace the interest with the FULL set of windows. Non-debug windows are
|
|
116784
|
-
* ignored and an absent one is dropped, so a lost disarm cannot leave a
|
|
116785
|
-
* channel running.
|
|
116786
|
-
*/
|
|
116787
|
-
apply(windows, nowMs) {
|
|
116788
|
-
this.windows.clear();
|
|
116789
|
-
for (const window of windows) {
|
|
116790
|
-
if (!isDebugChannelName(window.channel) || window.armedUntilMs <= nowMs) continue;
|
|
116791
|
-
const ids = window.deviceIds;
|
|
116792
|
-
this.windows.set(window.channel, {
|
|
116793
|
-
untilMs: window.armedUntilMs,
|
|
116794
|
-
devices: ids === null || ids.length === 0 ? null : new Set(ids)
|
|
116795
|
-
});
|
|
116796
|
-
}
|
|
116797
|
-
this.active = this.windows.size > 0;
|
|
116798
|
-
}
|
|
116799
|
-
/** Drop what has lapsed. Called on a timer, never from a log path. */
|
|
116800
|
-
tick(nowMs) {
|
|
116801
|
-
const closed = [];
|
|
116802
|
-
for (const [name, window] of this.windows) if (window.untilMs <= nowMs) {
|
|
116803
|
-
this.windows.delete(name);
|
|
116804
|
-
closed.push(name);
|
|
116805
|
-
}
|
|
116806
|
-
this.active = this.windows.size > 0;
|
|
116807
|
-
return closed;
|
|
116808
|
-
}
|
|
116809
|
-
/** `channel` is the full name, `debug.<channel>`. */
|
|
116810
|
-
wants(channel, deviceId) {
|
|
116811
|
-
if (!this.active) return false;
|
|
116812
|
-
const window = this.windows.get(channel);
|
|
116813
|
-
if (window === void 0) return false;
|
|
116814
|
-
return window.devices === null || window.devices.has(deviceId);
|
|
116815
|
-
}
|
|
116816
|
-
};
|
|
116817
|
-
var interestInstance = null;
|
|
116818
|
-
function getDebugInterest() {
|
|
116819
|
-
interestInstance ??= new DebugInterest();
|
|
116820
|
-
return interestInstance;
|
|
116821
|
-
}
|
|
116822
|
-
/**
|
|
116823
|
-
* Build `ctx.debug`. The thunk runs ONLY when somebody watches this
|
|
116824
|
-
* (channel, device) pair, and the line carries `tags: { deviceId }` plus the
|
|
116825
|
-
* `debug.<channel>` name as `logChannel`.
|
|
116826
|
-
*/
|
|
116827
|
-
function createDebugEmitter(logger, interest = getDebugInterest()) {
|
|
116828
|
-
return (channel, deviceId, thunk) => {
|
|
116829
|
-
if (!interest.active) return;
|
|
116830
|
-
const name = `${DEBUG_CHANNEL_PREFIX}${channel}`;
|
|
116831
|
-
if (!interest.wants(name, deviceId)) return;
|
|
116832
|
-
const line = thunk();
|
|
116833
|
-
const tags = {
|
|
116834
|
-
deviceId,
|
|
116835
|
-
logChannel: name
|
|
116836
|
-
};
|
|
116837
|
-
logger.info(line.message, line.meta === void 0 ? { tags } : {
|
|
116838
|
-
tags,
|
|
116839
|
-
meta: line.meta
|
|
116840
|
-
});
|
|
116841
|
-
};
|
|
116842
|
-
}
|
|
116843
116872
|
/**
|
|
116844
116873
|
* Per-component log CHANNELS — the gate a hot path consults, and the registry
|
|
116845
116874
|
* an addon declares its channels in.
|
|
@@ -122516,6 +122545,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
122516
122545
|
"load-contribution": system(INTERNAL),
|
|
122517
122546
|
"local-network": system(NETWORK),
|
|
122518
122547
|
"log-channels": system(INTERNAL),
|
|
122548
|
+
"integration-instances": system(INTERNAL),
|
|
122519
122549
|
"log-destination": system(INTERNAL),
|
|
122520
122550
|
"login-method": system(IDENTITY),
|
|
122521
122551
|
"mesh-network": system(NETWORK),
|
|
@@ -144722,7 +144752,7 @@ var TargetKindCapsSchema = object({
|
|
|
144722
144752
|
* the union is large and not meant for runtime validation here; the exported
|
|
144723
144753
|
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
144724
144754
|
*/
|
|
144725
|
-
var ConfigSchemaPassthrough$
|
|
144755
|
+
var ConfigSchemaPassthrough$4 = unknown();
|
|
144726
144756
|
var TargetKindSchema = object({
|
|
144727
144757
|
kind: string(),
|
|
144728
144758
|
label: string(),
|
|
@@ -144759,7 +144789,7 @@ var TargetKindSchema = object({
|
|
|
144759
144789
|
* its raster path, which is the safe default for an unknown image.
|
|
144760
144790
|
*/
|
|
144761
144791
|
iconMediaType: string().optional(),
|
|
144762
|
-
configSchema: ConfigSchemaPassthrough$
|
|
144792
|
+
configSchema: ConfigSchemaPassthrough$4,
|
|
144763
144793
|
/**
|
|
144764
144794
|
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
144765
144795
|
* base URL, instance, chat, recipient). A save that changes any of them
|
|
@@ -153655,10 +153685,23 @@ var DeviceDataEntrySchema = object({
|
|
|
153655
153685
|
/** Rows (or files) held for the device in the store. */
|
|
153656
153686
|
count: number().int().nonnegative()
|
|
153657
153687
|
});
|
|
153658
|
-
|
|
153659
|
-
|
|
153660
|
-
|
|
153661
|
-
|
|
153688
|
+
var deviceRemovalHookCapability = {
|
|
153689
|
+
name: "device-removal-hook",
|
|
153690
|
+
scope: "system",
|
|
153691
|
+
mode: "collection",
|
|
153692
|
+
internal: true,
|
|
153693
|
+
methods: {
|
|
153694
|
+
/** Delete everything this addon keeps for the deleted device. Idempotent. */
|
|
153695
|
+
onDeviceRemoved: method(DeviceRemovedInputSchema, DeviceRemovedResultSchema, {
|
|
153696
|
+
kind: "mutation",
|
|
153697
|
+
auth: "admin"
|
|
153698
|
+
}),
|
|
153699
|
+
/** Which devices this addon holds data for, per store. Reads only. */
|
|
153700
|
+
listDeviceData: method(_void(), array(DeviceDataEntrySchema).readonly(), { auth: "admin" })
|
|
153701
|
+
},
|
|
153702
|
+
/** In-process only — enumerated through `addons.listCapabilityProviders`. */
|
|
153703
|
+
mount: { kind: "skip" }
|
|
153704
|
+
};
|
|
153662
153705
|
/**
|
|
153663
153706
|
* device-runs - the durable record of ONE run of a device: a washing cycle, a
|
|
153664
153707
|
* dishwasher cycle, later a mow or a clean (D992).
|
|
@@ -156521,6 +156564,89 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
156521
156564
|
auth: "admin"
|
|
156522
156565
|
});
|
|
156523
156566
|
/**
|
|
156567
|
+
* `integration-instances` - the connection of an addon that holds ONE or SEVERAL
|
|
156568
|
+
* (an account, a relay, a destination) as typed records (D985, D986). The addon's
|
|
156569
|
+
* own function page reads and writes them (Logging for Loki); the Integrations
|
|
156570
|
+
* page lists only the integrations that bring devices (D996).
|
|
156571
|
+
*
|
|
156572
|
+
* A collection of providers: each addon that holds connections registers one and a
|
|
156573
|
+
* caller reaches a provider with the pinned `addonId`.
|
|
156574
|
+
*
|
|
156575
|
+
* Identity: the instance id is immutable and opaque to the caller. A record the
|
|
156576
|
+
* Loki migration made keeps the `integrations` registry id it was created under
|
|
156577
|
+
* (`int_NNNN`); a single instance made by its function page takes the addon id.
|
|
156578
|
+
* `name` is the editable label.
|
|
156579
|
+
*
|
|
156580
|
+
* Secrets: every field the instance kind marks `.meta({ secret: true })` reaches
|
|
156581
|
+
* the form as a `SecretFieldView`, never text (D789). A patch value omitted, `''`
|
|
156582
|
+
* or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
|
|
156583
|
+
* clears it.
|
|
156584
|
+
*/
|
|
156585
|
+
var ConfigSchemaPassthrough$3 = custom();
|
|
156586
|
+
var IntegrationInstanceIdSchema = string().min(1);
|
|
156587
|
+
var IntegrationInstanceDescriptionSchema = object({
|
|
156588
|
+
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
156589
|
+
instanceMode: _enum(["single", "multiple"]),
|
|
156590
|
+
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
156591
|
+
kind: string(),
|
|
156592
|
+
/** True when `testInstance` really probes the remote. */
|
|
156593
|
+
canTest: boolean()
|
|
156594
|
+
});
|
|
156595
|
+
var IntegrationInstanceStatusSchema = object({
|
|
156596
|
+
state: _enum([
|
|
156597
|
+
"ok",
|
|
156598
|
+
"off",
|
|
156599
|
+
"error",
|
|
156600
|
+
"unknown"
|
|
156601
|
+
]),
|
|
156602
|
+
detail: string().optional()
|
|
156603
|
+
});
|
|
156604
|
+
var IntegrationInstanceSchema = object({
|
|
156605
|
+
id: IntegrationInstanceIdSchema,
|
|
156606
|
+
name: string(),
|
|
156607
|
+
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
156608
|
+
configured: boolean(),
|
|
156609
|
+
/**
|
|
156610
|
+
* The instance's own on/off switch (D996): always present, derived from the record body
|
|
156611
|
+
* (`enabled !== false`, so a row written before the field existed is on).
|
|
156612
|
+
*/
|
|
156613
|
+
enabled: boolean(),
|
|
156614
|
+
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
156615
|
+
formValues: record(string(), unknown()),
|
|
156616
|
+
status: IntegrationInstanceStatusSchema.optional()
|
|
156617
|
+
});
|
|
156618
|
+
var IntegrationInstancePatchSchema = object({
|
|
156619
|
+
id: IntegrationInstanceIdSchema,
|
|
156620
|
+
name: string().min(1).optional(),
|
|
156621
|
+
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
156622
|
+
values: record(string(), unknown())
|
|
156623
|
+
});
|
|
156624
|
+
method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }), method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }), method(object({
|
|
156625
|
+
id: IntegrationInstanceIdSchema,
|
|
156626
|
+
name: string().min(1)
|
|
156627
|
+
}), object({ id: string() }), {
|
|
156628
|
+
kind: "mutation",
|
|
156629
|
+
auth: "admin"
|
|
156630
|
+
}), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
156631
|
+
kind: "mutation",
|
|
156632
|
+
auth: "admin"
|
|
156633
|
+
}), method(object({
|
|
156634
|
+
id: IntegrationInstanceIdSchema,
|
|
156635
|
+
enabled: boolean()
|
|
156636
|
+
}), object({ ok: literal(true) }), {
|
|
156637
|
+
kind: "mutation",
|
|
156638
|
+
auth: "admin"
|
|
156639
|
+
}), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
156640
|
+
kind: "mutation",
|
|
156641
|
+
auth: "admin"
|
|
156642
|
+
}), optionalMethod(object({
|
|
156643
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
156644
|
+
values: record(string(), unknown())
|
|
156645
|
+
}), TestConnectionResultSchema$1, {
|
|
156646
|
+
kind: "mutation",
|
|
156647
|
+
auth: "admin"
|
|
156648
|
+
});
|
|
156649
|
+
/**
|
|
156524
156650
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
156525
156651
|
* session (server-initiated offer pattern, identical to the main
|
|
156526
156652
|
* video WebRTC flow) and the browser captures the mic; the server
|
|
@@ -167329,7 +167455,14 @@ var NcMediaPolicySchema = object({
|
|
|
167329
167455
|
*/
|
|
167330
167456
|
profile: CamProfileSchema.optional(),
|
|
167331
167457
|
/** See {@link NcLinkedCameraModeSchema}. */
|
|
167332
|
-
linkedCameras: NcLinkedCameraModeSchema.optional()
|
|
167458
|
+
linkedCameras: NcLinkedCameraModeSchema.optional(),
|
|
167459
|
+
/**
|
|
167460
|
+
* Sensor rules only: may this rule wake a SLEEPING battery camera to take
|
|
167461
|
+
* the linked still it attaches (D998)? Absent means yes, an explicit `false`
|
|
167462
|
+
* opts out. Deliberately not a Zod default: a rule that never chose keeps
|
|
167463
|
+
* following the hub's. One bounded wake per event; nothing is attached later.
|
|
167464
|
+
*/
|
|
167465
|
+
wakeSleepingCamera: boolean().optional()
|
|
167333
167466
|
});
|
|
167334
167467
|
/**
|
|
167335
167468
|
* Cooldown GRANULARITY over the subject's class — how much a fired
|
|
@@ -176492,7 +176625,16 @@ var smokeCapability = {
|
|
|
176492
176625
|
* future fields without breaking the existing call sites.
|
|
176493
176626
|
*/
|
|
176494
176627
|
var EmailAddressSchema = email();
|
|
176628
|
+
/**
|
|
176629
|
+
* Which relay of a multi-relay provider answers (D996). Absent = the provider's first / default
|
|
176630
|
+
* relay, so a caller written before relays existed keeps working unchanged. A provider with one
|
|
176631
|
+
* fixed relay ignores it. This is the integration instance id (`int_NNNN`), NOT the addon id: the
|
|
176632
|
+
* collection pin `addonId` still picks the provider, `instanceId` picks the relay inside it.
|
|
176633
|
+
*/
|
|
176634
|
+
var InstanceIdSchema = string().min(1).optional();
|
|
176635
|
+
var RelayPinSchema = object({ instanceId: InstanceIdSchema });
|
|
176495
176636
|
var SendEmailInputSchema = object({
|
|
176637
|
+
instanceId: InstanceIdSchema,
|
|
176496
176638
|
to: union([EmailAddressSchema, array(EmailAddressSchema).min(1)]),
|
|
176497
176639
|
cc: array(EmailAddressSchema).optional(),
|
|
176498
176640
|
bcc: array(EmailAddressSchema).optional(),
|
|
@@ -176528,11 +176670,11 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
|
|
|
176528
176670
|
kind: "mutation",
|
|
176529
176671
|
auth: "admin",
|
|
176530
176672
|
access: "create"
|
|
176531
|
-
}), method(
|
|
176673
|
+
}), method(RelayPinSchema, SmtpStatusSchema, {
|
|
176532
176674
|
kind: "mutation",
|
|
176533
176675
|
auth: "admin",
|
|
176534
176676
|
access: "view"
|
|
176535
|
-
}), method(
|
|
176677
|
+
}), method(RelayPinSchema, SmtpStatusSchema, { auth: "admin" });
|
|
176536
176678
|
/**
|
|
176537
176679
|
* A single device snapshot returned as base64 JPEG/PNG.
|
|
176538
176680
|
*
|
|
@@ -176600,10 +176742,12 @@ var snapshotCapability = {
|
|
|
176600
176742
|
* even when the cache is well within the device's
|
|
176601
176743
|
* `snapshotMaxAgeS` window.
|
|
176602
176744
|
*
|
|
176603
|
-
* **`force` is
|
|
176745
|
+
* **`force` is a demand with a named principal, not a freshness
|
|
176746
|
+
* preference** — an operator button, the scheduled refresh (D847), or a
|
|
176747
|
+
* notification rule that left the camera's wake allowed (D998). On a
|
|
176604
176748
|
* battery camera it is the one thing that walks past the wrapper's
|
|
176605
|
-
* sleep gate and wakes the camera, so a
|
|
176606
|
-
*
|
|
176749
|
+
* sleep gate and wakes the camera, so a poller, a thumbnail or a
|
|
176750
|
+
* prestream must NEVER set it. Every such caller
|
|
176607
176751
|
* gets the cached frame, which on a sleeping battery camera is the
|
|
176608
176752
|
* correct answer: stale but honest beats woken.
|
|
176609
176753
|
*/
|
|
@@ -181946,7 +182090,7 @@ function withClipEventLabels(provider, deps) {
|
|
|
181946
182090
|
listClips
|
|
181947
182091
|
};
|
|
181948
182092
|
}
|
|
181949
|
-
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
182093
|
+
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
181950
182094
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
181951
182095
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
181952
182096
|
DeviceRole.BinarySensor, DeviceRole.MotionSensor;
|
|
@@ -187144,6 +187288,69 @@ Object.freeze({
|
|
|
187144
187288
|
access: "create",
|
|
187145
187289
|
role: "admin"
|
|
187146
187290
|
},
|
|
187291
|
+
"integrationInstances.createInstance": {
|
|
187292
|
+
capName: "integration-instances",
|
|
187293
|
+
capScope: "system",
|
|
187294
|
+
addonId: null,
|
|
187295
|
+
access: "create",
|
|
187296
|
+
role: "admin"
|
|
187297
|
+
},
|
|
187298
|
+
"integrationInstances.deleteInstance": {
|
|
187299
|
+
capName: "integration-instances",
|
|
187300
|
+
capScope: "system",
|
|
187301
|
+
addonId: null,
|
|
187302
|
+
access: "delete",
|
|
187303
|
+
role: "admin"
|
|
187304
|
+
},
|
|
187305
|
+
"integrationInstances.describe": {
|
|
187306
|
+
capName: "integration-instances",
|
|
187307
|
+
capScope: "system",
|
|
187308
|
+
addonId: null,
|
|
187309
|
+
access: "view",
|
|
187310
|
+
role: "admin"
|
|
187311
|
+
},
|
|
187312
|
+
"integrationInstances.getInstance": {
|
|
187313
|
+
capName: "integration-instances",
|
|
187314
|
+
capScope: "system",
|
|
187315
|
+
addonId: null,
|
|
187316
|
+
access: "view",
|
|
187317
|
+
role: "admin"
|
|
187318
|
+
},
|
|
187319
|
+
"integrationInstances.instanceFormSchema": {
|
|
187320
|
+
capName: "integration-instances",
|
|
187321
|
+
capScope: "system",
|
|
187322
|
+
addonId: null,
|
|
187323
|
+
access: "view",
|
|
187324
|
+
role: "admin"
|
|
187325
|
+
},
|
|
187326
|
+
"integrationInstances.listInstances": {
|
|
187327
|
+
capName: "integration-instances",
|
|
187328
|
+
capScope: "system",
|
|
187329
|
+
addonId: null,
|
|
187330
|
+
access: "view",
|
|
187331
|
+
role: "admin"
|
|
187332
|
+
},
|
|
187333
|
+
"integrationInstances.setInstanceEnabled": {
|
|
187334
|
+
capName: "integration-instances",
|
|
187335
|
+
capScope: "system",
|
|
187336
|
+
addonId: null,
|
|
187337
|
+
access: "create",
|
|
187338
|
+
role: "admin"
|
|
187339
|
+
},
|
|
187340
|
+
"integrationInstances.testInstance": {
|
|
187341
|
+
capName: "integration-instances",
|
|
187342
|
+
capScope: "system",
|
|
187343
|
+
addonId: null,
|
|
187344
|
+
access: "create",
|
|
187345
|
+
role: "admin"
|
|
187346
|
+
},
|
|
187347
|
+
"integrationInstances.updateInstance": {
|
|
187348
|
+
capName: "integration-instances",
|
|
187349
|
+
capScope: "system",
|
|
187350
|
+
addonId: null,
|
|
187351
|
+
access: "create",
|
|
187352
|
+
role: "admin"
|
|
187353
|
+
},
|
|
187147
187354
|
"integrations.create": {
|
|
187148
187355
|
capName: "integrations",
|
|
187149
187356
|
capScope: "system",
|
|
@@ -205779,7 +205986,7 @@ var ReolinkHubSession = class {
|
|
|
205779
205986
|
this.getApi().catch((err) => {
|
|
205780
205987
|
this.#logger.warn("Reolink hub initial connect failed — will retry", {
|
|
205781
205988
|
tags: { integrationId: this.integrationId },
|
|
205782
|
-
meta: { error: errMsg$
|
|
205989
|
+
meta: { error: errMsg$3(err) }
|
|
205783
205990
|
});
|
|
205784
205991
|
});
|
|
205785
205992
|
}
|
|
@@ -205798,7 +206005,7 @@ var ReolinkHubSession = class {
|
|
|
205798
206005
|
this.getApi().catch((err) => {
|
|
205799
206006
|
this.#logger.warn("Reolink hub re-dial after settings change failed", {
|
|
205800
206007
|
tags: { integrationId: this.integrationId },
|
|
205801
|
-
meta: { error: errMsg$
|
|
206008
|
+
meta: { error: errMsg$3(err) }
|
|
205802
206009
|
});
|
|
205803
206010
|
});
|
|
205804
206011
|
}
|
|
@@ -205859,7 +206066,7 @@ var ReolinkHubSession = class {
|
|
|
205859
206066
|
api.client.on("error", (err) => {
|
|
205860
206067
|
this.#logger.warn("Reolink hub socket error — scheduling reconnect", {
|
|
205861
206068
|
tags: { integrationId: this.integrationId },
|
|
205862
|
-
meta: { error: errMsg$
|
|
206069
|
+
meta: { error: errMsg$3(err) }
|
|
205863
206070
|
});
|
|
205864
206071
|
this.#scheduleReconnect("socket-error");
|
|
205865
206072
|
});
|
|
@@ -205928,7 +206135,7 @@ var ReolinkHubSession = class {
|
|
|
205928
206135
|
isMultifocal: d.isMultifocal === true
|
|
205929
206136
|
}));
|
|
205930
206137
|
} catch (err) {
|
|
205931
|
-
lastError = errMsg$
|
|
206138
|
+
lastError = errMsg$3(err);
|
|
205932
206139
|
this.#logger.warn("Reolink hub channel refresh failed", {
|
|
205933
206140
|
tags: { integrationId: this.integrationId },
|
|
205934
206141
|
meta: { error: lastError }
|
|
@@ -206002,7 +206209,7 @@ var ReolinkHubSession = class {
|
|
|
206002
206209
|
meta: {
|
|
206003
206210
|
from: repair.from,
|
|
206004
206211
|
to: repair.to,
|
|
206005
|
-
error: errMsg$
|
|
206212
|
+
error: errMsg$3(err)
|
|
206006
206213
|
}
|
|
206007
206214
|
});
|
|
206008
206215
|
}
|
|
@@ -206115,7 +206322,7 @@ var ReolinkHubSession = class {
|
|
|
206115
206322
|
} catch (err) {
|
|
206116
206323
|
this.#logger.debug("Reolink hub: per-channel battery refresh failed", {
|
|
206117
206324
|
tags: { integrationId: this.integrationId },
|
|
206118
|
-
meta: { error: errMsg$
|
|
206325
|
+
meta: { error: errMsg$3(err) }
|
|
206119
206326
|
});
|
|
206120
206327
|
}
|
|
206121
206328
|
}
|
|
@@ -206163,7 +206370,7 @@ var ReolinkHubSession = class {
|
|
|
206163
206370
|
tags: { deviceId: child.id },
|
|
206164
206371
|
meta: {
|
|
206165
206372
|
channel,
|
|
206166
|
-
error: errMsg$
|
|
206373
|
+
error: errMsg$3(err)
|
|
206167
206374
|
}
|
|
206168
206375
|
});
|
|
206169
206376
|
}
|
|
@@ -209189,7 +209396,7 @@ var IntercomOrchestrator = class {
|
|
|
209189
209396
|
if (this.opts.wakeBeforeStart) try {
|
|
209190
209397
|
await this.opts.wakeBeforeStart();
|
|
209191
209398
|
} catch (err) {
|
|
209192
|
-
this.opts.logger.warn("intercom: pre-wake failed", { meta: { error: errMsg$
|
|
209399
|
+
this.opts.logger.warn("intercom: pre-wake failed", { meta: { error: errMsg$2(err) } });
|
|
209193
209400
|
throw err;
|
|
209194
209401
|
}
|
|
209195
209402
|
const talkSession = new ReolinkIntercomSession({
|
|
@@ -209204,7 +209411,7 @@ var IntercomOrchestrator = class {
|
|
|
209204
209411
|
try {
|
|
209205
209412
|
await talkSession.start();
|
|
209206
209413
|
} catch (err) {
|
|
209207
|
-
this.opts.logger.warn("intercom: talk session open failed", { meta: { error: errMsg$
|
|
209414
|
+
this.opts.logger.warn("intercom: talk session open failed", { meta: { error: errMsg$2(err) } });
|
|
209208
209415
|
throw err;
|
|
209209
209416
|
}
|
|
209210
209417
|
const realCameraRate = talkSession.sampleRate;
|
|
@@ -209223,7 +209430,7 @@ var IntercomOrchestrator = class {
|
|
|
209223
209430
|
} catch (err) {
|
|
209224
209431
|
await talkSession.stop().catch(() => {});
|
|
209225
209432
|
this.opts.logger.warn("intercom: audio-codec createDecodeSession failed", { meta: {
|
|
209226
|
-
error: errMsg$
|
|
209433
|
+
error: errMsg$2(err),
|
|
209227
209434
|
opusRate,
|
|
209228
209435
|
opusChannels,
|
|
209229
209436
|
targetRate
|
|
@@ -209245,7 +209452,7 @@ var IntercomOrchestrator = class {
|
|
|
209245
209452
|
this.session = active;
|
|
209246
209453
|
peer.onOpusFrame((frame, pts) => {
|
|
209247
209454
|
this.handleOpusFrame(active, frame, pts).catch((err) => {
|
|
209248
|
-
this.opts.logger.debug("intercom: opus frame pump error (dropped)", { meta: { error: errMsg$
|
|
209455
|
+
this.opts.logger.debug("intercom: opus frame pump error (dropped)", { meta: { error: errMsg$2(err) } });
|
|
209249
209456
|
});
|
|
209250
209457
|
});
|
|
209251
209458
|
let offer;
|
|
@@ -209253,7 +209460,7 @@ var IntercomOrchestrator = class {
|
|
|
209253
209460
|
offer = await peer.createOffer();
|
|
209254
209461
|
} catch (err) {
|
|
209255
209462
|
await this.stop(sessionId, "start-failed-cleanup").catch(() => {});
|
|
209256
|
-
this.opts.logger.warn("intercom: webrtc createOffer failed", { meta: { error: errMsg$
|
|
209463
|
+
this.opts.logger.warn("intercom: webrtc createOffer failed", { meta: { error: errMsg$2(err) } });
|
|
209257
209464
|
throw err;
|
|
209258
209465
|
}
|
|
209259
209466
|
this.opts.logger.info("intercom session opened", { meta: {
|
|
@@ -209302,7 +209509,7 @@ var IntercomOrchestrator = class {
|
|
|
209302
209509
|
} catch (err) {
|
|
209303
209510
|
this.opts.logger.debug("intercom: peer.close error (continuing)", { meta: {
|
|
209304
209511
|
sessionId,
|
|
209305
|
-
error: errMsg$
|
|
209512
|
+
error: errMsg$2(err)
|
|
209306
209513
|
} });
|
|
209307
209514
|
}
|
|
209308
209515
|
try {
|
|
@@ -209313,7 +209520,7 @@ var IntercomOrchestrator = class {
|
|
|
209313
209520
|
} catch (err) {
|
|
209314
209521
|
this.opts.logger.debug("intercom: audio-codec.closeSession error (continuing)", { meta: {
|
|
209315
209522
|
sessionId,
|
|
209316
|
-
error: errMsg$
|
|
209523
|
+
error: errMsg$2(err)
|
|
209317
209524
|
} });
|
|
209318
209525
|
}
|
|
209319
209526
|
try {
|
|
@@ -209321,7 +209528,7 @@ var IntercomOrchestrator = class {
|
|
|
209321
209528
|
} catch (err) {
|
|
209322
209529
|
this.opts.logger.debug("intercom: talk-session.stop error (continuing)", { meta: {
|
|
209323
209530
|
sessionId,
|
|
209324
|
-
error: errMsg$
|
|
209531
|
+
error: errMsg$2(err)
|
|
209325
209532
|
} });
|
|
209326
209533
|
}
|
|
209327
209534
|
this.opts.logger.info("intercom session closed", { meta: {
|
|
@@ -209377,7 +209584,7 @@ var IntercomOrchestrator = class {
|
|
|
209377
209584
|
function generateSessionId() {
|
|
209378
209585
|
return `intercom-${Date.now().toString(36)}-${Math.floor(Math.random() * 16777215).toString(36)}`;
|
|
209379
209586
|
}
|
|
209380
|
-
function errMsg$
|
|
209587
|
+
function errMsg$2(err) {
|
|
209381
209588
|
return err instanceof Error ? err.message : String(err);
|
|
209382
209589
|
}
|
|
209383
209590
|
//#endregion
|
|
@@ -209436,7 +209643,7 @@ var WeriftIntercomPeer = class {
|
|
|
209436
209643
|
for (const cb of this.opusCallbacks) try {
|
|
209437
209644
|
cb(payload, ptsMs);
|
|
209438
209645
|
} catch (err) {
|
|
209439
|
-
this.opts.logger.debug("intercom-peer: opus callback threw", { meta: { error: errMsg(err) } });
|
|
209646
|
+
this.opts.logger.debug("intercom-peer: opus callback threw", { meta: { error: errMsg$1(err) } });
|
|
209440
209647
|
}
|
|
209441
209648
|
});
|
|
209442
209649
|
if (rtpSub && typeof rtpSub.unsubscribe === "function") this.rtpUnsubscribe = () => rtpSub.unsubscribe?.();
|
|
@@ -209484,7 +209691,7 @@ var WeriftIntercomPeer = class {
|
|
|
209484
209691
|
}
|
|
209485
209692
|
}
|
|
209486
209693
|
};
|
|
209487
|
-
function errMsg(err) {
|
|
209694
|
+
function errMsg$1(err) {
|
|
209488
209695
|
return err instanceof Error ? err.message : String(err);
|
|
209489
209696
|
}
|
|
209490
209697
|
//#endregion
|
|
@@ -210960,7 +211167,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
210960
211167
|
* `true` iff this camera can run its own email/SMTP push path. Hub
|
|
210961
211168
|
* children are excluded: they share the parent's TCP socket and have
|
|
210962
211169
|
* no independent email config, so the NVR/Hub handles email centrally.
|
|
210963
|
-
* Consumed by the
|
|
211170
|
+
* Consumed by the camera mailbox bridge (`email-push-bridge.ts`)
|
|
210964
211171
|
* to decide which cameras accept inbound mail + can be auto-configured.
|
|
210965
211172
|
*/
|
|
210966
211173
|
get isEmailPushEligible() {
|
|
@@ -211666,12 +211873,12 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
211666
211873
|
}
|
|
211667
211874
|
if (!this.emailPushParamsResolver) {
|
|
211668
211875
|
this.ctx.logger.warn("email-push: resolver not wired — restart the Reolink addon", { tags: { deviceId: this.id } });
|
|
211669
|
-
throw userError("reolink.email-push-not-wired", void 0, "
|
|
211876
|
+
throw userError("reolink.email-push-not-wired", void 0, "The camera mailbox is not wired — restart the Reolink addon");
|
|
211670
211877
|
}
|
|
211671
211878
|
const params = await this.emailPushParamsResolver();
|
|
211672
211879
|
if (!params) {
|
|
211673
|
-
this.ctx.logger.warn("email-push: no manager params —
|
|
211674
|
-
throw new Error("
|
|
211880
|
+
this.ctx.logger.warn("email-push: no manager params — this camera cannot use the camera mailbox", { tags: { deviceId: this.id } });
|
|
211881
|
+
throw new Error("This camera cannot use the camera mailbox.");
|
|
211675
211882
|
}
|
|
211676
211883
|
const triggers = this.config.get("emailPushTriggers") ?? [
|
|
211677
211884
|
"MD",
|
|
@@ -218655,15 +218862,6 @@ function mapInferredTypeToSimpleEvents(inferred) {
|
|
|
218655
218862
|
default: return ["motion"];
|
|
218656
218863
|
}
|
|
218657
218864
|
}
|
|
218658
|
-
/** Default SMTP listen port. Avoid privileged 25; Reolink firmwares are
|
|
218659
|
-
* happy on any port the email config points at. */
|
|
218660
|
-
var DEFAULT_EMAIL_PUSH_PORT = 2525;
|
|
218661
|
-
/** Virtual mail domain — only used to extract the camera id from the
|
|
218662
|
-
* recipient (`cam-<id>@<domain>`). Never resolved via DNS. */
|
|
218663
|
-
var DEFAULT_EMAIL_PUSH_DOMAIN = "camstack.local";
|
|
218664
|
-
/** Reolink snapshots fit comfortably; 25 MB covers video attachments
|
|
218665
|
-
* too without letting a runaway upload exhaust memory. */
|
|
218666
|
-
var DEFAULT_EMAIL_PUSH_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
|
|
218667
218865
|
/**
|
|
218668
218866
|
* Enumerate candidate camera-facing LAN addresses, best first (private
|
|
218669
218867
|
* RFC-1918 ranges ranked ahead of public). Link-local and internal
|
|
@@ -218690,15 +218888,15 @@ function listLanHosts() {
|
|
|
218690
218888
|
}
|
|
218691
218889
|
//#endregion
|
|
218692
218890
|
//#region src/baichuan-webhook-server.ts
|
|
218693
|
-
function readString
|
|
218891
|
+
function readString(raw, key, fallback) {
|
|
218694
218892
|
const v = raw[key];
|
|
218695
218893
|
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
218696
218894
|
}
|
|
218697
|
-
function readNumber
|
|
218895
|
+
function readNumber(raw, key, fallback) {
|
|
218698
218896
|
const v = raw[key];
|
|
218699
218897
|
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
218700
218898
|
}
|
|
218701
|
-
function readBoolean
|
|
218899
|
+
function readBoolean(raw, key, fallback) {
|
|
218702
218900
|
const v = raw[key];
|
|
218703
218901
|
return typeof v === "boolean" ? v : fallback;
|
|
218704
218902
|
}
|
|
@@ -218714,9 +218912,9 @@ var ReolinkBaichuanWebhookServer = class {
|
|
|
218714
218912
|
if (this.settings) return this.settings;
|
|
218715
218913
|
const raw = await this.deps.readStore();
|
|
218716
218914
|
this.settings = {
|
|
218717
|
-
enabled: readBoolean
|
|
218718
|
-
port: readNumber
|
|
218719
|
-
bindHost: readString
|
|
218915
|
+
enabled: readBoolean(raw, "baichuanWebhookEnabled", true),
|
|
218916
|
+
port: readNumber(raw, "baichuanWebhookPort", DEFAULT_BAICHUAN_WEBHOOK_PORT),
|
|
218917
|
+
bindHost: readString(raw, "baichuanWebhookBindHost", "0.0.0.0")
|
|
218720
218918
|
};
|
|
218721
218919
|
return this.settings;
|
|
218722
218920
|
}
|
|
@@ -219023,338 +219221,420 @@ function buildCreationFormSchema() {
|
|
|
219023
219221
|
] };
|
|
219024
219222
|
}
|
|
219025
219223
|
//#endregion
|
|
219026
|
-
//#region src/email-push-
|
|
219027
|
-
/**
|
|
219028
|
-
* The
|
|
219029
|
-
*
|
|
219030
|
-
*
|
|
219031
|
-
|
|
219032
|
-
|
|
219033
|
-
|
|
219034
|
-
*
|
|
219035
|
-
*
|
|
219036
|
-
*
|
|
219037
|
-
*
|
|
219038
|
-
|
|
219039
|
-
|
|
219040
|
-
|
|
219041
|
-
|
|
219042
|
-
|
|
219043
|
-
|
|
219044
|
-
|
|
219045
|
-
|
|
219046
|
-
|
|
219047
|
-
|
|
219048
|
-
|
|
219049
|
-
|
|
219050
|
-
|
|
219051
|
-
|
|
219052
|
-
|
|
219053
|
-
|
|
219054
|
-
|
|
219055
|
-
|
|
219056
|
-
|
|
219057
|
-
|
|
219058
|
-
|
|
219059
|
-
|
|
219060
|
-
|
|
219061
|
-
|
|
219062
|
-
|
|
219063
|
-
|
|
219064
|
-
|
|
219065
|
-
|
|
219066
|
-
* "integration" into an operator-facing tab bar (D268 → D269).
|
|
219067
|
-
*/
|
|
219068
|
-
function buildEmailPushSettingsSchema(recommendedHost) {
|
|
219069
|
-
return { sections: [{
|
|
219070
|
-
id: "email-push",
|
|
219071
|
-
title: "Battery-cam Motion (Email/SMTP Push)",
|
|
219072
|
-
titleKey: "provider-reolink:emailPushSettings.s_email-push.title",
|
|
219073
|
-
description: "Lets battery cameras report motion by email without being woken.",
|
|
219074
|
-
descriptionKey: "provider-reolink:emailPushSettings.s_email-push.description",
|
|
219075
|
-
columns: 2,
|
|
219076
|
-
fields: [
|
|
219077
|
-
{
|
|
219078
|
-
type: "boolean",
|
|
219079
|
-
key: "emailPushEnabled",
|
|
219080
|
-
label: "Enable Email Push Server",
|
|
219081
|
-
labelKey: "provider-reolink:emailPushSettings.s_email-push.emailPushEnabled.label",
|
|
219082
|
-
default: true
|
|
219083
|
-
},
|
|
219084
|
-
{
|
|
219085
|
-
type: "info",
|
|
219086
|
-
key: "emailPushRecommendedHost",
|
|
219087
|
-
label: "Camera-facing host",
|
|
219088
|
-
labelKey: "provider-reolink:emailPushSettings.s_email-push.emailPushRecommendedHost.label",
|
|
219089
|
-
content: `Point cameras at this host: ${recommendedHost}`
|
|
219090
|
-
},
|
|
219091
|
-
{
|
|
219092
|
-
type: "number",
|
|
219093
|
-
key: "emailPushPort",
|
|
219094
|
-
label: "SMTP Port",
|
|
219095
|
-
labelKey: "provider-reolink:emailPushSettings.s_email-push.emailPushPort.label",
|
|
219096
|
-
default: DEFAULT_EMAIL_PUSH_PORT,
|
|
219097
|
-
min: 1,
|
|
219098
|
-
max: 65535
|
|
219099
|
-
},
|
|
219100
|
-
{
|
|
219101
|
-
type: "text",
|
|
219102
|
-
key: "emailPushDomain",
|
|
219103
|
-
label: "Virtual Domain",
|
|
219104
|
-
labelKey: "provider-reolink:emailPushSettings.s_email-push.emailPushDomain.label",
|
|
219105
|
-
default: DEFAULT_EMAIL_PUSH_DOMAIN,
|
|
219106
|
-
placeholder: DEFAULT_EMAIL_PUSH_DOMAIN
|
|
219107
|
-
}
|
|
219108
|
-
]
|
|
219109
|
-
}, {
|
|
219110
|
-
id: "email-push-auth",
|
|
219111
|
-
title: "Email Push Authentication",
|
|
219112
|
-
titleKey: "provider-reolink:emailPushSettings.s_email-push-auth.title",
|
|
219113
|
-
description: "Random values are generated on first boot; re-run Auto-configure on cameras after changing them.",
|
|
219114
|
-
descriptionKey: "provider-reolink:emailPushSettings.s_email-push-auth.description",
|
|
219115
|
-
columns: 2,
|
|
219116
|
-
fields: [
|
|
219117
|
-
{
|
|
219118
|
-
type: "boolean",
|
|
219119
|
-
key: "emailPushRequireAuth",
|
|
219120
|
-
label: "Require AUTH",
|
|
219121
|
-
labelKey: "provider-reolink:emailPushSettings.s_email-push-auth.emailPushRequireAuth.label",
|
|
219122
|
-
default: true
|
|
219123
|
-
},
|
|
219124
|
-
{
|
|
219125
|
-
type: "text",
|
|
219126
|
-
key: "emailPushAuthUsername",
|
|
219127
|
-
label: "Auth Username",
|
|
219128
|
-
labelKey: "provider-reolink:emailPushSettings.s_email-push-auth.emailPushAuthUsername.label"
|
|
219224
|
+
//#region src/email-push-bridge.ts
|
|
219225
|
+
/**
|
|
219226
|
+
* The Reolink side of the `smtp-inbound` broker kind (D999).
|
|
219227
|
+
*
|
|
219228
|
+
* Battery cameras report motion by mail. The mailbox they mail is a `broker` of kind
|
|
219229
|
+
* `smtp-inbound` owned by `smtp-nodemailer`; this bridge
|
|
219230
|
+
* - creates it on first need (`ensureMailbox`: one in-process promise, adopt-id, never at a bare boot),
|
|
219231
|
+
* - declares the `cam-<stableId>` recipients of the standalone cameras (`sync`),
|
|
219232
|
+
* - turns each `broker.message` into the camera's `handleSimpleEvent({ source: 'email' })`,
|
|
219233
|
+
* - replays what the bus lost from the broker's ring (seq gap, a broker that came back, subscribe).
|
|
219234
|
+
* It never deletes the mailbox: the operator owns it, and the bridge recreates it at the next need.
|
|
219235
|
+
* Events are telemetry (D8); the ring is the RPC reconcile.
|
|
219236
|
+
*/
|
|
219237
|
+
/** The addon that owns `smtp-inbound` mailboxes: routes the id-less calls (`add`). */
|
|
219238
|
+
var SMTP_BROKER_ADDON_ID = "smtp-nodemailer";
|
|
219239
|
+
/** Who this addon is, among the subscribers of a mailbox. */
|
|
219240
|
+
var REOLINK_SUBSCRIBER_ID = "provider-reolink";
|
|
219241
|
+
var MAILBOX_NAME = "Camera mailbox";
|
|
219242
|
+
var RECIPIENT_PREFIX = "cam-";
|
|
219243
|
+
/** The real port: `ctx.api.broker`, every call routed to the addon that owns the kind. */
|
|
219244
|
+
function createBrokerPort(api) {
|
|
219245
|
+
const route = { addonId: SMTP_BROKER_ADDON_ID };
|
|
219246
|
+
return {
|
|
219247
|
+
list: async () => api.broker.list.query({ kind: SMTP_INBOUND_KIND }),
|
|
219248
|
+
add: async ({ id, name, settings }) => api.broker.add.mutate({
|
|
219249
|
+
kind: SMTP_INBOUND_KIND,
|
|
219250
|
+
name,
|
|
219251
|
+
id,
|
|
219252
|
+
settings: { ...settings },
|
|
219253
|
+
...route
|
|
219254
|
+
}),
|
|
219255
|
+
getBrokerConfig: async (id) => api.broker.getBrokerConfig.query({
|
|
219256
|
+
id,
|
|
219257
|
+
...route
|
|
219258
|
+
}),
|
|
219259
|
+
subscribe: async (brokerId, recipients) => api.broker.subscribe.mutate({
|
|
219260
|
+
brokerId,
|
|
219261
|
+
filter: {
|
|
219262
|
+
recipients: [...recipients],
|
|
219263
|
+
subscriberId: REOLINK_SUBSCRIBER_ID
|
|
219129
219264
|
},
|
|
219130
|
-
|
|
219131
|
-
|
|
219132
|
-
|
|
219133
|
-
|
|
219134
|
-
|
|
219135
|
-
|
|
219136
|
-
|
|
219137
|
-
|
|
219138
|
-
|
|
219139
|
-
|
|
219140
|
-
|
|
219141
|
-
|
|
219142
|
-
|
|
219143
|
-
function readString(raw, key, fallback) {
|
|
219144
|
-
const v = raw[key];
|
|
219145
|
-
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
219265
|
+
...route
|
|
219266
|
+
}),
|
|
219267
|
+
unsubscribe: async (brokerId, subscriptionId) => api.broker.unsubscribe.mutate({
|
|
219268
|
+
brokerId,
|
|
219269
|
+
subscriptionId,
|
|
219270
|
+
...route
|
|
219271
|
+
}),
|
|
219272
|
+
getState: async (brokerId, key) => api.broker.getState.query({
|
|
219273
|
+
brokerId,
|
|
219274
|
+
key,
|
|
219275
|
+
...route
|
|
219276
|
+
})
|
|
219277
|
+
};
|
|
219146
219278
|
}
|
|
219147
|
-
|
|
219148
|
-
|
|
219149
|
-
|
|
219279
|
+
var BrokerMessageEventSchema = object({
|
|
219280
|
+
brokerId: string(),
|
|
219281
|
+
subscriptionId: string(),
|
|
219282
|
+
key: string(),
|
|
219283
|
+
payload: unknown()
|
|
219284
|
+
});
|
|
219285
|
+
var ReadyStateEventSchema = object({
|
|
219286
|
+
capName: string(),
|
|
219287
|
+
state: string()
|
|
219288
|
+
});
|
|
219289
|
+
var BrokerStatusEventSchema = object({
|
|
219290
|
+
brokerId: string(),
|
|
219291
|
+
status: string(),
|
|
219292
|
+
error: string().optional()
|
|
219293
|
+
});
|
|
219294
|
+
function errMsg(err) {
|
|
219295
|
+
return err instanceof Error ? err.message : String(err);
|
|
219150
219296
|
}
|
|
219151
|
-
function
|
|
219152
|
-
|
|
219153
|
-
return typeof v === "boolean" ? v : fallback;
|
|
219297
|
+
function isInUse(err) {
|
|
219298
|
+
return /already in use/i.test(errMsg(err));
|
|
219154
219299
|
}
|
|
219155
|
-
|
|
219156
|
-
* Owns the lib's SMTP intake for the Reolink addon. Lifecycle:
|
|
219157
|
-
* - `start()` from the provider's `onInitialize` (no-op when disabled).
|
|
219158
|
-
* - `restart()` from `onConfigChanged` so port/domain/auth edits apply.
|
|
219159
|
-
* - `stop()` from the provider's `onShutdown`.
|
|
219160
|
-
*/
|
|
219161
|
-
var ReolinkEmailPushServer = class {
|
|
219300
|
+
var EmailPushBridge = class {
|
|
219162
219301
|
deps;
|
|
219163
|
-
|
|
219164
|
-
|
|
219165
|
-
|
|
219302
|
+
mailbox = null;
|
|
219303
|
+
brokerId = null;
|
|
219304
|
+
subscriptionId = null;
|
|
219305
|
+
/** The recipient set the broker holds for this addon, as a key. */
|
|
219306
|
+
declared = null;
|
|
219307
|
+
lastSeq = 0;
|
|
219308
|
+
/** The seq baseline of the current mailbox was taken (before the first subscribe). */
|
|
219309
|
+
seeded = false;
|
|
219310
|
+
/** The last failure that was reported, so a mailbox provider that stays away is said once. */
|
|
219311
|
+
lastFailure = null;
|
|
219312
|
+
chain = Promise.resolve();
|
|
219166
219313
|
constructor(deps) {
|
|
219167
219314
|
this.deps = deps;
|
|
219168
219315
|
}
|
|
219169
|
-
/**
|
|
219170
|
-
|
|
219171
|
-
|
|
219172
|
-
|
|
219173
|
-
|
|
219174
|
-
|
|
219175
|
-
|
|
219176
|
-
|
|
219177
|
-
|
|
219178
|
-
|
|
219179
|
-
|
|
219180
|
-
|
|
219181
|
-
|
|
219182
|
-
|
|
219183
|
-
|
|
219184
|
-
|
|
219185
|
-
|
|
219186
|
-
|
|
219187
|
-
|
|
219188
|
-
|
|
219189
|
-
|
|
219190
|
-
|
|
219191
|
-
|
|
219316
|
+
/** The id of the mailbox, creating it when none exists. One in-flight promise. */
|
|
219317
|
+
ensureMailbox() {
|
|
219318
|
+
this.mailbox ??= this.findOrCreate().catch((err) => {
|
|
219319
|
+
this.mailbox = null;
|
|
219320
|
+
throw err;
|
|
219321
|
+
});
|
|
219322
|
+
return this.mailbox;
|
|
219323
|
+
}
|
|
219324
|
+
async findOrCreate() {
|
|
219325
|
+
const found = pickMailbox(await this.deps.port.list());
|
|
219326
|
+
if (found !== null) {
|
|
219327
|
+
await this.deps.pending?.clear();
|
|
219328
|
+
return found;
|
|
219329
|
+
}
|
|
219330
|
+
const pending = await this.deps.pending?.read() ?? null;
|
|
219331
|
+
try {
|
|
219332
|
+
const created = await this.deps.port.add({
|
|
219333
|
+
id: SMTP_INBOUND_DEFAULT_ID,
|
|
219334
|
+
name: MAILBOX_NAME,
|
|
219335
|
+
...pending === null ? {} : { settings: pending }
|
|
219336
|
+
});
|
|
219337
|
+
await this.deps.pending?.clear();
|
|
219338
|
+
this.deps.logger.info("email-push: created the camera mailbox", { meta: {
|
|
219339
|
+
brokerId: created.id,
|
|
219340
|
+
carriedLegacySettings: pending !== null
|
|
219341
|
+
} });
|
|
219342
|
+
return created.id;
|
|
219192
219343
|
} catch (err) {
|
|
219193
|
-
|
|
219344
|
+
if (!isInUse(err)) throw err;
|
|
219345
|
+
const again = pickMailbox(await this.deps.port.list());
|
|
219346
|
+
if (again === null) throw err;
|
|
219347
|
+
await this.deps.pending?.clear();
|
|
219348
|
+
return again;
|
|
219194
219349
|
}
|
|
219195
|
-
this.settings = {
|
|
219196
|
-
enabled: readBoolean(raw, "emailPushEnabled", true),
|
|
219197
|
-
port: readNumber(raw, "emailPushPort", DEFAULT_EMAIL_PUSH_PORT),
|
|
219198
|
-
bindHost: readString(raw, "emailPushBindHost", "0.0.0.0"),
|
|
219199
|
-
domain: readString(raw, "emailPushDomain", DEFAULT_EMAIL_PUSH_DOMAIN),
|
|
219200
|
-
requireAuth: readBoolean(raw, "emailPushRequireAuth", true),
|
|
219201
|
-
authUsername,
|
|
219202
|
-
authPassword,
|
|
219203
|
-
maxMessageBytes: readNumber(raw, "emailPushMaxMessageBytes", DEFAULT_EMAIL_PUSH_MAX_MESSAGE_BYTES)
|
|
219204
|
-
};
|
|
219205
|
-
return this.settings;
|
|
219206
219350
|
}
|
|
219207
|
-
|
|
219208
|
-
|
|
219209
|
-
reloadSettings() {
|
|
219210
|
-
this.settings = void 0;
|
|
219211
|
-
}
|
|
219212
|
-
buildConfig(settings) {
|
|
219213
|
-
return {
|
|
219214
|
-
port: settings.port,
|
|
219215
|
-
bindHost: settings.bindHost,
|
|
219216
|
-
domain: settings.domain,
|
|
219217
|
-
requireAuth: settings.requireAuth,
|
|
219218
|
-
authUsername: settings.authUsername,
|
|
219219
|
-
authPassword: settings.authPassword,
|
|
219220
|
-
tls: false,
|
|
219221
|
-
maxMessageBytes: settings.maxMessageBytes
|
|
219222
|
-
};
|
|
219223
|
-
}
|
|
219224
|
-
/**
|
|
219225
|
-
* Resolve a recipient local-part (`<id>`, already stripped of the
|
|
219226
|
-
* `cam-` prefix by the lib) to a camera id. Returns the matching
|
|
219227
|
-
* camera's `stableId` (which the lib echoes back as `event.cameraId`)
|
|
219228
|
-
* or `undefined` to reject the recipient at RCPT TO.
|
|
219229
|
-
*
|
|
219230
|
-
* Hub-attached children are skipped: they share the parent's TCP
|
|
219231
|
-
* socket and have no independent email path, so accepting one would
|
|
219232
|
-
* silently mis-route. Match is on `stableId` (durable across
|
|
219233
|
-
* re-adoption) — the same value `buildCameraSetupParams` writes into
|
|
219234
|
-
* the camera's email config.
|
|
219235
|
-
*/
|
|
219236
|
-
resolveCameraId(localPart) {
|
|
219237
|
-
const cam = this.findCameraByStableId(localPart);
|
|
219238
|
-
if (!cam || !cam.isEmailPushEligible) return void 0;
|
|
219239
|
-
return localPart;
|
|
219351
|
+
recipients() {
|
|
219352
|
+
return this.deps.listCameras().filter((cam) => cam.isEmailPushEligible).map((cam) => `${RECIPIENT_PREFIX}${cam.stableId}`).sort();
|
|
219240
219353
|
}
|
|
219241
|
-
|
|
219242
|
-
|
|
219354
|
+
enqueue(job) {
|
|
219355
|
+
const run = this.chain.then(job);
|
|
219356
|
+
this.chain = run.then(() => void 0, () => void 0);
|
|
219357
|
+
return run;
|
|
219243
219358
|
}
|
|
219244
|
-
/**
|
|
219245
|
-
*
|
|
219246
|
-
|
|
219247
|
-
|
|
219248
|
-
|
|
219249
|
-
|
|
219250
|
-
|
|
219251
|
-
|
|
219252
|
-
|
|
219253
|
-
|
|
219254
|
-
|
|
219255
|
-
|
|
219256
|
-
|
|
219257
|
-
|
|
219258
|
-
|
|
219259
|
-
|
|
219359
|
+
/**
|
|
219360
|
+
* Declare the recipients of the eligible cameras (again). Cheap when nothing changed.
|
|
219361
|
+
* A failure is logged, never thrown: a camera that cannot be declared does not stop the addon.
|
|
219362
|
+
*/
|
|
219363
|
+
sync(options = {}) {
|
|
219364
|
+
return this.enqueue(async () => {
|
|
219365
|
+
try {
|
|
219366
|
+
await this.doSync(options.force === true);
|
|
219367
|
+
this.lastFailure = null;
|
|
219368
|
+
} catch (err) {
|
|
219369
|
+
this.declared = null;
|
|
219370
|
+
this.subscriptionId = null;
|
|
219371
|
+
const message = errMsg(err);
|
|
219372
|
+
if (message !== this.lastFailure) {
|
|
219373
|
+
this.lastFailure = message;
|
|
219374
|
+
this.deps.logger.warn("email-push: the camera mailbox is not available; battery cameras cannot mail motion until it is", { meta: { error: message } });
|
|
219375
|
+
}
|
|
219260
219376
|
}
|
|
219261
219377
|
});
|
|
219262
|
-
for (const type of mapInferredTypeToSimpleEvents(event.inferredType)) cam.handleSimpleEvent({
|
|
219263
|
-
type,
|
|
219264
|
-
channel: cam.emailPushChannel,
|
|
219265
|
-
timestamp: event.receivedAtMs,
|
|
219266
|
-
source: "email"
|
|
219267
|
-
});
|
|
219268
219378
|
}
|
|
219269
|
-
|
|
219270
|
-
|
|
219271
|
-
|
|
219272
|
-
|
|
219273
|
-
|
|
219274
|
-
this.
|
|
219379
|
+
async doSync(force) {
|
|
219380
|
+
const recipients = this.recipients();
|
|
219381
|
+
if (recipients.length === 0) {
|
|
219382
|
+
if (this.brokerId !== null && this.subscriptionId !== null) await this.deps.port.unsubscribe(this.brokerId, this.subscriptionId);
|
|
219383
|
+
this.subscriptionId = null;
|
|
219384
|
+
this.declared = null;
|
|
219275
219385
|
return;
|
|
219276
219386
|
}
|
|
219277
|
-
|
|
219278
|
-
|
|
219387
|
+
const key = recipients.join(",");
|
|
219388
|
+
if (!force && this.declared === key && this.subscriptionId !== null) return;
|
|
219389
|
+
const brokerId = await this.ensureMailbox();
|
|
219390
|
+
await this.seedBaseline(brokerId);
|
|
219391
|
+
const { subscriptionId } = await this.deps.port.subscribe(brokerId, recipients);
|
|
219392
|
+
this.brokerId = brokerId;
|
|
219393
|
+
this.subscriptionId = subscriptionId;
|
|
219394
|
+
this.declared = key;
|
|
219395
|
+
await this.reconcile();
|
|
219396
|
+
}
|
|
219397
|
+
/**
|
|
219398
|
+
* What the mailbox retains from BEFORE this subscription is old news (a restart of this addon
|
|
219399
|
+
* must not re-fire a quarter hour of alarms). Nothing for our recipients is accepted before the
|
|
219400
|
+
* subscribe, so the mailbox's seq read now is an exact baseline: only what arrives after it is
|
|
219401
|
+
* processed, and a seq that went back means the mailbox restarted.
|
|
219402
|
+
*/
|
|
219403
|
+
async seedBaseline(brokerId) {
|
|
219404
|
+
const since = await this.readSince(brokerId, 0);
|
|
219405
|
+
if (since === null) {
|
|
219406
|
+
if (!this.seeded || this.brokerId !== brokerId) throw new Error("the camera mailbox state could not be read");
|
|
219279
219407
|
return;
|
|
219280
219408
|
}
|
|
219281
|
-
this.
|
|
219282
|
-
|
|
219283
|
-
|
|
219284
|
-
|
|
219285
|
-
|
|
219286
|
-
|
|
219287
|
-
|
|
219288
|
-
|
|
219409
|
+
if (!this.seeded || this.brokerId !== brokerId || since.lastSeq < this.lastSeq) {
|
|
219410
|
+
this.lastSeq = since.lastSeq;
|
|
219411
|
+
this.seeded = true;
|
|
219412
|
+
}
|
|
219413
|
+
}
|
|
219414
|
+
/** A `broker.message` event off the bus. */
|
|
219415
|
+
onMessage(raw) {
|
|
219416
|
+
return this.enqueue(async () => {
|
|
219417
|
+
const event = BrokerMessageEventSchema.safeParse(raw);
|
|
219418
|
+
if (!event.success) return;
|
|
219419
|
+
if (this.brokerId === null) {
|
|
219420
|
+
this.deps.logger.warn("email-push: a mailbox message arrived before the mailbox was known — dropped", { meta: {
|
|
219421
|
+
brokerId: event.data.brokerId,
|
|
219422
|
+
key: event.data.key
|
|
219423
|
+
} });
|
|
219424
|
+
return;
|
|
219425
|
+
}
|
|
219426
|
+
if (event.data.brokerId !== this.brokerId) return;
|
|
219427
|
+
const message = SmtpInboundMessageSchema.safeParse(event.data.payload);
|
|
219428
|
+
if (!message.success) {
|
|
219429
|
+
this.deps.logger.warn("email-push: a mailbox message did not parse — dropped", { meta: { key: event.data.key } });
|
|
219430
|
+
return;
|
|
219431
|
+
}
|
|
219432
|
+
if (message.data.seq <= this.lastSeq) return;
|
|
219433
|
+
if (message.data.seq > this.lastSeq + 1) {
|
|
219434
|
+
await this.reconcile();
|
|
219435
|
+
return;
|
|
219289
219436
|
}
|
|
219437
|
+
this.process(event.data.key, message.data);
|
|
219290
219438
|
});
|
|
219291
|
-
|
|
219292
|
-
|
|
219293
|
-
|
|
219294
|
-
|
|
219295
|
-
|
|
219296
|
-
|
|
219297
|
-
|
|
219298
|
-
|
|
219439
|
+
}
|
|
219440
|
+
/** A `broker.status-changed` event: a mailbox that came back has forgotten its subscriptions. */
|
|
219441
|
+
onStatusChanged(raw) {
|
|
219442
|
+
const event = BrokerStatusEventSchema.safeParse(raw);
|
|
219443
|
+
if (!event.success || event.data.brokerId !== this.brokerId) return Promise.resolve();
|
|
219444
|
+
if (event.data.status !== "connected") {
|
|
219445
|
+
this.deps.logger.warn("email-push: the camera mailbox is not connected", { meta: {
|
|
219446
|
+
status: event.data.status,
|
|
219447
|
+
error: event.data.error
|
|
219299
219448
|
} });
|
|
219300
|
-
|
|
219301
|
-
this.deps.logger.error("email-push: SMTP intake failed to start", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
219302
|
-
this.busOff?.();
|
|
219303
|
-
this.busOff = void 0;
|
|
219304
|
-
this.instance = void 0;
|
|
219449
|
+
return Promise.resolve();
|
|
219305
219450
|
}
|
|
219451
|
+
return this.sync({ force: true });
|
|
219306
219452
|
}
|
|
219307
|
-
/**
|
|
219308
|
-
|
|
219309
|
-
|
|
219310
|
-
|
|
219311
|
-
|
|
219453
|
+
/**
|
|
219454
|
+
* The mailbox provider (any `broker` provider) became ready: the subscription it may have lost
|
|
219455
|
+
* is declared again, and a mailbox that never existed is created if a camera needs one.
|
|
219456
|
+
*/
|
|
219457
|
+
onProviderReady(raw) {
|
|
219458
|
+
const event = ReadyStateEventSchema.safeParse(raw);
|
|
219459
|
+
if (!event.success || event.data.capName !== "broker" || event.data.state !== "ready") return Promise.resolve();
|
|
219460
|
+
return this.sync({ force: true });
|
|
219312
219461
|
}
|
|
219313
|
-
/**
|
|
219314
|
-
|
|
219315
|
-
|
|
219316
|
-
|
|
219317
|
-
|
|
219318
|
-
if (this.
|
|
219319
|
-
|
|
219320
|
-
|
|
219462
|
+
/** Replay what the ring holds after the last seq we processed. */
|
|
219463
|
+
async reconcile() {
|
|
219464
|
+
const id = this.brokerId;
|
|
219465
|
+
if (id === null) return;
|
|
219466
|
+
let since = await this.readSince(id, this.lastSeq);
|
|
219467
|
+
if (since !== null && since.lastSeq < this.lastSeq) {
|
|
219468
|
+
this.lastSeq = 0;
|
|
219469
|
+
since = await this.readSince(id, 0);
|
|
219470
|
+
}
|
|
219471
|
+
if (since === null) return;
|
|
219472
|
+
for (const entry of since.messages) this.process(entry.key, entry.message);
|
|
219473
|
+
}
|
|
219474
|
+
async readSince(id, seq) {
|
|
219475
|
+
try {
|
|
219476
|
+
const raw = await this.deps.port.getState(id, smtpSinceKey(seq));
|
|
219477
|
+
const parsed = SmtpInboundSinceSchema.safeParse(raw);
|
|
219478
|
+
if (parsed.success) return parsed.data;
|
|
219479
|
+
this.deps.logger.warn("email-push: the mailbox replay did not parse", { meta: { brokerId: id } });
|
|
219480
|
+
} catch (err) {
|
|
219481
|
+
this.deps.logger.warn("email-push: the mailbox replay could not be read", { meta: {
|
|
219482
|
+
brokerId: id,
|
|
219483
|
+
error: errMsg(err)
|
|
219484
|
+
} });
|
|
219321
219485
|
}
|
|
219486
|
+
return null;
|
|
219487
|
+
}
|
|
219488
|
+
process(key, message) {
|
|
219489
|
+
if (message.seq <= this.lastSeq) return;
|
|
219490
|
+
this.lastSeq = message.seq;
|
|
219491
|
+
const cam = this.deps.listCameras().find((c) => `${RECIPIENT_PREFIX}${c.stableId}` === key);
|
|
219492
|
+
if (cam === void 0) {
|
|
219493
|
+
this.deps.logger.warn("email-push: mail for an unknown camera — dropping", { meta: {
|
|
219494
|
+
key,
|
|
219495
|
+
seq: message.seq
|
|
219496
|
+
} });
|
|
219497
|
+
return;
|
|
219498
|
+
}
|
|
219499
|
+
const inferred = classifyEmailPushMessage({
|
|
219500
|
+
subject: message.subject,
|
|
219501
|
+
text: message.text
|
|
219502
|
+
});
|
|
219503
|
+
this.deps.logger.info("email-push: motion received from battery camera", {
|
|
219504
|
+
tags: { deviceId: cam.id },
|
|
219505
|
+
meta: {
|
|
219506
|
+
inferredType: inferred,
|
|
219507
|
+
subject: message.subject.slice(0, 80),
|
|
219508
|
+
seq: message.seq
|
|
219509
|
+
}
|
|
219510
|
+
});
|
|
219511
|
+
for (const type of mapInferredTypeToSimpleEvents(inferred)) cam.handleSimpleEvent({
|
|
219512
|
+
type,
|
|
219513
|
+
channel: cam.emailPushChannel,
|
|
219514
|
+
timestamp: message.receivedAt,
|
|
219515
|
+
source: "email"
|
|
219516
|
+
});
|
|
219322
219517
|
}
|
|
219323
219518
|
/**
|
|
219324
|
-
*
|
|
219325
|
-
*
|
|
219326
|
-
* resolvable LAN address (no camera-reachable host), or the camera is
|
|
219327
|
-
* hub-attached / not eligible. The caller passes these straight into
|
|
219328
|
-
* `api.setupEmailPushToManager`.
|
|
219519
|
+
* What a camera needs to mail the mailbox. `null` for a camera that cannot do email push.
|
|
219520
|
+
* Throws the reason when the hub has no internal address: there is no fallback host.
|
|
219329
219521
|
*/
|
|
219330
219522
|
async buildCameraSetupParams(cam) {
|
|
219331
219523
|
if (!cam.isEmailPushEligible) return null;
|
|
219332
|
-
const
|
|
219333
|
-
const
|
|
219334
|
-
if (!
|
|
219524
|
+
const brokerId = await this.ensureMailbox();
|
|
219525
|
+
const parsed = SmtpInboundConfigSchema.safeParse(await this.deps.port.getBrokerConfig(brokerId));
|
|
219526
|
+
if (!parsed.success) throw new Error("The camera mailbox answered a configuration this build cannot read");
|
|
219527
|
+
const config = parsed.data;
|
|
219528
|
+
if (config.host === null) throw new Error(config.hostReason ?? "The hub has no internal address (System → Network)");
|
|
219529
|
+
await this.sync();
|
|
219335
219530
|
return {
|
|
219336
|
-
managerHost,
|
|
219337
|
-
managerPort:
|
|
219338
|
-
domain:
|
|
219339
|
-
recipientLocalPart:
|
|
219340
|
-
...
|
|
219341
|
-
authUsername:
|
|
219342
|
-
authPassword:
|
|
219531
|
+
managerHost: config.host,
|
|
219532
|
+
managerPort: config.port,
|
|
219533
|
+
domain: config.domain,
|
|
219534
|
+
recipientLocalPart: `${RECIPIENT_PREFIX}${cam.stableId}`,
|
|
219535
|
+
...config.requireAuth ? {
|
|
219536
|
+
authUsername: config.username,
|
|
219537
|
+
authPassword: config.password
|
|
219343
219538
|
} : {}
|
|
219344
219539
|
};
|
|
219345
219540
|
}
|
|
219346
219541
|
};
|
|
219542
|
+
function pickMailbox(list) {
|
|
219543
|
+
return list.find((b) => b.id === "smtp-inbound-default")?.id ?? list[0]?.id ?? null;
|
|
219544
|
+
}
|
|
219347
219545
|
//#endregion
|
|
219348
|
-
//#region src/email-push-
|
|
219349
|
-
|
|
219350
|
-
|
|
219351
|
-
|
|
219352
|
-
|
|
219353
|
-
|
|
219354
|
-
|
|
219355
|
-
|
|
219546
|
+
//#region src/email-push-removal-hook.ts
|
|
219547
|
+
/**
|
|
219548
|
+
* A removed camera leaves its `cam-<stableId>` recipient in the mailbox (D865). The addon holds
|
|
219549
|
+
* no per-device DATA for the intake (the mailbox is owned by the mail addon), so removal only
|
|
219550
|
+
* redeclares the recipient set without the camera. Driven by the tombstone, never by absence.
|
|
219551
|
+
*/
|
|
219552
|
+
function createEmailPushRemovalHook(deps) {
|
|
219553
|
+
return {
|
|
219554
|
+
onDeviceRemoved: async () => {
|
|
219555
|
+
await deps.resync();
|
|
219556
|
+
return { counts: { "mailbox-recipients": 0 } };
|
|
219557
|
+
},
|
|
219558
|
+
listDeviceData: async () => []
|
|
219559
|
+
};
|
|
219356
219560
|
}
|
|
219357
219561
|
//#endregion
|
|
219562
|
+
//#region src/migrations/email-push-to-mailbox.ts
|
|
219563
|
+
/** The pending mailbox as kept in the addon store: everything but the password (a keychain secret). */
|
|
219564
|
+
var PendingMailboxSchema = object({
|
|
219565
|
+
port: number().int(),
|
|
219566
|
+
bindHost: string().optional(),
|
|
219567
|
+
domain: string(),
|
|
219568
|
+
requireAuth: boolean(),
|
|
219569
|
+
username: string()
|
|
219570
|
+
});
|
|
219571
|
+
/** The keys an install that ever used the intake has; any one of them means "carry it over". */
|
|
219572
|
+
var LEGACY_KEYS = [
|
|
219573
|
+
"emailPushPort",
|
|
219574
|
+
"emailPushDomain",
|
|
219575
|
+
"emailPushRequireAuth",
|
|
219576
|
+
"emailPushAuthUsername",
|
|
219577
|
+
"emailPushAuthPassword",
|
|
219578
|
+
"emailPushBindHost"
|
|
219579
|
+
];
|
|
219580
|
+
var DEFAULT_PORT = 2525;
|
|
219581
|
+
var DEFAULT_DOMAIN = "camstack.local";
|
|
219582
|
+
function text(stored, key) {
|
|
219583
|
+
const value = stored[key];
|
|
219584
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
219585
|
+
}
|
|
219586
|
+
function portOf(stored, ports) {
|
|
219587
|
+
const value = stored["emailPushPort"];
|
|
219588
|
+
const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
219589
|
+
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= 65535) return numeric;
|
|
219590
|
+
if (value !== void 0) ports.warn(`email-push migration: the legacy port is not usable; using ${DEFAULT_PORT}`);
|
|
219591
|
+
return DEFAULT_PORT;
|
|
219592
|
+
}
|
|
219593
|
+
async function moveEmailPushToMailbox(ports) {
|
|
219594
|
+
const stored = await ports.readStored();
|
|
219595
|
+
if (!LEGACY_KEYS.some((key) => stored[key] !== void 0)) return {
|
|
219596
|
+
outcome: "already-done",
|
|
219597
|
+
summary: { moved: false }
|
|
219598
|
+
};
|
|
219599
|
+
if (stored["emailPushEnabled"] === false) return {
|
|
219600
|
+
outcome: "already-done",
|
|
219601
|
+
summary: {
|
|
219602
|
+
moved: false,
|
|
219603
|
+
disabled: true
|
|
219604
|
+
}
|
|
219605
|
+
};
|
|
219606
|
+
const bindHost = text(stored, "emailPushBindHost");
|
|
219607
|
+
await ports.stashPending({
|
|
219608
|
+
port: portOf(stored, ports),
|
|
219609
|
+
...bindHost === null ? {} : { bindHost },
|
|
219610
|
+
domain: text(stored, "emailPushDomain") ?? DEFAULT_DOMAIN,
|
|
219611
|
+
requireAuth: stored["emailPushRequireAuth"] !== false,
|
|
219612
|
+
username: text(stored, "emailPushAuthUsername") ?? "",
|
|
219613
|
+
password: text(stored, "emailPushAuthPassword") ?? ""
|
|
219614
|
+
});
|
|
219615
|
+
return {
|
|
219616
|
+
outcome: "done",
|
|
219617
|
+
summary: { moved: true }
|
|
219618
|
+
};
|
|
219619
|
+
}
|
|
219620
|
+
/** The `provider-reolink` catalogue (D987): offered to the engine when the addon initialises. */
|
|
219621
|
+
var REOLINK_MIGRATIONS = [{
|
|
219622
|
+
meta: {
|
|
219623
|
+
id: "provider-reolink/2026-11-02-email-push-to-mailbox",
|
|
219624
|
+
owner: "provider-reolink",
|
|
219625
|
+
level: "addon-state",
|
|
219626
|
+
seq: 1,
|
|
219627
|
+
dependsOn: [],
|
|
219628
|
+
store: "settings",
|
|
219629
|
+
scope: "cluster",
|
|
219630
|
+
onFailure: "tolerate",
|
|
219631
|
+
bodyKind: "procedure",
|
|
219632
|
+
description: "Keep the SMTP intake settings (port, domain, credentials) as the pending camera mailbox, created by the bridge when the smtp-inbound broker is reachable, so no camera is reconfigured",
|
|
219633
|
+
fixtureMark: "a setting key the migration does not know"
|
|
219634
|
+
},
|
|
219635
|
+
procedure: moveEmailPushToMailbox
|
|
219636
|
+
}];
|
|
219637
|
+
//#endregion
|
|
219358
219638
|
//#region src/reolink-discovery-map.ts
|
|
219359
219639
|
/**
|
|
219360
219640
|
* Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
|
|
@@ -219574,7 +219854,7 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
219574
219854
|
lastDiscoveryAt: null,
|
|
219575
219855
|
candidateCount: 0,
|
|
219576
219856
|
adoptedCount: 0,
|
|
219577
|
-
lastError: errMsg$
|
|
219857
|
+
lastError: errMsg$3(err)
|
|
219578
219858
|
};
|
|
219579
219859
|
}
|
|
219580
219860
|
},
|
|
@@ -219635,7 +219915,7 @@ function buildReolinkAdoptionProvider(deps) {
|
|
|
219635
219915
|
logger.warn("reolink adopt: failed to adopt channel", { meta: {
|
|
219636
219916
|
childNativeId,
|
|
219637
219917
|
integrationId,
|
|
219638
|
-
error: errMsg$
|
|
219918
|
+
error: errMsg$3(err)
|
|
219639
219919
|
} });
|
|
219640
219920
|
failures++;
|
|
219641
219921
|
}
|
|
@@ -219815,7 +220095,7 @@ async function planHubMigration(saved, ports) {
|
|
|
219815
220095
|
try {
|
|
219816
220096
|
integrations = await ports.listIntegrations();
|
|
219817
220097
|
} catch (err) {
|
|
219818
|
-
ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$
|
|
220098
|
+
ports.logger.warn("Reolink hub migration: integrations unreadable — hubs left in place", { meta: { error: errMsg$3(err) } });
|
|
219819
220099
|
return {
|
|
219820
220100
|
rows: saved.filter((row) => row.type !== DeviceType.Hub),
|
|
219821
220101
|
retirements: []
|
|
@@ -219842,7 +220122,7 @@ async function planHubMigration(saved, ports) {
|
|
|
219842
220122
|
ports.logger.warn("Reolink hub migration failed — the hub device is left in place", {
|
|
219843
220123
|
tags: { deviceId: hub.id },
|
|
219844
220124
|
meta: {
|
|
219845
|
-
error: errMsg$
|
|
220125
|
+
error: errMsg$3(err),
|
|
219846
220126
|
name: hub.name
|
|
219847
220127
|
}
|
|
219848
220128
|
});
|
|
@@ -219880,7 +220160,7 @@ async function retireMigratedHubs(retirements, ports) {
|
|
|
219880
220160
|
} catch (err) {
|
|
219881
220161
|
ports.logger.warn("Reolink hub retirement failed — retried next boot", {
|
|
219882
220162
|
tags: { deviceId: r.hubDeviceId },
|
|
219883
|
-
meta: { error: errMsg$
|
|
220163
|
+
meta: { error: errMsg$3(err) }
|
|
219884
220164
|
});
|
|
219885
220165
|
}
|
|
219886
220166
|
return retired;
|
|
@@ -220126,7 +220406,7 @@ async function enrichHostIdentifiersFromLiveApi(detection, logger) {
|
|
|
220126
220406
|
}
|
|
220127
220407
|
var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
220128
220408
|
/** The email-push SMTP password is hidden state — kept in the keychain (D787). */
|
|
220129
|
-
secretStatePaths = ["emailPushAuthPassword"];
|
|
220409
|
+
secretStatePaths = ["emailPushAuthPassword", "mailboxPendingPassword"];
|
|
220130
220410
|
addonId = REOLINK_ADDON_ID;
|
|
220131
220411
|
providerName = "Reolink";
|
|
220132
220412
|
deviceClasses = {
|
|
@@ -220140,14 +220420,13 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220140
220420
|
* so a Test click and the subsequent Save share one Baichuan login.
|
|
220141
220421
|
*/
|
|
220142
220422
|
autodetectCache = new AutodetectCache();
|
|
220423
|
+
hubManager = null;
|
|
220143
220424
|
/**
|
|
220144
|
-
*
|
|
220145
|
-
*
|
|
220146
|
-
*
|
|
220147
|
-
* lifecycle and restarted on settings change.
|
|
220425
|
+
* The camera mailbox bridge (`email-push-bridge.ts`, D999): battery cameras mail motion to a
|
|
220426
|
+
* mailbox of the `smtp-inbound` broker, the bridge turns each message into the camera's
|
|
220427
|
+
* simple event. Built in `onInitialize`, once the addon context exists.
|
|
220148
220428
|
*/
|
|
220149
|
-
|
|
220150
|
-
emailPushServer;
|
|
220429
|
+
emailBridge;
|
|
220151
220430
|
baichuanWebhookServer;
|
|
220152
220431
|
/** The `log-channels` provider for this process. Stopped on shutdown so a
|
|
220153
220432
|
* diagnostic timer is never the reason a runner refuses to exit. */
|
|
@@ -220179,54 +220458,94 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220179
220458
|
}
|
|
220180
220459
|
/**
|
|
220181
220460
|
* Enumerate this addon's live `ReolinkCamera` instances from the
|
|
220182
|
-
* global device registry. Used by the
|
|
220461
|
+
* global device registry. Used by the camera mailbox bridge to resolve
|
|
220183
220462
|
* recipients + route inbound motion. Returns `[]` when the registry
|
|
220184
220463
|
* isn't available (worker context — providers are hub-only here).
|
|
220185
220464
|
*/
|
|
220186
220465
|
listReolinkCameras() {
|
|
220187
220466
|
const all = this.ctx.kernel.deviceRegistry?.getAll() ?? [];
|
|
220188
|
-
const
|
|
220467
|
+
const emailBridge = this.emailBridge;
|
|
220189
220468
|
const webhookServer = this.baichuanWebhookServer;
|
|
220190
220469
|
const cameras = [];
|
|
220191
220470
|
for (const d of all) if (d instanceof ReolinkCamera) {
|
|
220192
|
-
if (
|
|
220471
|
+
if (emailBridge) d.setEmailPushParamsResolver(() => emailBridge.buildCameraSetupParams(d));
|
|
220193
220472
|
if (webhookServer) d.setBaichuanWebhookParamsResolver(() => webhookServer.buildCameraSetupParams(d));
|
|
220194
220473
|
cameras.push(d);
|
|
220195
220474
|
}
|
|
220196
220475
|
return cameras;
|
|
220197
220476
|
}
|
|
220198
220477
|
/**
|
|
220199
|
-
*
|
|
220200
|
-
*
|
|
220201
|
-
* (needs `this.ctx`) and memoised.
|
|
220478
|
+
* The legacy SMTP AUTH password: hidden state, kept in the keychain (D787). Only the
|
|
220479
|
+
* `email-push-to-mailbox` migration reads it, to carry it into the mailbox.
|
|
220202
220480
|
*/
|
|
220203
|
-
_emailPushAuthUsernameState = null;
|
|
220204
220481
|
_emailPushAuthPasswordState = null;
|
|
220205
|
-
get emailPushAuthUsernameState() {
|
|
220206
|
-
if (!this._emailPushAuthUsernameState) this._emailPushAuthUsernameState = this.state("emailPushAuthUsername", string(), "");
|
|
220207
|
-
return this._emailPushAuthUsernameState;
|
|
220208
|
-
}
|
|
220209
220482
|
get emailPushAuthPasswordState() {
|
|
220210
220483
|
if (!this._emailPushAuthPasswordState) this._emailPushAuthPasswordState = this.state("emailPushAuthPassword", string(), "");
|
|
220211
220484
|
return this._emailPushAuthPasswordState;
|
|
220212
220485
|
}
|
|
220213
|
-
/**
|
|
220214
|
-
|
|
220215
|
-
|
|
220216
|
-
|
|
220486
|
+
/**
|
|
220487
|
+
* The mailbox the old intake described, waiting for a provider to create it: the migration
|
|
220488
|
+
* writes it (no network), the bridge reads it when it creates the mailbox and clears it after.
|
|
220489
|
+
* The password is a keychain secret of its own (`mailboxPendingPassword`).
|
|
220490
|
+
*/
|
|
220491
|
+
_mailboxPendingState = null;
|
|
220492
|
+
_mailboxPendingPasswordState = null;
|
|
220493
|
+
get mailboxPendingState() {
|
|
220494
|
+
this._mailboxPendingState ??= this.state("mailboxPending", PendingMailboxSchema.nullable(), null);
|
|
220495
|
+
return this._mailboxPendingState;
|
|
220496
|
+
}
|
|
220497
|
+
get mailboxPendingPasswordState() {
|
|
220498
|
+
this._mailboxPendingPasswordState ??= this.state("mailboxPendingPassword", string(), "");
|
|
220499
|
+
return this._mailboxPendingPasswordState;
|
|
220500
|
+
}
|
|
220501
|
+
async stashPendingMailbox(settings) {
|
|
220502
|
+
const { password, ...rest } = settings;
|
|
220503
|
+
await this.mailboxPendingPasswordState.set(password);
|
|
220504
|
+
await this.mailboxPendingState.set(rest);
|
|
220505
|
+
}
|
|
220506
|
+
/** The pending settings, or null; throws when the stored password cannot be resolved. */
|
|
220507
|
+
async readPendingMailbox() {
|
|
220508
|
+
const pending = await this.mailboxPendingState.get();
|
|
220509
|
+
if (pending === null) return null;
|
|
220510
|
+
const password = await this.mailboxPendingPasswordState.get();
|
|
220511
|
+
const raw = (await this.resolveGlobalStore())["mailboxPendingPassword"];
|
|
220512
|
+
if (password.length === 0 && raw !== void 0 && raw !== "" && isSecretMarker(raw)) throw new Error("the pending camera mailbox password could not be resolved");
|
|
220513
|
+
return {
|
|
220514
|
+
...pending,
|
|
220515
|
+
password
|
|
220516
|
+
};
|
|
220517
|
+
}
|
|
220518
|
+
async clearPendingMailbox() {
|
|
220519
|
+
if (await this.mailboxPendingState.get() === null) return;
|
|
220520
|
+
await this.mailboxPendingState.set(null);
|
|
220521
|
+
await this.mailboxPendingPasswordState.set("");
|
|
220522
|
+
}
|
|
220523
|
+
/** The camera mailbox bridge, built once. */
|
|
220524
|
+
ensureEmailBridge() {
|
|
220525
|
+
this.emailBridge ??= new EmailPushBridge({
|
|
220526
|
+
port: createBrokerPort(this.ctx.api),
|
|
220217
220527
|
listCameras: () => this.listReolinkCameras(),
|
|
220218
|
-
|
|
220219
|
-
|
|
220220
|
-
|
|
220221
|
-
|
|
220222
|
-
writeStore: async (patch) => {
|
|
220223
|
-
const username = patch["emailPushAuthUsername"];
|
|
220224
|
-
if (typeof username === "string") await this.emailPushAuthUsernameState.set(username);
|
|
220225
|
-
const password = patch["emailPushAuthPassword"];
|
|
220226
|
-
if (typeof password === "string") await this.emailPushAuthPasswordState.set(password);
|
|
220528
|
+
logger: this.ctx.logger.child("email-push"),
|
|
220529
|
+
pending: {
|
|
220530
|
+
read: () => this.readPendingMailbox(),
|
|
220531
|
+
clear: () => this.clearPendingMailbox()
|
|
220227
220532
|
}
|
|
220228
220533
|
});
|
|
220229
|
-
return this.
|
|
220534
|
+
return this.emailBridge;
|
|
220535
|
+
}
|
|
220536
|
+
/**
|
|
220537
|
+
* The stored intake settings the migration carries into the mailbox. A password that is
|
|
220538
|
+
* stored but cannot be resolved (a locked keychain) aborts: it is never read as empty.
|
|
220539
|
+
*/
|
|
220540
|
+
async readLegacyEmailPush() {
|
|
220541
|
+
const raw = await this.resolveGlobalStore();
|
|
220542
|
+
const password = await this.emailPushAuthPasswordState.get();
|
|
220543
|
+
const stored = raw["emailPushAuthPassword"];
|
|
220544
|
+
if (password.length === 0 && stored !== void 0 && stored !== "" && isSecretMarker(stored)) throw new Error("the legacy email-push password could not be resolved");
|
|
220545
|
+
return password.length > 0 ? {
|
|
220546
|
+
...raw,
|
|
220547
|
+
emailPushAuthPassword: password
|
|
220548
|
+
} : raw;
|
|
220230
220549
|
}
|
|
220231
220550
|
/** Lazily build the HaCfg webhook intake, wiring its provider deps. */
|
|
220232
220551
|
ensureBaichuanWebhookServer() {
|
|
@@ -220303,10 +220622,9 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220303
220622
|
}
|
|
220304
220623
|
this.logChannels?.stop();
|
|
220305
220624
|
await this.clipService?.disposeClipStreamDial().catch(() => {});
|
|
220306
|
-
await this.emailPushServer?.stop().catch(() => {});
|
|
220307
220625
|
await this.baichuanWebhookServer?.stop().catch(() => {});
|
|
220308
220626
|
await this.hubManager?.shutdown().catch((err) => {
|
|
220309
|
-
this.ctx.logger.warn("Reolink NVR sessions did not close cleanly", { meta: { error: errMsg$
|
|
220627
|
+
this.ctx.logger.warn("Reolink NVR sessions did not close cleanly", { meta: { error: errMsg$3(err) } });
|
|
220310
220628
|
});
|
|
220311
220629
|
this.hubManager = null;
|
|
220312
220630
|
reolinkHubSessions.clear();
|
|
@@ -220334,7 +220652,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220334
220652
|
}
|
|
220335
220653
|
await manager.reconcile(withSettings);
|
|
220336
220654
|
} catch (err) {
|
|
220337
|
-
this.ctx.logger.warn("Reolink integration reconcile failed — sessions left as they were", { meta: { error: errMsg$
|
|
220655
|
+
this.ctx.logger.warn("Reolink integration reconcile failed — sessions left as they were", { meta: { error: errMsg$3(err) } });
|
|
220338
220656
|
}
|
|
220339
220657
|
}
|
|
220340
220658
|
subscribeIntegrationLifecycle() {
|
|
@@ -220499,9 +220817,6 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220499
220817
|
*/
|
|
220500
220818
|
async onConfigChanged() {
|
|
220501
220819
|
await this.refreshClipRefreshGlobalStore();
|
|
220502
|
-
await this.emailPushServer?.restart().catch((err) => {
|
|
220503
|
-
this.ctx.logger.warn("email-push: restart after settings change failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
220504
|
-
});
|
|
220505
220820
|
await this.baichuanWebhookServer?.restart().catch((err) => {
|
|
220506
220821
|
this.ctx.logger.warn("baichuan-webhook: restart after settings change failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
220507
220822
|
});
|
|
@@ -220521,12 +220836,7 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220521
220836
|
* for core to select from.
|
|
220522
220837
|
*/
|
|
220523
220838
|
globalSettingsSchema() {
|
|
220524
|
-
|
|
220525
|
-
return { sections: [
|
|
220526
|
-
...buildEmailPushSettingsSchema(recommendedHost).sections,
|
|
220527
|
-
...buildBaichuanWebhookSettingsSchema(recommendedHost).sections,
|
|
220528
|
-
...buildClipRefreshSettingsSchema().sections
|
|
220529
|
-
] };
|
|
220839
|
+
return { sections: [...buildBaichuanWebhookSettingsSchema(listLanHosts()[0] ?? "127.0.0.1").sections, ...buildClipRefreshSettingsSchema().sections] };
|
|
220530
220840
|
}
|
|
220531
220841
|
/**
|
|
220532
220842
|
* Per-instance form (D985): an NVR / Home Hub integration shows its connection only; the
|
|
@@ -220545,18 +220855,17 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220545
220855
|
} catch (err) {
|
|
220546
220856
|
this.ctx.logger.warn("Reolink integration form: instance lookup failed — full schema shown", { meta: {
|
|
220547
220857
|
integrationId: instance.integrationId,
|
|
220548
|
-
error: errMsg$
|
|
220858
|
+
error: errMsg$3(err)
|
|
220549
220859
|
} });
|
|
220550
220860
|
return null;
|
|
220551
220861
|
}
|
|
220552
220862
|
}
|
|
220553
220863
|
/**
|
|
220554
|
-
*
|
|
220555
|
-
*
|
|
220556
|
-
* `email-push-schema.ts` / `baichuan-webhook-schema.ts`.
|
|
220864
|
+
* The HaCfg webhook section IS this integration's configuration — one intake serving
|
|
220865
|
+
* every Reolink camera. See `baichuan-webhook-schema.ts`. (The camera mailbox is a broker.)
|
|
220557
220866
|
*/
|
|
220558
220867
|
integrationSettingSections() {
|
|
220559
|
-
return [...
|
|
220868
|
+
return [...BAICHUAN_WEBHOOK_SECTION_IDS];
|
|
220560
220869
|
}
|
|
220561
220870
|
async onInitialize() {
|
|
220562
220871
|
const regs = this.providerRegistrationsFromSuper(await super.onInitialize());
|
|
@@ -220601,9 +220910,8 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220601
220910
|
this.startClipCatalogRefresh(this.clipService);
|
|
220602
220911
|
this.wireClipCatalogRefreshers();
|
|
220603
220912
|
await this.serveClipPlanes(this.clipService);
|
|
220604
|
-
this.
|
|
220605
|
-
|
|
220606
|
-
});
|
|
220913
|
+
await this.runMigrations(defineAddonMigrations(REOLINK_MIGRATIONS, this.migrationPorts()));
|
|
220914
|
+
this.wireEmailBridge();
|
|
220607
220915
|
this.ensureBaichuanWebhookServer().start().catch((err) => {
|
|
220608
220916
|
this.ctx.logger.warn("baichuan-webhook: HTTP intake failed to start at init", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
220609
220917
|
});
|
|
@@ -220613,8 +220921,9 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220613
220921
|
if (typeof data.deviceId !== "number") return;
|
|
220614
220922
|
const dev = this.ctx.kernel.deviceRegistry?.getById(data.deviceId);
|
|
220615
220923
|
if (dev instanceof ReolinkCamera) {
|
|
220616
|
-
const
|
|
220617
|
-
dev.setEmailPushParamsResolver(() =>
|
|
220924
|
+
const emailBridge = this.ensureEmailBridge();
|
|
220925
|
+
dev.setEmailPushParamsResolver(() => emailBridge.buildCameraSetupParams(dev));
|
|
220926
|
+
emailBridge.sync();
|
|
220618
220927
|
const webhookServer = this.ensureBaichuanWebhookServer();
|
|
220619
220928
|
dev.setBaichuanWebhookParamsResolver(() => webhookServer.buildCameraSetupParams(dev));
|
|
220620
220929
|
this.wireClipCatalogRefreshers();
|
|
@@ -220640,8 +220949,36 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
220640
220949
|
capability: deviceAdoptionCapability,
|
|
220641
220950
|
provider: this.buildAdoptionProvider()
|
|
220642
220951
|
});
|
|
220952
|
+
regs.push({
|
|
220953
|
+
capability: deviceRemovalHookCapability,
|
|
220954
|
+
provider: createEmailPushRemovalHook({ resync: () => this.ensureEmailBridge().sync() })
|
|
220955
|
+
});
|
|
220643
220956
|
return regs;
|
|
220644
220957
|
}
|
|
220958
|
+
/** What the catalogue's migration reads and writes (D987). */
|
|
220959
|
+
migrationPorts() {
|
|
220960
|
+
return {
|
|
220961
|
+
readStored: () => this.readLegacyEmailPush(),
|
|
220962
|
+
stashPending: (settings) => this.stashPendingMailbox(settings),
|
|
220963
|
+
warn: (message) => {
|
|
220964
|
+
this.ctx.logger.warn(message);
|
|
220965
|
+
}
|
|
220966
|
+
};
|
|
220967
|
+
}
|
|
220968
|
+
/** Listen to the mailbox's messages and keep the declared recipients current. */
|
|
220969
|
+
wireEmailBridge() {
|
|
220970
|
+
const bridge = this.ensureEmailBridge();
|
|
220971
|
+
this.subscribe({ category: EventCategory.BrokerMessage }, (e) => {
|
|
220972
|
+
bridge.onMessage(e.data);
|
|
220973
|
+
});
|
|
220974
|
+
this.subscribe({ category: EventCategory.BrokerStatusChanged }, (e) => {
|
|
220975
|
+
bridge.onStatusChanged(e.data);
|
|
220976
|
+
});
|
|
220977
|
+
this.subscribe({ category: EventCategory.SystemReadyState }, (e) => {
|
|
220978
|
+
bridge.onProviderReady(e.data);
|
|
220979
|
+
});
|
|
220980
|
+
bridge.sync();
|
|
220981
|
+
}
|
|
220645
220982
|
/** The clip catalog, its thumbnail cache and its two routes (D549). */
|
|
220646
220983
|
clipService = null;
|
|
220647
220984
|
clipRefreshTimer = null;
|
|
@@ -221005,8 +221342,8 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
221005
221342
|
onAfterCreate: async (device) => {
|
|
221006
221343
|
if (device instanceof ReolinkCamera) {
|
|
221007
221344
|
device.adoptApi(detectedApi);
|
|
221008
|
-
const
|
|
221009
|
-
device.setEmailPushParamsResolver(() =>
|
|
221345
|
+
const emailBridge = this.ensureEmailBridge();
|
|
221346
|
+
device.setEmailPushParamsResolver(() => emailBridge.buildCameraSetupParams(device));
|
|
221010
221347
|
const webhookServer = this.ensureBaichuanWebhookServer();
|
|
221011
221348
|
device.setBaichuanWebhookParamsResolver(() => webhookServer.buildCameraSetupParams(device));
|
|
221012
221349
|
}
|