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 +26 -20
- package/examples/knx-sync-example.json +2 -2
- package/nodes/rs485-debug.html +1 -1
- package/nodes/symi-485-bridge.html +4 -3
- package/nodes/symi-485-bridge.js +13 -13
- package/nodes/symi-485-config.html +1 -1
- package/nodes/symi-device.html +13 -5
- package/nodes/symi-gateway.html +1 -1
- package/nodes/symi-knx-bridge.js +6 -3
- package/nodes/symi-mqtt-sync.html +2 -2
- package/package.json +1 -1
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.
|
|
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.
|
|
1622
|
+
**版本**: 1.7.6
|
|
1617
1623
|
**协议**: 蓝牙MESH网关(初级版)串口协议V1.0
|
|
1618
|
-
**最后更新**:
|
|
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":
|
|
88
|
+
"listenallga": false,
|
|
89
89
|
"buttonEnabled": false,
|
|
90
90
|
"x": 620,
|
|
91
91
|
"y": 120,
|
package/nodes/rs485-debug.html
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
function loadHistory() {
|
|
36
36
|
if (!node.id) return;
|
|
37
|
-
$.getJSON('
|
|
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('
|
|
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('
|
|
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') {
|
package/nodes/symi-485-bridge.js
CHANGED
|
@@ -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 =
|
|
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.
|
|
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触发的(
|
|
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 <
|
|
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] <
|
|
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(`[
|
|
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('
|
|
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>');
|
package/nodes/symi-device.html
CHANGED
|
@@ -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=""
|
|
71
|
+
deviceSelect.append('<option value="">加载中...</option>');
|
|
71
72
|
|
|
72
|
-
|
|
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('
|
|
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
|
|
package/nodes/symi-gateway.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
$btn.prop('disabled', true).text('搜索中...');
|
|
38
38
|
$select.empty().append('<option value="">搜索中...</option>');
|
|
39
39
|
|
|
40
|
-
$.getJSON('
|
|
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>');
|
package/nodes/symi-knx-bridge.js
CHANGED
|
@@ -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
|
|
764
|
-
//
|
|
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] 发送:
|
|
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('
|
|
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('
|
|
79
|
+
$.getJSON('symi-mqtt-brand/devices/' + brandConfigId, function(devices) {
|
|
80
80
|
brandDevices = devices || [];
|
|
81
81
|
if (callback) callback();
|
|
82
82
|
}).fail(function() {
|