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-gateway.js
CHANGED
|
@@ -19,7 +19,7 @@ module.exports = function(RED) {
|
|
|
19
19
|
this.baudRate = parseInt(config.baudRate) || 115200;
|
|
20
20
|
|
|
21
21
|
this.client = null;
|
|
22
|
-
this.deviceManager = new DeviceManager(this.context(), this);
|
|
22
|
+
this.deviceManager = new DeviceManager(this.context(), this, this.id);
|
|
23
23
|
this.protocolHandler = new ProtocolHandler();
|
|
24
24
|
this.connected = false;
|
|
25
25
|
this.deviceListComplete = false;
|
|
@@ -27,6 +27,7 @@ module.exports = function(RED) {
|
|
|
27
27
|
|
|
28
28
|
// 状态事件处理队列 - 确保场景执行后的大量状态更新能被正确处理
|
|
29
29
|
this.stateEventQueue = [];
|
|
30
|
+
this.maxQueueSize = 100; // 最大队列大小,防止内存泄漏
|
|
30
31
|
this.isProcessingStateEvent = false;
|
|
31
32
|
this.sceneExecutionInProgress = false; // 场景执行中标志
|
|
32
33
|
this.sceneExecutionTimer = null; // 场景执行超时定时器
|
|
@@ -131,6 +132,11 @@ module.exports = function(RED) {
|
|
|
131
132
|
// 清空队列
|
|
132
133
|
this.stateEventQueue = [];
|
|
133
134
|
|
|
135
|
+
// 清理设备管理器资源
|
|
136
|
+
if (this.deviceManager) {
|
|
137
|
+
this.deviceManager.cleanup();
|
|
138
|
+
}
|
|
139
|
+
|
|
134
140
|
done();
|
|
135
141
|
});
|
|
136
142
|
};
|
|
@@ -151,9 +157,15 @@ module.exports = function(RED) {
|
|
|
151
157
|
if (frame.opcode === OP_RESP_DEVICE_LIST && frame.status === 0x00) {
|
|
152
158
|
this.parseDeviceListFrame(frame);
|
|
153
159
|
} else if (frame.isDeviceStatusEvent()) {
|
|
154
|
-
//
|
|
155
|
-
this.stateEventQueue.
|
|
156
|
-
|
|
160
|
+
// 将状态事件加入队列处理,限制队列大小防止内存泄漏
|
|
161
|
+
if (this.stateEventQueue.length < this.maxQueueSize) {
|
|
162
|
+
this.stateEventQueue.push(frame);
|
|
163
|
+
this.processStateEventQueue();
|
|
164
|
+
} else {
|
|
165
|
+
this.debug(`状态事件队列已满(${this.maxQueueSize}),丢弃旧事件`);
|
|
166
|
+
this.stateEventQueue.shift(); // 移除最旧的事件
|
|
167
|
+
this.stateEventQueue.push(frame);
|
|
168
|
+
}
|
|
157
169
|
} else if (frame.opcode === 0xB0) {
|
|
158
170
|
// 控制命令响应
|
|
159
171
|
this.debug(`[控制响应] 0xB0: ${frameHex} ${frame.status === 0 ? '(成功)' : '(失败)'}`)
|
|
@@ -214,12 +226,42 @@ module.exports = function(RED) {
|
|
|
214
226
|
this.debug(`${logPrefix} 地址=0x${event.networkAddress.toString(16).toUpperCase()}, 消息类型=0x${event.attrType.toString(16).toUpperCase()}, 参数=[${Array.from(event.parameters).map(p => '0x' + p.toString(16).toUpperCase()).join(', ')}]`);
|
|
215
227
|
}
|
|
216
228
|
|
|
229
|
+
// 获取更新前的三合一状态
|
|
230
|
+
const deviceBefore = this.deviceManager.getDeviceByAddress(event.networkAddress);
|
|
231
|
+
const wasThreeInOne = deviceBefore ? deviceBefore.isThreeInOne : false;
|
|
232
|
+
|
|
217
233
|
const device = this.deviceManager.updateDeviceState(
|
|
218
234
|
event.networkAddress,
|
|
219
235
|
event.attrType,
|
|
220
236
|
event.parameters
|
|
221
237
|
);
|
|
222
238
|
|
|
239
|
+
// 检测到新的三合一设备时记录日志
|
|
240
|
+
if (device && device.isThreeInOne && !wasThreeInOne) {
|
|
241
|
+
const attrName = event.attrType === 0x68 ? '新风' : event.attrType === 0x6B ? '地暖' : '0x94';
|
|
242
|
+
this.log(`[三合一检测] 收到 ${device.name} 的${attrName}响应 (0x${event.attrType.toString(16).toUpperCase()}),确认为三合一面板`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 检查是否是开关状态变化,且需要触发场景
|
|
246
|
+
if (device && event.attrType === 0x02 && !this.isQueryingStates && !this.sceneExecutionInProgress) {
|
|
247
|
+
// 检查是否有按键状态变化
|
|
248
|
+
if (device.lastChangedButtons && device.lastChangedButtons.length > 0) {
|
|
249
|
+
for (const change of device.lastChangedButtons) {
|
|
250
|
+
const sceneInfo = device.getButtonSceneId(change.button);
|
|
251
|
+
if (sceneInfo) {
|
|
252
|
+
this.log(`[按键场景] 检测到按键${change.button}(${sceneInfo.name})状态变化: ${change.oldState ? '开' : '关'} → ${change.newState ? '开' : '关'}, 触发场景${sceneInfo.sceneId}`);
|
|
253
|
+
|
|
254
|
+
// 发送场景控制命令
|
|
255
|
+
this.sendScene(sceneInfo.sceneId).then(() => {
|
|
256
|
+
this.log(`[按键场景] 场景${sceneInfo.sceneId}(${sceneInfo.name})控制命令已发送`);
|
|
257
|
+
}).catch(err => {
|
|
258
|
+
this.error(`[按键场景] 场景${sceneInfo.sceneId}控制命令发送失败: ${err.message}`);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
223
265
|
// 只在非查询状态期间才发送state-changed事件到MQTT
|
|
224
266
|
// 查询状态期间的事件只用于更新设备状态,不触发MQTT发布
|
|
225
267
|
if (device && !this.isQueryingStates) {
|
|
@@ -237,7 +279,8 @@ module.exports = function(RED) {
|
|
|
237
279
|
await new Promise(resolve => setImmediate(resolve));
|
|
238
280
|
}
|
|
239
281
|
} catch (error) {
|
|
240
|
-
|
|
282
|
+
// 状态事件解析错误改为debug级别,避免干扰正常日志(通常是网络噪音或不完整数据包)
|
|
283
|
+
this.debug(`Error parsing status event: ${error.message}`);
|
|
241
284
|
}
|
|
242
285
|
}
|
|
243
286
|
|
|
@@ -265,24 +308,37 @@ module.exports = function(RED) {
|
|
|
265
308
|
|
|
266
309
|
this.log(`Device ${index + 1}/${maxDevices}: ${device.name} @ 0x${networkAddress.toString(16).toUpperCase()}`);
|
|
267
310
|
|
|
268
|
-
// 对于温控器类型(
|
|
311
|
+
// 对于温控器类型(deviceType=10),需要通过查询新风/地暖来判断是否是三合一
|
|
312
|
+
// 空调温控器和三合一都属于同一品类,只有通过主动查询才能区分
|
|
269
313
|
if (deviceType === 10) {
|
|
270
|
-
|
|
271
|
-
|
|
314
|
+
// 如果设备已经被确认为三合一或普通温控器,跳过检测
|
|
315
|
+
if (device.isThreeInOne) {
|
|
316
|
+
this.log(`[三合一检测] ${device.name} 已确认为三合一面板(从缓存恢复)`);
|
|
317
|
+
} else if (device.thermostatConfirmed) {
|
|
318
|
+
this.log(`[三合一检测] ${device.name} 已确认为普通温控器(从缓存恢复)`);
|
|
319
|
+
} else {
|
|
320
|
+
device.needsThreeInOneCheck = true;
|
|
321
|
+
this.log(`[三合一检测] 发现温控器类型设备: ${device.name},将通过查询新风/地暖确认类型`);
|
|
322
|
+
}
|
|
272
323
|
}
|
|
273
324
|
|
|
274
325
|
if (index === maxDevices - 1) {
|
|
275
326
|
this.log(`Device discovery complete: ${maxDevices} devices`);
|
|
276
|
-
|
|
277
|
-
//
|
|
327
|
+
|
|
328
|
+
// 立即标记设备列表完成,让设备在HA中可用
|
|
329
|
+
this.deviceListComplete = true;
|
|
330
|
+
this.emit('device-list-complete', this.deviceManager.getAllDevices());
|
|
331
|
+
|
|
332
|
+
// 在后台异步查询设备状态和确认三合一设备
|
|
278
333
|
setTimeout(async () => {
|
|
279
|
-
this.
|
|
334
|
+
this.log('开始后台查询设备状态...');
|
|
335
|
+
this.isQueryingStates = true;
|
|
280
336
|
await this.queryAllDeviceStates();
|
|
281
|
-
this.isQueryingStates = false;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
this.emit('device-
|
|
337
|
+
this.isQueryingStates = false;
|
|
338
|
+
this.log('后台状态查询完成');
|
|
339
|
+
|
|
340
|
+
// 状态查询完成后,如果有设备类型变化(如三合一确认),重新发布MQTT Discovery
|
|
341
|
+
this.emit('device-states-synced', this.deviceManager.getAllDevices());
|
|
286
342
|
}, 500);
|
|
287
343
|
}
|
|
288
344
|
};
|
|
@@ -296,9 +352,11 @@ module.exports = function(RED) {
|
|
|
296
352
|
let queryAttrs = [];
|
|
297
353
|
|
|
298
354
|
// 根据设备类型查询不同属性
|
|
299
|
-
if ([1, 2, 3].includes(device.deviceType)) {
|
|
300
|
-
|
|
301
|
-
|
|
355
|
+
if ([1, 2, 3, 39].includes(device.deviceType)) {
|
|
356
|
+
// 6-8路开关使用0x45,1-4路使用0x02
|
|
357
|
+
const msgType = (device.channels >= 6) ? 0x45 : 0x02;
|
|
358
|
+
queryAttrs = [msgType];
|
|
359
|
+
this.log(`查询开关设备: ${device.name} (地址=0x${device.networkAddress.toString(16).toUpperCase()}, 路数=${device.channels}, msgType=0x${msgType.toString(16).toUpperCase()})`);
|
|
302
360
|
} else if (device.deviceType === 9) {
|
|
303
361
|
queryAttrs = [0x02, 0x0E]; // 插卡取电:开关状态、插卡状态
|
|
304
362
|
} else if (device.deviceType === 4 || device.deviceType === 0x18) {
|
|
@@ -306,14 +364,16 @@ module.exports = function(RED) {
|
|
|
306
364
|
} else if (device.deviceType === 5) {
|
|
307
365
|
queryAttrs = [0x05, 0x06]; // 窗帘:运行状态、位置
|
|
308
366
|
} else if (device.deviceType === 10) {
|
|
309
|
-
//
|
|
367
|
+
// 温控器类型:通过查询新风(0x68)和地暖(0x6B)来识别是否是三合一
|
|
368
|
+
// 研发说明:空调温控器和三合一都是同一品类(deviceType=10)
|
|
369
|
+
// 区分方法:主动查询新风/地暖状态,有反馈的是三合一,无反馈的是普通温控器
|
|
310
370
|
if (device.needsThreeInOneCheck) {
|
|
311
|
-
this.log(
|
|
312
|
-
//
|
|
313
|
-
queryAttrs = [0x68];
|
|
371
|
+
this.log(`[三合一检测] 开始检测设备 ${device.name} (0x${device.networkAddress.toString(16).toUpperCase()})`);
|
|
372
|
+
// 同时查询新风(0x68)和地暖(0x6B),任意一个有响应就确认是三合一
|
|
373
|
+
queryAttrs = [0x68, 0x6B];
|
|
314
374
|
} else if (device.isThreeInOne) {
|
|
315
375
|
// 已确认是三合一,查询完整状态
|
|
316
|
-
queryAttrs = [0x16, 0x1B, 0x1C, 0x1D, 0x68, 0x6A, 0x6B, 0x6C];
|
|
376
|
+
queryAttrs = [0x02, 0x16, 0x1B, 0x1C, 0x1D, 0x68, 0x6A, 0x6B, 0x6C];
|
|
317
377
|
} else {
|
|
318
378
|
// 普通温控器
|
|
319
379
|
queryAttrs = [0x02, 0x16, 0x1B, 0x1C, 0x1D];
|
|
@@ -328,25 +388,34 @@ module.exports = function(RED) {
|
|
|
328
388
|
|
|
329
389
|
// 等待设备响应,检查是否被标记为三合一
|
|
330
390
|
if (device.needsThreeInOneCheck) {
|
|
331
|
-
|
|
391
|
+
// 等待1.5秒确保响应到达(网络延迟可能较大)
|
|
392
|
+
await this.sleep(1500);
|
|
393
|
+
|
|
394
|
+
// 处理状态事件队列,确保所有响应都被处理
|
|
395
|
+
await this.processStateEventQueue();
|
|
396
|
+
|
|
332
397
|
if (device.isThreeInOne) {
|
|
333
|
-
this.log(
|
|
398
|
+
this.log(`[三合一检测] ${device.name} 确认为三合一面板`);
|
|
334
399
|
device.needsThreeInOneCheck = false;
|
|
335
400
|
this.deviceManager.saveDevices();
|
|
336
401
|
|
|
337
|
-
//
|
|
338
|
-
|
|
402
|
+
// 查询三合一的完整状态
|
|
403
|
+
this.log(`[三合一检测] 查询 ${device.name} 完整状态...`);
|
|
404
|
+
const threeInOneAttrs = [0x02, 0x16, 0x1B, 0x1C, 0x1D, 0x6A, 0x6C];
|
|
339
405
|
for (const attr of threeInOneAttrs) {
|
|
340
406
|
const frame = this.protocolHandler.buildDeviceStatusQueryFrame(device.networkAddress, attr);
|
|
341
407
|
await this.client.sendFrame(frame, 2);
|
|
342
408
|
await this.sleep(150);
|
|
343
409
|
}
|
|
344
410
|
} else {
|
|
345
|
-
this.log(
|
|
411
|
+
this.log(`[三合一检测] ${device.name} 确认为普通温控器`);
|
|
346
412
|
device.needsThreeInOneCheck = false;
|
|
347
|
-
device.thermostatConfirmed = true;
|
|
413
|
+
device.thermostatConfirmed = true;
|
|
348
414
|
this.deviceManager.saveDevices();
|
|
349
|
-
|
|
415
|
+
|
|
416
|
+
// 触发温控器确认事件
|
|
417
|
+
this.emit('thermostat-confirmed', device);
|
|
418
|
+
|
|
350
419
|
// 查询温控器状态
|
|
351
420
|
const tempCtrlAttrs = [0x02, 0x16, 0x1B, 0x1C, 0x1D];
|
|
352
421
|
for (const attr of tempCtrlAttrs) {
|
|
@@ -422,8 +491,18 @@ module.exports = function(RED) {
|
|
|
422
491
|
try {
|
|
423
492
|
// 从设备管理器获取当前状态
|
|
424
493
|
const device = this.deviceManager.getDeviceByAddress(networkAddr);
|
|
494
|
+
|
|
495
|
+
// 根据路数选择正确的msgType
|
|
496
|
+
// 1-4路使用0x02 (TYPE_ON_OFF)
|
|
497
|
+
// 6-8路使用0x45 (VD_GROUP_MSG_TYPE_1_8_ON_OFF)
|
|
498
|
+
let actualMsgType = attrType;
|
|
499
|
+
if (channels >= 6) {
|
|
500
|
+
actualMsgType = 0x45; // VD_GROUP_MSG_TYPE_1_8_ON_OFF
|
|
501
|
+
this.debug(`[开关控制] 6-8路开关使用msgType=0x45`);
|
|
502
|
+
}
|
|
503
|
+
|
|
425
504
|
let currentState = null;
|
|
426
|
-
|
|
505
|
+
|
|
427
506
|
if (device && typeof device.getCurrentSwitchState === 'function') {
|
|
428
507
|
currentState = device.getCurrentSwitchState();
|
|
429
508
|
this.debug(`使用缓存状态: 0x${currentState.toString(16).toUpperCase()}`);
|
|
@@ -433,37 +512,40 @@ module.exports = function(RED) {
|
|
|
433
512
|
} else {
|
|
434
513
|
// 如果没有当前状态,查询一次
|
|
435
514
|
this.debug(`查询开关状态: 地址0x${networkAddr.toString(16).toUpperCase()}`);
|
|
436
|
-
const queryFrame = this.protocolHandler.buildDeviceStatusQueryFrame(networkAddr,
|
|
515
|
+
const queryFrame = this.protocolHandler.buildDeviceStatusQueryFrame(networkAddr, actualMsgType);
|
|
437
516
|
await this.client.sendFrame(queryFrame, 2);
|
|
438
517
|
await this.sleep(200);
|
|
439
|
-
|
|
518
|
+
|
|
440
519
|
if (device && typeof device.getCurrentSwitchState === 'function') {
|
|
441
520
|
currentState = device.getCurrentSwitchState();
|
|
442
521
|
} else {
|
|
443
522
|
// 使用默认全关状态
|
|
444
|
-
const defaultStates = { 2: 0x05, 3: 0x15, 4: 0x55, 6: 0x5555 };
|
|
523
|
+
const defaultStates = { 2: 0x05, 3: 0x15, 4: 0x55, 6: 0x5555, 8: 0x5555 };
|
|
445
524
|
currentState = defaultStates[channels] || 0x55;
|
|
446
525
|
this.debug(`使用默认状态: 0x${currentState.toString(16).toUpperCase()}`);
|
|
447
526
|
}
|
|
448
527
|
}
|
|
449
|
-
|
|
528
|
+
|
|
450
529
|
// 使用状态组合算法
|
|
530
|
+
this.debug(`[开关控制] 路数=${channels}, 目标路=${targetChannel}, 目标状态=${targetState ? '开' : '关'}, 当前状态=0x${currentState ? currentState.toString(16).toUpperCase() : 'null'}`);
|
|
451
531
|
const stateValue = this.protocolHandler.buildSwitchState(channels, targetChannel, targetState, currentState);
|
|
452
532
|
|
|
453
533
|
let controlParam;
|
|
454
534
|
let stateValueHex;
|
|
455
535
|
if (Buffer.isBuffer(stateValue)) {
|
|
456
|
-
// 6路开关,2字节状态
|
|
536
|
+
// 6-8路开关,2字节状态
|
|
457
537
|
controlParam = stateValue;
|
|
458
538
|
stateValueHex = stateValue.toString('hex').toUpperCase();
|
|
539
|
+
this.debug(`[开关控制] 生成2字节状态值: 0x${stateValueHex} (${Array.from(stateValue).map(b => '0x' + b.toString(16).toUpperCase()).join(', ')})`);
|
|
459
540
|
} else {
|
|
460
541
|
// 1-4路开关,1字节状态
|
|
461
542
|
controlParam = Buffer.from([stateValue]);
|
|
462
543
|
stateValueHex = stateValue.toString(16).toUpperCase();
|
|
544
|
+
this.debug(`[开关控制] 生成1字节状态值: 0x${stateValueHex}`);
|
|
463
545
|
}
|
|
464
546
|
|
|
465
|
-
this.
|
|
466
|
-
const frame = this.protocolHandler.buildDeviceControlFrame(networkAddr,
|
|
547
|
+
this.log(`[开关控制] ${device.name} 第${targetChannel}路${targetState ? '开' : '关'}`);
|
|
548
|
+
const frame = this.protocolHandler.buildDeviceControlFrame(networkAddr, actualMsgType, controlParam);
|
|
467
549
|
return await this.client.sendFrame(frame, 1);
|
|
468
550
|
|
|
469
551
|
} catch (error) {
|