@apocaliss92/nodelink-js 0.2.1 → 0.2.3

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.
@@ -10,6 +10,8 @@ import {
10
10
  BC_CMD_ID_CMD_209,
11
11
  BC_CMD_ID_CMD_265,
12
12
  BC_CMD_ID_CMD_440,
13
+ BC_CMD_ID_DING_DONG_CTRL,
14
+ BC_CMD_ID_DING_DONG_OPT,
13
15
  BC_CMD_ID_FILE_INFO_LIST_CLOSE,
14
16
  BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO,
15
17
  BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD,
@@ -33,6 +35,9 @@ import {
33
35
  BC_CMD_ID_GET_BATTERY_INFO_LIST,
34
36
  BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
35
37
  BC_CMD_ID_GET_DAY_RECORDS,
38
+ BC_CMD_ID_GET_DING_DONG_CFG,
39
+ BC_CMD_ID_GET_DING_DONG_LIST,
40
+ BC_CMD_ID_GET_DING_DONG_SILENT,
36
41
  BC_CMD_ID_GET_EMAIL_TASK,
37
42
  BC_CMD_ID_GET_FTP_TASK,
38
43
  BC_CMD_ID_GET_HDD_INFO_LIST,
@@ -68,9 +73,12 @@ import {
68
73
  BC_CMD_ID_PUSH_SERIAL,
69
74
  BC_CMD_ID_PUSH_SLEEP_STATUS,
70
75
  BC_CMD_ID_PUSH_VIDEO_INPUT,
76
+ BC_CMD_ID_QUICK_REPLY_PLAY,
71
77
  BC_CMD_ID_SET_AI_ALARM,
72
78
  BC_CMD_ID_SET_AI_CFG,
73
79
  BC_CMD_ID_SET_AUDIO_TASK,
80
+ BC_CMD_ID_SET_DING_DONG_CFG,
81
+ BC_CMD_ID_SET_DING_DONG_SILENT,
74
82
  BC_CMD_ID_SET_MOTION_ALARM,
75
83
  BC_CMD_ID_SET_PIR_INFO,
76
84
  BC_CMD_ID_SET_WHITE_LED_STATE,
@@ -136,7 +144,7 @@ import {
136
144
  talkTraceLog,
137
145
  traceLog,
138
146
  xmlEscape
139
- } from "./chunk-YPU7RAEY.js";
147
+ } from "./chunk-NLTB7GTA.js";
140
148
 
141
149
  // src/protocol/framing.ts
142
150
  function encodeHeader(h) {
@@ -5392,6 +5400,8 @@ var NativeStreamFanout = class {
5392
5400
  } finally {
5393
5401
  for (const q of this.queues.values()) q.close();
5394
5402
  this.queues.clear();
5403
+ this.running = false;
5404
+ this.opts.onEnd?.();
5395
5405
  }
5396
5406
  })();
5397
5407
  }
@@ -5491,6 +5501,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5491
5501
  // Shared native stream fan-out (single camera stream, multiple RTSP clients)
5492
5502
  nativeFanout = null;
5493
5503
  noClientAutoStopTimer;
5504
+ // Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
5505
+ // When a new client connects while the stream is already running it does not need
5506
+ // to wait up to one full GOP interval for the next keyframe — we replay frames
5507
+ // from the last IDR in the prebuffer immediately.
5508
+ PREBUFFER_MAX_MS = 3e3;
5509
+ prebuffer = [];
5494
5510
  static isAdtsAacFrame(b) {
5495
5511
  return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
5496
5512
  }
@@ -5525,6 +5541,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5525
5541
  );
5526
5542
  return { sampleRate, channels, configHex };
5527
5543
  }
5544
+ /** Returns true if the raw (packed/Annex B) frame is an IDR (H.264) or IRAP (H.265). */
5545
+ isRawFrameKeyframe(frame) {
5546
+ try {
5547
+ if (frame.videoType === "H264") {
5548
+ const nals = _BaichuanRtspServer.splitAnnexBNals(
5549
+ convertToAnnexB(frame.data)
5550
+ );
5551
+ return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
5552
+ }
5553
+ if (frame.videoType === "H265") {
5554
+ const nals = splitAnnexBToNalPayloads2(convertToAnnexB2(frame.data));
5555
+ return nals.some(
5556
+ (n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
5557
+ );
5558
+ }
5559
+ } catch {
5560
+ }
5561
+ return false;
5562
+ }
5528
5563
  static parseInterleavedChannels(transportHeader) {
5529
5564
  const m = transportHeader.match(/interleaved\s*=\s*(\d+)\s*-\s*(\d+)/i);
5530
5565
  if (!m) return null;
@@ -5717,7 +5752,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5717
5752
  this.logger.warn(
5718
5753
  `[BaichuanRtspServer] Could not get stream metadata: ${error}`
5719
5754
  );
5720
- this.streamMetadata = { frameRate: 25, width: 1920, height: 1080 };
5755
+ this.streamMetadata = { frameRate: 25 };
5721
5756
  this.setFlowVideoType("H264", "metadata unavailable");
5722
5757
  }
5723
5758
  this.clientConnectionServer = net2.createServer((socket) => {
@@ -5749,7 +5784,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5749
5784
  */
5750
5785
  handleRtspConnection(socket) {
5751
5786
  const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
5752
- this.logger.info(`[BaichuanRtspServer] RTSP client connected: ${clientId}`);
5787
+ const connectTime = Date.now();
5788
+ this.logger.info(
5789
+ `[rebroadcast] client connected client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel}`
5790
+ );
5753
5791
  let sessionId = "";
5754
5792
  let buffer = Buffer.alloc(0);
5755
5793
  let clientFfmpeg;
@@ -5757,6 +5795,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5757
5795
  let clientUdpSocket = null;
5758
5796
  let clientUdpSocketAudio = null;
5759
5797
  const cleanup = () => {
5798
+ const sessionDurationMs = Date.now() - connectTime;
5799
+ const res = this.clientResources.get(clientId);
5800
+ const framesSent = res?.framesSent ?? 0;
5801
+ this.logger.info(
5802
+ `[rebroadcast] client disconnected client=${clientId} path=${this.path} profile=${this.profile} duration=${sessionDurationMs}ms frames=${framesSent}`
5803
+ );
5760
5804
  this.removeClient(clientId);
5761
5805
  this.authNonces.delete(clientId);
5762
5806
  const resources = this.clientResources.get(clientId);
@@ -5898,7 +5942,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5898
5942
  Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
5899
5943
  });
5900
5944
  } else if (method === "DESCRIBE") {
5901
- if (!this.firstFrameReceived && this.connectedClients.size === 0) {
5945
+ if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
5902
5946
  try {
5903
5947
  if (!this.nativeStreamActive) {
5904
5948
  await this.startNativeStream();
@@ -5910,7 +5954,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5910
5954
  }
5911
5955
  const { hasParamSets } = this.flow.getFmtp();
5912
5956
  if (!hasParamSets) {
5913
- const primingMs = this.api.client.getTransport() === "udp" ? 4e3 : 1500;
5957
+ const primingMs = this.api.client.getTransport() === "udp" ? 4e3 : 3e3;
5958
+ const primingStart = Date.now();
5959
+ this.logger.info(
5960
+ `[rebroadcast] DESCRIBE priming: waiting up to ${primingMs}ms for SPS/PPS client=${clientId} path=${this.path}`
5961
+ );
5914
5962
  try {
5915
5963
  await Promise.race([
5916
5964
  this.firstFramePromise || Promise.resolve(),
@@ -5918,6 +5966,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5918
5966
  ]);
5919
5967
  } catch {
5920
5968
  }
5969
+ const primingElapsed = Date.now() - primingStart;
5970
+ const { hasParamSets: hasParamSetsAfter } = this.flow.getFmtp();
5971
+ if (hasParamSetsAfter) {
5972
+ this.logger.info(
5973
+ `[rebroadcast] DESCRIBE priming: SPS/PPS received after ${primingElapsed}ms client=${clientId} path=${this.path}`
5974
+ );
5975
+ } else {
5976
+ this.logger.warn(
5977
+ `[rebroadcast] DESCRIBE priming: timed out after ${primingElapsed}ms without SPS/PPS \u2014 SDP will lack sprop-parameter-sets, downstream decoder may hang client=${clientId} path=${this.path}`
5978
+ );
5979
+ }
5921
5980
  }
5922
5981
  }
5923
5982
  {
@@ -5926,11 +5985,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5926
5985
  this.logger.info(
5927
5986
  `[BaichuanRtspServer] DESCRIBE SDP for ${clientId} path=${this.path} codec=${this.flow.sdpCodec} hasParamSets=${hasParamSets} fmtp=${fmtpPreview}`
5928
5987
  );
5929
- if (!hasParamSets) {
5930
- this.rtspDebugLog(
5931
- `DESCRIBE responding without parameter sets yet (client=${clientId}, path=${this.path}, flow=${this.flow.key})`
5932
- );
5933
- }
5934
5988
  }
5935
5989
  const sdp = this.generateSdp();
5936
5990
  sendResponse(
@@ -5980,7 +6034,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5980
6034
  seenFirstVideoKeyframe: false,
5981
6035
  setupTrack0: false,
5982
6036
  setupTrack1: false,
5983
- isPlaying: false
6037
+ isPlaying: false,
6038
+ connectTime
5984
6039
  });
5985
6040
  } else {
5986
6041
  existing.rtspSocket = socket;
@@ -6027,8 +6082,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6027
6082
  if (resources) {
6028
6083
  if (isTrack1) resources.setupTrack1 = true;
6029
6084
  else resources.setupTrack0 = true;
6030
- this.rtspDebugLog(
6031
- `SETUP done for ${clientId}: track0=${!!resources.setupTrack0} track1=${!!resources.setupTrack1} playing=${!!resources.isPlaying}`
6085
+ const transport2 = useTcpInterleaved ? "TCP/interleaved" : "UDP";
6086
+ const track = isTrack1 ? "track1(audio)" : "track0(video)";
6087
+ this.logger.info(
6088
+ `[rebroadcast] SETUP client=${clientId} ${track} transport=${transport2} session=${sessionId}`
6032
6089
  );
6033
6090
  }
6034
6091
  }
@@ -6053,8 +6110,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6053
6110
  const resources = this.clientResources.get(clientId);
6054
6111
  if (resources) {
6055
6112
  resources.isPlaying = true;
6056
- this.rtspDebugLog(
6057
- `PLAY for ${clientId}: track0=${!!resources.setupTrack0} track1=${!!resources.setupTrack1} playing=${!!resources.isPlaying}`
6113
+ const hasAudio = !!resources.setupTrack1;
6114
+ this.logger.info(
6115
+ `[rebroadcast] PLAY client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel} codec=${this.flow.sdpCodec} audio=${hasAudio} session=${sessionId}`
6058
6116
  );
6059
6117
  }
6060
6118
  }
@@ -6063,6 +6121,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6063
6121
  Range: "npt=0.000-"
6064
6122
  });
6065
6123
  } else if (method === "TEARDOWN") {
6124
+ this.logger.info(
6125
+ `[rebroadcast] TEARDOWN client=${clientId} session=${sessionId}`
6126
+ );
6066
6127
  cleanup();
6067
6128
  sendResponse(200, "OK", {
6068
6129
  Session: sessionId
@@ -6128,10 +6189,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6128
6189
  sdp += `a=control:track1\r
6129
6190
  `;
6130
6191
  }
6131
- sdp += `a=setup:passive\r
6132
- `;
6133
- sdp += `a=connection:new\r
6134
- `;
6135
6192
  return sdp;
6136
6193
  }
6137
6194
  /**
@@ -6157,7 +6214,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6157
6214
  this.logger.warn(
6158
6215
  `[BaichuanRtspServer] Could not fetch stream metadata: ${error}`
6159
6216
  );
6160
- streamMetadata = { frameRate: 25, width: 1920, height: 1080 };
6217
+ streamMetadata = { frameRate: 25 };
6161
6218
  }
6162
6219
  }
6163
6220
  const ffmpegFormat = this.flow.ffmpegFormat;
@@ -6202,6 +6259,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6202
6259
  return false;
6203
6260
  if (channel === audioRtpChannel && !resources2?.setupTrack1)
6204
6261
  return false;
6262
+ const buffered = rtspSocket.writableLength;
6263
+ if (buffered > 10 * 1024 * 1024) {
6264
+ this.logger.warn(
6265
+ `[rebroadcast] backpressure: ${Math.round(buffered / 1024)}KB buffered for client=${clientId} \u2014 disconnecting`
6266
+ );
6267
+ rtspSocket.destroy();
6268
+ return false;
6269
+ }
6205
6270
  try {
6206
6271
  return rtspSocket.write(frameRtpOverTcp(channel, msg));
6207
6272
  } catch (error) {
@@ -6631,6 +6696,24 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6631
6696
  let frameCount = 0;
6632
6697
  let lastFrameTime = Date.now();
6633
6698
  const targetFrameInterval = streamMetadata && streamMetadata.frameRate > 0 ? 1e3 / streamMetadata.frameRate : 40;
6699
+ const prebufferSnap = this.prebuffer.slice();
6700
+ let lastIdrIdx = -1;
6701
+ for (let i = prebufferSnap.length - 1; i >= 0; i--) {
6702
+ if (prebufferSnap[i].isKeyframe) {
6703
+ lastIdrIdx = i;
6704
+ break;
6705
+ }
6706
+ }
6707
+ const prebufferFrames = lastIdrIdx >= 0 ? prebufferSnap.slice(lastIdrIdx) : [];
6708
+ if (prebufferFrames.length > 0) {
6709
+ this.logger.info(
6710
+ `[rebroadcast] prebuffer replay client=${clientId} frames=${prebufferFrames.length} starting from IDR`
6711
+ );
6712
+ }
6713
+ const combined = async function* () {
6714
+ for (const entry of prebufferFrames) yield entry.frame;
6715
+ for await (const f of clientGenerator) yield f;
6716
+ };
6634
6717
  const feedFrames = async () => {
6635
6718
  try {
6636
6719
  this.rtspDebugLog(
@@ -6642,7 +6725,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6642
6725
  let firstVideoFrameSeenLogged = false;
6643
6726
  let h265WaitParamSetsLogged = false;
6644
6727
  let h265WaitIrapLogged = false;
6645
- for await (const frame of clientGenerator) {
6728
+ for await (const frame of combined()) {
6646
6729
  if (!this.connectedClients.has(clientId)) {
6647
6730
  this.rtspDebugLog(
6648
6731
  `Client ${clientId} disconnected, stopping frame feed`
@@ -6745,15 +6828,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6745
6828
  `Sent ${frameCount} frames to client ${clientId} (frame size: ${frame.data.length} bytes)`
6746
6829
  );
6747
6830
  }
6748
- const now = Date.now();
6749
- const timeSinceLastFrame = now - lastFrameTime;
6750
- const waitTime = targetFrameInterval - timeSinceLastFrame;
6751
- if (waitTime > 0) {
6752
- await new Promise(
6753
- (resolve) => setTimeout(resolve, Math.min(waitTime, targetFrameInterval * 2))
6754
- );
6831
+ if (!useDirectRtp) {
6832
+ const now = Date.now();
6833
+ const timeSinceLastFrame = now - lastFrameTime;
6834
+ const waitTime = targetFrameInterval - timeSinceLastFrame;
6835
+ if (waitTime > 0) {
6836
+ await new Promise(
6837
+ (resolve) => setTimeout(resolve, Math.min(waitTime, targetFrameInterval * 2))
6838
+ );
6839
+ }
6840
+ lastFrameTime = Date.now();
6755
6841
  }
6756
- lastFrameTime = Date.now();
6757
6842
  if (useDirectRtp) {
6758
6843
  const videoType = frame.videoType ?? this.flow.videoType;
6759
6844
  const normalizedVideoData = videoType === "H264" ? convertToAnnexB(frame.data) : convertToAnnexB2(frame.data);
@@ -6826,6 +6911,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6826
6911
  }
6827
6912
  if (!firstVideoWriteLogged) {
6828
6913
  firstVideoWriteLogged = true;
6914
+ const clientConnectTime = resources?.connectTime ?? Date.now();
6915
+ const ttffMs = Date.now() - clientConnectTime;
6916
+ this.logger.info(
6917
+ `[rebroadcast] first keyframe \u2192 client client=${clientId} codec=${videoType} ttff=${ttffMs}ms`
6918
+ );
6829
6919
  if (rtspDebug) {
6830
6920
  const headHex = frame.data.subarray(0, 16).toString("hex");
6831
6921
  rtspDebugLog(
@@ -6833,6 +6923,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6833
6923
  );
6834
6924
  }
6835
6925
  }
6926
+ if (resources) {
6927
+ resources.framesSent = (resources.framesSent ?? 0) + 1;
6928
+ }
6836
6929
  sendVideoAccessUnit(videoType, normalizedVideoData, true);
6837
6930
  } else {
6838
6931
  try {
@@ -6917,8 +7010,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6917
7010
  this.firstAudioPromise = new Promise((resolve) => {
6918
7011
  this.firstAudioResolve = resolve;
6919
7012
  });
6920
- this.rtspDebugLog(
6921
- `Starting native stream for profile ${this.profile} (waiting for camera to start transmitting...)`
7013
+ this.logger.info(
7014
+ `[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
6922
7015
  );
6923
7016
  await this.flow.startKeepAlive(this.api);
6924
7017
  this.nativeFanout = new NativeStreamFanout({
@@ -6956,11 +7049,41 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6956
7049
  if (hasParamSets) {
6957
7050
  this.markFirstFrameReceived();
6958
7051
  }
7052
+ const isKeyframe = this.isRawFrameKeyframe(frame);
7053
+ this.prebuffer.push({
7054
+ frame: { ...frame, data: Buffer.from(frame.data) },
7055
+ time: Date.now(),
7056
+ isKeyframe
7057
+ });
7058
+ const cutoff = Date.now() - this.PREBUFFER_MAX_MS;
7059
+ let trimIdx = 0;
7060
+ while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
7061
+ trimIdx++;
7062
+ }
7063
+ if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
6959
7064
  },
6960
7065
  onError: (error) => {
6961
7066
  this.logger.warn(
6962
7067
  `[BaichuanRtspServer] Shared native stream error: ${error}`
6963
7068
  );
7069
+ },
7070
+ onEnd: () => {
7071
+ if (!this.nativeStreamActive) return;
7072
+ this.nativeStreamActive = false;
7073
+ this.firstFrameReceived = false;
7074
+ this.firstFramePromise = null;
7075
+ this.firstFrameResolve = null;
7076
+ this.nativeFanout = null;
7077
+ this.prebuffer = [];
7078
+ this.logger.info(
7079
+ `[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
7080
+ );
7081
+ if (this.connectedClients.size > 0) {
7082
+ this.logger.info(
7083
+ `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
7084
+ );
7085
+ setImmediate(() => void this.startNativeStream());
7086
+ }
6964
7087
  }
6965
7088
  });
6966
7089
  this.nativeFanout.start();
@@ -6999,7 +7122,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6999
7122
  if (!this.nativeStreamActive) {
7000
7123
  return;
7001
7124
  }
7002
- this.rtspDebugLog(`Stopping native stream`);
7125
+ this.logger.info(
7126
+ `[rebroadcast] native stream stopping profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
7127
+ );
7003
7128
  this.flow.stopKeepAlive();
7004
7129
  this.clearNoClientAutoStopTimer();
7005
7130
  this.nativeStreamActive = false;
@@ -7018,6 +7143,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7018
7143
  this.nativeFanout = null;
7019
7144
  await fanout.stop();
7020
7145
  }
7146
+ this.prebuffer = [];
7021
7147
  if (this.tempStreamGenerator) {
7022
7148
  try {
7023
7149
  await this.tempStreamGenerator.return(void 0);
@@ -7033,9 +7159,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7033
7159
  if (this.connectedClients.has(clientId)) {
7034
7160
  this.connectedClients.delete(clientId);
7035
7161
  this.emit("clientDisconnected", clientId);
7036
- this.logger.info(
7037
- `[BaichuanRtspServer] RTSP client disconnected: ${clientId}`
7038
- );
7039
7162
  if (this.connectedClients.size === 0) {
7040
7163
  void this.stopNativeStream();
7041
7164
  }
@@ -7268,10 +7391,12 @@ function parseSupportXml(xml) {
7268
7391
  }
7269
7392
  function getSupportItemForChannel(support, channel) {
7270
7393
  if (!support?.items?.length) return void 0;
7271
- const scoreSupportItem = (item) => {
7394
+ const candidates = support.items.filter((i) => i.chnID === channel);
7395
+ if (!candidates.length) return void 0;
7396
+ const score = (item) => {
7272
7397
  const anyItem = item;
7273
- let score = 0;
7274
- if (anyItem.name == null) score += 2;
7398
+ let result = 0;
7399
+ if (anyItem.name == null) result += 100;
7275
7400
  const capabilityKeys = [
7276
7401
  "ptzType",
7277
7402
  "ptzControl",
@@ -7283,20 +7408,17 @@ function getSupportItemForChannel(support, channel) {
7283
7408
  "motion",
7284
7409
  "encCtrl",
7285
7410
  "newIspCfg",
7286
- "remoteAbility"
7411
+ "remoteAbility",
7412
+ "aitype",
7413
+ "videoClip",
7414
+ "snap"
7287
7415
  ];
7288
7416
  for (const k of capabilityKeys) {
7289
- if (anyItem[k] !== void 0) score += 3;
7417
+ if (anyItem[k] !== void 0) result += 3;
7290
7418
  }
7291
- score += Math.min(10, Math.max(0, Object.keys(anyItem).length - 1));
7292
- return score;
7293
- };
7294
- const pickBest = (chnId) => {
7295
- const candidates = support.items.filter((i) => i.chnID === chnId);
7296
- if (!candidates.length) return void 0;
7297
- return candidates.slice().sort((a, b) => scoreSupportItem(b) - scoreSupportItem(a))[0];
7419
+ return result;
7298
7420
  };
7299
- return pickBest(channel);
7421
+ return candidates.sort((a, b) => score(b) - score(a))[0];
7300
7422
  }
7301
7423
  function computeDeviceCapabilities(params) {
7302
7424
  const { channel } = params;
@@ -7328,6 +7450,7 @@ function computeDeviceCapabilities(params) {
7328
7450
  flat,
7329
7451
  /white\s*led|whiteLed|flood\s*light|floodlight/i
7330
7452
  );
7453
+ const hasSirenFromSupport = supportItem ? isTruthyNumberLike(supportItem.audioVersion) : false;
7331
7454
  const hasSirenFromAbilities = abilitiesHasAny(
7332
7455
  flat,
7333
7456
  /audio\s*alarm|audioAlarm|siren|pushAlarn|audioPlay/i
@@ -7340,6 +7463,9 @@ function computeDeviceCapabilities(params) {
7340
7463
  const hasPirFromSupport = supportItem ? isTruthyNumberLike(supportItem.rfCfg) || isTruthyNumberLike(supportItem.newRfCfg) || isTruthyNumberLike(supportItem.rfVersion) || isTruthyNumberLike(supportItem.battery) : false;
7341
7464
  const hasAutotrackingFromSupport = supportItem ? isTruthyNumberLike(supportItem.autoPt) || isTruthyNumberLike(supportItem.smartAI) : false;
7342
7465
  const hasAutotrackingFromAbilities = abilitiesHasAny(flat, /smartTrack/i);
7466
+ const hasBattery = hasBatteryFromSupport || hasBatteryFromAbilities;
7467
+ const isDoorbell = isDoorbellFromSupport || isDoorbellFromModel;
7468
+ const hasWirelessChimeFromAbilities = abilitiesHasAny(flat, /dingDong|dingdong/i);
7343
7469
  const hasPan = hasPanTiltFromSupport || hasPanTiltFromAbilities;
7344
7470
  const hasTilt = hasPanTiltFromSupport || hasPanTiltFromAbilities;
7345
7471
  const hasZoom = hasZoomFromSupport || hasZoomFromAbilities;
@@ -7355,14 +7481,15 @@ function computeDeviceCapabilities(params) {
7355
7481
  hasZoom: finalHasZoom,
7356
7482
  hasPresets: finalHasPresets,
7357
7483
  hasPtz: ptzDisabledBySupport ? false : hasPtzFromSupport || finalHasPan || finalHasTilt || finalHasZoom || finalHasPresets,
7358
- hasBattery: hasBatteryFromSupport || hasBatteryFromAbilities,
7484
+ hasBattery,
7359
7485
  hasIntercom: hasIntercomFromSupport,
7360
- hasSiren: hasSirenFromAbilities,
7486
+ hasSiren: hasSirenFromSupport || hasSirenFromAbilities,
7361
7487
  // lightType >= 2 indicates controllable white LED / floodlight (1 = IR only)
7362
7488
  hasFloodlight: Number.isFinite(lightType) ? lightType >= 2 : hasFloodlightFromAbilities,
7363
7489
  hasPir: hasPirFromAbilities || hasPirFromSupport,
7364
- isDoorbell: isDoorbellFromSupport || isDoorbellFromModel,
7365
- hasAutotracking: hasAutotrackingFromSupport || hasAutotrackingFromAbilities
7490
+ isDoorbell,
7491
+ hasAutotracking: ptzDisabledBySupport ? false : hasAutotrackingFromSupport || hasAutotrackingFromAbilities,
7492
+ hasWirelessChime: isDoorbell || hasWirelessChimeFromAbilities
7366
7493
  };
7367
7494
  if (ptzMode !== void 0) result.ptzMode = ptzMode;
7368
7495
  return result;
@@ -9237,6 +9364,161 @@ var discoverDeviceUidViaBaichuanGetP2p = async (params) => {
9237
9364
  return extractReolinkUidLike(p2pXml);
9238
9365
  };
9239
9366
 
9367
+ // src/reolink/baichuan/utils/chime.ts
9368
+ var buildDingDongGetParamsXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
9369
+ <body>
9370
+ <dingdongDeviceOpt version="1.1">
9371
+ <id>${chimeId}</id>
9372
+ <opt>getParam</opt>
9373
+ </dingdongDeviceOpt>
9374
+ </body>`;
9375
+ var buildDingDongSetParamsXml = (chimeId, params) => `<?xml version="1.0" encoding="UTF-8" ?>
9376
+ <body>
9377
+ <dingdongDeviceOpt version="1.1">
9378
+ <opt>setParam</opt>
9379
+ <id>${chimeId}</id>
9380
+ ${params.volLevel !== void 0 ? `<volLevel>${params.volLevel}</volLevel>` : ""}
9381
+ ${params.ledState !== void 0 ? `<ledState>${params.ledState}</ledState>` : ""}
9382
+ ${params.name !== void 0 ? `<name>${params.name}</name>` : ""}
9383
+ </dingdongDeviceOpt>
9384
+ </body>`;
9385
+ var buildDingDongRingXml = (chimeId, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
9386
+ <body>
9387
+ <dingdongDeviceOpt version="1.1">
9388
+ <id>${chimeId}</id>
9389
+ <opt>ringWithMusic</opt>
9390
+ <musicId>${musicId}</musicId>
9391
+ </dingdongDeviceOpt>
9392
+ </body>`;
9393
+ var buildSetDingDongCfgXml = (chimeId, eventType, state, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
9394
+ <body>
9395
+ <dingdongCfg version="1.1">
9396
+ <deviceCfg>
9397
+ <id>${chimeId}</id>
9398
+ <alarminCfg>
9399
+ <valid>${state}</valid>
9400
+ <musicId>${musicId}</musicId>
9401
+ <type>${eventType}</type>
9402
+ </alarminCfg>
9403
+ </deviceCfg>
9404
+ </dingdongCfg>
9405
+ </body>`;
9406
+ var buildGetDingDongCtrlXml = () => `<?xml version="1.0" encoding="UTF-8" ?>
9407
+ <body>
9408
+ <dingdongCtrl version="1.1">
9409
+ <opt>machineStateGet</opt>
9410
+ </dingdongCtrl>
9411
+ </body>`;
9412
+ var buildSetDingDongCtrlXml = (chimeType, enabled, time) => `<?xml version="1.0" encoding="UTF-8" ?>
9413
+ <body>
9414
+ <dingdongCtrl version="1.1">
9415
+ <opt>machineStateSet</opt>
9416
+ <type>${chimeType}</type>
9417
+ <bopen>${enabled}</bopen>
9418
+ <bsave>1</bsave>
9419
+ <time>${time}</time>
9420
+ </dingdongCtrl>
9421
+ </body>`;
9422
+ var buildQuickReplyPlayXml = (channel, fileId) => `<?xml version="1.0" encoding="UTF-8" ?>
9423
+ <body>
9424
+ <audioFileInfo version="1.1">
9425
+ <channelId>${channel}</channelId>
9426
+ <id>${fileId}</id>
9427
+ <timeout>0</timeout>
9428
+ </audioFileInfo>
9429
+ </body>`;
9430
+ var parseDingDongListFromXml = (xml) => {
9431
+ const devices = [];
9432
+ const blocks = getXmlBlocks(xml, "dingdongDeviceInfo");
9433
+ for (const block of blocks) {
9434
+ const idText = getXmlText(block, "deviceId") ?? getXmlText(block, "id");
9435
+ const name = getXmlText(block, "deviceName") ?? getXmlText(block, "name") ?? "";
9436
+ const netStateText = getXmlText(block, "netState") ?? getXmlText(block, "netstate");
9437
+ if (idText === void 0) continue;
9438
+ const id = Number(idText);
9439
+ if (!Number.isFinite(id)) continue;
9440
+ devices.push({
9441
+ id,
9442
+ name,
9443
+ netState: netStateText !== void 0 ? Number(netStateText) : 0
9444
+ });
9445
+ }
9446
+ return devices;
9447
+ };
9448
+ var parseDingDongParamsFromXml = (xml) => {
9449
+ const name = getXmlText(xml, "name");
9450
+ const volLevelText = getXmlText(xml, "volLevel");
9451
+ const ledStateText = getXmlText(xml, "ledState");
9452
+ const result = {};
9453
+ if (name !== void 0) result.name = name;
9454
+ if (volLevelText !== void 0) {
9455
+ const n = Number(volLevelText);
9456
+ if (Number.isFinite(n)) result.volLevel = n;
9457
+ }
9458
+ if (ledStateText !== void 0) {
9459
+ const n = Number(ledStateText);
9460
+ if (Number.isFinite(n)) result.ledState = n;
9461
+ }
9462
+ return result;
9463
+ };
9464
+ var parseDingDongCfgFromXml = (xml) => {
9465
+ const configs = [];
9466
+ const deviceBlocks = getXmlBlocks(xml, "deviceCfg");
9467
+ for (const deviceBlock of deviceBlocks) {
9468
+ const idText = getXmlText(deviceBlock, "ringId") ?? getXmlText(deviceBlock, "id");
9469
+ if (idText === void 0) continue;
9470
+ const id = Number(idText);
9471
+ if (!Number.isFinite(id)) continue;
9472
+ const typeMap = {};
9473
+ const alarmBlocks = getXmlBlocks(deviceBlock, "alarminCfg");
9474
+ for (const alarmBlock of alarmBlocks) {
9475
+ const type = getXmlText(alarmBlock, "type");
9476
+ if (!type) continue;
9477
+ const validText = getXmlText(alarmBlock, "switch") ?? getXmlText(alarmBlock, "valid");
9478
+ const musicIdText = getXmlText(alarmBlock, "musicId");
9479
+ typeMap[type] = {
9480
+ valid: validText !== void 0 ? Number(validText) : 0,
9481
+ musicId: musicIdText !== void 0 ? Number(musicIdText) : 0
9482
+ };
9483
+ }
9484
+ configs.push({ id, type: typeMap });
9485
+ }
9486
+ return configs;
9487
+ };
9488
+ var parseHardwiredChimeFromXml = (xml) => {
9489
+ const type = getXmlText(xml, "type") ?? "";
9490
+ const bopenText = getXmlText(xml, "bopen") ?? getXmlText(xml, "enable");
9491
+ const timeText = getXmlText(xml, "time");
9492
+ return {
9493
+ type,
9494
+ enabled: bopenText === "1",
9495
+ time: timeText !== void 0 ? Number(timeText) : 0
9496
+ };
9497
+ };
9498
+ var buildGetDingDongSilentXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
9499
+ <body>
9500
+ <dingdongSilentMode version="1.1">
9501
+ <id>${chimeId}</id>
9502
+ </dingdongSilentMode>
9503
+ </body>`;
9504
+ var buildSetDingDongSilentXml = (chimeId, time) => `<?xml version="1.0" encoding="UTF-8" ?>
9505
+ <body>
9506
+ <dingdongSilentMode version="1.1">
9507
+ <id>${chimeId}</id>
9508
+ <time>${time}</time>
9509
+ <type>63</type>
9510
+ </dingdongSilentMode>
9511
+ </body>`;
9512
+ var parseWirelessChimeSilentFromXml = (xml, chimeId) => {
9513
+ const timeText = getXmlText(xml, "time");
9514
+ const time = timeText !== void 0 ? Number(timeText) : 0;
9515
+ return {
9516
+ id: chimeId,
9517
+ time,
9518
+ active: time === 0
9519
+ };
9520
+ };
9521
+
9240
9522
  // src/reolink/baichuan/utils/eventsGetEvents.ts
9241
9523
  var parseAiTypeToken = (aiTypeRaw) => {
9242
9524
  const raw = (aiTypeRaw ?? "").trim();
@@ -9546,6 +9828,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
9546
9828
  host;
9547
9829
  username;
9548
9830
  password;
9831
+ /**
9832
+ * Set to `true` after `close()` is called.
9833
+ * Once closed, the API instance should not be reused.
9834
+ */
9835
+ _closed = false;
9549
9836
  // ─────────────────────────────────────────────────────────────────────────────
9550
9837
  // SOCKET POOL - Tag-based socket management
9551
9838
  // ─────────────────────────────────────────────────────────────────────────────
@@ -9575,10 +9862,194 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
9575
9862
  get client() {
9576
9863
  const entry = this.socketPool.get("general");
9577
9864
  if (!entry) {
9865
+ if (this._closed) {
9866
+ throw new Error(
9867
+ "[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
9868
+ );
9869
+ }
9578
9870
  throw new Error("[ReolinkBaichuanApi] General socket not initialized");
9579
9871
  }
9580
9872
  return entry.client;
9581
9873
  }
9874
+ /**
9875
+ * `true` after `close()` has been called. A closed API should not be reused;
9876
+ * the consumer should create a new instance.
9877
+ */
9878
+ get isClosed() {
9879
+ return this._closed;
9880
+ }
9881
+ /**
9882
+ * `true` when the API is usable: not closed, general socket exists, socket
9883
+ * is connected and the client is logged in.
9884
+ *
9885
+ * This is the recommended way for consumers to check whether the API is
9886
+ * still valid before issuing commands, instead of directly accessing
9887
+ * `api.client.isSocketConnected()` / `api.client.loggedIn` (which throws
9888
+ * if the socket pool was already destroyed).
9889
+ */
9890
+ get isReady() {
9891
+ if (this._closed) return false;
9892
+ const entry = this.socketPool.get("general");
9893
+ if (!entry) return false;
9894
+ try {
9895
+ return entry.client.isSocketConnected() && entry.client.loggedIn;
9896
+ } catch {
9897
+ return false;
9898
+ }
9899
+ }
9900
+ /** Promise tracking an in-flight reconnection from `ensureConnected()`. */
9901
+ _ensureConnectedPromise;
9902
+ /**
9903
+ * Ensure the "general" socket is connected and logged in.
9904
+ * If the socket is disconnected or the pool entry was destroyed, a new
9905
+ * general socket is created, logged in, and all event/push/guard listeners
9906
+ * are re-attached automatically.
9907
+ *
9908
+ * This is a **no-op** when the API is already {@link isReady}.
9909
+ *
9910
+ * @throws If `close()` was called — the API is permanently closed and a new
9911
+ * instance must be created.
9912
+ */
9913
+ async ensureConnected() {
9914
+ if (this._closed) {
9915
+ throw new Error(
9916
+ "[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
9917
+ );
9918
+ }
9919
+ if (this.isReady) return;
9920
+ if (this._ensureConnectedPromise) {
9921
+ return this._ensureConnectedPromise;
9922
+ }
9923
+ this._ensureConnectedPromise = this.reconnectGeneralSocket();
9924
+ try {
9925
+ await this._ensureConnectedPromise;
9926
+ } finally {
9927
+ this._ensureConnectedPromise = void 0;
9928
+ }
9929
+ }
9930
+ /**
9931
+ * Internal: destroy the current general socket (if any), create a new one,
9932
+ * login, and re-attach all listeners.
9933
+ */
9934
+ async reconnectGeneralSocket() {
9935
+ const oldEntry = this.socketPool.get("general");
9936
+ if (oldEntry) {
9937
+ oldEntry.client.removeAllListeners();
9938
+ if (oldEntry.idleCloseTimer) clearTimeout(oldEntry.idleCloseTimer);
9939
+ if (oldEntry.generalPermitRelease) {
9940
+ try {
9941
+ oldEntry.generalPermitRelease();
9942
+ } catch {
9943
+ }
9944
+ }
9945
+ this.socketPool.delete("general");
9946
+ try {
9947
+ await oldEntry.client.close({ reason: "reconnect", skipLogout: true });
9948
+ } catch {
9949
+ }
9950
+ }
9951
+ const newClient = new BaichuanClient(this.clientOptions);
9952
+ this.socketPool.set("general", {
9953
+ client: newClient,
9954
+ refCount: 1,
9955
+ // general socket is always "in use"
9956
+ createdAt: Date.now(),
9957
+ lastUsedAt: Date.now(),
9958
+ idleCloseTimer: void 0,
9959
+ generalPermitRelease: void 0
9960
+ });
9961
+ this.setupGeneralClientListeners();
9962
+ await this.client.login();
9963
+ this.logger.log?.(
9964
+ "[ReolinkBaichuanApi] General socket reconnected successfully"
9965
+ );
9966
+ if (this.simpleEventListeners.size > 0) {
9967
+ this.simpleEventSubscribed = false;
9968
+ this.simpleEventWatchdogRecoveryAttempts = 0;
9969
+ this.simpleEventWatchdogLastRecoveryAt = 0;
9970
+ try {
9971
+ await this.ensureSimpleEventSubscribed();
9972
+ this.simpleEventLastReceivedAt = Date.now();
9973
+ this.logger.log?.(
9974
+ `[ReolinkBaichuanApi] Events re-subscribed after reconnection (listeners=${this.simpleEventListeners.size})`
9975
+ );
9976
+ } catch (e) {
9977
+ (this.logger.debug ?? this.logger.log).call(
9978
+ this.logger,
9979
+ `[ReolinkBaichuanApi] Event re-subscribe after reconnection failed, watchdog will retry`,
9980
+ formatErrorForLog(e)
9981
+ );
9982
+ }
9983
+ }
9984
+ }
9985
+ /**
9986
+ * Attach event, push, channelInfo, and guard listeners to the current
9987
+ * "general" client. Called from the constructor and from
9988
+ * {@link reconnectGeneralSocket}.
9989
+ */
9990
+ setupGeneralClientListeners() {
9991
+ const client = this.client;
9992
+ client.on("event", (event) => {
9993
+ const mapped = mapToSimpleEvent(event);
9994
+ if (!mapped) return;
9995
+ this.dispatchSimpleEvent(mapped);
9996
+ });
9997
+ client.on("channelInfo", (xml) => {
9998
+ try {
9999
+ this.parseAndStoreChannelInfo(xml);
10000
+ } catch (e) {
10001
+ this.logger.warn?.(
10002
+ "[ReolinkBaichuanApi] Error parsing channel info from push",
10003
+ formatErrorForLog(e)
10004
+ );
10005
+ }
10006
+ });
10007
+ client.on("push", (frame) => {
10008
+ const cmdId = frame.header.cmdId;
10009
+ if (cmdId !== BC_CMD_ID_PUSH_VIDEO_INPUT && cmdId !== BC_CMD_ID_PUSH_SERIAL && cmdId !== BC_CMD_ID_PUSH_NET_INFO && cmdId !== BC_CMD_ID_PUSH_DINGDONG_LIST && cmdId !== BC_CMD_ID_PUSH_SLEEP_STATUS && cmdId !== BC_CMD_ID_PUSH_COORDINATE_POINT_LIST) {
10010
+ return;
10011
+ }
10012
+ try {
10013
+ if (frame.body.length === 0) return;
10014
+ const xml = client.tryDecryptXml(
10015
+ frame.body,
10016
+ frame.header.channelId,
10017
+ client.enc
10018
+ );
10019
+ if (!xml || !xml.startsWith("<?xml")) return;
10020
+ this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
10021
+ } catch (e) {
10022
+ this.logger.debug?.(
10023
+ "[ReolinkBaichuanApi] Error parsing settings push",
10024
+ formatErrorForLog(e)
10025
+ );
10026
+ }
10027
+ });
10028
+ if (this.rebootAfterDisconnectionsPerMinute > 0) {
10029
+ client.on("close", () => {
10030
+ try {
10031
+ void this.maybeRebootOnDisconnectStorm();
10032
+ } catch {
10033
+ }
10034
+ });
10035
+ }
10036
+ if (this.rebootAfterConsecutiveEconnreset > 0) {
10037
+ client.on("close", () => {
10038
+ try {
10039
+ void this.maybeRebootOnEconnresetStorm();
10040
+ } catch {
10041
+ }
10042
+ });
10043
+ }
10044
+ if (!this.sessionGuardIntervalTimer) {
10045
+ client.once("push", () => {
10046
+ void this.logActiveSessionsOnStartup();
10047
+ this.sessionGuardIntervalTimer = setInterval(() => {
10048
+ void this.maybeRebootOnTooManySessions();
10049
+ }, 6e4);
10050
+ });
10051
+ }
10052
+ }
9582
10053
  /**
9583
10054
  * Cached camera UID. May be initially undefined if not provided in the constructor.
9584
10055
  * Will be lazily populated on demand when needed (e.g. for recordings).
@@ -10519,42 +10990,6 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10519
10990
  logger: this.logger,
10520
10991
  debugConfig: generalClient.getDebugConfig?.()
10521
10992
  });
10522
- this.client.on("event", (event) => {
10523
- const mapped = mapToSimpleEvent(event);
10524
- if (!mapped) return;
10525
- this.dispatchSimpleEvent(mapped);
10526
- });
10527
- this.client.on("channelInfo", (xml) => {
10528
- try {
10529
- this.parseAndStoreChannelInfo(xml);
10530
- } catch (e) {
10531
- this.logger.warn?.(
10532
- "[ReolinkBaichuanApi] Error parsing channel info from push",
10533
- formatErrorForLog(e)
10534
- );
10535
- }
10536
- });
10537
- this.client.on("push", (frame) => {
10538
- const cmdId = frame.header.cmdId;
10539
- if (cmdId !== BC_CMD_ID_PUSH_VIDEO_INPUT && cmdId !== BC_CMD_ID_PUSH_SERIAL && cmdId !== BC_CMD_ID_PUSH_NET_INFO && cmdId !== BC_CMD_ID_PUSH_DINGDONG_LIST && cmdId !== BC_CMD_ID_PUSH_SLEEP_STATUS && cmdId !== BC_CMD_ID_PUSH_COORDINATE_POINT_LIST) {
10540
- return;
10541
- }
10542
- try {
10543
- if (frame.body.length === 0) return;
10544
- const xml = this.client.tryDecryptXml(
10545
- frame.body,
10546
- frame.header.channelId,
10547
- this.client.enc
10548
- );
10549
- if (!xml || !xml.startsWith("<?xml")) return;
10550
- this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
10551
- } catch (e) {
10552
- this.logger.debug?.(
10553
- "[ReolinkBaichuanApi] Error parsing settings push",
10554
- formatErrorForLog(e)
10555
- );
10556
- }
10557
- });
10558
10993
  const maxSessions = opts.maxDedicatedSessionsBeforeReboot;
10559
10994
  if (typeof maxSessions === "number" && Number.isFinite(maxSessions) && maxSessions > 0) {
10560
10995
  this.maxDedicatedSessionsBeforeReboot = Math.floor(maxSessions);
@@ -10563,32 +10998,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10563
10998
  if (typeof disconnectThreshold === "number" && Number.isFinite(disconnectThreshold)) {
10564
10999
  this.rebootAfterDisconnectionsPerMinute = Math.floor(disconnectThreshold);
10565
11000
  }
10566
- if (this.rebootAfterDisconnectionsPerMinute > 0) {
10567
- this.client.on("close", () => {
10568
- try {
10569
- void this.maybeRebootOnDisconnectStorm();
10570
- } catch {
10571
- }
10572
- });
10573
- }
10574
11001
  const econnresetThreshold = opts.rebootAfterConsecutiveEconnreset;
10575
11002
  if (typeof econnresetThreshold === "number" && Number.isFinite(econnresetThreshold)) {
10576
11003
  this.rebootAfterConsecutiveEconnreset = Math.floor(econnresetThreshold);
10577
11004
  }
10578
- if (this.rebootAfterConsecutiveEconnreset > 0) {
10579
- this.client.on("close", () => {
10580
- try {
10581
- void this.maybeRebootOnEconnresetStorm();
10582
- } catch {
10583
- }
10584
- });
10585
- }
10586
- this.client.once("push", () => {
10587
- void this.logActiveSessionsOnStartup();
10588
- this.sessionGuardIntervalTimer = setInterval(() => {
10589
- void this.maybeRebootOnTooManySessions();
10590
- }, 6e4);
10591
- });
11005
+ this.setupGeneralClientListeners();
10592
11006
  }
10593
11007
  /**
10594
11008
  * CGI forward: fetch RTSP URL for a channel via `GetRtspUrl`.
@@ -11419,6 +11833,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11419
11833
  );
11420
11834
  }
11421
11835
  async close(options) {
11836
+ if (this._closed) return;
11837
+ this._closed = true;
11422
11838
  if (this.sessionGuardIntervalTimer) {
11423
11839
  clearInterval(this.sessionGuardIntervalTimer);
11424
11840
  this.sessionGuardIntervalTimer = void 0;
@@ -11481,7 +11897,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11481
11897
  }
11482
11898
  async handleSendXml400(params, frame, retry) {
11483
11899
  const emptyBody = frame.body.length === 0;
11484
- const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes: camera sleeping/waking (battery), expired session, invalid username/password, or unsupported command on NVR/Hub.";
11900
+ const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes: expired session, invalid username/password, or unsupported command on NVR/Hub.";
11485
11901
  if (this.isSendXmlFailFast400(params, frame.body.length)) {
11486
11902
  throw new Error(emptyBody400Msg);
11487
11903
  }
@@ -11997,11 +12413,50 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11997
12413
  * Minimal per-channel inventory for NVR-connected devices.
11998
12414
  *
11999
12415
  * Intended to be fast: avoids AI/abilities and returns only the common identity + battery hints.
12416
+ *
12417
+ * @param options.source - Data source for the channel list (default: `"cgi"`):
12418
+ * - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
12419
+ * no dependency on async push messages. Recommended for first-call discovery.
12420
+ * - `"baichuan"`: Uses the cmd_id 145 push cache populated when the NVR sends channel
12421
+ * info after login + event subscription. This push is *asynchronous*: if it has not
12422
+ * arrived yet, the result will have zero channels. Callers must retry (nvr.ts does this
12423
+ * with a 1-second loop). Note: explicitly requesting cmd_id 145 is not supported.
12000
12424
  */
12001
12425
  async getNvrChannelsSummary(options) {
12002
- const source = options?.source ?? "baichuan";
12003
- const pushInfo = this.getChannelInfoFromPushCache();
12004
- const channels = (options?.channels?.length ? options.channels : Array.from(pushInfo.keys())).map((c) => Number(c)).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
12426
+ const source = options?.source ?? "cgi";
12427
+ let channels;
12428
+ const cgiStatusByChannel = /* @__PURE__ */ new Map();
12429
+ if (options?.channels?.length) {
12430
+ channels = options.channels.map((c) => Number(c)).filter((n) => Number.isFinite(n));
12431
+ } else if (source === "cgi") {
12432
+ try {
12433
+ const { channels: cgiChannels, channelsResponse } = await this.cgiApi.getChannels();
12434
+ const status = channelsResponse?.[0]?.value?.status ?? [];
12435
+ for (const s of status) {
12436
+ const ch = Number(s?.channel);
12437
+ if (!Number.isFinite(ch)) continue;
12438
+ cgiStatusByChannel.set(ch, {
12439
+ ...s.name != null ? { name: s.name } : {},
12440
+ ...s.uid != null ? { uid: s.uid } : {},
12441
+ sleeping: s.sleep === 1
12442
+ });
12443
+ }
12444
+ channels = cgiChannels;
12445
+ this.logger.debug?.(
12446
+ `[ReolinkBaichuanApi] getNvrChannelsSummary: CGI found ${channels.length} channel(s): [${channels.join(", ")}]`
12447
+ );
12448
+ } catch (e) {
12449
+ const msg = e instanceof Error ? e.message : String(e);
12450
+ this.logger.warn?.(
12451
+ `[ReolinkBaichuanApi] getNvrChannelsSummary: CGI GetChannelstatus failed (${msg}), returning empty`
12452
+ );
12453
+ channels = [];
12454
+ }
12455
+ } else {
12456
+ const pushInfo2 = this.getChannelInfoFromPushCache();
12457
+ channels = Array.from(pushInfo2.keys()).map((c) => Number(c)).filter((n) => Number.isFinite(n));
12458
+ }
12459
+ channels = channels.sort((a, b) => a - b);
12005
12460
  const support = await this.getSupportInfo().catch(() => {
12006
12461
  this.logger.error?.(
12007
12462
  "[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
@@ -12031,7 +12486,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12031
12486
  );
12032
12487
  }
12033
12488
  }
12034
- const cacheKey = `baichuan:${channels.join(",")}`;
12489
+ const cacheKey = `${source}:${channels.join(",")}`;
12035
12490
  const cached = this.nvrChannelsSummaryCache.get(cacheKey);
12036
12491
  if (cached) {
12037
12492
  return {
@@ -12052,8 +12507,10 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12052
12507
  } catch {
12053
12508
  }
12054
12509
  }
12510
+ const pushInfo = this.getChannelInfoFromPushCache();
12055
12511
  const devices = channels.map((channel) => {
12056
- const cached2 = pushInfo.get(channel);
12512
+ const pushCached = pushInfo.get(channel);
12513
+ const cgiStatus = cgiStatusByChannel.get(channel);
12057
12514
  const info = infoPerChannel.get(channel);
12058
12515
  const networkInfo = networkInfoPerChannel.get(channel);
12059
12516
  const isBattery = isBatteryByChannel.get(channel) ?? false;
@@ -12061,6 +12518,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12061
12518
  const isDoorbell = (isDoorbellByChannel.get(channel) ?? false) || /doorbell/i.test(model);
12062
12519
  const normalizedModel = model ? model.trim() : void 0;
12063
12520
  const isMultifocal = normalizedModel ? isDualLenseModel(normalizedModel) : false;
12521
+ const name = pushCached?.name || cgiStatus?.name || "";
12522
+ const uid = pushCached?.uid || cgiStatus?.uid || "";
12523
+ const sleeping = pushCached?.sleeping ?? cgiStatus?.sleeping;
12064
12524
  return {
12065
12525
  channel,
12066
12526
  isBattery,
@@ -12070,19 +12530,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12070
12530
  ...networkInfo?.ip ? { ip: networkInfo.ip } : {},
12071
12531
  ...networkInfo?.mac ? { mac: networkInfo.mac } : {},
12072
12532
  ...networkInfo?.activeLink ? { activeLink: networkInfo.activeLink } : {},
12073
- ...cached2?.name ? { name: cached2.name } : {},
12074
- ...cached2?.uid ? { uid: cached2.uid } : {},
12075
- ...cached2?.state ? { state: cached2.state } : {},
12076
- ...typeof cached2?.index === "number" ? { index: cached2.index } : {},
12077
- ...cached2?.streamSupport?.length ? { streamSupport: cached2.streamSupport } : {},
12078
- ...cached2?.wifiState ? { wifiState: cached2.wifiState } : {},
12079
- ...cached2?.networkSegment ? { networkSegment: cached2.networkSegment } : {},
12080
- ...typeof cached2?.changed === "boolean" ? { changed: cached2.changed } : {},
12081
- ...typeof cached2?.abilityChanged === "boolean" ? { abilityChanged: cached2.abilityChanged } : {},
12082
- ...typeof cached2?.online === "boolean" ? { online: cached2.online } : {},
12083
- ...typeof cached2?.sleeping === "boolean" ? { sleeping: cached2.sleeping } : {},
12084
- ...cached2?.loginState ? { loginState: cached2.loginState } : {},
12085
- ...typeof cached2?.updatedAtMs === "number" ? { updatedAtMs: cached2.updatedAtMs } : {}
12533
+ ...name ? { name } : {},
12534
+ ...uid ? { uid } : {},
12535
+ ...pushCached?.state ? { state: pushCached.state } : {},
12536
+ ...typeof pushCached?.index === "number" ? { index: pushCached.index } : {},
12537
+ ...pushCached?.streamSupport?.length ? { streamSupport: pushCached.streamSupport } : {},
12538
+ ...pushCached?.wifiState ? { wifiState: pushCached.wifiState } : {},
12539
+ ...pushCached?.networkSegment ? { networkSegment: pushCached.networkSegment } : {},
12540
+ ...typeof pushCached?.changed === "boolean" ? { changed: pushCached.changed } : {},
12541
+ ...typeof pushCached?.abilityChanged === "boolean" ? { abilityChanged: pushCached.abilityChanged } : {},
12542
+ ...typeof pushCached?.online === "boolean" ? { online: pushCached.online } : {},
12543
+ ...typeof sleeping === "boolean" ? { sleeping } : {},
12544
+ ...pushCached?.loginState ? { loginState: pushCached.loginState } : {},
12545
+ ...typeof pushCached?.updatedAtMs === "number" ? { updatedAtMs: pushCached.updatedAtMs } : {}
12086
12546
  };
12087
12547
  });
12088
12548
  const result = { channels, devices };
@@ -16350,13 +16810,12 @@ ${xml}`
16350
16810
  ]);
16351
16811
  const support = supportResult.status === "fulfilled" ? supportResult.value : void 0;
16352
16812
  const abilities = abilitiesResult.status === "fulfilled" ? abilitiesResult.value : void 0;
16353
- const supportItem = this.pickBestSupportItem(support, ch);
16354
- const capabilities = this.parseCapabilitiesFromSupport(
16355
- ch,
16356
- supportItem,
16357
- support,
16358
- abilities
16359
- );
16813
+ const supportItem = getSupportItemForChannel(support, ch);
16814
+ const capabilities = computeDeviceCapabilities({
16815
+ channel: ch,
16816
+ ...support != null && { support },
16817
+ ...abilities != null && { abilities }
16818
+ });
16360
16819
  const item = supportItem;
16361
16820
  const lightType = item?.lightType;
16362
16821
  const ledCtrl = item?.ledCtrl;
@@ -16372,6 +16831,25 @@ ${xml}`
16372
16831
  });
16373
16832
  capabilities.hasFloodlight = probed;
16374
16833
  }
16834
+ let dingDongListIds;
16835
+ let dingDongCfgIds;
16836
+ let wirelessChimeError;
16837
+ if (capabilities.hasWirelessChime) {
16838
+ try {
16839
+ const list = await this.getDingDongList(ch);
16840
+ dingDongListIds = list.map((d) => d.id);
16841
+ const first = list[0];
16842
+ const fromList = first !== void 0 && first.id >= 0;
16843
+ if (!fromList) {
16844
+ const configs = await this.getDingDongCfg(ch);
16845
+ dingDongCfgIds = configs.map((c) => c.id);
16846
+ capabilities.hasWirelessChime = configs.some((c) => c.id >= 0);
16847
+ }
16848
+ } catch (e) {
16849
+ capabilities.hasWirelessChime = false;
16850
+ wirelessChimeError = e instanceof Error ? e.message : String(e);
16851
+ }
16852
+ }
16375
16853
  const features = this.parseFeaturesFromSupport(support);
16376
16854
  const objects = await this.getAiDetectTypes(ch, { timeoutMs: 1500 });
16377
16855
  const autotrackingProbed = await this.probeAutotrackingSupport(ch, {
@@ -16408,7 +16886,10 @@ ${xml}`
16408
16886
  ...abilities && {
16409
16887
  abilityMergedKeyCount: Object.keys(abilities).length
16410
16888
  },
16411
- ...support?.items && { supportItemCount: support.items.length }
16889
+ ...support?.items && { supportItemCount: support.items.length },
16890
+ ...dingDongListIds !== void 0 && { dingDongListIds },
16891
+ ...dingDongCfgIds !== void 0 && { dingDongCfgIds },
16892
+ ...wirelessChimeError !== void 0 && { wirelessChimeError }
16412
16893
  };
16413
16894
  const result = {
16414
16895
  capabilities,
@@ -16435,90 +16916,6 @@ ${xml}`
16435
16916
  this.deviceCapabilitiesCache.clear();
16436
16917
  }
16437
16918
  }
16438
- /**
16439
- * Pick the best SupportItem for a channel.
16440
- * Prefers items without a name (capability items) over named items (googleHome, amazonAlexa).
16441
- */
16442
- pickBestSupportItem(support, channel) {
16443
- if (!support?.items?.length) return void 0;
16444
- const candidates = support.items.filter((i) => i.chnID === channel);
16445
- if (!candidates.length) return void 0;
16446
- const score = (item) => {
16447
- const anyItem = item;
16448
- let result = 0;
16449
- if (anyItem.name == null) result += 100;
16450
- const capabilityKeys = [
16451
- "ptzType",
16452
- "ptzControl",
16453
- "ptzPreset",
16454
- "ledCtrl",
16455
- "lightType",
16456
- "battery",
16457
- "audioVersion",
16458
- "motion",
16459
- "encCtrl",
16460
- "newIspCfg",
16461
- "remoteAbility",
16462
- "aitype",
16463
- "videoClip",
16464
- "snap"
16465
- ];
16466
- for (const k of capabilityKeys) {
16467
- if (anyItem[k] !== void 0) result += 3;
16468
- }
16469
- return result;
16470
- };
16471
- return candidates.sort((a, b) => score(b) - score(a))[0];
16472
- }
16473
- /**
16474
- * Parse device capabilities from SupportInfo.
16475
- * Uses SupportInfo as the single source of truth with AbilityInfo as fallback.
16476
- */
16477
- parseCapabilitiesFromSupport(channel, supportItem, support, abilities) {
16478
- const truthy = (v) => {
16479
- if (typeof v === "number") return v > 0;
16480
- if (typeof v === "string") {
16481
- const n = Number(v);
16482
- return Number.isFinite(n) ? n > 0 : v.length > 0 && v !== "0";
16483
- }
16484
- return Boolean(v);
16485
- };
16486
- const item = supportItem;
16487
- const ptzMode = support?.ptzMode?.toLowerCase();
16488
- const ptzType = item ? truthy(item.ptzType) : false;
16489
- const ptzControl = item ? truthy(item.ptzControl) : false;
16490
- const hasPtzFromItem = ptzType || ptzControl;
16491
- const hasPtzFromMode = ptzMode ? ptzMode !== "none" && ptzMode !== "0" : false;
16492
- const hasPanTilt = ptzMode ? ptzMode.includes("pt") || ptzMode === "ptz" : hasPtzFromItem;
16493
- const hasZoom = ptzMode ? ptzMode.includes("z") : hasPtzFromItem;
16494
- const hasPresets = item ? truthy(item.ptzPreset) : false;
16495
- const hasBattery = item ? truthy(item.battery) : false;
16496
- const hasSiren = item ? truthy(item.audioVersion) : false;
16497
- const lightType = item?.lightType;
16498
- const hasFloodlight = typeof lightType === "number" ? lightType >= 2 : false;
16499
- const hasPir = item ? truthy(item.rfCfg) || truthy(item.newRfCfg) || truthy(item.rfVersion) : false;
16500
- const isDoorbell = item ? truthy(item.doorbellVersion) : false;
16501
- const hasIntercom = truthy(support?.audioTalk) || (item ? truthy(item.ipcAudioTalk) : false);
16502
- return {
16503
- channel,
16504
- ...ptzMode && { ptzMode },
16505
- hasPan: hasPanTilt,
16506
- hasTilt: hasPanTilt,
16507
- hasZoom,
16508
- hasPresets,
16509
- hasPtz: hasPtzFromItem || hasPtzFromMode || hasPanTilt || hasZoom,
16510
- hasBattery,
16511
- hasIntercom,
16512
- hasSiren,
16513
- hasFloodlight,
16514
- hasPir,
16515
- isDoorbell,
16516
- // Autotracking: explicit flags only (autoPt or smartAI)
16517
- // Note: the heuristic (ptzControl && aitype) was too aggressive and caused false positives
16518
- // on cameras that have PTZ and AI detection but NOT autotracking capability.
16519
- hasAutotracking: item ? truthy(item.autoPt) || truthy(item.smartAI) : false
16520
- };
16521
- }
16522
16919
  /**
16523
16920
  * Parse support features from SupportInfo.
16524
16921
  */
@@ -17287,7 +17684,7 @@ ${xml}`
17287
17684
  * @returns Test results for all stream types and profiles
17288
17685
  */
17289
17686
  async testChannelStreams(channel, logger) {
17290
- const { testChannelStreams } = await import("./DiagnosticsTools-NUMCYEKQ.js");
17687
+ const { testChannelStreams } = await import("./DiagnosticsTools-FNLGCOVA.js");
17291
17688
  return await testChannelStreams({
17292
17689
  api: this,
17293
17690
  channel: this.normalizeChannel(channel),
@@ -17303,7 +17700,7 @@ ${xml}`
17303
17700
  * @returns Complete diagnostics for all channels and streams
17304
17701
  */
17305
17702
  async collectMultifocalDiagnostics(logger) {
17306
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-NUMCYEKQ.js");
17703
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-FNLGCOVA.js");
17307
17704
  return await collectMultifocalDiagnostics({
17308
17705
  api: this,
17309
17706
  logger
@@ -19391,6 +19788,216 @@ ${scheduleItems}
19391
19788
  const channel = 0;
19392
19789
  return await this.getSnapshot(channel);
19393
19790
  }
19791
+ // --------------------
19792
+ // Chime / DingDong APIs
19793
+ // --------------------
19794
+ /**
19795
+ * Get the list of paired wireless chime devices.
19796
+ * cmd_id: 484 (GetDingDongList)
19797
+ *
19798
+ * @param channel - Channel number (0-based, default 0)
19799
+ * @returns Array of paired chime devices
19800
+ */
19801
+ async getDingDongList(channel) {
19802
+ const ch = this.normalizeChannel(channel);
19803
+ const xml = await this.sendXml({
19804
+ cmdId: BC_CMD_ID_GET_DING_DONG_LIST,
19805
+ channel: ch
19806
+ });
19807
+ return parseDingDongListFromXml(xml);
19808
+ }
19809
+ /**
19810
+ * Get parameters (name, volume, LED state) for a specific wireless chime.
19811
+ * cmd_id: 485 (DingDongOpt, option getParam)
19812
+ *
19813
+ * @param chimeId - The chime device ID
19814
+ * @param channel - Channel number (0-based, default 0)
19815
+ * @returns Chime parameters
19816
+ */
19817
+ async getDingDongParams(chimeId, channel) {
19818
+ const ch = this.normalizeChannel(channel);
19819
+ const payloadXml = buildDingDongGetParamsXml(chimeId);
19820
+ const xml = await this.sendXml({
19821
+ cmdId: BC_CMD_ID_DING_DONG_OPT,
19822
+ channel: ch,
19823
+ payloadXml
19824
+ });
19825
+ return parseDingDongParamsFromXml(xml);
19826
+ }
19827
+ /**
19828
+ * Set parameters (name, volume, LED state) for a specific wireless chime.
19829
+ * cmd_id: 485 (DingDongOpt, option setParam)
19830
+ *
19831
+ * @param chimeId - The chime device ID
19832
+ * @param params - Parameters to set (volLevel, ledState, name)
19833
+ * @param channel - Channel number (0-based, default 0)
19834
+ */
19835
+ async setDingDongParams(chimeId, params, channel) {
19836
+ const ch = this.normalizeChannel(channel);
19837
+ const payloadXml = buildDingDongSetParamsXml(chimeId, params);
19838
+ await this.sendXml({
19839
+ cmdId: BC_CMD_ID_DING_DONG_OPT,
19840
+ channel: ch,
19841
+ payloadXml
19842
+ });
19843
+ }
19844
+ /**
19845
+ * Trigger a wireless chime to ring with a specific ringtone.
19846
+ * cmd_id: 485 (DingDongOpt, option ringWithMusic)
19847
+ *
19848
+ * @param chimeId - The chime device ID
19849
+ * @param musicId - The ringtone/music ID to play
19850
+ * @param channel - Channel number (0-based, default 0)
19851
+ */
19852
+ async ringDingDong(chimeId, musicId, channel) {
19853
+ const ch = this.normalizeChannel(channel);
19854
+ const payloadXml = buildDingDongRingXml(chimeId, musicId);
19855
+ await this.sendXml({
19856
+ cmdId: BC_CMD_ID_DING_DONG_OPT,
19857
+ channel: ch,
19858
+ payloadXml
19859
+ });
19860
+ }
19861
+ /**
19862
+ * Get the per-event alarm configuration for paired wireless chimes.
19863
+ * cmd_id: 486 (GetDingDongCfg)
19864
+ *
19865
+ * @param channel - Channel number (0-based, default 0)
19866
+ * @returns Array of chime configurations (one per paired chime)
19867
+ */
19868
+ async getDingDongCfg(channel) {
19869
+ const ch = this.normalizeChannel(channel);
19870
+ const xml = await this.sendXml({
19871
+ cmdId: BC_CMD_ID_GET_DING_DONG_CFG,
19872
+ channel: ch
19873
+ });
19874
+ return parseDingDongCfgFromXml(xml);
19875
+ }
19876
+ /**
19877
+ * Set the per-event alarm configuration for a specific wireless chime.
19878
+ * cmd_id: 487 (SetDingDongCfg)
19879
+ *
19880
+ * @param chimeId - The chime ring/device ID
19881
+ * @param eventType - Event type string (e.g. "doorbell", "package", "people")
19882
+ * @param state - 0 = disabled, 1 = enabled
19883
+ * @param musicId - Ringtone ID to use for this event type
19884
+ * @param channel - Channel number (0-based, default 0)
19885
+ */
19886
+ async setDingDongCfg(chimeId, eventType, state, musicId, channel) {
19887
+ const ch = this.normalizeChannel(channel);
19888
+ const payloadXml = buildSetDingDongCfgXml(chimeId, eventType, state, musicId);
19889
+ await this.sendXml({
19890
+ cmdId: BC_CMD_ID_SET_DING_DONG_CFG,
19891
+ channel: ch,
19892
+ payloadXml
19893
+ });
19894
+ }
19895
+ /** Cache of last known hardwired chime state per channel, used to avoid re-fetching on every set. */
19896
+ _hardwiredChimeCache = /* @__PURE__ */ new Map();
19897
+ /**
19898
+ * Get the hardwired (wired-in) chime state.
19899
+ * cmd_id: 483 (GetDingDongCtrl)
19900
+ *
19901
+ * Note: calling this may briefly trigger the physical chime to rattle.
19902
+ *
19903
+ * @param channel - Channel number (0-based, default 0)
19904
+ * @returns Hardwired chime state (type, enabled, time)
19905
+ */
19906
+ async getHardwiredChime(channel) {
19907
+ const ch = this.normalizeChannel(channel);
19908
+ const payloadXml = buildGetDingDongCtrlXml();
19909
+ const xml = await this.sendXml({
19910
+ cmdId: BC_CMD_ID_DING_DONG_CTRL,
19911
+ channel: ch,
19912
+ payloadXml
19913
+ });
19914
+ const state = parseHardwiredChimeFromXml(xml);
19915
+ this._hardwiredChimeCache.set(ch, state);
19916
+ return state;
19917
+ }
19918
+ /**
19919
+ * Set the hardwired (wired-in) chime state.
19920
+ * cmd_id: 483 (SetDingDongCtrl)
19921
+ *
19922
+ * Uses the cached state from a previous getHardwiredChime call to fill in
19923
+ * missing type/time fields, avoiding a double round-trip on every set.
19924
+ * Falls back to fetching if no cache is available.
19925
+ *
19926
+ * @param params - Chime configuration (type, enabled, time)
19927
+ * @param channel - Channel number (0-based, default 0)
19928
+ */
19929
+ async setHardwiredChime(params, channel) {
19930
+ const ch = this.normalizeChannel(channel);
19931
+ let current = this._hardwiredChimeCache.get(ch);
19932
+ if (!current) {
19933
+ current = await this.getHardwiredChime(ch);
19934
+ }
19935
+ const chimeType = params.type ?? current.type;
19936
+ const enabled = params.enabled ? 1 : 0;
19937
+ const time = params.time ?? current.time;
19938
+ const payloadXml = buildSetDingDongCtrlXml(chimeType, enabled, time);
19939
+ const xml = await this.sendXml({
19940
+ cmdId: BC_CMD_ID_DING_DONG_CTRL,
19941
+ channel: ch,
19942
+ payloadXml
19943
+ });
19944
+ const newState = parseHardwiredChimeFromXml(xml);
19945
+ this._hardwiredChimeCache.set(ch, newState);
19946
+ return newState;
19947
+ }
19948
+ /**
19949
+ * Play an audio file on the doorbell / chime device.
19950
+ * cmd_id: 349 (QuickReplyPlay)
19951
+ *
19952
+ * @param fileId - The audio file ID to play
19953
+ * @param channel - Channel number (0-based, default 0)
19954
+ */
19955
+ async quickReplyPlay(fileId, channel) {
19956
+ const ch = this.normalizeChannel(channel);
19957
+ const payloadXml = buildQuickReplyPlayXml(ch, fileId);
19958
+ await this.sendXml({
19959
+ cmdId: BC_CMD_ID_QUICK_REPLY_PLAY,
19960
+ channel: ch,
19961
+ payloadXml
19962
+ });
19963
+ }
19964
+ /**
19965
+ * Get the silent mode state of a paired wireless chime.
19966
+ * cmd_id: 609 (GetDingDongSilent)
19967
+ *
19968
+ * @param chimeId - The wireless chime device ID (from getDingDongList)
19969
+ * @param channel - Channel number (0-based, default 0)
19970
+ * @returns Wireless chime silent state (time=0 means active/not silenced)
19971
+ */
19972
+ async getDingDongSilent(chimeId, channel) {
19973
+ const ch = this.normalizeChannel(channel);
19974
+ const payloadXml = buildGetDingDongSilentXml(chimeId);
19975
+ const xml = await this.sendXml({
19976
+ cmdId: BC_CMD_ID_GET_DING_DONG_SILENT,
19977
+ channel: ch,
19978
+ payloadXml
19979
+ });
19980
+ return parseWirelessChimeSilentFromXml(xml, chimeId);
19981
+ }
19982
+ /**
19983
+ * Set the silent mode of a paired wireless chime.
19984
+ * cmd_id: 610 (SetDingDongSilent)
19985
+ *
19986
+ * @param chimeId - The wireless chime device ID (from getDingDongList)
19987
+ * @param time - Silence duration in seconds. 0 = not silenced (chime active), >0 = silenced for this many seconds.
19988
+ * @param channel - Channel number (0-based, default 0)
19989
+ * @returns Updated wireless chime silent state
19990
+ */
19991
+ async setDingDongSilent(chimeId, time, channel) {
19992
+ const ch = this.normalizeChannel(channel);
19993
+ const payloadXml = buildSetDingDongSilentXml(chimeId, time);
19994
+ const xml = await this.sendXml({
19995
+ cmdId: BC_CMD_ID_SET_DING_DONG_SILENT,
19996
+ channel: ch,
19997
+ payloadXml
19998
+ });
19999
+ return parseWirelessChimeSilentFromXml(xml, chimeId);
20000
+ }
19394
20001
  };
19395
20002
 
19396
20003
  // src/reolink/discovery.ts
@@ -20388,6 +20995,7 @@ export {
20388
20995
  flattenAbilitiesForChannel,
20389
20996
  abilitiesHasAny,
20390
20997
  parseSupportXml,
20998
+ getSupportItemForChannel,
20391
20999
  computeDeviceCapabilities,
20392
21000
  DUAL_LENS_DUAL_MOTION_MODELS,
20393
21001
  DUAL_LENS_SINGLE_MOTION_MODELS,
@@ -20406,4 +21014,4 @@ export {
20406
21014
  isTcpFailureThatShouldFallbackToUdp,
20407
21015
  autoDetectDeviceType
20408
21016
  };
20409
- //# sourceMappingURL=chunk-PCPEXOWB.js.map
21017
+ //# sourceMappingURL=chunk-RWYEGEWG.js.map