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.
- package/README.md +219 -48
- package/examples/basic-example.json +151 -0
- package/lib/device-manager.js +109 -23
- package/lib/mqtt-helper.js +25 -4
- package/lib/protocol.js +22 -14
- package/lib/tcp-client.js +15 -13
- package/nodes/rs485-debug.html +238 -0
- package/nodes/rs485-debug.js +220 -0
- package/nodes/symi-485-bridge.html +376 -0
- package/nodes/symi-485-bridge.js +776 -0
- package/nodes/symi-485-config.html +125 -0
- package/nodes/symi-485-config.js +275 -0
- package/nodes/symi-cloud-sync.html +4 -4
- package/nodes/symi-cloud-sync.js +43 -10
- package/nodes/symi-device.html +1 -0
- package/nodes/symi-device.js +23 -14
- package/nodes/symi-gateway.js +121 -39
- package/nodes/symi-mqtt.js +233 -49
- package/package.json +7 -4
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('symi-485-config', {
|
|
3
|
+
category: 'config',
|
|
4
|
+
defaults: {
|
|
5
|
+
name: { value: '' },
|
|
6
|
+
connectionType: { value: 'serial' },
|
|
7
|
+
host: { value: '' },
|
|
8
|
+
port: { value: 502 },
|
|
9
|
+
serialPort: { value: '' },
|
|
10
|
+
baudRate: { value: 9600 },
|
|
11
|
+
parity: { value: 'none' }
|
|
12
|
+
},
|
|
13
|
+
label: function() {
|
|
14
|
+
return this.name || (this.connectionType === 'tcp'
|
|
15
|
+
? 'RS485 (' + this.host + ':' + this.port + ')'
|
|
16
|
+
: 'RS485 (' + this.serialPort + ')');
|
|
17
|
+
},
|
|
18
|
+
oneditprepare: function() {
|
|
19
|
+
var node = this;
|
|
20
|
+
|
|
21
|
+
// 连接类型切换
|
|
22
|
+
$('#node-config-input-connectionType').on('change', function() {
|
|
23
|
+
if ($(this).val() === 'tcp') {
|
|
24
|
+
$('.tcp-config').show();
|
|
25
|
+
$('.serial-config').hide();
|
|
26
|
+
} else {
|
|
27
|
+
$('.tcp-config').hide();
|
|
28
|
+
$('.serial-config').show();
|
|
29
|
+
}
|
|
30
|
+
}).trigger('change');
|
|
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
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
$('#btn-refresh-ports').on('click', loadSerialPorts);
|
|
49
|
+
loadSerialPorts();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<script type="text/html" data-template-name="symi-485-config">
|
|
55
|
+
<div class="form-row">
|
|
56
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> 名称</label>
|
|
57
|
+
<input type="text" id="node-config-input-name" placeholder="如:客厅RS485">
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="form-row">
|
|
61
|
+
<label for="node-config-input-connectionType"><i class="fa fa-plug"></i> 连接方式</label>
|
|
62
|
+
<select id="node-config-input-connectionType">
|
|
63
|
+
<option value="serial">串口 (RS485)</option>
|
|
64
|
+
<option value="tcp">TCP/IP (Modbus TCP)</option>
|
|
65
|
+
</select>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="serial-config">
|
|
69
|
+
<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>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="form-row">
|
|
78
|
+
<label for="node-config-input-baudRate"><i class="fa fa-tachometer"></i> 波特率</label>
|
|
79
|
+
<select id="node-config-input-baudRate">
|
|
80
|
+
<option value="9600">9600</option>
|
|
81
|
+
<option value="19200">19200</option>
|
|
82
|
+
<option value="38400">38400</option>
|
|
83
|
+
<option value="57600">57600</option>
|
|
84
|
+
<option value="115200">115200</option>
|
|
85
|
+
</select>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="form-row">
|
|
89
|
+
<label for="node-config-input-parity"><i class="fa fa-check-square"></i> 校验位</label>
|
|
90
|
+
<select id="node-config-input-parity">
|
|
91
|
+
<option value="none">无 (None)</option>
|
|
92
|
+
<option value="even">偶校验 (Even)</option>
|
|
93
|
+
<option value="odd">奇校验 (Odd)</option>
|
|
94
|
+
</select>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="tcp-config" style="display:none">
|
|
99
|
+
<div class="form-row">
|
|
100
|
+
<label for="node-config-input-host"><i class="fa fa-server"></i> 主机地址</label>
|
|
101
|
+
<input type="text" id="node-config-input-host" placeholder="192.168.1.100">
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div class="form-row">
|
|
105
|
+
<label for="node-config-input-port"><i class="fa fa-random"></i> 端口</label>
|
|
106
|
+
<input type="number" id="node-config-input-port" placeholder="502">
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<script type="text/html" data-help-name="symi-485-config">
|
|
112
|
+
<p>RS485/Modbus连接配置节点</p>
|
|
113
|
+
<h3>连接方式</h3>
|
|
114
|
+
<dl>
|
|
115
|
+
<dt>串口 (RS485)</dt>
|
|
116
|
+
<dd>通过USB转RS485模块连接,选择对应的串口</dd>
|
|
117
|
+
<dt>TCP/IP (Modbus TCP)</dt>
|
|
118
|
+
<dd>通过RS485转TCP网关连接,输入IP和端口</dd>
|
|
119
|
+
</dl>
|
|
120
|
+
<h3>串口参数</h3>
|
|
121
|
+
<ul>
|
|
122
|
+
<li>波特率:常用9600、19200、115200</li>
|
|
123
|
+
<li>校验位:根据设备要求选择</li>
|
|
124
|
+
</ul>
|
|
125
|
+
</script>
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symi RS485 Config Node - RS485/Modbus连接配置节点
|
|
3
|
+
* 使用与Mesh网关相同的技术栈(SerialPort/net)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { SerialPort } = require('serialport');
|
|
7
|
+
const net = require('net');
|
|
8
|
+
|
|
9
|
+
module.exports = function(RED) {
|
|
10
|
+
|
|
11
|
+
function SymiRS485ConfigNode(config) {
|
|
12
|
+
RED.nodes.createNode(this, config);
|
|
13
|
+
const node = this;
|
|
14
|
+
|
|
15
|
+
// 配置
|
|
16
|
+
node.name = config.name || '';
|
|
17
|
+
node.connectionType = config.connectionType || 'serial';
|
|
18
|
+
node.host = config.host || '';
|
|
19
|
+
node.port = parseInt(config.port) || 502;
|
|
20
|
+
node.serialPort = config.serialPort || '';
|
|
21
|
+
node.baudRate = parseInt(config.baudRate) || 9600;
|
|
22
|
+
node.parity = config.parity || 'none';
|
|
23
|
+
|
|
24
|
+
// 连接状态
|
|
25
|
+
node.client = null;
|
|
26
|
+
node.connected = false;
|
|
27
|
+
node.receiveBuffer = Buffer.alloc(0);
|
|
28
|
+
node.users = [];
|
|
29
|
+
|
|
30
|
+
// 注册使用者
|
|
31
|
+
node.register = function(userNode) {
|
|
32
|
+
if (!node.users.includes(userNode)) {
|
|
33
|
+
node.users.push(userNode);
|
|
34
|
+
}
|
|
35
|
+
if (node.users.length === 1) {
|
|
36
|
+
node.connect();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// 注销使用者
|
|
41
|
+
node.deregister = function(userNode) {
|
|
42
|
+
const idx = node.users.indexOf(userNode);
|
|
43
|
+
if (idx >= 0) {
|
|
44
|
+
node.users.splice(idx, 1);
|
|
45
|
+
}
|
|
46
|
+
if (node.users.length === 0) {
|
|
47
|
+
node.disconnect();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// 连接
|
|
52
|
+
node.connect = function() {
|
|
53
|
+
if (node.connected) return;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
if (node.connectionType === 'serial') {
|
|
57
|
+
if (!node.serialPort) {
|
|
58
|
+
node.error('未配置串口');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
node.client = new SerialPort({
|
|
63
|
+
path: node.serialPort,
|
|
64
|
+
baudRate: node.baudRate,
|
|
65
|
+
dataBits: 8,
|
|
66
|
+
stopBits: 1,
|
|
67
|
+
parity: node.parity,
|
|
68
|
+
autoOpen: false
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
node.client.on('open', () => {
|
|
72
|
+
node.connected = true;
|
|
73
|
+
node.log(`RS485串口已连接: ${node.serialPort}`);
|
|
74
|
+
node.emit('connected');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
node.client.on('data', (data) => {
|
|
78
|
+
// 触发原始数据事件(用于调试节点)
|
|
79
|
+
node.emit('data', data);
|
|
80
|
+
// 帧解析
|
|
81
|
+
node.handleData(data);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
node.client.on('error', (err) => {
|
|
85
|
+
node.error(`RS485串口错误: ${err.message}`);
|
|
86
|
+
node.emit('error', err);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
node.client.on('close', () => {
|
|
90
|
+
if (node.connected) {
|
|
91
|
+
node.connected = false;
|
|
92
|
+
node.log('RS485串口已断开');
|
|
93
|
+
node.emit('disconnected');
|
|
94
|
+
}
|
|
95
|
+
// 自动重连(静默)
|
|
96
|
+
if (node.users.length > 0 && !node._reconnecting) {
|
|
97
|
+
node._reconnecting = true;
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
node._reconnecting = false;
|
|
100
|
+
node.connect();
|
|
101
|
+
}, 5000);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
node.client.open();
|
|
106
|
+
|
|
107
|
+
} else if (node.connectionType === 'tcp') {
|
|
108
|
+
if (!node.host) {
|
|
109
|
+
node.error('未配置TCP主机');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
node.client = new net.Socket();
|
|
114
|
+
|
|
115
|
+
node.client.on('connect', () => {
|
|
116
|
+
node.connected = true;
|
|
117
|
+
node.warn(`[RS485] TCP已连接: ${node.host}:${node.port}`);
|
|
118
|
+
node.emit('connected');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
node.client.on('data', (data) => {
|
|
122
|
+
// 触发原始数据事件(用于调试节点)
|
|
123
|
+
node.emit('data', data);
|
|
124
|
+
// 帧解析
|
|
125
|
+
node.handleData(data);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
node.client.on('error', (err) => {
|
|
129
|
+
node.error(`RS485 TCP错误: ${err.message}`);
|
|
130
|
+
node.emit('error', err);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
node.client.on('close', () => {
|
|
134
|
+
if (node.connected) {
|
|
135
|
+
node.connected = false;
|
|
136
|
+
node.log('RS485 TCP已断开');
|
|
137
|
+
node.emit('disconnected');
|
|
138
|
+
}
|
|
139
|
+
// 自动重连(静默)
|
|
140
|
+
if (node.users.length > 0 && !node._reconnecting) {
|
|
141
|
+
node._reconnecting = true;
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
node._reconnecting = false;
|
|
144
|
+
node.connect();
|
|
145
|
+
}, 5000);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
node.client.connect(node.port, node.host);
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
node.error(`RS485连接失败: ${err.message}`);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// 断开连接
|
|
157
|
+
node.disconnect = function() {
|
|
158
|
+
if (node.client) {
|
|
159
|
+
try {
|
|
160
|
+
if (node.connectionType === 'serial') {
|
|
161
|
+
node.client.close();
|
|
162
|
+
} else {
|
|
163
|
+
node.client.destroy();
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {}
|
|
166
|
+
node.client = null;
|
|
167
|
+
}
|
|
168
|
+
node.connected = false;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// 处理接收数据(仅做帧解析,原始数据已在上层emit)
|
|
172
|
+
node.handleData = function(data) {
|
|
173
|
+
node.receiveBuffer = Buffer.concat([node.receiveBuffer, data]);
|
|
174
|
+
|
|
175
|
+
// 防止缓冲区过大(最大8KB)
|
|
176
|
+
if (node.receiveBuffer.length > 8192) {
|
|
177
|
+
node.receiveBuffer = node.receiveBuffer.subarray(-2048);
|
|
178
|
+
node.warn('接收缓冲区溢出,已截断');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Modbus RTU最小帧长度为4字节
|
|
182
|
+
while (node.receiveBuffer.length >= 4) {
|
|
183
|
+
const frameLen = node.getFrameLength(node.receiveBuffer);
|
|
184
|
+
if (frameLen === 0 || node.receiveBuffer.length < frameLen) break;
|
|
185
|
+
|
|
186
|
+
const frame = node.receiveBuffer.subarray(0, frameLen);
|
|
187
|
+
node.receiveBuffer = node.receiveBuffer.subarray(frameLen);
|
|
188
|
+
|
|
189
|
+
if (node.validateCRC(frame)) {
|
|
190
|
+
node.emit('frame', frame);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// 获取帧长度
|
|
196
|
+
node.getFrameLength = function(buffer) {
|
|
197
|
+
if (buffer.length < 2) return 0;
|
|
198
|
+
const fc = buffer[1];
|
|
199
|
+
if (fc === 0x03 || fc === 0x04) {
|
|
200
|
+
if (buffer.length < 3) return 0;
|
|
201
|
+
return 3 + buffer[2] + 2;
|
|
202
|
+
} else if (fc === 0x05 || fc === 0x06) {
|
|
203
|
+
return 8;
|
|
204
|
+
} else if (fc >= 0x80) {
|
|
205
|
+
return 5;
|
|
206
|
+
}
|
|
207
|
+
return 8;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// 验证CRC
|
|
211
|
+
node.validateCRC = function(frame) {
|
|
212
|
+
if (frame.length < 4) return false;
|
|
213
|
+
const data = frame.subarray(0, frame.length - 2);
|
|
214
|
+
const receivedCRC = frame.readUInt16LE(frame.length - 2);
|
|
215
|
+
const calculatedCRC = node.calculateCRC16(data);
|
|
216
|
+
return receivedCRC === calculatedCRC;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// CRC16计算
|
|
220
|
+
node.calculateCRC16 = function(buffer) {
|
|
221
|
+
let crc = 0xFFFF;
|
|
222
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
223
|
+
crc ^= buffer[i];
|
|
224
|
+
for (let j = 0; j < 8; j++) {
|
|
225
|
+
if (crc & 0x0001) {
|
|
226
|
+
crc = (crc >> 1) ^ 0xA001;
|
|
227
|
+
} else {
|
|
228
|
+
crc >>= 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return crc;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// 发送数据
|
|
236
|
+
node.send = function(data) {
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
if (!node.connected || !node.client) {
|
|
239
|
+
reject(new Error('RS485未连接'));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
243
|
+
node.client.write(buffer, (err) => {
|
|
244
|
+
if (err) {
|
|
245
|
+
reject(err);
|
|
246
|
+
} else {
|
|
247
|
+
// 发送tx事件用于调试
|
|
248
|
+
node.emit('tx', buffer);
|
|
249
|
+
resolve();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// 构建并发送Modbus帧
|
|
256
|
+
node.writeRegister = function(slaveAddr, registerAddr, value) {
|
|
257
|
+
const buffer = Buffer.alloc(8);
|
|
258
|
+
buffer.writeUInt8(slaveAddr, 0);
|
|
259
|
+
buffer.writeUInt8(0x06, 1); // 写单个寄存器
|
|
260
|
+
buffer.writeUInt16BE(registerAddr, 2);
|
|
261
|
+
buffer.writeUInt16BE(value, 4);
|
|
262
|
+
const crc = node.calculateCRC16(buffer.subarray(0, 6));
|
|
263
|
+
buffer.writeUInt16LE(crc, 6);
|
|
264
|
+
return node.send(buffer);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// 清理
|
|
268
|
+
node.on('close', (done) => {
|
|
269
|
+
node.disconnect();
|
|
270
|
+
done();
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
RED.nodes.registerType('symi-485-config', SymiRS485ConfigNode);
|
|
275
|
+
};
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
name: { value: '' },
|
|
7
7
|
gateway: { value: '', type: 'symi-gateway', required: true },
|
|
8
8
|
mqttConfig: { value: '', type: 'symi-mqtt' },
|
|
9
|
-
appId: { value: '
|
|
10
|
-
appSecret: { value: '
|
|
9
|
+
appId: { value: '', required: false },
|
|
10
|
+
appSecret: { value: '', required: false },
|
|
11
11
|
hotelId: { value: '' },
|
|
12
12
|
roomNo: { value: '' },
|
|
13
13
|
roomUuid: { value: '' },
|
|
@@ -314,12 +314,12 @@
|
|
|
314
314
|
|
|
315
315
|
<div class="form-row">
|
|
316
316
|
<label for="node-input-appId"><i class="fa fa-key"></i> App ID</label>
|
|
317
|
-
<input type="text" id="node-input-appId" placeholder="
|
|
317
|
+
<input type="text" id="node-input-appId" placeholder="请输入App ID">
|
|
318
318
|
</div>
|
|
319
319
|
|
|
320
320
|
<div class="form-row">
|
|
321
321
|
<label for="node-input-appSecret"><i class="fa fa-lock"></i> App Secret</label>
|
|
322
|
-
<input type="password" id="node-input-appSecret" placeholder="
|
|
322
|
+
<input type="password" id="node-input-appSecret" placeholder="请输入App Secret">
|
|
323
323
|
</div>
|
|
324
324
|
|
|
325
325
|
<div class="form-row">
|
package/nodes/symi-cloud-sync.js
CHANGED
|
@@ -154,12 +154,12 @@ module.exports = function(RED) {
|
|
|
154
154
|
node.log(`设备名称已更新: ${oldName} -> ${newName} (MAC: ${cloudDevice.mac})`);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
//
|
|
157
|
+
// 同步按键完整配置(名称、类型、场景ID等)
|
|
158
158
|
if (cloudDevice.sub_device && Array.isArray(cloudDevice.sub_device) && cloudDevice.sub_device.length > 0) {
|
|
159
159
|
const newSubNames = cloudDevice.sub_device.map(sub => sub.sub_name);
|
|
160
160
|
const oldSubNames = localDevice.subDeviceNames || [];
|
|
161
161
|
|
|
162
|
-
node.log(`[
|
|
162
|
+
node.log(`[按键配置] 设备: ${localDevice.name}, 云端按键数: ${cloudDevice.sub_device.length}, 按键名称: [${newSubNames.join(', ')}]`);
|
|
163
163
|
|
|
164
164
|
// 检查是否有变化
|
|
165
165
|
const hasChanged = newSubNames.length !== oldSubNames.length ||
|
|
@@ -168,12 +168,40 @@ module.exports = function(RED) {
|
|
|
168
168
|
if (hasChanged) {
|
|
169
169
|
localDevice.subDeviceNames = newSubNames;
|
|
170
170
|
deviceUpdated = true;
|
|
171
|
-
node.log(`[
|
|
171
|
+
node.log(`[按键配置] 设备按键名称已更新: ${localDevice.name} -> [${newSubNames.join(', ')}]`);
|
|
172
172
|
} else {
|
|
173
|
-
node.log(`[
|
|
173
|
+
node.log(`[按键配置] 设备按键名称无变化: ${localDevice.name}`);
|
|
174
174
|
}
|
|
175
|
+
|
|
176
|
+
// 存储完整的按键配置(包含场景ID等信息)
|
|
177
|
+
localDevice.subDeviceConfigs = cloudDevice.sub_device.map(sub => ({
|
|
178
|
+
sub_name: sub.sub_name,
|
|
179
|
+
sub_type: sub.sub_type, // "普通", "场景", "双控", "总控"
|
|
180
|
+
switch_type: sub.switch_type, // "normal", "light"
|
|
181
|
+
scene_id: sub.scene_id, // 场景按键的场景ID
|
|
182
|
+
on_scene_id: sub.on_scene_id, // 双控/总控的开场景ID
|
|
183
|
+
off_scene_id: sub.off_scene_id // 双控/总控的关场景ID
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
// 记录场景绑定信息
|
|
187
|
+
const sceneBindings = cloudDevice.sub_device
|
|
188
|
+
.map((sub, idx) => {
|
|
189
|
+
if (sub.sub_type === '场景' && sub.scene_id) {
|
|
190
|
+
return `按键${idx + 1}(${sub.sub_name})→场景${sub.scene_id}`;
|
|
191
|
+
} else if ((sub.sub_type === '双控' || sub.sub_type === '总控') && (sub.on_scene_id || sub.off_scene_id)) {
|
|
192
|
+
return `按键${idx + 1}(${sub.sub_name})→开:场景${sub.on_scene_id || '无'}/关:场景${sub.off_scene_id || '无'}`;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
})
|
|
196
|
+
.filter(Boolean);
|
|
197
|
+
|
|
198
|
+
if (sceneBindings.length > 0) {
|
|
199
|
+
node.log(`[场景绑定] ${localDevice.name}: ${sceneBindings.join(', ')}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
deviceUpdated = true;
|
|
175
203
|
} else {
|
|
176
|
-
node.log(`[
|
|
204
|
+
node.log(`[按键配置] 设备无sub_device数据: ${localDevice.name}`);
|
|
177
205
|
}
|
|
178
206
|
|
|
179
207
|
if (deviceUpdated) {
|
|
@@ -201,8 +229,8 @@ module.exports = function(RED) {
|
|
|
201
229
|
|
|
202
230
|
if (updatedCount > 0 && node.mqttConfig) {
|
|
203
231
|
setTimeout(() => {
|
|
204
|
-
node.log('重新发布MQTT Discovery
|
|
205
|
-
node.mqttConfig.publishAllDiscovery(devices);
|
|
232
|
+
node.log('重新发布MQTT Discovery配置(名称已更新,强制更新)');
|
|
233
|
+
node.mqttConfig.publishAllDiscovery(devices, true); // forceUpdate=true
|
|
206
234
|
}, 1000);
|
|
207
235
|
}
|
|
208
236
|
};
|
|
@@ -223,11 +251,14 @@ module.exports = function(RED) {
|
|
|
223
251
|
const config = generateSceneButtonConfig(scene, roomNo, mqttPrefix);
|
|
224
252
|
|
|
225
253
|
setTimeout(() => {
|
|
254
|
+
node.log(`[场景按钮] 发布: ${scene.scene_name}, topic=${config.topic}`);
|
|
255
|
+
node.debug(`[场景按钮] payload=${config.payload}`);
|
|
256
|
+
|
|
226
257
|
mqttClient.publish(config.topic, config.payload, { retain: true }, (err) => {
|
|
227
258
|
if (err) {
|
|
228
259
|
node.error(`发布场景按钮失败: ${scene.scene_name}, ${err.message}`);
|
|
229
260
|
} else {
|
|
230
|
-
node.
|
|
261
|
+
node.log(`场景按钮已发布: ${scene.scene_name}`);
|
|
231
262
|
}
|
|
232
263
|
});
|
|
233
264
|
}, index * 100);
|
|
@@ -256,6 +287,8 @@ module.exports = function(RED) {
|
|
|
256
287
|
if (node.mqttConfig) {
|
|
257
288
|
node.mqttConfig.on('scene-trigger', (topic, message) => {
|
|
258
289
|
const roomNo = node.roomNo || 'unknown';
|
|
290
|
+
|
|
291
|
+
// 直接使用原始roomNo匹配topic
|
|
259
292
|
if (topic.startsWith(`symi_mesh/room_${roomNo}/scene/`) && topic.endsWith('/trigger')) {
|
|
260
293
|
const sceneIdMatch = topic.match(/scene\/(\d+)\/trigger/);
|
|
261
294
|
if (sceneIdMatch) {
|
|
@@ -272,9 +305,9 @@ module.exports = function(RED) {
|
|
|
272
305
|
node.log(`[场景控制] 场景控制帧: ${frame.toString('hex').toUpperCase()}`);
|
|
273
306
|
|
|
274
307
|
node.gateway.sendScene(sceneId).then(() => {
|
|
275
|
-
node.log(`[场景控制]
|
|
308
|
+
node.log(`[场景控制] 场景控制命令已发送: ${scene.scene_name} (ID: ${sceneId})`);
|
|
276
309
|
}).catch(err => {
|
|
277
|
-
node.error(`[场景控制]
|
|
310
|
+
node.error(`[场景控制] 场景控制命令发送失败: ${err.message}`);
|
|
278
311
|
});
|
|
279
312
|
} else {
|
|
280
313
|
node.error('[场景控制] 网关未连接,无法执行场景');
|
package/nodes/symi-device.html
CHANGED
package/nodes/symi-device.js
CHANGED
|
@@ -37,21 +37,30 @@ module.exports = function(RED) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
node.device = null;
|
|
40
|
-
|
|
40
|
+
|
|
41
|
+
// 检查设备是否已配置
|
|
42
|
+
if (!node.deviceMac) {
|
|
43
|
+
node.status({ fill: 'grey', shape: 'ring', text: '请选择设备' });
|
|
44
|
+
} else {
|
|
45
|
+
node.status({ fill: 'yellow', shape: 'ring', text: '等待连接' });
|
|
46
|
+
}
|
|
41
47
|
|
|
42
48
|
const updateDevice = () => {
|
|
43
|
-
if (node.deviceMac) {
|
|
44
|
-
node.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
if (!node.deviceMac) {
|
|
50
|
+
node.status({ fill: 'grey', shape: 'ring', text: '请选择设备' });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
node.device = node.gateway.getDevice(node.deviceMac);
|
|
55
|
+
if (node.device) {
|
|
56
|
+
const channelInfo = node.device.channels > 1 ? ` 第${node.channel}路` : '';
|
|
57
|
+
node.status({
|
|
58
|
+
fill: 'green',
|
|
59
|
+
shape: 'dot',
|
|
60
|
+
text: `已连接${channelInfo}`
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
node.status({ fill: 'yellow', shape: 'ring', text: '等待设备' });
|
|
55
64
|
}
|
|
56
65
|
};
|
|
57
66
|
|
|
@@ -134,7 +143,7 @@ module.exports = function(RED) {
|
|
|
134
143
|
let command = null;
|
|
135
144
|
|
|
136
145
|
if (entityType === 'switch') {
|
|
137
|
-
const channel = parseInt(msg.channel) ||
|
|
146
|
+
const channel = parseInt(msg.channel) || this.channel || 1;
|
|
138
147
|
let value;
|
|
139
148
|
|
|
140
149
|
if (typeof payload === 'boolean') {
|