node-red-contrib-symi-mesh 1.8.7 → 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 +23 -0
- package/nodes/rs485-debug.js +1 -1
- package/nodes/symi-485-bridge.js +25 -25
- package/nodes/symi-485-config.js +4 -4
- package/nodes/symi-cloud-sync.js +11 -11
- package/nodes/symi-device.js +1 -1
- package/nodes/symi-ha-sync.js +10 -10
- package/nodes/symi-knx-bridge.js +62 -27
- package/nodes/symi-knx-ha-bridge.js +4 -4
- package/nodes/symi-mqtt-brand.js +4 -4
- package/nodes/symi-mqtt-sync.js +3 -3
- package/nodes/symi-mqtt.js +18 -18
- package/nodes/symi-rs485-sync.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -704,6 +704,29 @@ 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
|
+
|
|
719
|
+
### v1.8.8 (2026-01-14)
|
|
720
|
+
|
|
721
|
+
**KNX 双向同步深度修复与协议鲁棒性增强**:
|
|
722
|
+
- **分包与粘包彻底解决**:重构 `ProtocolHandler` 缓存机制,支持 **8+1 分包、多包连续粘包**等极端情况下的完整解析。通过 Buffer 累加与帧头部实时扫描技术,确保无论串口数据如何切分,都能还原为完整的协议帧。
|
|
723
|
+
- **KNX 双向同步死循环修复**:
|
|
724
|
+
- **MAC 地址标准化**:统一所有节点(配置、事件、缓存)的 MAC 地址为小写且无冒号格式,彻底解决由于大小写不一致导致的“找不到映射”或“防死循环误杀”问题。
|
|
725
|
+
- **设备级回显消除**:引入 `allKnxAddrs` 关联检查,KNX 发出指令后,自动屏蔽该设备下所有关联地址(命令/状态/位置)的短时回显,防止自发自收导致的同步环路。
|
|
726
|
+
- **4 键开关通道精准匹配**:修复多路开关状态上报时,`switch_1` 到 `switch_4` 的动态订阅逻辑,确保 Mesh 端的每一路状态都能精准同步到对应的 KNX 组地址。
|
|
727
|
+
- **生产环境日志优化**:所有原始报文降级为 `debug` 级别,仅保留关键的同步逻辑日志为 `log` 级别,确保在长时间运行下不占用额外硬盘 IO,防止 Node-RED 变慢。
|
|
728
|
+
- **三合一面板持久化增强**:完善 `symi-mesh-data` 目录下的 JSON 持久化逻辑,确保三合一设备类型在 Node-RED 重启后能立即恢复,无需重新探测。
|
|
729
|
+
|
|
707
730
|
### v1.8.7 (2026-01-08)
|
|
708
731
|
|
|
709
732
|
**生产环境日志优化与稳定性增强**:
|
package/nodes/rs485-debug.js
CHANGED
package/nodes/symi-485-bridge.js
CHANGED
|
@@ -954,7 +954,7 @@ module.exports = function(RED) {
|
|
|
954
954
|
});
|
|
955
955
|
} catch (e) {
|
|
956
956
|
node.mappings = [];
|
|
957
|
-
node.
|
|
957
|
+
node.log(`映射配置解析失败: ${e.message}`);
|
|
958
958
|
}
|
|
959
959
|
|
|
960
960
|
if (!node.gateway) {
|
|
@@ -1000,12 +1000,12 @@ module.exports = function(RED) {
|
|
|
1000
1000
|
};
|
|
1001
1001
|
|
|
1002
1002
|
const onRS485Disconnected = () => {
|
|
1003
|
-
node.
|
|
1003
|
+
node.log(`[RS485 Bridge] 已断开 ${rs485Info}`);
|
|
1004
1004
|
node.status({ fill: 'yellow', shape: 'ring', text: `已断开 ${rs485Info}` });
|
|
1005
1005
|
};
|
|
1006
1006
|
|
|
1007
1007
|
const onRS485Error = (err) => {
|
|
1008
|
-
node.
|
|
1008
|
+
node.log(`[RS485 Bridge] 连接错误 ${rs485Info}: ${err.message}`);
|
|
1009
1009
|
node.status({ fill: 'red', shape: 'ring', text: `错误 ${rs485Info}` });
|
|
1010
1010
|
};
|
|
1011
1011
|
|
|
@@ -1247,7 +1247,7 @@ module.exports = function(RED) {
|
|
|
1247
1247
|
const hexStr = frame.toString('hex').toUpperCase().match(/.{2}/g).join(' ');
|
|
1248
1248
|
node.log(`[Mesh->杜亚] ${actionName}: ${hexStr}`);
|
|
1249
1249
|
}).catch(err => {
|
|
1250
|
-
node.
|
|
1250
|
+
node.log(`[Mesh->杜亚] 发送失败: ${err.message}`);
|
|
1251
1251
|
});
|
|
1252
1252
|
}
|
|
1253
1253
|
|
|
@@ -1259,7 +1259,7 @@ module.exports = function(RED) {
|
|
|
1259
1259
|
const hexStr = posFrame.toString('hex').toUpperCase().match(/.{2}/g).join(' ');
|
|
1260
1260
|
node.log(`[Mesh->杜亚] 同步位置${currentPosition}%: ${hexStr}`);
|
|
1261
1261
|
}).catch(err => {
|
|
1262
|
-
node.
|
|
1262
|
+
node.log(`[Mesh->杜亚] 位置同步失败: ${err.message}`);
|
|
1263
1263
|
});
|
|
1264
1264
|
}, 100);
|
|
1265
1265
|
}
|
|
@@ -1320,7 +1320,7 @@ module.exports = function(RED) {
|
|
|
1320
1320
|
node.sendCustomCode(hexCode).then(() => {
|
|
1321
1321
|
node.log(`[Mesh->自定义窗帘] ${actionName}: ${hexCode}`);
|
|
1322
1322
|
}).catch(err => {
|
|
1323
|
-
node.
|
|
1323
|
+
node.log(`[Mesh->自定义窗帘] 发送失败: ${err.message}`);
|
|
1324
1324
|
});
|
|
1325
1325
|
} else {
|
|
1326
1326
|
node.debug(`[Mesh->自定义窗帘] 无匹配码, action=${curtainAction}, status=${curtainStatus}`);
|
|
@@ -1485,7 +1485,7 @@ module.exports = function(RED) {
|
|
|
1485
1485
|
const hexStr = frame.toString('hex').toUpperCase();
|
|
1486
1486
|
node.log(`[Mesh控制->杜亚] 窗帘 ${actionName}, 立即发送: ${hexStr.match(/.{2}/g).join(' ')}`);
|
|
1487
1487
|
}).catch(err => {
|
|
1488
|
-
node.
|
|
1488
|
+
node.log(`[Mesh控制->杜亚] 发送失败: ${err.message}`);
|
|
1489
1489
|
});
|
|
1490
1490
|
}
|
|
1491
1491
|
}
|
|
@@ -1534,7 +1534,7 @@ module.exports = function(RED) {
|
|
|
1534
1534
|
node.sendCustomCode(hexCode).then(() => {
|
|
1535
1535
|
node.log(`[Mesh控制->自定义] 窗帘 ${actionName}, 立即发送: ${hexCode}`);
|
|
1536
1536
|
}).catch(err => {
|
|
1537
|
-
node.
|
|
1537
|
+
node.log(`[Mesh控制->自定义] 发送失败: ${err.message}`);
|
|
1538
1538
|
});
|
|
1539
1539
|
}
|
|
1540
1540
|
}
|
|
@@ -1566,7 +1566,7 @@ module.exports = function(RED) {
|
|
|
1566
1566
|
node.queueCommand = function(cmd) {
|
|
1567
1567
|
// 队列过大时丢弃旧命令,防止内存溢出
|
|
1568
1568
|
if (node.commandQueue.length >= MAX_QUEUE_SIZE) {
|
|
1569
|
-
node.
|
|
1569
|
+
node.log(`[RS485 Bridge] 命令队列已满(${MAX_QUEUE_SIZE}),丢弃最旧命令`);
|
|
1570
1570
|
node.commandQueue.shift(); // 丢弃最旧的命令
|
|
1571
1571
|
}
|
|
1572
1572
|
|
|
@@ -1610,7 +1610,7 @@ module.exports = function(RED) {
|
|
|
1610
1610
|
// 命令之间延迟50ms
|
|
1611
1611
|
await node.sleep(50);
|
|
1612
1612
|
} catch (err) {
|
|
1613
|
-
node.
|
|
1613
|
+
node.log(`同步失败: ${err.message}`);
|
|
1614
1614
|
}
|
|
1615
1615
|
}
|
|
1616
1616
|
|
|
@@ -1787,7 +1787,7 @@ module.exports = function(RED) {
|
|
|
1787
1787
|
await node.sendCustomCode(hexCode);
|
|
1788
1788
|
node.log(`[Mesh->自定义] 开关(${foundKey}): ${switchValue ? '开' : '关'}, 发送: ${hexCode}`);
|
|
1789
1789
|
} else {
|
|
1790
|
-
node.
|
|
1790
|
+
node.log(`[Mesh->自定义] 开关(${foundKey}): ${switchValue ? '开' : '关'}, 缺少${switchValue ? 'sendOn' : 'sendOff'}码`);
|
|
1791
1791
|
}
|
|
1792
1792
|
}
|
|
1793
1793
|
}
|
|
@@ -1944,7 +1944,7 @@ module.exports = function(RED) {
|
|
|
1944
1944
|
await node.sendCustomCode(hexCode);
|
|
1945
1945
|
node.log(`[Mesh->自定义] 空调 ${key}=${value}, 发送: ${hexCode}`);
|
|
1946
1946
|
} else if (codeType) {
|
|
1947
|
-
node.
|
|
1947
|
+
node.log(`[Mesh->自定义] 空调 ${key}=${value}, 缺少${codeType}码`);
|
|
1948
1948
|
}
|
|
1949
1949
|
}
|
|
1950
1950
|
}
|
|
@@ -2117,7 +2117,7 @@ module.exports = function(RED) {
|
|
|
2117
2117
|
}
|
|
2118
2118
|
}
|
|
2119
2119
|
} catch (err) {
|
|
2120
|
-
node.
|
|
2120
|
+
node.log(`RS485写入失败: ${meshKey}=${value}, ${err.message}`);
|
|
2121
2121
|
}
|
|
2122
2122
|
}
|
|
2123
2123
|
|
|
@@ -2129,7 +2129,7 @@ module.exports = function(RED) {
|
|
|
2129
2129
|
if (!hexCode) return;
|
|
2130
2130
|
const hexStr = hexCode.replace(/\s/g, '');
|
|
2131
2131
|
if (!/^[0-9A-Fa-f]+$/.test(hexStr)) {
|
|
2132
|
-
node.
|
|
2132
|
+
node.log(`无效的十六进制码: ${hexCode}`);
|
|
2133
2133
|
return;
|
|
2134
2134
|
}
|
|
2135
2135
|
const frame = Buffer.from(hexStr, 'hex');
|
|
@@ -2159,10 +2159,10 @@ module.exports = function(RED) {
|
|
|
2159
2159
|
}
|
|
2160
2160
|
|
|
2161
2161
|
if (!meshDevice) {
|
|
2162
|
-
node.
|
|
2162
|
+
node.log(`[RS485->Mesh] 未找到Mesh设备: ${meshMac} (规范化: ${macNormalized})`);
|
|
2163
2163
|
// 输出可用设备列表帮助调试
|
|
2164
2164
|
const allDevices = node.gateway.deviceManager?.getAllDevices() || [];
|
|
2165
|
-
node.
|
|
2165
|
+
node.log(`[RS485->Mesh] 可用设备: ${allDevices.map(d => d.macAddress).join(', ')}`);
|
|
2166
2166
|
return;
|
|
2167
2167
|
}
|
|
2168
2168
|
|
|
@@ -2187,7 +2187,7 @@ module.exports = function(RED) {
|
|
|
2187
2187
|
node.log(`[杜亚->Mesh] 窗帘位置: ${pos}%`);
|
|
2188
2188
|
}
|
|
2189
2189
|
} catch (err) {
|
|
2190
|
-
node.
|
|
2190
|
+
node.log(`[杜亚->Mesh] 写入失败: ${err.message}`);
|
|
2191
2191
|
}
|
|
2192
2192
|
|
|
2193
2193
|
node.status({ fill: 'blue', shape: 'dot', text: `杜亚同步 ${node.mappings.length}个` });
|
|
@@ -2246,7 +2246,7 @@ module.exports = function(RED) {
|
|
|
2246
2246
|
node.log(`[自定义->Mesh] 窗帘: ${value} (动作码${action})`);
|
|
2247
2247
|
}
|
|
2248
2248
|
} catch (err) {
|
|
2249
|
-
node.
|
|
2249
|
+
node.log(`Mesh写入失败: ${key}=${value}, ${err.message}`);
|
|
2250
2250
|
}
|
|
2251
2251
|
}
|
|
2252
2252
|
|
|
@@ -2356,7 +2356,7 @@ module.exports = function(RED) {
|
|
|
2356
2356
|
node.log(`[RS485->Mesh] 新风风速: RS485值${value} -> Mesh值${meshSpeed}`);
|
|
2357
2357
|
}
|
|
2358
2358
|
} catch (err) {
|
|
2359
|
-
node.
|
|
2359
|
+
node.log(`Mesh写入失败: ${key}=${value}, ${err.message}`);
|
|
2360
2360
|
}
|
|
2361
2361
|
}
|
|
2362
2362
|
|
|
@@ -2418,7 +2418,7 @@ module.exports = function(RED) {
|
|
|
2418
2418
|
// 发送RS485帧(通过配置节点)
|
|
2419
2419
|
node.sendRS485Frame = async function(frame) {
|
|
2420
2420
|
if (!node.rs485Config || !node.rs485Config.connected) {
|
|
2421
|
-
node.
|
|
2421
|
+
node.log('RS485未连接,无法发送数据');
|
|
2422
2422
|
return;
|
|
2423
2423
|
}
|
|
2424
2424
|
try {
|
|
@@ -2439,7 +2439,7 @@ module.exports = function(RED) {
|
|
|
2439
2439
|
timestamp: new Date().toISOString()
|
|
2440
2440
|
});
|
|
2441
2441
|
} catch (err) {
|
|
2442
|
-
node.
|
|
2442
|
+
node.log(`RS485发送失败: ${err.message}`);
|
|
2443
2443
|
}
|
|
2444
2444
|
};
|
|
2445
2445
|
|
|
@@ -2511,7 +2511,7 @@ module.exports = function(RED) {
|
|
|
2511
2511
|
}
|
|
2512
2512
|
|
|
2513
2513
|
if (!foundMapping) {
|
|
2514
|
-
node.
|
|
2514
|
+
node.log(`[杜亚] 未找到匹配的映射, 帧地址=${duyaData.addrHigh}:${duyaData.addrLow},请检查RS485桥配置`);
|
|
2515
2515
|
}
|
|
2516
2516
|
} else {
|
|
2517
2517
|
node.debug(`[杜亚帧检测] 解析失败,可能funcCode不是0x03`);
|
|
@@ -2566,7 +2566,7 @@ module.exports = function(RED) {
|
|
|
2566
2566
|
// 转换为中弘命令
|
|
2567
2567
|
const zhCmd = convertSymiToZhonghong(symiData, mapping);
|
|
2568
2568
|
if (!zhCmd) {
|
|
2569
|
-
node.
|
|
2569
|
+
node.log(`[SYMI->Zhonghong] 未知操作码: ${symiData.opCode}`);
|
|
2570
2570
|
continue;
|
|
2571
2571
|
}
|
|
2572
2572
|
|
|
@@ -2589,7 +2589,7 @@ module.exports = function(RED) {
|
|
|
2589
2589
|
timestamp: new Date().toISOString()
|
|
2590
2590
|
});
|
|
2591
2591
|
}).catch(err => {
|
|
2592
|
-
node.
|
|
2592
|
+
node.log(`[SYMI->Zhonghong] 发送失败: ${err.message}`);
|
|
2593
2593
|
});
|
|
2594
2594
|
|
|
2595
2595
|
return;
|
|
@@ -2681,7 +2681,7 @@ module.exports = function(RED) {
|
|
|
2681
2681
|
timestamp: new Date().toISOString()
|
|
2682
2682
|
});
|
|
2683
2683
|
}).catch(err => {
|
|
2684
|
-
node.
|
|
2684
|
+
node.log(`[Zhonghong->SYMI] 发送失败: ${err.message}`);
|
|
2685
2685
|
});
|
|
2686
2686
|
|
|
2687
2687
|
return;
|
package/nodes/symi-485-config.js
CHANGED
|
@@ -105,7 +105,7 @@ module.exports = function(RED) {
|
|
|
105
105
|
try {
|
|
106
106
|
if (node.connectionType === 'serial') {
|
|
107
107
|
if (!node.serialPort) {
|
|
108
|
-
node.
|
|
108
|
+
node.log('未配置串口');
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -157,7 +157,7 @@ module.exports = function(RED) {
|
|
|
157
157
|
|
|
158
158
|
} else if (node.connectionType === 'tcp') {
|
|
159
159
|
if (!node.host) {
|
|
160
|
-
node.
|
|
160
|
+
node.log('未配置TCP主机');
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -210,11 +210,11 @@ module.exports = function(RED) {
|
|
|
210
210
|
family: 4 // 强制IPv4,避免IPv6连接失败导致AggregateError
|
|
211
211
|
});
|
|
212
212
|
} catch (connectErr) {
|
|
213
|
-
node.
|
|
213
|
+
node.log(`RS485 TCP连接异常: ${connectErr.message}`);
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
} catch (err) {
|
|
217
|
-
node.
|
|
217
|
+
node.log(`RS485连接失败: ${err.message}`);
|
|
218
218
|
}
|
|
219
219
|
};
|
|
220
220
|
|
package/nodes/symi-cloud-sync.js
CHANGED
|
@@ -23,13 +23,13 @@ module.exports = function(RED) {
|
|
|
23
23
|
node.selectedScenes = config.selectedScenes || [];
|
|
24
24
|
|
|
25
25
|
if (!node.gateway) {
|
|
26
|
-
node.
|
|
26
|
+
node.log('未配置网关');
|
|
27
27
|
node.status({ fill: 'red', shape: 'ring', text: '未配置网关' });
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
if (!node.appId || !node.appSecret) {
|
|
32
|
-
node.
|
|
32
|
+
node.log('未配置云端认证信息');
|
|
33
33
|
node.status({ fill: 'yellow', shape: 'ring', text: '未配置认证' });
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
@@ -55,12 +55,12 @@ module.exports = function(RED) {
|
|
|
55
55
|
|
|
56
56
|
const syncFromCloud = async () => {
|
|
57
57
|
if (node.syncInProgress) {
|
|
58
|
-
node.
|
|
58
|
+
node.log('同步正在进行中,跳过');
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if (!node.hotelId || !node.roomNo) {
|
|
63
|
-
node.
|
|
63
|
+
node.log('未配置酒店ID或房间号,跳过同步');
|
|
64
64
|
node.status({ fill: 'yellow', shape: 'ring', text: '未配置房间' });
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
@@ -96,7 +96,7 @@ module.exports = function(RED) {
|
|
|
96
96
|
node.status({ fill: 'green', shape: 'dot', text: `已同步 ${new Date().toLocaleTimeString()}` });
|
|
97
97
|
|
|
98
98
|
} catch (error) {
|
|
99
|
-
node.
|
|
99
|
+
node.log(`云端同步失败: ${error.message}`);
|
|
100
100
|
node.status({ fill: 'red', shape: 'ring', text: '同步失败' });
|
|
101
101
|
|
|
102
102
|
const cached = loadCachedData();
|
|
@@ -112,7 +112,7 @@ module.exports = function(RED) {
|
|
|
112
112
|
|
|
113
113
|
const applyCloudData = (cloudData) => {
|
|
114
114
|
if (!cloudData || !cloudData.devices) {
|
|
115
|
-
node.
|
|
115
|
+
node.log('无可用的云端数据');
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -237,7 +237,7 @@ module.exports = function(RED) {
|
|
|
237
237
|
|
|
238
238
|
const publishSceneButtons = (scenes) => {
|
|
239
239
|
if (!node.mqttConfig || !node.mqttConfig.mqttClient) {
|
|
240
|
-
node.
|
|
240
|
+
node.log('MQTT客户端未连接,无法发布场景按钮');
|
|
241
241
|
return;
|
|
242
242
|
}
|
|
243
243
|
|
|
@@ -256,7 +256,7 @@ module.exports = function(RED) {
|
|
|
256
256
|
|
|
257
257
|
mqttClient.publish(config.topic, config.payload, { retain: true }, (err) => {
|
|
258
258
|
if (err) {
|
|
259
|
-
node.
|
|
259
|
+
node.log(`发布场景按钮失败: ${scene.scene_name}, ${err.message}`);
|
|
260
260
|
} else {
|
|
261
261
|
node.log(`场景按钮已发布: ${scene.scene_name}`);
|
|
262
262
|
}
|
|
@@ -307,13 +307,13 @@ module.exports = function(RED) {
|
|
|
307
307
|
node.gateway.sendScene(sceneId).then(() => {
|
|
308
308
|
node.log(`[场景控制] 场景控制命令已发送: ${scene.scene_name} (ID: ${sceneId})`);
|
|
309
309
|
}).catch(err => {
|
|
310
|
-
node.
|
|
310
|
+
node.log(`[场景控制] 场景控制命令发送失败: ${err.message}`);
|
|
311
311
|
});
|
|
312
312
|
} else {
|
|
313
|
-
node.
|
|
313
|
+
node.log('[场景控制] 网关未连接,无法执行场景');
|
|
314
314
|
}
|
|
315
315
|
} else {
|
|
316
|
-
node.
|
|
316
|
+
node.log(`[场景控制] 场景ID ${sceneId} 未找到`);
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
}
|
package/nodes/symi-device.js
CHANGED
package/nodes/symi-ha-sync.js
CHANGED
|
@@ -1049,7 +1049,7 @@ module.exports = function(RED) {
|
|
|
1049
1049
|
node.debug(`[HA->Symi] 空调风速: ${oldAttrs.fan_mode} -> ${attrs.fan_mode} (mesh: ${meshFan})`);
|
|
1050
1050
|
syncDataList.push({ type: 'fan_mode', value: meshFan });
|
|
1051
1051
|
} else {
|
|
1052
|
-
node.
|
|
1052
|
+
node.log(`[HA->Symi] 未知的空调风速值: "${attrs.fan_mode}",请检查映射配置`);
|
|
1053
1053
|
}
|
|
1054
1054
|
}
|
|
1055
1055
|
}
|
|
@@ -1138,7 +1138,7 @@ module.exports = function(RED) {
|
|
|
1138
1138
|
}
|
|
1139
1139
|
await node.sleep(50);
|
|
1140
1140
|
} catch (err) {
|
|
1141
|
-
node.
|
|
1141
|
+
node.log(`同步失败: ${err.message}`);
|
|
1142
1142
|
}
|
|
1143
1143
|
}
|
|
1144
1144
|
} finally {
|
|
@@ -1240,7 +1240,7 @@ module.exports = function(RED) {
|
|
|
1240
1240
|
|
|
1241
1241
|
// 如果 service 为空,说明该属性不支持同步到 HA
|
|
1242
1242
|
if (!service) {
|
|
1243
|
-
node.
|
|
1243
|
+
node.log(`[Symi->HA] 属性 ${syncData.type} 在领域 ${domain} 下暂不支持同步`);
|
|
1244
1244
|
return;
|
|
1245
1245
|
}
|
|
1246
1246
|
|
|
@@ -1260,7 +1260,7 @@ module.exports = function(RED) {
|
|
|
1260
1260
|
node.log(`[Symi->HA] ${mapping.symiName || mapping.symiMac} -> ${mapping.haEntityId}: ${syncData.type}=${JSON.stringify(syncData.value)}`);
|
|
1261
1261
|
|
|
1262
1262
|
} catch (err) {
|
|
1263
|
-
node.
|
|
1263
|
+
node.log(`[Symi->HA] 调用失败: ${err.message}`);
|
|
1264
1264
|
}
|
|
1265
1265
|
};
|
|
1266
1266
|
|
|
@@ -1272,13 +1272,13 @@ module.exports = function(RED) {
|
|
|
1272
1272
|
// 动态获取gateway(可能在初始化后才可用)
|
|
1273
1273
|
const currentGateway = gateway || (node.mqttNode && node.mqttNode.gateway);
|
|
1274
1274
|
if (!currentGateway) {
|
|
1275
|
-
node.
|
|
1275
|
+
node.log(`[HA->Symi] 网关未就绪,无法控制设备`);
|
|
1276
1276
|
return;
|
|
1277
1277
|
}
|
|
1278
1278
|
|
|
1279
1279
|
const device = currentGateway.getDevice(mapping.symiMac);
|
|
1280
1280
|
if (!device) {
|
|
1281
|
-
node.
|
|
1281
|
+
node.log(`[HA->Symi] 设备未找到: ${mapping.symiMac}`);
|
|
1282
1282
|
return;
|
|
1283
1283
|
}
|
|
1284
1284
|
|
|
@@ -1347,7 +1347,7 @@ module.exports = function(RED) {
|
|
|
1347
1347
|
node.log(`[HA->Symi] ${mapping.haEntityId} -> ${device.name || mapping.symiMac}: ${syncData.type}=${JSON.stringify(syncData.value)}`);
|
|
1348
1348
|
|
|
1349
1349
|
} catch (err) {
|
|
1350
|
-
node.
|
|
1350
|
+
node.log(`[HA->Symi] 控制失败: ${err.message}`);
|
|
1351
1351
|
}
|
|
1352
1352
|
};
|
|
1353
1353
|
|
|
@@ -1362,7 +1362,7 @@ module.exports = function(RED) {
|
|
|
1362
1362
|
// 动态获取gateway
|
|
1363
1363
|
const currentGateway = gateway || (node.mqttNode && node.mqttNode.gateway);
|
|
1364
1364
|
if (!currentGateway) {
|
|
1365
|
-
node.
|
|
1365
|
+
node.log(`[HA->Symi] 网关未就绪,无法控制三合一设备`);
|
|
1366
1366
|
return;
|
|
1367
1367
|
}
|
|
1368
1368
|
|
|
@@ -1442,11 +1442,11 @@ module.exports = function(RED) {
|
|
|
1442
1442
|
await currentGateway.sendControl(networkAddr, attrType, param);
|
|
1443
1443
|
node.log(`[HA->Symi] ${mapping.haEntityId} -> ${mapping.symiName}(${subType}): ${syncData.type}=${JSON.stringify(syncData.value)}`);
|
|
1444
1444
|
} else {
|
|
1445
|
-
node.
|
|
1445
|
+
node.log(`[HA->Symi] 三合一控制未匹配: subType=${subType}, syncData.type=${syncData.type}`);
|
|
1446
1446
|
}
|
|
1447
1447
|
|
|
1448
1448
|
} catch (err) {
|
|
1449
|
-
node.
|
|
1449
|
+
node.log(`[HA->Symi] 三合一控制失败: ${err.message}`);
|
|
1450
1450
|
}
|
|
1451
1451
|
};
|
|
1452
1452
|
|
package/nodes/symi-knx-bridge.js
CHANGED
|
@@ -137,7 +137,7 @@ module.exports = function(RED) {
|
|
|
137
137
|
});
|
|
138
138
|
} catch (e) {
|
|
139
139
|
node.mappings = [];
|
|
140
|
-
node.
|
|
140
|
+
node.log(`映射配置解析失败: ${e.message}`);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
if (!node.gateway) {
|
|
@@ -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({
|
|
@@ -259,13 +260,14 @@ module.exports = function(RED) {
|
|
|
259
260
|
// syncLock会导致队列处理期间丢失事件,改用per-device时间戳防回环
|
|
260
261
|
if (node.initializing) return;
|
|
261
262
|
|
|
262
|
-
const mac = eventData.device.macAddress;
|
|
263
|
+
const mac = (eventData.device.macAddress || '').toLowerCase();
|
|
263
264
|
const state = eventData.state || {};
|
|
264
265
|
|
|
265
266
|
// 状态缓存比较
|
|
266
267
|
if (!node.stateCache[mac]) node.stateCache[mac] = {};
|
|
267
268
|
const cached = node.stateCache[mac];
|
|
268
269
|
const changed = {};
|
|
270
|
+
|
|
269
271
|
const isFirstState = Object.keys(cached).length === 0;
|
|
270
272
|
|
|
271
273
|
for (const [key, value] of Object.entries(state)) {
|
|
@@ -296,12 +298,17 @@ module.exports = function(RED) {
|
|
|
296
298
|
node.log(`[Mesh事件] MAC=${macNormalized}, 找到${matchedMappings.length}个映射, 变化: ${JSON.stringify(changed)}`);
|
|
297
299
|
|
|
298
300
|
for (const mapping of matchedMappings) {
|
|
299
|
-
const loopKey = `${
|
|
301
|
+
const loopKey = `${mapping.meshMac}_${mapping.meshChannel}`;
|
|
300
302
|
|
|
301
303
|
// 窗帘设备需要单独处理防死循环(动作和位置分开检查)
|
|
302
304
|
if (mapping.deviceType === 'cover') {
|
|
303
305
|
// 窗帘的防死循环在内部单独处理,这里不跳过
|
|
304
|
-
}
|
|
306
|
+
}
|
|
307
|
+
// 开关设备使用带值的精细化key,这里先不检查
|
|
308
|
+
else if (mapping.deviceType === 'switch') {
|
|
309
|
+
// 在具体处理逻辑中检查
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
305
312
|
// 其他设备统一防死循环检查
|
|
306
313
|
if (node.shouldPreventSync('mesh-to-knx', loopKey)) {
|
|
307
314
|
node.log(`[Mesh->KNX] 跳过(防死循环): ${loopKey}`);
|
|
@@ -313,13 +320,22 @@ module.exports = function(RED) {
|
|
|
313
320
|
if (mapping.deviceType === 'switch') {
|
|
314
321
|
const switchKey = `switch_${mapping.meshChannel}`;
|
|
315
322
|
if (changed[switchKey] !== undefined) {
|
|
316
|
-
|
|
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}`);
|
|
317
333
|
node.queueCommand({
|
|
318
334
|
direction: 'mesh-to-knx',
|
|
319
335
|
mapping: mapping,
|
|
320
336
|
type: 'switch',
|
|
321
|
-
value:
|
|
322
|
-
key:
|
|
337
|
+
value: val,
|
|
338
|
+
key: specificLoopKey
|
|
323
339
|
});
|
|
324
340
|
}
|
|
325
341
|
}
|
|
@@ -550,7 +566,7 @@ module.exports = function(RED) {
|
|
|
550
566
|
}
|
|
551
567
|
await node.sleep(50); // 命令间隔50ms
|
|
552
568
|
} catch (err) {
|
|
553
|
-
node.
|
|
569
|
+
node.log(`同步失败: ${err.message}`);
|
|
554
570
|
}
|
|
555
571
|
}
|
|
556
572
|
} finally {
|
|
@@ -759,6 +775,7 @@ module.exports = function(RED) {
|
|
|
759
775
|
// 记录发送到的KNX地址和时间,防止自己发的命令又被处理
|
|
760
776
|
const destAddr = knxMsg.knx.destination;
|
|
761
777
|
node.lastKnxAddrSent[destAddr] = Date.now();
|
|
778
|
+
node.lastKnxValueSent[destAddr] = knxMsg.payload;
|
|
762
779
|
|
|
763
780
|
// knxUltimate输入格式:topic + destination + payload + dpt + event
|
|
764
781
|
// topic: 当setTopicType=str时,knxUltimate使用msg.topic作为目标地址
|
|
@@ -928,7 +945,7 @@ module.exports = function(RED) {
|
|
|
928
945
|
}]);
|
|
929
946
|
|
|
930
947
|
} catch (err) {
|
|
931
|
-
node.
|
|
948
|
+
node.log(`[KNX->Mesh] 发送失败: ${err.message}`);
|
|
932
949
|
}
|
|
933
950
|
|
|
934
951
|
node.status({ fill: 'blue', shape: 'dot', text: `KNX→Mesh ${node.mappings.length}个映射` });
|
|
@@ -941,6 +958,12 @@ module.exports = function(RED) {
|
|
|
941
958
|
return;
|
|
942
959
|
}
|
|
943
960
|
|
|
961
|
+
// 如果是来自我们自己节点的 debug 输出,直接跳过
|
|
962
|
+
if (msg.topic === 'mesh-to-knx' || msg.topic === 'knx-to-mesh') {
|
|
963
|
+
done && done();
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
944
967
|
// 从消息中提取KNX组地址
|
|
945
968
|
const groupAddr = msg.knx?.destination || msg.topic || '';
|
|
946
969
|
const value = msg.payload;
|
|
@@ -951,14 +974,6 @@ module.exports = function(RED) {
|
|
|
951
974
|
return;
|
|
952
975
|
}
|
|
953
976
|
|
|
954
|
-
// 检查是否是我们自己发出去的命令(防止自己发的命令被监听后又处理)
|
|
955
|
-
const lastSentTime = node.lastKnxAddrSent[groupAddr] || 0;
|
|
956
|
-
if (Date.now() - lastSentTime < DEFAULT_TIMEOUT) {
|
|
957
|
-
node.debug(`[KNX输入] 跳过(自己发的): ${groupAddr}`);
|
|
958
|
-
done && done();
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
977
|
// 查找映射
|
|
963
978
|
const mapping = node.findKnxMapping(groupAddr);
|
|
964
979
|
if (!mapping) {
|
|
@@ -966,25 +981,44 @@ module.exports = function(RED) {
|
|
|
966
981
|
done && done();
|
|
967
982
|
return;
|
|
968
983
|
}
|
|
969
|
-
|
|
970
|
-
//
|
|
971
|
-
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
984
|
+
|
|
985
|
+
// 检查是否是我们自己发出去的命令(防止自己发的命令被监听后又处理)
|
|
986
|
+
// 检查该设备的所有关联地址,只要有一个最近发送过且值相同,就认为是回显
|
|
987
|
+
const isEcho = mapping.allKnxAddrs.some(addr => {
|
|
988
|
+
const lastSentTime = node.lastKnxAddrSent[addr] || 0;
|
|
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;
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
if (isEcho) {
|
|
999
|
+
node.debug(`[KNX输入] 跳过(自己发的回显): ${groupAddr} (设备: ${mapping.name}, 值: ${msg.payload})`);
|
|
977
1000
|
done && done();
|
|
978
1001
|
return;
|
|
979
1002
|
}
|
|
980
1003
|
|
|
981
|
-
|
|
1004
|
+
// 确定地址功能(优先使用地址匹配,比DPT更可靠)
|
|
1005
|
+
const addrFunc = node.getKnxAddrFunction(mapping, groupAddr);
|
|
982
1006
|
|
|
983
1007
|
// 根据设备类型和地址功能处理
|
|
984
1008
|
if (mapping.deviceType === 'switch') {
|
|
985
1009
|
// 开关命令(只处理cmd地址)
|
|
986
1010
|
if (addrFunc === 'cmd' || addrFunc === 'status') {
|
|
987
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}`);
|
|
988
1022
|
node.queueCommand({
|
|
989
1023
|
direction: 'knx-to-mesh',
|
|
990
1024
|
mapping: mapping,
|
|
@@ -999,6 +1033,7 @@ module.exports = function(RED) {
|
|
|
999
1033
|
else if (mapping.deviceType === 'cover') {
|
|
1000
1034
|
const deviceKey = (mapping.meshMac || '').toLowerCase().replace(/:/g, '');
|
|
1001
1035
|
const now = Date.now();
|
|
1036
|
+
const loopKey = `${mapping.meshMac}_${mapping.meshChannel}_cover`; // 窗帘使用通用key,因为有专门的controlLock
|
|
1002
1037
|
|
|
1003
1038
|
// KNX主动发起控制,直接抢占锁定(用户操作优先)
|
|
1004
1039
|
node.controlLock[deviceKey] = { controller: 'knx', lockUntil: now + COVER_CONTROL_LOCK_MS };
|
|
@@ -16,7 +16,7 @@ module.exports = function(RED) {
|
|
|
16
16
|
try {
|
|
17
17
|
knxEntities = JSON.parse(config.knxEntities || '[]');
|
|
18
18
|
} catch (e) {
|
|
19
|
-
node.
|
|
19
|
+
node.log('KNX实体配置解析失败: ' + e.message);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
try {
|
|
@@ -52,7 +52,7 @@ module.exports = function(RED) {
|
|
|
52
52
|
}
|
|
53
53
|
} catch (e) {
|
|
54
54
|
node.mappings = [];
|
|
55
|
-
node.
|
|
55
|
+
node.log('映射配置解析失败: ' + e.message);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
node.commandQueue = [];
|
|
@@ -304,7 +304,7 @@ module.exports = function(RED) {
|
|
|
304
304
|
}
|
|
305
305
|
await node.sleep(50);
|
|
306
306
|
} catch (err) {
|
|
307
|
-
node.
|
|
307
|
+
node.log(`同步失败: ${err.message}`);
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
310
|
} finally {
|
|
@@ -377,7 +377,7 @@ module.exports = function(RED) {
|
|
|
377
377
|
}]);
|
|
378
378
|
}
|
|
379
379
|
} catch (err) {
|
|
380
|
-
node.
|
|
380
|
+
node.log(`[KNX->HA] 调用HA服务失败: ${err.message}`);
|
|
381
381
|
}
|
|
382
382
|
};
|
|
383
383
|
|
package/nodes/symi-mqtt-brand.js
CHANGED
|
@@ -102,7 +102,7 @@ module.exports = function(RED) {
|
|
|
102
102
|
const now = Date.now();
|
|
103
103
|
if (now - node._lastErrorLog > 60000) {
|
|
104
104
|
node._lastErrorLog = now;
|
|
105
|
-
node.
|
|
105
|
+
node.log(msg);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -121,7 +121,7 @@ module.exports = function(RED) {
|
|
|
121
121
|
|
|
122
122
|
const uploadTopic = getUploadTopic();
|
|
123
123
|
if (!uploadTopic) {
|
|
124
|
-
node.
|
|
124
|
+
node.log('品牌MQTT配置不完整:缺少项目代码或设备SN');
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -152,7 +152,7 @@ module.exports = function(RED) {
|
|
|
152
152
|
if (!err) {
|
|
153
153
|
node.log(`已订阅: ${uploadTopic}`);
|
|
154
154
|
} else {
|
|
155
|
-
node.
|
|
155
|
+
node.log(`订阅失败: ${err.message}`);
|
|
156
156
|
}
|
|
157
157
|
});
|
|
158
158
|
|
|
@@ -264,7 +264,7 @@ module.exports = function(RED) {
|
|
|
264
264
|
// 发布控制命令
|
|
265
265
|
node.publish = function(st, si, fn, fv) {
|
|
266
266
|
if (!node._client || !node._connected) {
|
|
267
|
-
node.
|
|
267
|
+
node.log('品牌MQTT未连接,无法发送命令');
|
|
268
268
|
return false;
|
|
269
269
|
}
|
|
270
270
|
|
package/nodes/symi-mqtt-sync.js
CHANGED
|
@@ -155,7 +155,7 @@ module.exports = function(RED) {
|
|
|
155
155
|
const now = Date.now();
|
|
156
156
|
if (now - node._lastErrorLog > ERROR_LOG_INTERVAL) {
|
|
157
157
|
node._lastErrorLog = now;
|
|
158
|
-
node.
|
|
158
|
+
node.log(msg);
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
@@ -406,7 +406,7 @@ module.exports = function(RED) {
|
|
|
406
406
|
});
|
|
407
407
|
}
|
|
408
408
|
} catch (e) {
|
|
409
|
-
node.
|
|
409
|
+
node.log(`[Brand→Mesh] 同步失败: ${e.message}`);
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
412
|
|
|
@@ -619,7 +619,7 @@ module.exports = function(RED) {
|
|
|
619
619
|
}
|
|
620
620
|
});
|
|
621
621
|
} catch (e) {
|
|
622
|
-
node.
|
|
622
|
+
node.log(`[Mesh→Brand] 同步失败: ${e.message}`);
|
|
623
623
|
}
|
|
624
624
|
}
|
|
625
625
|
|
package/nodes/symi-mqtt.js
CHANGED
|
@@ -31,7 +31,7 @@ module.exports = function(RED) {
|
|
|
31
31
|
node.gateway = RED.nodes.getNode(config.gateway);
|
|
32
32
|
|
|
33
33
|
if (!node.gateway) {
|
|
34
|
-
node.
|
|
34
|
+
node.log('未配置网关');
|
|
35
35
|
node.status({ fill: 'red', shape: 'ring', text: '未配置网关' });
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
@@ -61,7 +61,7 @@ module.exports = function(RED) {
|
|
|
61
61
|
node.publishAllDiscovery(devices);
|
|
62
62
|
}, 3000);
|
|
63
63
|
} else {
|
|
64
|
-
node.
|
|
64
|
+
node.log('设备列表已完成但MQTT未连接,等待MQTT连接后发布');
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
67
|
node.gateway.on('device-list-complete', node._handlers.deviceListComplete);
|
|
@@ -116,7 +116,7 @@ module.exports = function(RED) {
|
|
|
116
116
|
node.subscriptions.set(topic, deviceMacForSub);
|
|
117
117
|
node.log(`[三合一] 订阅成功: ${topic}`);
|
|
118
118
|
} else {
|
|
119
|
-
node.
|
|
119
|
+
node.log(`[三合一] 订阅失败: ${topic}`);
|
|
120
120
|
}
|
|
121
121
|
});
|
|
122
122
|
}
|
|
@@ -160,7 +160,7 @@ module.exports = function(RED) {
|
|
|
160
160
|
node.subscriptions.set(topic, deviceMacForSub);
|
|
161
161
|
node.debug(`订阅topic: ${topic} -> ${deviceMacForSub}`);
|
|
162
162
|
} else {
|
|
163
|
-
node.
|
|
163
|
+
node.log(`订阅失败: ${topic}, ${err.message}`);
|
|
164
164
|
}
|
|
165
165
|
});
|
|
166
166
|
}
|
|
@@ -229,7 +229,7 @@ module.exports = function(RED) {
|
|
|
229
229
|
if (!isAvailable) {
|
|
230
230
|
// 只在首次或每分钟记录一次警告,避免日志刷屏
|
|
231
231
|
if (!node._lastMqttWarn || Date.now() - node._lastMqttWarn > 60000) {
|
|
232
|
-
node.
|
|
232
|
+
node.log(`MQTT broker ${host}:${port} 不可用,每30秒重试`);
|
|
233
233
|
node._lastMqttWarn = Date.now();
|
|
234
234
|
}
|
|
235
235
|
node.status({ fill: 'yellow', shape: 'ring', text: `Broker不可用 ${host}:${port}` });
|
|
@@ -254,7 +254,7 @@ module.exports = function(RED) {
|
|
|
254
254
|
// 立即绑定错误处理
|
|
255
255
|
node.mqttClient.on('error', (error) => {
|
|
256
256
|
if (error.code !== 'ECONNREFUSED' && error.code !== 'ENOTFOUND') {
|
|
257
|
-
node.
|
|
257
|
+
node.log(`MQTT错误: ${error.message}`);
|
|
258
258
|
}
|
|
259
259
|
node.status({ fill: 'red', shape: 'ring', text: '连接失败' });
|
|
260
260
|
});
|
|
@@ -291,7 +291,7 @@ module.exports = function(RED) {
|
|
|
291
291
|
});
|
|
292
292
|
|
|
293
293
|
} catch (error) {
|
|
294
|
-
node.
|
|
294
|
+
node.log(`MQTT连接失败: ${error.message}`);
|
|
295
295
|
node.status({ fill: 'red', shape: 'ring', text: '失败' });
|
|
296
296
|
}
|
|
297
297
|
};
|
|
@@ -343,7 +343,7 @@ module.exports = function(RED) {
|
|
|
343
343
|
node.log(`[MQTT] 发布 ${device.name} availability: online`);
|
|
344
344
|
node.mqttClient.publish(`symi_mesh/${macClean}/availability`, 'online', { retain: true }, (err) => {
|
|
345
345
|
if (err) {
|
|
346
|
-
node.
|
|
346
|
+
node.log(`发布availability失败: ${err.message}`);
|
|
347
347
|
} else {
|
|
348
348
|
node.log(`[MQTT] ${device.name} availability已发布`);
|
|
349
349
|
}
|
|
@@ -354,7 +354,7 @@ module.exports = function(RED) {
|
|
|
354
354
|
setTimeout(() => {
|
|
355
355
|
node.mqttClient.publish(config.topic, config.payload, { retain: true }, (err) => {
|
|
356
356
|
if (err) {
|
|
357
|
-
node.
|
|
357
|
+
node.log(`发布discovery失败: ${config.topic}, ${err.message}`);
|
|
358
358
|
}
|
|
359
359
|
});
|
|
360
360
|
}, index * 50 + 100);
|
|
@@ -369,7 +369,7 @@ module.exports = function(RED) {
|
|
|
369
369
|
node.subscriptions.set(topic, deviceMacForSub);
|
|
370
370
|
node.log(`[MQTT] 订阅成功: ${topic}`);
|
|
371
371
|
} else {
|
|
372
|
-
node.
|
|
372
|
+
node.log(`[MQTT] 订阅失败: ${topic}, ${err.message}`);
|
|
373
373
|
}
|
|
374
374
|
});
|
|
375
375
|
}
|
|
@@ -389,13 +389,13 @@ module.exports = function(RED) {
|
|
|
389
389
|
const topic = `symi_mesh/room_${roomNo}/scene/+/trigger`;
|
|
390
390
|
|
|
391
391
|
if (!node.mqttClient || !node.mqttClient.connected) {
|
|
392
|
-
node.
|
|
392
|
+
node.log('MQTT客户端未连接,无法订阅场景触发');
|
|
393
393
|
return;
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
node.mqttClient.subscribe(topic, (err) => {
|
|
397
397
|
if (err) {
|
|
398
|
-
node.
|
|
398
|
+
node.log(`订阅场景触发主题失败: ${err.message}`);
|
|
399
399
|
} else {
|
|
400
400
|
node.log(`已订阅场景触发主题: ${topic}`);
|
|
401
401
|
}
|
|
@@ -409,7 +409,7 @@ module.exports = function(RED) {
|
|
|
409
409
|
// 先发布availability: online,确保HA实体可用
|
|
410
410
|
node.mqttClient.publish(`symi_mesh/${macClean}/availability`, 'online', { retain: true }, (err) => {
|
|
411
411
|
if (err) {
|
|
412
|
-
node.
|
|
412
|
+
node.log(`发布availability失败: ${err.message}`);
|
|
413
413
|
}
|
|
414
414
|
});
|
|
415
415
|
|
|
@@ -1049,7 +1049,7 @@ module.exports = function(RED) {
|
|
|
1049
1049
|
node.debug(`[MQTT发布] ${p.topic} = ${p.payload}`);
|
|
1050
1050
|
});
|
|
1051
1051
|
} else if (publishes.length > 0) {
|
|
1052
|
-
node.
|
|
1052
|
+
node.log(`[MQTT发布失败] MQTT未连接,无法发布${publishes.length}条消息`);
|
|
1053
1053
|
}
|
|
1054
1054
|
};
|
|
1055
1055
|
|
|
@@ -1067,7 +1067,7 @@ module.exports = function(RED) {
|
|
|
1067
1067
|
|
|
1068
1068
|
if (!deviceMac) {
|
|
1069
1069
|
// 输出当前所有订阅,帮助调试
|
|
1070
|
-
node.
|
|
1070
|
+
node.log(`[MQTT] 未找到topic订阅: ${topic}`);
|
|
1071
1071
|
node.debug(`[MQTT] 当前订阅列表 (${node.subscriptions.size}个):`);
|
|
1072
1072
|
node.subscriptions.forEach((mac, t) => {
|
|
1073
1073
|
node.debug(` ${t} -> ${mac}`);
|
|
@@ -1077,7 +1077,7 @@ module.exports = function(RED) {
|
|
|
1077
1077
|
|
|
1078
1078
|
const device = node.gateway.getDevice(deviceMac);
|
|
1079
1079
|
if (!device) {
|
|
1080
|
-
node.
|
|
1080
|
+
node.log(`[MQTT] 设备未找到: MAC=${deviceMac}, topic=${topic}`);
|
|
1081
1081
|
return;
|
|
1082
1082
|
}
|
|
1083
1083
|
|
|
@@ -1124,7 +1124,7 @@ module.exports = function(RED) {
|
|
|
1124
1124
|
await node.gateway.sendScene(sceneId);
|
|
1125
1125
|
node.log(`[MQTT场景] 场景${sceneId}(${sceneInfo.name})控制命令已发送`);
|
|
1126
1126
|
} catch(err) {
|
|
1127
|
-
node.
|
|
1127
|
+
node.log(`[MQTT场景] 场景触发失败: ${err.message}`);
|
|
1128
1128
|
}
|
|
1129
1129
|
})();
|
|
1130
1130
|
|
|
@@ -1361,7 +1361,7 @@ module.exports = function(RED) {
|
|
|
1361
1361
|
isUserControl: true
|
|
1362
1362
|
});
|
|
1363
1363
|
} else {
|
|
1364
|
-
node.
|
|
1364
|
+
node.log(`[MQTT] 无法触发事件: device=${!!device}, gateway=${!!node.gateway}, deviceManager=${!!node.gateway?.deviceManager}`);
|
|
1365
1365
|
}
|
|
1366
1366
|
}
|
|
1367
1367
|
|
package/nodes/symi-rs485-sync.js
CHANGED
|
@@ -366,7 +366,7 @@ module.exports = function(RED) {
|
|
|
366
366
|
const hexStr = cmd.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
|
|
367
367
|
node.log(`[A->B] 已发送到B: ${hexStr}`);
|
|
368
368
|
}).catch(err => {
|
|
369
|
-
node.
|
|
369
|
+
node.log(`[A->B] 发送失败: ${err.message}`);
|
|
370
370
|
});
|
|
371
371
|
}
|
|
372
372
|
}
|
|
@@ -431,7 +431,7 @@ module.exports = function(RED) {
|
|
|
431
431
|
const hexStr = cmd.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
|
|
432
432
|
node.log(`[B->A] 已发送到A: ${hexStr}`);
|
|
433
433
|
}).catch(err => {
|
|
434
|
-
node.
|
|
434
|
+
node.log(`[B->A] 发送失败: ${err.message}`);
|
|
435
435
|
});
|
|
436
436
|
}
|
|
437
437
|
}
|
|
@@ -783,7 +783,7 @@ module.exports = function(RED) {
|
|
|
783
783
|
if (mapping.protocolA === 'zhonghong') {
|
|
784
784
|
const cmd = buildZhonghongQueryCmd(mapping.configA);
|
|
785
785
|
node.rs485ConfigA.send(cmd).catch(err => {
|
|
786
|
-
node.
|
|
786
|
+
node.log(`查询A失败: ${err.message}`);
|
|
787
787
|
});
|
|
788
788
|
}
|
|
789
789
|
}
|