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 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
  **生产环境日志优化与稳定性增强**:
@@ -178,7 +178,7 @@ module.exports = function(RED) {
178
178
  handleFrame(frame, 'TX');
179
179
  node.log('手动发送: ' + formatHex(frame));
180
180
  } else if (!node.rs485Config.connected) {
181
- node.warn('RS485未连接,无法发送');
181
+ node.log('RS485未连接,无法发送');
182
182
  }
183
183
  }
184
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;
@@ -105,7 +105,7 @@ module.exports = function(RED) {
105
105
  try {
106
106
  if (node.connectionType === 'serial') {
107
107
  if (!node.serialPort) {
108
- node.error('未配置串口');
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.error('未配置TCP主机');
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.error(`RS485 TCP连接异常: ${connectErr.message}`);
213
+ node.log(`RS485 TCP连接异常: ${connectErr.message}`);
214
214
  }
215
215
  }
216
216
  } catch (err) {
217
- node.error(`RS485连接失败: ${err.message}`);
217
+ node.log(`RS485连接失败: ${err.message}`);
218
218
  }
219
219
  };
220
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
  }
@@ -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
  }
@@ -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) {
@@ -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 = `${mac}_${mapping.meshChannel}`;
301
+ const loopKey = `${mapping.meshMac}_${mapping.meshChannel}`;
300
302
 
301
303
  // 窗帘设备需要单独处理防死循环(动作和位置分开检查)
302
304
  if (mapping.deviceType === 'cover') {
303
305
  // 窗帘的防死循环在内部单独处理,这里不跳过
304
- } else {
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
- node.log(`[Mesh->KNX] 开关变化: ${mapping.name || eventData.device.name} CH${mapping.meshChannel} = ${changed[switchKey]}`);
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: changed[switchKey],
322
- key: loopKey
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.error(`同步失败: ${err.message}`);
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.error(`[KNX->Mesh] 发送失败: ${err.message}`);
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
- // 确定地址功能(优先使用地址匹配,比DPT更可靠)
971
- const addrFunc = node.getKnxAddrFunction(mapping, groupAddr);
972
- const loopKey = `${mapping.meshMac}_${mapping.meshChannel}`;
973
-
974
- // 防死循环检查(基于设备的双向同步防护)
975
- if (node.shouldPreventSync('knx-to-mesh', loopKey)) {
976
- node.debug(`[KNX->Mesh] 跳过同步(防死循环): ${loopKey}, addr=${groupAddr}`);
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
- node.log(`[KNX输入] 收到: ${groupAddr} = ${value}, DPT=${dpt}, 功能=${addrFunc}, 设备=${mapping.deviceType}`);
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.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
 
@@ -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.7",
3
+ "version": "1.8.9",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "nodes/symi-gateway.js",
6
6
  "scripts": {