node-red-contrib-symi-mesh 1.6.7 → 1.6.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
@@ -1036,6 +1036,18 @@ node-red-contrib-symi-mesh/
1036
1036
  - 多设备类型:开关、调光灯、窗帘、空调、新风、地暖
1037
1037
  - 智能通道选择:根据Mesh设备实际路数显示可选通道
1038
1038
 
1039
+ ### v1.6.8 (2025-12-15)
1040
+ - **KNX双向同步修复**:彻底修复米家/面板操作KNX不同步的问题
1041
+ - 修复0x45消息类型处理:米家/面板操作发送msgType=0x45,不再限制channels>=6
1042
+ - 支持1-4路开关2字节参数解析(米家/面板场景触发格式)
1043
+ - 场景执行通知(0x11)后自动查询设备状态
1044
+ - **syncLock阻塞修复**:移除handleMeshStateChange中syncLock检查
1045
+ - 避免队列处理期间丢失状态变化事件
1046
+ - 改用per-device时间戳防回环机制
1047
+ - **processQueue健壮性**:添加try/finally确保processing标志正确重置
1048
+ - **防死循环参数调整**:LOOP_PREVENTION_MS调整为800ms
1049
+ - **同时修复RS485桥接**:应用相同syncLock修复
1050
+
1039
1051
  ### v1.6.6 (2025-12-09)
1040
1052
  - **三合一面板完整双向同步**:支持空调+新风+地暖独立RS485映射
1041
1053
  - 三合一0x94协议完整解析:空调(开关/模式/风速/温度)、地暖(开关/温度)、新风(开关/风速)
@@ -1092,8 +1104,8 @@ Copyright (c) 2025 SYMI 亖米
1092
1104
  ## 关于
1093
1105
 
1094
1106
  **作者**: SYMI 亖米
1095
- **版本**: 1.6.7
1107
+ **版本**: 1.6.8
1096
1108
  **协议**: 蓝牙MESH网关(初级版)串口协议V1.0
1097
- **最后更新**: 2025-12-10
1109
+ **最后更新**: 2025-12-15
1098
1110
  **仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
1099
1111
  **npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
@@ -85,7 +85,9 @@ class DeviceInfo {
85
85
  case 0x45:
86
86
  // 6-8路开关状态上报(2字节,小端序)
87
87
  // 用于场景执行后的状态同步
88
- if (this.channels >= 6) {
88
+ // 注意:米家/面板操作会发送0x45类型,不管实际路数
89
+ // 窗帘(type=5)不使用0x45
90
+ if (this.deviceType !== 5) {
89
91
  this.handleSwitchState(parameters);
90
92
  }
91
93
  break;
@@ -385,8 +387,14 @@ class DeviceInfo {
385
387
  const value = parameters[0];
386
388
  this.state.switch = value === 0x02;
387
389
  } else if (this.channels <= 4) {
388
- // 1-4路开关:1字节,每2位表示1路
389
- const value = parameters[0];
390
+ // 1-4路开关:通常1字节,但米家/面板操作可能发送2字节(0x45类型)
391
+ let value;
392
+ if (parameters.length >= 2) {
393
+ // 2字节:小端序(米家/面板场景触发)
394
+ value = parameters[0] | (parameters[1] << 8);
395
+ } else {
396
+ value = parameters[0];
397
+ }
390
398
  // 保存原始状态值供控制时使用
391
399
  this.state.switchState = value;
392
400
  for (let i = 0; i < this.channels; i++) {
@@ -530,7 +530,7 @@ module.exports = function(RED) {
530
530
 
531
531
  // Mesh设备状态变化处理(事件驱动)
532
532
  const handleMeshStateChange = (eventData) => {
533
- if (node.syncLock) return;
533
+ // 只检查initializing,不检查syncLock以避免丢失事件
534
534
  if (node.initializing) return;
535
535
 
536
536
  const mac = eventData.device.macAddress;
@@ -972,7 +972,7 @@ module.exports = function(RED) {
972
972
 
973
973
  // RS485 device state change handler (event-driven)
974
974
  const handleModbusStateChange = (data) => {
975
- if (node.syncLock) return;
975
+ // 不检查syncLock以避免丢失事件,使用时间戳防回环
976
976
 
977
977
  const mapping = node.findRS485Mapping(data.device.modbusAddress);
978
978
  if (!mapping) return;
@@ -1025,36 +1025,36 @@ module.exports = function(RED) {
1025
1025
  if (node.processing || node.commandQueue.length === 0) return;
1026
1026
 
1027
1027
  node.processing = true;
1028
- node.syncLock = true;
1029
1028
  let multiChange = node.commandQueue.length > 1;
1030
1029
 
1031
- while (node.commandQueue.length > 0) {
1032
- const cmd = node.commandQueue.shift();
1033
- try {
1034
- if (cmd.direction === 'mesh-to-modbus') {
1035
- await node.syncMeshToModbus(cmd);
1036
- } else if (cmd.direction === 'modbus-to-mesh') {
1037
- await node.syncModbusToMesh(cmd);
1030
+ try {
1031
+ while (node.commandQueue.length > 0) {
1032
+ const cmd = node.commandQueue.shift();
1033
+ try {
1034
+ if (cmd.direction === 'mesh-to-modbus') {
1035
+ await node.syncMeshToModbus(cmd);
1036
+ } else if (cmd.direction === 'modbus-to-mesh') {
1037
+ await node.syncModbusToMesh(cmd);
1038
+ }
1039
+ // 命令之间延迟50ms
1040
+ await node.sleep(50);
1041
+ } catch (err) {
1042
+ node.error(`同步失败: ${err.message}`);
1038
1043
  }
1039
- // 命令之间延迟50ms
1040
- await node.sleep(50);
1041
- } catch (err) {
1042
- node.error(`同步失败: ${err.message}`);
1043
1044
  }
1044
- }
1045
1045
 
1046
- // 如果发生多次变化,安排验证
1047
- if (multiChange && !node.pendingVerify) {
1048
- node.pendingVerify = true;
1049
- setTimeout(() => {
1050
- node.verifySync();
1051
- node.pendingVerify = false;
1052
- }, 200);
1046
+ // 如果发生多次变化,安排验证
1047
+ if (multiChange && !node.pendingVerify) {
1048
+ node.pendingVerify = true;
1049
+ setTimeout(() => {
1050
+ node.verifySync();
1051
+ node.pendingVerify = false;
1052
+ }, 200);
1053
+ }
1054
+ } finally {
1055
+ node.processing = false;
1056
+ node.lastSyncTime = Date.now();
1053
1057
  }
1054
-
1055
- node.syncLock = false;
1056
- node.processing = false;
1057
- node.lastSyncTime = Date.now();
1058
1058
  };
1059
1059
 
1060
1060
  // 多实体变化后验证同步状态
@@ -2098,7 +2098,6 @@ module.exports = function(RED) {
2098
2098
  node.curtainCache = {};
2099
2099
  node.lastSentTime = {};
2100
2100
  node.processing = false;
2101
- node.syncLock = false;
2102
2101
 
2103
2102
  node.log('[RS485 Bridge] 节点已清理');
2104
2103
  done();
@@ -217,6 +217,13 @@ module.exports = function(RED) {
217
217
  Buffer.from([frame.checksum])
218
218
  ]).toString('hex').toUpperCase();
219
219
  this.log(`[场景执行] 收到场景执行通知事件: 场景ID=${sceneId}, 设备地址=0x${event.networkAddress.toString(16).toUpperCase()}, 原始帧=${frameHex}`);
220
+
221
+ // 发出场景执行事件,让桥接节点知道需要查询设备状态
222
+ this.emit('scene-executed', {
223
+ sceneId: sceneId,
224
+ triggerAddress: event.networkAddress,
225
+ timestamp: Date.now()
226
+ });
220
227
  continue;
221
228
  }
222
229
 
@@ -3,7 +3,7 @@
3
3
  * 支持开关、窗帘等设备的双向状态同步
4
4
  * 事件驱动架构,命令队列顺序处理,防死循环机制
5
5
  *
6
- * 版本: 1.6.7
6
+ * 版本: 1.6.8
7
7
  */
8
8
 
9
9
  module.exports = function(RED) {
@@ -151,7 +151,7 @@ module.exports = function(RED) {
151
151
  node.lastKnxToMesh = {}; // 记录KNX->Mesh发送时间,防止回环
152
152
 
153
153
  // 防死循环参数
154
- const LOOP_PREVENTION_MS = 1000; // 500ms内不处理反向同步
154
+ const LOOP_PREVENTION_MS = 800; // 800ms内不处理反向同步,防止回环
155
155
  const DEBOUNCE_MS = 100; // 100ms防抖
156
156
  const MAX_QUEUE_SIZE = 100; // 最大队列大小
157
157
 
@@ -249,7 +249,9 @@ module.exports = function(RED) {
249
249
 
250
250
  // ========== Mesh设备状态变化处理 ==========
251
251
  const handleMeshStateChange = (eventData) => {
252
- if (node.syncLock || node.initializing) return;
252
+ // 只检查initializing,不检查syncLock
253
+ // syncLock会导致队列处理期间丢失事件,改用per-device时间戳防回环
254
+ if (node.initializing) return;
253
255
 
254
256
  const mac = eventData.device.macAddress;
255
257
  const state = eventData.state || {};
@@ -489,25 +491,25 @@ module.exports = function(RED) {
489
491
  if (node.processing || node.commandQueue.length === 0) return;
490
492
 
491
493
  node.processing = true;
492
- node.syncLock = true;
493
494
 
494
- while (node.commandQueue.length > 0) {
495
- const cmd = node.commandQueue.shift();
496
- try {
497
- if (cmd.direction === 'mesh-to-knx') {
498
- await node.syncMeshToKnx(cmd);
499
- } else if (cmd.direction === 'knx-to-mesh') {
500
- await node.syncKnxToMesh(cmd);
495
+ try {
496
+ while (node.commandQueue.length > 0) {
497
+ const cmd = node.commandQueue.shift();
498
+ try {
499
+ if (cmd.direction === 'mesh-to-knx') {
500
+ await node.syncMeshToKnx(cmd);
501
+ } else if (cmd.direction === 'knx-to-mesh') {
502
+ await node.syncKnxToMesh(cmd);
503
+ }
504
+ await node.sleep(50); // 命令间隔50ms
505
+ } catch (err) {
506
+ node.error(`同步失败: ${err.message}`);
501
507
  }
502
- await node.sleep(50); // 命令间隔50ms
503
- } catch (err) {
504
- node.error(`同步失败: ${err.message}`);
505
508
  }
509
+ } finally {
510
+ node.processing = false;
511
+ node.lastSyncTime = Date.now();
506
512
  }
507
-
508
- node.syncLock = false;
509
- node.processing = false;
510
- node.lastSyncTime = Date.now();
511
513
  };
512
514
 
513
515
  // ========== Mesh -> KNX 同步 ==========
@@ -1018,6 +1020,48 @@ module.exports = function(RED) {
1018
1020
  // 监听Mesh设备状态变化
1019
1021
  node.gateway.on('device-state-changed', handleMeshStateChange);
1020
1022
 
1023
+ // ========== 场景执行事件处理 ==========
1024
+ // 当收到场景执行通知时,查询所有已映射设备的状态
1025
+ const handleSceneExecuted = (eventData) => {
1026
+ if (node.initializing) return;
1027
+
1028
+ node.log(`[场景执行] 检测到场景${eventData.sceneId}执行,延迟查询设备状态`);
1029
+
1030
+ // 延迟300ms后查询设备状态,给设备执行时间
1031
+ setTimeout(async () => {
1032
+ // 收集所有已映射的唯一设备地址
1033
+ const mappedAddresses = new Set();
1034
+ for (const mapping of node.mappings) {
1035
+ const mac = node.normalizeMac(mapping.meshMac);
1036
+ const device = node.gateway.deviceManager.getDeviceByMac(mac);
1037
+ if (device && device.networkAddress) {
1038
+ mappedAddresses.add(device.networkAddress);
1039
+ }
1040
+ }
1041
+
1042
+ if (mappedAddresses.size === 0) {
1043
+ node.debug(`[场景执行] 没有已映射的设备需要查询`);
1044
+ return;
1045
+ }
1046
+
1047
+ node.log(`[场景执行] 查询${mappedAddresses.size}个设备的状态`);
1048
+
1049
+ // 逐个查询设备状态
1050
+ for (const addr of mappedAddresses) {
1051
+ try {
1052
+ // 查询开关状态 (0x02)
1053
+ const queryFrame = node.gateway.protocolHandler.buildDeviceStatusQueryFrame(addr, 0x02);
1054
+ await node.gateway.client.sendFrame(queryFrame, 2);
1055
+ await node.sleep(100);
1056
+ } catch (err) {
1057
+ node.debug(`[场景执行] 查询设备0x${addr.toString(16)}状态失败: ${err.message}`);
1058
+ }
1059
+ }
1060
+ }, 300);
1061
+ };
1062
+
1063
+ node.gateway.on('scene-executed', handleSceneExecuted);
1064
+
1021
1065
  // ========== 清理 ==========
1022
1066
  node.on('close', function(done) {
1023
1067
  // 清除初始化定时器
@@ -1028,6 +1072,7 @@ module.exports = function(RED) {
1028
1072
  // 移除事件监听
1029
1073
  if (node.gateway) {
1030
1074
  node.gateway.removeListener('device-state-changed', handleMeshStateChange);
1075
+ node.gateway.removeListener('scene-executed', handleSceneExecuted);
1031
1076
  }
1032
1077
 
1033
1078
  // 清空队列和缓存
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.6.7",
3
+ "version": "1.6.8",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "nodes/symi-gateway.js",
6
6
  "scripts": {