node-red-contrib-symi-mesh 1.6.5 → 1.6.6
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 -27
- package/lib/device-manager.js +30 -3
- package/lib/mqtt-helper.js +3 -3
- package/lib/tcp-client.js +29 -13
- package/nodes/symi-485-bridge.html +15 -3
- package/nodes/symi-485-bridge.js +747 -206
- package/nodes/symi-485-config.js +54 -4
- package/nodes/symi-device.js +7 -7
- package/nodes/symi-gateway.js +86 -14
- package/nodes/symi-mqtt.js +22 -15
- package/package.json +2 -2
package/nodes/symi-485-bridge.js
CHANGED
|
@@ -180,6 +180,46 @@ module.exports = function(RED) {
|
|
|
180
180
|
}
|
|
181
181
|
},
|
|
182
182
|
// ===== 地暖 (A3B3协议头) =====
|
|
183
|
+
// 从机地址: 客餐厅=0x3C(60), 主卧=0x3D(61), 次卧1=0x3E(62), 次卧2=0x3F(63)
|
|
184
|
+
// 开关: 0x0039, 开=0x02, 关=0x00
|
|
185
|
+
// 温度: 0x0043
|
|
186
|
+
'floor_heating_living': {
|
|
187
|
+
name: '客餐厅地暖',
|
|
188
|
+
type: 'climate',
|
|
189
|
+
defaultAddress: 0x3C,
|
|
190
|
+
registers: {
|
|
191
|
+
switch: { address: 0x0039, type: 'holding', on: 2, off: 0 },
|
|
192
|
+
targetTemp: { address: 0x0043, type: 'holding' }
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
'floor_heating_master': {
|
|
196
|
+
name: '主卧地暖',
|
|
197
|
+
type: 'climate',
|
|
198
|
+
defaultAddress: 0x3D,
|
|
199
|
+
registers: {
|
|
200
|
+
switch: { address: 0x0039, type: 'holding', on: 2, off: 0 },
|
|
201
|
+
targetTemp: { address: 0x0043, type: 'holding' }
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
'floor_heating_bedroom2_1': {
|
|
205
|
+
name: '次卧1地暖',
|
|
206
|
+
type: 'climate',
|
|
207
|
+
defaultAddress: 0x3E,
|
|
208
|
+
registers: {
|
|
209
|
+
switch: { address: 0x0039, type: 'holding', on: 2, off: 0 },
|
|
210
|
+
targetTemp: { address: 0x0043, type: 'holding' }
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
'floor_heating_bedroom2_2': {
|
|
214
|
+
name: '次卧2地暖',
|
|
215
|
+
type: 'climate',
|
|
216
|
+
defaultAddress: 0x3F,
|
|
217
|
+
registers: {
|
|
218
|
+
switch: { address: 0x0039, type: 'holding', on: 2, off: 0 },
|
|
219
|
+
targetTemp: { address: 0x0043, type: 'holding' }
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
// 通用地暖(自定义从机地址)
|
|
183
223
|
'floor_heating': {
|
|
184
224
|
name: '地暖',
|
|
185
225
|
type: 'climate',
|
|
@@ -189,9 +229,13 @@ module.exports = function(RED) {
|
|
|
189
229
|
}
|
|
190
230
|
},
|
|
191
231
|
// ===== 新风 (A3B3协议头) =====
|
|
232
|
+
// 从机地址: 0x3C (60) - 与客餐厅地暖共用从机
|
|
233
|
+
// 开关: 0x0039, 开=0x01, 关=0x00 (注意:与地暖开关值不同)
|
|
234
|
+
// 风速: 0x004B, 高=0x02, 低=0x00
|
|
192
235
|
'fresh_air': {
|
|
193
236
|
name: '新风',
|
|
194
237
|
type: 'fan',
|
|
238
|
+
defaultAddress: 0x3C,
|
|
195
239
|
registers: {
|
|
196
240
|
switch: { address: 0x0039, type: 'holding', on: 1, off: 0 },
|
|
197
241
|
fanSpeed: { address: 0x004B, type: 'holding', map: { 0: 'low', 2: 'high' } }
|
|
@@ -374,6 +418,8 @@ module.exports = function(RED) {
|
|
|
374
418
|
node.mappings.forEach((m, i) => {
|
|
375
419
|
if (m.brand === 'duya') {
|
|
376
420
|
node.log(`[映射${i+1}] Mesh: ${m.meshMac} <-> 杜亚窗帘: 地址${m.addrHigh.toString(16).padStart(2,'0')} ${m.addrLow.toString(16).padStart(2,'0')}`);
|
|
421
|
+
} else if (m.brand === 'custom' && m.customCodes) {
|
|
422
|
+
node.log(`[映射${i+1}] Mesh: ${m.meshMac} <-> 自定义: ${m.device}, codes=${JSON.stringify(m.customCodes)}`);
|
|
377
423
|
} else {
|
|
378
424
|
node.log(`[映射${i+1}] Mesh: ${m.meshMac} CH${m.meshChannel} <-> RS485: 从机${m.address} ${m.brand}/${m.device} CH${m.rs485Channel}`);
|
|
379
425
|
}
|
|
@@ -452,11 +498,17 @@ module.exports = function(RED) {
|
|
|
452
498
|
);
|
|
453
499
|
};
|
|
454
500
|
|
|
455
|
-
// Find mapping for RS485 device
|
|
501
|
+
// Find mapping for RS485 device (返回第一个匹配,用于向后兼容)
|
|
456
502
|
node.findRS485Mapping = function(address) {
|
|
457
503
|
const addr = parseInt(address);
|
|
458
504
|
return node.mappings.find(m => m.address === addr);
|
|
459
505
|
};
|
|
506
|
+
|
|
507
|
+
// Find ALL mappings for RS485 device address (用于RS485->Mesh同步)
|
|
508
|
+
node.findAllRS485Mappings = function(address) {
|
|
509
|
+
const addr = parseInt(address);
|
|
510
|
+
return node.mappings.filter(m => m.address === addr);
|
|
511
|
+
};
|
|
460
512
|
|
|
461
513
|
// 获取映射的寄存器配置
|
|
462
514
|
node.getRegistersForMapping = function(mapping) {
|
|
@@ -488,6 +540,7 @@ module.exports = function(RED) {
|
|
|
488
540
|
if (!node.stateCache[mac]) node.stateCache[mac] = {};
|
|
489
541
|
const cached = node.stateCache[mac];
|
|
490
542
|
const changed = {};
|
|
543
|
+
const isFirstState = Object.keys(cached).length === 0; // 首次收到该设备状态
|
|
491
544
|
|
|
492
545
|
for (const [key, value] of Object.entries(state)) {
|
|
493
546
|
if (cached[key] !== value) {
|
|
@@ -498,11 +551,25 @@ module.exports = function(RED) {
|
|
|
498
551
|
|
|
499
552
|
if (Object.keys(changed).length === 0) return; // 无变化
|
|
500
553
|
|
|
554
|
+
// 首次收到设备状态时只记录缓存,不触发同步(避免启动时批量发码)
|
|
555
|
+
// 但窗帘状态(curtainStatus)除外,因为这是用户的控制命令
|
|
556
|
+
const hasCurtainStatusChange = changed.curtainStatus !== undefined;
|
|
557
|
+
if (isFirstState && !hasCurtainStatusChange) {
|
|
558
|
+
node.debug(`[Mesh事件] MAC=${mac} 首次状态,仅缓存: ${JSON.stringify(changed)}`);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
501
562
|
node.log(`[Mesh事件] MAC=${mac}, 变化: ${JSON.stringify(changed)}`);
|
|
502
563
|
|
|
564
|
+
// 规范化MAC地址用于比较
|
|
565
|
+
const macNormalized = mac.toLowerCase().replace(/:/g, '');
|
|
566
|
+
|
|
503
567
|
// 遍历映射,只处理有对应变化的映射
|
|
504
568
|
for (const mapping of node.mappings) {
|
|
505
|
-
|
|
569
|
+
const mappingMacNormalized = (mapping.meshMac || '').toLowerCase().replace(/:/g, '');
|
|
570
|
+
if (mappingMacNormalized !== macNormalized) continue;
|
|
571
|
+
|
|
572
|
+
node.log(`[Mesh事件] 匹配到映射: brand=${mapping.brand}, device=${mapping.device}, codes=${JSON.stringify(mapping.customCodes)}`);
|
|
506
573
|
|
|
507
574
|
const configChannel = mapping.meshChannel || 1;
|
|
508
575
|
const switchKey = `switch_${configChannel}`;
|
|
@@ -513,9 +580,221 @@ module.exports = function(RED) {
|
|
|
513
580
|
const isCurtain = device.includes('curtain') || mapping.brand === 'duya';
|
|
514
581
|
const isAC = device.includes('ac') || device.includes('climate') || device.includes('thermostat');
|
|
515
582
|
|
|
583
|
+
// 【杜亚窗帘】处理Mesh面板控制同步到485
|
|
584
|
+
// 根据协议文档:
|
|
585
|
+
// - subOpcode=0x05 (NODE_ACK) = 设备确认执行命令,是用户控制
|
|
586
|
+
// - subOpcode=0x06 (NODE_STATUS) 在0xB0后1500ms内也是用户控制
|
|
587
|
+
// - attrType=0x05 (CURT_RUN_STATUS) = 状态: 1=开, 2=关, 3=停
|
|
588
|
+
// - attrType=0x06 (CURT_RUN_PER_POS) = 位置: 0-100%
|
|
589
|
+
if (mapping.brand === 'duya') {
|
|
590
|
+
const now = Date.now();
|
|
591
|
+
const curtainKey = `duya_curtain_${mac}`;
|
|
592
|
+
|
|
593
|
+
// 只处理用户控制帧
|
|
594
|
+
if (!eventData.isUserControl) {
|
|
595
|
+
continue; // 非用户控制(电机反馈),忽略
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const addrHigh = mapping.addrHigh || 1;
|
|
599
|
+
const addrLow = mapping.addrLow || 1;
|
|
600
|
+
|
|
601
|
+
// 获取设备状态
|
|
602
|
+
const device = node.gateway.deviceManager.getDeviceByMac(mac);
|
|
603
|
+
const currentPosition = device ? device.state.curtainPosition : null;
|
|
604
|
+
|
|
605
|
+
// 百分比控制 (attrType=0x06)
|
|
606
|
+
// 只记录位置,不立即发送(等status=2时再判断是否发百分比)
|
|
607
|
+
if (eventData.attrType === 0x06) {
|
|
608
|
+
const position = eventData.parameters && eventData.parameters.length > 0
|
|
609
|
+
? eventData.parameters[0]
|
|
610
|
+
: null;
|
|
611
|
+
if (position !== null && position !== 0xFF) {
|
|
612
|
+
// 记录最新位置,用于status=2时判断
|
|
613
|
+
if (!node.curtainLastPos) node.curtainLastPos = {};
|
|
614
|
+
node.curtainLastPos[curtainKey] = position;
|
|
615
|
+
node.debug(`[Mesh->杜亚] 记录位置: ${position}%`);
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 状态控制 (attrType=0x05)
|
|
621
|
+
if (eventData.attrType !== 0x05) {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const newStatus = eventData.parameters && eventData.parameters.length > 0
|
|
626
|
+
? eventData.parameters[0]
|
|
627
|
+
: null;
|
|
628
|
+
|
|
629
|
+
// 忽略 null 值
|
|
630
|
+
if (newStatus === null) {
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// 1秒防抖:避免重复发送相同状态
|
|
635
|
+
if (!node.curtainDebounce) node.curtainDebounce = {};
|
|
636
|
+
const debounceKey = `${curtainKey}_status_${newStatus}`;
|
|
637
|
+
const lastTime = node.curtainDebounce[debounceKey] || 0;
|
|
638
|
+
if (now - lastTime < 1000) {
|
|
639
|
+
node.debug(`[Mesh->杜亚] 状态${newStatus}在1秒内重复, 忽略`);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
node.curtainDebounce[debounceKey] = now;
|
|
643
|
+
|
|
644
|
+
node.log(`[Mesh->杜亚] 用户控制(subOpcode=0x${eventData.subOpcode.toString(16)}): status=${newStatus}, position=${currentPosition}%`);
|
|
645
|
+
|
|
646
|
+
// 【米家协议状态处理】:
|
|
647
|
+
// status=0 → 打开中 → 发open
|
|
648
|
+
// status=1 → 关闭中 → 发close
|
|
649
|
+
// status=2 → 停止 → 判断位置:
|
|
650
|
+
// - 位置=0% → 关闭到头,不发
|
|
651
|
+
// - 位置=100% → 打开到头,不发
|
|
652
|
+
// - 位置=其他 → 用户暂停或百分比控制,发stop+position
|
|
653
|
+
|
|
654
|
+
let action = null, actionName = '';
|
|
655
|
+
let sendPosition = false;
|
|
656
|
+
|
|
657
|
+
if (newStatus === 0) {
|
|
658
|
+
// 米家: 打开中
|
|
659
|
+
action = 'open';
|
|
660
|
+
actionName = '打开';
|
|
661
|
+
} else if (newStatus === 1) {
|
|
662
|
+
// 米家: 关闭中
|
|
663
|
+
action = 'close';
|
|
664
|
+
actionName = '关闭';
|
|
665
|
+
} else if (newStatus === 2) {
|
|
666
|
+
// 米家: 停止
|
|
667
|
+
const pos = currentPosition !== null ? currentPosition : (node.curtainLastPos ? node.curtainLastPos[curtainKey] : 50);
|
|
668
|
+
if (pos <= 2) {
|
|
669
|
+
// 关闭到头,不发命令
|
|
670
|
+
node.debug(`[Mesh->杜亚] 关闭到头(${pos}%),不发命令`);
|
|
671
|
+
continue;
|
|
672
|
+
} else if (pos >= 98) {
|
|
673
|
+
// 打开到头,不发命令
|
|
674
|
+
node.debug(`[Mesh->杜亚] 打开到头(${pos}%),不发命令`);
|
|
675
|
+
continue;
|
|
676
|
+
} else {
|
|
677
|
+
// 中间位置停止,发stop + position
|
|
678
|
+
action = 'stop';
|
|
679
|
+
actionName = '暂停';
|
|
680
|
+
sendPosition = true;
|
|
681
|
+
}
|
|
682
|
+
} else if (newStatus === 3) {
|
|
683
|
+
// 小程序: 停止
|
|
684
|
+
action = 'stop';
|
|
685
|
+
actionName = '暂停';
|
|
686
|
+
} else {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// 发送动作命令
|
|
691
|
+
if (action) {
|
|
692
|
+
const frame = buildA6B6Frame(addrHigh, addrLow, action);
|
|
693
|
+
node.sendRS485Frame(frame).then(() => {
|
|
694
|
+
const hexStr = frame.toString('hex').toUpperCase().match(/.{2}/g).join(' ');
|
|
695
|
+
node.log(`[Mesh->杜亚] ${actionName}: ${hexStr}`);
|
|
696
|
+
}).catch(err => {
|
|
697
|
+
node.error(`[Mesh->杜亚] 发送失败: ${err.message}`);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// 发送位置命令(暂停时同步位置)
|
|
702
|
+
if (sendPosition && currentPosition !== null && currentPosition > 2 && currentPosition < 98) {
|
|
703
|
+
setTimeout(() => {
|
|
704
|
+
const posFrame = buildA6B6Frame(addrHigh, addrLow, 'position', currentPosition);
|
|
705
|
+
node.sendRS485Frame(posFrame).then(() => {
|
|
706
|
+
const hexStr = posFrame.toString('hex').toUpperCase().match(/.{2}/g).join(' ');
|
|
707
|
+
node.log(`[Mesh->杜亚] 同步位置${currentPosition}%: ${hexStr}`);
|
|
708
|
+
}).catch(err => {
|
|
709
|
+
node.error(`[Mesh->杜亚] 位置同步失败: ${err.message}`);
|
|
710
|
+
});
|
|
711
|
+
}, 100); // 延迟100ms发送位置命令
|
|
712
|
+
}
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// 【自定义窗帘】处理Mesh面板控制同步到485(逻辑同杜亚)
|
|
717
|
+
// 必须检查isUserControl,只有NODE_ACK(subOpcode=0x05)才是用户控制
|
|
718
|
+
if (mapping.brand === 'custom' && mapping.device === 'custom_curtain' && mapping.customCodes) {
|
|
719
|
+
const now = Date.now();
|
|
720
|
+
const curtainKey = `custom_curtain_${mac}`;
|
|
721
|
+
|
|
722
|
+
// 【重要】只处理用户控制帧,忽略电机反馈
|
|
723
|
+
if (!eventData.isUserControl) {
|
|
724
|
+
continue; // 非用户控制(NODE_STATUS),忽略
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// 只处理窗帘状态事件 (attrType=0x05)
|
|
728
|
+
if (eventData.attrType !== 0x05) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const newStatus = eventData.parameters && eventData.parameters.length > 0
|
|
733
|
+
? eventData.parameters[0]
|
|
734
|
+
: null;
|
|
735
|
+
|
|
736
|
+
// 忽略 null 值
|
|
737
|
+
if (newStatus === null) {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// 1秒防抖:避免重复发送相同状态(与杜亚窗帘一致)
|
|
742
|
+
if (!node.curtainDebounce) node.curtainDebounce = {};
|
|
743
|
+
const debounceKey = `${curtainKey}_status`;
|
|
744
|
+
const lastTime = node.curtainDebounce[debounceKey] || 0;
|
|
745
|
+
if (now - lastTime < 1000) {
|
|
746
|
+
node.debug(`[Mesh->自定义窗帘] 状态1秒内重复, 忽略`);
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
node.curtainDebounce[debounceKey] = now;
|
|
750
|
+
|
|
751
|
+
// 【兼容两种协议】:
|
|
752
|
+
// 米家协议: 0=打开中, 1=关闭中, 2=停止
|
|
753
|
+
// 小程序协议: 1=打开, 2=关闭, 3=停止
|
|
754
|
+
// 使用device.state.curtainAction来获取正确的动作
|
|
755
|
+
const device = node.gateway.deviceManager.getDeviceByMac(mac);
|
|
756
|
+
const curtainAction = device ? device.state.curtainAction : null;
|
|
757
|
+
|
|
758
|
+
const codes = mapping.customCodes;
|
|
759
|
+
let hexCode = null, actionName = '';
|
|
760
|
+
|
|
761
|
+
if (curtainAction === 'opening' && codes.open) {
|
|
762
|
+
hexCode = codes.open;
|
|
763
|
+
actionName = '打开';
|
|
764
|
+
} else if (curtainAction === 'closing' && codes.close) {
|
|
765
|
+
hexCode = codes.close;
|
|
766
|
+
actionName = '关闭';
|
|
767
|
+
} else if (curtainAction === 'stopped' && codes.stop) {
|
|
768
|
+
hexCode = codes.stop;
|
|
769
|
+
actionName = '暂停';
|
|
770
|
+
} else {
|
|
771
|
+
// 回退:直接使用原始status值(兼容旧逻辑,小程序协议)
|
|
772
|
+
if (newStatus === 1 && codes.open) {
|
|
773
|
+
hexCode = codes.open; actionName = '打开';
|
|
774
|
+
} else if (newStatus === 2 && codes.close) {
|
|
775
|
+
hexCode = codes.close; actionName = '关闭';
|
|
776
|
+
} else if (newStatus === 3 && codes.stop) {
|
|
777
|
+
hexCode = codes.stop; actionName = '暂停';
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (hexCode) {
|
|
782
|
+
node.sendCustomCode(hexCode).then(() => {
|
|
783
|
+
node.log(`[Mesh->自定义] ${actionName}: ${hexCode}`);
|
|
784
|
+
}).catch(err => {
|
|
785
|
+
node.error(`[Mesh->自定义] 发送失败: ${err.message}`);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// 其他窗帘类型标记
|
|
792
|
+
const isCurtainControlled = false;
|
|
793
|
+
|
|
516
794
|
// 只有对应类型的状态变化才触发对应类型的映射
|
|
517
795
|
const hasSwitchChange = isSwitch && changed[switchKey] !== undefined;
|
|
518
|
-
|
|
796
|
+
// 窗帘:如果是杜亚窗帘,跳过状态变化处理(已在curtain-control中处理)
|
|
797
|
+
const hasCurtainChange = isCurtain && !isCurtainControlled && (
|
|
519
798
|
changed.curtainAction !== undefined ||
|
|
520
799
|
changed.curtainPosition !== undefined ||
|
|
521
800
|
changed.curtainStatus !== undefined
|
|
@@ -523,10 +802,26 @@ module.exports = function(RED) {
|
|
|
523
802
|
const hasACChange = isAC && (
|
|
524
803
|
changed.targetTemp !== undefined ||
|
|
525
804
|
changed.acMode !== undefined ||
|
|
526
|
-
changed.acFanSpeed !== undefined
|
|
805
|
+
changed.acFanSpeed !== undefined ||
|
|
806
|
+
// 三合一面板使用climate前缀
|
|
807
|
+
changed.climateSwitch !== undefined ||
|
|
808
|
+
changed.climateMode !== undefined ||
|
|
809
|
+
changed.fanMode !== undefined
|
|
810
|
+
);
|
|
811
|
+
// 新风设备检测
|
|
812
|
+
const isFreshAir = device.includes('fresh_air');
|
|
813
|
+
const hasFreshAirChange = isFreshAir && (
|
|
814
|
+
changed.freshAirSwitch !== undefined ||
|
|
815
|
+
changed.freshAirSpeed !== undefined
|
|
816
|
+
);
|
|
817
|
+
// 地暖设备检测
|
|
818
|
+
const isFloorHeating = device.includes('floor_heating');
|
|
819
|
+
const hasFloorHeatingChange = isFloorHeating && (
|
|
820
|
+
changed.floorHeatingSwitch !== undefined ||
|
|
821
|
+
changed.floorHeatingTemp !== undefined
|
|
527
822
|
);
|
|
528
823
|
|
|
529
|
-
if (!hasSwitchChange && !hasCurtainChange && !hasACChange) {
|
|
824
|
+
if (!hasSwitchChange && !hasCurtainChange && !hasACChange && !hasFreshAirChange && !hasFloorHeatingChange) {
|
|
530
825
|
continue;
|
|
531
826
|
}
|
|
532
827
|
|
|
@@ -558,66 +853,119 @@ module.exports = function(RED) {
|
|
|
558
853
|
};
|
|
559
854
|
|
|
560
855
|
// 窗帘控制命令处理(立即同步,不等状态反馈)
|
|
856
|
+
// 同时支持杜亚窗帘和自定义窗帘
|
|
561
857
|
const handleCurtainControl = (eventData) => {
|
|
562
858
|
const mac = eventData.mac;
|
|
563
859
|
|
|
564
860
|
node.log(`[curtain-control事件] MAC=${mac}, action=${eventData.action}, position=${eventData.position}`);
|
|
565
861
|
|
|
566
|
-
//
|
|
862
|
+
// 查找窗帘映射(杜亚或自定义窗帘)
|
|
567
863
|
const macNormalized = mac.toLowerCase().replace(/:/g, '');
|
|
568
864
|
for (const mapping of node.mappings) {
|
|
569
865
|
const mappingMacNormalized = (mapping.meshMac || '').toLowerCase().replace(/:/g, '');
|
|
570
|
-
if (mappingMacNormalized !== macNormalized
|
|
866
|
+
if (mappingMacNormalized !== macNormalized) continue;
|
|
571
867
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
868
|
+
// ===== 杜亚窗帘 =====
|
|
869
|
+
if (mapping.brand === 'duya') {
|
|
870
|
+
node.log(`[curtain-control] 匹配到杜亚映射: ${mapping.meshMac}`);
|
|
871
|
+
|
|
872
|
+
const addrHigh = mapping.addrHigh || 1;
|
|
873
|
+
const addrLow = mapping.addrLow || 1;
|
|
874
|
+
|
|
875
|
+
let frame = null;
|
|
876
|
+
let actionName = '';
|
|
877
|
+
|
|
878
|
+
// 处理动作命令 (attrType=0x05)
|
|
879
|
+
if (eventData.action !== null) {
|
|
880
|
+
if (eventData.action === 1) {
|
|
881
|
+
frame = buildA6B6Frame(addrHigh, addrLow, 'open');
|
|
882
|
+
actionName = '打开';
|
|
883
|
+
} else if (eventData.action === 2) {
|
|
884
|
+
frame = buildA6B6Frame(addrHigh, addrLow, 'close');
|
|
885
|
+
actionName = '关闭';
|
|
886
|
+
} else if (eventData.action === 3) {
|
|
887
|
+
frame = buildA6B6Frame(addrHigh, addrLow, 'stop');
|
|
888
|
+
actionName = '暂停';
|
|
889
|
+
}
|
|
591
890
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
891
|
+
// 处理位置命令 (attrType=0x06)
|
|
892
|
+
else if (eventData.position !== null) {
|
|
893
|
+
const pos = eventData.position;
|
|
894
|
+
if (pos >= 95) {
|
|
895
|
+
frame = buildA6B6Frame(addrHigh, addrLow, 'open');
|
|
896
|
+
actionName = '打开';
|
|
897
|
+
} else if (pos <= 5) {
|
|
898
|
+
frame = buildA6B6Frame(addrHigh, addrLow, 'close');
|
|
899
|
+
actionName = '关闭';
|
|
900
|
+
} else {
|
|
901
|
+
frame = buildA6B6Frame(addrHigh, addrLow, 'position', pos);
|
|
902
|
+
actionName = `位置${pos}%`;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (frame) {
|
|
907
|
+
// 立即发送,记录时间防止状态反馈重复发码
|
|
908
|
+
node.lastCurtainControlTime = Date.now();
|
|
909
|
+
node.lastMeshToRS485Time = Date.now();
|
|
910
|
+
if (!node.lastSentTime) node.lastSentTime = {};
|
|
911
|
+
node.lastSentTime[`duya_${mac}_${actionName}`] = Date.now();
|
|
912
|
+
|
|
913
|
+
node.sendRS485Frame(frame).then(() => {
|
|
914
|
+
const hexStr = frame.toString('hex').toUpperCase();
|
|
915
|
+
node.log(`[Mesh控制->杜亚] 窗帘 ${actionName}, 立即发送: ${hexStr.match(/.{2}/g).join(' ')}`);
|
|
916
|
+
}).catch(err => {
|
|
917
|
+
node.error(`[Mesh控制->杜亚] 发送失败: ${err.message}`);
|
|
918
|
+
});
|
|
605
919
|
}
|
|
606
920
|
}
|
|
607
|
-
|
|
608
|
-
if (
|
|
609
|
-
|
|
610
|
-
node.lastCurtainControlTime = Date.now(); // 专门用于curtain-control事件
|
|
611
|
-
node.lastMeshToRS485Time = Date.now();
|
|
612
|
-
if (!node.lastSentTime) node.lastSentTime = {};
|
|
613
|
-
node.lastSentTime[`duya_${mac}_${actionName}`] = Date.now();
|
|
921
|
+
// ===== 自定义窗帘 =====
|
|
922
|
+
else if (mapping.brand === 'custom' && mapping.device === 'custom_curtain' && mapping.customCodes) {
|
|
923
|
+
const codes = mapping.customCodes;
|
|
614
924
|
|
|
615
|
-
node.
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
925
|
+
node.log(`[curtain-control] 匹配到自定义窗帘映射: ${mapping.meshMac}`);
|
|
926
|
+
|
|
927
|
+
let hexCode = null;
|
|
928
|
+
let actionName = '';
|
|
929
|
+
|
|
930
|
+
// 处理动作命令 (attrType=0x05): 1=打开, 2=关闭, 3=停止
|
|
931
|
+
if (eventData.action !== null) {
|
|
932
|
+
if (eventData.action === 1 && codes.open) {
|
|
933
|
+
hexCode = codes.open;
|
|
934
|
+
actionName = '打开';
|
|
935
|
+
} else if (eventData.action === 2 && codes.close) {
|
|
936
|
+
hexCode = codes.close;
|
|
937
|
+
actionName = '关闭';
|
|
938
|
+
} else if (eventData.action === 3 && codes.stop) {
|
|
939
|
+
hexCode = codes.stop;
|
|
940
|
+
actionName = '暂停';
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
// 处理位置命令 (attrType=0x06)
|
|
944
|
+
else if (eventData.position !== null) {
|
|
945
|
+
const pos = eventData.position;
|
|
946
|
+
if (pos >= 95 && codes.open) {
|
|
947
|
+
hexCode = codes.open;
|
|
948
|
+
actionName = '打开';
|
|
949
|
+
} else if (pos <= 5 && codes.close) {
|
|
950
|
+
hexCode = codes.close;
|
|
951
|
+
actionName = '关闭';
|
|
952
|
+
}
|
|
953
|
+
// 自定义窗帘不支持百分比位置,只能发开/关
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (hexCode) {
|
|
957
|
+
// 立即发送,记录时间防止状态反馈重复发码
|
|
958
|
+
node.lastCurtainControlTime = Date.now();
|
|
959
|
+
node.lastMeshToRS485Time = Date.now();
|
|
960
|
+
if (!node.lastSentTime) node.lastSentTime = {};
|
|
961
|
+
node.lastSentTime[`custom_curtain_${mac}_${hexCode}`] = Date.now();
|
|
962
|
+
|
|
963
|
+
node.sendCustomCode(hexCode).then(() => {
|
|
964
|
+
node.log(`[Mesh控制->自定义] 窗帘 ${actionName}, 立即发送: ${hexCode}`);
|
|
965
|
+
}).catch(err => {
|
|
966
|
+
node.error(`[Mesh控制->自定义] 发送失败: ${err.message}`);
|
|
967
|
+
});
|
|
968
|
+
}
|
|
621
969
|
}
|
|
622
970
|
}
|
|
623
971
|
};
|
|
@@ -642,16 +990,30 @@ module.exports = function(RED) {
|
|
|
642
990
|
});
|
|
643
991
|
};
|
|
644
992
|
|
|
645
|
-
//
|
|
993
|
+
// 命令队列顺序处理(限制最大100条,防止内存溢出)
|
|
994
|
+
const MAX_QUEUE_SIZE = 100;
|
|
646
995
|
node.queueCommand = function(cmd) {
|
|
647
|
-
//
|
|
996
|
+
// 队列过大时丢弃旧命令,防止内存溢出
|
|
997
|
+
if (node.commandQueue.length >= MAX_QUEUE_SIZE) {
|
|
998
|
+
node.warn(`[RS485 Bridge] 命令队列已满(${MAX_QUEUE_SIZE}),丢弃最旧命令`);
|
|
999
|
+
node.commandQueue.shift(); // 丢弃最旧的命令
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// 检查队列中是否有完全相同映射的命令(防抖)
|
|
1003
|
+
// 必须是同一个映射(meshMac + meshChannel + address + rs485Channel)才能合并
|
|
648
1004
|
const existing = node.commandQueue.find(c =>
|
|
649
1005
|
c.direction === cmd.direction &&
|
|
1006
|
+
c.mapping && cmd.mapping &&
|
|
1007
|
+
c.mapping.meshMac === cmd.mapping.meshMac &&
|
|
1008
|
+
c.mapping.meshChannel === cmd.mapping.meshChannel &&
|
|
1009
|
+
c.mapping.address === cmd.mapping.address &&
|
|
1010
|
+
c.mapping.rs485Channel === cmd.mapping.rs485Channel &&
|
|
650
1011
|
Date.now() - c.timestamp < 100
|
|
651
1012
|
);
|
|
652
1013
|
if (existing) {
|
|
653
|
-
//
|
|
1014
|
+
// 合并状态(仅限完全相同的映射)
|
|
654
1015
|
existing.state = { ...existing.state, ...cmd.state };
|
|
1016
|
+
node.debug(`[队列] 合并相同映射的命令: ${cmd.mapping.meshMac} CH${cmd.mapping.meshChannel}`);
|
|
655
1017
|
return;
|
|
656
1018
|
}
|
|
657
1019
|
|
|
@@ -740,24 +1102,32 @@ module.exports = function(RED) {
|
|
|
740
1102
|
let frame = null;
|
|
741
1103
|
let actionName = '';
|
|
742
1104
|
|
|
743
|
-
|
|
1105
|
+
// 【兼容两种协议】:
|
|
1106
|
+
// 米家协议: 0=打开中, 1=关闭中, 2=停止
|
|
1107
|
+
// 小程序协议: 1=打开, 2=关闭, 3=停止
|
|
1108
|
+
// 使用curtainAction来判断动作(由device-manager解析)
|
|
1109
|
+
const curtainAction = state.curtainAction;
|
|
744
1110
|
|
|
745
|
-
|
|
746
|
-
|
|
1111
|
+
if (curtainAction === 'opening') {
|
|
1112
|
+
frame = buildA6B6Frame(addrHigh, addrLow, 'open');
|
|
1113
|
+
actionName = '打开';
|
|
1114
|
+
} else if (curtainAction === 'closing') {
|
|
1115
|
+
frame = buildA6B6Frame(addrHigh, addrLow, 'close');
|
|
1116
|
+
actionName = '关闭';
|
|
1117
|
+
} else if (curtainAction === 'stopped') {
|
|
747
1118
|
frame = buildA6B6Frame(addrHigh, addrLow, 'stop');
|
|
748
1119
|
actionName = '暂停';
|
|
749
1120
|
}
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
}
|
|
1121
|
+
// 回退:如果curtainAction未设置,使用原始status值(小程序协议)
|
|
1122
|
+
else if (currentStatus === 1) {
|
|
1123
|
+
frame = buildA6B6Frame(addrHigh, addrLow, 'open');
|
|
1124
|
+
actionName = '打开';
|
|
1125
|
+
} else if (currentStatus === 2) {
|
|
1126
|
+
frame = buildA6B6Frame(addrHigh, addrLow, 'close');
|
|
1127
|
+
actionName = '关闭';
|
|
1128
|
+
} else if (currentStatus === 3) {
|
|
1129
|
+
frame = buildA6B6Frame(addrHigh, addrLow, 'stop');
|
|
1130
|
+
actionName = '暂停';
|
|
761
1131
|
}
|
|
762
1132
|
// 3. 只有位置变化(没有状态变化),中间位置发百分比码
|
|
763
1133
|
else if (state.curtainPosition !== undefined &&
|
|
@@ -932,8 +1302,8 @@ module.exports = function(RED) {
|
|
|
932
1302
|
node.log(`[Mesh->RS485] 从机${mapping.address} 按键${rs485Channel}: ${value ? '开' : '关'}`);
|
|
933
1303
|
}
|
|
934
1304
|
}
|
|
935
|
-
// 空调开关
|
|
936
|
-
else if (meshKey === 'acSwitch' && registers && registers.switch) {
|
|
1305
|
+
// 空调开关 (支持acSwitch和climateSwitch两种字段名)
|
|
1306
|
+
else if ((meshKey === 'acSwitch' || meshKey === 'climateSwitch') && registers && registers.switch) {
|
|
937
1307
|
const writeValue = value ? (registers.switch.on !== undefined ? registers.switch.on : 1) :
|
|
938
1308
|
(registers.switch.off !== undefined ? registers.switch.off : 0);
|
|
939
1309
|
await node.writeModbusRegister(mapping.address, registers.switch, writeValue);
|
|
@@ -944,46 +1314,100 @@ module.exports = function(RED) {
|
|
|
944
1314
|
await node.writeModbusRegister(mapping.address, registers.targetTemp, value);
|
|
945
1315
|
node.log(`[Mesh->RS485] 目标温度: ${value}°C`);
|
|
946
1316
|
}
|
|
947
|
-
// 空调模式
|
|
948
|
-
// Mesh
|
|
949
|
-
//
|
|
950
|
-
else if (meshKey === 'acMode' && registers && registers.mode) {
|
|
951
|
-
//
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1317
|
+
// 空调模式 (支持acMode和climateMode两种字段名)
|
|
1318
|
+
// Mesh协议: 1=制冷, 2=制热, 3=送风, 4=除湿
|
|
1319
|
+
// RS485(话语前湾): 1=制热, 2=制冷, 4=送风, 8=除湿
|
|
1320
|
+
else if ((meshKey === 'acMode' || meshKey === 'climateMode') && registers && registers.mode) {
|
|
1321
|
+
// Mesh值转RS485值
|
|
1322
|
+
const meshToRs485 = { 1: 2, 2: 1, 3: 4, 4: 8 }; // 1=cool->2, 2=heat->1, 3=fan->4, 4=dry->8
|
|
1323
|
+
let rs485Value = meshToRs485[value];
|
|
1324
|
+
if (rs485Value === undefined) {
|
|
1325
|
+
// 尝试使用寄存器map反向映射
|
|
1326
|
+
if (registers.mode.map) {
|
|
1327
|
+
const modeNames = { 1: 'cool', 2: 'heat', 3: 'fan', 4: 'dry' };
|
|
1328
|
+
const meshModeName = modeNames[value] || value;
|
|
1329
|
+
const found = Object.entries(registers.mode.map).find(([k, v]) => v === meshModeName);
|
|
1330
|
+
if (found) {
|
|
1331
|
+
rs485Value = parseInt(found[0]);
|
|
1332
|
+
}
|
|
959
1333
|
}
|
|
960
1334
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
//
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1335
|
+
if (rs485Value !== undefined) {
|
|
1336
|
+
await node.writeModbusRegister(mapping.address, registers.mode, rs485Value);
|
|
1337
|
+
node.log(`[Mesh->RS485] 空调模式: Mesh值${value} -> RS485值${rs485Value}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
// 风速 (支持acFanSpeed和fanMode两种字段名)
|
|
1341
|
+
// 三合一0x94协议: 0=自动, 1=低, 2=中, 4=高
|
|
1342
|
+
// RS485(话语前湾): 1=低风, 2=中风, 3=高风
|
|
1343
|
+
else if ((meshKey === 'acFanSpeed' || meshKey === 'fanMode') && registers && registers.fanSpeed) {
|
|
1344
|
+
// 三合一0x94协议值转RS485值: 1=低->1, 2=中->2, 4=高->3, 0=自动->1(默认低)
|
|
1345
|
+
const meshToRs485 = { 0: 1, 1: 1, 2: 2, 4: 3 };
|
|
1346
|
+
let rs485Value = meshToRs485[value];
|
|
1347
|
+
if (rs485Value === undefined) {
|
|
1348
|
+
// 尝试使用寄存器map反向映射
|
|
1349
|
+
if (registers.fanSpeed.map) {
|
|
1350
|
+
const speedNames = { 0: 'auto', 1: 'low', 2: 'medium', 4: 'high' };
|
|
1351
|
+
const meshSpeedName = speedNames[value] || value;
|
|
1352
|
+
const found = Object.entries(registers.fanSpeed.map).find(([k, v]) => v === meshSpeedName);
|
|
1353
|
+
if (found) {
|
|
1354
|
+
rs485Value = parseInt(found[0]);
|
|
1355
|
+
}
|
|
975
1356
|
}
|
|
976
|
-
} else {
|
|
977
|
-
// 默认直接使用Mesh值
|
|
978
|
-
rs485Value = value;
|
|
979
1357
|
}
|
|
980
|
-
|
|
981
|
-
|
|
1358
|
+
if (rs485Value !== undefined) {
|
|
1359
|
+
await node.writeModbusRegister(mapping.address, registers.fanSpeed, rs485Value);
|
|
1360
|
+
node.log(`[Mesh->RS485] 空调风速: Mesh值${value} -> RS485值${rs485Value}`);
|
|
1361
|
+
}
|
|
982
1362
|
}
|
|
983
1363
|
else if (meshKey === 'brightness' && registers && registers.brightness) {
|
|
984
1364
|
await node.writeModbusRegister(mapping.address, registers.brightness, value);
|
|
985
1365
|
node.debug(`[Mesh->RS485] 亮度: ${value}`);
|
|
986
1366
|
}
|
|
1367
|
+
// ===== 地暖控制 =====
|
|
1368
|
+
// Mesh状态: floorHeatingSwitch (true/false), floorHeatingTemp (18-32)
|
|
1369
|
+
else if (meshKey === 'floorHeatingSwitch' && registers && registers.switch) {
|
|
1370
|
+
const writeValue = value ? (registers.switch.on !== undefined ? registers.switch.on : 2) :
|
|
1371
|
+
(registers.switch.off !== undefined ? registers.switch.off : 0);
|
|
1372
|
+
await node.writeModbusRegister(mapping.address, registers.switch, writeValue);
|
|
1373
|
+
node.log(`[Mesh->RS485] 地暖开关: ${value ? '开' : '关'}, 值=${writeValue}`);
|
|
1374
|
+
}
|
|
1375
|
+
else if (meshKey === 'floorHeatingTemp' && registers && registers.targetTemp) {
|
|
1376
|
+
await node.writeModbusRegister(mapping.address, registers.targetTemp, value);
|
|
1377
|
+
node.log(`[Mesh->RS485] 地暖温度: ${value}°C`);
|
|
1378
|
+
}
|
|
1379
|
+
// ===== 新风控制 =====
|
|
1380
|
+
// Mesh状态: freshAirSwitch (true/false), freshAirSpeed (1=高,2=中,3=低,4=自动)
|
|
1381
|
+
else if (meshKey === 'freshAirSwitch' && registers && registers.switch) {
|
|
1382
|
+
const writeValue = value ? (registers.switch.on !== undefined ? registers.switch.on : 1) :
|
|
1383
|
+
(registers.switch.off !== undefined ? registers.switch.off : 0);
|
|
1384
|
+
await node.writeModbusRegister(mapping.address, registers.switch, writeValue);
|
|
1385
|
+
node.log(`[Mesh->RS485] 新风开关: ${value ? '开' : '关'}, 值=${writeValue}`);
|
|
1386
|
+
}
|
|
1387
|
+
else if (meshKey === 'freshAirSpeed' && registers && registers.fanSpeed) {
|
|
1388
|
+
// Mesh新风风速: 1=高, 2=中, 3=低, 4=自动
|
|
1389
|
+
// 话语前湾新风: 0=低速, 2=高速
|
|
1390
|
+
const meshToRs485 = { 1: 2, 2: 2, 3: 0, 4: 0 }; // 高/中->高速, 低/自动->低速
|
|
1391
|
+
const rs485Value = meshToRs485[value] !== undefined ? meshToRs485[value] : 0;
|
|
1392
|
+
await node.writeModbusRegister(mapping.address, registers.fanSpeed, rs485Value);
|
|
1393
|
+
node.log(`[Mesh->RS485] 新风风速: Mesh值${value} -> RS485值${rs485Value}`);
|
|
1394
|
+
}
|
|
1395
|
+
// ===== 空调开关(兼容climateSwitch) =====
|
|
1396
|
+
else if (meshKey === 'climateSwitch' && registers && registers.switch) {
|
|
1397
|
+
const writeValue = value ? (registers.switch.on !== undefined ? registers.switch.on : 1) :
|
|
1398
|
+
(registers.switch.off !== undefined ? registers.switch.off : 0);
|
|
1399
|
+
await node.writeModbusRegister(mapping.address, registers.switch, writeValue);
|
|
1400
|
+
node.log(`[Mesh->RS485] 空调开关(climateSwitch): ${value ? '开' : '关'}`);
|
|
1401
|
+
}
|
|
1402
|
+
// ===== 空调模式(兼容climateMode) =====
|
|
1403
|
+
else if (meshKey === 'climateMode' && registers && registers.mode) {
|
|
1404
|
+
const meshToRs485 = { 1: 2, 2: 1, 3: 4, 4: 8 };
|
|
1405
|
+
const rs485Value = meshToRs485[value];
|
|
1406
|
+
if (rs485Value !== undefined) {
|
|
1407
|
+
await node.writeModbusRegister(mapping.address, registers.mode, rs485Value);
|
|
1408
|
+
node.log(`[Mesh->RS485] 空调模式(climateMode): Mesh值${value} -> RS485值${rs485Value}`);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
987
1411
|
} catch (err) {
|
|
988
1412
|
node.error(`RS485写入失败: ${meshKey}=${value}, ${err.message}`);
|
|
989
1413
|
}
|
|
@@ -1008,12 +1432,33 @@ module.exports = function(RED) {
|
|
|
1008
1432
|
node.syncModbusToMesh = async function(cmd) {
|
|
1009
1433
|
const { mapping, registers, state, customMode } = cmd;
|
|
1010
1434
|
|
|
1011
|
-
//
|
|
1012
|
-
const
|
|
1435
|
+
// 规范化MAC地址 - 尝试多种格式查找设备
|
|
1436
|
+
const meshMac = mapping.meshMac || '';
|
|
1437
|
+
const macNormalized = meshMac.toLowerCase().replace(/:/g, '');
|
|
1438
|
+
|
|
1439
|
+
// 尝试多种格式查找Mesh设备
|
|
1440
|
+
let meshDevice = node.gateway.getDevice(meshMac); // 原始格式
|
|
1013
1441
|
if (!meshDevice) {
|
|
1014
|
-
node.
|
|
1442
|
+
meshDevice = node.gateway.getDevice(macNormalized); // 无冒号小写
|
|
1443
|
+
}
|
|
1444
|
+
if (!meshDevice) {
|
|
1445
|
+
// 尝试从设备管理器中遍历查找
|
|
1446
|
+
const allDevices = node.gateway.deviceManager?.getAllDevices() || [];
|
|
1447
|
+
meshDevice = allDevices.find(d => {
|
|
1448
|
+
const devMac = (d.macAddress || '').toLowerCase().replace(/:/g, '');
|
|
1449
|
+
return devMac === macNormalized;
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
if (!meshDevice) {
|
|
1454
|
+
node.warn(`[RS485->Mesh] 未找到Mesh设备: ${meshMac} (规范化: ${macNormalized})`);
|
|
1455
|
+
// 输出可用设备列表帮助调试
|
|
1456
|
+
const allDevices = node.gateway.deviceManager?.getAllDevices() || [];
|
|
1457
|
+
node.warn(`[RS485->Mesh] 可用设备: ${allDevices.map(d => d.macAddress).join(', ')}`);
|
|
1015
1458
|
return;
|
|
1016
1459
|
}
|
|
1460
|
+
|
|
1461
|
+
node.log(`[RS485->Mesh] 找到设备: ${meshDevice.name}, MAC=${meshDevice.macAddress}, 网络地址=0x${meshDevice.networkAddress.toString(16)}`);
|
|
1017
1462
|
|
|
1018
1463
|
const channel = mapping.meshChannel || 1;
|
|
1019
1464
|
|
|
@@ -1095,50 +1540,51 @@ module.exports = function(RED) {
|
|
|
1095
1540
|
else if (key.startsWith('led')) {
|
|
1096
1541
|
node.debug(`[RS485] 指示灯${key}: ${value}`);
|
|
1097
1542
|
}
|
|
1098
|
-
// 空调开关 -
|
|
1543
|
+
// 空调开关 - 0x02是开关属性 (0x01=关, 0x02=开)
|
|
1099
1544
|
else if (key === 'acSwitch' || (key === 'switch' && mapping.device && mapping.device.includes('ac'))) {
|
|
1100
|
-
const param = Buffer.from([value ?
|
|
1101
|
-
await node.gateway.sendControl(meshDevice.networkAddress,
|
|
1545
|
+
const param = Buffer.from([value ? 0x02 : 0x01]);
|
|
1546
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
|
|
1102
1547
|
node.log(`[RS485->Mesh] 空调开关: ${value ? '开' : '关'}`);
|
|
1103
1548
|
}
|
|
1104
|
-
// 目标温度 -
|
|
1549
|
+
// 目标温度 - 0x1B是目标温度属性
|
|
1105
1550
|
else if (key === 'targetTemp') {
|
|
1106
1551
|
const param = Buffer.from([Math.round(value)]);
|
|
1107
|
-
await node.gateway.sendControl(meshDevice.networkAddress,
|
|
1552
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x1B, param);
|
|
1108
1553
|
node.log(`[RS485->Mesh] 目标温度: ${value}°C`);
|
|
1109
1554
|
}
|
|
1110
|
-
// 空调模式 -
|
|
1111
|
-
// RS485: 1=制热, 2=制冷, 4=送风, 8=除湿
|
|
1112
|
-
// Mesh
|
|
1555
|
+
// 空调模式 - 0x1D是模式属性
|
|
1556
|
+
// RS485(话语前湾): 1=制热, 2=制冷, 4=送风, 8=除湿
|
|
1557
|
+
// Mesh协议: 1=制冷, 2=制热, 3=送风, 4=除湿
|
|
1113
1558
|
else if (key === 'mode') {
|
|
1114
1559
|
// 从RS485值或字符串转换为Mesh值
|
|
1115
|
-
let meshMode =
|
|
1560
|
+
let meshMode = 1; // 默认制冷
|
|
1116
1561
|
if (typeof value === 'string') {
|
|
1117
|
-
const modeMap = { 'cool':
|
|
1118
|
-
meshMode = modeMap[value] !== undefined ? modeMap[value] :
|
|
1562
|
+
const modeMap = { 'cool': 1, 'heat': 2, 'fan': 3, 'dry': 4 };
|
|
1563
|
+
meshMode = modeMap[value] !== undefined ? modeMap[value] : 1;
|
|
1119
1564
|
} else {
|
|
1120
|
-
// RS485数值转Mesh值
|
|
1121
|
-
const rs485ToMesh = { 1:
|
|
1122
|
-
meshMode = rs485ToMesh[value] !== undefined ? rs485ToMesh[value] :
|
|
1565
|
+
// RS485(话语前湾)数值转Mesh值
|
|
1566
|
+
const rs485ToMesh = { 1: 2, 2: 1, 4: 3, 8: 4 }; // 1=heat->2, 2=cool->1, 4=fan->3, 8=dry->4
|
|
1567
|
+
meshMode = rs485ToMesh[value] !== undefined ? rs485ToMesh[value] : 1;
|
|
1123
1568
|
}
|
|
1124
1569
|
const param = Buffer.from([meshMode]);
|
|
1125
|
-
await node.gateway.sendControl(meshDevice.networkAddress,
|
|
1570
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x1D, param);
|
|
1126
1571
|
node.log(`[RS485->Mesh] 空调模式: RS485值${value} -> Mesh值${meshMode}`);
|
|
1127
1572
|
}
|
|
1128
|
-
// 风速 -
|
|
1129
|
-
// RS485: 1=低风, 2=中风, 3=高风
|
|
1130
|
-
// Mesh
|
|
1573
|
+
// 风速 - 0x1C是风速属性
|
|
1574
|
+
// RS485(话语前湾): 1=低风, 2=中风, 3=高风
|
|
1575
|
+
// Mesh协议: 1=高, 2=中, 3=低, 4=自动
|
|
1131
1576
|
else if (key === 'fanSpeed') {
|
|
1132
|
-
let meshSpeed =
|
|
1577
|
+
let meshSpeed = 3; // 默认低
|
|
1133
1578
|
if (typeof value === 'string') {
|
|
1134
|
-
const speedMap = { 'low':
|
|
1135
|
-
meshSpeed = speedMap[value] !== undefined ? speedMap[value] :
|
|
1579
|
+
const speedMap = { 'low': 3, 'medium': 2, 'high': 1, 'auto': 4 };
|
|
1580
|
+
meshSpeed = speedMap[value] !== undefined ? speedMap[value] : 3;
|
|
1136
1581
|
} else {
|
|
1137
|
-
// RS485
|
|
1138
|
-
|
|
1582
|
+
// RS485(话语前湾)数值转Mesh值: 1=低->3, 2=中->2, 3=高->1
|
|
1583
|
+
const rs485ToMesh = { 1: 3, 2: 2, 3: 1 };
|
|
1584
|
+
meshSpeed = rs485ToMesh[value] !== undefined ? rs485ToMesh[value] : 3;
|
|
1139
1585
|
}
|
|
1140
1586
|
const param = Buffer.from([meshSpeed]);
|
|
1141
|
-
await node.gateway.sendControl(meshDevice.networkAddress,
|
|
1587
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x1C, param);
|
|
1142
1588
|
node.log(`[RS485->Mesh] 空调风速: RS485值${value} -> Mesh值${meshSpeed}`);
|
|
1143
1589
|
}
|
|
1144
1590
|
else if (key === 'brightness') {
|
|
@@ -1146,6 +1592,37 @@ module.exports = function(RED) {
|
|
|
1146
1592
|
await node.gateway.sendControl(meshDevice.networkAddress, 0x03, param);
|
|
1147
1593
|
node.debug(`[RS485->Mesh] 亮度: ${value}`);
|
|
1148
1594
|
}
|
|
1595
|
+
// ===== 地暖控制 (RS485->Mesh) =====
|
|
1596
|
+
// 地暖开关 - 0x6B是地暖开关属性 (0x02=开, 0x01=关)
|
|
1597
|
+
else if (key === 'floorHeatingSwitch') {
|
|
1598
|
+
const param = Buffer.from([value ? 0x02 : 0x01]);
|
|
1599
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x6B, param);
|
|
1600
|
+
node.log(`[RS485->Mesh] 地暖开关: ${value ? '开' : '关'}`);
|
|
1601
|
+
}
|
|
1602
|
+
// 地暖温度 - 0x6C是地暖温度属性 (18-32°C)
|
|
1603
|
+
else if (key === 'floorHeatingTemp') {
|
|
1604
|
+
const temp = Math.max(18, Math.min(32, Math.round(value)));
|
|
1605
|
+
const param = Buffer.from([temp]);
|
|
1606
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x6C, param);
|
|
1607
|
+
node.log(`[RS485->Mesh] 地暖温度: ${temp}°C`);
|
|
1608
|
+
}
|
|
1609
|
+
// ===== 新风控制 (RS485->Mesh) =====
|
|
1610
|
+
// 新风开关 - 0x68是新风开关属性 (0x02=开, 0x01=关)
|
|
1611
|
+
else if (key === 'freshAirSwitch') {
|
|
1612
|
+
const param = Buffer.from([value ? 0x02 : 0x01]);
|
|
1613
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x68, param);
|
|
1614
|
+
node.log(`[RS485->Mesh] 新风开关: ${value ? '开' : '关'}`);
|
|
1615
|
+
}
|
|
1616
|
+
// 新风风速 - 0x6A是新风风速属性 (1=高,2=中,3=低,4=自动)
|
|
1617
|
+
else if (key === 'freshAirSpeed') {
|
|
1618
|
+
// 话语前湾新风: 0=低速, 2=高速 -> Mesh: 1=高,3=低
|
|
1619
|
+
let meshSpeed = 3; // 默认低
|
|
1620
|
+
if (value === 2 || value === 'high') meshSpeed = 1; // 高速
|
|
1621
|
+
else if (value === 0 || value === 'low') meshSpeed = 3; // 低速
|
|
1622
|
+
const param = Buffer.from([meshSpeed]);
|
|
1623
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x6A, param);
|
|
1624
|
+
node.log(`[RS485->Mesh] 新风风速: RS485值${value} -> Mesh值${meshSpeed}`);
|
|
1625
|
+
}
|
|
1149
1626
|
} catch (err) {
|
|
1150
1627
|
node.error(`Mesh写入失败: ${key}=${value}, ${err.message}`);
|
|
1151
1628
|
}
|
|
@@ -1241,19 +1718,35 @@ module.exports = function(RED) {
|
|
|
1241
1718
|
const hexStr = frame.toString('hex').toUpperCase();
|
|
1242
1719
|
const hexFormatted = hexStr.match(/.{1,2}/g)?.join(' ') || '';
|
|
1243
1720
|
|
|
1244
|
-
|
|
1721
|
+
node.log(`[RS485收到] ${hexFormatted} (${frame.length}字节)`);
|
|
1722
|
+
|
|
1723
|
+
// ===== 杜亚窗帘协议检测 (55开头) =====
|
|
1245
1724
|
if (frame[0] === 0x55 && frame.length >= 7) {
|
|
1725
|
+
node.log(`[杜亚帧检测] 检测到55帧头, 长度=${frame.length}, 开始解析...`);
|
|
1246
1726
|
const duyaData = parseA6B6Frame(frame);
|
|
1247
1727
|
if (duyaData) {
|
|
1728
|
+
node.log(`[杜亚帧解析] 成功! 地址高=${duyaData.addrHigh}, 地址低=${duyaData.addrLow}, 动作=${duyaData.action}`);
|
|
1729
|
+
|
|
1248
1730
|
// 查找匹配的杜亚映射
|
|
1731
|
+
let foundMapping = false;
|
|
1732
|
+
node.log(`[杜亚映射查找] 当前映射数: ${node.mappings.length}`);
|
|
1249
1733
|
for (const mapping of node.mappings) {
|
|
1250
|
-
if (mapping.brand !== 'duya')
|
|
1734
|
+
if (mapping.brand !== 'duya') {
|
|
1735
|
+
// 检查是否是自定义窗帘(也可能匹配55帧)
|
|
1736
|
+
if (mapping.brand === 'custom' && mapping.device === 'custom_curtain') {
|
|
1737
|
+
node.debug(`[杜亚映射] 发现自定义窗帘映射,将在后续自定义码匹配中处理`);
|
|
1738
|
+
}
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1251
1741
|
|
|
1252
1742
|
// 检查2字节地址是否匹配
|
|
1253
1743
|
const mapAddrHigh = mapping.addrHigh || 1;
|
|
1254
1744
|
const mapAddrLow = mapping.addrLow || 1;
|
|
1255
1745
|
|
|
1746
|
+
node.debug(`[杜亚映射检查] 配置地址=${mapAddrHigh}:${mapAddrLow}, 帧地址=${duyaData.addrHigh}:${duyaData.addrLow}`);
|
|
1747
|
+
|
|
1256
1748
|
if (duyaData.addrHigh === mapAddrHigh && duyaData.addrLow === mapAddrLow) {
|
|
1749
|
+
foundMapping = true;
|
|
1257
1750
|
// 防死循环: 1秒内忽略响应
|
|
1258
1751
|
if (node.lastMeshToRS485Time && Date.now() - node.lastMeshToRS485Time < 1000) {
|
|
1259
1752
|
node.debug(`[防循环] 忽略杜亚响应: ${hexFormatted}`);
|
|
@@ -1284,15 +1777,24 @@ module.exports = function(RED) {
|
|
|
1284
1777
|
return;
|
|
1285
1778
|
}
|
|
1286
1779
|
}
|
|
1780
|
+
|
|
1781
|
+
if (!foundMapping) {
|
|
1782
|
+
node.warn(`[杜亚] 未找到匹配的映射, 帧地址=${duyaData.addrHigh}:${duyaData.addrLow},请检查RS485桥配置`);
|
|
1783
|
+
}
|
|
1784
|
+
} else {
|
|
1785
|
+
node.debug(`[杜亚帧检测] 解析失败,可能funcCode不是0x03`);
|
|
1287
1786
|
}
|
|
1288
1787
|
}
|
|
1289
1788
|
|
|
1290
|
-
//
|
|
1789
|
+
// 检查自定义码匹配(遍历所有映射)
|
|
1790
|
+
node.log(`[自定义码检测] 开始匹配, 当前帧hex=${hexStr}, 映射数=${node.mappings.length}`);
|
|
1291
1791
|
for (const mapping of node.mappings) {
|
|
1292
1792
|
if (mapping.brand === 'custom' && mapping.customCodes) {
|
|
1293
1793
|
const codes = mapping.customCodes;
|
|
1294
1794
|
let matchedAction = null;
|
|
1295
1795
|
|
|
1796
|
+
node.debug(`[自定义码检测] 检查映射: device=${mapping.device}, meshMac=${mapping.meshMac}, codes=${JSON.stringify(codes)}`);
|
|
1797
|
+
|
|
1296
1798
|
// 开关类型:匹配on/off
|
|
1297
1799
|
if (mapping.device === 'custom_switch') {
|
|
1298
1800
|
if (codes.on && hexStr.includes(codes.on.replace(/\s/g, '').toUpperCase())) {
|
|
@@ -1301,14 +1803,24 @@ module.exports = function(RED) {
|
|
|
1301
1803
|
matchedAction = { switch: false };
|
|
1302
1804
|
}
|
|
1303
1805
|
}
|
|
1304
|
-
// 窗帘类型:匹配open/close/stop
|
|
1806
|
+
// 窗帘类型:匹配open/close/stop(只设置action,避免重复发码)
|
|
1305
1807
|
else if (mapping.device === 'custom_curtain') {
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1808
|
+
const openCode = codes.open ? codes.open.replace(/\s/g, '').toUpperCase() : '';
|
|
1809
|
+
const closeCode = codes.close ? codes.close.replace(/\s/g, '').toUpperCase() : '';
|
|
1810
|
+
const stopCode = codes.stop ? codes.stop.replace(/\s/g, '').toUpperCase() : '';
|
|
1811
|
+
node.debug(`[自定义窗帘匹配] 帧hex=${hexStr}, open=${openCode}, close=${closeCode}, stop=${stopCode}`);
|
|
1812
|
+
|
|
1813
|
+
if (openCode && hexStr.includes(openCode)) {
|
|
1814
|
+
matchedAction = { action: 'open' };
|
|
1815
|
+
node.log(`[自定义窗帘匹配] 匹配到打开码!`);
|
|
1816
|
+
} else if (closeCode && hexStr.includes(closeCode)) {
|
|
1817
|
+
matchedAction = { action: 'close' };
|
|
1818
|
+
node.log(`[自定义窗帘匹配] 匹配到关闭码!`);
|
|
1819
|
+
} else if (stopCode && hexStr.includes(stopCode)) {
|
|
1820
|
+
matchedAction = { action: 'stop' };
|
|
1821
|
+
node.log(`[自定义窗帘匹配] 匹配到停止码!`);
|
|
1822
|
+
} else {
|
|
1823
|
+
node.debug(`[自定义窗帘匹配] 未匹配到任何码`);
|
|
1312
1824
|
}
|
|
1313
1825
|
}
|
|
1314
1826
|
// 场景类型:匹配trigger
|
|
@@ -1363,22 +1875,18 @@ module.exports = function(RED) {
|
|
|
1363
1875
|
|
|
1364
1876
|
const slaveAddr = frame[0];
|
|
1365
1877
|
const fc = frame[1];
|
|
1878
|
+
const hexFormatted = frame.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
|
|
1366
1879
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
// 自定义模式不走标准Modbus解析
|
|
1375
|
-
if (mapping.brand === 'custom') return;
|
|
1376
|
-
|
|
1377
|
-
const registers = node.getRegistersForMapping(mapping);
|
|
1378
|
-
if (!registers) {
|
|
1379
|
-
node.debug(`未找到设备${mapping.device}的寄存器定义`);
|
|
1880
|
+
node.log(`[Modbus解析] 从机=${slaveAddr}, 功能码=0x${fc.toString(16)}, 帧=${hexFormatted}`);
|
|
1881
|
+
|
|
1882
|
+
// 查找所有匹配从机地址的映射
|
|
1883
|
+
const allMappings = node.findAllRS485Mappings(slaveAddr);
|
|
1884
|
+
if (allMappings.length === 0) {
|
|
1885
|
+
node.debug(`[Modbus解析] 未找到从机${slaveAddr}的映射配置`);
|
|
1380
1886
|
return;
|
|
1381
1887
|
}
|
|
1888
|
+
|
|
1889
|
+
node.log(`[Modbus解析] 找到${allMappings.length}个匹配映射`);
|
|
1382
1890
|
|
|
1383
1891
|
// 防死循环:检查是否刚刚从Mesh发送过来
|
|
1384
1892
|
if (node.lastMeshToRS485Time && Date.now() - node.lastMeshToRS485Time < 500) {
|
|
@@ -1386,15 +1894,45 @@ module.exports = function(RED) {
|
|
|
1386
1894
|
return;
|
|
1387
1895
|
}
|
|
1388
1896
|
|
|
1389
|
-
//
|
|
1390
|
-
|
|
1897
|
+
// 解析寄存器地址和值
|
|
1898
|
+
if (fc !== 0x06 && fc !== 0x10) {
|
|
1899
|
+
// 暂不处理其他功能码
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1391
1902
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1903
|
+
const regAddr = frame.readUInt16BE(2);
|
|
1904
|
+
const value = frame.readUInt16BE(4);
|
|
1905
|
+
|
|
1906
|
+
// 根据寄存器地址确定是哪个rs485Channel
|
|
1907
|
+
// 话语前湾开关寄存器:0x1031=CH1, 0x1032=CH2, 0x1033=CH3, 0x1034=CH4, 0x1035=CH5, 0x1036=CH6
|
|
1908
|
+
let rs485ChannelFromReg = null;
|
|
1909
|
+
if (regAddr >= 0x1031 && regAddr <= 0x1036) {
|
|
1910
|
+
rs485ChannelFromReg = regAddr - 0x1030; // 0x1031->1, 0x1032->2, ...
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
node.log(`[Modbus解析] 寄存器=0x${regAddr.toString(16).toUpperCase()}, 值=${value}, rs485Channel=${rs485ChannelFromReg}`);
|
|
1914
|
+
|
|
1915
|
+
// 遍历所有匹配的映射,只处理rs485Channel匹配的
|
|
1916
|
+
for (const mapping of allMappings) {
|
|
1917
|
+
// 自定义模式不走标准Modbus解析
|
|
1918
|
+
if (mapping.brand === 'custom') continue;
|
|
1919
|
+
|
|
1920
|
+
const mappingRs485Channel = mapping.rs485Channel || 1;
|
|
1921
|
+
|
|
1922
|
+
// 如果是开关寄存器,检查rs485Channel是否匹配
|
|
1923
|
+
if (rs485ChannelFromReg !== null && rs485ChannelFromReg !== mappingRs485Channel) {
|
|
1924
|
+
node.debug(`[Modbus解析] 跳过映射: rs485Channel不匹配 (帧=${rs485ChannelFromReg}, 映射=${mappingRs485Channel})`);
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
const registers = node.getRegistersForMapping(mapping);
|
|
1929
|
+
if (!registers) {
|
|
1930
|
+
node.debug(`未找到设备${mapping.device}的寄存器定义`);
|
|
1931
|
+
continue;
|
|
1932
|
+
}
|
|
1396
1933
|
|
|
1397
1934
|
// 查找匹配的寄存器定义
|
|
1935
|
+
let state = {};
|
|
1398
1936
|
for (const [key, reg] of Object.entries(registers)) {
|
|
1399
1937
|
if (reg.address === regAddr) {
|
|
1400
1938
|
if (key.startsWith('switch') || key.startsWith('led')) {
|
|
@@ -1408,51 +1946,35 @@ module.exports = function(RED) {
|
|
|
1408
1946
|
break;
|
|
1409
1947
|
}
|
|
1410
1948
|
}
|
|
1411
|
-
} else if (fc === 0x03 || fc === 0x04) {
|
|
1412
|
-
const byteCount = frame[2];
|
|
1413
|
-
for (let i = 0; i < byteCount / 2; i++) {
|
|
1414
|
-
const value = frame.readUInt16BE(3 + i * 2);
|
|
1415
|
-
for (const [key, reg] of Object.entries(registers)) {
|
|
1416
|
-
if (reg.map) {
|
|
1417
|
-
state[key] = reg.map[value] || value;
|
|
1418
|
-
} else {
|
|
1419
|
-
state[key] = value;
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
} else if (fc === 0x20) {
|
|
1424
|
-
if (frame.length >= 9) {
|
|
1425
|
-
const startReg = frame.readUInt16BE(2);
|
|
1426
|
-
const count = frame.readUInt16BE(4);
|
|
1427
|
-
node.debug(`[RS485] 从机${slaveAddr} 自定义上报: 起始0x${startReg.toString(16)}, 数量${count}`);
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
if (Object.keys(state).length > 0) {
|
|
1432
|
-
node.log(`[RS485->Mesh] 设备@${slaveAddr} 状态变化: ${JSON.stringify(state)}`);
|
|
1433
|
-
|
|
1434
|
-
// 输出调试信息到节点输出端口
|
|
1435
|
-
node.send({
|
|
1436
|
-
topic: 'rs485-state-change',
|
|
1437
|
-
payload: {
|
|
1438
|
-
direction: 'RS485→Mesh',
|
|
1439
|
-
slaveAddr: slaveAddr,
|
|
1440
|
-
funcCode: fc,
|
|
1441
|
-
brand: mapping.brand,
|
|
1442
|
-
device: mapping.device,
|
|
1443
|
-
meshMac: mapping.meshMac,
|
|
1444
|
-
state: state
|
|
1445
|
-
},
|
|
1446
|
-
timestamp: new Date().toISOString()
|
|
1447
|
-
});
|
|
1448
1949
|
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1950
|
+
if (Object.keys(state).length > 0) {
|
|
1951
|
+
node.log(`[RS485->Mesh] 映射匹配: meshMac=${mapping.meshMac}, meshCH=${mapping.meshChannel}, rs485CH=${mappingRs485Channel}, 状态=${JSON.stringify(state)}`);
|
|
1952
|
+
|
|
1953
|
+
// 输出调试信息到节点输出端口
|
|
1954
|
+
node.send({
|
|
1955
|
+
topic: 'rs485-state-change',
|
|
1956
|
+
payload: {
|
|
1957
|
+
direction: 'RS485→Mesh',
|
|
1958
|
+
slaveAddr: slaveAddr,
|
|
1959
|
+
funcCode: fc,
|
|
1960
|
+
brand: mapping.brand,
|
|
1961
|
+
device: mapping.device,
|
|
1962
|
+
meshMac: mapping.meshMac,
|
|
1963
|
+
meshChannel: mapping.meshChannel,
|
|
1964
|
+
rs485Channel: mappingRs485Channel,
|
|
1965
|
+
state: state
|
|
1966
|
+
},
|
|
1967
|
+
timestamp: new Date().toISOString()
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
node.queueCommand({
|
|
1971
|
+
direction: 'modbus-to-mesh',
|
|
1972
|
+
mapping: mapping,
|
|
1973
|
+
registers: registers,
|
|
1974
|
+
state: state,
|
|
1975
|
+
timestamp: Date.now()
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1456
1978
|
}
|
|
1457
1979
|
};
|
|
1458
1980
|
|
|
@@ -1554,9 +2076,11 @@ module.exports = function(RED) {
|
|
|
1554
2076
|
// 清理
|
|
1555
2077
|
node.on('close', (done) => {
|
|
1556
2078
|
// 移除Mesh网关事件监听器
|
|
1557
|
-
node.gateway
|
|
1558
|
-
|
|
1559
|
-
|
|
2079
|
+
if (node.gateway) {
|
|
2080
|
+
node.gateway.removeListener('device-list-complete', init);
|
|
2081
|
+
node.gateway.removeListener('device-state-changed', handleMeshStateChange);
|
|
2082
|
+
node.gateway.removeListener('curtain-control', handleCurtainControl);
|
|
2083
|
+
}
|
|
1560
2084
|
|
|
1561
2085
|
// 移除RS485配置节点事件监听器
|
|
1562
2086
|
if (node.rs485Config && node._rs485Handlers) {
|
|
@@ -1567,6 +2091,15 @@ module.exports = function(RED) {
|
|
|
1567
2091
|
node.rs485Config.deregister(node);
|
|
1568
2092
|
}
|
|
1569
2093
|
|
|
2094
|
+
// 清理缓存和队列,防止内存泄漏
|
|
2095
|
+
node.stateCache = {};
|
|
2096
|
+
node.commandQueue = [];
|
|
2097
|
+
node.curtainDebounce = {};
|
|
2098
|
+
node.curtainCache = {};
|
|
2099
|
+
node.lastSentTime = {};
|
|
2100
|
+
node.processing = false;
|
|
2101
|
+
node.syncLock = false;
|
|
2102
|
+
|
|
1570
2103
|
node.log('[RS485 Bridge] 节点已清理');
|
|
1571
2104
|
done();
|
|
1572
2105
|
});
|
|
@@ -1603,12 +2136,20 @@ module.exports = function(RED) {
|
|
|
1603
2136
|
displayName = (d.name || '设备') + '_' + macClean;
|
|
1604
2137
|
}
|
|
1605
2138
|
|
|
2139
|
+
// 三合一面板特殊处理
|
|
2140
|
+
const isThreeInOne = d.isThreeInOne || false;
|
|
2141
|
+
if (isThreeInOne) {
|
|
2142
|
+
displayName = '三合一面板_' + macClean;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
1606
2145
|
return {
|
|
1607
2146
|
mac: d.macAddress,
|
|
1608
2147
|
name: displayName,
|
|
1609
2148
|
originalName: d.name,
|
|
1610
2149
|
type: d.deviceType,
|
|
1611
|
-
channels: channels
|
|
2150
|
+
channels: channels,
|
|
2151
|
+
isThreeInOne: isThreeInOne,
|
|
2152
|
+
entityType: d.getEntityType ? d.getEntityType() : 'unknown'
|
|
1612
2153
|
};
|
|
1613
2154
|
});
|
|
1614
2155
|
res.json(devices);
|