node-red-contrib-symi-mesh 1.2.4 → 1.3.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 +311 -13
- package/lib/cloud-api.js +96 -0
- package/lib/device-manager.js +7 -0
- package/lib/mqtt-helper.js +33 -2
- package/lib/protocol.js +19 -2
- package/nodes/symi-cloud-sync.html +430 -0
- package/nodes/symi-cloud-sync.js +388 -0
- package/nodes/symi-device.html +7 -1
- package/nodes/symi-gateway.html +3 -3
- package/nodes/symi-gateway.js +104 -19
- package/nodes/symi-mqtt.html +28 -23
- package/nodes/symi-mqtt.js +46 -16
- package/package.json +5 -3
package/nodes/symi-mqtt.html
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script type="text/javascript">
|
|
2
2
|
RED.nodes.registerType('symi-mqtt', {
|
|
3
|
-
category: '
|
|
4
|
-
color: '#DB4437',
|
|
3
|
+
category: 'config',
|
|
5
4
|
defaults: {
|
|
6
5
|
name: { value: '' },
|
|
7
6
|
gateway: { value: '', type: 'symi-gateway', required: true },
|
|
@@ -10,44 +9,50 @@
|
|
|
10
9
|
mqttPassword: { value: '' },
|
|
11
10
|
mqttPrefix: { value: 'homeassistant' }
|
|
12
11
|
},
|
|
13
|
-
inputs: 0,
|
|
14
|
-
outputs: 0,
|
|
15
|
-
icon: 'bridge-dash.png',
|
|
16
12
|
label: function() {
|
|
17
|
-
return this.name || 'MQTT
|
|
13
|
+
return this.name || 'MQTT配置';
|
|
14
|
+
},
|
|
15
|
+
oneditprepare: function() {
|
|
16
|
+
// 自动填充默认值到输入框
|
|
17
|
+
if (!this.mqttBroker || this.mqttBroker === '') {
|
|
18
|
+
$('#node-config-input-mqttBroker').val('mqtt://localhost:1883');
|
|
19
|
+
}
|
|
20
|
+
if (!this.mqttPrefix || this.mqttPrefix === '') {
|
|
21
|
+
$('#node-config-input-mqttPrefix').val('homeassistant');
|
|
22
|
+
}
|
|
18
23
|
}
|
|
19
24
|
});
|
|
20
25
|
</script>
|
|
21
26
|
|
|
22
27
|
<script type="text/html" data-template-name="symi-mqtt">
|
|
23
28
|
<div class="form-row">
|
|
24
|
-
<label for="node-input-name"><i class="fa fa-tag"></i> 名称</label>
|
|
25
|
-
<input type="text" id="node-input-name" placeholder="MQTT
|
|
29
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> 名称</label>
|
|
30
|
+
<input type="text" id="node-config-input-name" placeholder="MQTT配置">
|
|
26
31
|
</div>
|
|
27
|
-
|
|
32
|
+
|
|
28
33
|
<div class="form-row">
|
|
29
|
-
<label for="node-input-gateway"><i class="fa fa-server"></i> 网关</label>
|
|
30
|
-
<input type="text" id="node-input-gateway">
|
|
34
|
+
<label for="node-config-input-gateway"><i class="fa fa-server"></i> 网关</label>
|
|
35
|
+
<input type="text" id="node-config-input-gateway">
|
|
31
36
|
</div>
|
|
32
|
-
|
|
37
|
+
|
|
33
38
|
<div class="form-row">
|
|
34
|
-
<label for="node-input-mqttBroker"><i class="fa fa-rss"></i> MQTT地址</label>
|
|
35
|
-
<input type="text" id="node-input-mqttBroker" placeholder="mqtt://localhost:1883">
|
|
39
|
+
<label for="node-config-input-mqttBroker"><i class="fa fa-rss"></i> MQTT地址</label>
|
|
40
|
+
<input type="text" id="node-config-input-mqttBroker" placeholder="mqtt://localhost:1883">
|
|
36
41
|
</div>
|
|
37
|
-
|
|
42
|
+
|
|
38
43
|
<div class="form-row">
|
|
39
|
-
<label for="node-input-mqttUsername"><i class="fa fa-user"></i> 用户名</label>
|
|
40
|
-
<input type="text" id="node-input-mqttUsername" placeholder="可选">
|
|
44
|
+
<label for="node-config-input-mqttUsername"><i class="fa fa-user"></i> 用户名</label>
|
|
45
|
+
<input type="text" id="node-config-input-mqttUsername" placeholder="可选">
|
|
41
46
|
</div>
|
|
42
|
-
|
|
47
|
+
|
|
43
48
|
<div class="form-row">
|
|
44
|
-
<label for="node-input-mqttPassword"><i class="fa fa-lock"></i> 密码</label>
|
|
45
|
-
<input type="password" id="node-input-mqttPassword" placeholder="可选">
|
|
49
|
+
<label for="node-config-input-mqttPassword"><i class="fa fa-lock"></i> 密码</label>
|
|
50
|
+
<input type="password" id="node-config-input-mqttPassword" placeholder="可选">
|
|
46
51
|
</div>
|
|
47
|
-
|
|
52
|
+
|
|
48
53
|
<div class="form-row">
|
|
49
|
-
<label for="node-input-mqttPrefix"><i class="fa fa-folder-open"></i> HA前缀</label>
|
|
50
|
-
<input type="text" id="node-input-mqttPrefix" placeholder="homeassistant">
|
|
54
|
+
<label for="node-config-input-mqttPrefix"><i class="fa fa-folder-open"></i> HA前缀</label>
|
|
55
|
+
<input type="text" id="node-config-input-mqttPrefix" placeholder="homeassistant">
|
|
51
56
|
</div>
|
|
52
57
|
</script>
|
|
53
58
|
|
package/nodes/symi-mqtt.js
CHANGED
|
@@ -10,18 +10,20 @@ module.exports = function(RED) {
|
|
|
10
10
|
function SymiMQTTNode(config) {
|
|
11
11
|
RED.nodes.createNode(this, config);
|
|
12
12
|
const node = this;
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
node.gateway = RED.nodes.getNode(config.gateway);
|
|
15
|
-
|
|
16
|
-
node.mqttUsername = config.mqttUsername;
|
|
17
|
-
node.mqttPassword = config.mqttPassword;
|
|
18
|
-
node.mqttPrefix = config.mqttPrefix || 'homeassistant';
|
|
19
|
-
|
|
15
|
+
|
|
20
16
|
if (!node.gateway) {
|
|
21
17
|
node.error('未配置网关');
|
|
22
18
|
node.status({ fill: 'red', shape: 'ring', text: '未配置网关' });
|
|
23
19
|
return;
|
|
24
20
|
}
|
|
21
|
+
|
|
22
|
+
// 使用配置的MQTT参数,如果为空则使用默认值
|
|
23
|
+
node.mqttBroker = config.mqttBroker || 'mqtt://localhost:1883';
|
|
24
|
+
node.mqttUsername = config.mqttUsername;
|
|
25
|
+
node.mqttPassword = config.mqttPassword;
|
|
26
|
+
node.mqttPrefix = config.mqttPrefix || 'homeassistant';
|
|
25
27
|
|
|
26
28
|
node.mqttClient = null;
|
|
27
29
|
node.publishedDevices = new Set();
|
|
@@ -209,6 +211,24 @@ module.exports = function(RED) {
|
|
|
209
211
|
});
|
|
210
212
|
};
|
|
211
213
|
|
|
214
|
+
SymiMQTTNode.prototype.subscribeSceneTriggers = function(roomNo) {
|
|
215
|
+
const node = this;
|
|
216
|
+
const topic = `symi_mesh/room_${roomNo}/scene/+/trigger`;
|
|
217
|
+
|
|
218
|
+
if (!node.mqttClient || !node.mqttClient.connected) {
|
|
219
|
+
node.warn('MQTT客户端未连接,无法订阅场景触发');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
node.mqttClient.subscribe(topic, (err) => {
|
|
224
|
+
if (err) {
|
|
225
|
+
node.error(`订阅场景触发主题失败: ${err.message}`);
|
|
226
|
+
} else {
|
|
227
|
+
node.log(`已订阅场景触发主题: ${topic}`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
212
232
|
// 发布设备初始状态
|
|
213
233
|
function publishInitialDeviceState(device, node) {
|
|
214
234
|
const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
|
|
@@ -311,7 +331,10 @@ module.exports = function(RED) {
|
|
|
311
331
|
|
|
312
332
|
switch (attrType) {
|
|
313
333
|
case 0x02:
|
|
334
|
+
case 0x45:
|
|
314
335
|
// 开关状态反馈 (TYPE_ON_OFF)
|
|
336
|
+
// 0x02: 1-4路开关状态反馈
|
|
337
|
+
// 0x45: 6路开关状态反馈(场景执行时使用)
|
|
315
338
|
// 窗帘(type=5)不使用0x02,忽略避免误操作
|
|
316
339
|
if (device.deviceType === 5) {
|
|
317
340
|
node.debug(`窗帘设备忽略0x02消息(窗帘使用0x05/0x06)`);
|
|
@@ -852,13 +875,20 @@ module.exports = function(RED) {
|
|
|
852
875
|
|
|
853
876
|
SymiMQTTNode.prototype.handleMQTTMessage = function(topic, message) {
|
|
854
877
|
const node = this;
|
|
878
|
+
|
|
879
|
+
// 检查是否是场景触发消息
|
|
880
|
+
if (topic.match(/symi_mesh\/room_\w+\/scene\/\d+\/trigger/)) {
|
|
881
|
+
node.emit('scene-trigger', topic, message);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
|
|
855
885
|
const deviceMac = node.subscriptions.get(topic);
|
|
856
|
-
|
|
886
|
+
|
|
857
887
|
if (!deviceMac) {
|
|
858
888
|
node.warn(`未找到topic订阅: ${topic}`);
|
|
859
889
|
return;
|
|
860
890
|
}
|
|
861
|
-
|
|
891
|
+
|
|
862
892
|
const device = node.gateway.getDevice(deviceMac);
|
|
863
893
|
if (!device) {
|
|
864
894
|
node.warn(`设备未找到: ${deviceMac}`);
|
|
@@ -866,24 +896,24 @@ module.exports = function(RED) {
|
|
|
866
896
|
}
|
|
867
897
|
|
|
868
898
|
const payload = message.toString();
|
|
869
|
-
node.
|
|
870
|
-
|
|
899
|
+
node.log(`[MQTT收到] topic=${topic}, payload=${payload}, deviceType=${device.deviceType}, isThreeInOne=${device.isThreeInOne}`);
|
|
900
|
+
|
|
871
901
|
const commands = node.parseMQTTCommand(topic, payload, device);
|
|
872
|
-
|
|
902
|
+
|
|
873
903
|
if (commands && commands.length > 0) {
|
|
874
|
-
node.
|
|
904
|
+
node.log(`[MQTT解析] 解析出${commands.length}个命令:`);
|
|
875
905
|
commands.forEach((cmd, idx) => {
|
|
876
|
-
node.
|
|
906
|
+
node.log(` 命令${idx + 1}: attrType=0x${cmd.attrType.toString(16).toUpperCase()}, param=[${Array.from(cmd.param).map(p => '0x' + p.toString(16).toUpperCase()).join(', ')}]`);
|
|
877
907
|
});
|
|
878
|
-
|
|
908
|
+
|
|
879
909
|
// 使用for循环而不是forEach以正确处理async
|
|
880
910
|
(async () => {
|
|
881
911
|
for (const command of commands) {
|
|
882
912
|
const paramHex = Array.from(command.param).map(p => '0x' + p.toString(16).toUpperCase()).join(' ');
|
|
883
|
-
node.
|
|
913
|
+
node.log(`[MQTT发送] → 网关: addr=0x${device.networkAddress.toString(16).toUpperCase()}, attr=0x${command.attrType.toString(16).toUpperCase()}, param=[${paramHex}]`);
|
|
884
914
|
try {
|
|
885
915
|
await node.gateway.sendControl(device.networkAddress, command.attrType, command.param);
|
|
886
|
-
node.
|
|
916
|
+
node.log(`[MQTT发送] ✓ 成功: ${device.name}`);
|
|
887
917
|
|
|
888
918
|
// 立即发布状态更新(optimistic update)
|
|
889
919
|
node.publishCommandFeedback(device, command, payload, topic);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-mesh",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
"nodes": {
|
|
31
31
|
"symi-gateway": "nodes/symi-gateway.js",
|
|
32
32
|
"symi-device": "nodes/symi-device.js",
|
|
33
|
-
"symi-mqtt": "nodes/symi-mqtt.js"
|
|
33
|
+
"symi-mqtt": "nodes/symi-mqtt.js",
|
|
34
|
+
"symi-cloud-sync": "nodes/symi-cloud-sync.js"
|
|
34
35
|
}
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
38
|
+
"axios": "^1.13.2",
|
|
37
39
|
"mqtt": "^5.3.0",
|
|
38
40
|
"serialport": "^12.0.0"
|
|
39
41
|
},
|