@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.
@@ -60,7 +60,7 @@ var init_urls = __esm({
60
60
  function bcHeaderHasPayloadOffset(messageClass) {
61
61
  return messageClass === BC_CLASS_MODERN_24 || messageClass === BC_CLASS_MODERN_24_ALT || messageClass === BC_CLASS_FILE_DOWNLOAD;
62
62
  }
63
- var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_SET_VIDEO_INPUT, BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_ENC, BC_CMD_ID_SET_ENC, BC_CMD_ID_GET_PRIVACY_MASK, BC_CMD_ID_SET_PRIVACY_MASK, BC_CMD_ID_SET_AI_DENOISE, BC_CMD_ID_SET_LED_STATE, BC_CMD_ID_SET_AUDIO_CFG, BC_CMD_ID_GET_AUTO_FOCUS, BC_CMD_ID_SET_AUTO_FOCUS, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST, BC_CMD_ID_DING_DONG_CTRL, BC_CMD_ID_GET_DING_DONG_LIST, BC_CMD_ID_DING_DONG_OPT, BC_CMD_ID_GET_DING_DONG_CFG, BC_CMD_ID_SET_DING_DONG_CFG, BC_CMD_ID_QUICK_REPLY_PLAY, BC_CMD_ID_GET_DING_DONG_SILENT, BC_CMD_ID_SET_DING_DONG_SILENT;
63
+ var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_SET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_VERSION_INFO, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_SET_VIDEO_INPUT, BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_ENC, BC_CMD_ID_SET_ENC, BC_CMD_ID_GET_PRIVACY_MASK, BC_CMD_ID_SET_PRIVACY_MASK, BC_CMD_ID_SET_AI_DENOISE, BC_CMD_ID_SET_LED_STATE, BC_CMD_ID_SET_AUDIO_CFG, BC_CMD_ID_SET_EMAIL_TASK, BC_CMD_ID_GET_AUTO_FOCUS, BC_CMD_ID_SET_AUTO_FOCUS, BC_CMD_ID_GET_EMAIL, BC_CMD_ID_SET_EMAIL, BC_CMD_ID_TEST_EMAIL, BC_CMD_ID_GET_NTP, BC_CMD_ID_SET_NTP, BC_CMD_ID_SET_SYSTEM_GENERAL, BC_CMD_ID_GET_DST, BC_CMD_ID_SET_DST, BC_CMD_ID_GET_AUTO_REBOOT, BC_CMD_ID_SET_AUTO_REBOOT, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST, BC_CMD_ID_DING_DONG_CTRL, BC_CMD_ID_GET_DING_DONG_LIST, BC_CMD_ID_DING_DONG_OPT, BC_CMD_ID_GET_DING_DONG_CFG, BC_CMD_ID_SET_DING_DONG_CFG, BC_CMD_ID_QUICK_REPLY_PLAY, BC_CMD_ID_GET_DING_DONG_SILENT, BC_CMD_ID_SET_DING_DONG_SILENT;
64
64
  var init_constants = __esm({
65
65
  "src/protocol/constants.ts"() {
66
66
  "use strict";
@@ -126,9 +126,11 @@ var init_constants = __esm({
126
126
  BC_CMD_ID_PING = 93;
127
127
  BC_CMD_ID_CHANNEL_INFO_ALL = 145;
128
128
  BC_CMD_ID_GET_OSD_DATETIME = 44;
129
+ BC_CMD_ID_SET_OSD_DATETIME = 45;
129
130
  BC_CMD_ID_GET_RECORD_CFG = 54;
130
131
  BC_CMD_ID_GET_ABILITY_SUPPORT = 58;
131
132
  BC_CMD_ID_GET_FTP_TASK = 70;
133
+ BC_CMD_ID_GET_VERSION_INFO = 80;
132
134
  BC_CMD_ID_GET_RECORD = 81;
133
135
  BC_CMD_ID_GET_HDD_INFO_LIST = 102;
134
136
  BC_CMD_ID_GET_WIFI_SIGNAL = 115;
@@ -163,8 +165,19 @@ var init_constants = __esm({
163
165
  BC_CMD_ID_SET_AI_DENOISE = 440;
164
166
  BC_CMD_ID_SET_LED_STATE = 209;
165
167
  BC_CMD_ID_SET_AUDIO_CFG = 265;
168
+ BC_CMD_ID_SET_EMAIL_TASK = 216;
166
169
  BC_CMD_ID_GET_AUTO_FOCUS = 224;
167
170
  BC_CMD_ID_SET_AUTO_FOCUS = 225;
171
+ BC_CMD_ID_GET_EMAIL = 42;
172
+ BC_CMD_ID_SET_EMAIL = 43;
173
+ BC_CMD_ID_TEST_EMAIL = 141;
174
+ BC_CMD_ID_GET_NTP = 38;
175
+ BC_CMD_ID_SET_NTP = 39;
176
+ BC_CMD_ID_SET_SYSTEM_GENERAL = 105;
177
+ BC_CMD_ID_GET_DST = 106;
178
+ BC_CMD_ID_SET_DST = 107;
179
+ BC_CMD_ID_GET_AUTO_REBOOT = 101;
180
+ BC_CMD_ID_SET_AUTO_REBOOT = 100;
168
181
  BC_CMD_ID_CMD_123 = 123;
169
182
  BC_CMD_ID_CMD_209 = 209;
170
183
  BC_CMD_ID_CMD_265 = 265;
@@ -488,10 +501,15 @@ var init_BcMediaCodec = __esm({
488
501
  strict;
489
502
  amountSkipped = 0;
490
503
  logger;
504
+ onUnknownChunk;
491
505
  constructor(strict = false, logger) {
492
506
  this.strict = strict;
493
507
  this.logger = logger;
494
508
  }
509
+ /** Register a listener that fires for every unknown chunk before recovery. */
510
+ setUnknownChunkListener(listener) {
511
+ this.onUnknownChunk = listener;
512
+ }
495
513
  /**
496
514
  * Push data into the codec buffer and try to parse complete BcMedia packets.
497
515
  * Returns an array of complete BcMedia packets found.
@@ -552,6 +570,13 @@ var init_BcMediaCodec = __esm({
552
570
  }
553
571
  }
554
572
  if (next > 0) {
573
+ if (this.onUnknownChunk) {
574
+ this.onUnknownChunk({
575
+ magic,
576
+ preview: Buffer.from(this.buffer.subarray(0, Math.min(256, next))),
577
+ skipped: next
578
+ });
579
+ }
555
580
  this.amountSkipped += next;
556
581
  this.buffer = this.buffer.subarray(next);
557
582
  continue;
@@ -1414,6 +1439,10 @@ var init_BcMediaAnnexBDecoder = __esm({
1414
1439
  });
1415
1440
 
1416
1441
  // src/baichuan/stream/BaichuanVideoStream.ts
1442
+ var BaichuanVideoStream_exports = {};
1443
+ __export(BaichuanVideoStream_exports, {
1444
+ BaichuanVideoStream: () => BaichuanVideoStream
1445
+ });
1417
1446
  function removeEmulationPreventionBytes(rbsp) {
1418
1447
  const out = [];
1419
1448
  for (let i = 0; i < rbsp.length; i++) {
@@ -1547,6 +1576,15 @@ var init_BaichuanVideoStream = __esm({
1547
1576
  acceptAnyStreamType;
1548
1577
  lockedChannelId;
1549
1578
  bcMediaCodec;
1579
+ /**
1580
+ * Diagnostic-only accessor for the BcMedia codec. Used by tools that need to
1581
+ * inspect unknown chunks (for example to discover undocumented audio
1582
+ * sub-packets the parser currently skips). Not part of the supported public
1583
+ * surface — do not rely on it in application code.
1584
+ */
1585
+ get _bcMediaCodec() {
1586
+ return this.bcMediaCodec;
1587
+ }
1550
1588
  debugH264LogsLeft;
1551
1589
  debugSavedSamples;
1552
1590
  warnedNonAnnexBOnce = false;
@@ -1578,6 +1616,14 @@ var init_BaichuanVideoStream = __esm({
1578
1616
  // Stateful AES decryptor for fragmented BcMedia packets (full_aes mode)
1579
1617
  // In CFB mode, continuation frames must use the cipher state from previous frames.
1580
1618
  aesStreamDecryptor = null;
1619
+ // Latest frame dimensions reported by BcMedia InfoV1/V2 packets.
1620
+ // Used to attach width/height context to the `additionalHeader` event so
1621
+ // consumers can normalize box coordinates to a fraction of the stream size.
1622
+ latestFrameWidth;
1623
+ latestFrameHeight;
1624
+ // Teardown returned by ReolinkBaichuanApi._registerVideoStreamForDetection.
1625
+ // Called from stop() to detach the detection bridge.
1626
+ detectionTeardown;
1581
1627
  /**
1582
1628
  * Pending startup error stashed when emitSafeError is called before any
1583
1629
  * "error" listener is registered (e.g. camera returns 400 during start()).
@@ -2240,6 +2286,16 @@ var init_BaichuanVideoStream = __esm({
2240
2286
  }
2241
2287
  videoType = detectedCodec;
2242
2288
  }
2289
+ if (media.additionalHeader && media.additionalHeader.length > 0) {
2290
+ this.emit("additionalHeader", {
2291
+ raw: media.additionalHeader,
2292
+ frameType: "Iframe",
2293
+ videoType,
2294
+ microseconds: media.microseconds,
2295
+ ...this.latestFrameWidth !== void 0 ? { frameWidth: this.latestFrameWidth } : {},
2296
+ ...this.latestFrameHeight !== void 0 ? { frameHeight: this.latestFrameHeight } : {}
2297
+ });
2298
+ }
2243
2299
  const annexBData = videoType === "H265" ? convertToAnnexB2(media.data) : convertToAnnexB(media.data);
2244
2300
  const isKeyframe = true;
2245
2301
  maybeCacheParamSets(annexBData, "Iframe", videoType);
@@ -2321,6 +2377,18 @@ var init_BaichuanVideoStream = __esm({
2321
2377
  }
2322
2378
  } else if (media.type === "Pframe") {
2323
2379
  const chunk = media.data;
2380
+ if (media.additionalHeader && media.additionalHeader.length > 0) {
2381
+ const detected = detectVideoCodecFromNal(chunk);
2382
+ const videoTypeForHeader = detected ?? media.videoType;
2383
+ this.emit("additionalHeader", {
2384
+ raw: media.additionalHeader,
2385
+ frameType: "Pframe",
2386
+ videoType: videoTypeForHeader,
2387
+ microseconds: media.microseconds,
2388
+ ...this.latestFrameWidth !== void 0 ? { frameWidth: this.latestFrameWidth } : {},
2389
+ ...this.latestFrameHeight !== void 0 ? { frameHeight: this.latestFrameHeight } : {}
2390
+ });
2391
+ }
2324
2392
  let videoType = media.videoType;
2325
2393
  const detectedCodec = detectVideoCodecFromNal(chunk);
2326
2394
  if (detectedCodec && detectedCodec !== videoType) {
@@ -2363,6 +2431,8 @@ var init_BaichuanVideoStream = __esm({
2363
2431
  this.emit("audioFrame", media.data);
2364
2432
  }
2365
2433
  if (media.type === "InfoV1" || media.type === "InfoV2") {
2434
+ if (media.videoWidth > 0) this.latestFrameWidth = media.videoWidth;
2435
+ if (media.videoHeight > 0) this.latestFrameHeight = media.videoHeight;
2366
2436
  }
2367
2437
  }
2368
2438
  if (totalFramesReceived <= 10 || totalFramesReceived % 20 === 0 && (videoFramesEmitted > 0 || audioFramesEmitted > 0)) {
@@ -2382,6 +2452,12 @@ var init_BaichuanVideoStream = __esm({
2382
2452
  this.client.on("push", this.videoFrameHandler);
2383
2453
  this.active = true;
2384
2454
  this.startWatchdog();
2455
+ if (this.api && typeof this.api._registerVideoStreamForDetection === "function") {
2456
+ this.detectionTeardown = this.api._registerVideoStreamForDetection(this, {
2457
+ channel: this.channel,
2458
+ profile: this.profile
2459
+ });
2460
+ }
2385
2461
  this.lastMediaAtMs = Date.now();
2386
2462
  if (this.api) {
2387
2463
  try {
@@ -2479,6 +2555,13 @@ var init_BaichuanVideoStream = __esm({
2479
2555
  }
2480
2556
  }
2481
2557
  this.active = false;
2558
+ if (this.detectionTeardown) {
2559
+ try {
2560
+ this.detectionTeardown();
2561
+ } catch {
2562
+ }
2563
+ this.detectionTeardown = void 0;
2564
+ }
2482
2565
  this.emit("close");
2483
2566
  }
2484
2567
  isActive() {
@@ -2714,6 +2797,15 @@ function applyXmlTagPatch(xml, tag, value) {
2714
2797
  const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
2715
2798
  return xml.replace(re, `<${tag}>${v}</${tag}>`);
2716
2799
  }
2800
+ function upsertXmlTag(xml, tag, value) {
2801
+ if (value === void 0) return xml;
2802
+ const v = typeof value === "boolean" ? value ? 1 : 0 : value;
2803
+ const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
2804
+ if (re.test(xml)) {
2805
+ return xml.replace(re, `<${tag}>${v}</${tag}>`);
2806
+ }
2807
+ return `${xml}<${tag}>${v}</${tag}>`;
2808
+ }
2717
2809
  function patchNestedTag(xml, parent, child, value) {
2718
2810
  if (value === void 0) return xml;
2719
2811
  const v = typeof value === "boolean" ? value ? 1 : 0 : value;
@@ -2729,6 +2821,15 @@ function applyStreamPatch(xml, streamTag, patch) {
2729
2821
  );
2730
2822
  return xml.replace(re, (_match, open, body, close) => {
2731
2823
  let next = body;
2824
+ if (patch.audio !== void 0) {
2825
+ next = applyXmlTagPatch(next, "audio", patch.audio);
2826
+ }
2827
+ if (patch.width !== void 0) {
2828
+ next = applyXmlTagPatch(next, "width", patch.width);
2829
+ }
2830
+ if (patch.height !== void 0) {
2831
+ next = applyXmlTagPatch(next, "height", patch.height);
2832
+ }
2732
2833
  if (patch.bitRate !== void 0) {
2733
2834
  next = applyXmlTagPatch(next, "bitRate", patch.bitRate);
2734
2835
  }
@@ -2740,6 +2841,23 @@ function applyStreamPatch(xml, streamTag, patch) {
2740
2841
  const intVal = patch.videoEncType === "h265" ? 1 : 0;
2741
2842
  next = applyXmlTagPatch(next, "videoEncType", intVal);
2742
2843
  }
2844
+ if (patch.encoderType !== void 0) {
2845
+ next = upsertXmlTag(next, "encoderType", patch.encoderType);
2846
+ }
2847
+ if (patch.encoderProfile !== void 0) {
2848
+ next = upsertXmlTag(next, "encoderProfile", patch.encoderProfile);
2849
+ }
2850
+ if (patch.gop !== void 0) {
2851
+ const gopBlockRe = /(<gop[^>]*>)([\s\S]*?)(<\/gop>)/;
2852
+ if (gopBlockRe.test(next)) {
2853
+ next = next.replace(
2854
+ gopBlockRe,
2855
+ (_m, gOpen, gBody, gClose) => `${gOpen}${applyXmlTagPatch(gBody, "cur", patch.gop)}${gClose}`
2856
+ );
2857
+ } else {
2858
+ next = `${next}<gop><cur>${patch.gop}</cur></gop>`;
2859
+ }
2860
+ }
2743
2861
  return `${open}${next}${close}`;
2744
2862
  });
2745
2863
  }
@@ -12313,6 +12431,23 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
12313
12431
  * even if the current client instance is idle/disconnected.
12314
12432
  */
12315
12433
  static streamingRegistry = /* @__PURE__ */ new Map();
12434
+ /**
12435
+ * Per-device set of live BaichuanClient instances.
12436
+ *
12437
+ * Why: when a streaming client unsubscribes (e.g. RTSP grace timer expires
12438
+ * and SocketPool tears the streaming socket down), the global streaming
12439
+ * registry decrements but the GENERAL client of the same device has no
12440
+ * way of knowing — its idle-disconnect timer was last evaluated while
12441
+ * `isDeviceStreamingActive()` was still true (because the streaming socket
12442
+ * was still alive) and wasn't rescheduled. Without this registry the
12443
+ * general socket stays connected, the 60-second session-guard timer keeps
12444
+ * sending getOnlineUserList() to the camera, and a battery camera ends up
12445
+ * waking up every minute (issue #18).
12446
+ *
12447
+ * On streamingRegistry decrement-to-zero we walk this set and kick every
12448
+ * sibling's idle-disconnect timer so it can re-evaluate eligibility.
12449
+ */
12450
+ static deviceClients = /* @__PURE__ */ new Map();
12316
12451
  /**
12317
12452
  * Per-host D2C_DISC backoff state that persists across client instance recreation.
12318
12453
  *
@@ -12427,6 +12562,29 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
12427
12562
  // AlarmEventList (cmdId=33) can be very chatty (often sent every second).
12428
12563
  // Track last per-channel alarm state so we only emit on transitions.
12429
12564
  alarmEventState = /* @__PURE__ */ new Map();
12565
+ /** Whether this instance is currently in BaichuanClient.deviceClients. */
12566
+ registeredInDeviceClients = false;
12567
+ registerInDeviceClients() {
12568
+ if (this.registeredInDeviceClients) return;
12569
+ const key = this.getDeviceRegistryKey();
12570
+ let set = _BaichuanClient.deviceClients.get(key);
12571
+ if (!set) {
12572
+ set = /* @__PURE__ */ new Set();
12573
+ _BaichuanClient.deviceClients.set(key, set);
12574
+ }
12575
+ set.add(this);
12576
+ this.registeredInDeviceClients = true;
12577
+ }
12578
+ unregisterFromDeviceClients() {
12579
+ if (!this.registeredInDeviceClients) return;
12580
+ const key = this.getDeviceRegistryKey();
12581
+ const set = _BaichuanClient.deviceClients.get(key);
12582
+ if (set) {
12583
+ set.delete(this);
12584
+ if (set.size === 0) _BaichuanClient.deviceClients.delete(key);
12585
+ }
12586
+ this.registeredInDeviceClients = false;
12587
+ }
12430
12588
  constructor(options) {
12431
12589
  super();
12432
12590
  this.opts = options;
@@ -12441,6 +12599,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
12441
12599
  code: err?.code
12442
12600
  });
12443
12601
  });
12602
+ this.registerInDeviceClients();
12444
12603
  }
12445
12604
  newSocketSessionId(transport) {
12446
12605
  const short = (0, import_node_crypto2.randomUUID)().split("-")[0] ?? (0, import_node_crypto2.randomUUID)().slice(0, 8);
@@ -12697,6 +12856,18 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
12697
12856
  activeStreamClients: nextCount
12698
12857
  });
12699
12858
  this.contributesToGlobalStreamingRegistry = shouldContribute;
12859
+ if (!shouldContribute && nextCount === 0) {
12860
+ const siblings = _BaichuanClient.deviceClients.get(key);
12861
+ if (siblings) {
12862
+ for (const sib of siblings) {
12863
+ if (sib === this) continue;
12864
+ try {
12865
+ sib.kickIdleDisconnectTimer();
12866
+ } catch {
12867
+ }
12868
+ }
12869
+ }
12870
+ }
12700
12871
  }
12701
12872
  /**
12702
12873
  * True if the device should be considered "awake" due to active streaming.
@@ -13161,6 +13332,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
13161
13332
  `transport=tcp host=${this.opts.host} port=${port}${sid ? ` sid=${sid}` : ""}${remote ? ` remote=${remote}` : ""}${peer ? ` peer=${peer}` : ""}`
13162
13333
  );
13163
13334
  this.logSocketState("tcp_connected");
13335
+ this.registerInDeviceClients();
13164
13336
  this.startKeepAlive();
13165
13337
  this.kickIdleDisconnectTimer();
13166
13338
  }
@@ -13477,6 +13649,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
13477
13649
  this.logDebug("udp_close_error", e);
13478
13650
  }
13479
13651
  }
13652
+ this.unregisterFromDeviceClients();
13480
13653
  }
13481
13654
  handleFrame(frame) {
13482
13655
  const now = Date.now();
@@ -15528,6 +15701,309 @@ function parseXmlFragmentToJson(xml) {
15528
15701
  return parsed.root;
15529
15702
  }
15530
15703
 
15704
+ // src/reolink/baichuan/utils/email.ts
15705
+ init_xml();
15706
+ var parseInt01 = (text) => {
15707
+ if (text === void 0) return void 0;
15708
+ return text.trim() === "1" ? 1 : 0;
15709
+ };
15710
+ var parseNumberSafe = (text) => {
15711
+ if (text === void 0) return void 0;
15712
+ const n = Number(text);
15713
+ return Number.isFinite(n) ? n : void 0;
15714
+ };
15715
+ var VALID_ATTACHMENT_TYPES = [
15716
+ "picture",
15717
+ "video",
15718
+ "none"
15719
+ ];
15720
+ var VALID_TEXT_TYPES = ["withText", "noText"];
15721
+ var normalizeAttachmentType = (text) => {
15722
+ const t = (text ?? "").trim();
15723
+ return VALID_ATTACHMENT_TYPES.includes(t) ? t : "picture";
15724
+ };
15725
+ var normalizeTextType = (text) => {
15726
+ const t = (text ?? "").trim();
15727
+ return VALID_TEXT_TYPES.includes(t) ? t : "withText";
15728
+ };
15729
+ function parseEmailConfigFromXml(xml) {
15730
+ const cfg = {
15731
+ smtpServer: getXmlText(xml, "smtpServer") ?? "",
15732
+ userName: getXmlText(xml, "userName") ?? "",
15733
+ password: getXmlText(xml, "password") ?? "",
15734
+ address1: getXmlText(xml, "address1") ?? "",
15735
+ address2: getXmlText(xml, "address2") ?? "",
15736
+ address3: getXmlText(xml, "address3") ?? "",
15737
+ smtpPort: parseNumberSafe(getXmlText(xml, "smtpPort")) ?? 0,
15738
+ sendNickname: getXmlText(xml, "sendNickname") ?? "",
15739
+ attachment: parseInt01(getXmlText(xml, "attachment")) ?? 0,
15740
+ attachmentType: normalizeAttachmentType(getXmlText(xml, "attachmentType")),
15741
+ textType: normalizeTextType(getXmlText(xml, "textType")),
15742
+ ssl: parseInt01(getXmlText(xml, "ssl")) ?? 0,
15743
+ interval: parseNumberSafe(getXmlText(xml, "interval")) ?? 30
15744
+ };
15745
+ const senderMaxLen = parseNumberSafe(getXmlText(xml, "senderMaxLen"));
15746
+ if (senderMaxLen !== void 0) cfg.senderMaxLen = senderMaxLen;
15747
+ const pwdMaxLen = parseNumberSafe(getXmlText(xml, "pwdMaxLen"));
15748
+ if (pwdMaxLen !== void 0) cfg.pwdMaxLen = pwdMaxLen;
15749
+ const ability = parseNumberSafe(getXmlText(xml, "emailAttachAbility"));
15750
+ if (ability !== void 0) cfg.emailAttachAbility = ability;
15751
+ return cfg;
15752
+ }
15753
+ function buildSetEmailXml(current, patch) {
15754
+ const merged = { ...current, ...patch };
15755
+ return ensureXmlHeader(
15756
+ `<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>`
15757
+ );
15758
+ }
15759
+ function parseEmailTaskFromXml(xml) {
15760
+ const channelId = parseNumberSafe(getXmlText(xml, "channelId")) ?? 0;
15761
+ const enable = parseInt01(getXmlText(xml, "enable")) ?? 0;
15762
+ const items = [];
15763
+ const itemRe = /<item>([\s\S]*?)<\/item>/g;
15764
+ let match;
15765
+ while ((match = itemRe.exec(xml)) !== null) {
15766
+ const block = match[1] ?? "";
15767
+ items.push({
15768
+ type: getXmlText(block, "type") ?? "none",
15769
+ valueTable: getXmlText(block, "valueTable") ?? ""
15770
+ });
15771
+ }
15772
+ return { channelId, enable, typeScheduleList: items };
15773
+ }
15774
+ function buildEmailScheduleValueTable(spec) {
15775
+ if (spec.kind === "always") return "1".repeat(168);
15776
+ const grid = new Array(168).fill("0");
15777
+ if (spec.kind === "never") return grid.join("");
15778
+ for (const w of spec.windows) {
15779
+ const startH = Math.max(0, Math.min(24, w.startHour));
15780
+ const endH = Math.max(startH, Math.min(24, w.endHour));
15781
+ for (const d of w.days) {
15782
+ if (d < 0 || d > 6) continue;
15783
+ for (let h = startH; h < endH; h++) {
15784
+ grid[d * 24 + h] = "1";
15785
+ }
15786
+ }
15787
+ }
15788
+ return grid.join("");
15789
+ }
15790
+ function buildSetEmailTaskXml(task) {
15791
+ const items = task.typeScheduleList.map(
15792
+ (item) => `<item><type>${xmlEscape(item.type)}</type><valueTable>${xmlEscape(item.valueTable)}</valueTable></item>`
15793
+ ).join("");
15794
+ return ensureXmlHeader(
15795
+ `<body><EmailTask version="1.1"><channelId>${task.channelId}</channelId><enable>${task.enable}</enable><typeScheduleList>${items}</typeScheduleList></EmailTask></body>`
15796
+ );
15797
+ }
15798
+
15799
+ // src/reolink/baichuan/utils/ntp.ts
15800
+ init_xml();
15801
+ var parseNumberSafe2 = (text) => {
15802
+ if (text === void 0) return void 0;
15803
+ const n = Number(text);
15804
+ return Number.isFinite(n) ? n : void 0;
15805
+ };
15806
+ var parseInt012 = (text) => {
15807
+ if (text === void 0) return void 0;
15808
+ return text.trim() === "1" ? 1 : 0;
15809
+ };
15810
+ function parseNtpConfigFromXml(xml) {
15811
+ return {
15812
+ enable: parseInt012(getXmlText(xml, "enable")) ?? 0,
15813
+ server: getXmlText(xml, "server") ?? "",
15814
+ synchronizeInterval: parseNumberSafe2(getXmlText(xml, "synchronizeInterval")) ?? 1440,
15815
+ port: parseNumberSafe2(getXmlText(xml, "port")) ?? 123
15816
+ };
15817
+ }
15818
+ function buildSetNtpXml(current, patch) {
15819
+ const merged = { ...current, ...patch };
15820
+ return ensureXmlHeader(
15821
+ `<body><Ntp version="1.1"><enable>${merged.enable}</enable><server>${xmlEscape(merged.server)}</server><synchronizeInterval>${merged.synchronizeInterval}</synchronizeInterval><port>${merged.port}</port></Ntp></body>`
15822
+ );
15823
+ }
15824
+
15825
+ // src/reolink/baichuan/utils/dst.ts
15826
+ init_xml();
15827
+ var parseNumberSafe3 = (text) => {
15828
+ if (text === void 0) return void 0;
15829
+ const n = Number(text);
15830
+ return Number.isFinite(n) ? n : void 0;
15831
+ };
15832
+ var parseInt013 = (text) => {
15833
+ if (text === void 0) return void 0;
15834
+ return text.trim() === "1" ? 1 : 0;
15835
+ };
15836
+ var VALID_WEEKDAYS = [
15837
+ "Sunday",
15838
+ "Monday",
15839
+ "Tuesday",
15840
+ "Wednesday",
15841
+ "Thursday",
15842
+ "Friday",
15843
+ "Saturday"
15844
+ ];
15845
+ var normalizeWeekday = (text) => {
15846
+ const t = (text ?? "").trim();
15847
+ return VALID_WEEKDAYS.includes(t) ? t : "Sunday";
15848
+ };
15849
+ function parseDstConfigFromXml(xml) {
15850
+ const cfg = {
15851
+ enable: parseInt013(getXmlText(xml, "enable")) ?? 0,
15852
+ offset: parseNumberSafe3(getXmlText(xml, "offset")) ?? 1,
15853
+ startMonth: parseNumberSafe3(getXmlText(xml, "startMonth")) ?? 3,
15854
+ startWeekIndex: parseNumberSafe3(getXmlText(xml, "startWeekIndex")) ?? 5,
15855
+ startWeekday: normalizeWeekday(getXmlText(xml, "startWeekday")),
15856
+ startHour: parseNumberSafe3(getXmlText(xml, "startHour")) ?? 2,
15857
+ startMinute: parseNumberSafe3(getXmlText(xml, "startMinute")) ?? 0,
15858
+ startSecond: parseNumberSafe3(getXmlText(xml, "startSecond")) ?? 0,
15859
+ endMonth: parseNumberSafe3(getXmlText(xml, "endMonth")) ?? 10,
15860
+ endWeekIndex: parseNumberSafe3(getXmlText(xml, "endWeekIndex")) ?? 4,
15861
+ endWeekday: normalizeWeekday(getXmlText(xml, "endWeekday")),
15862
+ endHour: parseNumberSafe3(getXmlText(xml, "endHour")) ?? 3,
15863
+ endMinute: parseNumberSafe3(getXmlText(xml, "endMinute")) ?? 0,
15864
+ endSecond: parseNumberSafe3(getXmlText(xml, "endSecond")) ?? 0
15865
+ };
15866
+ const version = parseNumberSafe3(getXmlText(xml, "version"));
15867
+ if (version !== void 0) cfg.version = version;
15868
+ return cfg;
15869
+ }
15870
+ function buildSetDstXml(current, patch) {
15871
+ const merged = { ...current, ...patch };
15872
+ return ensureXmlHeader(
15873
+ `<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>`
15874
+ );
15875
+ }
15876
+
15877
+ // src/reolink/baichuan/utils/autoReboot.ts
15878
+ init_xml();
15879
+ var parseNumberSafe4 = (text) => {
15880
+ if (text === void 0) return void 0;
15881
+ const n = Number(text);
15882
+ return Number.isFinite(n) ? n : void 0;
15883
+ };
15884
+ var parseInt014 = (text) => {
15885
+ if (text === void 0) return void 0;
15886
+ return text.trim() === "1" ? 1 : 0;
15887
+ };
15888
+ var VALID_WEEKDAYS2 = [
15889
+ "Sunday",
15890
+ "Monday",
15891
+ "Tuesday",
15892
+ "Wednesday",
15893
+ "Thursday",
15894
+ "Friday",
15895
+ "Saturday",
15896
+ "everyday"
15897
+ ];
15898
+ var normalizeWeekday2 = (text) => {
15899
+ const t = (text ?? "").trim();
15900
+ return VALID_WEEKDAYS2.includes(t) ? t : "Sunday";
15901
+ };
15902
+ function parseAutoRebootFromXml(xml) {
15903
+ return {
15904
+ enable: parseInt014(getXmlText(xml, "enable")) ?? 0,
15905
+ weekDay: normalizeWeekday2(getXmlText(xml, "weekDay")),
15906
+ hour: parseNumberSafe4(getXmlText(xml, "hour")) ?? 0,
15907
+ minute: parseNumberSafe4(getXmlText(xml, "minute")) ?? 0,
15908
+ second: parseNumberSafe4(getXmlText(xml, "second")) ?? 0
15909
+ };
15910
+ }
15911
+ function buildSetAutoRebootXml(current, patch) {
15912
+ const merged = { ...current, ...patch };
15913
+ return ensureXmlHeader(
15914
+ `<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>`
15915
+ );
15916
+ }
15917
+
15918
+ // src/reolink/baichuan/utils/systemGeneral.ts
15919
+ init_xml();
15920
+ var parseNumberSafe5 = (text) => {
15921
+ if (text === void 0) return void 0;
15922
+ const n = Number(text);
15923
+ return Number.isFinite(n) ? n : void 0;
15924
+ };
15925
+ var parseInt015 = (text) => {
15926
+ if (text === void 0) return void 0;
15927
+ return text.trim() === "1" ? 1 : 0;
15928
+ };
15929
+ var VALID_OSD_FORMATS = ["DMY", "MDY", "YMD"];
15930
+ var normalizeOsdFormat = (text) => {
15931
+ const t = (text ?? "").trim();
15932
+ return VALID_OSD_FORMATS.includes(t) ? t : "YMD";
15933
+ };
15934
+ function parseSystemGeneralFromXml(xml) {
15935
+ return {
15936
+ timeZone: parseNumberSafe5(getXmlText(xml, "timeZone")) ?? 0,
15937
+ osdFormat: normalizeOsdFormat(getXmlText(xml, "osdFormat")),
15938
+ year: parseNumberSafe5(getXmlText(xml, "year")) ?? 0,
15939
+ month: parseNumberSafe5(getXmlText(xml, "month")) ?? 0,
15940
+ day: parseNumberSafe5(getXmlText(xml, "day")) ?? 0,
15941
+ hour: parseNumberSafe5(getXmlText(xml, "hour")) ?? 0,
15942
+ minute: parseNumberSafe5(getXmlText(xml, "minute")) ?? 0,
15943
+ second: parseNumberSafe5(getXmlText(xml, "second")) ?? 0,
15944
+ deviceId: parseNumberSafe5(getXmlText(xml, "deviceId")) ?? 0,
15945
+ timeFormat: parseInt015(getXmlText(xml, "timeFormat")) ?? 0,
15946
+ language: getXmlText(xml, "language") ?? "English",
15947
+ deviceName: getXmlText(xml, "deviceName") ?? "",
15948
+ loginLock: parseInt015(getXmlText(xml, "loginLock")) ?? 0,
15949
+ lockTime: parseNumberSafe5(getXmlText(xml, "lockTime")) ?? 0,
15950
+ allowedTimes: parseNumberSafe5(getXmlText(xml, "allowedTimes")) ?? 0,
15951
+ isDst: parseInt015(getXmlText(xml, "isDst")) ?? 0
15952
+ };
15953
+ }
15954
+ function buildSetSystemGeneralXml(patch) {
15955
+ const parts = [];
15956
+ 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;
15957
+ if (isDeviceNameOnly) {
15958
+ parts.push("<year>0</year>");
15959
+ parts.push(`<deviceName>${xmlEscape(patch.deviceName)}</deviceName>`);
15960
+ parts.push("<deviceNameOnly>1</deviceNameOnly>");
15961
+ } else if (patch.manualTime !== void 0) {
15962
+ const mt = patch.manualTime;
15963
+ if (patch.timeZone !== void 0)
15964
+ parts.push(`<timeZone>${patch.timeZone}</timeZone>`);
15965
+ if (patch.osdFormat !== void 0)
15966
+ parts.push(`<osdFormat>${patch.osdFormat}</osdFormat>`);
15967
+ parts.push(`<year>${mt.year}</year>`);
15968
+ parts.push(`<month>${mt.month}</month>`);
15969
+ parts.push(`<day>${mt.day}</day>`);
15970
+ parts.push(`<hour>${mt.hour}</hour>`);
15971
+ parts.push(`<minute>${mt.minute}</minute>`);
15972
+ parts.push(`<second>${mt.second}</second>`);
15973
+ if (patch.timeFormat !== void 0)
15974
+ parts.push(`<timeFormat>${patch.timeFormat}</timeFormat>`);
15975
+ if (patch.language !== void 0)
15976
+ parts.push(`<language>${xmlEscape(patch.language)}</language>`);
15977
+ if (patch.deviceName !== void 0)
15978
+ parts.push(`<deviceName>${xmlEscape(patch.deviceName)}</deviceName>`);
15979
+ if (patch.loginLock !== void 0)
15980
+ parts.push(`<loginLock>${patch.loginLock}</loginLock>`);
15981
+ if (patch.lockTime !== void 0)
15982
+ parts.push(`<lockTime>${patch.lockTime}</lockTime>`);
15983
+ if (patch.allowedTimes !== void 0)
15984
+ parts.push(`<allowedTimes>${patch.allowedTimes}</allowedTimes>`);
15985
+ } else {
15986
+ if (patch.timeZone !== void 0)
15987
+ parts.push(`<timeZone>${patch.timeZone}</timeZone>`);
15988
+ if (patch.osdFormat !== void 0)
15989
+ parts.push(`<osdFormat>${patch.osdFormat}</osdFormat>`);
15990
+ if (patch.timeFormat !== void 0)
15991
+ parts.push(`<timeFormat>${patch.timeFormat}</timeFormat>`);
15992
+ if (patch.language !== void 0)
15993
+ parts.push(`<language>${xmlEscape(patch.language)}</language>`);
15994
+ if (patch.loginLock !== void 0)
15995
+ parts.push(`<loginLock>${patch.loginLock}</loginLock>`);
15996
+ if (patch.lockTime !== void 0)
15997
+ parts.push(`<lockTime>${patch.lockTime}</lockTime>`);
15998
+ if (patch.allowedTimes !== void 0)
15999
+ parts.push(`<allowedTimes>${patch.allowedTimes}</allowedTimes>`);
16000
+ parts.push("<year>0</year>");
16001
+ }
16002
+ return ensureXmlHeader(
16003
+ `<body><SystemGeneral version="1.1">${parts.join("")}</SystemGeneral></body>`
16004
+ );
16005
+ }
16006
+
15531
16007
  // src/reolink/baichuan/ReolinkBaichuanApi.ts
15532
16008
  var import_jimp = require("jimp");
15533
16009
  init_ReolinkCgiApi();
@@ -15804,6 +16280,32 @@ var parseAbilityInfoXml = (xml) => {
15804
16280
  return abilities;
15805
16281
  };
15806
16282
 
16283
+ // src/reolink/baichuan/utils/versionInfo.ts
16284
+ init_xml();
16285
+ function parseVersionInfo(xml) {
16286
+ const out = {};
16287
+ const set = (key) => {
16288
+ const v = getXmlText(xml, key);
16289
+ if (v !== void 0) out[key] = v;
16290
+ };
16291
+ set("name");
16292
+ set("type");
16293
+ set("serialNumber");
16294
+ set("buildDay");
16295
+ set("hardwareVersion");
16296
+ set("cfgVersion");
16297
+ set("firmwareVersion");
16298
+ set("detail");
16299
+ set("IEClient");
16300
+ set("cc3200Version");
16301
+ set("spVersion");
16302
+ set("pakSuffix");
16303
+ set("itemNo");
16304
+ set("aiVersion");
16305
+ set("helpVersion");
16306
+ return out;
16307
+ }
16308
+
15807
16309
  // src/reolink/baichuan/utils/aiState.ts
15808
16310
  init_constants();
15809
16311
  init_xml();
@@ -16110,6 +16612,204 @@ var buildChannelPushDataLogSnapshot = (channelPushData) => {
16110
16612
  return { result: resultObj, storedChannels: Object.keys(resultObj) };
16111
16613
  };
16112
16614
 
16615
+ // src/reolink/baichuan/utils/detection.ts
16616
+ var lz4 = __toESM(require("lz4js"), 1);
16617
+
16618
+ // src/reolink/baichuan/utils/aiClassMap.ts
16619
+ function type1ToLabel(type1) {
16620
+ switch (type1) {
16621
+ case 1:
16622
+ return "people";
16623
+ case 2:
16624
+ return "vehicle";
16625
+ case 3:
16626
+ return "animal";
16627
+ case 11259375:
16628
+ return "face";
16629
+ default:
16630
+ return "unknown";
16631
+ }
16632
+ }
16633
+
16634
+ // src/reolink/baichuan/utils/detection.ts
16635
+ var MARKER_LENGTH = 8;
16636
+ var IFRAME_PREFIX_LENGTH = 8;
16637
+ var COUNTER_OFFSET = 8;
16638
+ var BASELINE_SIZE = 128;
16639
+ var FRAME_SIZE_TLV = Buffer.from([3, 4, 0]);
16640
+ var LZ4F_MAGIC = Buffer.from([4, 34, 77, 24]);
16641
+ var CONFIDENCE_DIVISOR = 100;
16642
+ var DEFAULT_AI_FRAME_WIDTH = 896;
16643
+ var DEFAULT_AI_FRAME_HEIGHT = 480;
16644
+ var LZ4_DECOMPRESS_MAX = 256 * 1024;
16645
+ function walkBoxes(buf, start, end, type1, type2, out) {
16646
+ let pos = start;
16647
+ while (pos + 3 <= end) {
16648
+ const t = buf[pos];
16649
+ if (t === 0) return;
16650
+ const length = buf[pos + 1] | buf[pos + 2] << 8;
16651
+ const recordEnd = pos + 3 + length;
16652
+ if (recordEnd > end) return;
16653
+ const isBoxType4 = t === 4 && (length === 10 || length === 13 || length === 14);
16654
+ const isBoxType2 = t === 2 && length === 10;
16655
+ if ((isBoxType4 || isBoxType2) && type1 !== 0 && type2 !== 0) {
16656
+ const x1 = buf.readUInt16LE(pos + 3);
16657
+ const y1 = buf.readUInt16LE(pos + 5);
16658
+ const x2 = buf.readUInt16LE(pos + 7);
16659
+ const y2 = buf.readUInt16LE(pos + 9);
16660
+ const conf = buf.readUInt16LE(pos + 11);
16661
+ if (x2 > x1 && y2 > y1) {
16662
+ out.push({ x1, y1, x2, y2, conf, label: type1ToLabel(type1) });
16663
+ }
16664
+ pos = recordEnd;
16665
+ continue;
16666
+ }
16667
+ 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]) {
16668
+ try {
16669
+ const decompressed = lz4.decompress(
16670
+ buf.subarray(pos + 3, recordEnd),
16671
+ LZ4_DECOMPRESS_MAX
16672
+ );
16673
+ const decBuf = Buffer.from(decompressed);
16674
+ walkBoxes(decBuf, 0, decBuf.length, 0, 0, out);
16675
+ } catch {
16676
+ }
16677
+ pos = recordEnd;
16678
+ continue;
16679
+ }
16680
+ if (length > 0) {
16681
+ let nextT1 = type1;
16682
+ let nextT2 = type2;
16683
+ if (type1 === 0) nextT1 = t;
16684
+ else if (type2 === 0) nextT2 = t;
16685
+ walkBoxes(buf, pos + 3, recordEnd, nextT1, nextT2, out);
16686
+ }
16687
+ pos = recordEnd;
16688
+ }
16689
+ }
16690
+ function decodeDetectionHeader(raw, frameType) {
16691
+ const markerOffset = frameType === "Iframe" ? IFRAME_PREFIX_LENGTH : 0;
16692
+ const blockLength = raw.length - markerOffset;
16693
+ const empty = {
16694
+ state: "invalid-marker",
16695
+ markerOffset,
16696
+ blockLength,
16697
+ boxes: []
16698
+ };
16699
+ if (blockLength < MARKER_LENGTH) return empty;
16700
+ if (!hasStandardMarker(raw, markerOffset)) return empty;
16701
+ if (blockLength < COUNTER_OFFSET + 4) return empty;
16702
+ const counter = raw.readUInt32LE(markerOffset + COUNTER_OFFSET);
16703
+ const rawBoxes = [];
16704
+ walkBoxes(raw, markerOffset, raw.length, 0, 0, rawBoxes);
16705
+ let aiFrameWidth = DEFAULT_AI_FRAME_WIDTH;
16706
+ let aiFrameHeight = DEFAULT_AI_FRAME_HEIGHT;
16707
+ let frameSizeFound = false;
16708
+ const searchStart = markerOffset + MARKER_LENGTH;
16709
+ for (let i = searchStart; i + 7 <= raw.length; i++) {
16710
+ if (raw[i] === FRAME_SIZE_TLV[0] && raw[i + 1] === FRAME_SIZE_TLV[1] && raw[i + 2] === FRAME_SIZE_TLV[2]) {
16711
+ const w = raw.readUInt16LE(i + 3);
16712
+ const h = raw.readUInt16LE(i + 5);
16713
+ if (w >= 64 && w <= 8192 && h >= 64 && h <= 8192) {
16714
+ aiFrameWidth = w;
16715
+ aiFrameHeight = h;
16716
+ frameSizeFound = true;
16717
+ break;
16718
+ }
16719
+ }
16720
+ }
16721
+ const specificity = {
16722
+ face: 4,
16723
+ animal: 3,
16724
+ people: 2,
16725
+ vehicle: 1,
16726
+ unknown: 0
16727
+ };
16728
+ const dedup = /* @__PURE__ */ new Map();
16729
+ for (const rb of rawBoxes) {
16730
+ if (rb.x2 > aiFrameWidth || rb.y2 > aiFrameHeight) continue;
16731
+ const key = `${rb.x1}_${rb.y1}_${rb.x2}_${rb.y2}`;
16732
+ const prev = dedup.get(key);
16733
+ if (!prev || (specificity[rb.label] ?? 0) > (specificity[prev.label] ?? 0)) {
16734
+ dedup.set(key, rb);
16735
+ }
16736
+ }
16737
+ const boxes = [];
16738
+ for (const rb of dedup.values()) {
16739
+ boxes.push({
16740
+ x: rb.x1 / aiFrameWidth,
16741
+ y: rb.y1 / aiFrameHeight,
16742
+ width: (rb.x2 - rb.x1) / aiFrameWidth,
16743
+ height: (rb.y2 - rb.y1) / aiFrameHeight,
16744
+ ...rb.conf > 0 && rb.conf <= 100 ? { confidence: rb.conf / CONFIDENCE_DIVISOR } : {},
16745
+ ...rb.label !== "unknown" ? { label: rb.label } : {}
16746
+ });
16747
+ }
16748
+ let state;
16749
+ if (boxes.length > 0) state = "overlay-decoded";
16750
+ else if (blockLength === BASELINE_SIZE) state = "no-overlay";
16751
+ else state = "overlay-undecoded";
16752
+ return {
16753
+ state,
16754
+ markerOffset,
16755
+ blockLength,
16756
+ counter,
16757
+ ...frameSizeFound ? { aiFrameWidth, aiFrameHeight } : {},
16758
+ boxes
16759
+ };
16760
+ }
16761
+ function hasStandardMarker(raw, offset) {
16762
+ if (raw.length < offset + MARKER_LENGTH) return false;
16763
+ 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;
16764
+ }
16765
+
16766
+ // src/reolink/baichuan/utils/encOptions.ts
16767
+ function buildEncOptions(list, channel) {
16768
+ const result = { channel };
16769
+ const main2 = aggregateByType(list, "mainStream");
16770
+ const sub = aggregateByType(list, "subStream");
16771
+ const third = aggregateByType(list, "thirdStream");
16772
+ if (main2) result.mainStream = main2;
16773
+ if (sub) result.subStream = sub;
16774
+ if (third) result.thirdStream = third;
16775
+ return result;
16776
+ }
16777
+ function aggregateByType(list, type) {
16778
+ const seen = /* @__PURE__ */ new Map();
16779
+ for (const stream of list.streams) {
16780
+ for (const eb of stream.encodeTables) {
16781
+ if (eb.type !== type) continue;
16782
+ const mapped = mapEncodeTable(eb);
16783
+ if (!mapped) continue;
16784
+ const key = `${mapped.width}x${mapped.height}`;
16785
+ if (!seen.has(key)) seen.set(key, mapped);
16786
+ }
16787
+ }
16788
+ if (seen.size === 0) return void 0;
16789
+ return {
16790
+ type,
16791
+ resolutions: [...seen.values()],
16792
+ encoderTypes: ["vbr", "cbr"],
16793
+ encoderProfiles: ["high", "main", "baseline"]
16794
+ };
16795
+ }
16796
+ function mapEncodeTable(eb) {
16797
+ if (eb.width == null || eb.height == null) return void 0;
16798
+ const videoEncTypes = (eb.videoEncTypeList ?? (eb.videoEncType != null ? [eb.videoEncType] : [])).map(
16799
+ (t) => t === 0 ? "h264" : t === 1 ? "h265" : void 0
16800
+ ).filter((t) => t !== void 0);
16801
+ return {
16802
+ width: eb.width,
16803
+ height: eb.height,
16804
+ videoEncTypes,
16805
+ ...eb.defaultFramerate != null ? { defaultFramerate: eb.defaultFramerate } : {},
16806
+ ...eb.defaultBitrate != null ? { defaultBitrate: eb.defaultBitrate } : {},
16807
+ ...eb.defaultGop != null ? { defaultGop: eb.defaultGop } : {},
16808
+ framerateOptions: eb.framerateTable ?? [],
16809
+ bitrateOptions: eb.bitrateTable ?? []
16810
+ };
16811
+ }
16812
+
16113
16813
  // src/reolink/baichuan/utils/events.ts
16114
16814
  var mapToSimpleEvent = (event) => {
16115
16815
  const timestamp = event.timestamp ?? Date.now();
@@ -18233,6 +18933,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18233
18933
  sessionGuardIntervalTimer;
18234
18934
  simpleEventListeners = /* @__PURE__ */ new Set();
18235
18935
  simpleEventSubscribed = false;
18936
+ // Detection events are sourced from BcMedia additionalHeader on active video
18937
+ // streams. Unlike simpleEvent, no Baichuan subscribe command is needed — the
18938
+ // data flows whenever a stream is open. Active streams register themselves via
18939
+ // _registerVideoStreamForDetection (called from BaichuanVideoStream.start).
18940
+ detectionEventListeners = /* @__PURE__ */ new Set();
18941
+ detectionEventStreamHooks = /* @__PURE__ */ new Map();
18942
+ // Auto-managed substream for `onObjectDetections` listeners. Reference-counted
18943
+ // by the listener set: the substream is opened on the first listener and torn
18944
+ // down with the last one. Mirrors `onSimpleEvent`'s subscribe/unsubscribe
18945
+ // lifecycle so a caller never has to manage a video stream just to read AI
18946
+ // detections.
18947
+ objectDetectionListeners = /* @__PURE__ */ new Set();
18948
+ objectDetectionStream;
18949
+ objectDetectionStreamStartInFlight;
18950
+ objectDetectionInternalListener;
18236
18951
  simpleEventSubscribeInFlight;
18237
18952
  simpleEventUnsubscribeInFlight;
18238
18953
  simpleEventResubscribeTimer;
@@ -19668,6 +20383,205 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
19668
20383
  }
19669
20384
  }
19670
20385
  }
20386
+ /**
20387
+ * Subscribe to per-frame detection events sourced from the BcMedia
20388
+ * `additionalHeader` block on active video streams.
20389
+ *
20390
+ * Mirrors {@link onSimpleEvent} but is fed by the streaming side-channel:
20391
+ * one event fires for every I-frame / P-frame that carries an overlay block.
20392
+ * Coordinates are reported in normalized [0, 1] fractions of the source
20393
+ * frame, so the same box renders correctly on mainStream, subStream, and
20394
+ * externStream.
20395
+ *
20396
+ * Unlike `onSimpleEvent`, no Baichuan subscribe command is involved — events
20397
+ * only flow while a video stream is open. The library hooks every
20398
+ * `BaichuanVideoStream` created via this API for the listener's lifetime.
20399
+ */
20400
+ onDetection(callback) {
20401
+ this.detectionEventListeners.add(callback);
20402
+ }
20403
+ /**
20404
+ * Remove a single detection callback, or all of them if `callback` is omitted.
20405
+ */
20406
+ offDetection(callback) {
20407
+ if (callback) {
20408
+ this.detectionEventListeners.delete(callback);
20409
+ } else {
20410
+ this.detectionEventListeners.clear();
20411
+ }
20412
+ }
20413
+ /**
20414
+ * Subscribe to AI object detections (people / vehicle / animal / face boxes
20415
+ * with class label and confidence) without managing a video stream yourself.
20416
+ *
20417
+ * Mirrors {@link onSimpleEvent} end-to-end: the API opens a dedicated
20418
+ * substream behind the scenes on the first listener, forwards every box-bearing
20419
+ * `additionalHeader` to your callback, and tears the stream down when the last
20420
+ * listener unsubscribes. The substream is the lightest profile (typically
20421
+ * 640×360) so the additional bandwidth/CPU overhead is minimal.
20422
+ *
20423
+ * Each event carries normalized `[0, 1]` box coordinates, a class label, and
20424
+ * a confidence score — render-ready without further conversion.
20425
+ */
20426
+ async onObjectDetections(callback) {
20427
+ this.objectDetectionListeners.add(callback);
20428
+ this.logger.debug?.(
20429
+ `[ReolinkBaichuanApi] onObjectDetections: registering listener (total=${this.objectDetectionListeners.size})`
20430
+ );
20431
+ await this.ensureObjectDetectionStream();
20432
+ }
20433
+ /**
20434
+ * Remove one detection callback, or all of them if `callback` is omitted.
20435
+ * When the last listener is removed the auto-managed substream is closed.
20436
+ */
20437
+ async offObjectDetections(callback) {
20438
+ if (callback) {
20439
+ this.objectDetectionListeners.delete(callback);
20440
+ } else {
20441
+ this.objectDetectionListeners.clear();
20442
+ }
20443
+ if (this.objectDetectionListeners.size === 0) {
20444
+ await this.tearDownObjectDetectionStream();
20445
+ }
20446
+ }
20447
+ async ensureObjectDetectionStream() {
20448
+ if (this.objectDetectionStream) return;
20449
+ if (this.objectDetectionStreamStartInFlight) {
20450
+ await this.objectDetectionStreamStartInFlight;
20451
+ return;
20452
+ }
20453
+ this.objectDetectionStreamStartInFlight = (async () => {
20454
+ const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => (init_BaichuanVideoStream(), BaichuanVideoStream_exports));
20455
+ const sessionKey = `live:object-detections:ch0:sub`;
20456
+ const dedicated = await this.createDedicatedSession(sessionKey);
20457
+ const stream = new BaichuanVideoStream2({
20458
+ client: dedicated.client,
20459
+ api: this,
20460
+ channel: 0,
20461
+ profile: "sub",
20462
+ logger: this.logger
20463
+ });
20464
+ this.objectDetectionInternalListener = (event) => {
20465
+ for (const cb of this.objectDetectionListeners) {
20466
+ try {
20467
+ void Promise.resolve(cb(event)).catch((e) => {
20468
+ (this.logger.warn ?? this.logger.error).call(
20469
+ this.logger,
20470
+ "[ReolinkBaichuanApi] onObjectDetections handler error",
20471
+ formatErrorForLog(e)
20472
+ );
20473
+ });
20474
+ } catch (e) {
20475
+ (this.logger.warn ?? this.logger.error).call(
20476
+ this.logger,
20477
+ "[ReolinkBaichuanApi] onObjectDetections handler error",
20478
+ formatErrorForLog(e)
20479
+ );
20480
+ }
20481
+ }
20482
+ };
20483
+ this.detectionEventListeners.add(this.objectDetectionInternalListener);
20484
+ try {
20485
+ await stream.start();
20486
+ } catch (e) {
20487
+ if (this.objectDetectionInternalListener) {
20488
+ this.detectionEventListeners.delete(
20489
+ this.objectDetectionInternalListener
20490
+ );
20491
+ this.objectDetectionInternalListener = void 0;
20492
+ }
20493
+ await dedicated.release().catch(() => {
20494
+ });
20495
+ throw e;
20496
+ }
20497
+ this.objectDetectionStream = {
20498
+ stop: () => stream.stop(),
20499
+ release: () => dedicated.release()
20500
+ };
20501
+ this.logger.debug?.(
20502
+ `[ReolinkBaichuanApi] onObjectDetections: substream started (key=${sessionKey})`
20503
+ );
20504
+ })();
20505
+ try {
20506
+ await this.objectDetectionStreamStartInFlight;
20507
+ } finally {
20508
+ this.objectDetectionStreamStartInFlight = void 0;
20509
+ }
20510
+ }
20511
+ async tearDownObjectDetectionStream() {
20512
+ const handle = this.objectDetectionStream;
20513
+ this.objectDetectionStream = void 0;
20514
+ if (this.objectDetectionInternalListener) {
20515
+ this.detectionEventListeners.delete(this.objectDetectionInternalListener);
20516
+ this.objectDetectionInternalListener = void 0;
20517
+ }
20518
+ if (!handle) return;
20519
+ try {
20520
+ await handle.stop();
20521
+ } catch (e) {
20522
+ this.logger.debug?.(
20523
+ `[ReolinkBaichuanApi] onObjectDetections: stream stop error: ${formatErrorForLog(e)}`
20524
+ );
20525
+ }
20526
+ try {
20527
+ await handle.release();
20528
+ } catch (e) {
20529
+ this.logger.debug?.(
20530
+ `[ReolinkBaichuanApi] onObjectDetections: session release error: ${formatErrorForLog(e)}`
20531
+ );
20532
+ }
20533
+ this.logger.debug?.(
20534
+ `[ReolinkBaichuanApi] onObjectDetections: substream torn down`
20535
+ );
20536
+ }
20537
+ /**
20538
+ * Internal: invoked by BaichuanVideoStream when it starts so the API can hook
20539
+ * its `additionalHeader` event. Returns a teardown function the stream calls
20540
+ * on stop. Not intended for direct use by consumers.
20541
+ */
20542
+ _registerVideoStreamForDetection(stream, context) {
20543
+ const listener = (info) => {
20544
+ if (this.detectionEventListeners.size === 0) return;
20545
+ const decoded = decodeDetectionHeader(info.raw, info.frameType);
20546
+ const event = {
20547
+ channel: context.channel,
20548
+ microseconds: info.microseconds,
20549
+ profile: context.profile,
20550
+ boxes: decoded.boxes,
20551
+ ...info.frameWidth !== void 0 ? { frameWidth: info.frameWidth } : {},
20552
+ ...info.frameHeight !== void 0 ? { frameHeight: info.frameHeight } : {},
20553
+ decodeState: decoded.state,
20554
+ rawHeader: info.raw
20555
+ };
20556
+ this.dispatchDetectionEvent(event);
20557
+ };
20558
+ stream.on("additionalHeader", listener);
20559
+ const teardown = () => {
20560
+ stream.off("additionalHeader", listener);
20561
+ this.detectionEventStreamHooks.delete(stream);
20562
+ };
20563
+ this.detectionEventStreamHooks.set(stream, teardown);
20564
+ return teardown;
20565
+ }
20566
+ dispatchDetectionEvent(evt) {
20567
+ for (const cb of this.detectionEventListeners) {
20568
+ try {
20569
+ void Promise.resolve(cb(evt)).catch((e) => {
20570
+ (this.logger.warn ?? this.logger.error).call(
20571
+ this.logger,
20572
+ "[ReolinkBaichuanApi] onDetection handler error",
20573
+ formatErrorForLog(e)
20574
+ );
20575
+ });
20576
+ } catch (e) {
20577
+ (this.logger.warn ?? this.logger.error).call(
20578
+ this.logger,
20579
+ "[ReolinkBaichuanApi] onDetection handler error",
20580
+ formatErrorForLog(e)
20581
+ );
20582
+ }
20583
+ }
20584
+ }
19671
20585
  startSimpleEventResubscribeTimer() {
19672
20586
  if (this.simpleEventResubscribeTimer) return;
19673
20587
  if (this.simpleEventListeners.size === 0) return;
@@ -20050,6 +20964,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
20050
20964
  this.stopUdpSleepInference();
20051
20965
  this.stopSimpleEventWatchdog();
20052
20966
  this.stopSimpleEventResubscribeTimer();
20967
+ this.objectDetectionListeners.clear();
20968
+ await this.tearDownObjectDetectionStream().catch(() => {
20969
+ });
20053
20970
  await this.cleanup();
20054
20971
  await this.stopAllActiveStreams();
20055
20972
  await this.cleanupSocketPool();
@@ -20397,6 +21314,53 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
20397
21314
  const xml = `<?xml version="1.0" encoding="UTF-8" ?><body><${tag} version="1.1"><enable>${params.enable ? 1 : 0}</enable></${tag}></body>`;
20398
21315
  await this.sendXml({ cmdId: 36, payloadXml: xml });
20399
21316
  }
21317
+ /**
21318
+ * Full port-config setter (cmd_id 36). Patches one or more of the six
21319
+ * service ports the camera serves — Server (Baichuan), HTTP, HTTPS,
21320
+ * RTSP, RTMP, ONVIF. Each entry takes an optional `port` (number) and
21321
+ * `enable` (boolean); fields the caller doesn't pass are left alone.
21322
+ *
21323
+ * Sends one block per port that has any field set, then issues a
21324
+ * single cmd_36 with the merged body. The camera accepts multiple
21325
+ * `<XxxPort>` siblings in the same payload.
21326
+ *
21327
+ * Wire format observed on E1 Zoom:
21328
+ *
21329
+ * <body>
21330
+ * <RtspPort version="1.1">
21331
+ * <rtspPort>554</rtspPort>
21332
+ * <enable>1</enable>
21333
+ * </RtspPort>
21334
+ * <HttpsPort version="1.1">
21335
+ * <enable>0</enable>
21336
+ * </HttpsPort>
21337
+ * ...
21338
+ * </body>
21339
+ */
21340
+ async setPortConfig(patch) {
21341
+ const blocks = [];
21342
+ const append = (tag, portField, cfg) => {
21343
+ if (!cfg) return;
21344
+ if (cfg.port === void 0 && cfg.enable === void 0) return;
21345
+ const inner = [];
21346
+ if (cfg.port !== void 0) {
21347
+ inner.push(`<${portField}>${cfg.port}</${portField}>`);
21348
+ }
21349
+ if (cfg.enable !== void 0) {
21350
+ inner.push(`<enable>${cfg.enable ? 1 : 0}</enable>`);
21351
+ }
21352
+ blocks.push(`<${tag} version="1.1">${inner.join("")}</${tag}>`);
21353
+ };
21354
+ append("ServerPort", "serverPort", patch.server);
21355
+ append("HttpPort", "httpPort", patch.http);
21356
+ append("HttpsPort", "httpsPort", patch.https);
21357
+ append("RtspPort", "rtspPort", patch.rtsp);
21358
+ append("RtmpPort", "rtmpPort", patch.rtmp);
21359
+ append("OnvifPort", "onvifPort", patch.onvif);
21360
+ if (blocks.length === 0) return;
21361
+ const xml = `<?xml version="1.0" encoding="UTF-8" ?><body>${blocks.join("")}</body>`;
21362
+ await this.sendXml({ cmdId: 36, payloadXml: xml });
21363
+ }
20400
21364
  /** GetDevInfo via Baichuan: host cmd_id 80, channel cmd_id 318 */
20401
21365
  async getInfo(channel, options) {
20402
21366
  const req = { cmdId: channel == null ? 80 : 318 };
@@ -24081,6 +25045,27 @@ ${stderr}`)
24081
25045
  );
24082
25046
  }
24083
25047
  }
25048
+ async gotoPtzPreset(arg1, arg2) {
25049
+ const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
25050
+ const presetId = arg2 === void 0 ? arg1 : arg2;
25051
+ const channelId = ch;
25052
+ const payloadXml = buildPtzPresetXmlV2(channelId, presetId, "toPos");
25053
+ const extensionXml = buildChannelExtensionXml(channelId);
25054
+ const frame = await this.client.sendFrame({
25055
+ cmdId: BC_CMD_ID_PTZ_CONTROL_PRESET,
25056
+ channel: ch,
25057
+ channelIdOverride: channelId,
25058
+ extensionXml,
25059
+ payloadXml,
25060
+ messageClass: BC_CLASS_MODERN_24,
25061
+ streamType: 0
25062
+ });
25063
+ if (frame.header.responseCode !== 200) {
25064
+ throw new Error(
25065
+ `PTZ goto preset rejected (response_code ${frame.header.responseCode})`
25066
+ );
25067
+ }
25068
+ }
24084
25069
  async deletePtzPreset(arg1, arg2) {
24085
25070
  const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
24086
25071
  const presetId = arg2 === void 0 ? arg1 : arg2;
@@ -24732,22 +25717,62 @@ ${stderr}`)
24732
25717
  const channel = typeof arg1 === "number" ? arg1 : arg3;
24733
25718
  const enabled = typeof arg1 === "number" ? arg2 : arg1;
24734
25719
  const sensitivity = typeof arg1 === "number" ? arg3 : arg2;
24735
- const ch = this.normalizeChannel(channel);
25720
+ return await this.setMotionAlarmFull({
25721
+ ...channel !== void 0 ? { channel } : {},
25722
+ enabled,
25723
+ ...sensitivity !== void 0 ? { sensitivity } : {}
25724
+ });
25725
+ }
25726
+ /**
25727
+ * Set motion alarm with full control, including the detection-zone grid.
25728
+ *
25729
+ * Wire format observed on E1 Zoom (cmd_id=47 SetMdAlarm body):
25730
+ *
25731
+ * <MD version="1.1">
25732
+ * <channelId>0</channelId>
25733
+ * <enable>1</enable>
25734
+ * <usepir>0</usepir>
25735
+ * <width>60</width> <height>33</height>
25736
+ * <scope>
25737
+ * <columns>96</columns> <rows>64</rows>
25738
+ * <valueTable>{base64 6144-bit bitmap}</valueTable>
25739
+ * </scope>
25740
+ * ... other camera-specific fields ...
25741
+ * </MD>
25742
+ *
25743
+ * We do a read-modify-write of the GET response so any camera-specific
25744
+ * extension fields are preserved untouched. Pass `valueTable` to update
25745
+ * the detection zone — see `encodeMotionScopeBitmap` for the bitmap layout.
25746
+ *
25747
+ * @param channel - 0-based channel
25748
+ * @param enabled - toggle motion detection on/off (optional)
25749
+ * @param sensitivity - 0-50, higher = more sensitive (optional)
25750
+ * @param valueTable - base64-encoded grid bitmap; size must match
25751
+ * `<scope><columns>×<rows></scope>` from the GET (optional)
25752
+ */
25753
+ async setMotionAlarmFull(opts) {
25754
+ const ch = this.normalizeChannel(opts.channel);
24736
25755
  const currentXml = await this.sendXml({
24737
25756
  cmdId: BC_CMD_ID_GET_MOTION_ALARM,
24738
25757
  channel: ch
24739
25758
  });
24740
25759
  let modifiedXml = currentXml;
24741
- if (enabled !== void 0) {
25760
+ if (opts.enabled !== void 0) {
24742
25761
  modifiedXml = modifiedXml.replace(
24743
25762
  /<enable>[^<]*<\/enable>/,
24744
- `<enable>${enabled ? "1" : "0"}</enable>`
25763
+ `<enable>${opts.enabled ? "1" : "0"}</enable>`
24745
25764
  );
24746
25765
  }
24747
- if (sensitivity !== void 0) {
25766
+ if (opts.sensitivity !== void 0) {
24748
25767
  modifiedXml = modifiedXml.replace(
24749
25768
  /<sensitivityDefault>[^<]*<\/sensitivityDefault>/,
24750
- `<sensitivityDefault>${sensitivity}</sensitivityDefault>`
25769
+ `<sensitivityDefault>${opts.sensitivity}</sensitivityDefault>`
25770
+ );
25771
+ }
25772
+ if (opts.valueTable !== void 0) {
25773
+ modifiedXml = modifiedXml.replace(
25774
+ /<valueTable>[^<]*<\/valueTable>/,
25775
+ `<valueTable>${opts.valueTable}</valueTable>`
24751
25776
  );
24752
25777
  }
24753
25778
  await this.sendXml({
@@ -26091,12 +27116,24 @@ ${xml}`
26091
27116
  }
26092
27117
  /**
26093
27118
  * SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
26094
- * unspecified fields. Mirrors reolink_aio's `SetEnc`.
27119
+ * unspecified fields. Mirrors reolink_aio's `SetEnc` plus the additional
27120
+ * `width`/`height`/`encoderType`/`encoderProfile`/`gop`/`thirdStream`
27121
+ * fields observed in the official mobile app (see `pcap/resolution.pcapng`).
27122
+ *
27123
+ * Field meaning per stream:
27124
+ * - `audio` — 0/1 toggle
27125
+ * - `width`/`height` — resolution in pixels. Must be one of the
27126
+ * resolutions returned by {@link getStreamInfoList}.
27127
+ * - `bitRate` — kbps. Must match the table from `getStreamInfoList`.
27128
+ * - `frameRate` — fps. Must match the table from `getStreamInfoList`.
27129
+ * - `videoEncType` — `"h264"` or `"h265"`
27130
+ * - `encoderType` — `"vbr"` or `"cbr"`
27131
+ * - `encoderProfile` — `"high"`, `"main"`, or `"baseline"`
27132
+ * - `gop` — keyframe interval in seconds (sets `<gop><cur>`)
26095
27133
  *
26096
27134
  * @param channel - Channel number (0-based)
26097
- * @param patch - Fields to update on `mainStream` and/or `subStream`,
26098
- * plus a top-level `audio` toggle (0/1). Pass only what you want
26099
- * to change.
27135
+ * @param patch - Fields to update. Pass only the fields you want to change;
27136
+ * everything else is preserved from the device's current configuration.
26100
27137
  */
26101
27138
  async setEnc(channel, patch, options) {
26102
27139
  const ch = this.normalizeChannel(channel);
@@ -26113,6 +27150,7 @@ ${xml}`
26113
27150
  }
26114
27151
  xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
26115
27152
  xml = applyStreamPatch(xml, "subStream", patch.subStream);
27153
+ xml = applyStreamPatch(xml, "thirdStream", patch.thirdStream);
26116
27154
  await this.sendXml({
26117
27155
  cmdId: BC_CMD_ID_SET_ENC,
26118
27156
  channel: ch,
@@ -26720,6 +27758,71 @@ ${xml}`
26720
27758
  `PCAP-derived settings GET failed for cmdId=${params.cmdId}: ${String(lastErr)}`
26721
27759
  );
26722
27760
  }
27761
+ /**
27762
+ * Update the OSD timestamp + channel-name overlay via cmd_id=45
27763
+ * (SetOsdDatetime). The schema is the same `<body><OsdDatetime>` +
27764
+ * `<OsdChannelName>` block returned by `getOsdDatetime` — we
27765
+ * read-modify-write so any extension fields the camera sent are
27766
+ * preserved.
27767
+ *
27768
+ * Position is in **camera pixel coordinates** (e.g. (1,1) for top-left,
27769
+ * not preset strings). Set `enable=0` to hide the overlay; the camera
27770
+ * keeps the stored position so re-enabling later restores it.
27771
+ */
27772
+ async setOsdDatetime(channel, patch, options) {
27773
+ const ch = this.normalizeChannel(channel);
27774
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
27775
+ let xml = await this.sendPcapDerivedSettingsGetXml({
27776
+ cmdId: BC_CMD_ID_GET_OSD_DATETIME,
27777
+ channel: ch,
27778
+ ...timeoutOpts
27779
+ });
27780
+ const patchBlock = (block, fields) => {
27781
+ const start = xml.indexOf(`<${block}`);
27782
+ if (start < 0) return;
27783
+ const end = xml.indexOf(`</${block}>`, start);
27784
+ if (end < 0) return;
27785
+ let body = xml.slice(start, end);
27786
+ for (const [tag, value] of Object.entries(fields)) {
27787
+ if (value === void 0) continue;
27788
+ const raw = typeof value === "boolean" ? value ? "1" : "0" : String(value);
27789
+ const escaped = raw.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
27790
+ if (body.includes(`<${tag}>`)) {
27791
+ body = body.replace(
27792
+ new RegExp(`<${tag}>[^<]*<\\/${tag}>`),
27793
+ `<${tag}>${escaped}</${tag}>`
27794
+ );
27795
+ } else {
27796
+ body += `<${tag}>${escaped}</${tag}>`;
27797
+ }
27798
+ }
27799
+ xml = xml.slice(0, start) + body + xml.slice(end);
27800
+ };
27801
+ if (patch.datetime) {
27802
+ patchBlock("OsdDatetime", {
27803
+ enable: patch.datetime.enable,
27804
+ topLeftX: patch.datetime.topLeftX,
27805
+ topLeftY: patch.datetime.topLeftY,
27806
+ language: patch.datetime.language
27807
+ });
27808
+ }
27809
+ if (patch.channelName) {
27810
+ patchBlock("OsdChannelName", {
27811
+ name: patch.channelName.name,
27812
+ enable: patch.channelName.enable,
27813
+ topLeftX: patch.channelName.topLeftX,
27814
+ topLeftY: patch.channelName.topLeftY,
27815
+ enWatermark: patch.channelName.enWatermark,
27816
+ enBgcolor: patch.channelName.enBgcolor
27817
+ });
27818
+ }
27819
+ await this.sendXml({
27820
+ cmdId: BC_CMD_ID_SET_OSD_DATETIME,
27821
+ channel: ch,
27822
+ payloadXml: ensureXmlHeader(xml),
27823
+ ...timeoutOpts
27824
+ });
27825
+ }
26723
27826
  async getOsdDatetime(channel, options) {
26724
27827
  const rawXml = await this.sendPcapDerivedSettingsGetXml({
26725
27828
  cmdId: BC_CMD_ID_GET_OSD_DATETIME,
@@ -26912,6 +28015,41 @@ ${xml}`
26912
28015
  });
26913
28016
  return { streams };
26914
28017
  }
28018
+ /**
28019
+ * Return the set of values `setEnc` will accept on each stream of `channel`.
28020
+ * Aggregates `getStreamInfoList` (cmd_146) into a UI-friendly shape:
28021
+ * per-stream resolutions with their allowed codecs/framerates/bitrates plus
28022
+ * the enumerated encoder modes/profiles Reolink exposes.
28023
+ *
28024
+ * Useful for populating selectors and validating user input before calling
28025
+ * `setEnc` — picking an unsupported combination causes the camera to reject
28026
+ * the SET_ENC command (responseCode != 200).
28027
+ */
28028
+ async getEncOptions(channel, options) {
28029
+ const list = await this.getStreamInfoList(channel, options);
28030
+ return buildEncOptions(list, channel);
28031
+ }
28032
+ /**
28033
+ * Read the camera's `<VersionInfo>` block (cmd_id=80). Returns the
28034
+ * friendly name, model code (e.g. `"E1 Zoom"`), serial number, firmware
28035
+ * version, hardware revision, build day, AI model bundle version, etc.
28036
+ *
28037
+ * This is the same info the Reolink mobile app shows in "About this
28038
+ * device" — distinct from `getSystemGeneral` (cmd_104) which carries
28039
+ * time/locale.
28040
+ *
28041
+ * No channel parameter: this command is device-global on NVRs/Hubs and
28042
+ * camera-global on standalone cameras. Pass an explicit channel via the
28043
+ * underlying `sendXml` only if a specific firmware demands it (none we've
28044
+ * tested do).
28045
+ */
28046
+ async getVersionInfo(options) {
28047
+ const xml = await this.sendXml({
28048
+ cmdId: BC_CMD_ID_GET_VERSION_INFO,
28049
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28050
+ });
28051
+ return parseVersionInfo(xml);
28052
+ }
26915
28053
  async getLedState(channel, options) {
26916
28054
  const rawXml = await this.sendPcapDerivedSettingsGetXml({
26917
28055
  cmdId: BC_CMD_ID_GET_LED_STATE,
@@ -26994,7 +28132,279 @@ ${xml}`
26994
28132
  ...channel != null ? { channel } : {},
26995
28133
  ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26996
28134
  });
26997
- return parseXmlFragmentToJson(xml);
28135
+ return parseEmailTaskFromXml(xml);
28136
+ }
28137
+ /**
28138
+ * SetEmailTask via Baichuan (cmdId=216). Updates the email alarm schedule
28139
+ * (per-event-type 7×24 valueTable + master enable).
28140
+ *
28141
+ * Reolink expects the FULL `typeScheduleList` — pass the array from a prior
28142
+ * GET and only flip the entries you care about. Slots you don't track must
28143
+ * be sent back unchanged to avoid the camera dropping them.
28144
+ */
28145
+ async setEmailTask(channel, task, options) {
28146
+ const ch = this.normalizeChannel(channel);
28147
+ const payloadXml = buildSetEmailTaskXml({ ...task, channelId: ch });
28148
+ await this.sendXml({
28149
+ cmdId: BC_CMD_ID_SET_EMAIL_TASK,
28150
+ channel: ch,
28151
+ payloadXml,
28152
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28153
+ });
28154
+ }
28155
+ /**
28156
+ * Convenience wrapper that patches the schedule of one or more trigger
28157
+ * types on the camera's EmailTask without touching the others.
28158
+ *
28159
+ * Pass a high-level schedule spec (`always` / `never` / explicit windows)
28160
+ * and the trigger types it should apply to. The method:
28161
+ *
28162
+ * 1. Reads the current EmailTask via GET (so we keep every existing slot).
28163
+ * 2. Builds the new `valueTable` once from `schedule`.
28164
+ * 3. Replaces the `valueTable` of every matching `type` in the list.
28165
+ * 4. Appends entries for any requested type not already present.
28166
+ * 5. Writes the merged list back via SET.
28167
+ *
28168
+ * Returns the list of types that were actually touched.
28169
+ */
28170
+ async patchEmailSchedule(channel, spec, options) {
28171
+ const current = await this.getEmailTask(channel, options);
28172
+ const newValueTable = buildEmailScheduleValueTable(spec.schedule);
28173
+ const targetSet = new Set(spec.types);
28174
+ const touched = [];
28175
+ const updatedList = current.typeScheduleList.map((item) => {
28176
+ if (targetSet.has(item.type)) {
28177
+ touched.push(item.type);
28178
+ return { ...item, valueTable: newValueTable };
28179
+ }
28180
+ return item;
28181
+ });
28182
+ for (const t of spec.types) {
28183
+ if (!current.typeScheduleList.some((item) => item.type === t)) {
28184
+ updatedList.push({ type: t, valueTable: newValueTable });
28185
+ touched.push(t);
28186
+ }
28187
+ }
28188
+ await this.setEmailTask(
28189
+ channel,
28190
+ {
28191
+ channelId: current.channelId,
28192
+ enable: spec.enable ?? current.enable,
28193
+ typeScheduleList: updatedList
28194
+ },
28195
+ options
28196
+ );
28197
+ return { touchedTypes: touched };
28198
+ }
28199
+ // ====================================================================
28200
+ // Email server (cmdId 42/43/141), NTP (38/39), DST (106/107),
28201
+ // SystemGeneral SET (105), AutoReboot (100/101).
28202
+ // Schemas derived from Reolink Client pcap (2026-05-16).
28203
+ // ====================================================================
28204
+ /**
28205
+ * Read the SMTP email configuration (cmdId=42). Returns the full `<Email>`
28206
+ * block including capability hints (`senderMaxLen`, `pwdMaxLen`,
28207
+ * `emailAttachAbility`).
28208
+ */
28209
+ async getEmail(options) {
28210
+ const xml = await this.sendXml({
28211
+ cmdId: BC_CMD_ID_GET_EMAIL,
28212
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28213
+ });
28214
+ return parseEmailConfigFromXml(xml);
28215
+ }
28216
+ /**
28217
+ * Patch the SMTP email configuration (cmdId=43). Reads the current config
28218
+ * first then merges the patch — Reolink rejects partial `<Email>` blocks.
28219
+ */
28220
+ async setEmail(patch, options) {
28221
+ const current = await this.getEmail(options);
28222
+ const payloadXml = buildSetEmailXml(current, patch);
28223
+ await this.sendXml({
28224
+ cmdId: BC_CMD_ID_SET_EMAIL,
28225
+ payloadXml,
28226
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28227
+ });
28228
+ }
28229
+ /**
28230
+ * Send a test email using either the current config or an override patch
28231
+ * (cmdId=141). Returns true when the camera reports 200 (test succeeded),
28232
+ * false when it reports 482 (test failed — server unreachable / bad creds).
28233
+ * Other non-200 codes propagate as exceptions via `sendXml`.
28234
+ */
28235
+ async testEmail(patch, options) {
28236
+ const current = await this.getEmail(options);
28237
+ const payloadXml = buildSetEmailXml(current, patch ?? {});
28238
+ const timeoutMs = options?.timeoutMs ?? 6e4;
28239
+ try {
28240
+ await this.sendXml({
28241
+ cmdId: BC_CMD_ID_TEST_EMAIL,
28242
+ payloadXml,
28243
+ timeoutMs
28244
+ });
28245
+ return true;
28246
+ } catch (err) {
28247
+ const msg = err instanceof Error ? err.message : String(err);
28248
+ if (msg.includes("response_code 482") || msg.includes("response_code=482")) {
28249
+ return false;
28250
+ }
28251
+ throw err;
28252
+ }
28253
+ }
28254
+ /**
28255
+ * Read the NTP server configuration (cmdId=38).
28256
+ */
28257
+ async getNtp(options) {
28258
+ const xml = await this.sendXml({
28259
+ cmdId: BC_CMD_ID_GET_NTP,
28260
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28261
+ });
28262
+ return parseNtpConfigFromXml(xml);
28263
+ }
28264
+ /**
28265
+ * Patch the NTP server configuration (cmdId=39). Reads the current state
28266
+ * first and merges the patch — Reolink rejects partial `<Ntp>` blocks.
28267
+ */
28268
+ async setNtp(patch, options) {
28269
+ const current = await this.getNtp(options);
28270
+ const payloadXml = buildSetNtpXml(current, patch);
28271
+ await this.sendXml({
28272
+ cmdId: BC_CMD_ID_SET_NTP,
28273
+ payloadXml,
28274
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28275
+ });
28276
+ }
28277
+ /**
28278
+ * Patch SystemGeneral (cmdId=105). Supports partial payloads: include only
28279
+ * the fields you want to change. By default the builder emits `<year>0</year>`
28280
+ * as the "do not set manual clock" marker; pass `manualTime` to actually
28281
+ * set the date/time. Setting only `deviceName` automatically uses the
28282
+ * Reolink Client's `deviceNameOnly=1` shape.
28283
+ */
28284
+ async setSystemGeneral(patch, options) {
28285
+ const payloadXml = buildSetSystemGeneralXml(patch);
28286
+ await this.sendXml({
28287
+ cmdId: BC_CMD_ID_SET_SYSTEM_GENERAL,
28288
+ payloadXml,
28289
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28290
+ });
28291
+ }
28292
+ /**
28293
+ * Read the Daylight Saving Time configuration (cmdId=106).
28294
+ */
28295
+ async getDst(options) {
28296
+ const xml = await this.sendXml({
28297
+ cmdId: BC_CMD_ID_GET_DST,
28298
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28299
+ });
28300
+ return parseDstConfigFromXml(xml);
28301
+ }
28302
+ /**
28303
+ * Patch the DST configuration (cmdId=107). Reads the current state first
28304
+ * and merges the patch.
28305
+ */
28306
+ async setDst(patch, options) {
28307
+ const current = await this.getDst(options);
28308
+ const payloadXml = buildSetDstXml(current, patch);
28309
+ await this.sendXml({
28310
+ cmdId: BC_CMD_ID_SET_DST,
28311
+ payloadXml,
28312
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28313
+ });
28314
+ }
28315
+ /**
28316
+ * Read the auto-reboot schedule (cmdId=101).
28317
+ */
28318
+ async getAutoReboot(options) {
28319
+ const xml = await this.sendXml({
28320
+ cmdId: BC_CMD_ID_GET_AUTO_REBOOT,
28321
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28322
+ });
28323
+ return parseAutoRebootFromXml(xml);
28324
+ }
28325
+ /**
28326
+ * Patch the auto-reboot schedule (cmdId=100).
28327
+ */
28328
+ async setAutoReboot(patch, options) {
28329
+ const current = await this.getAutoReboot(options);
28330
+ const payloadXml = buildSetAutoRebootXml(current, patch);
28331
+ await this.sendXml({
28332
+ cmdId: BC_CMD_ID_SET_AUTO_REBOOT,
28333
+ payloadXml,
28334
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
28335
+ });
28336
+ }
28337
+ /**
28338
+ * High-level helper that configures the camera to deliver motion alerts via
28339
+ * SMTP to the local nodelink manager. Orchestrates `setEmail` + `setEmailTask`
28340
+ * in a single call so UI code can offer "auto-configure" without juggling
28341
+ * the underlying commands.
28342
+ *
28343
+ * Pass `runTest: true` to also send a test email (cmdId=141). Returns a
28344
+ * structured result describing each leg of the flow so the caller can show
28345
+ * granular feedback.
28346
+ *
28347
+ * @param params Auto-configuration parameters
28348
+ * @param channel Logical channel (default 0). Used for the EmailTask SET.
28349
+ */
28350
+ async setupEmailPushToManager(params, channel, options) {
28351
+ const port = params.managerPort ?? 2525;
28352
+ const domain = params.domain ?? "nodelink.local";
28353
+ const recipient = `${params.recipientLocalPart}@${domain}`;
28354
+ const triggers = params.triggerTypes ?? ["MD", "people", "vehicle"];
28355
+ const attachmentType = params.attachmentType ?? "picture";
28356
+ const interval = params.interval ?? 30;
28357
+ const emailPatch = {
28358
+ smtpServer: params.managerHost,
28359
+ smtpPort: port,
28360
+ userName: params.authUsername ?? recipient,
28361
+ password: params.authPassword ?? "",
28362
+ address1: recipient,
28363
+ address2: "",
28364
+ address3: "",
28365
+ sendNickname: params.sendNickname ?? params.recipientLocalPart,
28366
+ attachment: attachmentType === "none" ? 0 : 1,
28367
+ attachmentType,
28368
+ textType: "withText",
28369
+ ssl: 0,
28370
+ interval
28371
+ };
28372
+ await this.setEmail(emailPatch, options);
28373
+ const fullWeekOn = "1".repeat(168);
28374
+ const current = await this.getEmailTask(channel, options);
28375
+ const triggerSet = new Set(triggers);
28376
+ const touched = [];
28377
+ const updatedList = current.typeScheduleList.map((item) => {
28378
+ if (triggerSet.has(item.type)) {
28379
+ touched.push(item.type);
28380
+ return { ...item, valueTable: fullWeekOn };
28381
+ }
28382
+ return item;
28383
+ });
28384
+ for (const t of triggers) {
28385
+ if (!current.typeScheduleList.some((item) => item.type === t)) {
28386
+ updatedList.push({ type: t, valueTable: fullWeekOn });
28387
+ touched.push(t);
28388
+ }
28389
+ }
28390
+ await this.setEmailTask(
28391
+ channel,
28392
+ {
28393
+ channelId: current.channelId,
28394
+ enable: 1,
28395
+ typeScheduleList: updatedList
28396
+ },
28397
+ options
28398
+ );
28399
+ const result = {
28400
+ setEmail: { applied: true },
28401
+ setEmailTask: { applied: true, touchedTypes: touched }
28402
+ };
28403
+ if (params.runTest) {
28404
+ const ok = await this.testEmail(emailPatch, options);
28405
+ result.testEmail = { success: ok };
28406
+ }
28407
+ return result;
26998
28408
  }
26999
28409
  /**
27000
28410
  * Get siren-on-motion state via AudioTask (cmdId=232).
@@ -27263,7 +28673,7 @@ ${xml}`
27263
28673
  cmdId: BC_CMD_ID_GET_SYSTEM_GENERAL,
27264
28674
  ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
27265
28675
  });
27266
- return parseXmlFragmentToJson(xml);
28676
+ return parseSystemGeneralFromXml(xml);
27267
28677
  }
27268
28678
  /**
27269
28679
  * Get device support/capability flags.