@apocaliss92/nodelink-js 0.2.1 → 0.2.2

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
  }
@@ -5717,7 +5727,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5717
5727
  this.logger.warn(
5718
5728
  `[BaichuanRtspServer] Could not get stream metadata: ${error}`
5719
5729
  );
5720
- this.streamMetadata = { frameRate: 25, width: 1920, height: 1080 };
5730
+ this.streamMetadata = { frameRate: 25 };
5721
5731
  this.setFlowVideoType("H264", "metadata unavailable");
5722
5732
  }
5723
5733
  this.clientConnectionServer = net2.createServer((socket) => {
@@ -5749,7 +5759,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5749
5759
  */
5750
5760
  handleRtspConnection(socket) {
5751
5761
  const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
5752
- this.logger.info(`[BaichuanRtspServer] RTSP client connected: ${clientId}`);
5762
+ const connectTime = Date.now();
5763
+ this.logger.info(
5764
+ `[rebroadcast] client connected client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel}`
5765
+ );
5753
5766
  let sessionId = "";
5754
5767
  let buffer = Buffer.alloc(0);
5755
5768
  let clientFfmpeg;
@@ -5757,6 +5770,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5757
5770
  let clientUdpSocket = null;
5758
5771
  let clientUdpSocketAudio = null;
5759
5772
  const cleanup = () => {
5773
+ const sessionDurationMs = Date.now() - connectTime;
5774
+ const res = this.clientResources.get(clientId);
5775
+ const framesSent = res?.framesSent ?? 0;
5776
+ this.logger.info(
5777
+ `[rebroadcast] client disconnected client=${clientId} path=${this.path} profile=${this.profile} duration=${sessionDurationMs}ms frames=${framesSent}`
5778
+ );
5760
5779
  this.removeClient(clientId);
5761
5780
  this.authNonces.delete(clientId);
5762
5781
  const resources = this.clientResources.get(clientId);
@@ -5898,7 +5917,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5898
5917
  Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
5899
5918
  });
5900
5919
  } else if (method === "DESCRIBE") {
5901
- if (!this.firstFrameReceived && this.connectedClients.size === 0) {
5920
+ if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
5902
5921
  try {
5903
5922
  if (!this.nativeStreamActive) {
5904
5923
  await this.startNativeStream();
@@ -5980,7 +5999,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
5980
5999
  seenFirstVideoKeyframe: false,
5981
6000
  setupTrack0: false,
5982
6001
  setupTrack1: false,
5983
- isPlaying: false
6002
+ isPlaying: false,
6003
+ connectTime
5984
6004
  });
5985
6005
  } else {
5986
6006
  existing.rtspSocket = socket;
@@ -6027,8 +6047,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6027
6047
  if (resources) {
6028
6048
  if (isTrack1) resources.setupTrack1 = true;
6029
6049
  else resources.setupTrack0 = true;
6030
- this.rtspDebugLog(
6031
- `SETUP done for ${clientId}: track0=${!!resources.setupTrack0} track1=${!!resources.setupTrack1} playing=${!!resources.isPlaying}`
6050
+ const transport2 = useTcpInterleaved ? "TCP/interleaved" : "UDP";
6051
+ const track = isTrack1 ? "track1(audio)" : "track0(video)";
6052
+ this.logger.info(
6053
+ `[rebroadcast] SETUP client=${clientId} ${track} transport=${transport2} session=${sessionId}`
6032
6054
  );
6033
6055
  }
6034
6056
  }
@@ -6053,8 +6075,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6053
6075
  const resources = this.clientResources.get(clientId);
6054
6076
  if (resources) {
6055
6077
  resources.isPlaying = true;
6056
- this.rtspDebugLog(
6057
- `PLAY for ${clientId}: track0=${!!resources.setupTrack0} track1=${!!resources.setupTrack1} playing=${!!resources.isPlaying}`
6078
+ const hasAudio = !!resources.setupTrack1;
6079
+ this.logger.info(
6080
+ `[rebroadcast] PLAY client=${clientId} path=${this.path} profile=${this.profile} channel=${this.channel} codec=${this.flow.sdpCodec} audio=${hasAudio} session=${sessionId}`
6058
6081
  );
6059
6082
  }
6060
6083
  }
@@ -6063,6 +6086,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6063
6086
  Range: "npt=0.000-"
6064
6087
  });
6065
6088
  } else if (method === "TEARDOWN") {
6089
+ this.logger.info(
6090
+ `[rebroadcast] TEARDOWN client=${clientId} session=${sessionId}`
6091
+ );
6066
6092
  cleanup();
6067
6093
  sendResponse(200, "OK", {
6068
6094
  Session: sessionId
@@ -6157,7 +6183,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6157
6183
  this.logger.warn(
6158
6184
  `[BaichuanRtspServer] Could not fetch stream metadata: ${error}`
6159
6185
  );
6160
- streamMetadata = { frameRate: 25, width: 1920, height: 1080 };
6186
+ streamMetadata = { frameRate: 25 };
6161
6187
  }
6162
6188
  }
6163
6189
  const ffmpegFormat = this.flow.ffmpegFormat;
@@ -6745,15 +6771,17 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6745
6771
  `Sent ${frameCount} frames to client ${clientId} (frame size: ${frame.data.length} bytes)`
6746
6772
  );
6747
6773
  }
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
- );
6774
+ if (!useDirectRtp) {
6775
+ const now = Date.now();
6776
+ const timeSinceLastFrame = now - lastFrameTime;
6777
+ const waitTime = targetFrameInterval - timeSinceLastFrame;
6778
+ if (waitTime > 0) {
6779
+ await new Promise(
6780
+ (resolve) => setTimeout(resolve, Math.min(waitTime, targetFrameInterval * 2))
6781
+ );
6782
+ }
6783
+ lastFrameTime = Date.now();
6755
6784
  }
6756
- lastFrameTime = Date.now();
6757
6785
  if (useDirectRtp) {
6758
6786
  const videoType = frame.videoType ?? this.flow.videoType;
6759
6787
  const normalizedVideoData = videoType === "H264" ? convertToAnnexB(frame.data) : convertToAnnexB2(frame.data);
@@ -6826,6 +6854,11 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6826
6854
  }
6827
6855
  if (!firstVideoWriteLogged) {
6828
6856
  firstVideoWriteLogged = true;
6857
+ const clientConnectTime = resources?.connectTime ?? Date.now();
6858
+ const ttffMs = Date.now() - clientConnectTime;
6859
+ this.logger.info(
6860
+ `[rebroadcast] first keyframe \u2192 client client=${clientId} codec=${videoType} ttff=${ttffMs}ms`
6861
+ );
6829
6862
  if (rtspDebug) {
6830
6863
  const headHex = frame.data.subarray(0, 16).toString("hex");
6831
6864
  rtspDebugLog(
@@ -6833,6 +6866,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6833
6866
  );
6834
6867
  }
6835
6868
  }
6869
+ if (resources) {
6870
+ resources.framesSent = (resources.framesSent ?? 0) + 1;
6871
+ }
6836
6872
  sendVideoAccessUnit(videoType, normalizedVideoData, true);
6837
6873
  } else {
6838
6874
  try {
@@ -6917,8 +6953,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6917
6953
  this.firstAudioPromise = new Promise((resolve) => {
6918
6954
  this.firstAudioResolve = resolve;
6919
6955
  });
6920
- this.rtspDebugLog(
6921
- `Starting native stream for profile ${this.profile} (waiting for camera to start transmitting...)`
6956
+ this.logger.info(
6957
+ `[rebroadcast] native stream starting profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
6922
6958
  );
6923
6959
  await this.flow.startKeepAlive(this.api);
6924
6960
  this.nativeFanout = new NativeStreamFanout({
@@ -6961,6 +6997,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6961
6997
  this.logger.warn(
6962
6998
  `[BaichuanRtspServer] Shared native stream error: ${error}`
6963
6999
  );
7000
+ },
7001
+ onEnd: () => {
7002
+ if (!this.nativeStreamActive) return;
7003
+ this.nativeStreamActive = false;
7004
+ this.firstFrameReceived = false;
7005
+ this.firstFramePromise = null;
7006
+ this.firstFrameResolve = null;
7007
+ this.nativeFanout = null;
7008
+ this.logger.info(
7009
+ `[rebroadcast] native stream ended (camera sleeping or connection lost) profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
7010
+ );
7011
+ if (this.connectedClients.size > 0) {
7012
+ this.logger.info(
7013
+ `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
7014
+ );
7015
+ setImmediate(() => void this.startNativeStream());
7016
+ }
6964
7017
  }
6965
7018
  });
6966
7019
  this.nativeFanout.start();
@@ -6999,7 +7052,9 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
6999
7052
  if (!this.nativeStreamActive) {
7000
7053
  return;
7001
7054
  }
7002
- this.rtspDebugLog(`Stopping native stream`);
7055
+ this.logger.info(
7056
+ `[rebroadcast] native stream stopping profile=${this.profile} channel=${this.channel} clients=${this.connectedClients.size}`
7057
+ );
7003
7058
  this.flow.stopKeepAlive();
7004
7059
  this.clearNoClientAutoStopTimer();
7005
7060
  this.nativeStreamActive = false;
@@ -7033,9 +7088,6 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends EventEmitter3 {
7033
7088
  if (this.connectedClients.has(clientId)) {
7034
7089
  this.connectedClients.delete(clientId);
7035
7090
  this.emit("clientDisconnected", clientId);
7036
- this.logger.info(
7037
- `[BaichuanRtspServer] RTSP client disconnected: ${clientId}`
7038
- );
7039
7091
  if (this.connectedClients.size === 0) {
7040
7092
  void this.stopNativeStream();
7041
7093
  }
@@ -7268,10 +7320,12 @@ function parseSupportXml(xml) {
7268
7320
  }
7269
7321
  function getSupportItemForChannel(support, channel) {
7270
7322
  if (!support?.items?.length) return void 0;
7271
- const scoreSupportItem = (item) => {
7323
+ const candidates = support.items.filter((i) => i.chnID === channel);
7324
+ if (!candidates.length) return void 0;
7325
+ const score = (item) => {
7272
7326
  const anyItem = item;
7273
- let score = 0;
7274
- if (anyItem.name == null) score += 2;
7327
+ let result = 0;
7328
+ if (anyItem.name == null) result += 100;
7275
7329
  const capabilityKeys = [
7276
7330
  "ptzType",
7277
7331
  "ptzControl",
@@ -7283,20 +7337,17 @@ function getSupportItemForChannel(support, channel) {
7283
7337
  "motion",
7284
7338
  "encCtrl",
7285
7339
  "newIspCfg",
7286
- "remoteAbility"
7340
+ "remoteAbility",
7341
+ "aitype",
7342
+ "videoClip",
7343
+ "snap"
7287
7344
  ];
7288
7345
  for (const k of capabilityKeys) {
7289
- if (anyItem[k] !== void 0) score += 3;
7346
+ if (anyItem[k] !== void 0) result += 3;
7290
7347
  }
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];
7348
+ return result;
7298
7349
  };
7299
- return pickBest(channel);
7350
+ return candidates.sort((a, b) => score(b) - score(a))[0];
7300
7351
  }
7301
7352
  function computeDeviceCapabilities(params) {
7302
7353
  const { channel } = params;
@@ -7328,6 +7379,7 @@ function computeDeviceCapabilities(params) {
7328
7379
  flat,
7329
7380
  /white\s*led|whiteLed|flood\s*light|floodlight/i
7330
7381
  );
7382
+ const hasSirenFromSupport = supportItem ? isTruthyNumberLike(supportItem.audioVersion) : false;
7331
7383
  const hasSirenFromAbilities = abilitiesHasAny(
7332
7384
  flat,
7333
7385
  /audio\s*alarm|audioAlarm|siren|pushAlarn|audioPlay/i
@@ -7340,6 +7392,9 @@ function computeDeviceCapabilities(params) {
7340
7392
  const hasPirFromSupport = supportItem ? isTruthyNumberLike(supportItem.rfCfg) || isTruthyNumberLike(supportItem.newRfCfg) || isTruthyNumberLike(supportItem.rfVersion) || isTruthyNumberLike(supportItem.battery) : false;
7341
7393
  const hasAutotrackingFromSupport = supportItem ? isTruthyNumberLike(supportItem.autoPt) || isTruthyNumberLike(supportItem.smartAI) : false;
7342
7394
  const hasAutotrackingFromAbilities = abilitiesHasAny(flat, /smartTrack/i);
7395
+ const hasBattery = hasBatteryFromSupport || hasBatteryFromAbilities;
7396
+ const isDoorbell = isDoorbellFromSupport || isDoorbellFromModel;
7397
+ const hasWirelessChimeFromAbilities = abilitiesHasAny(flat, /dingDong|dingdong/i);
7343
7398
  const hasPan = hasPanTiltFromSupport || hasPanTiltFromAbilities;
7344
7399
  const hasTilt = hasPanTiltFromSupport || hasPanTiltFromAbilities;
7345
7400
  const hasZoom = hasZoomFromSupport || hasZoomFromAbilities;
@@ -7355,14 +7410,15 @@ function computeDeviceCapabilities(params) {
7355
7410
  hasZoom: finalHasZoom,
7356
7411
  hasPresets: finalHasPresets,
7357
7412
  hasPtz: ptzDisabledBySupport ? false : hasPtzFromSupport || finalHasPan || finalHasTilt || finalHasZoom || finalHasPresets,
7358
- hasBattery: hasBatteryFromSupport || hasBatteryFromAbilities,
7413
+ hasBattery,
7359
7414
  hasIntercom: hasIntercomFromSupport,
7360
- hasSiren: hasSirenFromAbilities,
7415
+ hasSiren: hasSirenFromSupport || hasSirenFromAbilities,
7361
7416
  // lightType >= 2 indicates controllable white LED / floodlight (1 = IR only)
7362
7417
  hasFloodlight: Number.isFinite(lightType) ? lightType >= 2 : hasFloodlightFromAbilities,
7363
7418
  hasPir: hasPirFromAbilities || hasPirFromSupport,
7364
- isDoorbell: isDoorbellFromSupport || isDoorbellFromModel,
7365
- hasAutotracking: hasAutotrackingFromSupport || hasAutotrackingFromAbilities
7419
+ isDoorbell,
7420
+ hasAutotracking: ptzDisabledBySupport ? false : hasAutotrackingFromSupport || hasAutotrackingFromAbilities,
7421
+ hasWirelessChime: isDoorbell || hasWirelessChimeFromAbilities
7366
7422
  };
7367
7423
  if (ptzMode !== void 0) result.ptzMode = ptzMode;
7368
7424
  return result;
@@ -9237,6 +9293,161 @@ var discoverDeviceUidViaBaichuanGetP2p = async (params) => {
9237
9293
  return extractReolinkUidLike(p2pXml);
9238
9294
  };
9239
9295
 
9296
+ // src/reolink/baichuan/utils/chime.ts
9297
+ var buildDingDongGetParamsXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
9298
+ <body>
9299
+ <dingdongDeviceOpt version="1.1">
9300
+ <id>${chimeId}</id>
9301
+ <opt>getParam</opt>
9302
+ </dingdongDeviceOpt>
9303
+ </body>`;
9304
+ var buildDingDongSetParamsXml = (chimeId, params) => `<?xml version="1.0" encoding="UTF-8" ?>
9305
+ <body>
9306
+ <dingdongDeviceOpt version="1.1">
9307
+ <opt>setParam</opt>
9308
+ <id>${chimeId}</id>
9309
+ ${params.volLevel !== void 0 ? `<volLevel>${params.volLevel}</volLevel>` : ""}
9310
+ ${params.ledState !== void 0 ? `<ledState>${params.ledState}</ledState>` : ""}
9311
+ ${params.name !== void 0 ? `<name>${params.name}</name>` : ""}
9312
+ </dingdongDeviceOpt>
9313
+ </body>`;
9314
+ var buildDingDongRingXml = (chimeId, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
9315
+ <body>
9316
+ <dingdongDeviceOpt version="1.1">
9317
+ <id>${chimeId}</id>
9318
+ <opt>ringWithMusic</opt>
9319
+ <musicId>${musicId}</musicId>
9320
+ </dingdongDeviceOpt>
9321
+ </body>`;
9322
+ var buildSetDingDongCfgXml = (chimeId, eventType, state, musicId) => `<?xml version="1.0" encoding="UTF-8" ?>
9323
+ <body>
9324
+ <dingdongCfg version="1.1">
9325
+ <deviceCfg>
9326
+ <id>${chimeId}</id>
9327
+ <alarminCfg>
9328
+ <valid>${state}</valid>
9329
+ <musicId>${musicId}</musicId>
9330
+ <type>${eventType}</type>
9331
+ </alarminCfg>
9332
+ </deviceCfg>
9333
+ </dingdongCfg>
9334
+ </body>`;
9335
+ var buildGetDingDongCtrlXml = () => `<?xml version="1.0" encoding="UTF-8" ?>
9336
+ <body>
9337
+ <dingdongCtrl version="1.1">
9338
+ <opt>machineStateGet</opt>
9339
+ </dingdongCtrl>
9340
+ </body>`;
9341
+ var buildSetDingDongCtrlXml = (chimeType, enabled, time) => `<?xml version="1.0" encoding="UTF-8" ?>
9342
+ <body>
9343
+ <dingdongCtrl version="1.1">
9344
+ <opt>machineStateSet</opt>
9345
+ <type>${chimeType}</type>
9346
+ <bopen>${enabled}</bopen>
9347
+ <bsave>1</bsave>
9348
+ <time>${time}</time>
9349
+ </dingdongCtrl>
9350
+ </body>`;
9351
+ var buildQuickReplyPlayXml = (channel, fileId) => `<?xml version="1.0" encoding="UTF-8" ?>
9352
+ <body>
9353
+ <audioFileInfo version="1.1">
9354
+ <channelId>${channel}</channelId>
9355
+ <id>${fileId}</id>
9356
+ <timeout>0</timeout>
9357
+ </audioFileInfo>
9358
+ </body>`;
9359
+ var parseDingDongListFromXml = (xml) => {
9360
+ const devices = [];
9361
+ const blocks = getXmlBlocks(xml, "dingdongDeviceInfo");
9362
+ for (const block of blocks) {
9363
+ const idText = getXmlText(block, "deviceId") ?? getXmlText(block, "id");
9364
+ const name = getXmlText(block, "deviceName") ?? getXmlText(block, "name") ?? "";
9365
+ const netStateText = getXmlText(block, "netState") ?? getXmlText(block, "netstate");
9366
+ if (idText === void 0) continue;
9367
+ const id = Number(idText);
9368
+ if (!Number.isFinite(id)) continue;
9369
+ devices.push({
9370
+ id,
9371
+ name,
9372
+ netState: netStateText !== void 0 ? Number(netStateText) : 0
9373
+ });
9374
+ }
9375
+ return devices;
9376
+ };
9377
+ var parseDingDongParamsFromXml = (xml) => {
9378
+ const name = getXmlText(xml, "name");
9379
+ const volLevelText = getXmlText(xml, "volLevel");
9380
+ const ledStateText = getXmlText(xml, "ledState");
9381
+ const result = {};
9382
+ if (name !== void 0) result.name = name;
9383
+ if (volLevelText !== void 0) {
9384
+ const n = Number(volLevelText);
9385
+ if (Number.isFinite(n)) result.volLevel = n;
9386
+ }
9387
+ if (ledStateText !== void 0) {
9388
+ const n = Number(ledStateText);
9389
+ if (Number.isFinite(n)) result.ledState = n;
9390
+ }
9391
+ return result;
9392
+ };
9393
+ var parseDingDongCfgFromXml = (xml) => {
9394
+ const configs = [];
9395
+ const deviceBlocks = getXmlBlocks(xml, "deviceCfg");
9396
+ for (const deviceBlock of deviceBlocks) {
9397
+ const idText = getXmlText(deviceBlock, "ringId") ?? getXmlText(deviceBlock, "id");
9398
+ if (idText === void 0) continue;
9399
+ const id = Number(idText);
9400
+ if (!Number.isFinite(id)) continue;
9401
+ const typeMap = {};
9402
+ const alarmBlocks = getXmlBlocks(deviceBlock, "alarminCfg");
9403
+ for (const alarmBlock of alarmBlocks) {
9404
+ const type = getXmlText(alarmBlock, "type");
9405
+ if (!type) continue;
9406
+ const validText = getXmlText(alarmBlock, "switch") ?? getXmlText(alarmBlock, "valid");
9407
+ const musicIdText = getXmlText(alarmBlock, "musicId");
9408
+ typeMap[type] = {
9409
+ valid: validText !== void 0 ? Number(validText) : 0,
9410
+ musicId: musicIdText !== void 0 ? Number(musicIdText) : 0
9411
+ };
9412
+ }
9413
+ configs.push({ id, type: typeMap });
9414
+ }
9415
+ return configs;
9416
+ };
9417
+ var parseHardwiredChimeFromXml = (xml) => {
9418
+ const type = getXmlText(xml, "type") ?? "";
9419
+ const bopenText = getXmlText(xml, "bopen") ?? getXmlText(xml, "enable");
9420
+ const timeText = getXmlText(xml, "time");
9421
+ return {
9422
+ type,
9423
+ enabled: bopenText === "1",
9424
+ time: timeText !== void 0 ? Number(timeText) : 0
9425
+ };
9426
+ };
9427
+ var buildGetDingDongSilentXml = (chimeId) => `<?xml version="1.0" encoding="UTF-8" ?>
9428
+ <body>
9429
+ <dingdongSilentMode version="1.1">
9430
+ <id>${chimeId}</id>
9431
+ </dingdongSilentMode>
9432
+ </body>`;
9433
+ var buildSetDingDongSilentXml = (chimeId, time) => `<?xml version="1.0" encoding="UTF-8" ?>
9434
+ <body>
9435
+ <dingdongSilentMode version="1.1">
9436
+ <id>${chimeId}</id>
9437
+ <time>${time}</time>
9438
+ <type>63</type>
9439
+ </dingdongSilentMode>
9440
+ </body>`;
9441
+ var parseWirelessChimeSilentFromXml = (xml, chimeId) => {
9442
+ const timeText = getXmlText(xml, "time");
9443
+ const time = timeText !== void 0 ? Number(timeText) : 0;
9444
+ return {
9445
+ id: chimeId,
9446
+ time,
9447
+ active: time === 0
9448
+ };
9449
+ };
9450
+
9240
9451
  // src/reolink/baichuan/utils/eventsGetEvents.ts
9241
9452
  var parseAiTypeToken = (aiTypeRaw) => {
9242
9453
  const raw = (aiTypeRaw ?? "").trim();
@@ -9546,6 +9757,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
9546
9757
  host;
9547
9758
  username;
9548
9759
  password;
9760
+ /**
9761
+ * Set to `true` after `close()` is called.
9762
+ * Once closed, the API instance should not be reused.
9763
+ */
9764
+ _closed = false;
9549
9765
  // ─────────────────────────────────────────────────────────────────────────────
9550
9766
  // SOCKET POOL - Tag-based socket management
9551
9767
  // ─────────────────────────────────────────────────────────────────────────────
@@ -9575,10 +9791,194 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
9575
9791
  get client() {
9576
9792
  const entry = this.socketPool.get("general");
9577
9793
  if (!entry) {
9794
+ if (this._closed) {
9795
+ throw new Error(
9796
+ "[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
9797
+ );
9798
+ }
9578
9799
  throw new Error("[ReolinkBaichuanApi] General socket not initialized");
9579
9800
  }
9580
9801
  return entry.client;
9581
9802
  }
9803
+ /**
9804
+ * `true` after `close()` has been called. A closed API should not be reused;
9805
+ * the consumer should create a new instance.
9806
+ */
9807
+ get isClosed() {
9808
+ return this._closed;
9809
+ }
9810
+ /**
9811
+ * `true` when the API is usable: not closed, general socket exists, socket
9812
+ * is connected and the client is logged in.
9813
+ *
9814
+ * This is the recommended way for consumers to check whether the API is
9815
+ * still valid before issuing commands, instead of directly accessing
9816
+ * `api.client.isSocketConnected()` / `api.client.loggedIn` (which throws
9817
+ * if the socket pool was already destroyed).
9818
+ */
9819
+ get isReady() {
9820
+ if (this._closed) return false;
9821
+ const entry = this.socketPool.get("general");
9822
+ if (!entry) return false;
9823
+ try {
9824
+ return entry.client.isSocketConnected() && entry.client.loggedIn;
9825
+ } catch {
9826
+ return false;
9827
+ }
9828
+ }
9829
+ /** Promise tracking an in-flight reconnection from `ensureConnected()`. */
9830
+ _ensureConnectedPromise;
9831
+ /**
9832
+ * Ensure the "general" socket is connected and logged in.
9833
+ * If the socket is disconnected or the pool entry was destroyed, a new
9834
+ * general socket is created, logged in, and all event/push/guard listeners
9835
+ * are re-attached automatically.
9836
+ *
9837
+ * This is a **no-op** when the API is already {@link isReady}.
9838
+ *
9839
+ * @throws If `close()` was called — the API is permanently closed and a new
9840
+ * instance must be created.
9841
+ */
9842
+ async ensureConnected() {
9843
+ if (this._closed) {
9844
+ throw new Error(
9845
+ "[ReolinkBaichuanApi] API has been closed \u2014 create a new instance to reconnect"
9846
+ );
9847
+ }
9848
+ if (this.isReady) return;
9849
+ if (this._ensureConnectedPromise) {
9850
+ return this._ensureConnectedPromise;
9851
+ }
9852
+ this._ensureConnectedPromise = this.reconnectGeneralSocket();
9853
+ try {
9854
+ await this._ensureConnectedPromise;
9855
+ } finally {
9856
+ this._ensureConnectedPromise = void 0;
9857
+ }
9858
+ }
9859
+ /**
9860
+ * Internal: destroy the current general socket (if any), create a new one,
9861
+ * login, and re-attach all listeners.
9862
+ */
9863
+ async reconnectGeneralSocket() {
9864
+ const oldEntry = this.socketPool.get("general");
9865
+ if (oldEntry) {
9866
+ oldEntry.client.removeAllListeners();
9867
+ if (oldEntry.idleCloseTimer) clearTimeout(oldEntry.idleCloseTimer);
9868
+ if (oldEntry.generalPermitRelease) {
9869
+ try {
9870
+ oldEntry.generalPermitRelease();
9871
+ } catch {
9872
+ }
9873
+ }
9874
+ this.socketPool.delete("general");
9875
+ try {
9876
+ await oldEntry.client.close({ reason: "reconnect", skipLogout: true });
9877
+ } catch {
9878
+ }
9879
+ }
9880
+ const newClient = new BaichuanClient(this.clientOptions);
9881
+ this.socketPool.set("general", {
9882
+ client: newClient,
9883
+ refCount: 1,
9884
+ // general socket is always "in use"
9885
+ createdAt: Date.now(),
9886
+ lastUsedAt: Date.now(),
9887
+ idleCloseTimer: void 0,
9888
+ generalPermitRelease: void 0
9889
+ });
9890
+ this.setupGeneralClientListeners();
9891
+ await this.client.login();
9892
+ this.logger.log?.(
9893
+ "[ReolinkBaichuanApi] General socket reconnected successfully"
9894
+ );
9895
+ if (this.simpleEventListeners.size > 0) {
9896
+ this.simpleEventSubscribed = false;
9897
+ this.simpleEventWatchdogRecoveryAttempts = 0;
9898
+ this.simpleEventWatchdogLastRecoveryAt = 0;
9899
+ try {
9900
+ await this.ensureSimpleEventSubscribed();
9901
+ this.simpleEventLastReceivedAt = Date.now();
9902
+ this.logger.log?.(
9903
+ `[ReolinkBaichuanApi] Events re-subscribed after reconnection (listeners=${this.simpleEventListeners.size})`
9904
+ );
9905
+ } catch (e) {
9906
+ (this.logger.debug ?? this.logger.log).call(
9907
+ this.logger,
9908
+ `[ReolinkBaichuanApi] Event re-subscribe after reconnection failed, watchdog will retry`,
9909
+ formatErrorForLog(e)
9910
+ );
9911
+ }
9912
+ }
9913
+ }
9914
+ /**
9915
+ * Attach event, push, channelInfo, and guard listeners to the current
9916
+ * "general" client. Called from the constructor and from
9917
+ * {@link reconnectGeneralSocket}.
9918
+ */
9919
+ setupGeneralClientListeners() {
9920
+ const client = this.client;
9921
+ client.on("event", (event) => {
9922
+ const mapped = mapToSimpleEvent(event);
9923
+ if (!mapped) return;
9924
+ this.dispatchSimpleEvent(mapped);
9925
+ });
9926
+ client.on("channelInfo", (xml) => {
9927
+ try {
9928
+ this.parseAndStoreChannelInfo(xml);
9929
+ } catch (e) {
9930
+ this.logger.warn?.(
9931
+ "[ReolinkBaichuanApi] Error parsing channel info from push",
9932
+ formatErrorForLog(e)
9933
+ );
9934
+ }
9935
+ });
9936
+ client.on("push", (frame) => {
9937
+ const cmdId = frame.header.cmdId;
9938
+ 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) {
9939
+ return;
9940
+ }
9941
+ try {
9942
+ if (frame.body.length === 0) return;
9943
+ const xml = client.tryDecryptXml(
9944
+ frame.body,
9945
+ frame.header.channelId,
9946
+ client.enc
9947
+ );
9948
+ if (!xml || !xml.startsWith("<?xml")) return;
9949
+ this.parseAndStoreSettingsPush(cmdId, xml, frame.header.channelId);
9950
+ } catch (e) {
9951
+ this.logger.debug?.(
9952
+ "[ReolinkBaichuanApi] Error parsing settings push",
9953
+ formatErrorForLog(e)
9954
+ );
9955
+ }
9956
+ });
9957
+ if (this.rebootAfterDisconnectionsPerMinute > 0) {
9958
+ client.on("close", () => {
9959
+ try {
9960
+ void this.maybeRebootOnDisconnectStorm();
9961
+ } catch {
9962
+ }
9963
+ });
9964
+ }
9965
+ if (this.rebootAfterConsecutiveEconnreset > 0) {
9966
+ client.on("close", () => {
9967
+ try {
9968
+ void this.maybeRebootOnEconnresetStorm();
9969
+ } catch {
9970
+ }
9971
+ });
9972
+ }
9973
+ if (!this.sessionGuardIntervalTimer) {
9974
+ client.once("push", () => {
9975
+ void this.logActiveSessionsOnStartup();
9976
+ this.sessionGuardIntervalTimer = setInterval(() => {
9977
+ void this.maybeRebootOnTooManySessions();
9978
+ }, 6e4);
9979
+ });
9980
+ }
9981
+ }
9582
9982
  /**
9583
9983
  * Cached camera UID. May be initially undefined if not provided in the constructor.
9584
9984
  * Will be lazily populated on demand when needed (e.g. for recordings).
@@ -10519,42 +10919,6 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10519
10919
  logger: this.logger,
10520
10920
  debugConfig: generalClient.getDebugConfig?.()
10521
10921
  });
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
10922
  const maxSessions = opts.maxDedicatedSessionsBeforeReboot;
10559
10923
  if (typeof maxSessions === "number" && Number.isFinite(maxSessions) && maxSessions > 0) {
10560
10924
  this.maxDedicatedSessionsBeforeReboot = Math.floor(maxSessions);
@@ -10563,32 +10927,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
10563
10927
  if (typeof disconnectThreshold === "number" && Number.isFinite(disconnectThreshold)) {
10564
10928
  this.rebootAfterDisconnectionsPerMinute = Math.floor(disconnectThreshold);
10565
10929
  }
10566
- if (this.rebootAfterDisconnectionsPerMinute > 0) {
10567
- this.client.on("close", () => {
10568
- try {
10569
- void this.maybeRebootOnDisconnectStorm();
10570
- } catch {
10571
- }
10572
- });
10573
- }
10574
10930
  const econnresetThreshold = opts.rebootAfterConsecutiveEconnreset;
10575
10931
  if (typeof econnresetThreshold === "number" && Number.isFinite(econnresetThreshold)) {
10576
10932
  this.rebootAfterConsecutiveEconnreset = Math.floor(econnresetThreshold);
10577
10933
  }
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
- });
10934
+ this.setupGeneralClientListeners();
10592
10935
  }
10593
10936
  /**
10594
10937
  * CGI forward: fetch RTSP URL for a channel via `GetRtspUrl`.
@@ -11419,6 +11762,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11419
11762
  );
11420
11763
  }
11421
11764
  async close(options) {
11765
+ if (this._closed) return;
11766
+ this._closed = true;
11422
11767
  if (this.sessionGuardIntervalTimer) {
11423
11768
  clearInterval(this.sessionGuardIntervalTimer);
11424
11769
  this.sessionGuardIntervalTimer = void 0;
@@ -11481,7 +11826,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11481
11826
  }
11482
11827
  async handleSendXml400(params, frame, retry) {
11483
11828
  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.";
11829
+ const emptyBody400Msg = "Baichuan request failed (responseCode 400, empty body). Possible causes: expired session, invalid username/password, or unsupported command on NVR/Hub.";
11485
11830
  if (this.isSendXmlFailFast400(params, frame.body.length)) {
11486
11831
  throw new Error(emptyBody400Msg);
11487
11832
  }
@@ -11997,11 +12342,50 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
11997
12342
  * Minimal per-channel inventory for NVR-connected devices.
11998
12343
  *
11999
12344
  * Intended to be fast: avoids AI/abilities and returns only the common identity + battery hints.
12345
+ *
12346
+ * @param options.source - Data source for the channel list (default: `"cgi"`):
12347
+ * - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
12348
+ * no dependency on async push messages. Recommended for first-call discovery.
12349
+ * - `"baichuan"`: Uses the cmd_id 145 push cache populated when the NVR sends channel
12350
+ * info after login + event subscription. This push is *asynchronous*: if it has not
12351
+ * arrived yet, the result will have zero channels. Callers must retry (nvr.ts does this
12352
+ * with a 1-second loop). Note: explicitly requesting cmd_id 145 is not supported.
12000
12353
  */
12001
12354
  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);
12355
+ const source = options?.source ?? "cgi";
12356
+ let channels;
12357
+ const cgiStatusByChannel = /* @__PURE__ */ new Map();
12358
+ if (options?.channels?.length) {
12359
+ channels = options.channels.map((c) => Number(c)).filter((n) => Number.isFinite(n));
12360
+ } else if (source === "cgi") {
12361
+ try {
12362
+ const { channels: cgiChannels, channelsResponse } = await this.cgiApi.getChannels();
12363
+ const status = channelsResponse?.[0]?.value?.status ?? [];
12364
+ for (const s of status) {
12365
+ const ch = Number(s?.channel);
12366
+ if (!Number.isFinite(ch)) continue;
12367
+ cgiStatusByChannel.set(ch, {
12368
+ ...s.name != null ? { name: s.name } : {},
12369
+ ...s.uid != null ? { uid: s.uid } : {},
12370
+ sleeping: s.sleep === 1
12371
+ });
12372
+ }
12373
+ channels = cgiChannels;
12374
+ this.logger.debug?.(
12375
+ `[ReolinkBaichuanApi] getNvrChannelsSummary: CGI found ${channels.length} channel(s): [${channels.join(", ")}]`
12376
+ );
12377
+ } catch (e) {
12378
+ const msg = e instanceof Error ? e.message : String(e);
12379
+ this.logger.warn?.(
12380
+ `[ReolinkBaichuanApi] getNvrChannelsSummary: CGI GetChannelstatus failed (${msg}), returning empty`
12381
+ );
12382
+ channels = [];
12383
+ }
12384
+ } else {
12385
+ const pushInfo2 = this.getChannelInfoFromPushCache();
12386
+ channels = Array.from(pushInfo2.keys()).map((c) => Number(c)).filter((n) => Number.isFinite(n));
12387
+ }
12388
+ channels = channels.sort((a, b) => a - b);
12005
12389
  const support = await this.getSupportInfo().catch(() => {
12006
12390
  this.logger.error?.(
12007
12391
  "[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
@@ -12031,7 +12415,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12031
12415
  );
12032
12416
  }
12033
12417
  }
12034
- const cacheKey = `baichuan:${channels.join(",")}`;
12418
+ const cacheKey = `${source}:${channels.join(",")}`;
12035
12419
  const cached = this.nvrChannelsSummaryCache.get(cacheKey);
12036
12420
  if (cached) {
12037
12421
  return {
@@ -12052,8 +12436,10 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12052
12436
  } catch {
12053
12437
  }
12054
12438
  }
12439
+ const pushInfo = this.getChannelInfoFromPushCache();
12055
12440
  const devices = channels.map((channel) => {
12056
- const cached2 = pushInfo.get(channel);
12441
+ const pushCached = pushInfo.get(channel);
12442
+ const cgiStatus = cgiStatusByChannel.get(channel);
12057
12443
  const info = infoPerChannel.get(channel);
12058
12444
  const networkInfo = networkInfoPerChannel.get(channel);
12059
12445
  const isBattery = isBatteryByChannel.get(channel) ?? false;
@@ -12061,6 +12447,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12061
12447
  const isDoorbell = (isDoorbellByChannel.get(channel) ?? false) || /doorbell/i.test(model);
12062
12448
  const normalizedModel = model ? model.trim() : void 0;
12063
12449
  const isMultifocal = normalizedModel ? isDualLenseModel(normalizedModel) : false;
12450
+ const name = pushCached?.name || cgiStatus?.name || "";
12451
+ const uid = pushCached?.uid || cgiStatus?.uid || "";
12452
+ const sleeping = pushCached?.sleeping ?? cgiStatus?.sleeping;
12064
12453
  return {
12065
12454
  channel,
12066
12455
  isBattery,
@@ -12070,19 +12459,19 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
12070
12459
  ...networkInfo?.ip ? { ip: networkInfo.ip } : {},
12071
12460
  ...networkInfo?.mac ? { mac: networkInfo.mac } : {},
12072
12461
  ...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 } : {}
12462
+ ...name ? { name } : {},
12463
+ ...uid ? { uid } : {},
12464
+ ...pushCached?.state ? { state: pushCached.state } : {},
12465
+ ...typeof pushCached?.index === "number" ? { index: pushCached.index } : {},
12466
+ ...pushCached?.streamSupport?.length ? { streamSupport: pushCached.streamSupport } : {},
12467
+ ...pushCached?.wifiState ? { wifiState: pushCached.wifiState } : {},
12468
+ ...pushCached?.networkSegment ? { networkSegment: pushCached.networkSegment } : {},
12469
+ ...typeof pushCached?.changed === "boolean" ? { changed: pushCached.changed } : {},
12470
+ ...typeof pushCached?.abilityChanged === "boolean" ? { abilityChanged: pushCached.abilityChanged } : {},
12471
+ ...typeof pushCached?.online === "boolean" ? { online: pushCached.online } : {},
12472
+ ...typeof sleeping === "boolean" ? { sleeping } : {},
12473
+ ...pushCached?.loginState ? { loginState: pushCached.loginState } : {},
12474
+ ...typeof pushCached?.updatedAtMs === "number" ? { updatedAtMs: pushCached.updatedAtMs } : {}
12086
12475
  };
12087
12476
  });
12088
12477
  const result = { channels, devices };
@@ -16350,13 +16739,12 @@ ${xml}`
16350
16739
  ]);
16351
16740
  const support = supportResult.status === "fulfilled" ? supportResult.value : void 0;
16352
16741
  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
- );
16742
+ const supportItem = getSupportItemForChannel(support, ch);
16743
+ const capabilities = computeDeviceCapabilities({
16744
+ channel: ch,
16745
+ ...support != null && { support },
16746
+ ...abilities != null && { abilities }
16747
+ });
16360
16748
  const item = supportItem;
16361
16749
  const lightType = item?.lightType;
16362
16750
  const ledCtrl = item?.ledCtrl;
@@ -16372,6 +16760,25 @@ ${xml}`
16372
16760
  });
16373
16761
  capabilities.hasFloodlight = probed;
16374
16762
  }
16763
+ let dingDongListIds;
16764
+ let dingDongCfgIds;
16765
+ let wirelessChimeError;
16766
+ if (capabilities.hasWirelessChime) {
16767
+ try {
16768
+ const list = await this.getDingDongList(ch);
16769
+ dingDongListIds = list.map((d) => d.id);
16770
+ const first = list[0];
16771
+ const fromList = first !== void 0 && first.id >= 0;
16772
+ if (!fromList) {
16773
+ const configs = await this.getDingDongCfg(ch);
16774
+ dingDongCfgIds = configs.map((c) => c.id);
16775
+ capabilities.hasWirelessChime = configs.some((c) => c.id >= 0);
16776
+ }
16777
+ } catch (e) {
16778
+ capabilities.hasWirelessChime = false;
16779
+ wirelessChimeError = e instanceof Error ? e.message : String(e);
16780
+ }
16781
+ }
16375
16782
  const features = this.parseFeaturesFromSupport(support);
16376
16783
  const objects = await this.getAiDetectTypes(ch, { timeoutMs: 1500 });
16377
16784
  const autotrackingProbed = await this.probeAutotrackingSupport(ch, {
@@ -16408,7 +16815,10 @@ ${xml}`
16408
16815
  ...abilities && {
16409
16816
  abilityMergedKeyCount: Object.keys(abilities).length
16410
16817
  },
16411
- ...support?.items && { supportItemCount: support.items.length }
16818
+ ...support?.items && { supportItemCount: support.items.length },
16819
+ ...dingDongListIds !== void 0 && { dingDongListIds },
16820
+ ...dingDongCfgIds !== void 0 && { dingDongCfgIds },
16821
+ ...wirelessChimeError !== void 0 && { wirelessChimeError }
16412
16822
  };
16413
16823
  const result = {
16414
16824
  capabilities,
@@ -16435,90 +16845,6 @@ ${xml}`
16435
16845
  this.deviceCapabilitiesCache.clear();
16436
16846
  }
16437
16847
  }
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
16848
  /**
16523
16849
  * Parse support features from SupportInfo.
16524
16850
  */
@@ -17287,7 +17613,7 @@ ${xml}`
17287
17613
  * @returns Test results for all stream types and profiles
17288
17614
  */
17289
17615
  async testChannelStreams(channel, logger) {
17290
- const { testChannelStreams } = await import("./DiagnosticsTools-NUMCYEKQ.js");
17616
+ const { testChannelStreams } = await import("./DiagnosticsTools-FNLGCOVA.js");
17291
17617
  return await testChannelStreams({
17292
17618
  api: this,
17293
17619
  channel: this.normalizeChannel(channel),
@@ -17303,7 +17629,7 @@ ${xml}`
17303
17629
  * @returns Complete diagnostics for all channels and streams
17304
17630
  */
17305
17631
  async collectMultifocalDiagnostics(logger) {
17306
- const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-NUMCYEKQ.js");
17632
+ const { collectMultifocalDiagnostics } = await import("./DiagnosticsTools-FNLGCOVA.js");
17307
17633
  return await collectMultifocalDiagnostics({
17308
17634
  api: this,
17309
17635
  logger
@@ -19391,6 +19717,216 @@ ${scheduleItems}
19391
19717
  const channel = 0;
19392
19718
  return await this.getSnapshot(channel);
19393
19719
  }
19720
+ // --------------------
19721
+ // Chime / DingDong APIs
19722
+ // --------------------
19723
+ /**
19724
+ * Get the list of paired wireless chime devices.
19725
+ * cmd_id: 484 (GetDingDongList)
19726
+ *
19727
+ * @param channel - Channel number (0-based, default 0)
19728
+ * @returns Array of paired chime devices
19729
+ */
19730
+ async getDingDongList(channel) {
19731
+ const ch = this.normalizeChannel(channel);
19732
+ const xml = await this.sendXml({
19733
+ cmdId: BC_CMD_ID_GET_DING_DONG_LIST,
19734
+ channel: ch
19735
+ });
19736
+ return parseDingDongListFromXml(xml);
19737
+ }
19738
+ /**
19739
+ * Get parameters (name, volume, LED state) for a specific wireless chime.
19740
+ * cmd_id: 485 (DingDongOpt, option getParam)
19741
+ *
19742
+ * @param chimeId - The chime device ID
19743
+ * @param channel - Channel number (0-based, default 0)
19744
+ * @returns Chime parameters
19745
+ */
19746
+ async getDingDongParams(chimeId, channel) {
19747
+ const ch = this.normalizeChannel(channel);
19748
+ const payloadXml = buildDingDongGetParamsXml(chimeId);
19749
+ const xml = await this.sendXml({
19750
+ cmdId: BC_CMD_ID_DING_DONG_OPT,
19751
+ channel: ch,
19752
+ payloadXml
19753
+ });
19754
+ return parseDingDongParamsFromXml(xml);
19755
+ }
19756
+ /**
19757
+ * Set parameters (name, volume, LED state) for a specific wireless chime.
19758
+ * cmd_id: 485 (DingDongOpt, option setParam)
19759
+ *
19760
+ * @param chimeId - The chime device ID
19761
+ * @param params - Parameters to set (volLevel, ledState, name)
19762
+ * @param channel - Channel number (0-based, default 0)
19763
+ */
19764
+ async setDingDongParams(chimeId, params, channel) {
19765
+ const ch = this.normalizeChannel(channel);
19766
+ const payloadXml = buildDingDongSetParamsXml(chimeId, params);
19767
+ await this.sendXml({
19768
+ cmdId: BC_CMD_ID_DING_DONG_OPT,
19769
+ channel: ch,
19770
+ payloadXml
19771
+ });
19772
+ }
19773
+ /**
19774
+ * Trigger a wireless chime to ring with a specific ringtone.
19775
+ * cmd_id: 485 (DingDongOpt, option ringWithMusic)
19776
+ *
19777
+ * @param chimeId - The chime device ID
19778
+ * @param musicId - The ringtone/music ID to play
19779
+ * @param channel - Channel number (0-based, default 0)
19780
+ */
19781
+ async ringDingDong(chimeId, musicId, channel) {
19782
+ const ch = this.normalizeChannel(channel);
19783
+ const payloadXml = buildDingDongRingXml(chimeId, musicId);
19784
+ await this.sendXml({
19785
+ cmdId: BC_CMD_ID_DING_DONG_OPT,
19786
+ channel: ch,
19787
+ payloadXml
19788
+ });
19789
+ }
19790
+ /**
19791
+ * Get the per-event alarm configuration for paired wireless chimes.
19792
+ * cmd_id: 486 (GetDingDongCfg)
19793
+ *
19794
+ * @param channel - Channel number (0-based, default 0)
19795
+ * @returns Array of chime configurations (one per paired chime)
19796
+ */
19797
+ async getDingDongCfg(channel) {
19798
+ const ch = this.normalizeChannel(channel);
19799
+ const xml = await this.sendXml({
19800
+ cmdId: BC_CMD_ID_GET_DING_DONG_CFG,
19801
+ channel: ch
19802
+ });
19803
+ return parseDingDongCfgFromXml(xml);
19804
+ }
19805
+ /**
19806
+ * Set the per-event alarm configuration for a specific wireless chime.
19807
+ * cmd_id: 487 (SetDingDongCfg)
19808
+ *
19809
+ * @param chimeId - The chime ring/device ID
19810
+ * @param eventType - Event type string (e.g. "doorbell", "package", "people")
19811
+ * @param state - 0 = disabled, 1 = enabled
19812
+ * @param musicId - Ringtone ID to use for this event type
19813
+ * @param channel - Channel number (0-based, default 0)
19814
+ */
19815
+ async setDingDongCfg(chimeId, eventType, state, musicId, channel) {
19816
+ const ch = this.normalizeChannel(channel);
19817
+ const payloadXml = buildSetDingDongCfgXml(chimeId, eventType, state, musicId);
19818
+ await this.sendXml({
19819
+ cmdId: BC_CMD_ID_SET_DING_DONG_CFG,
19820
+ channel: ch,
19821
+ payloadXml
19822
+ });
19823
+ }
19824
+ /** Cache of last known hardwired chime state per channel, used to avoid re-fetching on every set. */
19825
+ _hardwiredChimeCache = /* @__PURE__ */ new Map();
19826
+ /**
19827
+ * Get the hardwired (wired-in) chime state.
19828
+ * cmd_id: 483 (GetDingDongCtrl)
19829
+ *
19830
+ * Note: calling this may briefly trigger the physical chime to rattle.
19831
+ *
19832
+ * @param channel - Channel number (0-based, default 0)
19833
+ * @returns Hardwired chime state (type, enabled, time)
19834
+ */
19835
+ async getHardwiredChime(channel) {
19836
+ const ch = this.normalizeChannel(channel);
19837
+ const payloadXml = buildGetDingDongCtrlXml();
19838
+ const xml = await this.sendXml({
19839
+ cmdId: BC_CMD_ID_DING_DONG_CTRL,
19840
+ channel: ch,
19841
+ payloadXml
19842
+ });
19843
+ const state = parseHardwiredChimeFromXml(xml);
19844
+ this._hardwiredChimeCache.set(ch, state);
19845
+ return state;
19846
+ }
19847
+ /**
19848
+ * Set the hardwired (wired-in) chime state.
19849
+ * cmd_id: 483 (SetDingDongCtrl)
19850
+ *
19851
+ * Uses the cached state from a previous getHardwiredChime call to fill in
19852
+ * missing type/time fields, avoiding a double round-trip on every set.
19853
+ * Falls back to fetching if no cache is available.
19854
+ *
19855
+ * @param params - Chime configuration (type, enabled, time)
19856
+ * @param channel - Channel number (0-based, default 0)
19857
+ */
19858
+ async setHardwiredChime(params, channel) {
19859
+ const ch = this.normalizeChannel(channel);
19860
+ let current = this._hardwiredChimeCache.get(ch);
19861
+ if (!current) {
19862
+ current = await this.getHardwiredChime(ch);
19863
+ }
19864
+ const chimeType = params.type ?? current.type;
19865
+ const enabled = params.enabled ? 1 : 0;
19866
+ const time = params.time ?? current.time;
19867
+ const payloadXml = buildSetDingDongCtrlXml(chimeType, enabled, time);
19868
+ const xml = await this.sendXml({
19869
+ cmdId: BC_CMD_ID_DING_DONG_CTRL,
19870
+ channel: ch,
19871
+ payloadXml
19872
+ });
19873
+ const newState = parseHardwiredChimeFromXml(xml);
19874
+ this._hardwiredChimeCache.set(ch, newState);
19875
+ return newState;
19876
+ }
19877
+ /**
19878
+ * Play an audio file on the doorbell / chime device.
19879
+ * cmd_id: 349 (QuickReplyPlay)
19880
+ *
19881
+ * @param fileId - The audio file ID to play
19882
+ * @param channel - Channel number (0-based, default 0)
19883
+ */
19884
+ async quickReplyPlay(fileId, channel) {
19885
+ const ch = this.normalizeChannel(channel);
19886
+ const payloadXml = buildQuickReplyPlayXml(ch, fileId);
19887
+ await this.sendXml({
19888
+ cmdId: BC_CMD_ID_QUICK_REPLY_PLAY,
19889
+ channel: ch,
19890
+ payloadXml
19891
+ });
19892
+ }
19893
+ /**
19894
+ * Get the silent mode state of a paired wireless chime.
19895
+ * cmd_id: 609 (GetDingDongSilent)
19896
+ *
19897
+ * @param chimeId - The wireless chime device ID (from getDingDongList)
19898
+ * @param channel - Channel number (0-based, default 0)
19899
+ * @returns Wireless chime silent state (time=0 means active/not silenced)
19900
+ */
19901
+ async getDingDongSilent(chimeId, channel) {
19902
+ const ch = this.normalizeChannel(channel);
19903
+ const payloadXml = buildGetDingDongSilentXml(chimeId);
19904
+ const xml = await this.sendXml({
19905
+ cmdId: BC_CMD_ID_GET_DING_DONG_SILENT,
19906
+ channel: ch,
19907
+ payloadXml
19908
+ });
19909
+ return parseWirelessChimeSilentFromXml(xml, chimeId);
19910
+ }
19911
+ /**
19912
+ * Set the silent mode of a paired wireless chime.
19913
+ * cmd_id: 610 (SetDingDongSilent)
19914
+ *
19915
+ * @param chimeId - The wireless chime device ID (from getDingDongList)
19916
+ * @param time - Silence duration in seconds. 0 = not silenced (chime active), >0 = silenced for this many seconds.
19917
+ * @param channel - Channel number (0-based, default 0)
19918
+ * @returns Updated wireless chime silent state
19919
+ */
19920
+ async setDingDongSilent(chimeId, time, channel) {
19921
+ const ch = this.normalizeChannel(channel);
19922
+ const payloadXml = buildSetDingDongSilentXml(chimeId, time);
19923
+ const xml = await this.sendXml({
19924
+ cmdId: BC_CMD_ID_SET_DING_DONG_SILENT,
19925
+ channel: ch,
19926
+ payloadXml
19927
+ });
19928
+ return parseWirelessChimeSilentFromXml(xml, chimeId);
19929
+ }
19394
19930
  };
19395
19931
 
19396
19932
  // src/reolink/discovery.ts
@@ -20388,6 +20924,7 @@ export {
20388
20924
  flattenAbilitiesForChannel,
20389
20925
  abilitiesHasAny,
20390
20926
  parseSupportXml,
20927
+ getSupportItemForChannel,
20391
20928
  computeDeviceCapabilities,
20392
20929
  DUAL_LENS_DUAL_MOTION_MODELS,
20393
20930
  DUAL_LENS_SINGLE_MOTION_MODELS,
@@ -20406,4 +20943,4 @@ export {
20406
20943
  isTcpFailureThatShouldFallbackToUdp,
20407
20944
  autoDetectDeviceType
20408
20945
  };
20409
- //# sourceMappingURL=chunk-PCPEXOWB.js.map
20946
+ //# sourceMappingURL=chunk-MN7GUZT7.js.map