node-red-contrib-symi-mesh 1.3.1 → 1.6.1

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.
@@ -33,43 +33,160 @@ module.exports = function(RED) {
33
33
 
34
34
  node.connectMQTT();
35
35
 
36
- node.gateway.on('device-list-complete', (devices) => {
36
+ // 存储事件处理函数引用,便于关闭时移除
37
+ node._handlers = {};
38
+
39
+ node._handlers.deviceListComplete = (devices) => {
37
40
  if (node.mqttClient && node.mqttClient.connected) {
41
+ // 延迟3秒等待云端同步完成后再发布Discovery
38
42
  setTimeout(() => {
39
43
  node.log(`网关设备列表同步完成,发布${devices.length}个设备到MQTT`);
40
44
  node.publishAllDiscovery(devices);
41
- }, 500);
45
+ }, 3000);
42
46
  } else {
43
47
  node.warn('设备列表已完成但MQTT未连接,等待MQTT连接后发布');
44
48
  }
45
- });
49
+ };
50
+ node.gateway.on('device-list-complete', node._handlers.deviceListComplete);
46
51
 
47
- node.gateway.on('gateway-connected', () => {
52
+ node._handlers.gatewayConnected = () => {
48
53
  node.log('网关已连接,等待设备发现完成');
49
- });
54
+ };
55
+ node.gateway.on('gateway-connected', node._handlers.gatewayConnected);
50
56
 
51
- node.gateway.on('gateway-disconnected', () => {
57
+ node._handlers.gatewayDisconnected = () => {
52
58
  node.log('网关已断开');
53
- });
59
+ };
60
+ node.gateway.on('gateway-disconnected', node._handlers.gatewayDisconnected);
54
61
 
55
- node.gateway.on('device-state-changed', (eventData) => {
62
+ node._handlers.deviceStateChanged = (eventData) => {
56
63
  if (node.mqttClient && node.mqttClient.connected) {
57
64
  node.publishDeviceState(eventData);
58
65
  }
59
- });
60
-
61
-
62
- node.on('close', (done) => {
66
+ };
67
+ node.gateway.on('device-state-changed', node._handlers.deviceStateChanged);
68
+
69
+ // 监听设备状态同步完成事件,处理三合一设备的MQTT发布
70
+ node._handlers.deviceStatesSynced = (devices) => {
63
71
  if (node.mqttClient && node.mqttClient.connected) {
64
- const devices = node.gateway.deviceManager.getAllDevices();
72
+ // 检查是否有新确认的三合一设备需要发布
65
73
  devices.forEach(device => {
66
- const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
67
- node.mqttClient.publish(`symi_mesh/${macClean}/availability`, 'offline', { retain: true });
74
+ if (device.isThreeInOne) {
75
+ const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
76
+ if (!node.publishedDevices.has(macClean)) {
77
+ node.log(`[三合一] 发布三合一设备 ${device.name} 到MQTT`);
78
+
79
+ // 发布availability
80
+ node.mqttClient.publish(`symi_mesh/${macClean}/availability`, 'online', { retain: true });
81
+
82
+ // 发布Discovery配置
83
+ const configs = generateDiscoveryConfig(device, node.mqttPrefix, node);
84
+ configs.forEach((config, index) => {
85
+ setTimeout(() => {
86
+ node.mqttClient.publish(config.topic, config.payload, { retain: true });
87
+ }, index * 50);
88
+ });
89
+
90
+ // 订阅command topics
91
+ const topics = generateStateTopics(device);
92
+ const deviceMacForSub = device.macAddress; // 捕获当前设备MAC,避免闭包问题
93
+ node.log(`[三合一] 订阅${topics.command.length}个command topics`);
94
+ topics.command.forEach(topic => {
95
+ if (!node.subscriptions.has(topic)) {
96
+ node.log(`[三合一] 订阅: ${topic}`);
97
+ node.mqttClient.subscribe(topic, (err) => {
98
+ if (!err) {
99
+ node.subscriptions.set(topic, deviceMacForSub);
100
+ node.log(`[三合一] 订阅成功: ${topic}`);
101
+ } else {
102
+ node.error(`[三合一] 订阅失败: ${topic}`);
103
+ }
104
+ });
105
+ }
106
+ });
107
+
108
+ node.publishedDevices.add(macClean);
109
+
110
+ // 发布初始状态
111
+ setTimeout(() => {
112
+ publishInitialDeviceState(device, node);
113
+ }, 500);
114
+ }
115
+ }
68
116
  });
69
-
70
- setTimeout(() => {
71
- node.mqttClient.end(false, {}, done);
72
- }, 200);
117
+ }
118
+ };
119
+ node.gateway.on('device-states-synced', node._handlers.deviceStatesSynced);
120
+
121
+ // 监听温控器确认事件,确认后立即发布Discovery
122
+ node._handlers.thermostatConfirmed = (device) => {
123
+ if (node.mqttClient && node.mqttClient.connected) {
124
+ node.log(`温控器${device.name}已确认,发布Discovery配置`);
125
+ const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
126
+
127
+ // 发布availability
128
+ node.mqttClient.publish(`symi_mesh/${macClean}/availability`, 'online', { retain: true });
129
+
130
+ // 发布Discovery配置
131
+ const configs = generateDiscoveryConfig(device, node.mqttPrefix, node);
132
+ configs.forEach(config => {
133
+ node.mqttClient.publish(config.topic, config.payload, { retain: config.retain });
134
+ });
135
+
136
+ // 订阅command topics
137
+ const topics = generateStateTopics(device);
138
+ const deviceMacForSub = device.macAddress; // 捕获当前设备MAC,避免闭包问题
139
+ topics.command.forEach(topic => {
140
+ if (!node.subscriptions.has(topic)) {
141
+ node.mqttClient.subscribe(topic, (err) => {
142
+ if (!err) {
143
+ node.subscriptions.set(topic, deviceMacForSub);
144
+ node.debug(`订阅topic: ${topic} -> ${deviceMacForSub}`);
145
+ } else {
146
+ node.error(`订阅失败: ${topic}, ${err.message}`);
147
+ }
148
+ });
149
+ }
150
+ });
151
+
152
+ // 标记为已发布
153
+ node.publishedDevices.add(macClean);
154
+
155
+ // 发布初始状态
156
+ publishInitialDeviceState(device, node);
157
+ }
158
+ };
159
+ node.gateway.on('thermostat-confirmed', node._handlers.thermostatConfirmed);
160
+
161
+ node.on('close', (done) => {
162
+ // 移除gateway事件监听器,防止内存泄漏
163
+ if (node.gateway && node._handlers) {
164
+ node.gateway.removeListener('device-list-complete', node._handlers.deviceListComplete);
165
+ node.gateway.removeListener('gateway-connected', node._handlers.gatewayConnected);
166
+ node.gateway.removeListener('gateway-disconnected', node._handlers.gatewayDisconnected);
167
+ node.gateway.removeListener('device-state-changed', node._handlers.deviceStateChanged);
168
+ node.gateway.removeListener('device-states-synced', node._handlers.deviceStatesSynced);
169
+ node.gateway.removeListener('thermostat-confirmed', node._handlers.thermostatConfirmed);
170
+ }
171
+
172
+ // 清理MQTT资源
173
+ if (node.mqttClient) {
174
+ // 移除所有监听器
175
+ node.mqttClient.removeAllListeners();
176
+
177
+ if (node.mqttClient.connected) {
178
+ const devices = node.gateway.deviceManager.getAllDevices();
179
+ devices.forEach(device => {
180
+ const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
181
+ node.mqttClient.publish(`symi_mesh/${macClean}/availability`, 'offline', { retain: true });
182
+ });
183
+
184
+ setTimeout(() => {
185
+ node.mqttClient.end(false, {}, done);
186
+ }, 200);
187
+ } else {
188
+ done();
189
+ }
73
190
  } else {
74
191
  done();
75
192
  }
@@ -94,7 +211,7 @@ module.exports = function(RED) {
94
211
  node.mqttClient = mqtt.connect(node.mqttBroker, options);
95
212
 
96
213
  node.mqttClient.on('message', (topic, message) => {
97
- node.debug(`[MQTT消息] topic=${topic}, message=${message.toString()}`);
214
+ node.log(`[MQTT消息] topic=${topic}, message=${message.toString()}`);
98
215
  node.handleMQTTMessage(topic, message);
99
216
  });
100
217
 
@@ -121,10 +238,13 @@ module.exports = function(RED) {
121
238
  });
122
239
 
123
240
  node.mqttClient.on('error', (error) => {
124
- node.error(`MQTT错误: ${error.message}`);
241
+ // 只记录非连接错误,避免重连时大量日志
242
+ if (error.code !== 'ECONNREFUSED' && error.code !== 'ENOTFOUND') {
243
+ node.error(`MQTT错误: ${error.message}`);
244
+ }
125
245
  node.status({ fill: 'red', shape: 'ring', text: '错误' });
126
246
  });
127
-
247
+
128
248
  node.mqttClient.on('offline', () => {
129
249
  node.status({ fill: 'yellow', shape: 'ring', text: '离线' });
130
250
  });
@@ -135,46 +255,56 @@ module.exports = function(RED) {
135
255
  }
136
256
  };
137
257
 
138
- SymiMQTTNode.prototype.publishAllDiscovery = function(devices) {
258
+ SymiMQTTNode.prototype.publishAllDiscovery = function(devices, forceUpdate = false) {
139
259
  const node = this;
140
-
141
- const supportedTypes = [1, 2, 3, 4, 5, 8, 9, 10, 0x18];
142
-
260
+
261
+ const supportedTypes = [1, 2, 3, 4, 5, 8, 9, 10, 0x18, 39];
262
+
143
263
  devices.forEach(device => {
144
264
  const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
145
-
265
+
146
266
  // 跳过正在检测中的设备(等待三合一识别完成)
147
267
  if (device.needsThreeInOneCheck) {
148
- node.log(`设备${device.name}正在检测类型,跳过发布(等待识别完成)`);
268
+ node.debug(`设备${device.name}正在检测类型,跳过发布(等待识别完成)`);
149
269
  return; // 不添加到publishedDevices,等识别完成后再发布
150
270
  }
151
-
271
+
152
272
  // 跳过温控器类型但未确认的设备(防止在识别过程中被提前发布)
153
273
  if (device.deviceType === 10 && !device.isThreeInOne && !device.thermostatConfirmed) {
154
- node.log(`温控器${device.name}尚未确认类型,跳过发布`);
274
+ node.debug(`温控器${device.name}尚未确认类型,跳过发布(等待确认后自动发布)`);
155
275
  return;
156
276
  }
157
-
277
+
158
278
  // 三合一设备通过isThreeInOne标记识别,不依赖deviceType
159
279
  if (!supportedTypes.includes(device.deviceType) && !device.isThreeInOne) {
160
280
  node.log(`跳过非支持设备: ${device.name} (type=${device.deviceType})`);
161
281
  return;
162
282
  }
163
-
164
- if (node.publishedDevices.has(macClean)) {
165
- node.log(`设备已发布,跳过: ${device.name}`);
283
+
284
+ // 如果不是强制更新,且设备已发布,则跳过
285
+ if (!forceUpdate && node.publishedDevices.has(macClean)) {
286
+ node.debug(`设备已发布,跳过: ${device.name}`);
166
287
  return;
167
288
  }
289
+
290
+ // 如果是强制更新,先从已发布列表中移除,以便重新发布
291
+ if (forceUpdate && node.publishedDevices.has(macClean)) {
292
+ node.log(`强制更新设备配置: ${device.name}`);
293
+ node.publishedDevices.delete(macClean);
294
+ }
168
295
 
169
- const configs = generateDiscoveryConfig(device, node.mqttPrefix);
296
+ const configs = generateDiscoveryConfig(device, node.mqttPrefix, node);
170
297
  const topics = generateStateTopics(device);
171
298
 
172
- node.log(`发布设备 ${device.name} (${configs.length}个实体)`);
299
+ node.debug(`发布设备 ${device.name} (${configs.length}个实体)`);
173
300
 
174
301
  // 先立即发布availability: online,确保HA认为设备可用
302
+ node.log(`[MQTT] 发布 ${device.name} availability: online`);
175
303
  node.mqttClient.publish(`symi_mesh/${macClean}/availability`, 'online', { retain: true }, (err) => {
176
304
  if (err) {
177
305
  node.error(`发布availability失败: ${err.message}`);
306
+ } else {
307
+ node.log(`[MQTT] ${device.name} availability已发布`);
178
308
  }
179
309
  });
180
310
 
@@ -189,14 +319,16 @@ module.exports = function(RED) {
189
319
  }, index * 50 + 100);
190
320
  });
191
321
 
322
+ const deviceMacForSub = device.macAddress; // 捕获当前设备MAC,避免闭包问题
192
323
  topics.command.forEach(topic => {
193
324
  if (!node.subscriptions.has(topic)) {
325
+ node.log(`[MQTT] 订阅: ${topic}`);
194
326
  node.mqttClient.subscribe(topic, (err) => {
195
327
  if (!err) {
196
- node.subscriptions.set(topic, device.macAddress);
197
- node.debug(`订阅topic: ${topic} → ${device.macAddress}`);
328
+ node.subscriptions.set(topic, deviceMacForSub);
329
+ node.log(`[MQTT] 订阅成功: ${topic}`);
198
330
  } else {
199
- node.error(`订阅失败: ${topic}, ${err.message}`);
331
+ node.error(`[MQTT] 订阅失败: ${topic}, ${err.message}`);
200
332
  }
201
333
  });
202
334
  }
@@ -310,7 +442,7 @@ module.exports = function(RED) {
310
442
  node.mqttClient.publish(`symi_mesh/${macClean}/floor_heating/current_temp`, '20', { retain: true });
311
443
  }
312
444
 
313
- node.log(`发布设备 ${device.name} 初始状态`);
445
+ node.debug(`发布设备 ${device.name} 初始状态`);
314
446
  }
315
447
 
316
448
  SymiMQTTNode.prototype.publishDeviceState = function(eventData) {
@@ -409,6 +541,7 @@ module.exports = function(RED) {
409
541
  node.debug(`发布开关状态: ${switchState}`);
410
542
  } else {
411
543
  // 发布每个继电器的状态
544
+ node.debug(`[状态发布] ${device.name} (${device.channels}路) attrType=0x${attrType.toString(16).toUpperCase()}, state=${JSON.stringify(state)}`);
412
545
  for (let i = 1; i <= device.channels; i++) {
413
546
  const value = state[`switch_${i}`];
414
547
  if (value !== undefined) {
@@ -417,7 +550,9 @@ module.exports = function(RED) {
417
550
  topic: `symi_mesh/${macClean}/switch_${i}/state`,
418
551
  payload: switchState
419
552
  });
420
- node.debug(`发布开关路${i}: ${switchState}`);
553
+ node.debug(`[状态发布] ${device.name} 第${i}路: ${switchState} (value=${value})`);
554
+ } else {
555
+ node.warn(`[状态发布] ${device.name} 第${i}路状态未定义`);
421
556
  }
422
557
  }
423
558
  }
@@ -869,7 +1004,10 @@ module.exports = function(RED) {
869
1004
  if (publishes.length > 0 && node.mqttClient && node.mqttClient.connected) {
870
1005
  publishes.forEach(p => {
871
1006
  node.mqttClient.publish(p.topic, p.payload, { retain: true });
1007
+ node.debug(`[MQTT发布] ${p.topic} = ${p.payload}`);
872
1008
  });
1009
+ } else if (publishes.length > 0) {
1010
+ node.warn(`[MQTT发布失败] MQTT未连接,无法发布${publishes.length}条消息`);
873
1011
  }
874
1012
  };
875
1013
 
@@ -877,7 +1015,8 @@ module.exports = function(RED) {
877
1015
  const node = this;
878
1016
 
879
1017
  // 检查是否是场景触发消息
880
- if (topic.match(/symi_mesh\/room_\w+\/scene\/\d+\/trigger/)) {
1018
+ if (topic.match(/symi_mesh\/room_.+\/scene\/\d+\/trigger/)) {
1019
+ node.log(`[场景触发] 收到MQTT消息: ${topic}`);
881
1020
  node.emit('scene-trigger', topic, message);
882
1021
  return;
883
1022
  }
@@ -885,13 +1024,18 @@ module.exports = function(RED) {
885
1024
  const deviceMac = node.subscriptions.get(topic);
886
1025
 
887
1026
  if (!deviceMac) {
888
- node.warn(`未找到topic订阅: ${topic}`);
1027
+ // 输出当前所有订阅,帮助调试
1028
+ node.warn(`[MQTT] 未找到topic订阅: ${topic}`);
1029
+ node.debug(`[MQTT] 当前订阅列表 (${node.subscriptions.size}个):`);
1030
+ node.subscriptions.forEach((mac, t) => {
1031
+ node.debug(` ${t} -> ${mac}`);
1032
+ });
889
1033
  return;
890
1034
  }
891
1035
 
892
1036
  const device = node.gateway.getDevice(deviceMac);
893
1037
  if (!device) {
894
- node.warn(`设备未找到: ${deviceMac}`);
1038
+ node.warn(`[MQTT] 设备未找到: MAC=${deviceMac}, topic=${topic}`);
895
1039
  return;
896
1040
  }
897
1041
 
@@ -901,11 +1045,51 @@ module.exports = function(RED) {
901
1045
  const commands = node.parseMQTTCommand(topic, payload, device);
902
1046
 
903
1047
  if (commands && commands.length > 0) {
904
- node.log(`[MQTT解析] 解析出${commands.length}个命令:`);
1048
+ node.debug(`[MQTT解析] 解析出${commands.length}个命令:`);
905
1049
  commands.forEach((cmd, idx) => {
906
1050
  node.log(` 命令${idx + 1}: attrType=0x${cmd.attrType.toString(16).toUpperCase()}, param=[${Array.from(cmd.param).map(p => '0x' + p.toString(16).toUpperCase()).join(', ')}]`);
907
1051
  });
908
1052
 
1053
+ // 检查是否是开关控制命令,且该按键绑定了场景
1054
+ const isSwitch = (device.deviceType === 1 || device.deviceType === 2) && commands[0].attrType === 0x02;
1055
+ if (isSwitch && device.subDeviceConfigs && device.subDeviceConfigs.length > 0) {
1056
+ // 解析出是哪个按键
1057
+ const match = topic.match(/switch_(\d+)\/set/);
1058
+ const channel = match ? parseInt(match[1]) : 1;
1059
+
1060
+ // 检查该按键是否绑定场景
1061
+ const sceneInfo = device.getButtonSceneId(channel);
1062
+ if (sceneInfo) {
1063
+ // 该按键绑定了场景,应该触发场景而不是直接控制继电器
1064
+ node.log(`[MQTT场景] 按键${channel}(${sceneInfo.name})绑定了场景,触发场景而非控制继电器`);
1065
+
1066
+ (async () => {
1067
+ try {
1068
+ // 根据按键类型决定触发哪个场景
1069
+ let sceneId = sceneInfo.sceneId;
1070
+
1071
+ if (sceneInfo.type === 'dual_control' || sceneInfo.type === 'master_control') {
1072
+ // 双控/总控:根据目标状态选择场景
1073
+ const targetState = (payload === 'ON');
1074
+ const config = device.subDeviceConfigs[channel - 1];
1075
+ sceneId = targetState ? config.on_scene_id : config.off_scene_id;
1076
+ node.log(`[MQTT场景] ${sceneInfo.type === 'dual_control' ? '双控' : '总控'}按键,目标状态=${targetState ? '开' : '关'},触发场景${sceneId}`);
1077
+ } else {
1078
+ // 场景按键:直接触发场景
1079
+ node.log(`[MQTT场景] 场景按键,触发场景${sceneId}`);
1080
+ }
1081
+
1082
+ await node.gateway.sendScene(sceneId);
1083
+ node.log(`[MQTT场景] 场景${sceneId}(${sceneInfo.name})控制命令已发送`);
1084
+ } catch(err) {
1085
+ node.error(`[MQTT场景] 场景触发失败: ${err.message}`);
1086
+ }
1087
+ })();
1088
+
1089
+ return; // 不执行后续的继电器控制
1090
+ }
1091
+ }
1092
+
909
1093
  // 使用for循环而不是forEach以正确处理async
910
1094
  (async () => {
911
1095
  for (const command of commands) {
@@ -913,13 +1097,13 @@ module.exports = function(RED) {
913
1097
  node.log(`[MQTT发送] → 网关: addr=0x${device.networkAddress.toString(16).toUpperCase()}, attr=0x${command.attrType.toString(16).toUpperCase()}, param=[${paramHex}]`);
914
1098
  try {
915
1099
  await node.gateway.sendControl(device.networkAddress, command.attrType, command.param);
916
- node.log(`[MQTT发送] 成功: ${device.name}`);
917
-
1100
+ node.debug(`[MQTT发送] 成功: ${device.name}`);
1101
+
918
1102
  // 立即发布状态更新(optimistic update)
919
1103
  node.publishCommandFeedback(device, command, payload, topic);
920
-
1104
+
921
1105
  } catch(err) {
922
- node.error(`[MQTT发送] 失败: ${err.message}`);
1106
+ node.error(`[MQTT发送] 失败: ${err.message}`);
923
1107
  }
924
1108
  }
925
1109
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.3.1",
3
+ "version": "1.6.1",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -31,16 +31,19 @@
31
31
  "symi-gateway": "nodes/symi-gateway.js",
32
32
  "symi-device": "nodes/symi-device.js",
33
33
  "symi-mqtt": "nodes/symi-mqtt.js",
34
- "symi-cloud-sync": "nodes/symi-cloud-sync.js"
34
+ "symi-cloud-sync": "nodes/symi-cloud-sync.js",
35
+ "symi-485-config": "nodes/symi-485-config.js",
36
+ "symi-rs485-bridge": "nodes/symi-485-bridge.js",
37
+ "rs485-debug": "nodes/rs485-debug.js"
35
38
  }
36
39
  },
37
40
  "dependencies": {
38
- "axios": "^1.13.2",
41
+ "axios": "^1.7.9",
39
42
  "mqtt": "^5.3.0",
40
43
  "serialport": "^12.0.0"
41
44
  },
42
45
  "engines": {
43
- "node": ">=14.0.0"
46
+ "node": ">=18.0.0"
44
47
  },
45
48
  "files": [
46
49
  "lib/",