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 +14 -2
- package/lib/device-manager.js +11 -3
- package/nodes/symi-485-bridge.js +26 -27
- package/nodes/symi-gateway.js +7 -0
- package/nodes/symi-knx-bridge.js +63 -18
- package/package.json +1 -1
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.
|
|
1107
|
+
**版本**: 1.6.8
|
|
1096
1108
|
**协议**: 蓝牙MESH网关(初级版)串口协议V1.0
|
|
1097
|
-
**最后更新**: 2025-12-
|
|
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
|
package/lib/device-manager.js
CHANGED
|
@@ -85,7 +85,9 @@ class DeviceInfo {
|
|
|
85
85
|
case 0x45:
|
|
86
86
|
// 6-8路开关状态上报(2字节,小端序)
|
|
87
87
|
// 用于场景执行后的状态同步
|
|
88
|
-
|
|
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
|
|
389
|
-
|
|
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++) {
|
package/nodes/symi-485-bridge.js
CHANGED
|
@@ -530,7 +530,7 @@ module.exports = function(RED) {
|
|
|
530
530
|
|
|
531
531
|
// Mesh设备状态变化处理(事件驱动)
|
|
532
532
|
const handleMeshStateChange = (eventData) => {
|
|
533
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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();
|
package/nodes/symi-gateway.js
CHANGED
|
@@ -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
|
|
package/nodes/symi-knx-bridge.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 支持开关、窗帘等设备的双向状态同步
|
|
4
4
|
* 事件驱动架构,命令队列顺序处理,防死循环机制
|
|
5
5
|
*
|
|
6
|
-
* 版本: 1.6.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
// 清空队列和缓存
|