node-red-contrib-symi-mesh 1.7.1 → 1.7.3
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 +274 -20
- package/examples/knx-sync-example.json +122 -58
- package/examples/rs485-sync-example.json +76 -0
- package/lib/device-manager.js +96 -51
- package/lib/serial-client.js +23 -4
- package/nodes/rs485-debug.html +2 -1
- package/nodes/symi-485-bridge.html +233 -32
- package/nodes/symi-485-bridge.js +874 -98
- package/nodes/symi-485-config.html +44 -21
- package/nodes/symi-485-config.js +49 -11
- package/nodes/symi-cloud-sync.html +2 -0
- package/nodes/symi-device.html +5 -3
- package/nodes/symi-gateway.html +49 -1
- package/nodes/symi-gateway.js +43 -3
- package/nodes/symi-knx-bridge.html +3 -2
- package/nodes/symi-knx-bridge.js +3 -3
- package/nodes/symi-knx-ha-bridge.html +4 -3
- package/nodes/symi-knx-ha-bridge.js +2 -2
- package/nodes/symi-mqtt-brand.html +75 -0
- package/nodes/symi-mqtt-brand.js +238 -0
- package/nodes/symi-mqtt-sync.html +381 -0
- package/nodes/symi-mqtt-sync.js +473 -0
- package/nodes/symi-rs485-sync.html +361 -0
- package/nodes/symi-rs485-sync.js +765 -0
- package/package.json +5 -2
package/nodes/symi-485-bridge.js
CHANGED
|
@@ -322,10 +322,521 @@ module.exports = function(RED) {
|
|
|
322
322
|
// 用户需要在映射中配置: customCodes.trigger
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
|
+
},
|
|
326
|
+
// ===== SYMI空调面板协议 =====
|
|
327
|
+
// 帧格式: 7E [本机地址] [数据类型] [数据长度] [设备类型] [品牌ID] [设备地址] [设备通道] [房间信息3字节] [操作码] [操作信息] [CRC8] 7D
|
|
328
|
+
// 数据类型: 0x01=应答, 0x02=查询, 0x03=设置, 0x04=上报
|
|
329
|
+
// 操作码: 0x01=电源, 0x02=模式, 0x03=风速, 0x04=温度
|
|
330
|
+
'symi': {
|
|
331
|
+
name: 'SYMI空调面板',
|
|
332
|
+
protocol: 'symi',
|
|
333
|
+
devices: {
|
|
334
|
+
'climate': {
|
|
335
|
+
name: '空调',
|
|
336
|
+
type: 'climate',
|
|
337
|
+
protocol: 'symi'
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
// ===== 中弘VRF空调协议 =====
|
|
342
|
+
// 帧格式: [从机地址] [功能码] [控制值] [空调数量] [外机地址] [内机地址] [校验和]
|
|
343
|
+
// 功能码: 0x31=开关, 0x32=温度, 0x33=模式, 0x34=风速, 0x50=查询
|
|
344
|
+
// 模式: 0x01=制冷, 0x02=除湿, 0x04=送风, 0x08=制热
|
|
345
|
+
// 风速: 0x01=高, 0x02=中, 0x04=低
|
|
346
|
+
'zhonghong': {
|
|
347
|
+
name: '中弘VRF',
|
|
348
|
+
protocol: 'zhonghong',
|
|
349
|
+
devices: {
|
|
350
|
+
'climate': {
|
|
351
|
+
name: '空调',
|
|
352
|
+
type: 'climate',
|
|
353
|
+
protocol: 'zhonghong'
|
|
354
|
+
}
|
|
355
|
+
}
|
|
325
356
|
}
|
|
326
357
|
}
|
|
327
358
|
};
|
|
328
359
|
|
|
360
|
+
// ===== SYMI协议常量定义 =====
|
|
361
|
+
const SYMI_HEADER = 0x7E;
|
|
362
|
+
const SYMI_FOOTER = 0x7D;
|
|
363
|
+
const SYMI_DEVICE_TYPE_CLIMATE = 0x02;
|
|
364
|
+
|
|
365
|
+
// SYMI数据类型
|
|
366
|
+
const SYMI_DATA_TYPE = {
|
|
367
|
+
RESPONSE: 0x01, // 应答
|
|
368
|
+
QUERY: 0x02, // 查询
|
|
369
|
+
CONTROL: 0x03, // 设置/控制
|
|
370
|
+
REPORT: 0x04 // 上报
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// SYMI操作码
|
|
374
|
+
const SYMI_OP_CODE = {
|
|
375
|
+
POWER: 0x01, // 电源控制
|
|
376
|
+
MODE: 0x02, // 模式控制
|
|
377
|
+
FAN_SPEED: 0x03, // 风速控制
|
|
378
|
+
TEMPERATURE: 0x04 // 温度控制
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// SYMI模式值
|
|
382
|
+
const SYMI_MODE = {
|
|
383
|
+
AUTO: 0x00,
|
|
384
|
+
COOL: 0x01,
|
|
385
|
+
DEHUMIDIFY: 0x02,
|
|
386
|
+
FAN: 0x03,
|
|
387
|
+
HEAT: 0x04
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// SYMI风速值
|
|
391
|
+
const SYMI_FAN_SPEED = {
|
|
392
|
+
AUTO: 0x00,
|
|
393
|
+
LOW: 0x01,
|
|
394
|
+
MEDIUM: 0x02,
|
|
395
|
+
HIGH: 0x03
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// ===== 中弘协议常量定义 =====
|
|
399
|
+
const ZHONGHONG_FUNC = {
|
|
400
|
+
CTRL_SWITCH: 0x31,
|
|
401
|
+
CTRL_TEMPERATURE: 0x32,
|
|
402
|
+
CTRL_MODE: 0x33,
|
|
403
|
+
CTRL_FAN_MODE: 0x34,
|
|
404
|
+
QUERY: 0x50
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const ZHONGHONG_MODE = {
|
|
408
|
+
COOL: 0x01,
|
|
409
|
+
DRY: 0x02,
|
|
410
|
+
FAN: 0x04,
|
|
411
|
+
HEAT: 0x08
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const ZHONGHONG_FAN_SPEED = {
|
|
415
|
+
HIGH: 0x01,
|
|
416
|
+
MEDIUM: 0x02,
|
|
417
|
+
LOW: 0x04
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// ===== 模式转换映射 =====
|
|
421
|
+
// SYMI模式 -> 中弘模式
|
|
422
|
+
const SYMI_TO_ZH_MODE = {
|
|
423
|
+
0x00: 0x01, // auto -> cool
|
|
424
|
+
0x01: 0x01, // cool -> cool
|
|
425
|
+
0x02: 0x02, // dehumidify -> dry
|
|
426
|
+
0x03: 0x04, // fan -> fan
|
|
427
|
+
0x04: 0x08 // heat -> heat
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// 中弘模式 -> SYMI模式
|
|
431
|
+
const ZH_TO_SYMI_MODE = {
|
|
432
|
+
0x01: 0x01, // cool -> cool
|
|
433
|
+
0x02: 0x02, // dry -> dehumidify
|
|
434
|
+
0x04: 0x03, // fan -> fan
|
|
435
|
+
0x08: 0x04 // heat -> heat
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// SYMI风速 -> 中弘风速
|
|
439
|
+
const SYMI_TO_ZH_FAN = {
|
|
440
|
+
0x00: 0x04, // auto -> low
|
|
441
|
+
0x01: 0x04, // low -> low
|
|
442
|
+
0x02: 0x02, // medium -> medium
|
|
443
|
+
0x03: 0x01 // high -> high
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// 中弘风速 -> SYMI风速
|
|
447
|
+
const ZH_TO_SYMI_FAN = {
|
|
448
|
+
0x01: 0x03, // high -> high
|
|
449
|
+
0x02: 0x02, // medium -> medium
|
|
450
|
+
0x04: 0x01 // low -> low
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// ===== SYMI协议核心函数 =====
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 计算SYMI CRC8 (XOR校验)
|
|
457
|
+
* @param {Buffer} data - 从帧头到操作信息的数据
|
|
458
|
+
* @returns {number} - CRC8值
|
|
459
|
+
*/
|
|
460
|
+
function calcSymiCrc8(data) {
|
|
461
|
+
let crc = 0;
|
|
462
|
+
for (let i = 0; i < data.length; i++) {
|
|
463
|
+
crc ^= data[i];
|
|
464
|
+
}
|
|
465
|
+
return crc & 0xFF;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 解析SYMI帧
|
|
470
|
+
* @param {Buffer} frame - 原始帧数据
|
|
471
|
+
* @returns {Object|null} - 解析结果或null
|
|
472
|
+
*/
|
|
473
|
+
function parseSymiFrame(frame) {
|
|
474
|
+
// 验证帧头帧尾
|
|
475
|
+
if (frame[0] !== SYMI_HEADER || frame[frame.length - 1] !== SYMI_FOOTER) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 验证最小长度: 7E + localAddr + dataType + dataLen + ... + CRC + 7D
|
|
480
|
+
if (frame.length < 8) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const localAddr = frame[1];
|
|
485
|
+
const dataType = frame[2];
|
|
486
|
+
const dataLen = frame[3];
|
|
487
|
+
|
|
488
|
+
// 验证长度: header(1) + localAddr(1) + dataType(1) + dataLen(1) + data(dataLen) + crc(1) + footer(1)
|
|
489
|
+
const expectedLen = 4 + dataLen + 2;
|
|
490
|
+
if (frame.length !== expectedLen) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// 验证CRC (从帧头到操作信息,不含CRC和帧尾)
|
|
495
|
+
const crcIndex = frame.length - 2;
|
|
496
|
+
const calculatedCrc = calcSymiCrc8(frame.slice(0, crcIndex));
|
|
497
|
+
if (frame[crcIndex] !== calculatedCrc) {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 解析数据部分
|
|
502
|
+
// 格式: deviceType(1) + brandId(1) + deviceAddr(1) + deviceChannel(1) + roomInfo(3) + opCode(1) + opData(n)
|
|
503
|
+
const data = frame.slice(4, 4 + dataLen);
|
|
504
|
+
|
|
505
|
+
// 最小数据长度: deviceType + brandId + deviceAddr + deviceChannel + roomInfo(3) + opCode = 8
|
|
506
|
+
if (data.length < 8) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return {
|
|
511
|
+
localAddr,
|
|
512
|
+
dataType,
|
|
513
|
+
deviceType: data[0],
|
|
514
|
+
brandId: data[1],
|
|
515
|
+
deviceAddr: data[2],
|
|
516
|
+
deviceChannel: data[3],
|
|
517
|
+
roomInfo: data.slice(4, 7),
|
|
518
|
+
opCode: data[7],
|
|
519
|
+
opData: data.slice(8)
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* 构建SYMI帧
|
|
525
|
+
* @param {Object} params - 帧参数
|
|
526
|
+
* @returns {Buffer} - 完整帧
|
|
527
|
+
*/
|
|
528
|
+
function buildSymiFrame(params) {
|
|
529
|
+
const {
|
|
530
|
+
localAddr,
|
|
531
|
+
dataType,
|
|
532
|
+
deviceType = SYMI_DEVICE_TYPE_CLIMATE,
|
|
533
|
+
brandId = 0x00,
|
|
534
|
+
deviceAddr,
|
|
535
|
+
deviceChannel,
|
|
536
|
+
roomInfo = Buffer.from([0x00, 0x00, 0x00]),
|
|
537
|
+
opCode,
|
|
538
|
+
opData
|
|
539
|
+
} = params;
|
|
540
|
+
|
|
541
|
+
// 构建数据部分
|
|
542
|
+
const roomInfoBuf = Buffer.isBuffer(roomInfo) ? roomInfo : Buffer.from(roomInfo);
|
|
543
|
+
const opDataBuf = Buffer.isBuffer(opData) ? opData : Buffer.from(Array.isArray(opData) ? opData : [opData]);
|
|
544
|
+
|
|
545
|
+
const data = Buffer.concat([
|
|
546
|
+
Buffer.from([deviceType, brandId, deviceAddr, deviceChannel]),
|
|
547
|
+
roomInfoBuf,
|
|
548
|
+
Buffer.from([opCode]),
|
|
549
|
+
opDataBuf
|
|
550
|
+
]);
|
|
551
|
+
|
|
552
|
+
// 构建帧(不含CRC和尾部)
|
|
553
|
+
const frameWithoutCrc = Buffer.concat([
|
|
554
|
+
Buffer.from([SYMI_HEADER, localAddr, dataType, data.length]),
|
|
555
|
+
data
|
|
556
|
+
]);
|
|
557
|
+
|
|
558
|
+
// 计算CRC
|
|
559
|
+
const crc = calcSymiCrc8(frameWithoutCrc);
|
|
560
|
+
|
|
561
|
+
// 完整帧
|
|
562
|
+
return Buffer.concat([frameWithoutCrc, Buffer.from([crc, SYMI_FOOTER])]);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* 构建SYMI状态上报帧 (用于Zhonghong->SYMI同步)
|
|
567
|
+
* @param {Object} params - 状态参数
|
|
568
|
+
* @returns {Buffer} - 完整帧
|
|
569
|
+
*/
|
|
570
|
+
function buildSymiStatusFrame(params) {
|
|
571
|
+
const {
|
|
572
|
+
localAddr,
|
|
573
|
+
deviceAddr,
|
|
574
|
+
deviceChannel,
|
|
575
|
+
brandId = 0x00,
|
|
576
|
+
power, // boolean
|
|
577
|
+
mode, // SYMI mode value
|
|
578
|
+
fanSpeed, // SYMI fan speed value
|
|
579
|
+
targetTemp, // 16-30
|
|
580
|
+
currentTemp // room temperature
|
|
581
|
+
} = params;
|
|
582
|
+
|
|
583
|
+
// 状态上报数据: [switch, mode, fan, targetTemp, roomTemp]
|
|
584
|
+
const opData = Buffer.from([
|
|
585
|
+
power ? 0x01 : 0x00,
|
|
586
|
+
mode,
|
|
587
|
+
fanSpeed,
|
|
588
|
+
targetTemp,
|
|
589
|
+
currentTemp || targetTemp
|
|
590
|
+
]);
|
|
591
|
+
|
|
592
|
+
return buildSymiFrame({
|
|
593
|
+
localAddr,
|
|
594
|
+
dataType: SYMI_DATA_TYPE.REPORT,
|
|
595
|
+
deviceType: SYMI_DEVICE_TYPE_CLIMATE,
|
|
596
|
+
brandId,
|
|
597
|
+
deviceAddr,
|
|
598
|
+
deviceChannel,
|
|
599
|
+
opCode: 0x00, // 状态上报使用0x00
|
|
600
|
+
opData
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ===== 中弘协议核心函数 =====
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* 计算中弘校验和 (求和取低8位)
|
|
608
|
+
* @param {Buffer|Array} data - 不含校验和的帧数据
|
|
609
|
+
* @returns {number} - 校验和
|
|
610
|
+
*/
|
|
611
|
+
function calcZhonghongChecksum(data) {
|
|
612
|
+
let sum = 0;
|
|
613
|
+
for (let i = 0; i < data.length; i++) {
|
|
614
|
+
sum += data[i];
|
|
615
|
+
}
|
|
616
|
+
return sum & 0xFF;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* 构建中弘控制帧
|
|
621
|
+
* @param {Object} cmd - 命令参数
|
|
622
|
+
* @returns {Buffer} - 完整帧
|
|
623
|
+
*/
|
|
624
|
+
function buildZhonghongControlFrame(cmd) {
|
|
625
|
+
const frame = Buffer.from([
|
|
626
|
+
cmd.slaveAddr,
|
|
627
|
+
cmd.funcCode,
|
|
628
|
+
cmd.value,
|
|
629
|
+
0x01, // 空调数量
|
|
630
|
+
cmd.outdoorAddr,
|
|
631
|
+
cmd.indoorAddr
|
|
632
|
+
]);
|
|
633
|
+
|
|
634
|
+
const checksum = calcZhonghongChecksum(frame);
|
|
635
|
+
return Buffer.concat([frame, Buffer.from([checksum])]);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* 构建中弘查询帧
|
|
640
|
+
* @param {number} slaveAddr - 从机地址
|
|
641
|
+
* @param {number} outdoorAddr - 外机地址
|
|
642
|
+
* @param {number} indoorAddr - 内机地址
|
|
643
|
+
* @returns {Buffer} - 完整帧
|
|
644
|
+
*/
|
|
645
|
+
function buildZhonghongQueryFrame(slaveAddr, outdoorAddr, indoorAddr) {
|
|
646
|
+
const frame = Buffer.from([
|
|
647
|
+
slaveAddr,
|
|
648
|
+
ZHONGHONG_FUNC.QUERY, // 0x50 查询功能码
|
|
649
|
+
0x01, // 单机查询
|
|
650
|
+
0x01, // 空调数量
|
|
651
|
+
outdoorAddr,
|
|
652
|
+
indoorAddr
|
|
653
|
+
]);
|
|
654
|
+
|
|
655
|
+
const checksum = calcZhonghongChecksum(frame);
|
|
656
|
+
return Buffer.concat([frame, Buffer.from([checksum])]);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* 解析中弘状态响应
|
|
661
|
+
* @param {Buffer} frame - 原始帧数据
|
|
662
|
+
* @param {number} expectedSlaveAddr - 期望的从机地址
|
|
663
|
+
* @returns {Object|null} - 解析结果或null
|
|
664
|
+
*/
|
|
665
|
+
function parseZhonghongResponse(frame, expectedSlaveAddr) {
|
|
666
|
+
// 最小长度: slaveAddr + func + funcValue + num + (outdoor + indoor + status*8) + checksum
|
|
667
|
+
if (frame.length < 15) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const slaveAddr = frame[0];
|
|
672
|
+
const func = frame[1];
|
|
673
|
+
|
|
674
|
+
// 验证从机地址
|
|
675
|
+
if (expectedSlaveAddr !== undefined && slaveAddr !== expectedSlaveAddr) {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// 只处理查询响应
|
|
680
|
+
if (func !== ZHONGHONG_FUNC.QUERY) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const funcValue = frame[2];
|
|
685
|
+
const num = frame[3];
|
|
686
|
+
|
|
687
|
+
// 状态查询响应 (funcValue=0x01)
|
|
688
|
+
if (funcValue !== 0x01) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// 计算期望长度: 4 + num * 10 + 1
|
|
693
|
+
const expectedLen = 4 + num * 10 + 1;
|
|
694
|
+
if (frame.length < expectedLen) {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// 验证校验和
|
|
699
|
+
const checksum = calcZhonghongChecksum(frame.slice(0, expectedLen - 1));
|
|
700
|
+
if (frame[expectedLen - 1] !== checksum) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// 解析空调状态
|
|
705
|
+
const climates = [];
|
|
706
|
+
for (let i = 0; i < num; i++) {
|
|
707
|
+
const offset = 4 + i * 10;
|
|
708
|
+
climates.push({
|
|
709
|
+
outdoorAddr: frame[offset],
|
|
710
|
+
indoorAddr: frame[offset + 1],
|
|
711
|
+
onOff: frame[offset + 2] === 0x01,
|
|
712
|
+
targetTemp: frame[offset + 3],
|
|
713
|
+
mode: frame[offset + 4],
|
|
714
|
+
fanMode: frame[offset + 5],
|
|
715
|
+
currentTemp: frame[offset + 6]
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
slaveAddr,
|
|
721
|
+
funcValue,
|
|
722
|
+
num,
|
|
723
|
+
climates
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ===== 协议转换函数 =====
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* 转换SYMI命令到中弘命令
|
|
731
|
+
* @param {Object} symiCmd - SYMI解析结果
|
|
732
|
+
* @param {Object} mapping - 映射配置
|
|
733
|
+
* @returns {Object|null} - 中弘命令或null
|
|
734
|
+
*/
|
|
735
|
+
function convertSymiToZhonghong(symiCmd, mapping) {
|
|
736
|
+
const zhCmd = {
|
|
737
|
+
slaveAddr: mapping.zhSlaveAddr || 0x01,
|
|
738
|
+
outdoorAddr: mapping.zhOutdoorAddr || 0x01,
|
|
739
|
+
indoorAddr: mapping.zhIndoorAddr || 0x01
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
switch (symiCmd.opCode) {
|
|
743
|
+
case SYMI_OP_CODE.POWER:
|
|
744
|
+
zhCmd.funcCode = ZHONGHONG_FUNC.CTRL_SWITCH;
|
|
745
|
+
zhCmd.value = symiCmd.opData[0] === 0x01 ? 0x01 : 0x00;
|
|
746
|
+
break;
|
|
747
|
+
case SYMI_OP_CODE.MODE:
|
|
748
|
+
zhCmd.funcCode = ZHONGHONG_FUNC.CTRL_MODE;
|
|
749
|
+
zhCmd.value = SYMI_TO_ZH_MODE[symiCmd.opData[0]] || ZHONGHONG_MODE.COOL;
|
|
750
|
+
break;
|
|
751
|
+
case SYMI_OP_CODE.FAN_SPEED:
|
|
752
|
+
zhCmd.funcCode = ZHONGHONG_FUNC.CTRL_FAN_MODE;
|
|
753
|
+
zhCmd.value = SYMI_TO_ZH_FAN[symiCmd.opData[0]] || ZHONGHONG_FAN_SPEED.LOW;
|
|
754
|
+
break;
|
|
755
|
+
case SYMI_OP_CODE.TEMPERATURE:
|
|
756
|
+
zhCmd.funcCode = ZHONGHONG_FUNC.CTRL_TEMPERATURE;
|
|
757
|
+
zhCmd.value = clampTemperature(symiCmd.opData[0]);
|
|
758
|
+
break;
|
|
759
|
+
default:
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return zhCmd;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* 转换中弘状态到SYMI状态
|
|
768
|
+
* @param {Object} zhStatus - 中弘状态
|
|
769
|
+
* @param {Object} mapping - 映射配置
|
|
770
|
+
* @returns {Object} - SYMI状态帧参数
|
|
771
|
+
*/
|
|
772
|
+
function convertZhonghongToSymi(zhStatus, mapping) {
|
|
773
|
+
return {
|
|
774
|
+
localAddr: mapping.symiLocalAddr || 0x01,
|
|
775
|
+
deviceAddr: mapping.symiDeviceAddr || 0x01,
|
|
776
|
+
deviceChannel: mapping.symiDeviceChannel || 0x00,
|
|
777
|
+
brandId: mapping.symiBrandId || 0x00,
|
|
778
|
+
power: zhStatus.onOff,
|
|
779
|
+
mode: ZH_TO_SYMI_MODE[zhStatus.mode] || SYMI_MODE.COOL,
|
|
780
|
+
fanSpeed: ZH_TO_SYMI_FAN[zhStatus.fanMode] || SYMI_FAN_SPEED.LOW,
|
|
781
|
+
targetTemp: zhStatus.targetTemp,
|
|
782
|
+
currentTemp: zhStatus.currentTemp
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* 温度范围限制 (16-30°C)
|
|
788
|
+
* @param {number} temp - 输入温度
|
|
789
|
+
* @returns {number} - 限制后的温度
|
|
790
|
+
*/
|
|
791
|
+
function clampTemperature(temp) {
|
|
792
|
+
return Math.max(16, Math.min(30, temp));
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ===== RS485-to-RS485桥接状态缓存 =====
|
|
796
|
+
const climateStateCache = {};
|
|
797
|
+
const SYNC_COOLDOWN_MS = 1000;
|
|
798
|
+
const lastSyncTime = {};
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* 检查是否应该同步(防循环)
|
|
802
|
+
* @param {string} sourceKey - 源标识
|
|
803
|
+
* @returns {boolean} - 是否应该同步
|
|
804
|
+
*/
|
|
805
|
+
function shouldSync(sourceKey) {
|
|
806
|
+
const now = Date.now();
|
|
807
|
+
if (lastSyncTime[sourceKey] && now - lastSyncTime[sourceKey] < SYNC_COOLDOWN_MS) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
lastSyncTime[sourceKey] = now;
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* 检查状态是否变化(去重)
|
|
816
|
+
* @param {string} cacheKey - 缓存键
|
|
817
|
+
* @param {Object} newState - 新状态
|
|
818
|
+
* @returns {boolean} - 是否有变化
|
|
819
|
+
*/
|
|
820
|
+
function hasStateChanged(cacheKey, newState) {
|
|
821
|
+
const cached = climateStateCache[cacheKey];
|
|
822
|
+
if (!cached) {
|
|
823
|
+
climateStateCache[cacheKey] = { ...newState, lastUpdate: Date.now() };
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const changed =
|
|
828
|
+
cached.power !== newState.power ||
|
|
829
|
+
cached.mode !== newState.mode ||
|
|
830
|
+
cached.fanSpeed !== newState.fanSpeed ||
|
|
831
|
+
cached.targetTemp !== newState.targetTemp;
|
|
832
|
+
|
|
833
|
+
if (changed) {
|
|
834
|
+
climateStateCache[cacheKey] = { ...newState, lastUpdate: Date.now() };
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return changed;
|
|
838
|
+
}
|
|
839
|
+
|
|
329
840
|
// ===== 杜亚协议CRC16计算 =====
|
|
330
841
|
// 杜亚使用CRC16-MODBUS算法,低字节在前
|
|
331
842
|
function calcA6B6CRC(buffer) {
|
|
@@ -528,11 +1039,11 @@ module.exports = function(RED) {
|
|
|
528
1039
|
node.climateCache = {};
|
|
529
1040
|
// 首次启动标记 - 跳过初始状态同步
|
|
530
1041
|
node.initializing = true;
|
|
531
|
-
// 启动后延迟
|
|
1042
|
+
// 启动后延迟5秒再开始同步(等待Mesh网关完成设备发现)
|
|
532
1043
|
setTimeout(() => {
|
|
533
1044
|
node.initializing = false;
|
|
534
1045
|
node.log('[RS485 Bridge] 初始化完成,开始同步');
|
|
535
|
-
},
|
|
1046
|
+
}, 5000); // 5秒初始化延迟
|
|
536
1047
|
|
|
537
1048
|
// Mesh设备状态变化处理(事件驱动)
|
|
538
1049
|
const handleMeshStateChange = (eventData) => {
|
|
@@ -540,7 +1051,7 @@ module.exports = function(RED) {
|
|
|
540
1051
|
if (node.initializing) return;
|
|
541
1052
|
|
|
542
1053
|
const mac = eventData.device.macAddress;
|
|
543
|
-
|
|
1054
|
+
let state = eventData.state || {};
|
|
544
1055
|
|
|
545
1056
|
// 状态缓存比较,只处理真正变化的状态
|
|
546
1057
|
if (!node.stateCache[mac]) node.stateCache[mac] = {};
|
|
@@ -558,14 +1069,17 @@ module.exports = function(RED) {
|
|
|
558
1069
|
if (Object.keys(changed).length === 0) return; // 无变化
|
|
559
1070
|
|
|
560
1071
|
// 首次收到设备状态时只记录缓存,不触发同步(避免启动时批量发码)
|
|
561
|
-
//
|
|
1072
|
+
// 但以下情况除外,因为这些是用户的控制命令:
|
|
1073
|
+
// - 窗帘状态(curtainStatus)
|
|
1074
|
+
// - 来自用户控制的事件(isUserControl=true)
|
|
562
1075
|
const hasCurtainStatusChange = changed.curtainStatus !== undefined;
|
|
563
|
-
|
|
1076
|
+
const isUserControlEvent = eventData.isUserControl === true;
|
|
1077
|
+
if (isFirstState && !hasCurtainStatusChange && !isUserControlEvent) {
|
|
564
1078
|
node.debug(`[Mesh事件] MAC=${mac} 首次状态,仅缓存: ${JSON.stringify(changed)}`);
|
|
565
1079
|
return;
|
|
566
1080
|
}
|
|
567
1081
|
|
|
568
|
-
node.
|
|
1082
|
+
node.debug(`[Mesh事件] MAC=${mac}, 变化: ${JSON.stringify(changed)}`);
|
|
569
1083
|
|
|
570
1084
|
// 规范化MAC地址用于比较
|
|
571
1085
|
const macNormalized = mac.toLowerCase().replace(/:/g, '');
|
|
@@ -575,7 +1089,7 @@ module.exports = function(RED) {
|
|
|
575
1089
|
const mappingMacNormalized = (mapping.meshMac || '').toLowerCase().replace(/:/g, '');
|
|
576
1090
|
if (mappingMacNormalized !== macNormalized) continue;
|
|
577
1091
|
|
|
578
|
-
node.
|
|
1092
|
+
node.debug(`[Mesh事件] 匹配到映射: brand=${mapping.brand}, device=${mapping.device}`);
|
|
579
1093
|
|
|
580
1094
|
const configChannel = mapping.meshChannel || 1;
|
|
581
1095
|
const switchKey = `switch_${configChannel}`;
|
|
@@ -719,31 +1233,19 @@ module.exports = function(RED) {
|
|
|
719
1233
|
continue;
|
|
720
1234
|
}
|
|
721
1235
|
|
|
722
|
-
// 【自定义窗帘】处理Mesh面板控制同步到485
|
|
1236
|
+
// 【自定义窗帘】处理Mesh面板控制同步到485
|
|
723
1237
|
if (mapping.brand === 'custom' && mapping.device === 'custom_curtain' && mapping.customCodes) {
|
|
724
1238
|
const now = Date.now();
|
|
725
1239
|
const curtainKey = `custom_curtain_${mac}`;
|
|
726
1240
|
|
|
727
|
-
//
|
|
728
|
-
if (
|
|
729
|
-
continue; // 非用户控制(NODE_STATUS),忽略
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// 只处理窗帘状态事件 (attrType=0x05)
|
|
733
|
-
if (eventData.attrType !== 0x05) {
|
|
1241
|
+
// 检查是否有窗帘状态变化
|
|
1242
|
+
if (changed.curtainStatus === undefined && changed.curtainAction === undefined) {
|
|
734
1243
|
continue;
|
|
735
1244
|
}
|
|
736
1245
|
|
|
737
|
-
|
|
738
|
-
? eventData.parameters[0]
|
|
739
|
-
: null;
|
|
740
|
-
|
|
741
|
-
// 忽略 null 值
|
|
742
|
-
if (newStatus === null) {
|
|
743
|
-
continue;
|
|
744
|
-
}
|
|
1246
|
+
node.log(`[Mesh->自定义窗帘] ${eventData.device.name} 状态变化: ${JSON.stringify(changed)}`);
|
|
745
1247
|
|
|
746
|
-
// 1
|
|
1248
|
+
// 1秒防抖:避免重复发送相同状态
|
|
747
1249
|
if (!node.curtainDebounce) node.curtainDebounce = {};
|
|
748
1250
|
const debounceKey = `${curtainKey}_status`;
|
|
749
1251
|
const lastTime = node.curtainDebounce[debounceKey] || 0;
|
|
@@ -753,42 +1255,42 @@ module.exports = function(RED) {
|
|
|
753
1255
|
}
|
|
754
1256
|
node.curtainDebounce[debounceKey] = now;
|
|
755
1257
|
|
|
756
|
-
//
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
// 使用device.state.curtainAction来获取正确的动作
|
|
760
|
-
const device = node.gateway.deviceManager.getDeviceByMac(mac);
|
|
761
|
-
const curtainAction = device ? device.state.curtainAction : null;
|
|
1258
|
+
// 获取窗帘动作
|
|
1259
|
+
const curtainAction = changed.curtainAction || eventData.device.state.curtainAction;
|
|
1260
|
+
const curtainStatus = changed.curtainStatus;
|
|
762
1261
|
|
|
763
1262
|
const codes = mapping.customCodes;
|
|
764
1263
|
let hexCode = null, actionName = '';
|
|
765
1264
|
|
|
766
|
-
|
|
767
|
-
|
|
1265
|
+
// 优先使用curtainAction判断
|
|
1266
|
+
if (curtainAction === 'opening' && codes.sendOpen) {
|
|
1267
|
+
hexCode = codes.sendOpen;
|
|
768
1268
|
actionName = '打开';
|
|
769
|
-
} else if (curtainAction === 'closing' && codes.
|
|
770
|
-
hexCode = codes.
|
|
1269
|
+
} else if (curtainAction === 'closing' && codes.sendClose) {
|
|
1270
|
+
hexCode = codes.sendClose;
|
|
771
1271
|
actionName = '关闭';
|
|
772
|
-
} else if (curtainAction === 'stopped' && codes.
|
|
773
|
-
hexCode = codes.
|
|
774
|
-
actionName = '
|
|
775
|
-
} else {
|
|
776
|
-
//
|
|
777
|
-
if (
|
|
778
|
-
hexCode = codes.
|
|
779
|
-
} else if (
|
|
780
|
-
hexCode = codes.
|
|
781
|
-
} else if (
|
|
782
|
-
hexCode = codes.
|
|
1272
|
+
} else if (curtainAction === 'stopped' && codes.sendStop) {
|
|
1273
|
+
hexCode = codes.sendStop;
|
|
1274
|
+
actionName = '停止';
|
|
1275
|
+
} else if (curtainStatus !== undefined) {
|
|
1276
|
+
// 回退:使用curtainStatus(小程序协议:1=开,2=关,3=停)
|
|
1277
|
+
if (curtainStatus === 1 && codes.sendOpen) {
|
|
1278
|
+
hexCode = codes.sendOpen; actionName = '打开';
|
|
1279
|
+
} else if (curtainStatus === 2 && codes.sendClose) {
|
|
1280
|
+
hexCode = codes.sendClose; actionName = '关闭';
|
|
1281
|
+
} else if ((curtainStatus === 0 || curtainStatus === 3) && codes.sendStop) {
|
|
1282
|
+
hexCode = codes.sendStop; actionName = '停止';
|
|
783
1283
|
}
|
|
784
1284
|
}
|
|
785
1285
|
|
|
786
1286
|
if (hexCode) {
|
|
787
1287
|
node.sendCustomCode(hexCode).then(() => {
|
|
788
|
-
node.log(`[Mesh
|
|
1288
|
+
node.log(`[Mesh->自定义窗帘] ${actionName}: ${hexCode}`);
|
|
789
1289
|
}).catch(err => {
|
|
790
|
-
node.error(`[Mesh
|
|
1290
|
+
node.error(`[Mesh->自定义窗帘] 发送失败: ${err.message}`);
|
|
791
1291
|
});
|
|
1292
|
+
} else {
|
|
1293
|
+
node.debug(`[Mesh->自定义窗帘] 无匹配码, action=${curtainAction}, status=${curtainStatus}`);
|
|
792
1294
|
}
|
|
793
1295
|
continue;
|
|
794
1296
|
}
|
|
@@ -797,7 +1299,28 @@ module.exports = function(RED) {
|
|
|
797
1299
|
const isCurtainControlled = false;
|
|
798
1300
|
|
|
799
1301
|
// 只有对应类型的状态变化才触发对应类型的映射
|
|
800
|
-
|
|
1302
|
+
// 对于custom_switch,检查任意switch_*字段变化(因为用户可能控制任意一路)
|
|
1303
|
+
let hasSwitchChange = false;
|
|
1304
|
+
if (isSwitch) {
|
|
1305
|
+
// 检查配置的通道
|
|
1306
|
+
if (changed[switchKey] !== undefined) {
|
|
1307
|
+
hasSwitchChange = true;
|
|
1308
|
+
}
|
|
1309
|
+
// 对于custom品牌,只检查配置的通道或通用switch字段
|
|
1310
|
+
if (mapping.brand === 'custom' && !hasSwitchChange) {
|
|
1311
|
+
// 通用switch字段(单路开关或空调开关)
|
|
1312
|
+
if (changed.switch !== undefined || changed.acSwitch !== undefined || changed.climateSwitch !== undefined) {
|
|
1313
|
+
hasSwitchChange = true;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
// 非custom品牌也检查通用switch字段
|
|
1317
|
+
if (!hasSwitchChange && (changed.switch !== undefined || changed.acSwitch !== undefined)) {
|
|
1318
|
+
hasSwitchChange = true;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
node.debug(`[Mesh事件] isSwitch=${isSwitch}, hasSwitchChange=${hasSwitchChange}, switchKey=${switchKey}`);
|
|
1323
|
+
|
|
801
1324
|
// 窗帘:如果是杜亚窗帘,跳过状态变化处理(已在curtain-control中处理)
|
|
802
1325
|
const hasCurtainChange = isCurtain && !isCurtainControlled && (
|
|
803
1326
|
changed.curtainAction !== undefined ||
|
|
@@ -1179,17 +1702,59 @@ module.exports = function(RED) {
|
|
|
1179
1702
|
if (mapping.brand === 'custom' && mapping.customCodes) {
|
|
1180
1703
|
const codes = mapping.customCodes;
|
|
1181
1704
|
|
|
1182
|
-
|
|
1705
|
+
// 检查反馈选项:如果feedback=false,检查是否是RS485触发的状态变化
|
|
1706
|
+
// 如果是RS485触发的(500ms内有同步记录),则跳过发送反馈码
|
|
1707
|
+
const loopKey = `${mapping.meshMac}_${mapping.device}`;
|
|
1708
|
+
if (mapping.feedback === false) {
|
|
1709
|
+
const lastSync = node.lastSyncTime ? node.lastSyncTime[loopKey] : 0;
|
|
1710
|
+
if (lastSync && Date.now() - lastSync < 500) {
|
|
1711
|
+
node.debug(`[Mesh->自定义] 反馈已禁用,跳过发送`);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
node.debug(`[Mesh->自定义] 设备${mapping.device}, 状态: ${JSON.stringify(state)}`);
|
|
1183
1717
|
|
|
1184
1718
|
// 开关类型
|
|
1185
1719
|
if (mapping.device === 'custom_switch') {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1720
|
+
const configChannel = mapping.meshChannel || 1;
|
|
1721
|
+
const targetKey = `switch_${configChannel}`;
|
|
1722
|
+
|
|
1723
|
+
// 查找开关状态:只检查配置的通道或通用switch字段
|
|
1724
|
+
let switchValue = undefined;
|
|
1725
|
+
let foundKey = null;
|
|
1726
|
+
|
|
1727
|
+
// 1. 检查配置的通道
|
|
1728
|
+
if (state[targetKey] !== undefined) {
|
|
1729
|
+
switchValue = state[targetKey];
|
|
1730
|
+
foundKey = targetKey;
|
|
1731
|
+
}
|
|
1732
|
+
// 2. 回退到通用switch字段(单路开关或空调开关)
|
|
1733
|
+
if (switchValue === undefined && state.switch !== undefined) {
|
|
1734
|
+
switchValue = state.switch;
|
|
1735
|
+
foundKey = 'switch';
|
|
1736
|
+
}
|
|
1737
|
+
if (switchValue === undefined && state.acSwitch !== undefined) {
|
|
1738
|
+
switchValue = state.acSwitch;
|
|
1739
|
+
foundKey = 'acSwitch';
|
|
1740
|
+
}
|
|
1741
|
+
if (switchValue === undefined && state.climateSwitch !== undefined) {
|
|
1742
|
+
switchValue = state.climateSwitch;
|
|
1743
|
+
foundKey = 'climateSwitch';
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
if (switchValue !== undefined) {
|
|
1747
|
+
const hexCode = switchValue ? codes.sendOn : codes.sendOff;
|
|
1748
|
+
if (hexCode) {
|
|
1749
|
+
// 记录发送时间用于防死循环
|
|
1750
|
+
const loopKey = `${mapping.meshMac}_${mapping.device}`;
|
|
1751
|
+
if (!node.lastSyncTime) node.lastSyncTime = {};
|
|
1752
|
+
node.lastSyncTime[loopKey] = Date.now();
|
|
1753
|
+
|
|
1754
|
+
await node.sendCustomCode(hexCode);
|
|
1755
|
+
node.log(`[Mesh->自定义] 开关(${foundKey}): ${switchValue ? '开' : '关'}, 发送: ${hexCode}`);
|
|
1756
|
+
} else {
|
|
1757
|
+
node.warn(`[Mesh->自定义] 开关(${foundKey}): ${switchValue ? '开' : '关'}, 缺少${switchValue ? 'sendOn' : 'sendOff'}码`);
|
|
1193
1758
|
}
|
|
1194
1759
|
}
|
|
1195
1760
|
}
|
|
@@ -1238,6 +1803,12 @@ module.exports = function(RED) {
|
|
|
1238
1803
|
} else {
|
|
1239
1804
|
if (!node.lastSentTime) node.lastSentTime = {};
|
|
1240
1805
|
node.lastSentTime[cacheKey] = now;
|
|
1806
|
+
|
|
1807
|
+
// 记录发送时间用于防死循环
|
|
1808
|
+
const loopKey = `${mapping.meshMac}_${mapping.device}`;
|
|
1809
|
+
if (!node.lastSyncTime) node.lastSyncTime = {};
|
|
1810
|
+
node.lastSyncTime[loopKey] = now;
|
|
1811
|
+
|
|
1241
1812
|
await node.sendCustomCode(hexCode);
|
|
1242
1813
|
node.log(`[Mesh->自定义] 窗帘 ${actionName}, 发送: ${hexCode}`);
|
|
1243
1814
|
}
|
|
@@ -1245,47 +1816,75 @@ module.exports = function(RED) {
|
|
|
1245
1816
|
}
|
|
1246
1817
|
// 空调类型
|
|
1247
1818
|
else if (mapping.device === 'custom_climate') {
|
|
1248
|
-
node.
|
|
1819
|
+
node.debug(`[自定义空调] 处理状态: ${JSON.stringify(state)}, codes存在: ${!!codes}`);
|
|
1820
|
+
|
|
1821
|
+
// 记录发送时间用于防死循环
|
|
1822
|
+
const loopKey = `${mapping.meshMac}_${mapping.device}`;
|
|
1823
|
+
if (!node.lastSyncTime) node.lastSyncTime = {};
|
|
1824
|
+
|
|
1825
|
+
// 去重处理:记录已处理的命令类型
|
|
1826
|
+
const processedTypes = new Set();
|
|
1827
|
+
|
|
1249
1828
|
for (const [key, value] of Object.entries(state)) {
|
|
1250
1829
|
let hexCode = null;
|
|
1830
|
+
let codeType = '';
|
|
1831
|
+
let cmdCategory = ''; // 命令类别,用于去重
|
|
1251
1832
|
|
|
1252
|
-
// 1. 开关控制 (处理 acSwitch, climateSwitch, switch)
|
|
1833
|
+
// 1. 开关控制 (处理 acSwitch, climateSwitch, switch) - 只处理一次
|
|
1253
1834
|
if (key === 'acSwitch' || key === 'climateSwitch' || key === 'switch') {
|
|
1835
|
+
cmdCategory = 'switch';
|
|
1836
|
+
if (processedTypes.has(cmdCategory)) continue; // 跳过重复
|
|
1837
|
+
processedTypes.add(cmdCategory);
|
|
1838
|
+
|
|
1254
1839
|
const isOn = value === true || value === 1 || value === '1' || value === 'on' || value === 'ON';
|
|
1255
1840
|
hexCode = isOn ? (codes.acSendOn || codes.sendOn) : (codes.acSendOff || codes.sendOff);
|
|
1256
|
-
|
|
1841
|
+
codeType = isOn ? 'acSendOn' : 'acSendOff';
|
|
1257
1842
|
}
|
|
1258
|
-
// 2. 风速控制
|
|
1843
|
+
// 2. 风速控制 - 只处理一次
|
|
1259
1844
|
else if (['acFanSpeed', 'fanSpeed', 'climateFanSpeed', 'fanMode', 'fan_speed', 'fanLevel', 'fan_level', 'speed'].includes(key)) {
|
|
1845
|
+
cmdCategory = 'fan';
|
|
1846
|
+
if (processedTypes.has(cmdCategory)) continue; // 跳过重复
|
|
1847
|
+
processedTypes.add(cmdCategory);
|
|
1848
|
+
|
|
1260
1849
|
const val = parseInt(value);
|
|
1261
|
-
node.log(`[自定义空调] 风速控制: ${key}=${value}, val=${val}`);
|
|
1262
1850
|
// 标准Mesh协议: 1=高, 2=中, 3=低, 4=自动
|
|
1263
1851
|
if (val === 1) {
|
|
1264
1852
|
hexCode = codes.fanSendHigh;
|
|
1265
|
-
|
|
1853
|
+
codeType = 'fanSendHigh';
|
|
1266
1854
|
}
|
|
1267
1855
|
else if (val === 2) {
|
|
1268
1856
|
hexCode = codes.fanSendMid;
|
|
1269
|
-
|
|
1857
|
+
codeType = 'fanSendMid';
|
|
1270
1858
|
}
|
|
1271
|
-
else if (val === 3
|
|
1859
|
+
else if (val === 3) {
|
|
1272
1860
|
hexCode = codes.fanSendLow;
|
|
1273
|
-
|
|
1861
|
+
codeType = 'fanSendLow';
|
|
1862
|
+
}
|
|
1863
|
+
else if (val === 4 || val === 0) {
|
|
1864
|
+
hexCode = codes.fanSendAuto;
|
|
1865
|
+
codeType = 'fanSendAuto';
|
|
1274
1866
|
}
|
|
1275
1867
|
}
|
|
1276
|
-
// 3. 模式控制
|
|
1868
|
+
// 3. 模式控制 - 只处理一次
|
|
1277
1869
|
else if (key === 'acMode' || key === 'climateMode' || key === 'mode') {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1870
|
+
cmdCategory = 'mode';
|
|
1871
|
+
if (processedTypes.has(cmdCategory)) continue;
|
|
1872
|
+
processedTypes.add(cmdCategory);
|
|
1873
|
+
|
|
1874
|
+
if (value === 1) { hexCode = codes.modeSendCool; codeType = 'modeSendCool'; }
|
|
1875
|
+
else if (value === 2) { hexCode = codes.modeSendHeat; codeType = 'modeSendHeat'; }
|
|
1876
|
+
else if (value === 4) { hexCode = codes.modeSendDry; codeType = 'modeSendDry'; }
|
|
1877
|
+
else if (value === 3) { hexCode = codes.modeSendFan; codeType = 'modeSendFan'; }
|
|
1282
1878
|
|
|
1283
1879
|
// 更新缓存中的模式
|
|
1284
1880
|
if (!node.climateCache[mapping.meshMac]) node.climateCache[mapping.meshMac] = {};
|
|
1285
1881
|
node.climateCache[mapping.meshMac].mode = value;
|
|
1286
1882
|
}
|
|
1287
|
-
// 4. 温度控制
|
|
1883
|
+
// 4. 温度控制 - 只处理一次
|
|
1288
1884
|
else if (key === 'targetTemp' || key === 'acTargetTemp' || key === 'temperature') {
|
|
1885
|
+
cmdCategory = 'temp';
|
|
1886
|
+
if (processedTypes.has(cmdCategory)) continue;
|
|
1887
|
+
processedTypes.add(cmdCategory);
|
|
1289
1888
|
const temp = Math.round(value);
|
|
1290
1889
|
// 优先从当前变化中获取模式,否则从缓存中获取,默认制冷(1)
|
|
1291
1890
|
const mode = state.acMode || state.climateMode || (node.climateCache[mapping.meshMac] ? node.climateCache[mapping.meshMac].mode : 1);
|
|
@@ -1308,8 +1907,11 @@ module.exports = function(RED) {
|
|
|
1308
1907
|
}
|
|
1309
1908
|
|
|
1310
1909
|
if (hexCode) {
|
|
1910
|
+
node.lastSyncTime[loopKey] = Date.now();
|
|
1311
1911
|
await node.sendCustomCode(hexCode);
|
|
1312
1912
|
node.log(`[Mesh->自定义] 空调 ${key}=${value}, 发送: ${hexCode}`);
|
|
1913
|
+
} else if (codeType) {
|
|
1914
|
+
node.warn(`[Mesh->自定义] 空调 ${key}=${value}, 缺少${codeType}码`);
|
|
1313
1915
|
}
|
|
1314
1916
|
}
|
|
1315
1917
|
}
|
|
@@ -1568,10 +2170,10 @@ module.exports = function(RED) {
|
|
|
1568
2170
|
// 开关类型
|
|
1569
2171
|
if (key === 'switch' || key.startsWith('switch_')) {
|
|
1570
2172
|
const ch = key.startsWith('switch_') ? parseInt(key.replace('switch_', '')) : channel;
|
|
1571
|
-
const
|
|
1572
|
-
const param = Buffer.from([ch
|
|
2173
|
+
const totalChannels = meshDevice.channels || 1;
|
|
2174
|
+
const param = Buffer.from([totalChannels, ch, value ? 1 : 0]);
|
|
1573
2175
|
await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
|
|
1574
|
-
node.log(`[自定义->Mesh] 开关${ch}
|
|
2176
|
+
node.log(`[自定义->Mesh] 开关${ch}/${totalChannels}路: ${value ? '开' : '关'}`);
|
|
1575
2177
|
}
|
|
1576
2178
|
// 空调开关
|
|
1577
2179
|
else if (key === 'acSwitch' || key === 'climateSwitch') {
|
|
@@ -1628,12 +2230,11 @@ module.exports = function(RED) {
|
|
|
1628
2230
|
if (key.startsWith('switch')) {
|
|
1629
2231
|
const channelFromKey = parseInt(key.replace('switch', '')) || 1;
|
|
1630
2232
|
const ch = mapping.meshChannel || channelFromKey;
|
|
1631
|
-
|
|
1632
|
-
const
|
|
1633
|
-
const param = Buffer.from([ch - 1, onOff]);
|
|
2233
|
+
const totalChannels = meshDevice.channels || 1;
|
|
2234
|
+
const param = Buffer.from([totalChannels, ch, value ? 1 : 0]);
|
|
1634
2235
|
|
|
1635
2236
|
await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
|
|
1636
|
-
node.log(`[RS485->Mesh] 开关${ch}
|
|
2237
|
+
node.log(`[RS485->Mesh] 开关${ch}/${totalChannels}路: ${value ? '开' : '关'}`);
|
|
1637
2238
|
}
|
|
1638
2239
|
else if (key.startsWith('led')) {
|
|
1639
2240
|
node.debug(`[RS485] 指示灯${key}: ${value}`);
|
|
@@ -1789,7 +2390,7 @@ module.exports = function(RED) {
|
|
|
1789
2390
|
}
|
|
1790
2391
|
try {
|
|
1791
2392
|
const hexStr = frame.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
|
|
1792
|
-
node.
|
|
2393
|
+
node.debug(`[RS485 TX] 发送帧: ${hexStr}`);
|
|
1793
2394
|
|
|
1794
2395
|
await node.rs485Config.send(frame);
|
|
1795
2396
|
|
|
@@ -1811,12 +2412,12 @@ module.exports = function(RED) {
|
|
|
1811
2412
|
|
|
1812
2413
|
// 解析原始RS485帧 - 支持标准Modbus和自定义码匹配
|
|
1813
2414
|
node.parseRS485Frame = function(frame) {
|
|
1814
|
-
if (frame.length <
|
|
2415
|
+
if (frame.length < 2) return; // 自定义码可能很短,改为2字节
|
|
1815
2416
|
|
|
1816
2417
|
const hexStr = frame.toString('hex').toUpperCase().replace(/\s/g, ''); // 确保完全去掉空格
|
|
1817
2418
|
const hexFormatted = hexStr.match(/.{1,2}/g)?.join(' ') || '';
|
|
1818
2419
|
|
|
1819
|
-
node.log(`[RS485
|
|
2420
|
+
node.log(`[RS485帧解析] 收到: ${hexFormatted} (${frame.length}字节)`);
|
|
1820
2421
|
|
|
1821
2422
|
// ===== 杜亚窗帘协议检测 (55开头) =====
|
|
1822
2423
|
if (frame[0] === 0x55 && frame.length >= 7) {
|
|
@@ -1884,21 +2485,190 @@ module.exports = function(RED) {
|
|
|
1884
2485
|
}
|
|
1885
2486
|
}
|
|
1886
2487
|
|
|
2488
|
+
// ===== SYMI空调面板协议检测 (7E开头7D结尾) =====
|
|
2489
|
+
if (frame[0] === SYMI_HEADER && frame[frame.length - 1] === SYMI_FOOTER && frame.length >= 8) {
|
|
2490
|
+
node.log(`[SYMI帧检测] 检测到7E...7D帧, 长度=${frame.length}`);
|
|
2491
|
+
const symiData = parseSymiFrame(frame);
|
|
2492
|
+
if (symiData) {
|
|
2493
|
+
node.log(`[SYMI帧解析] 成功! localAddr=${symiData.localAddr}, dataType=${symiData.dataType}, opCode=${symiData.opCode}`);
|
|
2494
|
+
|
|
2495
|
+
// 只处理空调设备类型
|
|
2496
|
+
if (symiData.deviceType !== SYMI_DEVICE_TYPE_CLIMATE) {
|
|
2497
|
+
node.debug(`[SYMI] 非空调设备类型: ${symiData.deviceType}`);
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// 查找匹配的SYMI->Zhonghong桥接映射
|
|
2502
|
+
for (const mapping of node.mappings) {
|
|
2503
|
+
if (mapping.brand !== 'symi' || !mapping.zhBridgeTarget) {
|
|
2504
|
+
continue;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// 检查SYMI地址是否匹配
|
|
2508
|
+
const symiLocalAddr = mapping.symiLocalAddr || 0x01;
|
|
2509
|
+
const symiDeviceAddr = mapping.symiDeviceAddr || 0x01;
|
|
2510
|
+
const symiDeviceChannel = mapping.symiDeviceChannel || 0x00;
|
|
2511
|
+
|
|
2512
|
+
if (symiData.localAddr !== symiLocalAddr ||
|
|
2513
|
+
symiData.deviceAddr !== symiDeviceAddr ||
|
|
2514
|
+
symiData.deviceChannel !== symiDeviceChannel) {
|
|
2515
|
+
continue;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// 防循环检查
|
|
2519
|
+
const syncKey = `symi_${symiLocalAddr}_${symiDeviceAddr}_${symiDeviceChannel}`;
|
|
2520
|
+
if (!shouldSync(syncKey)) {
|
|
2521
|
+
node.debug(`[防循环] 忽略SYMI帧: ${hexFormatted}`);
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// 只处理控制命令 (dataType=0x03)
|
|
2526
|
+
if (symiData.dataType !== SYMI_DATA_TYPE.CONTROL) {
|
|
2527
|
+
node.debug(`[SYMI] 非控制命令: dataType=${symiData.dataType}`);
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
node.log(`[SYMI->Zhonghong] 控制命令: opCode=${symiData.opCode}, opData=${symiData.opData.toString('hex')}`);
|
|
2532
|
+
|
|
2533
|
+
// 转换为中弘命令
|
|
2534
|
+
const zhCmd = convertSymiToZhonghong(symiData, mapping);
|
|
2535
|
+
if (!zhCmd) {
|
|
2536
|
+
node.warn(`[SYMI->Zhonghong] 未知操作码: ${symiData.opCode}`);
|
|
2537
|
+
continue;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
// 构建并发送中弘控制帧
|
|
2541
|
+
const zhFrame = buildZhonghongControlFrame(zhCmd);
|
|
2542
|
+
node.sendRS485Frame(zhFrame).then(() => {
|
|
2543
|
+
const zhHex = zhFrame.toString('hex').toUpperCase().match(/.{2}/g).join(' ');
|
|
2544
|
+
node.log(`[SYMI->Zhonghong] 发送: ${zhHex}`);
|
|
2545
|
+
|
|
2546
|
+
// 输出调试信息
|
|
2547
|
+
node.send({
|
|
2548
|
+
topic: 'symi-to-zhonghong',
|
|
2549
|
+
payload: {
|
|
2550
|
+
direction: 'SYMI→Zhonghong',
|
|
2551
|
+
symiOpCode: symiData.opCode,
|
|
2552
|
+
zhFuncCode: zhCmd.funcCode,
|
|
2553
|
+
zhValue: zhCmd.value,
|
|
2554
|
+
frame: zhHex
|
|
2555
|
+
},
|
|
2556
|
+
timestamp: new Date().toISOString()
|
|
2557
|
+
});
|
|
2558
|
+
}).catch(err => {
|
|
2559
|
+
node.error(`[SYMI->Zhonghong] 发送失败: ${err.message}`);
|
|
2560
|
+
});
|
|
2561
|
+
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
node.debug(`[SYMI] 未找到匹配的桥接映射`);
|
|
2566
|
+
} else {
|
|
2567
|
+
node.debug(`[SYMI帧检测] CRC校验失败或格式错误`);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// ===== 中弘VRF协议检测 (查询响应) =====
|
|
2572
|
+
// 中弘响应帧: slaveAddr + 0x50 + funcValue + num + data... + checksum
|
|
2573
|
+
if (frame.length >= 15 && frame[1] === ZHONGHONG_FUNC.QUERY) {
|
|
2574
|
+
node.log(`[中弘帧检测] 检测到查询响应, 长度=${frame.length}`);
|
|
2575
|
+
|
|
2576
|
+
// 查找匹配的Zhonghong->SYMI桥接映射
|
|
2577
|
+
for (const mapping of node.mappings) {
|
|
2578
|
+
if (mapping.brand !== 'zhonghong' || !mapping.symiBridgeTarget) {
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
const zhSlaveAddr = mapping.zhSlaveAddr || 0x01;
|
|
2583
|
+
const zhOutdoorAddr = mapping.zhOutdoorAddr || 0x01;
|
|
2584
|
+
const zhIndoorAddr = mapping.zhIndoorAddr || 0x01;
|
|
2585
|
+
|
|
2586
|
+
// 验证从机地址
|
|
2587
|
+
if (frame[0] !== zhSlaveAddr) {
|
|
2588
|
+
continue;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
const zhResponse = parseZhonghongResponse(frame, zhSlaveAddr);
|
|
2592
|
+
if (!zhResponse) {
|
|
2593
|
+
node.debug(`[中弘帧检测] 解析失败或校验错误`);
|
|
2594
|
+
continue;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// 查找匹配的空调状态
|
|
2598
|
+
const climate = zhResponse.climates.find(c =>
|
|
2599
|
+
c.outdoorAddr === zhOutdoorAddr && c.indoorAddr === zhIndoorAddr
|
|
2600
|
+
);
|
|
2601
|
+
|
|
2602
|
+
if (!climate) {
|
|
2603
|
+
node.debug(`[中弘] 未找到匹配的空调: outdoor=${zhOutdoorAddr}, indoor=${zhIndoorAddr}`);
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
// 防循环检查
|
|
2608
|
+
const syncKey = `zh_${zhSlaveAddr}_${zhOutdoorAddr}_${zhIndoorAddr}`;
|
|
2609
|
+
if (!shouldSync(syncKey)) {
|
|
2610
|
+
node.debug(`[防循环] 忽略中弘响应: ${hexFormatted}`);
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// 状态去重检查
|
|
2615
|
+
const cacheKey = `zh_climate_${zhSlaveAddr}_${zhOutdoorAddr}_${zhIndoorAddr}`;
|
|
2616
|
+
const newState = {
|
|
2617
|
+
power: climate.onOff,
|
|
2618
|
+
mode: climate.mode,
|
|
2619
|
+
fanSpeed: climate.fanMode,
|
|
2620
|
+
targetTemp: climate.targetTemp
|
|
2621
|
+
};
|
|
2622
|
+
|
|
2623
|
+
if (!hasStateChanged(cacheKey, newState)) {
|
|
2624
|
+
node.debug(`[中弘] 状态未变化,跳过同步`);
|
|
2625
|
+
continue;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
node.log(`[Zhonghong->SYMI] 状态: power=${climate.onOff}, mode=${climate.mode}, fan=${climate.fanMode}, temp=${climate.targetTemp}°C`);
|
|
2629
|
+
|
|
2630
|
+
// 转换为SYMI状态
|
|
2631
|
+
const symiParams = convertZhonghongToSymi(climate, mapping);
|
|
2632
|
+
|
|
2633
|
+
// 构建并发送SYMI状态帧
|
|
2634
|
+
const symiFrame = buildSymiStatusFrame(symiParams);
|
|
2635
|
+
node.sendRS485Frame(symiFrame).then(() => {
|
|
2636
|
+
const symiHex = symiFrame.toString('hex').toUpperCase().match(/.{2}/g).join(' ');
|
|
2637
|
+
node.log(`[Zhonghong->SYMI] 发送: ${symiHex}`);
|
|
2638
|
+
|
|
2639
|
+
// 输出调试信息
|
|
2640
|
+
node.send({
|
|
2641
|
+
topic: 'zhonghong-to-symi',
|
|
2642
|
+
payload: {
|
|
2643
|
+
direction: 'Zhonghong→SYMI',
|
|
2644
|
+
zhStatus: climate,
|
|
2645
|
+
symiParams: symiParams,
|
|
2646
|
+
frame: symiHex
|
|
2647
|
+
},
|
|
2648
|
+
timestamp: new Date().toISOString()
|
|
2649
|
+
});
|
|
2650
|
+
}).catch(err => {
|
|
2651
|
+
node.error(`[Zhonghong->SYMI] 发送失败: ${err.message}`);
|
|
2652
|
+
});
|
|
2653
|
+
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
|
|
1887
2658
|
// 检查自定义码匹配(遍历所有映射)
|
|
1888
|
-
node.
|
|
2659
|
+
node.debug(`[自定义码检测] 开始匹配, 当前帧hex=${hexStr}, 映射数=${node.mappings.length}`);
|
|
1889
2660
|
for (const mapping of node.mappings) {
|
|
1890
2661
|
if (mapping.brand === 'custom' && mapping.customCodes) {
|
|
1891
2662
|
const codes = mapping.customCodes;
|
|
1892
2663
|
let matchedAction = null;
|
|
1893
2664
|
|
|
1894
|
-
node.
|
|
2665
|
+
node.debug(`[自定义码检测] 检查映射: device=${mapping.device}, meshMac=${mapping.meshMac}`);
|
|
1895
2666
|
|
|
1896
2667
|
// 开关类型:匹配recvOn/recvOff
|
|
1897
2668
|
if (mapping.device === 'custom_switch') {
|
|
1898
2669
|
const recvOn = (codes.recvOn || '').replace(/\s/g, '').toUpperCase();
|
|
1899
2670
|
const recvOff = (codes.recvOff || '').replace(/\s/g, '').toUpperCase();
|
|
1900
|
-
node.
|
|
1901
|
-
node.log(`[自定义开关] 包含recvOn? ${hexStr.includes(recvOn)}, 包含recvOff? ${hexStr.includes(recvOff)}`);
|
|
2671
|
+
node.debug(`[自定义开关] recvOn=${recvOn}, recvOff=${recvOff}`);
|
|
1902
2672
|
|
|
1903
2673
|
// 翻转模式:收开码=收关码
|
|
1904
2674
|
if (recvOn && recvOff && recvOn === recvOff && hexStr.includes(recvOn)) {
|
|
@@ -1907,14 +2677,14 @@ module.exports = function(RED) {
|
|
|
1907
2677
|
const stateKey = mapping.meshChannel > 1 ? `switch_${channel}` : 'switch';
|
|
1908
2678
|
const currentState = device?.state?.[stateKey] || false;
|
|
1909
2679
|
matchedAction = { switch: !currentState };
|
|
1910
|
-
node.
|
|
2680
|
+
node.debug(`[自定义开关] 翻转: ${stateKey} ${currentState} -> ${!currentState}`);
|
|
1911
2681
|
} else {
|
|
1912
2682
|
if (recvOn && hexStr.includes(recvOn)) {
|
|
1913
2683
|
matchedAction = { switch: true };
|
|
1914
|
-
node.
|
|
2684
|
+
node.debug(`[自定义开关] 匹配到收开码`);
|
|
1915
2685
|
} else if (recvOff && hexStr.includes(recvOff)) {
|
|
1916
2686
|
matchedAction = { switch: false };
|
|
1917
|
-
node.
|
|
2687
|
+
node.debug(`[自定义开关] 匹配到收关码`);
|
|
1918
2688
|
}
|
|
1919
2689
|
}
|
|
1920
2690
|
}
|
|
@@ -1926,13 +2696,13 @@ module.exports = function(RED) {
|
|
|
1926
2696
|
|
|
1927
2697
|
if (recvOpen && hexStr.includes(recvOpen)) {
|
|
1928
2698
|
matchedAction = { action: 'open' };
|
|
1929
|
-
node.
|
|
2699
|
+
node.debug(`[自定义窗帘] 收开码`);
|
|
1930
2700
|
} else if (recvClose && hexStr.includes(recvClose)) {
|
|
1931
2701
|
matchedAction = { action: 'close' };
|
|
1932
|
-
node.
|
|
2702
|
+
node.debug(`[自定义窗帘] 收关码`);
|
|
1933
2703
|
} else if (recvStop && hexStr.includes(recvStop)) {
|
|
1934
2704
|
matchedAction = { action: 'stop' };
|
|
1935
|
-
node.
|
|
2705
|
+
node.debug(`[自定义窗帘] 收停码`);
|
|
1936
2706
|
}
|
|
1937
2707
|
}
|
|
1938
2708
|
// 空调类型:匹配收码
|
|
@@ -2024,7 +2794,7 @@ module.exports = function(RED) {
|
|
|
2024
2794
|
if (!node.climateCache[mapping.meshMac]) node.climateCache[mapping.meshMac] = {};
|
|
2025
2795
|
node.climateCache[mapping.meshMac].mode = matchedAction.acMode;
|
|
2026
2796
|
}
|
|
2027
|
-
node.
|
|
2797
|
+
node.debug(`[自定义空调] 匹配: ${JSON.stringify(matchedAction)}`);
|
|
2028
2798
|
}
|
|
2029
2799
|
}
|
|
2030
2800
|
// 场景类型:匹配trigger
|
|
@@ -2036,7 +2806,10 @@ module.exports = function(RED) {
|
|
|
2036
2806
|
|
|
2037
2807
|
if (matchedAction) {
|
|
2038
2808
|
// 防死循环:检查是否刚刚从Mesh发送过来
|
|
2039
|
-
|
|
2809
|
+
// 使用映射特定的时间戳,避免其他设备的同步影响
|
|
2810
|
+
const loopKey = `${mapping.meshMac}_${mapping.device}`;
|
|
2811
|
+
if (!node.lastSyncTime) node.lastSyncTime = {};
|
|
2812
|
+
if (node.lastSyncTime[loopKey] && Date.now() - node.lastSyncTime[loopKey] < 500) {
|
|
2040
2813
|
node.debug(`[防循环] 忽略刚刚同步的帧: ${hexFormatted}`);
|
|
2041
2814
|
return;
|
|
2042
2815
|
}
|
|
@@ -2056,6 +2829,9 @@ module.exports = function(RED) {
|
|
|
2056
2829
|
timestamp: new Date().toISOString()
|
|
2057
2830
|
});
|
|
2058
2831
|
|
|
2832
|
+
// 记录同步时间用于防死循环
|
|
2833
|
+
node.lastSyncTime[loopKey] = Date.now();
|
|
2834
|
+
|
|
2059
2835
|
node.queueCommand({
|
|
2060
2836
|
direction: 'modbus-to-mesh',
|
|
2061
2837
|
mapping: mapping,
|
|
@@ -2081,7 +2857,7 @@ module.exports = function(RED) {
|
|
|
2081
2857
|
const fc = frame[1];
|
|
2082
2858
|
const hexFormatted = frame.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
|
|
2083
2859
|
|
|
2084
|
-
node.
|
|
2860
|
+
node.debug(`[Modbus解析] 从机=${slaveAddr}, 功能码=0x${fc.toString(16)}`);
|
|
2085
2861
|
|
|
2086
2862
|
// 查找所有匹配从机地址的映射
|
|
2087
2863
|
const allMappings = node.findAllRS485Mappings(slaveAddr);
|
|
@@ -2090,7 +2866,7 @@ module.exports = function(RED) {
|
|
|
2090
2866
|
return;
|
|
2091
2867
|
}
|
|
2092
2868
|
|
|
2093
|
-
node.
|
|
2869
|
+
node.debug(`[Modbus解析] 找到${allMappings.length}个匹配映射`);
|
|
2094
2870
|
|
|
2095
2871
|
// 防死循环:检查是否刚刚从Mesh发送过来
|
|
2096
2872
|
if (node.lastMeshToRS485Time && Date.now() - node.lastMeshToRS485Time < 500) {
|
|
@@ -2114,7 +2890,7 @@ module.exports = function(RED) {
|
|
|
2114
2890
|
rs485ChannelFromReg = regAddr - 0x1030; // 0x1031->1, 0x1032->2, ...
|
|
2115
2891
|
}
|
|
2116
2892
|
|
|
2117
|
-
node.
|
|
2893
|
+
node.debug(`[Modbus解析] 寄存器=0x${regAddr.toString(16).toUpperCase()}, 值=${value}, rs485Channel=${rs485ChannelFromReg}`);
|
|
2118
2894
|
|
|
2119
2895
|
// 遍历所有匹配的映射,只处理rs485Channel匹配的
|
|
2120
2896
|
for (const mapping of allMappings) {
|
|
@@ -2145,14 +2921,14 @@ module.exports = function(RED) {
|
|
|
2145
2921
|
state[key] = reg.map[value] || value;
|
|
2146
2922
|
} else {
|
|
2147
2923
|
state[key] = value;
|
|
2924
|
+
node.debug(`[RS485] 从机${slaveAddr} 寄存器0x${regAddr.toString(16).toUpperCase()}: ${key}=${value}`);
|
|
2148
2925
|
}
|
|
2149
|
-
node.log(`[RS485] 从机${slaveAddr} 寄存器0x${regAddr.toString(16).toUpperCase()}: ${key}=${value}`);
|
|
2150
2926
|
break;
|
|
2151
2927
|
}
|
|
2152
2928
|
}
|
|
2153
2929
|
|
|
2154
2930
|
if (Object.keys(state).length > 0) {
|
|
2155
|
-
node.
|
|
2931
|
+
node.debug(`[RS485->Mesh] 映射匹配: meshMac=${mapping.meshMac}, meshCH=${mapping.meshChannel}, rs485CH=${mappingRs485Channel}`);
|
|
2156
2932
|
|
|
2157
2933
|
// 输出调试信息到节点输出端口
|
|
2158
2934
|
node.send({
|