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.
- package/README.md +313 -329
- package/examples/knx-sync-example.json +48 -410
- package/lib/device-manager.js +30 -3
- package/lib/mqtt-helper.js +3 -3
- package/lib/tcp-client.js +33 -13
- package/nodes/symi-485-bridge.html +15 -3
- package/nodes/symi-485-bridge.js +747 -206
- package/nodes/symi-485-config.js +90 -5
- package/nodes/symi-device.js +7 -7
- package/nodes/symi-gateway.js +86 -14
- package/nodes/symi-knx-bridge.html +368 -0
- package/nodes/symi-knx-bridge.js +1065 -0
- package/nodes/symi-mqtt.js +74 -25
- package/package.json +5 -4
package/nodes/symi-mqtt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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:
|
|
693
|
+
payload: state.curtainAction
|
|
651
694
|
});
|
|
652
|
-
node.debug(`发布窗帘运行状态: ${
|
|
695
|
+
node.debug(`发布窗帘运行状态: ${state.curtainAction} (原始值=${state.curtainStatus}, 协议=${state.curtainProtocol || 'unknown'})`);
|
|
653
696
|
|
|
654
|
-
//
|
|
655
|
-
if (state.
|
|
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(
|
|
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
|
-
|
|
676
|
-
|
|
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:
|
|
722
|
+
payload: state.curtainAction
|
|
681
723
|
});
|
|
682
|
-
node.debug(`同时发布窗帘状态: ${
|
|
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.
|
|
3
|
+
"version": "1.6.7",
|
|
4
4
|
"description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
|
|
5
|
-
"main": "
|
|
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.
|
|
43
|
+
"mqtt": "^5.10.0",
|
|
43
44
|
"serialport": "^12.0.0"
|
|
44
45
|
},
|
|
45
46
|
"engines": {
|