node-red-contrib-symi-mesh 1.8.6 → 1.8.8
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 +21 -0
- package/lib/tcp-client.js +1 -1
- package/nodes/rs485-debug.js +4 -3
- package/nodes/symi-485-bridge.js +25 -25
- package/nodes/symi-485-config.js +17 -6
- package/nodes/symi-cloud-sync.js +12 -12
- package/nodes/symi-device.js +5 -5
- package/nodes/symi-gateway.js +12 -1
- package/nodes/symi-ha-sync.js +12 -12
- package/nodes/symi-knx-bridge.js +25 -13
- 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 +20 -20
- package/nodes/symi-rs485-sync.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -558,6 +558,8 @@ npm install node-red-contrib-home-assistant-websocket
|
|
|
558
558
|
|
|
559
559
|
| 节点 | 用途 |
|
|
560
560
|
|-----|------|
|
|
561
|
+
| **Symi RS485 Sync** | RS485多机批量同步 |
|
|
562
|
+
| **RS485调试** | 原始485字节流抓取显示 |
|
|
561
563
|
| **Symi Gateway** | 网关连接(TCP/串口) |
|
|
562
564
|
| **Symi MQTT** | MQTT桥接,设备发布到HA |
|
|
563
565
|
| **Symi Device** | Flow中单设备控制/监听 |
|
|
@@ -702,6 +704,25 @@ node-red-contrib-symi-mesh/
|
|
|
702
704
|
|
|
703
705
|
## 更新日志
|
|
704
706
|
|
|
707
|
+
### v1.8.8 (2026-01-14)
|
|
708
|
+
|
|
709
|
+
**KNX 双向同步深度修复与协议鲁棒性增强**:
|
|
710
|
+
- **分包与粘包彻底解决**:重构 `ProtocolHandler` 缓存机制,支持 **8+1 分包、多包连续粘包**等极端情况下的完整解析。通过 Buffer 累加与帧头部实时扫描技术,确保无论串口数据如何切分,都能还原为完整的协议帧。
|
|
711
|
+
- **KNX 双向同步死循环修复**:
|
|
712
|
+
- **MAC 地址标准化**:统一所有节点(配置、事件、缓存)的 MAC 地址为小写且无冒号格式,彻底解决由于大小写不一致导致的“找不到映射”或“防死循环误杀”问题。
|
|
713
|
+
- **设备级回显消除**:引入 `allKnxAddrs` 关联检查,KNX 发出指令后,自动屏蔽该设备下所有关联地址(命令/状态/位置)的短时回显,防止自发自收导致的同步环路。
|
|
714
|
+
- **4 键开关通道精准匹配**:修复多路开关状态上报时,`switch_1` 到 `switch_4` 的动态订阅逻辑,确保 Mesh 端的每一路状态都能精准同步到对应的 KNX 组地址。
|
|
715
|
+
- **生产环境日志优化**:所有原始报文降级为 `debug` 级别,仅保留关键的同步逻辑日志为 `log` 级别,确保在长时间运行下不占用额外硬盘 IO,防止 Node-RED 变慢。
|
|
716
|
+
- **三合一面板持久化增强**:完善 `symi-mesh-data` 目录下的 JSON 持久化逻辑,确保三合一设备类型在 Node-RED 重启后能立即恢复,无需重新探测。
|
|
717
|
+
|
|
718
|
+
### v1.8.7 (2026-01-08)
|
|
719
|
+
|
|
720
|
+
**生产环境日志优化与稳定性增强**:
|
|
721
|
+
- **错误日志节流 (Throttling)**:在网关连接和 RS485 配置中引入 60 秒节流机制,同类网络错误(如 `ECONNREFUSED`)每分钟仅记录一次,彻底解决离线时的日志刷屏问题。
|
|
722
|
+
- **日志级别降级**:将所有节点的 `node.error` 和 `node.warn` 统一降级为 `node.log` (Info 级别),保持 Node-RED 控制台整洁,仅在调试模式下显示详细信息。
|
|
723
|
+
- **TCP 客户端优化**:在 `tcp-client` 库级别拦截常见的网络波动报错,提升系统在高频重连场景下的静默稳定性。
|
|
724
|
+
- **全量节点适配**:完成 MQTT、HA同步、云端同步、RS485、KNX 等所有功能节点的日志规范化清理。
|
|
725
|
+
|
|
705
726
|
### v1.8.6 (2026-01-07)
|
|
706
727
|
|
|
707
728
|
**核心修复与同步增强**:
|
package/lib/tcp-client.js
CHANGED
|
@@ -111,7 +111,7 @@ class TCPClient extends EventEmitter {
|
|
|
111
111
|
// 只在首次连接或重要错误时记录,避免大量重复日志
|
|
112
112
|
// ECONNRESET通常是网络波动,不记录错误
|
|
113
113
|
else if (!this.connected && error.code !== 'ECONNREFUSED' && error.code !== 'ECONNRESET') {
|
|
114
|
-
this.logger.
|
|
114
|
+
this.logger.log('TCP client error: ' + error.message);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
// 确保错误不会导致uncaught exception
|
package/nodes/rs485-debug.js
CHANGED
|
@@ -145,12 +145,13 @@ module.exports = function(RED) {
|
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
node.rs485Config.on('disconnected', function() {
|
|
148
|
-
node.
|
|
148
|
+
node.log('RS485连接已断开');
|
|
149
149
|
updateStatus();
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
node.rs485Config.on('error', function(err) {
|
|
153
|
-
|
|
153
|
+
// 降级为 log,避免控制台报错
|
|
154
|
+
node.log('RS485错误: ' + err.message);
|
|
154
155
|
node.status({ fill: 'red', shape: 'ring', text: '错误: ' + err.message });
|
|
155
156
|
});
|
|
156
157
|
|
|
@@ -177,7 +178,7 @@ module.exports = function(RED) {
|
|
|
177
178
|
handleFrame(frame, 'TX');
|
|
178
179
|
node.log('手动发送: ' + formatHex(frame));
|
|
179
180
|
} else if (!node.rs485Config.connected) {
|
|
180
|
-
node.
|
|
181
|
+
node.log('RS485未连接,无法发送');
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
});
|
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
|
@@ -66,6 +66,17 @@ module.exports = function(RED) {
|
|
|
66
66
|
node.receiveBuffer = Buffer.alloc(0);
|
|
67
67
|
node.users = [];
|
|
68
68
|
|
|
69
|
+
// 限流错误日志
|
|
70
|
+
node._lastErrorLog = 0;
|
|
71
|
+
node._ERROR_LOG_INTERVAL = 60000;
|
|
72
|
+
node.logErrorThrottled = function(msg) {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
if (now - node._lastErrorLog > node._ERROR_LOG_INTERVAL) {
|
|
75
|
+
node._lastErrorLog = now;
|
|
76
|
+
node.log(msg);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
69
80
|
// 注册使用者
|
|
70
81
|
node.register = function(userNode) {
|
|
71
82
|
if (!node.users.includes(userNode)) {
|
|
@@ -94,7 +105,7 @@ module.exports = function(RED) {
|
|
|
94
105
|
try {
|
|
95
106
|
if (node.connectionType === 'serial') {
|
|
96
107
|
if (!node.serialPort) {
|
|
97
|
-
node.
|
|
108
|
+
node.log('未配置串口');
|
|
98
109
|
return;
|
|
99
110
|
}
|
|
100
111
|
|
|
@@ -122,7 +133,7 @@ module.exports = function(RED) {
|
|
|
122
133
|
});
|
|
123
134
|
|
|
124
135
|
node.client.on('error', (err) => {
|
|
125
|
-
node.
|
|
136
|
+
node.logErrorThrottled(`RS485串口错误: ${err.message}`);
|
|
126
137
|
node.emit('error', err);
|
|
127
138
|
});
|
|
128
139
|
|
|
@@ -146,7 +157,7 @@ module.exports = function(RED) {
|
|
|
146
157
|
|
|
147
158
|
} else if (node.connectionType === 'tcp') {
|
|
148
159
|
if (!node.host) {
|
|
149
|
-
node.
|
|
160
|
+
node.log('未配置TCP主机');
|
|
150
161
|
return;
|
|
151
162
|
}
|
|
152
163
|
|
|
@@ -170,7 +181,7 @@ module.exports = function(RED) {
|
|
|
170
181
|
if (err.name === 'AggregateError' || err.errors) {
|
|
171
182
|
node.debug(`RS485 TCP连接失败: 无法连接到 ${node.host}:${node.port}`);
|
|
172
183
|
} else {
|
|
173
|
-
node.
|
|
184
|
+
node.logErrorThrottled(`RS485 TCP错误: ${err.message}`);
|
|
174
185
|
}
|
|
175
186
|
node.emit('error', err);
|
|
176
187
|
});
|
|
@@ -199,11 +210,11 @@ module.exports = function(RED) {
|
|
|
199
210
|
family: 4 // 强制IPv4,避免IPv6连接失败导致AggregateError
|
|
200
211
|
});
|
|
201
212
|
} catch (connectErr) {
|
|
202
|
-
node.
|
|
213
|
+
node.log(`RS485 TCP连接异常: ${connectErr.message}`);
|
|
203
214
|
}
|
|
204
215
|
}
|
|
205
216
|
} catch (err) {
|
|
206
|
-
node.
|
|
217
|
+
node.log(`RS485连接失败: ${err.message}`);
|
|
207
218
|
}
|
|
208
219
|
};
|
|
209
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
|
}
|
|
@@ -332,7 +332,7 @@ module.exports = function(RED) {
|
|
|
332
332
|
msg.payload = { scene_id: scene.scene_id };
|
|
333
333
|
node.send(msg);
|
|
334
334
|
} else {
|
|
335
|
-
node.
|
|
335
|
+
node.log(`场景ID ${msg.payload.scene_id} 不存在`);
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
});
|
package/nodes/symi-device.js
CHANGED
|
@@ -31,7 +31,7 @@ module.exports = function(RED) {
|
|
|
31
31
|
}
|
|
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
|
}
|
|
@@ -99,21 +99,21 @@ module.exports = function(RED) {
|
|
|
99
99
|
node.on('input', async function(msg) {
|
|
100
100
|
try {
|
|
101
101
|
if (!node.gateway.connected) {
|
|
102
|
-
node.
|
|
102
|
+
node.log('网关未连接');
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
if (!node.device) {
|
|
107
107
|
updateDevice();
|
|
108
108
|
if (!node.device) {
|
|
109
|
-
node.
|
|
109
|
+
node.log('设备未找到');
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
const command = node.parseInputCommand(msg);
|
|
115
115
|
if (!command) {
|
|
116
|
-
node.
|
|
116
|
+
node.log('无效的命令格式');
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -122,7 +122,7 @@ module.exports = function(RED) {
|
|
|
122
122
|
node.status({ fill: 'green', shape: 'dot', text: '命令已发送' });
|
|
123
123
|
|
|
124
124
|
} catch (error) {
|
|
125
|
-
node.
|
|
125
|
+
node.log(`控制失败: ${error.message}`);
|
|
126
126
|
node.status({ fill: 'red', shape: 'dot', text: '控制失败' });
|
|
127
127
|
}
|
|
128
128
|
});
|
package/nodes/symi-gateway.js
CHANGED
|
@@ -70,6 +70,17 @@ module.exports = function(RED) {
|
|
|
70
70
|
this.sceneExecutionInProgress = false; // 场景执行中标志
|
|
71
71
|
this.sceneExecutionTimer = null; // 场景执行超时定时器
|
|
72
72
|
|
|
73
|
+
// 限流错误日志
|
|
74
|
+
this._lastErrorLog = 0;
|
|
75
|
+
this._ERROR_LOG_INTERVAL = 60000;
|
|
76
|
+
this.logErrorThrottled = function(msg) {
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
if (now - this._lastErrorLog > this._ERROR_LOG_INTERVAL) {
|
|
79
|
+
this._lastErrorLog = now;
|
|
80
|
+
this.log(msg);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
73
84
|
this.log(`Initializing Symi Gateway: ${this.connectionType === 'tcp' ? `${this.host}:${this.port}` : this.serialPort}`);
|
|
74
85
|
|
|
75
86
|
// 三合一设备检测已在 queryAllDeviceStates 中实现,无需额外事件监听
|
|
@@ -128,7 +139,7 @@ module.exports = function(RED) {
|
|
|
128
139
|
|
|
129
140
|
} catch (error) {
|
|
130
141
|
// 初始连接失败,但自动重连会继续尝试
|
|
131
|
-
this.
|
|
142
|
+
this.logErrorThrottled(`Initial connection failed: ${error.message}, will retry automatically`);
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
// 节点关闭时清理资源
|
package/nodes/symi-ha-sync.js
CHANGED
|
@@ -117,7 +117,7 @@ module.exports = function(RED) {
|
|
|
117
117
|
}
|
|
118
118
|
} catch (e) {
|
|
119
119
|
node.mappings = [];
|
|
120
|
-
node.
|
|
120
|
+
node.log('映射配置解析失败: ' + e.message);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
node.commandQueue = [];
|
|
@@ -162,7 +162,7 @@ module.exports = function(RED) {
|
|
|
162
162
|
|
|
163
163
|
if (configError) {
|
|
164
164
|
node.status({ fill: 'red', shape: 'ring', text: configError });
|
|
165
|
-
node.
|
|
165
|
+
node.log(`[HA同步] ${configError}`);
|
|
166
166
|
// 不要return,继续注册input监听器
|
|
167
167
|
} else {
|
|
168
168
|
// 初始状态:只有Mesh→HA方向,等待HA输入连接
|
|
@@ -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) {
|
|
@@ -259,13 +259,14 @@ module.exports = function(RED) {
|
|
|
259
259
|
// syncLock会导致队列处理期间丢失事件,改用per-device时间戳防回环
|
|
260
260
|
if (node.initializing) return;
|
|
261
261
|
|
|
262
|
-
const mac = eventData.device.macAddress;
|
|
262
|
+
const mac = (eventData.device.macAddress || '').toLowerCase();
|
|
263
263
|
const state = eventData.state || {};
|
|
264
264
|
|
|
265
265
|
// 状态缓存比较
|
|
266
266
|
if (!node.stateCache[mac]) node.stateCache[mac] = {};
|
|
267
267
|
const cached = node.stateCache[mac];
|
|
268
268
|
const changed = {};
|
|
269
|
+
|
|
269
270
|
const isFirstState = Object.keys(cached).length === 0;
|
|
270
271
|
|
|
271
272
|
for (const [key, value] of Object.entries(state)) {
|
|
@@ -296,7 +297,7 @@ module.exports = function(RED) {
|
|
|
296
297
|
node.log(`[Mesh事件] MAC=${macNormalized}, 找到${matchedMappings.length}个映射, 变化: ${JSON.stringify(changed)}`);
|
|
297
298
|
|
|
298
299
|
for (const mapping of matchedMappings) {
|
|
299
|
-
const loopKey = `${
|
|
300
|
+
const loopKey = `${mapping.meshMac}_${mapping.meshChannel}`;
|
|
300
301
|
|
|
301
302
|
// 窗帘设备需要单独处理防死循环(动作和位置分开检查)
|
|
302
303
|
if (mapping.deviceType === 'cover') {
|
|
@@ -550,7 +551,7 @@ module.exports = function(RED) {
|
|
|
550
551
|
}
|
|
551
552
|
await node.sleep(50); // 命令间隔50ms
|
|
552
553
|
} catch (err) {
|
|
553
|
-
node.
|
|
554
|
+
node.log(`同步失败: ${err.message}`);
|
|
554
555
|
}
|
|
555
556
|
}
|
|
556
557
|
} finally {
|
|
@@ -928,7 +929,7 @@ module.exports = function(RED) {
|
|
|
928
929
|
}]);
|
|
929
930
|
|
|
930
931
|
} catch (err) {
|
|
931
|
-
node.
|
|
932
|
+
node.log(`[KNX->Mesh] 发送失败: ${err.message}`);
|
|
932
933
|
}
|
|
933
934
|
|
|
934
935
|
node.status({ fill: 'blue', shape: 'dot', text: `KNX→Mesh ${node.mappings.length}个映射` });
|
|
@@ -941,6 +942,12 @@ module.exports = function(RED) {
|
|
|
941
942
|
return;
|
|
942
943
|
}
|
|
943
944
|
|
|
945
|
+
// 如果是来自我们自己节点的 debug 输出,直接跳过
|
|
946
|
+
if (msg.topic === 'mesh-to-knx' || msg.topic === 'knx-to-mesh') {
|
|
947
|
+
done && done();
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
|
|
944
951
|
// 从消息中提取KNX组地址
|
|
945
952
|
const groupAddr = msg.knx?.destination || msg.topic || '';
|
|
946
953
|
const value = msg.payload;
|
|
@@ -951,14 +958,6 @@ module.exports = function(RED) {
|
|
|
951
958
|
return;
|
|
952
959
|
}
|
|
953
960
|
|
|
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
961
|
// 查找映射
|
|
963
962
|
const mapping = node.findKnxMapping(groupAddr);
|
|
964
963
|
if (!mapping) {
|
|
@@ -966,6 +965,19 @@ module.exports = function(RED) {
|
|
|
966
965
|
done && done();
|
|
967
966
|
return;
|
|
968
967
|
}
|
|
968
|
+
|
|
969
|
+
// 检查是否是我们自己发出去的命令(防止自己发的命令被监听后又处理)
|
|
970
|
+
// 检查该设备的所有关联地址,只要有一个最近发送过,就认为是回显
|
|
971
|
+
const wasSentRecently = mapping.allKnxAddrs.some(addr => {
|
|
972
|
+
const lastSentTime = node.lastKnxAddrSent[addr] || 0;
|
|
973
|
+
return (Date.now() - lastSentTime) < DEFAULT_TIMEOUT;
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
if (wasSentRecently) {
|
|
977
|
+
node.debug(`[KNX输入] 跳过(自己发的回显): ${groupAddr} (设备: ${mapping.name})`);
|
|
978
|
+
done && done();
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
969
981
|
|
|
970
982
|
// 确定地址功能(优先使用地址匹配,比DPT更可靠)
|
|
971
983
|
const addrFunc = node.getKnxAddrFunction(mapping, groupAddr);
|
|
@@ -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
|
|
|
@@ -1145,12 +1145,12 @@ module.exports = function(RED) {
|
|
|
1145
1145
|
node.publishCommandFeedback(device, command, payload, topic);
|
|
1146
1146
|
|
|
1147
1147
|
} catch(err) {
|
|
1148
|
-
node.
|
|
1148
|
+
node.log(`[MQTT发送] 失败: ${err.message}`);
|
|
1149
1149
|
}
|
|
1150
1150
|
}
|
|
1151
1151
|
})();
|
|
1152
1152
|
} else {
|
|
1153
|
-
node.
|
|
1153
|
+
node.log(`[MQTT解析] 无法解析命令 - topic: ${topic}, payload: ${payload}`);
|
|
1154
1154
|
}
|
|
1155
1155
|
};
|
|
1156
1156
|
|
|
@@ -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
|
}
|