node-red-contrib-symi-mesh 1.6.5 → 1.6.7

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.
@@ -4,8 +4,25 @@
4
4
  */
5
5
 
6
6
  const mqtt = require('mqtt');
7
+ const net = require('net');
7
8
  const { generateDiscoveryConfig, generateStateTopics, convertStateValue } = require('../lib/mqtt-helper');
8
9
 
10
+ // 检查TCP端口是否可用
11
+ function checkPort(host, port, timeout = 2000) {
12
+ return new Promise((resolve) => {
13
+ const socket = new net.Socket();
14
+ socket.setTimeout(timeout);
15
+ socket.on('connect', () => { socket.destroy(); resolve(true); });
16
+ socket.on('timeout', () => { socket.destroy(); resolve(false); });
17
+ socket.on('error', () => { socket.destroy(); resolve(false); });
18
+ try {
19
+ socket.connect({ port, host, family: 4 });
20
+ } catch (e) {
21
+ resolve(false);
22
+ }
23
+ });
24
+ }
25
+
9
26
  module.exports = function(RED) {
10
27
  function SymiMQTTNode(config) {
11
28
  RED.nodes.createNode(this, config);
@@ -193,10 +210,34 @@ module.exports = function(RED) {
193
210
  });
194
211
  }
195
212
 
196
- SymiMQTTNode.prototype.connectMQTT = function() {
213
+ SymiMQTTNode.prototype.connectMQTT = async function() {
197
214
  const node = this;
198
215
 
199
216
  try {
217
+ // 解析broker地址
218
+ let host = 'localhost', port = 1883;
219
+ try {
220
+ const url = new URL(node.mqttBroker);
221
+ host = url.hostname || 'localhost';
222
+ port = parseInt(url.port) || 1883;
223
+ } catch (e) {
224
+ // 使用默认值
225
+ }
226
+
227
+ // 先检查端口是否可用,避免连接失败导致崩溃
228
+ const isAvailable = await checkPort(host, port, 3000);
229
+ if (!isAvailable) {
230
+ // 只在首次或每分钟记录一次警告,避免日志刷屏
231
+ if (!node._lastMqttWarn || Date.now() - node._lastMqttWarn > 60000) {
232
+ node.warn(`MQTT broker ${host}:${port} 不可用,每30秒重试`);
233
+ node._lastMqttWarn = Date.now();
234
+ }
235
+ node.status({ fill: 'yellow', shape: 'ring', text: `Broker不可用 ${host}:${port}` });
236
+ // 30秒后重试
237
+ setTimeout(() => node.connectMQTT(), 30000);
238
+ return;
239
+ }
240
+
200
241
  const options = {
201
242
  clientId: `symi-mesh-${Math.random().toString(16).substring(2, 10)}`,
202
243
  clean: true,
@@ -210,6 +251,14 @@ module.exports = function(RED) {
210
251
 
211
252
  node.mqttClient = mqtt.connect(node.mqttBroker, options);
212
253
 
254
+ // 立即绑定错误处理
255
+ node.mqttClient.on('error', (error) => {
256
+ if (error.code !== 'ECONNREFUSED' && error.code !== 'ENOTFOUND') {
257
+ node.error(`MQTT错误: ${error.message}`);
258
+ }
259
+ node.status({ fill: 'red', shape: 'ring', text: '连接失败' });
260
+ });
261
+
213
262
  node.mqttClient.on('message', (topic, message) => {
214
263
  node.log(`[MQTT消息] topic=${topic}, message=${message.toString()}`);
215
264
  node.handleMQTTMessage(topic, message);
@@ -236,14 +285,6 @@ module.exports = function(RED) {
236
285
  node.debug('MQTT正在重连...');
237
286
  node.status({ fill: 'yellow', shape: 'ring', text: '重连中' });
238
287
  });
239
-
240
- node.mqttClient.on('error', (error) => {
241
- // 只记录非连接错误,避免重连时大量日志
242
- if (error.code !== 'ECONNREFUSED' && error.code !== 'ENOTFOUND') {
243
- node.error(`MQTT错误: ${error.message}`);
244
- }
245
- node.status({ fill: 'red', shape: 'ring', text: '错误' });
246
- });
247
288
 
248
289
  node.mqttClient.on('offline', () => {
249
290
  node.status({ fill: 'yellow', shape: 'ring', text: '离线' });
@@ -258,7 +299,7 @@ module.exports = function(RED) {
258
299
  SymiMQTTNode.prototype.publishAllDiscovery = function(devices, forceUpdate = false) {
259
300
  const node = this;
260
301
 
261
- const supportedTypes = [1, 2, 3, 4, 5, 8, 9, 10, 0x18, 39];
302
+ const supportedTypes = [1, 2, 3, 4, 5, 8, 9, 10, 11, 0x18, 39];
262
303
 
263
304
  devices.forEach(device => {
264
305
  const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
@@ -552,7 +593,8 @@ module.exports = function(RED) {
552
593
  });
553
594
  node.debug(`[状态发布] ${device.name} 第${i}路: ${switchState} (value=${value})`);
554
595
  } else {
555
- node.warn(`[状态发布] ${device.name} 第${i}路状态未定义`);
596
+ // 部分路未使用是正常情况,不需要警告
597
+ node.debug(`[状态发布] ${device.name} 第${i}路状态未定义`);
556
598
  }
557
599
  }
558
600
  }
@@ -641,23 +683,24 @@ module.exports = function(RED) {
641
683
 
642
684
  case 0x05:
643
685
  // 窗帘运行状态反馈 (CURT_RUN_STATUS)
644
- // 协议:0=空闲/到头, 1=打开中, 2=关闭中, 3=停止
645
- if (state.curtainStatus !== undefined) {
646
- const curtainStates = { 0: 'stopped', 1: 'opening', 2: 'closing', 3: 'stopped' };
647
- const curtainState = curtainStates[state.curtainStatus] || 'stopped';
686
+ // 【兼容两种协议】:
687
+ // 米家协议: 0=打开中, 1=关闭中, 2=停止
688
+ // 小程序协议: 1=打开, 2=关闭, 3=停止
689
+ // 使用curtainAction(由device-manager解析)来发布状态
690
+ if (state.curtainAction !== undefined) {
648
691
  publishes.push({
649
692
  topic: `symi_mesh/${macClean}/cover/state`,
650
- payload: curtainState
693
+ payload: state.curtainAction
651
694
  });
652
- node.debug(`发布窗帘运行状态: ${curtainState} (原始值=${state.curtainStatus})`);
695
+ node.debug(`发布窗帘运行状态: ${state.curtainAction} (原始值=${state.curtainStatus}, 协议=${state.curtainProtocol || 'unknown'})`);
653
696
 
654
- // 到头(status=0)时也发布position,确保HA显示正确位置
655
- if (state.curtainStatus === 0 && state.curtainPosition !== undefined) {
697
+ // 停止时也发布position,确保HA显示正确位置
698
+ if (state.curtainAction === 'stopped' && state.curtainPosition !== undefined) {
656
699
  publishes.push({
657
700
  topic: `symi_mesh/${macClean}/cover/position`,
658
701
  payload: state.curtainPosition.toString()
659
702
  });
660
- node.debug(`窗帘到头,同时发布位置: ${state.curtainPosition}%`);
703
+ node.debug(`窗帘停止,同时发布位置: ${state.curtainPosition}%`);
661
704
  }
662
705
  }
663
706
  break;
@@ -672,14 +715,13 @@ module.exports = function(RED) {
672
715
  node.debug(`发布窗帘位置: ${state.curtainPosition}%`);
673
716
 
674
717
  // 同时发布运行状态(确保HA正确显示)
675
- if (state.curtainStatus !== undefined) {
676
- const curtainStates = { 0: 'stopped', 1: 'opening', 2: 'closing', 3: 'stopped' };
677
- const curtainState = curtainStates[state.curtainStatus] || 'stopped';
718
+ // 使用curtainAction(由device-manager解析,兼容米家和小程序协议)
719
+ if (state.curtainAction !== undefined) {
678
720
  publishes.push({
679
721
  topic: `symi_mesh/${macClean}/cover/state`,
680
- payload: curtainState
722
+ payload: state.curtainAction
681
723
  });
682
- node.debug(`同时发布窗帘状态: ${curtainState}`);
724
+ node.debug(`同时发布窗帘状态: ${state.curtainAction}`);
683
725
  }
684
726
  }
685
727
  break;
@@ -1312,6 +1354,13 @@ module.exports = function(RED) {
1312
1354
  const action = actions[payload];
1313
1355
  if (action) {
1314
1356
  commands.push({ attrType: 0x05, param: Buffer.from([action]) });
1357
+
1358
+ // 发出窗帘控制事件,通知485桥用户的真实意图
1359
+ this.emit('curtain-control', {
1360
+ mac: device.macAddress,
1361
+ action: payload // 'OPEN', 'CLOSE', 'STOP'
1362
+ });
1363
+ this.debug(`[MQTT] 窗帘控制命令: ${payload}, MAC=${device.macAddress}`);
1315
1364
  }
1316
1365
 
1317
1366
  } else if (topic.includes('/position/set')) {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
- "main": "index.js",
5
+ "main": "nodes/symi-gateway.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"
8
8
  },
@@ -34,12 +34,13 @@
34
34
  "symi-cloud-sync": "nodes/symi-cloud-sync.js",
35
35
  "symi-485-config": "nodes/symi-485-config.js",
36
36
  "symi-rs485-bridge": "nodes/symi-485-bridge.js",
37
- "rs485-debug": "nodes/rs485-debug.js"
37
+ "rs485-debug": "nodes/rs485-debug.js",
38
+ "symi-knx-bridge": "nodes/symi-knx-bridge.js"
38
39
  }
39
40
  },
40
41
  "dependencies": {
41
42
  "axios": "^1.7.9",
42
- "mqtt": "^5.3.0",
43
+ "mqtt": "^5.10.0",
43
44
  "serialport": "^12.0.0"
44
45
  },
45
46
  "engines": {