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
package/nodes/symi-mqtt.js
CHANGED
|
@@ -33,43 +33,160 @@ module.exports = function(RED) {
|
|
|
33
33
|
|
|
34
34
|
node.connectMQTT();
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// 存储事件处理函数引用,便于关闭时移除
|
|
37
|
+
node._handlers = {};
|
|
38
|
+
|
|
39
|
+
node._handlers.deviceListComplete = (devices) => {
|
|
37
40
|
if (node.mqttClient && node.mqttClient.connected) {
|
|
41
|
+
// 延迟3秒等待云端同步完成后再发布Discovery
|
|
38
42
|
setTimeout(() => {
|
|
39
43
|
node.log(`网关设备列表同步完成,发布${devices.length}个设备到MQTT`);
|
|
40
44
|
node.publishAllDiscovery(devices);
|
|
41
|
-
},
|
|
45
|
+
}, 3000);
|
|
42
46
|
} else {
|
|
43
47
|
node.warn('设备列表已完成但MQTT未连接,等待MQTT连接后发布');
|
|
44
48
|
}
|
|
45
|
-
}
|
|
49
|
+
};
|
|
50
|
+
node.gateway.on('device-list-complete', node._handlers.deviceListComplete);
|
|
46
51
|
|
|
47
|
-
node.
|
|
52
|
+
node._handlers.gatewayConnected = () => {
|
|
48
53
|
node.log('网关已连接,等待设备发现完成');
|
|
49
|
-
}
|
|
54
|
+
};
|
|
55
|
+
node.gateway.on('gateway-connected', node._handlers.gatewayConnected);
|
|
50
56
|
|
|
51
|
-
node.
|
|
57
|
+
node._handlers.gatewayDisconnected = () => {
|
|
52
58
|
node.log('网关已断开');
|
|
53
|
-
}
|
|
59
|
+
};
|
|
60
|
+
node.gateway.on('gateway-disconnected', node._handlers.gatewayDisconnected);
|
|
54
61
|
|
|
55
|
-
node.
|
|
62
|
+
node._handlers.deviceStateChanged = (eventData) => {
|
|
56
63
|
if (node.mqttClient && node.mqttClient.connected) {
|
|
57
64
|
node.publishDeviceState(eventData);
|
|
58
65
|
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
};
|
|
67
|
+
node.gateway.on('device-state-changed', node._handlers.deviceStateChanged);
|
|
68
|
+
|
|
69
|
+
// 监听设备状态同步完成事件,处理三合一设备的MQTT发布
|
|
70
|
+
node._handlers.deviceStatesSynced = (devices) => {
|
|
63
71
|
if (node.mqttClient && node.mqttClient.connected) {
|
|
64
|
-
|
|
72
|
+
// 检查是否有新确认的三合一设备需要发布
|
|
65
73
|
devices.forEach(device => {
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
if (device.isThreeInOne) {
|
|
75
|
+
const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
|
|
76
|
+
if (!node.publishedDevices.has(macClean)) {
|
|
77
|
+
node.log(`[三合一] 发布三合一设备 ${device.name} 到MQTT`);
|
|
78
|
+
|
|
79
|
+
// 发布availability
|
|
80
|
+
node.mqttClient.publish(`symi_mesh/${macClean}/availability`, 'online', { retain: true });
|
|
81
|
+
|
|
82
|
+
// 发布Discovery配置
|
|
83
|
+
const configs = generateDiscoveryConfig(device, node.mqttPrefix, node);
|
|
84
|
+
configs.forEach((config, index) => {
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
node.mqttClient.publish(config.topic, config.payload, { retain: true });
|
|
87
|
+
}, index * 50);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 订阅command topics
|
|
91
|
+
const topics = generateStateTopics(device);
|
|
92
|
+
const deviceMacForSub = device.macAddress; // 捕获当前设备MAC,避免闭包问题
|
|
93
|
+
node.log(`[三合一] 订阅${topics.command.length}个command topics`);
|
|
94
|
+
topics.command.forEach(topic => {
|
|
95
|
+
if (!node.subscriptions.has(topic)) {
|
|
96
|
+
node.log(`[三合一] 订阅: ${topic}`);
|
|
97
|
+
node.mqttClient.subscribe(topic, (err) => {
|
|
98
|
+
if (!err) {
|
|
99
|
+
node.subscriptions.set(topic, deviceMacForSub);
|
|
100
|
+
node.log(`[三合一] 订阅成功: ${topic}`);
|
|
101
|
+
} else {
|
|
102
|
+
node.error(`[三合一] 订阅失败: ${topic}`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
node.publishedDevices.add(macClean);
|
|
109
|
+
|
|
110
|
+
// 发布初始状态
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
publishInitialDeviceState(device, node);
|
|
113
|
+
}, 500);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
68
116
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
node.gateway.on('device-states-synced', node._handlers.deviceStatesSynced);
|
|
120
|
+
|
|
121
|
+
// 监听温控器确认事件,确认后立即发布Discovery
|
|
122
|
+
node._handlers.thermostatConfirmed = (device) => {
|
|
123
|
+
if (node.mqttClient && node.mqttClient.connected) {
|
|
124
|
+
node.log(`温控器${device.name}已确认,发布Discovery配置`);
|
|
125
|
+
const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
|
|
126
|
+
|
|
127
|
+
// 发布availability
|
|
128
|
+
node.mqttClient.publish(`symi_mesh/${macClean}/availability`, 'online', { retain: true });
|
|
129
|
+
|
|
130
|
+
// 发布Discovery配置
|
|
131
|
+
const configs = generateDiscoveryConfig(device, node.mqttPrefix, node);
|
|
132
|
+
configs.forEach(config => {
|
|
133
|
+
node.mqttClient.publish(config.topic, config.payload, { retain: config.retain });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 订阅command topics
|
|
137
|
+
const topics = generateStateTopics(device);
|
|
138
|
+
const deviceMacForSub = device.macAddress; // 捕获当前设备MAC,避免闭包问题
|
|
139
|
+
topics.command.forEach(topic => {
|
|
140
|
+
if (!node.subscriptions.has(topic)) {
|
|
141
|
+
node.mqttClient.subscribe(topic, (err) => {
|
|
142
|
+
if (!err) {
|
|
143
|
+
node.subscriptions.set(topic, deviceMacForSub);
|
|
144
|
+
node.debug(`订阅topic: ${topic} -> ${deviceMacForSub}`);
|
|
145
|
+
} else {
|
|
146
|
+
node.error(`订阅失败: ${topic}, ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// 标记为已发布
|
|
153
|
+
node.publishedDevices.add(macClean);
|
|
154
|
+
|
|
155
|
+
// 发布初始状态
|
|
156
|
+
publishInitialDeviceState(device, node);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
node.gateway.on('thermostat-confirmed', node._handlers.thermostatConfirmed);
|
|
160
|
+
|
|
161
|
+
node.on('close', (done) => {
|
|
162
|
+
// 移除gateway事件监听器,防止内存泄漏
|
|
163
|
+
if (node.gateway && node._handlers) {
|
|
164
|
+
node.gateway.removeListener('device-list-complete', node._handlers.deviceListComplete);
|
|
165
|
+
node.gateway.removeListener('gateway-connected', node._handlers.gatewayConnected);
|
|
166
|
+
node.gateway.removeListener('gateway-disconnected', node._handlers.gatewayDisconnected);
|
|
167
|
+
node.gateway.removeListener('device-state-changed', node._handlers.deviceStateChanged);
|
|
168
|
+
node.gateway.removeListener('device-states-synced', node._handlers.deviceStatesSynced);
|
|
169
|
+
node.gateway.removeListener('thermostat-confirmed', node._handlers.thermostatConfirmed);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 清理MQTT资源
|
|
173
|
+
if (node.mqttClient) {
|
|
174
|
+
// 移除所有监听器
|
|
175
|
+
node.mqttClient.removeAllListeners();
|
|
176
|
+
|
|
177
|
+
if (node.mqttClient.connected) {
|
|
178
|
+
const devices = node.gateway.deviceManager.getAllDevices();
|
|
179
|
+
devices.forEach(device => {
|
|
180
|
+
const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
|
|
181
|
+
node.mqttClient.publish(`symi_mesh/${macClean}/availability`, 'offline', { retain: true });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
node.mqttClient.end(false, {}, done);
|
|
186
|
+
}, 200);
|
|
187
|
+
} else {
|
|
188
|
+
done();
|
|
189
|
+
}
|
|
73
190
|
} else {
|
|
74
191
|
done();
|
|
75
192
|
}
|
|
@@ -94,7 +211,7 @@ module.exports = function(RED) {
|
|
|
94
211
|
node.mqttClient = mqtt.connect(node.mqttBroker, options);
|
|
95
212
|
|
|
96
213
|
node.mqttClient.on('message', (topic, message) => {
|
|
97
|
-
node.
|
|
214
|
+
node.log(`[MQTT消息] topic=${topic}, message=${message.toString()}`);
|
|
98
215
|
node.handleMQTTMessage(topic, message);
|
|
99
216
|
});
|
|
100
217
|
|
|
@@ -121,10 +238,13 @@ module.exports = function(RED) {
|
|
|
121
238
|
});
|
|
122
239
|
|
|
123
240
|
node.mqttClient.on('error', (error) => {
|
|
124
|
-
|
|
241
|
+
// 只记录非连接错误,避免重连时大量日志
|
|
242
|
+
if (error.code !== 'ECONNREFUSED' && error.code !== 'ENOTFOUND') {
|
|
243
|
+
node.error(`MQTT错误: ${error.message}`);
|
|
244
|
+
}
|
|
125
245
|
node.status({ fill: 'red', shape: 'ring', text: '错误' });
|
|
126
246
|
});
|
|
127
|
-
|
|
247
|
+
|
|
128
248
|
node.mqttClient.on('offline', () => {
|
|
129
249
|
node.status({ fill: 'yellow', shape: 'ring', text: '离线' });
|
|
130
250
|
});
|
|
@@ -135,46 +255,56 @@ module.exports = function(RED) {
|
|
|
135
255
|
}
|
|
136
256
|
};
|
|
137
257
|
|
|
138
|
-
SymiMQTTNode.prototype.publishAllDiscovery = function(devices) {
|
|
258
|
+
SymiMQTTNode.prototype.publishAllDiscovery = function(devices, forceUpdate = false) {
|
|
139
259
|
const node = this;
|
|
140
|
-
|
|
141
|
-
const supportedTypes = [1, 2, 3, 4, 5, 8, 9, 10, 0x18];
|
|
142
|
-
|
|
260
|
+
|
|
261
|
+
const supportedTypes = [1, 2, 3, 4, 5, 8, 9, 10, 0x18, 39];
|
|
262
|
+
|
|
143
263
|
devices.forEach(device => {
|
|
144
264
|
const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
|
|
145
|
-
|
|
265
|
+
|
|
146
266
|
// 跳过正在检测中的设备(等待三合一识别完成)
|
|
147
267
|
if (device.needsThreeInOneCheck) {
|
|
148
|
-
node.
|
|
268
|
+
node.debug(`设备${device.name}正在检测类型,跳过发布(等待识别完成)`);
|
|
149
269
|
return; // 不添加到publishedDevices,等识别完成后再发布
|
|
150
270
|
}
|
|
151
|
-
|
|
271
|
+
|
|
152
272
|
// 跳过温控器类型但未确认的设备(防止在识别过程中被提前发布)
|
|
153
273
|
if (device.deviceType === 10 && !device.isThreeInOne && !device.thermostatConfirmed) {
|
|
154
|
-
node.
|
|
274
|
+
node.debug(`温控器${device.name}尚未确认类型,跳过发布(等待确认后自动发布)`);
|
|
155
275
|
return;
|
|
156
276
|
}
|
|
157
|
-
|
|
277
|
+
|
|
158
278
|
// 三合一设备通过isThreeInOne标记识别,不依赖deviceType
|
|
159
279
|
if (!supportedTypes.includes(device.deviceType) && !device.isThreeInOne) {
|
|
160
280
|
node.log(`跳过非支持设备: ${device.name} (type=${device.deviceType})`);
|
|
161
281
|
return;
|
|
162
282
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
283
|
+
|
|
284
|
+
// 如果不是强制更新,且设备已发布,则跳过
|
|
285
|
+
if (!forceUpdate && node.publishedDevices.has(macClean)) {
|
|
286
|
+
node.debug(`设备已发布,跳过: ${device.name}`);
|
|
166
287
|
return;
|
|
167
288
|
}
|
|
289
|
+
|
|
290
|
+
// 如果是强制更新,先从已发布列表中移除,以便重新发布
|
|
291
|
+
if (forceUpdate && node.publishedDevices.has(macClean)) {
|
|
292
|
+
node.log(`强制更新设备配置: ${device.name}`);
|
|
293
|
+
node.publishedDevices.delete(macClean);
|
|
294
|
+
}
|
|
168
295
|
|
|
169
|
-
const configs = generateDiscoveryConfig(device, node.mqttPrefix);
|
|
296
|
+
const configs = generateDiscoveryConfig(device, node.mqttPrefix, node);
|
|
170
297
|
const topics = generateStateTopics(device);
|
|
171
298
|
|
|
172
|
-
node.
|
|
299
|
+
node.debug(`发布设备 ${device.name} (${configs.length}个实体)`);
|
|
173
300
|
|
|
174
301
|
// 先立即发布availability: online,确保HA认为设备可用
|
|
302
|
+
node.log(`[MQTT] 发布 ${device.name} availability: online`);
|
|
175
303
|
node.mqttClient.publish(`symi_mesh/${macClean}/availability`, 'online', { retain: true }, (err) => {
|
|
176
304
|
if (err) {
|
|
177
305
|
node.error(`发布availability失败: ${err.message}`);
|
|
306
|
+
} else {
|
|
307
|
+
node.log(`[MQTT] ${device.name} availability已发布`);
|
|
178
308
|
}
|
|
179
309
|
});
|
|
180
310
|
|
|
@@ -189,14 +319,16 @@ module.exports = function(RED) {
|
|
|
189
319
|
}, index * 50 + 100);
|
|
190
320
|
});
|
|
191
321
|
|
|
322
|
+
const deviceMacForSub = device.macAddress; // 捕获当前设备MAC,避免闭包问题
|
|
192
323
|
topics.command.forEach(topic => {
|
|
193
324
|
if (!node.subscriptions.has(topic)) {
|
|
325
|
+
node.log(`[MQTT] 订阅: ${topic}`);
|
|
194
326
|
node.mqttClient.subscribe(topic, (err) => {
|
|
195
327
|
if (!err) {
|
|
196
|
-
node.subscriptions.set(topic,
|
|
197
|
-
node.
|
|
328
|
+
node.subscriptions.set(topic, deviceMacForSub);
|
|
329
|
+
node.log(`[MQTT] 订阅成功: ${topic}`);
|
|
198
330
|
} else {
|
|
199
|
-
node.error(
|
|
331
|
+
node.error(`[MQTT] 订阅失败: ${topic}, ${err.message}`);
|
|
200
332
|
}
|
|
201
333
|
});
|
|
202
334
|
}
|
|
@@ -310,7 +442,7 @@ module.exports = function(RED) {
|
|
|
310
442
|
node.mqttClient.publish(`symi_mesh/${macClean}/floor_heating/current_temp`, '20', { retain: true });
|
|
311
443
|
}
|
|
312
444
|
|
|
313
|
-
node.
|
|
445
|
+
node.debug(`发布设备 ${device.name} 初始状态`);
|
|
314
446
|
}
|
|
315
447
|
|
|
316
448
|
SymiMQTTNode.prototype.publishDeviceState = function(eventData) {
|
|
@@ -409,6 +541,7 @@ module.exports = function(RED) {
|
|
|
409
541
|
node.debug(`发布开关状态: ${switchState}`);
|
|
410
542
|
} else {
|
|
411
543
|
// 发布每个继电器的状态
|
|
544
|
+
node.debug(`[状态发布] ${device.name} (${device.channels}路) attrType=0x${attrType.toString(16).toUpperCase()}, state=${JSON.stringify(state)}`);
|
|
412
545
|
for (let i = 1; i <= device.channels; i++) {
|
|
413
546
|
const value = state[`switch_${i}`];
|
|
414
547
|
if (value !== undefined) {
|
|
@@ -417,7 +550,9 @@ module.exports = function(RED) {
|
|
|
417
550
|
topic: `symi_mesh/${macClean}/switch_${i}/state`,
|
|
418
551
|
payload: switchState
|
|
419
552
|
});
|
|
420
|
-
node.debug(
|
|
553
|
+
node.debug(`[状态发布] ${device.name} 第${i}路: ${switchState} (value=${value})`);
|
|
554
|
+
} else {
|
|
555
|
+
node.warn(`[状态发布] ${device.name} 第${i}路状态未定义`);
|
|
421
556
|
}
|
|
422
557
|
}
|
|
423
558
|
}
|
|
@@ -869,7 +1004,10 @@ module.exports = function(RED) {
|
|
|
869
1004
|
if (publishes.length > 0 && node.mqttClient && node.mqttClient.connected) {
|
|
870
1005
|
publishes.forEach(p => {
|
|
871
1006
|
node.mqttClient.publish(p.topic, p.payload, { retain: true });
|
|
1007
|
+
node.debug(`[MQTT发布] ${p.topic} = ${p.payload}`);
|
|
872
1008
|
});
|
|
1009
|
+
} else if (publishes.length > 0) {
|
|
1010
|
+
node.warn(`[MQTT发布失败] MQTT未连接,无法发布${publishes.length}条消息`);
|
|
873
1011
|
}
|
|
874
1012
|
};
|
|
875
1013
|
|
|
@@ -877,7 +1015,8 @@ module.exports = function(RED) {
|
|
|
877
1015
|
const node = this;
|
|
878
1016
|
|
|
879
1017
|
// 检查是否是场景触发消息
|
|
880
|
-
if (topic.match(/symi_mesh\/room_
|
|
1018
|
+
if (topic.match(/symi_mesh\/room_.+\/scene\/\d+\/trigger/)) {
|
|
1019
|
+
node.log(`[场景触发] 收到MQTT消息: ${topic}`);
|
|
881
1020
|
node.emit('scene-trigger', topic, message);
|
|
882
1021
|
return;
|
|
883
1022
|
}
|
|
@@ -885,13 +1024,18 @@ module.exports = function(RED) {
|
|
|
885
1024
|
const deviceMac = node.subscriptions.get(topic);
|
|
886
1025
|
|
|
887
1026
|
if (!deviceMac) {
|
|
888
|
-
|
|
1027
|
+
// 输出当前所有订阅,帮助调试
|
|
1028
|
+
node.warn(`[MQTT] 未找到topic订阅: ${topic}`);
|
|
1029
|
+
node.debug(`[MQTT] 当前订阅列表 (${node.subscriptions.size}个):`);
|
|
1030
|
+
node.subscriptions.forEach((mac, t) => {
|
|
1031
|
+
node.debug(` ${t} -> ${mac}`);
|
|
1032
|
+
});
|
|
889
1033
|
return;
|
|
890
1034
|
}
|
|
891
1035
|
|
|
892
1036
|
const device = node.gateway.getDevice(deviceMac);
|
|
893
1037
|
if (!device) {
|
|
894
|
-
node.warn(
|
|
1038
|
+
node.warn(`[MQTT] 设备未找到: MAC=${deviceMac}, topic=${topic}`);
|
|
895
1039
|
return;
|
|
896
1040
|
}
|
|
897
1041
|
|
|
@@ -901,11 +1045,51 @@ module.exports = function(RED) {
|
|
|
901
1045
|
const commands = node.parseMQTTCommand(topic, payload, device);
|
|
902
1046
|
|
|
903
1047
|
if (commands && commands.length > 0) {
|
|
904
|
-
node.
|
|
1048
|
+
node.debug(`[MQTT解析] 解析出${commands.length}个命令:`);
|
|
905
1049
|
commands.forEach((cmd, idx) => {
|
|
906
1050
|
node.log(` 命令${idx + 1}: attrType=0x${cmd.attrType.toString(16).toUpperCase()}, param=[${Array.from(cmd.param).map(p => '0x' + p.toString(16).toUpperCase()).join(', ')}]`);
|
|
907
1051
|
});
|
|
908
1052
|
|
|
1053
|
+
// 检查是否是开关控制命令,且该按键绑定了场景
|
|
1054
|
+
const isSwitch = (device.deviceType === 1 || device.deviceType === 2) && commands[0].attrType === 0x02;
|
|
1055
|
+
if (isSwitch && device.subDeviceConfigs && device.subDeviceConfigs.length > 0) {
|
|
1056
|
+
// 解析出是哪个按键
|
|
1057
|
+
const match = topic.match(/switch_(\d+)\/set/);
|
|
1058
|
+
const channel = match ? parseInt(match[1]) : 1;
|
|
1059
|
+
|
|
1060
|
+
// 检查该按键是否绑定场景
|
|
1061
|
+
const sceneInfo = device.getButtonSceneId(channel);
|
|
1062
|
+
if (sceneInfo) {
|
|
1063
|
+
// 该按键绑定了场景,应该触发场景而不是直接控制继电器
|
|
1064
|
+
node.log(`[MQTT场景] 按键${channel}(${sceneInfo.name})绑定了场景,触发场景而非控制继电器`);
|
|
1065
|
+
|
|
1066
|
+
(async () => {
|
|
1067
|
+
try {
|
|
1068
|
+
// 根据按键类型决定触发哪个场景
|
|
1069
|
+
let sceneId = sceneInfo.sceneId;
|
|
1070
|
+
|
|
1071
|
+
if (sceneInfo.type === 'dual_control' || sceneInfo.type === 'master_control') {
|
|
1072
|
+
// 双控/总控:根据目标状态选择场景
|
|
1073
|
+
const targetState = (payload === 'ON');
|
|
1074
|
+
const config = device.subDeviceConfigs[channel - 1];
|
|
1075
|
+
sceneId = targetState ? config.on_scene_id : config.off_scene_id;
|
|
1076
|
+
node.log(`[MQTT场景] ${sceneInfo.type === 'dual_control' ? '双控' : '总控'}按键,目标状态=${targetState ? '开' : '关'},触发场景${sceneId}`);
|
|
1077
|
+
} else {
|
|
1078
|
+
// 场景按键:直接触发场景
|
|
1079
|
+
node.log(`[MQTT场景] 场景按键,触发场景${sceneId}`);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
await node.gateway.sendScene(sceneId);
|
|
1083
|
+
node.log(`[MQTT场景] 场景${sceneId}(${sceneInfo.name})控制命令已发送`);
|
|
1084
|
+
} catch(err) {
|
|
1085
|
+
node.error(`[MQTT场景] 场景触发失败: ${err.message}`);
|
|
1086
|
+
}
|
|
1087
|
+
})();
|
|
1088
|
+
|
|
1089
|
+
return; // 不执行后续的继电器控制
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
909
1093
|
// 使用for循环而不是forEach以正确处理async
|
|
910
1094
|
(async () => {
|
|
911
1095
|
for (const command of commands) {
|
|
@@ -913,13 +1097,13 @@ module.exports = function(RED) {
|
|
|
913
1097
|
node.log(`[MQTT发送] → 网关: addr=0x${device.networkAddress.toString(16).toUpperCase()}, attr=0x${command.attrType.toString(16).toUpperCase()}, param=[${paramHex}]`);
|
|
914
1098
|
try {
|
|
915
1099
|
await node.gateway.sendControl(device.networkAddress, command.attrType, command.param);
|
|
916
|
-
node.
|
|
917
|
-
|
|
1100
|
+
node.debug(`[MQTT发送] 成功: ${device.name}`);
|
|
1101
|
+
|
|
918
1102
|
// 立即发布状态更新(optimistic update)
|
|
919
1103
|
node.publishCommandFeedback(device, command, payload, topic);
|
|
920
|
-
|
|
1104
|
+
|
|
921
1105
|
} catch(err) {
|
|
922
|
-
node.error(`[MQTT发送]
|
|
1106
|
+
node.error(`[MQTT发送] 失败: ${err.message}`);
|
|
923
1107
|
}
|
|
924
1108
|
}
|
|
925
1109
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-mesh",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -31,16 +31,19 @@
|
|
|
31
31
|
"symi-gateway": "nodes/symi-gateway.js",
|
|
32
32
|
"symi-device": "nodes/symi-device.js",
|
|
33
33
|
"symi-mqtt": "nodes/symi-mqtt.js",
|
|
34
|
-
"symi-cloud-sync": "nodes/symi-cloud-sync.js"
|
|
34
|
+
"symi-cloud-sync": "nodes/symi-cloud-sync.js",
|
|
35
|
+
"symi-485-config": "nodes/symi-485-config.js",
|
|
36
|
+
"symi-rs485-bridge": "nodes/symi-485-bridge.js",
|
|
37
|
+
"rs485-debug": "nodes/rs485-debug.js"
|
|
35
38
|
}
|
|
36
39
|
},
|
|
37
40
|
"dependencies": {
|
|
38
|
-
"axios": "^1.
|
|
41
|
+
"axios": "^1.7.9",
|
|
39
42
|
"mqtt": "^5.3.0",
|
|
40
43
|
"serialport": "^12.0.0"
|
|
41
44
|
},
|
|
42
45
|
"engines": {
|
|
43
|
-
"node": ">=
|
|
46
|
+
"node": ">=18.0.0"
|
|
44
47
|
},
|
|
45
48
|
"files": [
|
|
46
49
|
"lib/",
|