node-red-contrib-symi-mesh 1.8.8 → 1.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -704,6 +704,18 @@ node-red-contrib-symi-mesh/
704
704
 
705
705
  ## 更新日志
706
706
 
707
+ ### v1.8.9 (2026-01-14)
708
+
709
+ **KNX 协议回显消除算法升级**:
710
+ - **值校验机制**:在原有的“时间窗口”回显消除基础上,新增“状态值”深度比对。
711
+ - **原理**:当 KNX 收到消息时,不仅检查该设备最近是否发送过命令,还对比收到的值与发出的值是否一致。
712
+ - **效果**:彻底解决“Mesh 开 -> KNX 开 -> 立即 KNX 关”场景下,OFF 指令被误判为回显而被拦截的问题。只有当时间相近且值完全相同时,才判定为回显并忽略;值发生变化时,立即响应并同步,实现毫秒级双向跟随。
713
+ **KNX 开关双向同步逻辑深度优化**:
714
+ - **防死循环机制精细化**:将开关设备的防死循环锁从“设备+通道”级别细化到“设备+通道+状态值”级别。
715
+ - **修复场景**:KNX 控制打开(ON)后,立即手动关闭 Mesh 设备(OFF),旧版本会因死循环锁未过期而丢弃 OFF 状态上报。
716
+ - **新逻辑**:KNX 发送 ON 指令只锁定 Mesh->KNX 的 ON 上报,不影响 Mesh->KNX 的 OFF 上报,确保快速连续反向操作能 100% 同步。
717
+ - **状态同步零延迟**:移除不必要的通用防抖检查,确保 Mesh 端的状态变化能毫秒级同步到 KNX 总线。
718
+
707
719
  ### v1.8.8 (2026-01-14)
708
720
 
709
721
  **KNX 双向同步深度修复与协议鲁棒性增强**:
@@ -152,7 +152,8 @@ module.exports = function(RED) {
152
152
  node.lastSyncTime = 0;
153
153
  node.stateCache = {}; // Mesh设备状态缓存
154
154
  node.knxStateCache = {}; // KNX设备状态缓存
155
- node.lastKnxAddrSent = {}; // 记录发送到KNX地址的时间,防止自己发的命令又被处理
155
+ node.lastKnxAddrSent = {}; // 记录发送到KNX地址的时间
156
+ node.lastKnxValueSent = {}; // 记录发送到KNX地址的值
156
157
 
157
158
  // 初始化通用同步工具类
158
159
  node.syncUtils = new SyncUtils({
@@ -302,7 +303,12 @@ module.exports = function(RED) {
302
303
  // 窗帘设备需要单独处理防死循环(动作和位置分开检查)
303
304
  if (mapping.deviceType === 'cover') {
304
305
  // 窗帘的防死循环在内部单独处理,这里不跳过
305
- } else {
306
+ }
307
+ // 开关设备使用带值的精细化key,这里先不检查
308
+ else if (mapping.deviceType === 'switch') {
309
+ // 在具体处理逻辑中检查
310
+ }
311
+ else {
306
312
  // 其他设备统一防死循环检查
307
313
  if (node.shouldPreventSync('mesh-to-knx', loopKey)) {
308
314
  node.log(`[Mesh->KNX] 跳过(防死循环): ${loopKey}`);
@@ -314,13 +320,22 @@ module.exports = function(RED) {
314
320
  if (mapping.deviceType === 'switch') {
315
321
  const switchKey = `switch_${mapping.meshChannel}`;
316
322
  if (changed[switchKey] !== undefined) {
317
- node.log(`[Mesh->KNX] 开关变化: ${mapping.name || eventData.device.name} CH${mapping.meshChannel} = ${changed[switchKey]}`);
323
+ const val = changed[switchKey];
324
+ const switchValue = (val === 1 || val === true || val === 'on' || val === 'ON');
325
+ const specificLoopKey = `${loopKey}_switch_${switchValue}`;
326
+
327
+ if (node.shouldPreventSync('mesh-to-knx', specificLoopKey)) {
328
+ node.log(`[Mesh->KNX] 跳过(防死循环): ${specificLoopKey}`);
329
+ continue;
330
+ }
331
+
332
+ node.log(`[Mesh->KNX] 开关变化: ${mapping.name || eventData.device.name} CH${mapping.meshChannel} = ${val}`);
318
333
  node.queueCommand({
319
334
  direction: 'mesh-to-knx',
320
335
  mapping: mapping,
321
336
  type: 'switch',
322
- value: changed[switchKey],
323
- key: loopKey
337
+ value: val,
338
+ key: specificLoopKey
324
339
  });
325
340
  }
326
341
  }
@@ -760,6 +775,7 @@ module.exports = function(RED) {
760
775
  // 记录发送到的KNX地址和时间,防止自己发的命令又被处理
761
776
  const destAddr = knxMsg.knx.destination;
762
777
  node.lastKnxAddrSent[destAddr] = Date.now();
778
+ node.lastKnxValueSent[destAddr] = knxMsg.payload;
763
779
 
764
780
  // knxUltimate输入格式:topic + destination + payload + dpt + event
765
781
  // topic: 当setTopicType=str时,knxUltimate使用msg.topic作为目标地址
@@ -967,36 +983,42 @@ module.exports = function(RED) {
967
983
  }
968
984
 
969
985
  // 检查是否是我们自己发出去的命令(防止自己发的命令被监听后又处理)
970
- // 检查该设备的所有关联地址,只要有一个最近发送过,就认为是回显
971
- const wasSentRecently = mapping.allKnxAddrs.some(addr => {
986
+ // 检查该设备的所有关联地址,只要有一个最近发送过且值相同,就认为是回显
987
+ const isEcho = mapping.allKnxAddrs.some(addr => {
972
988
  const lastSentTime = node.lastKnxAddrSent[addr] || 0;
973
- return (Date.now() - lastSentTime) < DEFAULT_TIMEOUT;
989
+ const inWindow = (Date.now() - lastSentTime) < DEFAULT_TIMEOUT;
990
+ if (!inWindow) return false;
991
+
992
+ // 如果在时间窗口内,进一步检查值是否相同
993
+ const lastValue = node.lastKnxValueSent[addr];
994
+ // 对于布尔值,直接比较;对于数字,允许微小误差?KNX通常是精确的
995
+ return msg.payload === lastValue;
974
996
  });
975
997
 
976
- if (wasSentRecently) {
977
- node.debug(`[KNX输入] 跳过(自己发的回显): ${groupAddr} (设备: ${mapping.name})`);
998
+ if (isEcho) {
999
+ node.debug(`[KNX输入] 跳过(自己发的回显): ${groupAddr} (设备: ${mapping.name}, 值: ${msg.payload})`);
978
1000
  done && done();
979
1001
  return;
980
1002
  }
981
1003
 
982
1004
  // 确定地址功能(优先使用地址匹配,比DPT更可靠)
983
1005
  const addrFunc = node.getKnxAddrFunction(mapping, groupAddr);
984
- const loopKey = `${mapping.meshMac}_${mapping.meshChannel}`;
985
-
986
- // 防死循环检查(基于设备的双向同步防护)
987
- if (node.shouldPreventSync('knx-to-mesh', loopKey)) {
988
- node.debug(`[KNX->Mesh] 跳过同步(防死循环): ${loopKey}, addr=${groupAddr}`);
989
- done && done();
990
- return;
991
- }
992
-
993
- node.log(`[KNX输入] 收到: ${groupAddr} = ${value}, DPT=${dpt}, 功能=${addrFunc}, 设备=${mapping.deviceType}`);
994
1006
 
995
1007
  // 根据设备类型和地址功能处理
996
1008
  if (mapping.deviceType === 'switch') {
997
1009
  // 开关命令(只处理cmd地址)
998
1010
  if (addrFunc === 'cmd' || addrFunc === 'status') {
999
1011
  const switchValue = (value === 1 || value === true || value === 'on' || value === 'ON');
1012
+ const loopKey = `${mapping.meshMac}_${mapping.meshChannel}_switch_${switchValue}`;
1013
+
1014
+ // 防死循环检查
1015
+ if (node.shouldPreventSync('knx-to-mesh', loopKey)) {
1016
+ node.debug(`[KNX->Mesh] 跳过同步(防死循环): ${loopKey}, addr=${groupAddr}`);
1017
+ done && done();
1018
+ return;
1019
+ }
1020
+
1021
+ node.log(`[KNX输入] 收到: ${groupAddr} = ${value}, DPT=${dpt}, 功能=${addrFunc}, 设备=${mapping.deviceType}`);
1000
1022
  node.queueCommand({
1001
1023
  direction: 'knx-to-mesh',
1002
1024
  mapping: mapping,
@@ -1011,6 +1033,7 @@ module.exports = function(RED) {
1011
1033
  else if (mapping.deviceType === 'cover') {
1012
1034
  const deviceKey = (mapping.meshMac || '').toLowerCase().replace(/:/g, '');
1013
1035
  const now = Date.now();
1036
+ const loopKey = `${mapping.meshMac}_${mapping.meshChannel}_cover`; // 窗帘使用通用key,因为有专门的controlLock
1014
1037
 
1015
1038
  // KNX主动发起控制,直接抢占锁定(用户操作优先)
1016
1039
  node.controlLock[deviceKey] = { controller: 'knx', lockUntil: now + COVER_CONTROL_LOCK_MS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.8.8",
3
+ "version": "1.8.9",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "nodes/symi-gateway.js",
6
6
  "scripts": {