node-red-contrib-symi-mesh 1.7.4 → 1.7.7

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
@@ -1462,6 +1462,20 @@ node-red-contrib-symi-mesh/
1462
1462
 
1463
1463
  ## 更新日志
1464
1464
 
1465
+ ### v1.7.5 (2025-12-24)
1466
+ - **API路径兼容性修复**:修复HassOS环境下节点配置界面无法加载设备列表的问题
1467
+ - **根本原因**:HassOS Node-RED使用nginx反向代理,绝对路径`/api`会被拦截,需使用相对路径`api`
1468
+ - **修复范围**:所有节点HTML中的`$.getJSON()`调用统一使用相对路径
1469
+ - **开发规范**:后续开发节点时,所有前端API调用必须使用相对路径,不带前导斜杠
1470
+ - **KNX Bridge双向同步修复**:修复Mesh控制KNX开关不工作的问题
1471
+ - **根本原因**:knxUltimate节点当`setTopicType: "str"`时使用`msg.topic`作为目标地址,而非`msg.destination`
1472
+ - **修复方案**:发送消息同时包含`topic`和`destination`字段,确保兼容所有knxUltimate配置
1473
+ - **消息格式**:`{ topic, destination, payload, dpt, event }`
1474
+ - **防死循环**:不包含`knx`对象,避免触发knxUltimate的循环引用保护机制
1475
+ - **用户体验优化**:
1476
+ - 设备列表加载时显示"加载中..."提示
1477
+ - API调用失败时显示"加载失败,请重试"提示
1478
+
1465
1479
  ### v1.7.4 (2025-12-24)
1466
1480
  - **串口配置完善**:完整的串口参数配置,解决HassOS环境串口锁定问题
1467
1481
  - **完整参数**:波特率、数据位(7/8)、停止位(1/2)、校验位(None/Even/Odd)
@@ -1508,7 +1522,16 @@ node-red-contrib-symi-mesh/
1508
1522
  - 内存安全,无调试日志,断电断网恢复后正常工作
1509
1523
  - 缓存队列处理,符合MQTT协议要求
1510
1524
 
1511
- ### v1.7.2 (2025-12-22)
1525
+ ### v1.7.7 (2026-01-05)
1526
+ - **RS485桥接节点逻辑优化**:
1527
+ - 修复自定义码模式下"反馈"按钮点击部署后依旧开启的问题
1528
+ - 完善 `oneditsave` 逻辑,确保反馈选项状态被持久化保存
1529
+ - **防循环优化**:优化 `loopKey` 生成逻辑,加入 `meshChannel` 字段,支持多通道设备独立防循环,解决多路开关干扰问题
1530
+ - **冷却时间调整**:将防死循环冷却时间从 500ms 延长至 800ms,提高复杂网络环境下的同步稳定性
1531
+ - **反馈逻辑细化**:严格执行"反馈"勾选逻辑,未勾选时禁止回环发送自定义码到总线
1532
+ - **调试增强**:增加自定义码匹配成功的详细日志(包含设备 MAC 和具体动作)
1533
+
1534
+ ### v1.7.5 (2025-12-24)
1512
1535
  - **RS485双向同步节点**:新增`symi-rs485-sync`独立节点实现两种不同RS485协议之间的双向同步
1513
1536
  - 支持中弘VRF网关协议(功能码0x31-0x34控制,0x50查询)
1514
1537
  - 支持SYMI空调面板Modbus协议
@@ -1585,23 +1608,6 @@ node-red-contrib-symi-mesh/
1585
1608
  - 防死循环机制优化
1586
1609
  - 内存优化,防止日志溢出
1587
1610
 
1588
- ### v1.6.9 (2025-12-20)
1589
- - **KNX-HA双向同步**:新增`symi-knx-ha-bridge`节点
1590
- - 直接连接KNX与HA实体,实现双向同步
1591
- - 支持Tab分隔格式导入KNX组地址配置(与KNX桥接节点100%一致)
1592
- - 使用共享HA服务器节点,通过REST API自动加载HA实体列表
1593
- - 支持开关、灯光、窗帘、空调、风扇等设备类型
1594
- - 完整双向同步:KNX↔HA实时状态同步
1595
- - 事件驱动架构,通过HA events-state节点实时接收事件
1596
- - 智能防抖:调光300ms、窗帘500ms,只同步最终值
1597
- - 内置防死循环机制(800ms防抖)
1598
- - 快速输入框:支持输入实体ID或名称搜索,自动提示806+实体
1599
- - 手动刷新按钮,随时重新加载HA实体
1600
- - 宽屏界面:对话框最小1000px宽度
1601
- - 持久化配置保存,内存优化
1602
- - 连接knxUltimate节点,无缝集成
1603
- - 适用于已有KNX系统与HA整合的场景
1604
-
1605
1611
  ## 许可证
1606
1612
 
1607
1613
  MIT License
@@ -1613,8 +1619,8 @@ Copyright (c) 2025 SYMI 亖米
1613
1619
  ## 关于
1614
1620
 
1615
1621
  **作者**: SYMI 亖米
1616
- **版本**: 1.7.4
1622
+ **版本**: 1.7.6
1617
1623
  **协议**: 蓝牙MESH网关(初级版)串口协议V1.0
1618
- **最后更新**: 2025-12-24
1624
+ **最后更新**: 2026-01-05
1619
1625
  **仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
1620
1626
  **npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
@@ -80,12 +80,12 @@
80
80
  "notifyreadrequest": false,
81
81
  "notifyresponse": false,
82
82
  "notifywrite": true,
83
- "name": "Universal KNX",
83
+ "name": "Universal KNX Output",
84
84
  "outputtype": "write",
85
85
  "outputRBE": "false",
86
86
  "inputRBE": "false",
87
87
  "passthrough": "no",
88
- "listenallga": true,
88
+ "listenallga": false,
89
89
  "buttonEnabled": false,
90
90
  "x": 620,
91
91
  "y": 120,
@@ -34,7 +34,7 @@
34
34
 
35
35
  function loadHistory() {
36
36
  if (!node.id) return;
37
- $.getJSON('/rs485-debug/history/' + node.id, function(messages) {
37
+ $.getJSON('rs485-debug/history/' + node.id, function(messages) {
38
38
  var container = $('#debug-history');
39
39
 
40
40
  if (messages.length === 0) {
@@ -43,7 +43,7 @@
43
43
  if (callback) callback();
44
44
  return;
45
45
  }
46
- $.getJSON('/symi-rs485-bridge/mesh-devices/' + gatewayId)
46
+ $.getJSON('symi-rs485-bridge/mesh-devices/' + gatewayId)
47
47
  .done(function(devices) {
48
48
  meshDevices = devices || [];
49
49
  console.log('[RS485 Bridge] 加载Mesh设备:', meshDevices.length);
@@ -56,7 +56,7 @@
56
56
  }
57
57
 
58
58
  // 加载协议模板
59
- $.getJSON('/symi-rs485-bridge/protocols', function(data) {
59
+ $.getJSON('symi-rs485-bridge/protocols', function(data) {
60
60
  protocolData = data || { brands: {} };
61
61
  // 延迟加载设备,确保gateway选择框已初始化
62
62
  setTimeout(function() {
@@ -662,7 +662,8 @@
662
662
  brand: $(this).find('.brand-select').val() || '',
663
663
  device: $(this).find('.device-select').val() || '',
664
664
  address: address,
665
- rs485Channel: parseInt($(this).find('.rs485-channel').val()) || 1
665
+ rs485Channel: parseInt($(this).find('.rs485-channel').val()) || 1,
666
+ feedback: $(this).find('.feedback-checkbox').is(':checked')
666
667
  };
667
668
  // 保存杜亚窗帘2字节地址
668
669
  if (m.brand === 'duya') {
@@ -964,7 +964,7 @@ module.exports = function(RED) {
964
964
  node.commandQueue = [];
965
965
  node.processing = false;
966
966
  node.syncLock = false;
967
- node.lastSyncTime = 0;
967
+ node.lastSyncTime = {};
968
968
  node.pendingVerify = false;
969
969
 
970
970
  // RS485连接信息
@@ -1597,7 +1597,7 @@ module.exports = function(RED) {
1597
1597
  }
1598
1598
  } finally {
1599
1599
  node.processing = false;
1600
- node.lastSyncTime = Date.now();
1600
+ node.lastQueueProcessTime = Date.now();
1601
1601
  }
1602
1602
  };
1603
1603
 
@@ -1709,12 +1709,12 @@ module.exports = function(RED) {
1709
1709
  const codes = mapping.customCodes;
1710
1710
 
1711
1711
  // 检查反馈选项:如果feedback=false,检查是否是RS485触发的状态变化
1712
- // 如果是RS485触发的(500ms内有同步记录),则跳过发送反馈码
1713
- const loopKey = `${mapping.meshMac}_${mapping.device}`;
1712
+ // 如果是RS485触发的(800ms内有同步记录),则跳过发送反馈码,防止死循环
1713
+ const loopKey = `${mapping.meshMac}_${mapping.device}_${mapping.meshChannel || 1}`;
1714
1714
  if (mapping.feedback === false) {
1715
- const lastSync = node.lastSyncTime ? node.lastSyncTime[loopKey] : 0;
1716
- if (lastSync && Date.now() - lastSync < 500) {
1717
- node.debug(`[Mesh->自定义] 反馈已禁用,跳过发送`);
1715
+ const lastSync = (node.lastSyncTime && typeof node.lastSyncTime === 'object') ? node.lastSyncTime[loopKey] : 0;
1716
+ if (lastSync && Date.now() - lastSync < 800) {
1717
+ node.debug(`[Mesh->自定义] 反馈已禁用,跳过回环发送`);
1718
1718
  return;
1719
1719
  }
1720
1720
  }
@@ -1753,7 +1753,7 @@ module.exports = function(RED) {
1753
1753
  const hexCode = switchValue ? codes.sendOn : codes.sendOff;
1754
1754
  if (hexCode) {
1755
1755
  // 记录发送时间用于防死循环
1756
- const loopKey = `${mapping.meshMac}_${mapping.device}`;
1756
+ const loopKey = `${mapping.meshMac}_${mapping.device}_${mapping.meshChannel || 1}`;
1757
1757
  if (!node.lastSyncTime) node.lastSyncTime = {};
1758
1758
  node.lastSyncTime[loopKey] = Date.now();
1759
1759
 
@@ -1811,7 +1811,7 @@ module.exports = function(RED) {
1811
1811
  node.lastSentTime[cacheKey] = now;
1812
1812
 
1813
1813
  // 记录发送时间用于防死循环
1814
- const loopKey = `${mapping.meshMac}_${mapping.device}`;
1814
+ const loopKey = `${mapping.meshMac}_${mapping.device}_${mapping.meshChannel || 1}`;
1815
1815
  if (!node.lastSyncTime) node.lastSyncTime = {};
1816
1816
  node.lastSyncTime[loopKey] = now;
1817
1817
 
@@ -1825,7 +1825,7 @@ module.exports = function(RED) {
1825
1825
  node.debug(`[自定义空调] 处理状态: ${JSON.stringify(state)}, codes存在: ${!!codes}`);
1826
1826
 
1827
1827
  // 记录发送时间用于防死循环
1828
- const loopKey = `${mapping.meshMac}_${mapping.device}`;
1828
+ const loopKey = `${mapping.meshMac}_${mapping.device}_${mapping.meshChannel || 1}`;
1829
1829
  if (!node.lastSyncTime) node.lastSyncTime = {};
1830
1830
 
1831
1831
  // 去重处理:记录已处理的命令类型
@@ -2813,14 +2813,14 @@ module.exports = function(RED) {
2813
2813
  if (matchedAction) {
2814
2814
  // 防死循环:检查是否刚刚从Mesh发送过来
2815
2815
  // 使用映射特定的时间戳,避免其他设备的同步影响
2816
- const loopKey = `${mapping.meshMac}_${mapping.device}`;
2816
+ const loopKey = `${mapping.meshMac}_${mapping.device}_${mapping.meshChannel || 1}`;
2817
2817
  if (!node.lastSyncTime) node.lastSyncTime = {};
2818
- if (node.lastSyncTime[loopKey] && Date.now() - node.lastSyncTime[loopKey] < 500) {
2818
+ if (node.lastSyncTime[loopKey] && Date.now() - node.lastSyncTime[loopKey] < 800) {
2819
2819
  node.debug(`[防循环] 忽略刚刚同步的帧: ${hexFormatted}`);
2820
2820
  return;
2821
2821
  }
2822
2822
 
2823
- node.log(`[自定义码匹配] ${mapping.device}: ${JSON.stringify(matchedAction)}, 帧: ${hexFormatted}`);
2823
+ node.log(`[自定义码匹配成功] 设备:${mapping.device}, MAC:${mapping.meshMac}, 动作:${JSON.stringify(matchedAction)}`);
2824
2824
 
2825
2825
  // 输出调试信息到节点输出端口
2826
2826
  node.send({
@@ -38,7 +38,7 @@
38
38
  $btn.prop('disabled', true);
39
39
  $select.empty().append('<option value="">搜索中...</option>').show();
40
40
 
41
- $.getJSON('/symi-gateway/serial-ports', function(ports) {
41
+ $.getJSON('symi-gateway/serial-ports', function(ports) {
42
42
  $select.empty();
43
43
  if (ports && ports.length > 0) {
44
44
  $select.append('<option value="">-- 选择串口 --</option>');
@@ -60,6 +60,7 @@
60
60
 
61
61
  var loadDevices = function(gatewayId) {
62
62
  if (!gatewayId) {
63
+ console.log('[symi-device] 网关ID为空,跳过加载');
63
64
  return;
64
65
  }
65
66
 
@@ -67,11 +68,16 @@
67
68
  var currentValue = deviceSelect.val();
68
69
 
69
70
  deviceSelect.empty();
70
- deviceSelect.append('<option value="">-- 选择设备 --</option>');
71
+ deviceSelect.append('<option value="">加载中...</option>');
71
72
 
72
- $.getJSON('/symi-gateway/devices/' + gatewayId)
73
+ var apiUrl = 'symi-gateway/devices/' + gatewayId;
74
+ console.log('[symi-device] 加载设备列表, API:', apiUrl);
75
+
76
+ $.getJSON(apiUrl)
73
77
  .done(function(devices) {
74
- console.log('加载设备列表:', devices.length, '个设备');
78
+ console.log('[symi-device] 加载成功:', devices ? devices.length : 0, '个设备');
79
+ deviceSelect.empty();
80
+ deviceSelect.append('<option value="">-- 选择设备 --</option>');
75
81
  if (devices && devices.length > 0) {
76
82
  devices.forEach(function(device) {
77
83
  var label = device.name + ' (' + device.mac + ')';
@@ -99,8 +105,10 @@
99
105
 
100
106
  deviceSelect.trigger('change');
101
107
  })
102
- .fail(function(err) {
103
- console.log('加载设备失败:', err);
108
+ .fail(function(xhr, status, err) {
109
+ console.log('[symi-device] 加载设备失败:', status, err, xhr.responseText);
110
+ deviceSelect.empty();
111
+ deviceSelect.append('<option value="">加载失败,请重试</option>');
104
112
  });
105
113
  };
106
114
 
@@ -37,7 +37,7 @@
37
37
  $btn.prop('disabled', true).text('搜索中...');
38
38
  $select.empty().append('<option value="">搜索中...</option>');
39
39
 
40
- $.getJSON('/symi-gateway/serial-ports', function(ports) {
40
+ $.getJSON('symi-gateway/serial-ports', function(ports) {
41
41
  $select.empty();
42
42
  if (ports && ports.length > 0) {
43
43
  $select.append('<option value="">-- 选择串口 --</option>');
@@ -760,9 +760,12 @@ module.exports = function(RED) {
760
760
  const destAddr = knxMsg.knx.destination;
761
761
  node.lastKnxAddrSent[destAddr] = Date.now();
762
762
 
763
- // knxUltimate官方格式: https://supergiovane.github.io/node-red-contrib-knx-ultimate/wiki/Device
764
- // 必须包含: destination, payload, dpt, event
763
+ // knxUltimate输入格式:topic + destination + payload + dpt + event
764
+ // topic: 当setTopicType=str时,knxUltimate使用msg.topic作为目标地址
765
+ // destination: 官方备用字段
766
+ // 注意:不能包含knx对象,否则会触发knxUltimate的循环引用保护
765
767
  const sendMsg = {
768
+ topic: destAddr,
766
769
  destination: destAddr,
767
770
  payload: knxMsg.payload,
768
771
  dpt: knxMsg.dpt || knxMsg.knx.dpt,
@@ -784,7 +787,7 @@ module.exports = function(RED) {
784
787
  timestamp: new Date().toISOString()
785
788
  };
786
789
 
787
- node.log(`[Mesh->KNX] 发送: destination=${sendMsg.destination}, payload=${sendMsg.payload}, dpt=${sendMsg.dpt}, event=${sendMsg.event}`);
790
+ node.log(`[Mesh->KNX] 发送: topic=${sendMsg.topic}, payload=${sendMsg.payload}, dpt=${sendMsg.dpt}`);
788
791
 
789
792
  // 同时发送到两个输出端口(一次send调用)
790
793
  node.send([sendMsg, debugMsg]);
@@ -59,7 +59,7 @@
59
59
  if (callback) callback();
60
60
  return;
61
61
  }
62
- $.getJSON('/symi-gateway/devices/' + gatewayId, function(devices) {
62
+ $.getJSON('symi-gateway/devices/' + gatewayId, function(devices) {
63
63
  meshDevices = devices || [];
64
64
  if (callback) callback();
65
65
  }).fail(function() {
@@ -76,7 +76,7 @@
76
76
  if (callback) callback();
77
77
  return;
78
78
  }
79
- $.getJSON('/symi-mqtt-brand/devices/' + brandConfigId, function(devices) {
79
+ $.getJSON('symi-mqtt-brand/devices/' + brandConfigId, function(devices) {
80
80
  brandDevices = devices || [];
81
81
  if (callback) callback();
82
82
  }).fail(function() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.7.4",
3
+ "version": "1.7.7",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "nodes/symi-gateway.js",
6
6
  "scripts": {