@apocaliss92/nodelink-js 0.4.11 → 0.4.13

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.
@@ -1,3 +1,36 @@
1
+ import {
2
+ ReolinkCgiApi,
3
+ ReolinkHttpClient,
4
+ applyStreamPatch,
5
+ applyXmlTagPatch,
6
+ buildAbilityInfoExtensionXml,
7
+ buildBinaryExtensionXml,
8
+ buildChannelExtensionXml,
9
+ buildFloodlightManualXml,
10
+ buildLoginXml,
11
+ buildLogoutXml,
12
+ buildPreviewStopXml,
13
+ buildPreviewStopXmlV11,
14
+ buildPreviewXml,
15
+ buildPreviewXmlV11,
16
+ buildPtzControlXml,
17
+ buildPtzPresetXml,
18
+ buildPtzPresetXmlV2,
19
+ buildRtspUrl,
20
+ buildSirenManualXml,
21
+ buildSirenTimesXml,
22
+ buildStartZoomFocusXml,
23
+ collectNvrDiagnostics,
24
+ ensureXmlHeader,
25
+ getXmlText,
26
+ normalizeDayNightMode,
27
+ normalizeOpenClose,
28
+ parseRecordingFileName,
29
+ patchNestedTag,
30
+ runAllDiagnosticsConsecutively,
31
+ runMultifocalDiagnosticsConsecutively,
32
+ xmlEscape
33
+ } from "./chunk-2JNXKT3C.js";
1
34
  import {
2
35
  BC_CLASS_FILE_DOWNLOAD,
3
36
  BC_CLASS_LEGACY,
@@ -32,6 +65,7 @@ import {
32
65
  BC_CMD_ID_GET_AUDIO_CFG,
33
66
  BC_CMD_ID_GET_AUDIO_TASK,
34
67
  BC_CMD_ID_GET_AUTO_FOCUS,
68
+ BC_CMD_ID_GET_AUTO_REBOOT,
35
69
  BC_CMD_ID_GET_BATTERY_INFO,
36
70
  BC_CMD_ID_GET_BATTERY_INFO_LIST,
37
71
  BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
@@ -39,6 +73,8 @@ import {
39
73
  BC_CMD_ID_GET_DING_DONG_CFG,
40
74
  BC_CMD_ID_GET_DING_DONG_LIST,
41
75
  BC_CMD_ID_GET_DING_DONG_SILENT,
76
+ BC_CMD_ID_GET_DST,
77
+ BC_CMD_ID_GET_EMAIL,
42
78
  BC_CMD_ID_GET_EMAIL_TASK,
43
79
  BC_CMD_ID_GET_ENC,
44
80
  BC_CMD_ID_GET_FTP_TASK,
@@ -46,6 +82,7 @@ import {
46
82
  BC_CMD_ID_GET_KIT_AP_CFG,
47
83
  BC_CMD_ID_GET_LED_STATE,
48
84
  BC_CMD_ID_GET_MOTION_ALARM,
85
+ BC_CMD_ID_GET_NTP,
49
86
  BC_CMD_ID_GET_ONLINE_USER_LIST,
50
87
  BC_CMD_ID_GET_OSD_DATETIME,
51
88
  BC_CMD_ID_GET_PIR_INFO,
@@ -61,6 +98,7 @@ import {
61
98
  BC_CMD_ID_GET_SUPPORT,
62
99
  BC_CMD_ID_GET_SYSTEM_GENERAL,
63
100
  BC_CMD_ID_GET_TIMELAPSE_CFG,
101
+ BC_CMD_ID_GET_VERSION_INFO,
64
102
  BC_CMD_ID_GET_VIDEO_INPUT,
65
103
  BC_CMD_ID_GET_WHITE_LED,
66
104
  BC_CMD_ID_GET_WIFI,
@@ -83,14 +121,21 @@ import {
83
121
  BC_CMD_ID_SET_AUDIO_CFG,
84
122
  BC_CMD_ID_SET_AUDIO_TASK,
85
123
  BC_CMD_ID_SET_AUTO_FOCUS,
124
+ BC_CMD_ID_SET_AUTO_REBOOT,
86
125
  BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
87
126
  BC_CMD_ID_SET_DING_DONG_CFG,
88
127
  BC_CMD_ID_SET_DING_DONG_SILENT,
128
+ BC_CMD_ID_SET_DST,
129
+ BC_CMD_ID_SET_EMAIL,
130
+ BC_CMD_ID_SET_EMAIL_TASK,
89
131
  BC_CMD_ID_SET_ENC,
90
132
  BC_CMD_ID_SET_LED_STATE,
91
133
  BC_CMD_ID_SET_MOTION_ALARM,
134
+ BC_CMD_ID_SET_NTP,
135
+ BC_CMD_ID_SET_OSD_DATETIME,
92
136
  BC_CMD_ID_SET_PIR_INFO,
93
137
  BC_CMD_ID_SET_PRIVACY_MASK,
138
+ BC_CMD_ID_SET_SYSTEM_GENERAL,
94
139
  BC_CMD_ID_SET_VIDEO_INPUT,
95
140
  BC_CMD_ID_SET_WHITE_LED_STATE,
96
141
  BC_CMD_ID_SET_WHITE_LED_TASK,
@@ -100,6 +145,7 @@ import {
100
145
  BC_CMD_ID_TALK_ABILITY,
101
146
  BC_CMD_ID_TALK_CONFIG,
102
147
  BC_CMD_ID_TALK_RESET,
148
+ BC_CMD_ID_TEST_EMAIL,
103
149
  BC_CMD_ID_UDP_KEEP_ALIVE,
104
150
  BC_CMD_ID_VIDEO,
105
151
  BC_CMD_ID_VIDEO_STOP,
@@ -108,60 +154,29 @@ import {
108
154
  BC_TCP_DEFAULT_PORT,
109
155
  BaichuanVideoStream,
110
156
  BcMediaAnnexBDecoder,
111
- ReolinkCgiApi,
112
- ReolinkHttpClient,
113
157
  __require,
114
158
  aesDecrypt,
115
159
  aesEncrypt,
116
- applyStreamPatch,
117
- applyXmlTagPatch,
118
160
  bcDecrypt,
119
161
  bcEncrypt,
120
162
  bcHeaderHasPayloadOffset,
121
- buildAbilityInfoExtensionXml,
122
- buildBinaryExtensionXml,
123
- buildChannelExtensionXml,
124
- buildFloodlightManualXml,
125
- buildLoginXml,
126
- buildLogoutXml,
127
- buildPreviewStopXml,
128
- buildPreviewStopXmlV11,
129
- buildPreviewXml,
130
- buildPreviewXmlV11,
131
- buildPtzControlXml,
132
- buildPtzPresetXml,
133
- buildPtzPresetXmlV2,
134
- buildRtspUrl,
135
- buildSirenManualXml,
136
- buildSirenTimesXml,
137
- buildStartZoomFocusXml,
138
- collectNvrDiagnostics,
139
163
  convertToAnnexB,
140
164
  convertToAnnexB2,
141
165
  debugLog,
142
166
  deriveAesKey,
143
- ensureXmlHeader,
144
167
  eventTraceLog,
145
168
  extractPpsFromAnnexB,
146
169
  extractSpsFromAnnexB,
147
170
  extractVpsFromAnnexB,
148
- getXmlText,
149
171
  isH265Irap,
150
172
  md5StrModern,
151
- normalizeDayNightMode,
152
173
  normalizeDebugOptions,
153
- normalizeOpenClose,
154
- parseRecordingFileName,
155
- patchNestedTag,
156
174
  recordingsTraceLog,
157
- runAllDiagnosticsConsecutively,
158
- runMultifocalDiagnosticsConsecutively,
159
175
  splitAnnexBToNalPayloads,
160
176
  splitAnnexBToNalPayloads2,
161
177
  talkTraceLog,
162
- traceLog,
163
- xmlEscape
164
- } from "./chunk-EDLMKBG2.js";
178
+ traceLog
179
+ } from "./chunk-C57QV7IL.js";
165
180
 
166
181
  // src/protocol/framing.ts
167
182
  function encodeHeader(h) {
@@ -1870,6 +1885,23 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
1870
1885
  * even if the current client instance is idle/disconnected.
1871
1886
  */
1872
1887
  static streamingRegistry = /* @__PURE__ */ new Map();
1888
+ /**
1889
+ * Per-device set of live BaichuanClient instances.
1890
+ *
1891
+ * Why: when a streaming client unsubscribes (e.g. RTSP grace timer expires
1892
+ * and SocketPool tears the streaming socket down), the global streaming
1893
+ * registry decrements but the GENERAL client of the same device has no
1894
+ * way of knowing — its idle-disconnect timer was last evaluated while
1895
+ * `isDeviceStreamingActive()` was still true (because the streaming socket
1896
+ * was still alive) and wasn't rescheduled. Without this registry the
1897
+ * general socket stays connected, the 60-second session-guard timer keeps
1898
+ * sending getOnlineUserList() to the camera, and a battery camera ends up
1899
+ * waking up every minute (issue #18).
1900
+ *
1901
+ * On streamingRegistry decrement-to-zero we walk this set and kick every
1902
+ * sibling's idle-disconnect timer so it can re-evaluate eligibility.
1903
+ */
1904
+ static deviceClients = /* @__PURE__ */ new Map();
1873
1905
  /**
1874
1906
  * Per-host D2C_DISC backoff state that persists across client instance recreation.
1875
1907
  *
@@ -1984,6 +2016,29 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
1984
2016
  // AlarmEventList (cmdId=33) can be very chatty (often sent every second).
1985
2017
  // Track last per-channel alarm state so we only emit on transitions.
1986
2018
  alarmEventState = /* @__PURE__ */ new Map();
2019
+ /** Whether this instance is currently in BaichuanClient.deviceClients. */
2020
+ registeredInDeviceClients = false;
2021
+ registerInDeviceClients() {
2022
+ if (this.registeredInDeviceClients) return;
2023
+ const key = this.getDeviceRegistryKey();
2024
+ let set = _BaichuanClient.deviceClients.get(key);
2025
+ if (!set) {
2026
+ set = /* @__PURE__ */ new Set();
2027
+ _BaichuanClient.deviceClients.set(key, set);
2028
+ }
2029
+ set.add(this);
2030
+ this.registeredInDeviceClients = true;
2031
+ }
2032
+ unregisterFromDeviceClients() {
2033
+ if (!this.registeredInDeviceClients) return;
2034
+ const key = this.getDeviceRegistryKey();
2035
+ const set = _BaichuanClient.deviceClients.get(key);
2036
+ if (set) {
2037
+ set.delete(this);
2038
+ if (set.size === 0) _BaichuanClient.deviceClients.delete(key);
2039
+ }
2040
+ this.registeredInDeviceClients = false;
2041
+ }
1987
2042
  constructor(options) {
1988
2043
  super();
1989
2044
  this.opts = options;
@@ -1998,6 +2053,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
1998
2053
  code: err?.code
1999
2054
  });
2000
2055
  });
2056
+ this.registerInDeviceClients();
2001
2057
  }
2002
2058
  newSocketSessionId(transport) {
2003
2059
  const short = randomUUID().split("-")[0] ?? randomUUID().slice(0, 8);
@@ -2254,6 +2310,18 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2254
2310
  activeStreamClients: nextCount
2255
2311
  });
2256
2312
  this.contributesToGlobalStreamingRegistry = shouldContribute;
2313
+ if (!shouldContribute && nextCount === 0) {
2314
+ const siblings = _BaichuanClient.deviceClients.get(key);
2315
+ if (siblings) {
2316
+ for (const sib of siblings) {
2317
+ if (sib === this) continue;
2318
+ try {
2319
+ sib.kickIdleDisconnectTimer();
2320
+ } catch {
2321
+ }
2322
+ }
2323
+ }
2324
+ }
2257
2325
  }
2258
2326
  /**
2259
2327
  * True if the device should be considered "awake" due to active streaming.
@@ -2718,6 +2786,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
2718
2786
  `transport=tcp host=${this.opts.host} port=${port}${sid ? ` sid=${sid}` : ""}${remote ? ` remote=${remote}` : ""}${peer ? ` peer=${peer}` : ""}`
2719
2787
  );
2720
2788
  this.logSocketState("tcp_connected");
2789
+ this.registerInDeviceClients();
2721
2790
  this.startKeepAlive();
2722
2791
  this.kickIdleDisconnectTimer();
2723
2792
  }
@@ -3034,6 +3103,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter2 {
3034
3103
  this.logDebug("udp_close_error", e);
3035
3104
  }
3036
3105
  }
3106
+ this.unregisterFromDeviceClients();
3037
3107
  }
3038
3108
  handleFrame(frame) {
3039
3109
  const now = Date.now();
@@ -8275,6 +8345,304 @@ function parseXmlFragmentToJson(xml) {
8275
8345
  return parsed.root;
8276
8346
  }
8277
8347
 
8348
+ // src/reolink/baichuan/utils/email.ts
8349
+ var parseInt01 = (text) => {
8350
+ if (text === void 0) return void 0;
8351
+ return text.trim() === "1" ? 1 : 0;
8352
+ };
8353
+ var parseNumberSafe = (text) => {
8354
+ if (text === void 0) return void 0;
8355
+ const n = Number(text);
8356
+ return Number.isFinite(n) ? n : void 0;
8357
+ };
8358
+ var VALID_ATTACHMENT_TYPES = [
8359
+ "picture",
8360
+ "video",
8361
+ "none"
8362
+ ];
8363
+ var VALID_TEXT_TYPES = ["withText", "noText"];
8364
+ var normalizeAttachmentType = (text) => {
8365
+ const t = (text ?? "").trim();
8366
+ return VALID_ATTACHMENT_TYPES.includes(t) ? t : "picture";
8367
+ };
8368
+ var normalizeTextType = (text) => {
8369
+ const t = (text ?? "").trim();
8370
+ return VALID_TEXT_TYPES.includes(t) ? t : "withText";
8371
+ };
8372
+ function parseEmailConfigFromXml(xml) {
8373
+ const cfg = {
8374
+ smtpServer: getXmlText(xml, "smtpServer") ?? "",
8375
+ userName: getXmlText(xml, "userName") ?? "",
8376
+ password: getXmlText(xml, "password") ?? "",
8377
+ address1: getXmlText(xml, "address1") ?? "",
8378
+ address2: getXmlText(xml, "address2") ?? "",
8379
+ address3: getXmlText(xml, "address3") ?? "",
8380
+ smtpPort: parseNumberSafe(getXmlText(xml, "smtpPort")) ?? 0,
8381
+ sendNickname: getXmlText(xml, "sendNickname") ?? "",
8382
+ attachment: parseInt01(getXmlText(xml, "attachment")) ?? 0,
8383
+ attachmentType: normalizeAttachmentType(getXmlText(xml, "attachmentType")),
8384
+ textType: normalizeTextType(getXmlText(xml, "textType")),
8385
+ ssl: parseInt01(getXmlText(xml, "ssl")) ?? 0,
8386
+ interval: parseNumberSafe(getXmlText(xml, "interval")) ?? 30
8387
+ };
8388
+ const senderMaxLen = parseNumberSafe(getXmlText(xml, "senderMaxLen"));
8389
+ if (senderMaxLen !== void 0) cfg.senderMaxLen = senderMaxLen;
8390
+ const pwdMaxLen = parseNumberSafe(getXmlText(xml, "pwdMaxLen"));
8391
+ if (pwdMaxLen !== void 0) cfg.pwdMaxLen = pwdMaxLen;
8392
+ const ability = parseNumberSafe(getXmlText(xml, "emailAttachAbility"));
8393
+ if (ability !== void 0) cfg.emailAttachAbility = ability;
8394
+ return cfg;
8395
+ }
8396
+ function buildSetEmailXml(current, patch) {
8397
+ const merged = { ...current, ...patch };
8398
+ return ensureXmlHeader(
8399
+ `<body><Email version="1.1"><smtpServer>${xmlEscape(merged.smtpServer)}</smtpServer><userName>${xmlEscape(merged.userName)}</userName><password>${xmlEscape(merged.password)}</password><address1>${xmlEscape(merged.address1)}</address1><address2>${xmlEscape(merged.address2)}</address2><address3>${xmlEscape(merged.address3)}</address3><smtpPort>${merged.smtpPort}</smtpPort><sendNickname>${xmlEscape(merged.sendNickname)}</sendNickname><attachment>${merged.attachment}</attachment><attachmentType>${merged.attachmentType}</attachmentType><textType>${merged.textType}</textType><ssl>${merged.ssl}</ssl><interval>${merged.interval}</interval></Email></body>`
8400
+ );
8401
+ }
8402
+ function parseEmailTaskFromXml(xml) {
8403
+ const channelId = parseNumberSafe(getXmlText(xml, "channelId")) ?? 0;
8404
+ const enable = parseInt01(getXmlText(xml, "enable")) ?? 0;
8405
+ const items = [];
8406
+ const itemRe = /<item>([\s\S]*?)<\/item>/g;
8407
+ let match;
8408
+ while ((match = itemRe.exec(xml)) !== null) {
8409
+ const block = match[1] ?? "";
8410
+ items.push({
8411
+ type: getXmlText(block, "type") ?? "none",
8412
+ valueTable: getXmlText(block, "valueTable") ?? ""
8413
+ });
8414
+ }
8415
+ return { channelId, enable, typeScheduleList: items };
8416
+ }
8417
+ function buildEmailScheduleValueTable(spec) {
8418
+ if (spec.kind === "always") return "1".repeat(168);
8419
+ const grid = new Array(168).fill("0");
8420
+ if (spec.kind === "never") return grid.join("");
8421
+ for (const w of spec.windows) {
8422
+ const startH = Math.max(0, Math.min(24, w.startHour));
8423
+ const endH = Math.max(startH, Math.min(24, w.endHour));
8424
+ for (const d of w.days) {
8425
+ if (d < 0 || d > 6) continue;
8426
+ for (let h = startH; h < endH; h++) {
8427
+ grid[d * 24 + h] = "1";
8428
+ }
8429
+ }
8430
+ }
8431
+ return grid.join("");
8432
+ }
8433
+ function buildSetEmailTaskXml(task) {
8434
+ const items = task.typeScheduleList.map(
8435
+ (item) => `<item><type>${xmlEscape(item.type)}</type><valueTable>${xmlEscape(item.valueTable)}</valueTable></item>`
8436
+ ).join("");
8437
+ return ensureXmlHeader(
8438
+ `<body><EmailTask version="1.1"><channelId>${task.channelId}</channelId><enable>${task.enable}</enable><typeScheduleList>${items}</typeScheduleList></EmailTask></body>`
8439
+ );
8440
+ }
8441
+
8442
+ // src/reolink/baichuan/utils/ntp.ts
8443
+ var parseNumberSafe2 = (text) => {
8444
+ if (text === void 0) return void 0;
8445
+ const n = Number(text);
8446
+ return Number.isFinite(n) ? n : void 0;
8447
+ };
8448
+ var parseInt012 = (text) => {
8449
+ if (text === void 0) return void 0;
8450
+ return text.trim() === "1" ? 1 : 0;
8451
+ };
8452
+ function parseNtpConfigFromXml(xml) {
8453
+ return {
8454
+ enable: parseInt012(getXmlText(xml, "enable")) ?? 0,
8455
+ server: getXmlText(xml, "server") ?? "",
8456
+ synchronizeInterval: parseNumberSafe2(getXmlText(xml, "synchronizeInterval")) ?? 1440,
8457
+ port: parseNumberSafe2(getXmlText(xml, "port")) ?? 123
8458
+ };
8459
+ }
8460
+ function buildSetNtpXml(current, patch) {
8461
+ const merged = { ...current, ...patch };
8462
+ return ensureXmlHeader(
8463
+ `<body><Ntp version="1.1"><enable>${merged.enable}</enable><server>${xmlEscape(merged.server)}</server><synchronizeInterval>${merged.synchronizeInterval}</synchronizeInterval><port>${merged.port}</port></Ntp></body>`
8464
+ );
8465
+ }
8466
+
8467
+ // src/reolink/baichuan/utils/dst.ts
8468
+ var parseNumberSafe3 = (text) => {
8469
+ if (text === void 0) return void 0;
8470
+ const n = Number(text);
8471
+ return Number.isFinite(n) ? n : void 0;
8472
+ };
8473
+ var parseInt013 = (text) => {
8474
+ if (text === void 0) return void 0;
8475
+ return text.trim() === "1" ? 1 : 0;
8476
+ };
8477
+ var VALID_WEEKDAYS = [
8478
+ "Sunday",
8479
+ "Monday",
8480
+ "Tuesday",
8481
+ "Wednesday",
8482
+ "Thursday",
8483
+ "Friday",
8484
+ "Saturday"
8485
+ ];
8486
+ var normalizeWeekday = (text) => {
8487
+ const t = (text ?? "").trim();
8488
+ return VALID_WEEKDAYS.includes(t) ? t : "Sunday";
8489
+ };
8490
+ function parseDstConfigFromXml(xml) {
8491
+ const cfg = {
8492
+ enable: parseInt013(getXmlText(xml, "enable")) ?? 0,
8493
+ offset: parseNumberSafe3(getXmlText(xml, "offset")) ?? 1,
8494
+ startMonth: parseNumberSafe3(getXmlText(xml, "startMonth")) ?? 3,
8495
+ startWeekIndex: parseNumberSafe3(getXmlText(xml, "startWeekIndex")) ?? 5,
8496
+ startWeekday: normalizeWeekday(getXmlText(xml, "startWeekday")),
8497
+ startHour: parseNumberSafe3(getXmlText(xml, "startHour")) ?? 2,
8498
+ startMinute: parseNumberSafe3(getXmlText(xml, "startMinute")) ?? 0,
8499
+ startSecond: parseNumberSafe3(getXmlText(xml, "startSecond")) ?? 0,
8500
+ endMonth: parseNumberSafe3(getXmlText(xml, "endMonth")) ?? 10,
8501
+ endWeekIndex: parseNumberSafe3(getXmlText(xml, "endWeekIndex")) ?? 4,
8502
+ endWeekday: normalizeWeekday(getXmlText(xml, "endWeekday")),
8503
+ endHour: parseNumberSafe3(getXmlText(xml, "endHour")) ?? 3,
8504
+ endMinute: parseNumberSafe3(getXmlText(xml, "endMinute")) ?? 0,
8505
+ endSecond: parseNumberSafe3(getXmlText(xml, "endSecond")) ?? 0
8506
+ };
8507
+ const version = parseNumberSafe3(getXmlText(xml, "version"));
8508
+ if (version !== void 0) cfg.version = version;
8509
+ return cfg;
8510
+ }
8511
+ function buildSetDstXml(current, patch) {
8512
+ const merged = { ...current, ...patch };
8513
+ return ensureXmlHeader(
8514
+ `<body><Dst version="1.1"><enable>${merged.enable}</enable><offset>${merged.offset}</offset><startMonth>${merged.startMonth}</startMonth><startWeekIndex>${merged.startWeekIndex}</startWeekIndex><startWeekday>${xmlEscape(merged.startWeekday)}</startWeekday><startHour>${merged.startHour}</startHour><startMinute>${merged.startMinute}</startMinute><startSecond>${merged.startSecond}</startSecond><endMonth>${merged.endMonth}</endMonth><endWeekIndex>${merged.endWeekIndex}</endWeekIndex><endWeekday>${xmlEscape(merged.endWeekday)}</endWeekday><endHour>${merged.endHour}</endHour><endMinute>${merged.endMinute}</endMinute><endSecond>${merged.endSecond}</endSecond></Dst></body>`
8515
+ );
8516
+ }
8517
+
8518
+ // src/reolink/baichuan/utils/autoReboot.ts
8519
+ var parseNumberSafe4 = (text) => {
8520
+ if (text === void 0) return void 0;
8521
+ const n = Number(text);
8522
+ return Number.isFinite(n) ? n : void 0;
8523
+ };
8524
+ var parseInt014 = (text) => {
8525
+ if (text === void 0) return void 0;
8526
+ return text.trim() === "1" ? 1 : 0;
8527
+ };
8528
+ var VALID_WEEKDAYS2 = [
8529
+ "Sunday",
8530
+ "Monday",
8531
+ "Tuesday",
8532
+ "Wednesday",
8533
+ "Thursday",
8534
+ "Friday",
8535
+ "Saturday",
8536
+ "everyday"
8537
+ ];
8538
+ var normalizeWeekday2 = (text) => {
8539
+ const t = (text ?? "").trim();
8540
+ return VALID_WEEKDAYS2.includes(t) ? t : "Sunday";
8541
+ };
8542
+ function parseAutoRebootFromXml(xml) {
8543
+ return {
8544
+ enable: parseInt014(getXmlText(xml, "enable")) ?? 0,
8545
+ weekDay: normalizeWeekday2(getXmlText(xml, "weekDay")),
8546
+ hour: parseNumberSafe4(getXmlText(xml, "hour")) ?? 0,
8547
+ minute: parseNumberSafe4(getXmlText(xml, "minute")) ?? 0,
8548
+ second: parseNumberSafe4(getXmlText(xml, "second")) ?? 0
8549
+ };
8550
+ }
8551
+ function buildSetAutoRebootXml(current, patch) {
8552
+ const merged = { ...current, ...patch };
8553
+ return ensureXmlHeader(
8554
+ `<body><AutoReboot version="1.1"><enable>${merged.enable}</enable><weekDay>${xmlEscape(merged.weekDay)}</weekDay><hour>${merged.hour}</hour><minute>${merged.minute}</minute><second>${merged.second}</second></AutoReboot></body>`
8555
+ );
8556
+ }
8557
+
8558
+ // src/reolink/baichuan/utils/systemGeneral.ts
8559
+ var parseNumberSafe5 = (text) => {
8560
+ if (text === void 0) return void 0;
8561
+ const n = Number(text);
8562
+ return Number.isFinite(n) ? n : void 0;
8563
+ };
8564
+ var parseInt015 = (text) => {
8565
+ if (text === void 0) return void 0;
8566
+ return text.trim() === "1" ? 1 : 0;
8567
+ };
8568
+ var VALID_OSD_FORMATS = ["DMY", "MDY", "YMD"];
8569
+ var normalizeOsdFormat = (text) => {
8570
+ const t = (text ?? "").trim();
8571
+ return VALID_OSD_FORMATS.includes(t) ? t : "YMD";
8572
+ };
8573
+ function parseSystemGeneralFromXml(xml) {
8574
+ return {
8575
+ timeZone: parseNumberSafe5(getXmlText(xml, "timeZone")) ?? 0,
8576
+ osdFormat: normalizeOsdFormat(getXmlText(xml, "osdFormat")),
8577
+ year: parseNumberSafe5(getXmlText(xml, "year")) ?? 0,
8578
+ month: parseNumberSafe5(getXmlText(xml, "month")) ?? 0,
8579
+ day: parseNumberSafe5(getXmlText(xml, "day")) ?? 0,
8580
+ hour: parseNumberSafe5(getXmlText(xml, "hour")) ?? 0,
8581
+ minute: parseNumberSafe5(getXmlText(xml, "minute")) ?? 0,
8582
+ second: parseNumberSafe5(getXmlText(xml, "second")) ?? 0,
8583
+ deviceId: parseNumberSafe5(getXmlText(xml, "deviceId")) ?? 0,
8584
+ timeFormat: parseInt015(getXmlText(xml, "timeFormat")) ?? 0,
8585
+ language: getXmlText(xml, "language") ?? "English",
8586
+ deviceName: getXmlText(xml, "deviceName") ?? "",
8587
+ loginLock: parseInt015(getXmlText(xml, "loginLock")) ?? 0,
8588
+ lockTime: parseNumberSafe5(getXmlText(xml, "lockTime")) ?? 0,
8589
+ allowedTimes: parseNumberSafe5(getXmlText(xml, "allowedTimes")) ?? 0,
8590
+ isDst: parseInt015(getXmlText(xml, "isDst")) ?? 0
8591
+ };
8592
+ }
8593
+ function buildSetSystemGeneralXml(patch) {
8594
+ const parts = [];
8595
+ const isDeviceNameOnly = patch.deviceName !== void 0 && patch.timeZone === void 0 && patch.osdFormat === void 0 && patch.timeFormat === void 0 && patch.language === void 0 && patch.loginLock === void 0 && patch.lockTime === void 0 && patch.allowedTimes === void 0 && patch.manualTime === void 0;
8596
+ if (isDeviceNameOnly) {
8597
+ parts.push("<year>0</year>");
8598
+ parts.push(`<deviceName>${xmlEscape(patch.deviceName)}</deviceName>`);
8599
+ parts.push("<deviceNameOnly>1</deviceNameOnly>");
8600
+ } else if (patch.manualTime !== void 0) {
8601
+ const mt = patch.manualTime;
8602
+ if (patch.timeZone !== void 0)
8603
+ parts.push(`<timeZone>${patch.timeZone}</timeZone>`);
8604
+ if (patch.osdFormat !== void 0)
8605
+ parts.push(`<osdFormat>${patch.osdFormat}</osdFormat>`);
8606
+ parts.push(`<year>${mt.year}</year>`);
8607
+ parts.push(`<month>${mt.month}</month>`);
8608
+ parts.push(`<day>${mt.day}</day>`);
8609
+ parts.push(`<hour>${mt.hour}</hour>`);
8610
+ parts.push(`<minute>${mt.minute}</minute>`);
8611
+ parts.push(`<second>${mt.second}</second>`);
8612
+ if (patch.timeFormat !== void 0)
8613
+ parts.push(`<timeFormat>${patch.timeFormat}</timeFormat>`);
8614
+ if (patch.language !== void 0)
8615
+ parts.push(`<language>${xmlEscape(patch.language)}</language>`);
8616
+ if (patch.deviceName !== void 0)
8617
+ parts.push(`<deviceName>${xmlEscape(patch.deviceName)}</deviceName>`);
8618
+ if (patch.loginLock !== void 0)
8619
+ parts.push(`<loginLock>${patch.loginLock}</loginLock>`);
8620
+ if (patch.lockTime !== void 0)
8621
+ parts.push(`<lockTime>${patch.lockTime}</lockTime>`);
8622
+ if (patch.allowedTimes !== void 0)
8623
+ parts.push(`<allowedTimes>${patch.allowedTimes}</allowedTimes>`);
8624
+ } else {
8625
+ if (patch.timeZone !== void 0)
8626
+ parts.push(`<timeZone>${patch.timeZone}</timeZone>`);
8627
+ if (patch.osdFormat !== void 0)
8628
+ parts.push(`<osdFormat>${patch.osdFormat}</osdFormat>`);
8629
+ if (patch.timeFormat !== void 0)
8630
+ parts.push(`<timeFormat>${patch.timeFormat}</timeFormat>`);
8631
+ if (patch.language !== void 0)
8632
+ parts.push(`<language>${xmlEscape(patch.language)}</language>`);
8633
+ if (patch.loginLock !== void 0)
8634
+ parts.push(`<loginLock>${patch.loginLock}</loginLock>`);
8635
+ if (patch.lockTime !== void 0)
8636
+ parts.push(`<lockTime>${patch.lockTime}</lockTime>`);
8637
+ if (patch.allowedTimes !== void 0)
8638
+ parts.push(`<allowedTimes>${patch.allowedTimes}</allowedTimes>`);
8639
+ parts.push("<year>0</year>");
8640
+ }
8641
+ return ensureXmlHeader(
8642
+ `<body><SystemGeneral version="1.1">${parts.join("")}</SystemGeneral></body>`
8643
+ );
8644
+ }
8645
+
8278
8646
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
8279
8647
  import { Jimp, JimpMime } from "jimp";
8280
8648
 
@@ -8335,6 +8703,31 @@ var parseAbilityInfoXml = (xml) => {
8335
8703
  return abilities;
8336
8704
  };
8337
8705
 
8706
+ // src/reolink/baichuan/utils/versionInfo.ts
8707
+ function parseVersionInfo(xml) {
8708
+ const out = {};
8709
+ const set = (key) => {
8710
+ const v = getXmlText(xml, key);
8711
+ if (v !== void 0) out[key] = v;
8712
+ };
8713
+ set("name");
8714
+ set("type");
8715
+ set("serialNumber");
8716
+ set("buildDay");
8717
+ set("hardwareVersion");
8718
+ set("cfgVersion");
8719
+ set("firmwareVersion");
8720
+ set("detail");
8721
+ set("IEClient");
8722
+ set("cc3200Version");
8723
+ set("spVersion");
8724
+ set("pakSuffix");
8725
+ set("itemNo");
8726
+ set("aiVersion");
8727
+ set("helpVersion");
8728
+ return out;
8729
+ }
8730
+
8338
8731
  // src/reolink/baichuan/utils/logging.ts
8339
8732
  var formatErrorForLog = (e) => {
8340
8733
  if (e instanceof Error) {
@@ -8589,6 +8982,204 @@ var buildChannelPushDataLogSnapshot = (channelPushData) => {
8589
8982
  return { result: resultObj, storedChannels: Object.keys(resultObj) };
8590
8983
  };
8591
8984
 
8985
+ // src/reolink/baichuan/utils/detection.ts
8986
+ import * as lz4 from "lz4js";
8987
+
8988
+ // src/reolink/baichuan/utils/aiClassMap.ts
8989
+ function type1ToLabel(type1) {
8990
+ switch (type1) {
8991
+ case 1:
8992
+ return "people";
8993
+ case 2:
8994
+ return "vehicle";
8995
+ case 3:
8996
+ return "animal";
8997
+ case 11259375:
8998
+ return "face";
8999
+ default:
9000
+ return "unknown";
9001
+ }
9002
+ }
9003
+
9004
+ // src/reolink/baichuan/utils/detection.ts
9005
+ var MARKER_LENGTH = 8;
9006
+ var IFRAME_PREFIX_LENGTH = 8;
9007
+ var COUNTER_OFFSET = 8;
9008
+ var BASELINE_SIZE = 128;
9009
+ var FRAME_SIZE_TLV = Buffer.from([3, 4, 0]);
9010
+ var LZ4F_MAGIC = Buffer.from([4, 34, 77, 24]);
9011
+ var CONFIDENCE_DIVISOR = 100;
9012
+ var DEFAULT_AI_FRAME_WIDTH = 896;
9013
+ var DEFAULT_AI_FRAME_HEIGHT = 480;
9014
+ var LZ4_DECOMPRESS_MAX = 256 * 1024;
9015
+ function walkBoxes(buf, start, end, type1, type2, out) {
9016
+ let pos = start;
9017
+ while (pos + 3 <= end) {
9018
+ const t = buf[pos];
9019
+ if (t === 0) return;
9020
+ const length = buf[pos + 1] | buf[pos + 2] << 8;
9021
+ const recordEnd = pos + 3 + length;
9022
+ if (recordEnd > end) return;
9023
+ const isBoxType4 = t === 4 && (length === 10 || length === 13 || length === 14);
9024
+ const isBoxType2 = t === 2 && length === 10;
9025
+ if ((isBoxType4 || isBoxType2) && type1 !== 0 && type2 !== 0) {
9026
+ const x1 = buf.readUInt16LE(pos + 3);
9027
+ const y1 = buf.readUInt16LE(pos + 5);
9028
+ const x2 = buf.readUInt16LE(pos + 7);
9029
+ const y2 = buf.readUInt16LE(pos + 9);
9030
+ const conf = buf.readUInt16LE(pos + 11);
9031
+ if (x2 > x1 && y2 > y1) {
9032
+ out.push({ x1, y1, x2, y2, conf, label: type1ToLabel(type1) });
9033
+ }
9034
+ pos = recordEnd;
9035
+ continue;
9036
+ }
9037
+ if (type1 === 255 && type2 === 2 && t === 2 && length >= LZ4F_MAGIC.length && buf[pos + 3] === LZ4F_MAGIC[0] && buf[pos + 4] === LZ4F_MAGIC[1] && buf[pos + 5] === LZ4F_MAGIC[2] && buf[pos + 6] === LZ4F_MAGIC[3]) {
9038
+ try {
9039
+ const decompressed = lz4.decompress(
9040
+ buf.subarray(pos + 3, recordEnd),
9041
+ LZ4_DECOMPRESS_MAX
9042
+ );
9043
+ const decBuf = Buffer.from(decompressed);
9044
+ walkBoxes(decBuf, 0, decBuf.length, 0, 0, out);
9045
+ } catch {
9046
+ }
9047
+ pos = recordEnd;
9048
+ continue;
9049
+ }
9050
+ if (length > 0) {
9051
+ let nextT1 = type1;
9052
+ let nextT2 = type2;
9053
+ if (type1 === 0) nextT1 = t;
9054
+ else if (type2 === 0) nextT2 = t;
9055
+ walkBoxes(buf, pos + 3, recordEnd, nextT1, nextT2, out);
9056
+ }
9057
+ pos = recordEnd;
9058
+ }
9059
+ }
9060
+ function decodeDetectionHeader(raw, frameType) {
9061
+ const markerOffset = frameType === "Iframe" ? IFRAME_PREFIX_LENGTH : 0;
9062
+ const blockLength = raw.length - markerOffset;
9063
+ const empty = {
9064
+ state: "invalid-marker",
9065
+ markerOffset,
9066
+ blockLength,
9067
+ boxes: []
9068
+ };
9069
+ if (blockLength < MARKER_LENGTH) return empty;
9070
+ if (!hasStandardMarker(raw, markerOffset)) return empty;
9071
+ if (blockLength < COUNTER_OFFSET + 4) return empty;
9072
+ const counter = raw.readUInt32LE(markerOffset + COUNTER_OFFSET);
9073
+ const rawBoxes = [];
9074
+ walkBoxes(raw, markerOffset, raw.length, 0, 0, rawBoxes);
9075
+ let aiFrameWidth = DEFAULT_AI_FRAME_WIDTH;
9076
+ let aiFrameHeight = DEFAULT_AI_FRAME_HEIGHT;
9077
+ let frameSizeFound = false;
9078
+ const searchStart = markerOffset + MARKER_LENGTH;
9079
+ for (let i = searchStart; i + 7 <= raw.length; i++) {
9080
+ if (raw[i] === FRAME_SIZE_TLV[0] && raw[i + 1] === FRAME_SIZE_TLV[1] && raw[i + 2] === FRAME_SIZE_TLV[2]) {
9081
+ const w = raw.readUInt16LE(i + 3);
9082
+ const h = raw.readUInt16LE(i + 5);
9083
+ if (w >= 64 && w <= 8192 && h >= 64 && h <= 8192) {
9084
+ aiFrameWidth = w;
9085
+ aiFrameHeight = h;
9086
+ frameSizeFound = true;
9087
+ break;
9088
+ }
9089
+ }
9090
+ }
9091
+ const specificity = {
9092
+ face: 4,
9093
+ animal: 3,
9094
+ people: 2,
9095
+ vehicle: 1,
9096
+ unknown: 0
9097
+ };
9098
+ const dedup = /* @__PURE__ */ new Map();
9099
+ for (const rb of rawBoxes) {
9100
+ if (rb.x2 > aiFrameWidth || rb.y2 > aiFrameHeight) continue;
9101
+ const key = `${rb.x1}_${rb.y1}_${rb.x2}_${rb.y2}`;
9102
+ const prev = dedup.get(key);
9103
+ if (!prev || (specificity[rb.label] ?? 0) > (specificity[prev.label] ?? 0)) {
9104
+ dedup.set(key, rb);
9105
+ }
9106
+ }
9107
+ const boxes = [];
9108
+ for (const rb of dedup.values()) {
9109
+ boxes.push({
9110
+ x: rb.x1 / aiFrameWidth,
9111
+ y: rb.y1 / aiFrameHeight,
9112
+ width: (rb.x2 - rb.x1) / aiFrameWidth,
9113
+ height: (rb.y2 - rb.y1) / aiFrameHeight,
9114
+ ...rb.conf > 0 && rb.conf <= 100 ? { confidence: rb.conf / CONFIDENCE_DIVISOR } : {},
9115
+ ...rb.label !== "unknown" ? { label: rb.label } : {}
9116
+ });
9117
+ }
9118
+ let state;
9119
+ if (boxes.length > 0) state = "overlay-decoded";
9120
+ else if (blockLength === BASELINE_SIZE) state = "no-overlay";
9121
+ else state = "overlay-undecoded";
9122
+ return {
9123
+ state,
9124
+ markerOffset,
9125
+ blockLength,
9126
+ counter,
9127
+ ...frameSizeFound ? { aiFrameWidth, aiFrameHeight } : {},
9128
+ boxes
9129
+ };
9130
+ }
9131
+ function hasStandardMarker(raw, offset) {
9132
+ if (raw.length < offset + MARKER_LENGTH) return false;
9133
+ return raw[offset] === 255 && raw[offset + 2] === 0 && raw[offset + 3] === 1 && raw[offset + 4] === 11 && raw[offset + 5] === 0 && raw[offset + 6] === 1 && raw[offset + 7] === 8;
9134
+ }
9135
+
9136
+ // src/reolink/baichuan/utils/encOptions.ts
9137
+ function buildEncOptions(list, channel) {
9138
+ const result = { channel };
9139
+ const main = aggregateByType(list, "mainStream");
9140
+ const sub = aggregateByType(list, "subStream");
9141
+ const third = aggregateByType(list, "thirdStream");
9142
+ if (main) result.mainStream = main;
9143
+ if (sub) result.subStream = sub;
9144
+ if (third) result.thirdStream = third;
9145
+ return result;
9146
+ }
9147
+ function aggregateByType(list, type) {
9148
+ const seen = /* @__PURE__ */ new Map();
9149
+ for (const stream of list.streams) {
9150
+ for (const eb of stream.encodeTables) {
9151
+ if (eb.type !== type) continue;
9152
+ const mapped = mapEncodeTable(eb);
9153
+ if (!mapped) continue;
9154
+ const key = `${mapped.width}x${mapped.height}`;
9155
+ if (!seen.has(key)) seen.set(key, mapped);
9156
+ }
9157
+ }
9158
+ if (seen.size === 0) return void 0;
9159
+ return {
9160
+ type,
9161
+ resolutions: [...seen.values()],
9162
+ encoderTypes: ["vbr", "cbr"],
9163
+ encoderProfiles: ["high", "main", "baseline"]
9164
+ };
9165
+ }
9166
+ function mapEncodeTable(eb) {
9167
+ if (eb.width == null || eb.height == null) return void 0;
9168
+ const videoEncTypes = (eb.videoEncTypeList ?? (eb.videoEncType != null ? [eb.videoEncType] : [])).map(
9169
+ (t) => t === 0 ? "h264" : t === 1 ? "h265" : void 0
9170
+ ).filter((t) => t !== void 0);
9171
+ return {
9172
+ width: eb.width,
9173
+ height: eb.height,
9174
+ videoEncTypes,
9175
+ ...eb.defaultFramerate != null ? { defaultFramerate: eb.defaultFramerate } : {},
9176
+ ...eb.defaultBitrate != null ? { defaultBitrate: eb.defaultBitrate } : {},
9177
+ ...eb.defaultGop != null ? { defaultGop: eb.defaultGop } : {},
9178
+ framerateOptions: eb.framerateTable ?? [],
9179
+ bitrateOptions: eb.bitrateTable ?? []
9180
+ };
9181
+ }
9182
+
8592
9183
  // src/reolink/baichuan/utils/events.ts
8593
9184
  var mapToSimpleEvent = (event) => {
8594
9185
  const timestamp = event.timestamp ?? Date.now();
@@ -10688,6 +11279,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10688
11279
  sessionGuardIntervalTimer;
10689
11280
  simpleEventListeners = /* @__PURE__ */ new Set();
10690
11281
  simpleEventSubscribed = false;
11282
+ // Detection events are sourced from BcMedia additionalHeader on active video
11283
+ // streams. Unlike simpleEvent, no Baichuan subscribe command is needed — the
11284
+ // data flows whenever a stream is open. Active streams register themselves via
11285
+ // _registerVideoStreamForDetection (called from BaichuanVideoStream.start).
11286
+ detectionEventListeners = /* @__PURE__ */ new Set();
11287
+ detectionEventStreamHooks = /* @__PURE__ */ new Map();
11288
+ // Auto-managed substream for `onObjectDetections` listeners. Reference-counted
11289
+ // by the listener set: the substream is opened on the first listener and torn
11290
+ // down with the last one. Mirrors `onSimpleEvent`'s subscribe/unsubscribe
11291
+ // lifecycle so a caller never has to manage a video stream just to read AI
11292
+ // detections.
11293
+ objectDetectionListeners = /* @__PURE__ */ new Set();
11294
+ objectDetectionStream;
11295
+ objectDetectionStreamStartInFlight;
11296
+ objectDetectionInternalListener;
10691
11297
  simpleEventSubscribeInFlight;
10692
11298
  simpleEventUnsubscribeInFlight;
10693
11299
  simpleEventResubscribeTimer;
@@ -12123,6 +12729,205 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12123
12729
  }
12124
12730
  }
12125
12731
  }
12732
+ /**
12733
+ * Subscribe to per-frame detection events sourced from the BcMedia
12734
+ * `additionalHeader` block on active video streams.
12735
+ *
12736
+ * Mirrors {@link onSimpleEvent} but is fed by the streaming side-channel:
12737
+ * one event fires for every I-frame / P-frame that carries an overlay block.
12738
+ * Coordinates are reported in normalized [0, 1] fractions of the source
12739
+ * frame, so the same box renders correctly on mainStream, subStream, and
12740
+ * externStream.
12741
+ *
12742
+ * Unlike `onSimpleEvent`, no Baichuan subscribe command is involved — events
12743
+ * only flow while a video stream is open. The library hooks every
12744
+ * `BaichuanVideoStream` created via this API for the listener's lifetime.
12745
+ */
12746
+ onDetection(callback) {
12747
+ this.detectionEventListeners.add(callback);
12748
+ }
12749
+ /**
12750
+ * Remove a single detection callback, or all of them if `callback` is omitted.
12751
+ */
12752
+ offDetection(callback) {
12753
+ if (callback) {
12754
+ this.detectionEventListeners.delete(callback);
12755
+ } else {
12756
+ this.detectionEventListeners.clear();
12757
+ }
12758
+ }
12759
+ /**
12760
+ * Subscribe to AI object detections (people / vehicle / animal / face boxes
12761
+ * with class label and confidence) without managing a video stream yourself.
12762
+ *
12763
+ * Mirrors {@link onSimpleEvent} end-to-end: the API opens a dedicated
12764
+ * substream behind the scenes on the first listener, forwards every box-bearing
12765
+ * `additionalHeader` to your callback, and tears the stream down when the last
12766
+ * listener unsubscribes. The substream is the lightest profile (typically
12767
+ * 640×360) so the additional bandwidth/CPU overhead is minimal.
12768
+ *
12769
+ * Each event carries normalized `[0, 1]` box coordinates, a class label, and
12770
+ * a confidence score — render-ready without further conversion.
12771
+ */
12772
+ async onObjectDetections(callback) {
12773
+ this.objectDetectionListeners.add(callback);
12774
+ this.logger.debug?.(
12775
+ `[ReolinkBaichuanApi] onObjectDetections: registering listener (total=${this.objectDetectionListeners.size})`
12776
+ );
12777
+ await this.ensureObjectDetectionStream();
12778
+ }
12779
+ /**
12780
+ * Remove one detection callback, or all of them if `callback` is omitted.
12781
+ * When the last listener is removed the auto-managed substream is closed.
12782
+ */
12783
+ async offObjectDetections(callback) {
12784
+ if (callback) {
12785
+ this.objectDetectionListeners.delete(callback);
12786
+ } else {
12787
+ this.objectDetectionListeners.clear();
12788
+ }
12789
+ if (this.objectDetectionListeners.size === 0) {
12790
+ await this.tearDownObjectDetectionStream();
12791
+ }
12792
+ }
12793
+ async ensureObjectDetectionStream() {
12794
+ if (this.objectDetectionStream) return;
12795
+ if (this.objectDetectionStreamStartInFlight) {
12796
+ await this.objectDetectionStreamStartInFlight;
12797
+ return;
12798
+ }
12799
+ this.objectDetectionStreamStartInFlight = (async () => {
12800
+ const { BaichuanVideoStream: BaichuanVideoStream2 } = await import("./BaichuanVideoStream-HGPU2MZ3.js");
12801
+ const sessionKey = `live:object-detections:ch0:sub`;
12802
+ const dedicated = await this.createDedicatedSession(sessionKey);
12803
+ const stream = new BaichuanVideoStream2({
12804
+ client: dedicated.client,
12805
+ api: this,
12806
+ channel: 0,
12807
+ profile: "sub",
12808
+ logger: this.logger
12809
+ });
12810
+ this.objectDetectionInternalListener = (event) => {
12811
+ for (const cb of this.objectDetectionListeners) {
12812
+ try {
12813
+ void Promise.resolve(cb(event)).catch((e) => {
12814
+ (this.logger.warn ?? this.logger.error).call(
12815
+ this.logger,
12816
+ "[ReolinkBaichuanApi] onObjectDetections handler error",
12817
+ formatErrorForLog(e)
12818
+ );
12819
+ });
12820
+ } catch (e) {
12821
+ (this.logger.warn ?? this.logger.error).call(
12822
+ this.logger,
12823
+ "[ReolinkBaichuanApi] onObjectDetections handler error",
12824
+ formatErrorForLog(e)
12825
+ );
12826
+ }
12827
+ }
12828
+ };
12829
+ this.detectionEventListeners.add(this.objectDetectionInternalListener);
12830
+ try {
12831
+ await stream.start();
12832
+ } catch (e) {
12833
+ if (this.objectDetectionInternalListener) {
12834
+ this.detectionEventListeners.delete(
12835
+ this.objectDetectionInternalListener
12836
+ );
12837
+ this.objectDetectionInternalListener = void 0;
12838
+ }
12839
+ await dedicated.release().catch(() => {
12840
+ });
12841
+ throw e;
12842
+ }
12843
+ this.objectDetectionStream = {
12844
+ stop: () => stream.stop(),
12845
+ release: () => dedicated.release()
12846
+ };
12847
+ this.logger.debug?.(
12848
+ `[ReolinkBaichuanApi] onObjectDetections: substream started (key=${sessionKey})`
12849
+ );
12850
+ })();
12851
+ try {
12852
+ await this.objectDetectionStreamStartInFlight;
12853
+ } finally {
12854
+ this.objectDetectionStreamStartInFlight = void 0;
12855
+ }
12856
+ }
12857
+ async tearDownObjectDetectionStream() {
12858
+ const handle = this.objectDetectionStream;
12859
+ this.objectDetectionStream = void 0;
12860
+ if (this.objectDetectionInternalListener) {
12861
+ this.detectionEventListeners.delete(this.objectDetectionInternalListener);
12862
+ this.objectDetectionInternalListener = void 0;
12863
+ }
12864
+ if (!handle) return;
12865
+ try {
12866
+ await handle.stop();
12867
+ } catch (e) {
12868
+ this.logger.debug?.(
12869
+ `[ReolinkBaichuanApi] onObjectDetections: stream stop error: ${formatErrorForLog(e)}`
12870
+ );
12871
+ }
12872
+ try {
12873
+ await handle.release();
12874
+ } catch (e) {
12875
+ this.logger.debug?.(
12876
+ `[ReolinkBaichuanApi] onObjectDetections: session release error: ${formatErrorForLog(e)}`
12877
+ );
12878
+ }
12879
+ this.logger.debug?.(
12880
+ `[ReolinkBaichuanApi] onObjectDetections: substream torn down`
12881
+ );
12882
+ }
12883
+ /**
12884
+ * Internal: invoked by BaichuanVideoStream when it starts so the API can hook
12885
+ * its `additionalHeader` event. Returns a teardown function the stream calls
12886
+ * on stop. Not intended for direct use by consumers.
12887
+ */
12888
+ _registerVideoStreamForDetection(stream, context) {
12889
+ const listener = (info) => {
12890
+ if (this.detectionEventListeners.size === 0) return;
12891
+ const decoded = decodeDetectionHeader(info.raw, info.frameType);
12892
+ const event = {
12893
+ channel: context.channel,
12894
+ microseconds: info.microseconds,
12895
+ profile: context.profile,
12896
+ boxes: decoded.boxes,
12897
+ ...info.frameWidth !== void 0 ? { frameWidth: info.frameWidth } : {},
12898
+ ...info.frameHeight !== void 0 ? { frameHeight: info.frameHeight } : {},
12899
+ decodeState: decoded.state,
12900
+ rawHeader: info.raw
12901
+ };
12902
+ this.dispatchDetectionEvent(event);
12903
+ };
12904
+ stream.on("additionalHeader", listener);
12905
+ const teardown = () => {
12906
+ stream.off("additionalHeader", listener);
12907
+ this.detectionEventStreamHooks.delete(stream);
12908
+ };
12909
+ this.detectionEventStreamHooks.set(stream, teardown);
12910
+ return teardown;
12911
+ }
12912
+ dispatchDetectionEvent(evt) {
12913
+ for (const cb of this.detectionEventListeners) {
12914
+ try {
12915
+ void Promise.resolve(cb(evt)).catch((e) => {
12916
+ (this.logger.warn ?? this.logger.error).call(
12917
+ this.logger,
12918
+ "[ReolinkBaichuanApi] onDetection handler error",
12919
+ formatErrorForLog(e)
12920
+ );
12921
+ });
12922
+ } catch (e) {
12923
+ (this.logger.warn ?? this.logger.error).call(
12924
+ this.logger,
12925
+ "[ReolinkBaichuanApi] onDetection handler error",
12926
+ formatErrorForLog(e)
12927
+ );
12928
+ }
12929
+ }
12930
+ }
12126
12931
  startSimpleEventResubscribeTimer() {
12127
12932
  if (this.simpleEventResubscribeTimer) return;
12128
12933
  if (this.simpleEventListeners.size === 0) return;
@@ -12505,6 +13310,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12505
13310
  this.stopUdpSleepInference();
12506
13311
  this.stopSimpleEventWatchdog();
12507
13312
  this.stopSimpleEventResubscribeTimer();
13313
+ this.objectDetectionListeners.clear();
13314
+ await this.tearDownObjectDetectionStream().catch(() => {
13315
+ });
12508
13316
  await this.cleanup();
12509
13317
  await this.stopAllActiveStreams();
12510
13318
  await this.cleanupSocketPool();
@@ -12852,6 +13660,53 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12852
13660
  const xml = `<?xml version="1.0" encoding="UTF-8" ?><body><${tag} version="1.1"><enable>${params.enable ? 1 : 0}</enable></${tag}></body>`;
12853
13661
  await this.sendXml({ cmdId: 36, payloadXml: xml });
12854
13662
  }
13663
+ /**
13664
+ * Full port-config setter (cmd_id 36). Patches one or more of the six
13665
+ * service ports the camera serves — Server (Baichuan), HTTP, HTTPS,
13666
+ * RTSP, RTMP, ONVIF. Each entry takes an optional `port` (number) and
13667
+ * `enable` (boolean); fields the caller doesn't pass are left alone.
13668
+ *
13669
+ * Sends one block per port that has any field set, then issues a
13670
+ * single cmd_36 with the merged body. The camera accepts multiple
13671
+ * `<XxxPort>` siblings in the same payload.
13672
+ *
13673
+ * Wire format observed on E1 Zoom:
13674
+ *
13675
+ * <body>
13676
+ * <RtspPort version="1.1">
13677
+ * <rtspPort>554</rtspPort>
13678
+ * <enable>1</enable>
13679
+ * </RtspPort>
13680
+ * <HttpsPort version="1.1">
13681
+ * <enable>0</enable>
13682
+ * </HttpsPort>
13683
+ * ...
13684
+ * </body>
13685
+ */
13686
+ async setPortConfig(patch) {
13687
+ const blocks = [];
13688
+ const append = (tag, portField, cfg) => {
13689
+ if (!cfg) return;
13690
+ if (cfg.port === void 0 && cfg.enable === void 0) return;
13691
+ const inner = [];
13692
+ if (cfg.port !== void 0) {
13693
+ inner.push(`<${portField}>${cfg.port}</${portField}>`);
13694
+ }
13695
+ if (cfg.enable !== void 0) {
13696
+ inner.push(`<enable>${cfg.enable ? 1 : 0}</enable>`);
13697
+ }
13698
+ blocks.push(`<${tag} version="1.1">${inner.join("")}</${tag}>`);
13699
+ };
13700
+ append("ServerPort", "serverPort", patch.server);
13701
+ append("HttpPort", "httpPort", patch.http);
13702
+ append("HttpsPort", "httpsPort", patch.https);
13703
+ append("RtspPort", "rtspPort", patch.rtsp);
13704
+ append("RtmpPort", "rtmpPort", patch.rtmp);
13705
+ append("OnvifPort", "onvifPort", patch.onvif);
13706
+ if (blocks.length === 0) return;
13707
+ const xml = `<?xml version="1.0" encoding="UTF-8" ?><body>${blocks.join("")}</body>`;
13708
+ await this.sendXml({ cmdId: 36, payloadXml: xml });
13709
+ }
12855
13710
  /** GetDevInfo via Baichuan: host cmd_id 80, channel cmd_id 318 */
12856
13711
  async getInfo(channel, options) {
12857
13712
  const req = { cmdId: channel == null ? 80 : 318 };
@@ -16536,6 +17391,27 @@ ${stderr}`)
16536
17391
  );
16537
17392
  }
16538
17393
  }
17394
+ async gotoPtzPreset(arg1, arg2) {
17395
+ const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
17396
+ const presetId = arg2 === void 0 ? arg1 : arg2;
17397
+ const channelId = ch;
17398
+ const payloadXml = buildPtzPresetXmlV2(channelId, presetId, "toPos");
17399
+ const extensionXml = buildChannelExtensionXml(channelId);
17400
+ const frame = await this.client.sendFrame({
17401
+ cmdId: BC_CMD_ID_PTZ_CONTROL_PRESET,
17402
+ channel: ch,
17403
+ channelIdOverride: channelId,
17404
+ extensionXml,
17405
+ payloadXml,
17406
+ messageClass: BC_CLASS_MODERN_24,
17407
+ streamType: 0
17408
+ });
17409
+ if (frame.header.responseCode !== 200) {
17410
+ throw new Error(
17411
+ `PTZ goto preset rejected (response_code ${frame.header.responseCode})`
17412
+ );
17413
+ }
17414
+ }
16539
17415
  async deletePtzPreset(arg1, arg2) {
16540
17416
  const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
16541
17417
  const presetId = arg2 === void 0 ? arg1 : arg2;
@@ -17187,22 +18063,62 @@ ${stderr}`)
17187
18063
  const channel = typeof arg1 === "number" ? arg1 : arg3;
17188
18064
  const enabled = typeof arg1 === "number" ? arg2 : arg1;
17189
18065
  const sensitivity = typeof arg1 === "number" ? arg3 : arg2;
17190
- const ch = this.normalizeChannel(channel);
18066
+ return await this.setMotionAlarmFull({
18067
+ ...channel !== void 0 ? { channel } : {},
18068
+ enabled,
18069
+ ...sensitivity !== void 0 ? { sensitivity } : {}
18070
+ });
18071
+ }
18072
+ /**
18073
+ * Set motion alarm with full control, including the detection-zone grid.
18074
+ *
18075
+ * Wire format observed on E1 Zoom (cmd_id=47 SetMdAlarm body):
18076
+ *
18077
+ * <MD version="1.1">
18078
+ * <channelId>0</channelId>
18079
+ * <enable>1</enable>
18080
+ * <usepir>0</usepir>
18081
+ * <width>60</width> <height>33</height>
18082
+ * <scope>
18083
+ * <columns>96</columns> <rows>64</rows>
18084
+ * <valueTable>{base64 6144-bit bitmap}</valueTable>
18085
+ * </scope>
18086
+ * ... other camera-specific fields ...
18087
+ * </MD>
18088
+ *
18089
+ * We do a read-modify-write of the GET response so any camera-specific
18090
+ * extension fields are preserved untouched. Pass `valueTable` to update
18091
+ * the detection zone — see `encodeMotionScopeBitmap` for the bitmap layout.
18092
+ *
18093
+ * @param channel - 0-based channel
18094
+ * @param enabled - toggle motion detection on/off (optional)
18095
+ * @param sensitivity - 0-50, higher = more sensitive (optional)
18096
+ * @param valueTable - base64-encoded grid bitmap; size must match
18097
+ * `<scope><columns>×<rows></scope>` from the GET (optional)
18098
+ */
18099
+ async setMotionAlarmFull(opts) {
18100
+ const ch = this.normalizeChannel(opts.channel);
17191
18101
  const currentXml = await this.sendXml({
17192
18102
  cmdId: BC_CMD_ID_GET_MOTION_ALARM,
17193
18103
  channel: ch
17194
18104
  });
17195
18105
  let modifiedXml = currentXml;
17196
- if (enabled !== void 0) {
18106
+ if (opts.enabled !== void 0) {
17197
18107
  modifiedXml = modifiedXml.replace(
17198
18108
  /<enable>[^<]*<\/enable>/,
17199
- `<enable>${enabled ? "1" : "0"}</enable>`
18109
+ `<enable>${opts.enabled ? "1" : "0"}</enable>`
17200
18110
  );
17201
18111
  }
17202
- if (sensitivity !== void 0) {
18112
+ if (opts.sensitivity !== void 0) {
17203
18113
  modifiedXml = modifiedXml.replace(
17204
18114
  /<sensitivityDefault>[^<]*<\/sensitivityDefault>/,
17205
- `<sensitivityDefault>${sensitivity}</sensitivityDefault>`
18115
+ `<sensitivityDefault>${opts.sensitivity}</sensitivityDefault>`
18116
+ );
18117
+ }
18118
+ if (opts.valueTable !== void 0) {
18119
+ modifiedXml = modifiedXml.replace(
18120
+ /<valueTable>[^<]*<\/valueTable>/,
18121
+ `<valueTable>${opts.valueTable}</valueTable>`
17206
18122
  );
17207
18123
  }
17208
18124
  await this.sendXml({
@@ -18477,7 +19393,7 @@ ${xml}`
18477
19393
  * @returns Test results for all stream types and profiles
18478
19394
  */
18479
19395
  async testChannelStreams(channel, logger) {
18480
- const { testChannelStreams } = await import("./DiagnosticsTools-RNIDFEJK.js");
19396
+ const { testChannelStreams } = await import("./DiagnosticsTools-BQOWJ23L.js");
18481
19397
  return await testChannelStreams({
18482
19398
  api: this,
18483
19399
  channel: this.normalizeChannel(channel),
@@ -18493,7 +19409,7 @@ ${xml}`
18493
19409
  * @returns Complete diagnostics for all channels and streams
18494
19410
  */
18495
19411
  async collectMultifocalDiagnostics(logger) {
18496
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-RNIDFEJK.js");
19412
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-BQOWJ23L.js");
18497
19413
  return await collectMultifocalDiagnostics({
18498
19414
  api: this,
18499
19415
  logger
@@ -18546,12 +19462,24 @@ ${xml}`
18546
19462
  }
18547
19463
  /**
18548
19464
  * SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
18549
- * unspecified fields. Mirrors reolink_aio's `SetEnc`.
19465
+ * unspecified fields. Mirrors reolink_aio's `SetEnc` plus the additional
19466
+ * `width`/`height`/`encoderType`/`encoderProfile`/`gop`/`thirdStream`
19467
+ * fields observed in the official mobile app (see `pcap/resolution.pcapng`).
19468
+ *
19469
+ * Field meaning per stream:
19470
+ * - `audio` — 0/1 toggle
19471
+ * - `width`/`height` — resolution in pixels. Must be one of the
19472
+ * resolutions returned by {@link getStreamInfoList}.
19473
+ * - `bitRate` — kbps. Must match the table from `getStreamInfoList`.
19474
+ * - `frameRate` — fps. Must match the table from `getStreamInfoList`.
19475
+ * - `videoEncType` — `"h264"` or `"h265"`
19476
+ * - `encoderType` — `"vbr"` or `"cbr"`
19477
+ * - `encoderProfile` — `"high"`, `"main"`, or `"baseline"`
19478
+ * - `gop` — keyframe interval in seconds (sets `<gop><cur>`)
18550
19479
  *
18551
19480
  * @param channel - Channel number (0-based)
18552
- * @param patch - Fields to update on `mainStream` and/or `subStream`,
18553
- * plus a top-level `audio` toggle (0/1). Pass only what you want
18554
- * to change.
19481
+ * @param patch - Fields to update. Pass only the fields you want to change;
19482
+ * everything else is preserved from the device's current configuration.
18555
19483
  */
18556
19484
  async setEnc(channel, patch, options) {
18557
19485
  const ch = this.normalizeChannel(channel);
@@ -18568,6 +19496,7 @@ ${xml}`
18568
19496
  }
18569
19497
  xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
18570
19498
  xml = applyStreamPatch(xml, "subStream", patch.subStream);
19499
+ xml = applyStreamPatch(xml, "thirdStream", patch.thirdStream);
18571
19500
  await this.sendXml({
18572
19501
  cmdId: BC_CMD_ID_SET_ENC,
18573
19502
  channel: ch,
@@ -19175,6 +20104,71 @@ ${xml}`
19175
20104
  `PCAP-derived settings GET failed for cmdId=${params.cmdId}: ${String(lastErr)}`
19176
20105
  );
19177
20106
  }
20107
+ /**
20108
+ * Update the OSD timestamp + channel-name overlay via cmd_id=45
20109
+ * (SetOsdDatetime). The schema is the same `<body><OsdDatetime>` +
20110
+ * `<OsdChannelName>` block returned by `getOsdDatetime` — we
20111
+ * read-modify-write so any extension fields the camera sent are
20112
+ * preserved.
20113
+ *
20114
+ * Position is in **camera pixel coordinates** (e.g. (1,1) for top-left,
20115
+ * not preset strings). Set `enable=0` to hide the overlay; the camera
20116
+ * keeps the stored position so re-enabling later restores it.
20117
+ */
20118
+ async setOsdDatetime(channel, patch, options) {
20119
+ const ch = this.normalizeChannel(channel);
20120
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
20121
+ let xml = await this.sendPcapDerivedSettingsGetXml({
20122
+ cmdId: BC_CMD_ID_GET_OSD_DATETIME,
20123
+ channel: ch,
20124
+ ...timeoutOpts
20125
+ });
20126
+ const patchBlock = (block, fields) => {
20127
+ const start = xml.indexOf(`<${block}`);
20128
+ if (start < 0) return;
20129
+ const end = xml.indexOf(`</${block}>`, start);
20130
+ if (end < 0) return;
20131
+ let body = xml.slice(start, end);
20132
+ for (const [tag, value] of Object.entries(fields)) {
20133
+ if (value === void 0) continue;
20134
+ const raw = typeof value === "boolean" ? value ? "1" : "0" : String(value);
20135
+ const escaped = raw.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
20136
+ if (body.includes(`<${tag}>`)) {
20137
+ body = body.replace(
20138
+ new RegExp(`<${tag}>[^<]*<\\/${tag}>`),
20139
+ `<${tag}>${escaped}</${tag}>`
20140
+ );
20141
+ } else {
20142
+ body += `<${tag}>${escaped}</${tag}>`;
20143
+ }
20144
+ }
20145
+ xml = xml.slice(0, start) + body + xml.slice(end);
20146
+ };
20147
+ if (patch.datetime) {
20148
+ patchBlock("OsdDatetime", {
20149
+ enable: patch.datetime.enable,
20150
+ topLeftX: patch.datetime.topLeftX,
20151
+ topLeftY: patch.datetime.topLeftY,
20152
+ language: patch.datetime.language
20153
+ });
20154
+ }
20155
+ if (patch.channelName) {
20156
+ patchBlock("OsdChannelName", {
20157
+ name: patch.channelName.name,
20158
+ enable: patch.channelName.enable,
20159
+ topLeftX: patch.channelName.topLeftX,
20160
+ topLeftY: patch.channelName.topLeftY,
20161
+ enWatermark: patch.channelName.enWatermark,
20162
+ enBgcolor: patch.channelName.enBgcolor
20163
+ });
20164
+ }
20165
+ await this.sendXml({
20166
+ cmdId: BC_CMD_ID_SET_OSD_DATETIME,
20167
+ channel: ch,
20168
+ payloadXml: ensureXmlHeader(xml),
20169
+ ...timeoutOpts
20170
+ });
20171
+ }
19178
20172
  async getOsdDatetime(channel, options) {
19179
20173
  const rawXml = await this.sendPcapDerivedSettingsGetXml({
19180
20174
  cmdId: BC_CMD_ID_GET_OSD_DATETIME,
@@ -19367,6 +20361,41 @@ ${xml}`
19367
20361
  });
19368
20362
  return { streams };
19369
20363
  }
20364
+ /**
20365
+ * Return the set of values `setEnc` will accept on each stream of `channel`.
20366
+ * Aggregates `getStreamInfoList` (cmd_146) into a UI-friendly shape:
20367
+ * per-stream resolutions with their allowed codecs/framerates/bitrates plus
20368
+ * the enumerated encoder modes/profiles Reolink exposes.
20369
+ *
20370
+ * Useful for populating selectors and validating user input before calling
20371
+ * `setEnc` — picking an unsupported combination causes the camera to reject
20372
+ * the SET_ENC command (responseCode != 200).
20373
+ */
20374
+ async getEncOptions(channel, options) {
20375
+ const list = await this.getStreamInfoList(channel, options);
20376
+ return buildEncOptions(list, channel);
20377
+ }
20378
+ /**
20379
+ * Read the camera's `<VersionInfo>` block (cmd_id=80). Returns the
20380
+ * friendly name, model code (e.g. `"E1 Zoom"`), serial number, firmware
20381
+ * version, hardware revision, build day, AI model bundle version, etc.
20382
+ *
20383
+ * This is the same info the Reolink mobile app shows in "About this
20384
+ * device" — distinct from `getSystemGeneral` (cmd_104) which carries
20385
+ * time/locale.
20386
+ *
20387
+ * No channel parameter: this command is device-global on NVRs/Hubs and
20388
+ * camera-global on standalone cameras. Pass an explicit channel via the
20389
+ * underlying `sendXml` only if a specific firmware demands it (none we've
20390
+ * tested do).
20391
+ */
20392
+ async getVersionInfo(options) {
20393
+ const xml = await this.sendXml({
20394
+ cmdId: BC_CMD_ID_GET_VERSION_INFO,
20395
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
20396
+ });
20397
+ return parseVersionInfo(xml);
20398
+ }
19370
20399
  async getLedState(channel, options) {
19371
20400
  const rawXml = await this.sendPcapDerivedSettingsGetXml({
19372
20401
  cmdId: BC_CMD_ID_GET_LED_STATE,
@@ -19449,7 +20478,279 @@ ${xml}`
19449
20478
  ...channel != null ? { channel } : {},
19450
20479
  ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
19451
20480
  });
19452
- return parseXmlFragmentToJson(xml);
20481
+ return parseEmailTaskFromXml(xml);
20482
+ }
20483
+ /**
20484
+ * SetEmailTask via Baichuan (cmdId=216). Updates the email alarm schedule
20485
+ * (per-event-type 7×24 valueTable + master enable).
20486
+ *
20487
+ * Reolink expects the FULL `typeScheduleList` — pass the array from a prior
20488
+ * GET and only flip the entries you care about. Slots you don't track must
20489
+ * be sent back unchanged to avoid the camera dropping them.
20490
+ */
20491
+ async setEmailTask(channel, task, options) {
20492
+ const ch = this.normalizeChannel(channel);
20493
+ const payloadXml = buildSetEmailTaskXml({ ...task, channelId: ch });
20494
+ await this.sendXml({
20495
+ cmdId: BC_CMD_ID_SET_EMAIL_TASK,
20496
+ channel: ch,
20497
+ payloadXml,
20498
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
20499
+ });
20500
+ }
20501
+ /**
20502
+ * Convenience wrapper that patches the schedule of one or more trigger
20503
+ * types on the camera's EmailTask without touching the others.
20504
+ *
20505
+ * Pass a high-level schedule spec (`always` / `never` / explicit windows)
20506
+ * and the trigger types it should apply to. The method:
20507
+ *
20508
+ * 1. Reads the current EmailTask via GET (so we keep every existing slot).
20509
+ * 2. Builds the new `valueTable` once from `schedule`.
20510
+ * 3. Replaces the `valueTable` of every matching `type` in the list.
20511
+ * 4. Appends entries for any requested type not already present.
20512
+ * 5. Writes the merged list back via SET.
20513
+ *
20514
+ * Returns the list of types that were actually touched.
20515
+ */
20516
+ async patchEmailSchedule(channel, spec, options) {
20517
+ const current = await this.getEmailTask(channel, options);
20518
+ const newValueTable = buildEmailScheduleValueTable(spec.schedule);
20519
+ const targetSet = new Set(spec.types);
20520
+ const touched = [];
20521
+ const updatedList = current.typeScheduleList.map((item) => {
20522
+ if (targetSet.has(item.type)) {
20523
+ touched.push(item.type);
20524
+ return { ...item, valueTable: newValueTable };
20525
+ }
20526
+ return item;
20527
+ });
20528
+ for (const t of spec.types) {
20529
+ if (!current.typeScheduleList.some((item) => item.type === t)) {
20530
+ updatedList.push({ type: t, valueTable: newValueTable });
20531
+ touched.push(t);
20532
+ }
20533
+ }
20534
+ await this.setEmailTask(
20535
+ channel,
20536
+ {
20537
+ channelId: current.channelId,
20538
+ enable: spec.enable ?? current.enable,
20539
+ typeScheduleList: updatedList
20540
+ },
20541
+ options
20542
+ );
20543
+ return { touchedTypes: touched };
20544
+ }
20545
+ // ====================================================================
20546
+ // Email server (cmdId 42/43/141), NTP (38/39), DST (106/107),
20547
+ // SystemGeneral SET (105), AutoReboot (100/101).
20548
+ // Schemas derived from Reolink Client pcap (2026-05-16).
20549
+ // ====================================================================
20550
+ /**
20551
+ * Read the SMTP email configuration (cmdId=42). Returns the full `<Email>`
20552
+ * block including capability hints (`senderMaxLen`, `pwdMaxLen`,
20553
+ * `emailAttachAbility`).
20554
+ */
20555
+ async getEmail(options) {
20556
+ const xml = await this.sendXml({
20557
+ cmdId: BC_CMD_ID_GET_EMAIL,
20558
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
20559
+ });
20560
+ return parseEmailConfigFromXml(xml);
20561
+ }
20562
+ /**
20563
+ * Patch the SMTP email configuration (cmdId=43). Reads the current config
20564
+ * first then merges the patch — Reolink rejects partial `<Email>` blocks.
20565
+ */
20566
+ async setEmail(patch, options) {
20567
+ const current = await this.getEmail(options);
20568
+ const payloadXml = buildSetEmailXml(current, patch);
20569
+ await this.sendXml({
20570
+ cmdId: BC_CMD_ID_SET_EMAIL,
20571
+ payloadXml,
20572
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
20573
+ });
20574
+ }
20575
+ /**
20576
+ * Send a test email using either the current config or an override patch
20577
+ * (cmdId=141). Returns true when the camera reports 200 (test succeeded),
20578
+ * false when it reports 482 (test failed — server unreachable / bad creds).
20579
+ * Other non-200 codes propagate as exceptions via `sendXml`.
20580
+ */
20581
+ async testEmail(patch, options) {
20582
+ const current = await this.getEmail(options);
20583
+ const payloadXml = buildSetEmailXml(current, patch ?? {});
20584
+ const timeoutMs = options?.timeoutMs ?? 6e4;
20585
+ try {
20586
+ await this.sendXml({
20587
+ cmdId: BC_CMD_ID_TEST_EMAIL,
20588
+ payloadXml,
20589
+ timeoutMs
20590
+ });
20591
+ return true;
20592
+ } catch (err) {
20593
+ const msg = err instanceof Error ? err.message : String(err);
20594
+ if (msg.includes("response_code 482") || msg.includes("response_code=482")) {
20595
+ return false;
20596
+ }
20597
+ throw err;
20598
+ }
20599
+ }
20600
+ /**
20601
+ * Read the NTP server configuration (cmdId=38).
20602
+ */
20603
+ async getNtp(options) {
20604
+ const xml = await this.sendXml({
20605
+ cmdId: BC_CMD_ID_GET_NTP,
20606
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
20607
+ });
20608
+ return parseNtpConfigFromXml(xml);
20609
+ }
20610
+ /**
20611
+ * Patch the NTP server configuration (cmdId=39). Reads the current state
20612
+ * first and merges the patch — Reolink rejects partial `<Ntp>` blocks.
20613
+ */
20614
+ async setNtp(patch, options) {
20615
+ const current = await this.getNtp(options);
20616
+ const payloadXml = buildSetNtpXml(current, patch);
20617
+ await this.sendXml({
20618
+ cmdId: BC_CMD_ID_SET_NTP,
20619
+ payloadXml,
20620
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
20621
+ });
20622
+ }
20623
+ /**
20624
+ * Patch SystemGeneral (cmdId=105). Supports partial payloads: include only
20625
+ * the fields you want to change. By default the builder emits `<year>0</year>`
20626
+ * as the "do not set manual clock" marker; pass `manualTime` to actually
20627
+ * set the date/time. Setting only `deviceName` automatically uses the
20628
+ * Reolink Client's `deviceNameOnly=1` shape.
20629
+ */
20630
+ async setSystemGeneral(patch, options) {
20631
+ const payloadXml = buildSetSystemGeneralXml(patch);
20632
+ await this.sendXml({
20633
+ cmdId: BC_CMD_ID_SET_SYSTEM_GENERAL,
20634
+ payloadXml,
20635
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
20636
+ });
20637
+ }
20638
+ /**
20639
+ * Read the Daylight Saving Time configuration (cmdId=106).
20640
+ */
20641
+ async getDst(options) {
20642
+ const xml = await this.sendXml({
20643
+ cmdId: BC_CMD_ID_GET_DST,
20644
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
20645
+ });
20646
+ return parseDstConfigFromXml(xml);
20647
+ }
20648
+ /**
20649
+ * Patch the DST configuration (cmdId=107). Reads the current state first
20650
+ * and merges the patch.
20651
+ */
20652
+ async setDst(patch, options) {
20653
+ const current = await this.getDst(options);
20654
+ const payloadXml = buildSetDstXml(current, patch);
20655
+ await this.sendXml({
20656
+ cmdId: BC_CMD_ID_SET_DST,
20657
+ payloadXml,
20658
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
20659
+ });
20660
+ }
20661
+ /**
20662
+ * Read the auto-reboot schedule (cmdId=101).
20663
+ */
20664
+ async getAutoReboot(options) {
20665
+ const xml = await this.sendXml({
20666
+ cmdId: BC_CMD_ID_GET_AUTO_REBOOT,
20667
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
20668
+ });
20669
+ return parseAutoRebootFromXml(xml);
20670
+ }
20671
+ /**
20672
+ * Patch the auto-reboot schedule (cmdId=100).
20673
+ */
20674
+ async setAutoReboot(patch, options) {
20675
+ const current = await this.getAutoReboot(options);
20676
+ const payloadXml = buildSetAutoRebootXml(current, patch);
20677
+ await this.sendXml({
20678
+ cmdId: BC_CMD_ID_SET_AUTO_REBOOT,
20679
+ payloadXml,
20680
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
20681
+ });
20682
+ }
20683
+ /**
20684
+ * High-level helper that configures the camera to deliver motion alerts via
20685
+ * SMTP to the local nodelink manager. Orchestrates `setEmail` + `setEmailTask`
20686
+ * in a single call so UI code can offer "auto-configure" without juggling
20687
+ * the underlying commands.
20688
+ *
20689
+ * Pass `runTest: true` to also send a test email (cmdId=141). Returns a
20690
+ * structured result describing each leg of the flow so the caller can show
20691
+ * granular feedback.
20692
+ *
20693
+ * @param params Auto-configuration parameters
20694
+ * @param channel Logical channel (default 0). Used for the EmailTask SET.
20695
+ */
20696
+ async setupEmailPushToManager(params, channel, options) {
20697
+ const port = params.managerPort ?? 2525;
20698
+ const domain = params.domain ?? "nodelink.local";
20699
+ const recipient = `${params.recipientLocalPart}@${domain}`;
20700
+ const triggers = params.triggerTypes ?? ["MD", "people", "vehicle"];
20701
+ const attachmentType = params.attachmentType ?? "picture";
20702
+ const interval = params.interval ?? 30;
20703
+ const emailPatch = {
20704
+ smtpServer: params.managerHost,
20705
+ smtpPort: port,
20706
+ userName: params.authUsername ?? recipient,
20707
+ password: params.authPassword ?? "",
20708
+ address1: recipient,
20709
+ address2: "",
20710
+ address3: "",
20711
+ sendNickname: params.sendNickname ?? params.recipientLocalPart,
20712
+ attachment: attachmentType === "none" ? 0 : 1,
20713
+ attachmentType,
20714
+ textType: "withText",
20715
+ ssl: 0,
20716
+ interval
20717
+ };
20718
+ await this.setEmail(emailPatch, options);
20719
+ const fullWeekOn = "1".repeat(168);
20720
+ const current = await this.getEmailTask(channel, options);
20721
+ const triggerSet = new Set(triggers);
20722
+ const touched = [];
20723
+ const updatedList = current.typeScheduleList.map((item) => {
20724
+ if (triggerSet.has(item.type)) {
20725
+ touched.push(item.type);
20726
+ return { ...item, valueTable: fullWeekOn };
20727
+ }
20728
+ return item;
20729
+ });
20730
+ for (const t of triggers) {
20731
+ if (!current.typeScheduleList.some((item) => item.type === t)) {
20732
+ updatedList.push({ type: t, valueTable: fullWeekOn });
20733
+ touched.push(t);
20734
+ }
20735
+ }
20736
+ await this.setEmailTask(
20737
+ channel,
20738
+ {
20739
+ channelId: current.channelId,
20740
+ enable: 1,
20741
+ typeScheduleList: updatedList
20742
+ },
20743
+ options
20744
+ );
20745
+ const result = {
20746
+ setEmail: { applied: true },
20747
+ setEmailTask: { applied: true, touchedTypes: touched }
20748
+ };
20749
+ if (params.runTest) {
20750
+ const ok = await this.testEmail(emailPatch, options);
20751
+ result.testEmail = { success: ok };
20752
+ }
20753
+ return result;
19453
20754
  }
19454
20755
  /**
19455
20756
  * Get siren-on-motion state via AudioTask (cmdId=232).
@@ -19718,7 +21019,7 @@ ${xml}`
19718
21019
  cmdId: BC_CMD_ID_GET_SYSTEM_GENERAL,
19719
21020
  ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
19720
21021
  });
19721
- return parseXmlFragmentToJson(xml);
21022
+ return parseSystemGeneralFromXml(xml);
19722
21023
  }
19723
21024
  /**
19724
21025
  * Get device support/capability flags.
@@ -22560,4 +23861,4 @@ export {
22560
23861
  isTcpFailureThatShouldFallbackToUdp,
22561
23862
  autoDetectDeviceType
22562
23863
  };
22563
- //# sourceMappingURL=chunk-HGQ53FB3.js.map
23864
+ //# sourceMappingURL=chunk-R5AJV73A.js.map