node-red-contrib-symi-mesh 1.7.2 → 1.7.3

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
@@ -1457,6 +1457,35 @@ node-red-contrib-symi-mesh/
1457
1457
 
1458
1458
  ## 更新日志
1459
1459
 
1460
+ ### v1.7.3 (2025-12-24)
1461
+ - **MQTT同步节点**:新增`symi-mqtt-sync`节点实现第三方MQTT品牌设备与Mesh设备双向同步
1462
+ - **双MQTT配置节点架构**:
1463
+ - Mesh MQTT:选择`symi-mqtt`配置节点,获取Mesh设备列表
1464
+ - 品牌MQTT:选择`symi-mqtt-brand`配置节点(支持下拉选择+编辑+添加)
1465
+ - **品牌MQTT配置节点**:新增`symi-mqtt-brand`配置节点
1466
+ - 支持配置MQTT服务器地址、用户名、密码
1467
+ - 支持HYQW协议(项目代码、设备SN)
1468
+ - 自动发现品牌MQTT设备实体
1469
+ - 可扩展支持更多品牌协议
1470
+ - **实体映射**:
1471
+ - 左边选择Mesh设备+通道
1472
+ - 右边选择品牌设备+通道(灯具支持多路)
1473
+ - 相同类型实体一对一映射
1474
+ - **设备类型**:灯具(8)、空调(12)、窗帘(14)、地暖(16)、新风(36)
1475
+ - **双向同步**:MQTT↔Mesh实时状态同步,2秒防抖防死循环
1476
+ - **输入输出端口**:支持连接debug节点调试同步数据
1477
+ - **错误日志限流**:网络故障时每60秒最多记录一次错误
1478
+ - **断线自动重连**:5秒重连间隔
1479
+ - **串口支持优化**:
1480
+ - 所有串口节点支持手动输入+搜索选择
1481
+ - 兼容serialport v9/v10+,兼容HassOS环境
1482
+ - 串口错误日志限流,避免长时间故障时日志爆炸
1483
+ - **稳定性优化**:
1484
+ - 错误日志频率限制,长时间网络故障不影响系统性能
1485
+ - 静默处理非关键错误,生产级稳定性
1486
+ - 内存安全,无调试日志,断电断网恢复后正常工作
1487
+ - 缓存队列处理,符合MQTT协议要求
1488
+
1460
1489
  ### v1.7.2 (2025-12-22)
1461
1490
  - **RS485双向同步节点**:新增`symi-rs485-sync`独立节点实现两种不同RS485协议之间的双向同步
1462
1491
  - 支持中弘VRF网关协议(功能码0x31-0x34控制,0x50查询)
@@ -1575,8 +1604,8 @@ Copyright (c) 2025 SYMI 亖米
1575
1604
  ## 关于
1576
1605
 
1577
1606
  **作者**: SYMI 亖米
1578
- **版本**: 1.7.2
1607
+ **版本**: 1.7.3
1579
1608
  **协议**: 蓝牙MESH网关(初级版)串口协议V1.0
1580
- **最后更新**: 2025-12-23
1609
+ **最后更新**: 2025-12-24
1581
1610
  **仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
1582
1611
  **npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
@@ -20,6 +20,23 @@ class SerialClient extends EventEmitter {
20
20
  this.reconnectDelay = 5000;
21
21
  this.reconnectTimer = null;
22
22
  this.autoReconnect = true;
23
+ // 错误日志限流
24
+ this._lastErrorLog = 0;
25
+ this._errorLogInterval = 60000; // 60秒最多记录一次错误
26
+ this._reconnectAttempts = 0;
27
+ }
28
+
29
+ // 限流错误日志
30
+ _logErrorThrottled(msg) {
31
+ const now = Date.now();
32
+ if (now - this._lastErrorLog > this._errorLogInterval) {
33
+ this._lastErrorLog = now;
34
+ if (this._reconnectAttempts > 1) {
35
+ this.logger.warn(`${msg} (已尝试${this._reconnectAttempts}次)`);
36
+ } else {
37
+ this.logger.warn(msg);
38
+ }
39
+ }
23
40
  }
24
41
 
25
42
  async connect() {
@@ -98,7 +115,7 @@ class SerialClient extends EventEmitter {
98
115
 
99
116
  this.port.on('error', (error) => {
100
117
  clearTimeout(timeout);
101
- this.logger.error('Serial port error:', error.message);
118
+ this._logErrorThrottled(`串口错误: ${error.message}`);
102
119
  this.emit('error', error);
103
120
 
104
121
  if (!resolved && !rejected) {
@@ -153,13 +170,15 @@ class SerialClient extends EventEmitter {
153
170
  if (this.autoReconnect && !this.reconnectTimer) {
154
171
  this.reconnectTimer = setTimeout(() => {
155
172
  this.reconnectTimer = null;
156
- this.logger.log('Attempting to reconnect serial port...');
173
+ this._reconnectAttempts++;
157
174
  this.connect()
158
175
  .then(() => {
159
- this.logger.log('Serial port reconnected successfully');
176
+ this._reconnectAttempts = 0;
177
+ this._lastErrorLog = 0; // 重置错误日志限流
178
+ this.logger.log(`串口重连成功: ${this.portPath}`);
160
179
  })
161
180
  .catch((error) => {
162
- this.logger.error(`Serial port reconnect failed: ${error.message}`);
181
+ this._logErrorThrottled(`串口重连失败: ${error.message}`);
163
182
  // 失败后会自动继续尝试重连(通过handleDisconnect)
164
183
  });
165
184
  }, this.reconnectDelay);
@@ -29,24 +29,45 @@
29
29
  }
30
30
  }).trigger('change');
31
31
 
32
- // 加载串口列表
33
- function loadSerialPorts() {
34
- $.getJSON('/symi-rs485-bridge/serial-ports', function(ports) {
35
- var select = $('#node-config-input-serialPort');
36
- var currentVal = select.val() || node.serialPort;
37
- select.empty();
38
- select.append('<option value="">-- 选择串口 --</option>');
39
- ports.forEach(function(p) {
40
- var label = p.path;
41
- if (p.manufacturer) label += ' (' + p.manufacturer + ')';
42
- var sel = (p.path === currentVal) ? ' selected' : '';
43
- select.append('<option value="' + p.path + '"' + sel + '>' + label + '</option>');
44
- });
32
+ // 串口搜索
33
+ $('#btn-refresh-ports').on('click', function() {
34
+ var $btn = $(this);
35
+ var $select = $('#serial-port-select');
36
+ $btn.prop('disabled', true);
37
+ $select.empty().append('<option value="">搜索中...</option>').show();
38
+
39
+ $.getJSON('/symi-gateway/serial-ports', function(ports) {
40
+ $select.empty();
41
+ if (ports && ports.length > 0) {
42
+ $select.append('<option value="">-- 选择串口 --</option>');
43
+ ports.forEach(function(p) {
44
+ var label = p.path;
45
+ if (p.manufacturer) label += ' (' + p.manufacturer + ')';
46
+ var sel = (p.path === node.serialPort) ? ' selected' : '';
47
+ $select.append('<option value="' + p.path + '"' + sel + '>' + label + '</option>');
48
+ });
49
+ } else {
50
+ $select.append('<option value="">未发现串口</option>');
51
+ }
52
+ $btn.prop('disabled', false);
53
+ }).fail(function() {
54
+ $select.empty().append('<option value="">搜索失败</option>');
55
+ $btn.prop('disabled', false);
45
56
  });
57
+ });
58
+
59
+ // 串口选择
60
+ $('#serial-port-select').on('change', function() {
61
+ var val = $(this).val();
62
+ if (val) {
63
+ $('#node-config-input-serialPort').val(val);
64
+ }
65
+ });
66
+
67
+ // 初始化时如果已有串口配置,显示在下拉框
68
+ if (node.serialPort) {
69
+ $('#serial-port-select').append('<option value="' + node.serialPort + '" selected>' + node.serialPort + '</option>').show();
46
70
  }
47
-
48
- $('#btn-refresh-ports').on('click', loadSerialPorts);
49
- loadSerialPorts();
50
71
  }
51
72
  });
52
73
  </script>
@@ -67,11 +88,13 @@
67
88
 
68
89
  <div class="serial-config">
69
90
  <div class="form-row">
70
- <label for="node-config-input-serialPort"><i class="fa fa-usb"></i> 串口</label>
71
- <select id="node-config-input-serialPort" style="width:60%"></select>
72
- <button type="button" id="btn-refresh-ports" class="red-ui-button" style="margin-left:5px">
73
- <i class="fa fa-refresh"></i>
74
- </button>
91
+ <label for="node-config-input-serialPort"><i class="fa fa-usb"></i> 串口路径</label>
92
+ <input type="text" id="node-config-input-serialPort" placeholder="/dev/ttyUSB0 或 COM3" style="width:50%;">
93
+ <button type="button" id="btn-refresh-ports" class="red-ui-button" style="margin-left:5px">搜索串口</button>
94
+ </div>
95
+ <div class="form-row">
96
+ <label>&nbsp;</label>
97
+ <select id="serial-port-select" style="width:70%;display:none;"></select>
75
98
  </div>
76
99
 
77
100
  <div class="form-row">
@@ -15,6 +15,8 @@
15
15
  : `Symi Gateway (${this.serialPort})`);
16
16
  },
17
17
  oneditprepare: function() {
18
+ var node = this;
19
+
18
20
  $('#node-config-input-connectionType').on('change', function() {
19
21
  if ($(this).val() === 'tcp') {
20
22
  $('.tcp-config').show();
@@ -24,6 +26,47 @@
24
26
  $('.serial-config').show();
25
27
  }
26
28
  }).trigger('change');
29
+
30
+ // 串口自动发现
31
+ $('#serial-discover-btn').on('click', function() {
32
+ var $btn = $(this);
33
+ var $select = $('#serial-port-select');
34
+ $btn.prop('disabled', true).text('搜索中...');
35
+ $select.empty().append('<option value="">搜索中...</option>');
36
+
37
+ $.getJSON('/symi-gateway/serial-ports', function(ports) {
38
+ $select.empty();
39
+ if (ports && ports.length > 0) {
40
+ $select.append('<option value="">-- 选择串口 --</option>');
41
+ ports.forEach(function(port) {
42
+ var label = port.path;
43
+ if (port.manufacturer) label += ' (' + port.manufacturer + ')';
44
+ var selected = (node.serialPort === port.path) ? 'selected' : '';
45
+ $select.append('<option value="' + port.path + '" ' + selected + '>' + label + '</option>');
46
+ });
47
+ $select.show();
48
+ } else {
49
+ $select.append('<option value="">未发现串口</option>');
50
+ }
51
+ $btn.prop('disabled', false).text('搜索串口');
52
+ }).fail(function() {
53
+ $select.empty().append('<option value="">搜索失败</option>');
54
+ $btn.prop('disabled', false).text('搜索串口');
55
+ });
56
+ });
57
+
58
+ // 串口选择
59
+ $('#serial-port-select').on('change', function() {
60
+ var val = $(this).val();
61
+ if (val) {
62
+ $('#node-config-input-serialPort').val(val);
63
+ }
64
+ });
65
+
66
+ // 初始化时如果已有串口配置,显示在下拉框
67
+ if (node.serialPort) {
68
+ $('#serial-port-select').append('<option value="' + node.serialPort + '" selected>' + node.serialPort + '</option>').show();
69
+ }
27
70
  }
28
71
  });
29
72
  </script>
@@ -54,7 +97,12 @@
54
97
 
55
98
  <div class="form-row serial-config">
56
99
  <label for="node-config-input-serialPort"><i class="fa fa-usb"></i> 串口路径</label>
57
- <input type="text" id="node-config-input-serialPort" placeholder="/dev/ttyUSB0 或 COM3">
100
+ <input type="text" id="node-config-input-serialPort" placeholder="/dev/ttyUSB0 或 COM3" style="width:50%;">
101
+ <button type="button" id="serial-discover-btn" class="red-ui-button" style="margin-left:5px;">搜索串口</button>
102
+ </div>
103
+ <div class="form-row serial-config">
104
+ <label>&nbsp;</label>
105
+ <select id="serial-port-select" style="width:70%;display:none;"></select>
58
106
  </div>
59
107
 
60
108
  <div class="form-row serial-config">
@@ -682,6 +682,39 @@ module.exports = function(RED) {
682
682
  res.json([]);
683
683
  }
684
684
  });
685
+
686
+ // 串口自动发现API - 兼容不同版本serialport
687
+ RED.httpAdmin.get('/symi-gateway/serial-ports', async function(req, res) {
688
+ try {
689
+ let ports = [];
690
+ try {
691
+ // 尝试新版API (serialport v10+)
692
+ const { SerialPort } = require('serialport');
693
+ ports = await SerialPort.list();
694
+ } catch (e1) {
695
+ try {
696
+ // 尝试旧版API (serialport v9)
697
+ const SerialPort = require('serialport');
698
+ if (typeof SerialPort.list === 'function') {
699
+ ports = await SerialPort.list();
700
+ }
701
+ } catch (e2) {
702
+ // 静默处理
703
+ }
704
+ }
705
+ // 过滤常见串口设备
706
+ const filteredPorts = (ports || []).filter(function(p) {
707
+ const path = (p.path || '').toLowerCase();
708
+ // 排除蓝牙和内部设备
709
+ if (path.includes('bluetooth') || path.includes('bt-')) return false;
710
+ // 包含USB、tty、COM端口
711
+ return path.includes('usb') || path.includes('tty') || path.includes('com') || path.includes('serial');
712
+ });
713
+ res.json(filteredPorts);
714
+ } catch (e) {
715
+ res.json([]);
716
+ }
717
+ });
685
718
  }
686
719
  };
687
720
 
@@ -0,0 +1,75 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('symi-mqtt-brand', {
3
+ category: 'config',
4
+ defaults: {
5
+ name: { value: '' },
6
+ brand: { value: 'hyqw' },
7
+ mqttBroker: { value: 'mqtt://localhost:1883', required: true },
8
+ mqttUsername: { value: '' },
9
+ mqttPassword: { value: '' },
10
+ projectCode: { value: '', required: true },
11
+ deviceSn: { value: '', required: true }
12
+ },
13
+ label: function() {
14
+ if (this.name) return this.name;
15
+ if (this.projectCode && this.deviceSn) {
16
+ return 'HYQW: ' + this.projectCode;
17
+ }
18
+ return '品牌MQTT';
19
+ },
20
+ oneditprepare: function() {
21
+ var node = this;
22
+
23
+ // 品牌切换
24
+ function updateBrandUI() {
25
+ var brand = $('#node-config-input-brand').val();
26
+ if (brand === 'hyqw') {
27
+ $('.brand-hyqw-config').show();
28
+ } else {
29
+ $('.brand-hyqw-config').hide();
30
+ }
31
+ }
32
+ $('#node-config-input-brand').on('change', updateBrandUI);
33
+ updateBrandUI();
34
+ }
35
+ });
36
+ </script>
37
+
38
+ <script type="text/html" data-template-name="symi-mqtt-brand">
39
+ <div class="form-row">
40
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> 名称</label>
41
+ <input type="text" id="node-config-input-name" placeholder="如:花语前湾MQTT">
42
+ </div>
43
+
44
+ <div class="form-row">
45
+ <label for="node-config-input-brand"><i class="fa fa-plug"></i> 品牌协议</label>
46
+ <select id="node-config-input-brand">
47
+ <option value="hyqw">HYQW (花语前湾)</option>
48
+ </select>
49
+ </div>
50
+
51
+ <div class="form-row">
52
+ <label for="node-config-input-mqttBroker"><i class="fa fa-server"></i> MQTT地址</label>
53
+ <input type="text" id="node-config-input-mqttBroker" placeholder="mqtt://192.168.1.100:1883">
54
+ </div>
55
+
56
+ <div class="form-row">
57
+ <label for="node-config-input-mqttUsername"><i class="fa fa-user"></i> 用户名</label>
58
+ <input type="text" id="node-config-input-mqttUsername" placeholder="可选">
59
+ </div>
60
+
61
+ <div class="form-row">
62
+ <label for="node-config-input-mqttPassword"><i class="fa fa-lock"></i> 密码</label>
63
+ <input type="password" id="node-config-input-mqttPassword" placeholder="可选">
64
+ </div>
65
+
66
+ <div class="form-row brand-hyqw-config">
67
+ <label for="node-config-input-projectCode"><i class="fa fa-folder"></i> 项目代码</label>
68
+ <input type="text" id="node-config-input-projectCode" placeholder="如: SH-485-V22">
69
+ </div>
70
+
71
+ <div class="form-row brand-hyqw-config">
72
+ <label for="node-config-input-deviceSn"><i class="fa fa-barcode"></i> 设备SN</label>
73
+ <input type="text" id="node-config-input-deviceSn" placeholder="如: FB485V222024110500000377">
74
+ </div>
75
+ </script>
@@ -0,0 +1,238 @@
1
+ 'use strict';
2
+
3
+ const mqtt = require('mqtt');
4
+
5
+ module.exports = function(RED) {
6
+ // 品牌MQTT配置节点
7
+ function SymiMqttBrandNode(config) {
8
+ RED.nodes.createNode(this, config);
9
+ const node = this;
10
+
11
+ // 配置参数
12
+ node.name = config.name || '品牌MQTT';
13
+ node.brand = config.brand || 'hyqw';
14
+ node.mqttBroker = config.mqttBroker || 'mqtt://localhost:1883';
15
+ node.mqttUsername = config.mqttUsername || '';
16
+ node.mqttPassword = config.mqttPassword || '';
17
+ node.projectCode = config.projectCode || '';
18
+ node.deviceSn = config.deviceSn || '';
19
+
20
+ // 运行时状态
21
+ node._client = null;
22
+ node._connected = false;
23
+ node._discoveredDevices = new Map();
24
+ node._subscribers = new Set();
25
+ node._closing = false;
26
+ node._reconnectTimer = null;
27
+ node._lastErrorLog = 0;
28
+
29
+ // 设备类型定义
30
+ const DEVICE_TYPES = {
31
+ 8: { name: '灯具', type: 'light' },
32
+ 12: { name: '空调', type: 'climate' },
33
+ 14: { name: '窗帘', type: 'cover' },
34
+ 16: { name: '地暖', type: 'climate' },
35
+ 36: { name: '新风', type: 'fan' }
36
+ };
37
+
38
+ // 获取MQTT主题
39
+ function getUploadTopic() {
40
+ if (!node.projectCode || !node.deviceSn) return null;
41
+ return `FMQ/${node.projectCode}/${node.deviceSn}/UPLOAD/2002`;
42
+ }
43
+
44
+ function getDownTopic() {
45
+ if (!node.projectCode || !node.deviceSn) return null;
46
+ return `FMQ/${node.projectCode}/${node.deviceSn}/DOWN/2001`;
47
+ }
48
+
49
+ // 限流错误日志
50
+ function logErrorThrottled(msg) {
51
+ const now = Date.now();
52
+ if (now - node._lastErrorLog > 60000) {
53
+ node._lastErrorLog = now;
54
+ node.warn(msg);
55
+ }
56
+ }
57
+
58
+ // 连接MQTT
59
+ function connect() {
60
+ if (node._closing || node._client) return;
61
+
62
+ const uploadTopic = getUploadTopic();
63
+ if (!uploadTopic) {
64
+ node.warn('品牌MQTT配置不完整:缺少项目代码或设备SN');
65
+ return;
66
+ }
67
+
68
+ const brokerUrl = node.mqttBroker || 'mqtt://localhost:1883';
69
+ const options = {
70
+ clientId: `symi_brand_${node.id}_${Date.now()}`,
71
+ clean: true,
72
+ connectTimeout: 10000,
73
+ reconnectPeriod: 0,
74
+ keepalive: 60
75
+ };
76
+
77
+ if (node.mqttUsername) {
78
+ options.username = node.mqttUsername;
79
+ options.password = node.mqttPassword || '';
80
+ }
81
+
82
+ try {
83
+ node._client = mqtt.connect(brokerUrl, options);
84
+
85
+ node._client.on('connect', function() {
86
+ node._connected = true;
87
+ node._lastErrorLog = 0;
88
+ node.log(`品牌MQTT已连接: ${brokerUrl}`);
89
+
90
+ // 订阅上报主题
91
+ node._client.subscribe(uploadTopic, function(err) {
92
+ if (!err) {
93
+ node.log(`已订阅: ${uploadTopic}`);
94
+ }
95
+ });
96
+
97
+ // 通知订阅者
98
+ notifySubscribers('connected');
99
+ });
100
+
101
+ node._client.on('message', function(topic, payload) {
102
+ try {
103
+ const data = JSON.parse(payload.toString());
104
+ if (data.payload) {
105
+ const { st, si, fn, fv } = data.payload;
106
+ if (st !== undefined && si !== undefined) {
107
+ // 发现设备
108
+ const deviceKey = `${st}_${si}`;
109
+ const deviceType = DEVICE_TYPES[st] || { name: `类型${st}`, type: 'unknown' };
110
+
111
+ if (!node._discoveredDevices.has(deviceKey)) {
112
+ node._discoveredDevices.set(deviceKey, {
113
+ deviceType: st,
114
+ deviceId: si,
115
+ typeName: deviceType.name,
116
+ meshType: deviceType.type,
117
+ lastSeen: Date.now(),
118
+ lastState: { fn, fv }
119
+ });
120
+ node.log(`发现品牌设备: ${deviceType.name} ID:${si}`);
121
+ } else {
122
+ const dev = node._discoveredDevices.get(deviceKey);
123
+ dev.lastSeen = Date.now();
124
+ dev.lastState = { fn, fv };
125
+ }
126
+
127
+ // 通知订阅者状态更新
128
+ notifySubscribers('state', { st, si, fn, fv });
129
+ }
130
+ }
131
+ } catch (e) {
132
+ // 静默处理解析错误
133
+ }
134
+ });
135
+
136
+ node._client.on('error', function(err) {
137
+ logErrorThrottled(`品牌MQTT错误: ${err.message}`);
138
+ });
139
+
140
+ node._client.on('close', function() {
141
+ node._connected = false;
142
+ notifySubscribers('disconnected');
143
+
144
+ if (!node._closing && !node._reconnectTimer) {
145
+ node._reconnectTimer = setTimeout(function() {
146
+ node._reconnectTimer = null;
147
+ if (node._client) {
148
+ try { node._client.end(true); } catch(e) {}
149
+ node._client = null;
150
+ }
151
+ connect();
152
+ }, 5000);
153
+ }
154
+ });
155
+
156
+ } catch (err) {
157
+ logErrorThrottled(`品牌MQTT连接失败: ${err.message}`);
158
+ }
159
+ }
160
+
161
+ // 通知订阅者
162
+ function notifySubscribers(event, data) {
163
+ node._subscribers.forEach(function(callback) {
164
+ try {
165
+ callback(event, data);
166
+ } catch (e) {}
167
+ });
168
+ }
169
+
170
+ // 发布控制命令
171
+ node.publish = function(st, si, fn, fv) {
172
+ if (!node._client || !node._connected) return false;
173
+
174
+ const downTopic = getDownTopic();
175
+ if (!downTopic) return false;
176
+
177
+ const payload = JSON.stringify({ payload: { st, si, fn, fv } });
178
+ node._client.publish(downTopic, payload);
179
+ return true;
180
+ };
181
+
182
+ // 订阅状态更新
183
+ node.subscribe = function(callback) {
184
+ node._subscribers.add(callback);
185
+ return function() {
186
+ node._subscribers.delete(callback);
187
+ };
188
+ };
189
+
190
+ // 获取已发现的设备列表
191
+ node.getDiscoveredDevices = function() {
192
+ return Array.from(node._discoveredDevices.values());
193
+ };
194
+
195
+ // 检查是否已连接
196
+ node.isConnected = function() {
197
+ return node._connected;
198
+ };
199
+
200
+ // 启动连接
201
+ if (node.projectCode && node.deviceSn) {
202
+ setTimeout(connect, 1000);
203
+ }
204
+
205
+ // 清理
206
+ node.on('close', function(done) {
207
+ node._closing = true;
208
+
209
+ if (node._reconnectTimer) {
210
+ clearTimeout(node._reconnectTimer);
211
+ node._reconnectTimer = null;
212
+ }
213
+
214
+ if (node._client) {
215
+ try {
216
+ node._client.end(true);
217
+ } catch (e) {}
218
+ node._client = null;
219
+ }
220
+
221
+ node._subscribers.clear();
222
+ node._discoveredDevices.clear();
223
+ done();
224
+ });
225
+ }
226
+
227
+ RED.nodes.registerType('symi-mqtt-brand', SymiMqttBrandNode);
228
+
229
+ // HTTP API - 获取已发现的设备
230
+ RED.httpAdmin.get('/symi-mqtt-brand/devices/:id', function(req, res) {
231
+ const node = RED.nodes.getNode(req.params.id);
232
+ if (node && typeof node.getDiscoveredDevices === 'function') {
233
+ res.json(node.getDiscoveredDevices());
234
+ } else {
235
+ res.json([]);
236
+ }
237
+ });
238
+ };