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 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.error('TCP client error:', error.message);
114
+ this.logger.log('TCP client error: ' + error.message);
115
115
  }
116
116
 
117
117
  // 确保错误不会导致uncaught exception
@@ -145,12 +145,13 @@ module.exports = function(RED) {
145
145
  });
146
146
 
147
147
  node.rs485Config.on('disconnected', function() {
148
- node.warn('RS485连接已断开');
148
+ node.log('RS485连接已断开');
149
149
  updateStatus();
150
150
  });
151
151
 
152
152
  node.rs485Config.on('error', function(err) {
153
- node.error('RS485错误: ' + err.message);
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.warn('RS485未连接,无法发送');
181
+ node.log('RS485未连接,无法发送');
181
182
  }
182
183
  }
183
184
  });
@@ -954,7 +954,7 @@ module.exports = function(RED) {
954
954
  });
955
955
  } catch (e) {
956
956
  node.mappings = [];
957
- node.error(`映射配置解析失败: ${e.message}`);
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.warn(`[RS485 Bridge] 已断开 ${rs485Info}`);
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.error(`[RS485 Bridge] 连接错误 ${rs485Info}: ${err.message}`);
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.error(`[Mesh->杜亚] 发送失败: ${err.message}`);
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.error(`[Mesh->杜亚] 位置同步失败: ${err.message}`);
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.error(`[Mesh->自定义窗帘] 发送失败: ${err.message}`);
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.error(`[Mesh控制->杜亚] 发送失败: ${err.message}`);
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.error(`[Mesh控制->自定义] 发送失败: ${err.message}`);
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.warn(`[RS485 Bridge] 命令队列已满(${MAX_QUEUE_SIZE}),丢弃最旧命令`);
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.error(`同步失败: ${err.message}`);
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.warn(`[Mesh->自定义] 开关(${foundKey}): ${switchValue ? '开' : '关'}, 缺少${switchValue ? 'sendOn' : 'sendOff'}码`);
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.warn(`[Mesh->自定义] 空调 ${key}=${value}, 缺少${codeType}码`);
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.error(`RS485写入失败: ${meshKey}=${value}, ${err.message}`);
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.warn(`无效的十六进制码: ${hexCode}`);
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.warn(`[RS485->Mesh] 未找到Mesh设备: ${meshMac} (规范化: ${macNormalized})`);
2162
+ node.log(`[RS485->Mesh] 未找到Mesh设备: ${meshMac} (规范化: ${macNormalized})`);
2163
2163
  // 输出可用设备列表帮助调试
2164
2164
  const allDevices = node.gateway.deviceManager?.getAllDevices() || [];
2165
- node.warn(`[RS485->Mesh] 可用设备: ${allDevices.map(d => d.macAddress).join(', ')}`);
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.error(`[杜亚->Mesh] 写入失败: ${err.message}`);
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.error(`Mesh写入失败: ${key}=${value}, ${err.message}`);
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.error(`Mesh写入失败: ${key}=${value}, ${err.message}`);
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.warn('RS485未连接,无法发送数据');
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.error(`RS485发送失败: ${err.message}`);
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.warn(`[杜亚] 未找到匹配的映射, 帧地址=${duyaData.addrHigh}:${duyaData.addrLow},请检查RS485桥配置`);
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.warn(`[SYMI->Zhonghong] 未知操作码: ${symiData.opCode}`);
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.error(`[SYMI->Zhonghong] 发送失败: ${err.message}`);
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.error(`[Zhonghong->SYMI] 发送失败: ${err.message}`);
2684
+ node.log(`[Zhonghong->SYMI] 发送失败: ${err.message}`);
2685
2685
  });
2686
2686
 
2687
2687
  return;
@@ -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.error('未配置串口');
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.error(`RS485串口错误: ${err.message}`);
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.error('未配置TCP主机');
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.error(`RS485 TCP错误: ${err.message}`);
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.error(`RS485 TCP连接异常: ${connectErr.message}`);
213
+ node.log(`RS485 TCP连接异常: ${connectErr.message}`);
203
214
  }
204
215
  }
205
216
  } catch (err) {
206
- node.error(`RS485连接失败: ${err.message}`);
217
+ node.log(`RS485连接失败: ${err.message}`);
207
218
  }
208
219
  };
209
220
 
@@ -23,13 +23,13 @@ module.exports = function(RED) {
23
23
  node.selectedScenes = config.selectedScenes || [];
24
24
 
25
25
  if (!node.gateway) {
26
- node.error('未配置网关');
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.warn('未配置云端认证信息');
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.warn('同步正在进行中,跳过');
58
+ node.log('同步正在进行中,跳过');
59
59
  return;
60
60
  }
61
61
 
62
62
  if (!node.hotelId || !node.roomNo) {
63
- node.warn('未配置酒店ID或房间号,跳过同步');
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.error(`云端同步失败: ${error.message}`);
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.warn('无可用的云端数据');
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.warn('MQTT客户端未连接,无法发布场景按钮');
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.error(`发布场景按钮失败: ${scene.scene_name}, ${err.message}`);
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.error(`[场景控制] 场景控制命令发送失败: ${err.message}`);
310
+ node.log(`[场景控制] 场景控制命令发送失败: ${err.message}`);
311
311
  });
312
312
  } else {
313
- node.error('[场景控制] 网关未连接,无法执行场景');
313
+ node.log('[场景控制] 网关未连接,无法执行场景');
314
314
  }
315
315
  } else {
316
- node.warn(`[场景控制] 场景ID ${sceneId} 未找到`);
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.warn(`场景ID ${msg.payload.scene_id} 不存在`);
335
+ node.log(`场景ID ${msg.payload.scene_id} 不存在`);
336
336
  }
337
337
  }
338
338
  });
@@ -31,7 +31,7 @@ module.exports = function(RED) {
31
31
  }
32
32
 
33
33
  if (!node.gateway) {
34
- node.error('未配置网关');
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.error('网关未连接');
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.error('设备未找到');
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.warn('无效的命令格式');
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.error(`控制失败: ${error.message}`);
125
+ node.log(`控制失败: ${error.message}`);
126
126
  node.status({ fill: 'red', shape: 'dot', text: '控制失败' });
127
127
  }
128
128
  });
@@ -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.error(`Initial connection failed: ${error.message}, will retry automatically`);
142
+ this.logErrorThrottled(`Initial connection failed: ${error.message}, will retry automatically`);
132
143
  }
133
144
 
134
145
  // 节点关闭时清理资源
@@ -117,7 +117,7 @@ module.exports = function(RED) {
117
117
  }
118
118
  } catch (e) {
119
119
  node.mappings = [];
120
- node.error('映射配置解析失败: ' + e.message);
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.warn(`[HA同步] ${configError}`);
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.warn(`[HA->Symi] 未知的空调风速值: "${attrs.fan_mode}",请检查映射配置`);
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.error(`同步失败: ${err.message}`);
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.warn(`[Symi->HA] 属性 ${syncData.type} 在领域 ${domain} 下暂不支持同步`);
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.error(`[Symi->HA] 调用失败: ${err.message}`);
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.warn(`[HA->Symi] 网关未就绪,无法控制设备`);
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.warn(`[HA->Symi] 设备未找到: ${mapping.symiMac}`);
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.error(`[HA->Symi] 控制失败: ${err.message}`);
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.warn(`[HA->Symi] 网关未就绪,无法控制三合一设备`);
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.warn(`[HA->Symi] 三合一控制未匹配: subType=${subType}, syncData.type=${syncData.type}`);
1445
+ node.log(`[HA->Symi] 三合一控制未匹配: subType=${subType}, syncData.type=${syncData.type}`);
1446
1446
  }
1447
1447
 
1448
1448
  } catch (err) {
1449
- node.error(`[HA->Symi] 三合一控制失败: ${err.message}`);
1449
+ node.log(`[HA->Symi] 三合一控制失败: ${err.message}`);
1450
1450
  }
1451
1451
  };
1452
1452
 
@@ -137,7 +137,7 @@ module.exports = function(RED) {
137
137
  });
138
138
  } catch (e) {
139
139
  node.mappings = [];
140
- node.error(`映射配置解析失败: ${e.message}`);
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 = `${mac}_${mapping.meshChannel}`;
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.error(`同步失败: ${err.message}`);
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.error(`[KNX->Mesh] 发送失败: ${err.message}`);
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.error('KNX实体配置解析失败: ' + e.message);
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.error('映射配置解析失败: ' + e.message);
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.error(`同步失败: ${err.message}`);
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.error(`[KNX->HA] 调用HA服务失败: ${err.message}`);
380
+ node.log(`[KNX->HA] 调用HA服务失败: ${err.message}`);
381
381
  }
382
382
  };
383
383
 
@@ -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.warn(msg);
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.warn('品牌MQTT配置不完整:缺少项目代码或设备SN');
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.error(`订阅失败: ${err.message}`);
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.warn('品牌MQTT未连接,无法发送命令');
267
+ node.log('品牌MQTT未连接,无法发送命令');
268
268
  return false;
269
269
  }
270
270
 
@@ -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.warn(msg);
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.warn(`[Brand→Mesh] 同步失败: ${e.message}`);
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.warn(`[Mesh→Brand] 同步失败: ${e.message}`);
622
+ node.log(`[Mesh→Brand] 同步失败: ${e.message}`);
623
623
  }
624
624
  }
625
625
 
@@ -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.error('未配置网关');
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.warn('设备列表已完成但MQTT未连接,等待MQTT连接后发布');
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.error(`[三合一] 订阅失败: ${topic}`);
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.error(`订阅失败: ${topic}, ${err.message}`);
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.warn(`MQTT broker ${host}:${port} 不可用,每30秒重试`);
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.error(`MQTT错误: ${error.message}`);
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.error(`MQTT连接失败: ${error.message}`);
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.error(`发布availability失败: ${err.message}`);
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.error(`发布discovery失败: ${config.topic}, ${err.message}`);
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.error(`[MQTT] 订阅失败: ${topic}, ${err.message}`);
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.warn('MQTT客户端未连接,无法订阅场景触发');
392
+ node.log('MQTT客户端未连接,无法订阅场景触发');
393
393
  return;
394
394
  }
395
395
 
396
396
  node.mqttClient.subscribe(topic, (err) => {
397
397
  if (err) {
398
- node.error(`订阅场景触发主题失败: ${err.message}`);
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.error(`发布availability失败: ${err.message}`);
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.warn(`[MQTT发布失败] MQTT未连接,无法发布${publishes.length}条消息`);
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.warn(`[MQTT] 未找到topic订阅: ${topic}`);
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.warn(`[MQTT] 设备未找到: MAC=${deviceMac}, topic=${topic}`);
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.error(`[MQTT场景] 场景触发失败: ${err.message}`);
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.error(`[MQTT发送] 失败: ${err.message}`);
1148
+ node.log(`[MQTT发送] 失败: ${err.message}`);
1149
1149
  }
1150
1150
  }
1151
1151
  })();
1152
1152
  } else {
1153
- node.warn(`[MQTT解析] 无法解析命令 - topic: ${topic}, payload: ${payload}`);
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.warn(`[MQTT] 无法触发事件: device=${!!device}, gateway=${!!node.gateway}, deviceManager=${!!node.gateway?.deviceManager}`);
1364
+ node.log(`[MQTT] 无法触发事件: device=${!!device}, gateway=${!!node.gateway}, deviceManager=${!!node.gateway?.deviceManager}`);
1365
1365
  }
1366
1366
  }
1367
1367
 
@@ -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.error(`[A->B] 发送失败: ${err.message}`);
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.error(`[B->A] 发送失败: ${err.message}`);
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.error(`查询A失败: ${err.message}`);
786
+ node.log(`查询A失败: ${err.message}`);
787
787
  });
788
788
  }
789
789
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.8.6",
3
+ "version": "1.8.8",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "nodes/symi-gateway.js",
6
6
  "scripts": {