node-red-contrib-symi-mesh 1.6.9 → 1.7.0
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 +64 -19
- package/nodes/symi-485-bridge.html +206 -32
- package/nodes/symi-485-bridge.js +162 -48
- package/nodes/symi-knx-bridge.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -603,7 +603,7 @@ Tab分隔,每行一个实体:
|
|
|
603
603
|
|
|
604
604
|
#### 注意事项
|
|
605
605
|
|
|
606
|
-
1. **
|
|
606
|
+
1. **10秒初始化延迟**:部署后前10秒不同步,等待Mesh设备发现完成
|
|
607
607
|
2. **首次状态缓存**:启动后第一次状态仅缓存,第二次操作才会同步
|
|
608
608
|
3. **防死循环**:KNX控制后1秒内,Mesh状态变化不会反向同步
|
|
609
609
|
|
|
@@ -742,34 +742,67 @@ Mesh 四键开关 第2路 ↔ RS485 六键开关 第5路 地址:2 ✓ 支持
|
|
|
742
742
|
| 通用Modbus | 各类标准Modbus设备 |
|
|
743
743
|
| 自定义协议 | 任意485码匹配(开关/窗帘/场景) |
|
|
744
744
|
|
|
745
|
-
###
|
|
745
|
+
### 自定义协议模式(v1.7.0增强)
|
|
746
746
|
|
|
747
|
-
当内置协议无法满足需求时,可使用"自定义协议"模式,手动录入RS485
|
|
747
|
+
当内置协议无法满足需求时,可使用"自定义协议"模式,手动录入RS485十六进制码进行双向匹配。
|
|
748
748
|
|
|
749
|
-
####
|
|
750
|
-
- **打开码**:当Mesh开关打开时发送此码,收到此码时触发Mesh开关打开
|
|
751
|
-
- **关闭码**:当Mesh开关关闭时发送此码,收到此码时触发Mesh开关关闭
|
|
749
|
+
#### 自定义开关(4组码)
|
|
752
750
|
|
|
753
|
-
|
|
754
|
-
-
|
|
755
|
-
-
|
|
756
|
-
-
|
|
751
|
+
- **发开码**:Mesh开关打开时,发送此码到RS485总线
|
|
752
|
+
- **发关码**:Mesh开关关闭时,发送此码到RS485总线
|
|
753
|
+
- **收开码**:RS485总线收到此码时,触发Mesh开关打开
|
|
754
|
+
- **收关码**:RS485总线收到此码时,触发Mesh开关关闭
|
|
755
|
+
|
|
756
|
+
**翻转模式**:当收开码=收关码时,收到后自动翻转开关状态
|
|
757
|
+
|
|
758
|
+
#### 自定义窗帘(6组码)
|
|
759
|
+
|
|
760
|
+
- **发开码**、**发关码**、**发停码**:Mesh→RS485
|
|
761
|
+
- **收开码**、**收关码**、**收停码**:RS485→Mesh
|
|
762
|
+
|
|
763
|
+
#### 自定义空调(完整收发码)
|
|
764
|
+
|
|
765
|
+
- **开关**:发开码、发关码、收开码、收关码
|
|
766
|
+
- **风速**:发高风、发中风、发低风、收高风、收中风、收低风
|
|
767
|
+
- **模式**:发制冷、发制热、发除湿、发送风、收制冷、收制热、收除湿、收送风
|
|
768
|
+
- **温度**:发温度码、收温度码(支持16-30度,使用{TEMP}占位符)
|
|
757
769
|
|
|
758
770
|
#### 配置示例
|
|
771
|
+
|
|
772
|
+
**自定义开关(花语前湾大屏)**:
|
|
773
|
+
```
|
|
774
|
+
品牌: 自定义协议
|
|
775
|
+
类型: 自定义开关
|
|
776
|
+
发开码: 01 20 10 14 00 01 00 7F 82 B6
|
|
777
|
+
发关码: 01 20 10 14 00 01 00 FF 83 16
|
|
778
|
+
收开码: 01 20
|
|
779
|
+
收关码: 01 20
|
|
780
|
+
```
|
|
781
|
+
说明:收开码=收关码,收到后翻转开关状态
|
|
782
|
+
|
|
783
|
+
**自定义窗帘**:
|
|
759
784
|
```
|
|
760
785
|
品牌: 自定义协议
|
|
761
786
|
类型: 自定义窗帘
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
787
|
+
发开码: 55 02 02 03 01 49 44
|
|
788
|
+
发关码: 55 02 02 03 02 09 45
|
|
789
|
+
发停码: 55 02 02 03 03 C8 85
|
|
790
|
+
收开码: 55 02 02 03 01
|
|
791
|
+
收关码: 55 02 02 03 02
|
|
792
|
+
收停码: 55 02 02 03 03
|
|
765
793
|
```
|
|
766
794
|
|
|
767
795
|
#### 工作原理
|
|
768
|
-
1. **Mesh→RS485**:Mesh
|
|
769
|
-
2. **RS485→Mesh**:收到的RS485
|
|
770
|
-
3.
|
|
796
|
+
1. **Mesh→RS485**:Mesh设备状态变化时,发送对应的发码
|
|
797
|
+
2. **RS485→Mesh**:收到的RS485帧与收码匹配,触发对应Mesh设备
|
|
798
|
+
3. **翻转逻辑**:开关的收开码=收关码时,收到后翻转当前状态
|
|
799
|
+
4. **防死循环**:500ms内不重复触发,避免Mesh→RS485→Mesh循环
|
|
771
800
|
|
|
772
|
-
|
|
801
|
+
#### 注意事项
|
|
802
|
+
- 支持最多24字节(72个十六进制字符)数据录入
|
|
803
|
+
- 支持空格分隔(如 `55 01 01` 或 `550101`)
|
|
804
|
+
- 自定义模式不需要填写地址字段(自动隐藏)
|
|
805
|
+
- 配置持久化保存,重启后保持
|
|
773
806
|
|
|
774
807
|
### 注意事项
|
|
775
808
|
|
|
@@ -1221,6 +1254,18 @@ node-red-contrib-symi-mesh/
|
|
|
1221
1254
|
|
|
1222
1255
|
## 更新日志
|
|
1223
1256
|
|
|
1257
|
+
### v1.7.0 (2025-12-21)
|
|
1258
|
+
- **自定义协议增强**:RS485桥接节点自定义协议功能全面升级
|
|
1259
|
+
- 自定义开关:添加发开、发关、收开、收关4组码
|
|
1260
|
+
- 翻转模式:收开码=收关码时,收到后自动翻转开关状态
|
|
1261
|
+
- 自定义窗帘:添加发开、发关、发停、收开、收关、收停6组码
|
|
1262
|
+
- 自定义空调:新增完整收发码支持(开关、风速、模式、温度)
|
|
1263
|
+
- 支持最多24字节(72个十六进制字符)数据录入
|
|
1264
|
+
- 自定义模式自动隐藏地址字段
|
|
1265
|
+
- 配置持久化保存,重启后保持
|
|
1266
|
+
- 防死循环机制优化
|
|
1267
|
+
- 内存优化,防止日志溢出
|
|
1268
|
+
|
|
1224
1269
|
### v1.6.9 (2025-12-20)
|
|
1225
1270
|
- **KNX-HA双向同步**:新增`symi-knx-ha-bridge`节点
|
|
1226
1271
|
- 直接连接KNX与HA实体,实现双向同步
|
|
@@ -1308,8 +1353,8 @@ Copyright (c) 2025 SYMI 亖米
|
|
|
1308
1353
|
## 关于
|
|
1309
1354
|
|
|
1310
1355
|
**作者**: SYMI 亖米
|
|
1311
|
-
**版本**: 1.
|
|
1356
|
+
**版本**: 1.7.0
|
|
1312
1357
|
**协议**: 蓝牙MESH网关(初级版)串口协议V1.0
|
|
1313
|
-
**最后更新**: 2025-12-
|
|
1358
|
+
**最后更新**: 2025-12-21
|
|
1314
1359
|
**仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
|
|
1315
1360
|
**npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
|
|
@@ -160,9 +160,11 @@
|
|
|
160
160
|
|
|
161
161
|
mappings.forEach(function(m, idx) {
|
|
162
162
|
var row = $('<div class="mapping-row" data-idx="' + idx + '" style="flex-wrap:wrap;"></div>');
|
|
163
|
-
//
|
|
163
|
+
// 地址输入框:杜亚2字节地址,自定义模式隐藏,其他显示
|
|
164
164
|
var addrHtml = '';
|
|
165
|
-
if (m.brand === '
|
|
165
|
+
if (m.brand === 'custom') {
|
|
166
|
+
addrHtml = '<div class="addr-col" style="width:60px;visibility:hidden;"></div>';
|
|
167
|
+
} else if (m.brand === 'duya') {
|
|
166
168
|
addrHtml = '<div class="addr-col duya-addr" style="display:flex;gap:2px;">' +
|
|
167
169
|
'<input type="number" class="addr-high" value="' + (m.addrHigh || 1) + '" min="0" max="255" style="width:40px;" title="地址高字节" placeholder="高">' +
|
|
168
170
|
'<input type="number" class="addr-low" value="' + (m.addrLow || 1) + '" min="0" max="255" style="width:40px;" title="地址低字节" placeholder="低">' +
|
|
@@ -279,33 +281,88 @@
|
|
|
279
281
|
renderMappings();
|
|
280
282
|
});
|
|
281
283
|
|
|
282
|
-
// 自定义码输入事件
|
|
283
|
-
container.find('.custom-
|
|
284
|
+
// 自定义码输入事件 - 开关
|
|
285
|
+
container.find('.custom-send-on').off('change').on('change', function() {
|
|
284
286
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
285
287
|
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
286
|
-
mappings[idx].customCodes.
|
|
288
|
+
mappings[idx].customCodes.sendOn = $(this).val();
|
|
287
289
|
});
|
|
288
|
-
container.find('.custom-
|
|
290
|
+
container.find('.custom-send-off').off('change').on('change', function() {
|
|
289
291
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
290
292
|
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
291
|
-
mappings[idx].customCodes.
|
|
293
|
+
mappings[idx].customCodes.sendOff = $(this).val();
|
|
292
294
|
});
|
|
293
|
-
container.find('.custom-
|
|
295
|
+
container.find('.custom-recv-on').off('change').on('change', function() {
|
|
294
296
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
295
297
|
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
296
|
-
mappings[idx].customCodes.
|
|
298
|
+
mappings[idx].customCodes.recvOn = $(this).val();
|
|
297
299
|
});
|
|
298
|
-
container.find('.custom-
|
|
300
|
+
container.find('.custom-recv-off').off('change').on('change', function() {
|
|
299
301
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
300
302
|
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
301
|
-
mappings[idx].customCodes.
|
|
303
|
+
mappings[idx].customCodes.recvOff = $(this).val();
|
|
302
304
|
});
|
|
303
|
-
|
|
305
|
+
// 窗帘码
|
|
306
|
+
container.find('.custom-send-open, .custom-send-close, .custom-send-stop, .custom-recv-open, .custom-recv-close, .custom-recv-stop').off('change').on('change', function() {
|
|
304
307
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
305
308
|
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
306
|
-
mappings[idx].customCodes.
|
|
309
|
+
if ($(this).hasClass('custom-send-open')) mappings[idx].customCodes.sendOpen = $(this).val();
|
|
310
|
+
if ($(this).hasClass('custom-send-close')) mappings[idx].customCodes.sendClose = $(this).val();
|
|
311
|
+
if ($(this).hasClass('custom-send-stop')) mappings[idx].customCodes.sendStop = $(this).val();
|
|
312
|
+
if ($(this).hasClass('custom-recv-open')) mappings[idx].customCodes.recvOpen = $(this).val();
|
|
313
|
+
if ($(this).hasClass('custom-recv-close')) mappings[idx].customCodes.recvClose = $(this).val();
|
|
314
|
+
if ($(this).hasClass('custom-recv-stop')) mappings[idx].customCodes.recvStop = $(this).val();
|
|
307
315
|
});
|
|
308
|
-
|
|
316
|
+
// 空调码
|
|
317
|
+
container.find('[class^="custom-ac-"], [class^="custom-fan-"], [class^="custom-mode-"], [class^="custom-temp-"]').off('change').on('change', function() {
|
|
318
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
319
|
+
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
320
|
+
var cls = $(this).attr('class').split(' ')[0];
|
|
321
|
+
|
|
322
|
+
// 处理空调开关、风速、模式
|
|
323
|
+
if (cls.includes('ac-send-on')) mappings[idx].customCodes.acSendOn = $(this).val();
|
|
324
|
+
else if (cls.includes('ac-send-off')) mappings[idx].customCodes.acSendOff = $(this).val();
|
|
325
|
+
else if (cls.includes('ac-recv-on')) mappings[idx].customCodes.acRecvOn = $(this).val();
|
|
326
|
+
else if (cls.includes('ac-recv-off')) mappings[idx].customCodes.acRecvOff = $(this).val();
|
|
327
|
+
else if (cls.includes('fan-send-high')) mappings[idx].customCodes.fanSendHigh = $(this).val();
|
|
328
|
+
else if (cls.includes('fan-send-mid')) mappings[idx].customCodes.fanSendMid = $(this).val();
|
|
329
|
+
else if (cls.includes('fan-send-low')) mappings[idx].customCodes.fanSendLow = $(this).val();
|
|
330
|
+
else if (cls.includes('fan-recv-high')) mappings[idx].customCodes.fanRecvHigh = $(this).val();
|
|
331
|
+
else if (cls.includes('fan-recv-mid')) mappings[idx].customCodes.fanRecvMid = $(this).val();
|
|
332
|
+
else if (cls.includes('fan-recv-low')) mappings[idx].customCodes.fanRecvLow = $(this).val();
|
|
333
|
+
else if (cls.includes('mode-send-cool')) mappings[idx].customCodes.modeSendCool = $(this).val();
|
|
334
|
+
else if (cls.includes('mode-send-heat')) mappings[idx].customCodes.modeSendHeat = $(this).val();
|
|
335
|
+
else if (cls.includes('mode-send-dry')) mappings[idx].customCodes.modeSendDry = $(this).val();
|
|
336
|
+
else if (cls.includes('mode-send-fan')) mappings[idx].customCodes.modeSendFan = $(this).val();
|
|
337
|
+
else if (cls.includes('mode-recv-cool')) mappings[idx].customCodes.modeRecvCool = $(this).val();
|
|
338
|
+
else if (cls.includes('mode-recv-heat')) mappings[idx].customCodes.modeRecvHeat = $(this).val();
|
|
339
|
+
else if (cls.includes('mode-recv-dry')) mappings[idx].customCodes.modeRecvDry = $(this).val();
|
|
340
|
+
else if (cls.includes('mode-recv-fan')) mappings[idx].customCodes.modeRecvFan = $(this).val();
|
|
341
|
+
|
|
342
|
+
// 处理 16-30 度独立温度码
|
|
343
|
+
else if (cls.startsWith('custom-temp-')) {
|
|
344
|
+
// 提取 key, 如 tempSendCool16
|
|
345
|
+
var parts = cls.split('-'); // ["custom", "temp", "send", "cool", "16"]
|
|
346
|
+
if (parts.length === 5) {
|
|
347
|
+
var type = parts[2]; // send/recv
|
|
348
|
+
var mode = parts[3]; // cool/heat
|
|
349
|
+
var temp = parts[4]; // 16-30
|
|
350
|
+
var key = 'temp' + type.charAt(0).toUpperCase() + type.slice(1) +
|
|
351
|
+
mode.charAt(0).toUpperCase() + mode.slice(1) + temp;
|
|
352
|
+
mappings[idx].customCodes[key] = $(this).val();
|
|
353
|
+
} else {
|
|
354
|
+
// 兼容旧的占位符模式
|
|
355
|
+
if (cls.includes('temp-send-cool')) mappings[idx].customCodes.tempSendCool = $(this).val();
|
|
356
|
+
else if (cls.includes('temp-recv-cool')) mappings[idx].customCodes.tempRecvCool = $(this).val();
|
|
357
|
+
else if (cls.includes('temp-send-heat')) mappings[idx].customCodes.tempSendHeat = $(this).val();
|
|
358
|
+
else if (cls.includes('temp-recv-heat')) mappings[idx].customCodes.tempRecvHeat = $(this).val();
|
|
359
|
+
else if (cls.includes('temp-send')) mappings[idx].customCodes.tempSend = $(this).val();
|
|
360
|
+
else if (cls.includes('temp-recv')) mappings[idx].customCodes.tempRecv = $(this).val();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
// 场景码
|
|
365
|
+
container.find('.custom-trigger').off('change').on('change', function() {
|
|
309
366
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
310
367
|
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
311
368
|
mappings[idx].customCodes.trigger = $(this).val();
|
|
@@ -322,15 +379,79 @@
|
|
|
322
379
|
html += '<div style="font-size:11px;color:#f57c00;margin-bottom:6px;"><i class="fa fa-code"></i> 自定义RS485码(十六进制,如: 55 01 01 03 01 B9 00)</div>';
|
|
323
380
|
|
|
324
381
|
if (deviceId === 'custom_switch') {
|
|
325
|
-
html += '<div style="display:
|
|
326
|
-
html += '<label style="font-size:11px;"
|
|
327
|
-
html += '<label style="font-size:11px;"
|
|
382
|
+
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;">';
|
|
383
|
+
html += '<label style="font-size:11px;">发开码: <input type="text" class="custom-code-send-on" value="' + (codes.sendOn || '') + '" placeholder="01 20 10 14 00 01 00 7F..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
384
|
+
html += '<label style="font-size:11px;">发关码: <input type="text" class="custom-code-send-off" value="' + (codes.sendOff || '') + '" placeholder="01 20 10 14 00 01 00 FF..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
385
|
+
html += '<label style="font-size:11px;">收开码: <input type="text" class="custom-code-recv-on" value="' + (codes.recvOn || '') + '" placeholder="01 20..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
386
|
+
html += '<label style="font-size:11px;">收关码: <input type="text" class="custom-code-recv-off" value="' + (codes.recvOff || '') + '" placeholder="01 20..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
328
387
|
html += '</div>';
|
|
388
|
+
html += '<div style="font-size:10px;color:#666;margin-top:4px;">提示:收开码=收关码时,收到后翻转开关状态</div>';
|
|
329
389
|
} else if (deviceId === 'custom_curtain') {
|
|
330
|
-
html += '<div style="display:
|
|
331
|
-
html += '<label style="font-size:11px;"
|
|
332
|
-
html += '<label style="font-size:11px;"
|
|
333
|
-
html += '<label style="font-size:11px;"
|
|
390
|
+
html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;">';
|
|
391
|
+
html += '<label style="font-size:11px;">发开码: <input type="text" class="custom-code-send-open" value="' + (codes.sendOpen || '') + '" placeholder="55 02 02 03 01..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
392
|
+
html += '<label style="font-size:11px;">发关码: <input type="text" class="custom-code-send-close" value="' + (codes.sendClose || '') + '" placeholder="55 02 02 03 02..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
393
|
+
html += '<label style="font-size:11px;">发停码: <input type="text" class="custom-code-send-stop" value="' + (codes.sendStop || '') + '" placeholder="55 02 02 03 03..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
394
|
+
html += '<label style="font-size:11px;">收开码: <input type="text" class="custom-code-recv-open" value="' + (codes.recvOpen || '') + '" placeholder="01 20..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
395
|
+
html += '<label style="font-size:11px;">收关码: <input type="text" class="custom-code-recv-close" value="' + (codes.recvClose || '') + '" placeholder="01 20..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
396
|
+
html += '<label style="font-size:11px;">收停码: <input type="text" class="custom-code-recv-stop" value="' + (codes.recvStop || '') + '" placeholder="01 20..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
397
|
+
html += '</div>';
|
|
398
|
+
} else if (deviceId === 'custom_climate') {
|
|
399
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">开关控制</div>';
|
|
400
|
+
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:6px;">';
|
|
401
|
+
html += '<label style="font-size:10px;">发开码: <input type="text" class="custom-ac-send-on" value="' + (codes.acSendOn || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
402
|
+
html += '<label style="font-size:10px;">发关码: <input type="text" class="custom-ac-send-off" value="' + (codes.acSendOff || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
403
|
+
html += '<label style="font-size:10px;">收开码: <input type="text" class="custom-ac-recv-on" value="' + (codes.acRecvOn || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
404
|
+
html += '<label style="font-size:10px;">收关码: <input type="text" class="custom-ac-recv-off" value="' + (codes.acRecvOff || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
405
|
+
html += '</div>';
|
|
406
|
+
|
|
407
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">风速控制</div>';
|
|
408
|
+
html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;margin-bottom:6px;">';
|
|
409
|
+
html += '<label style="font-size:10px;">发高风: <input type="text" class="custom-fan-send-high" value="' + (codes.fanSendHigh || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
410
|
+
html += '<label style="font-size:10px;">发中风: <input type="text" class="custom-fan-send-mid" value="' + (codes.fanSendMid || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
411
|
+
html += '<label style="font-size:10px;">发低风: <input type="text" class="custom-fan-send-low" value="' + (codes.fanSendLow || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
412
|
+
html += '<label style="font-size:10px;">收高风: <input type="text" class="custom-fan-recv-high" value="' + (codes.fanRecvHigh || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
413
|
+
html += '<label style="font-size:10px;">收中风: <input type="text" class="custom-fan-recv-mid" value="' + (codes.fanRecvMid || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
414
|
+
html += '<label style="font-size:10px;">收低风: <input type="text" class="custom-fan-recv-low" value="' + (codes.fanRecvLow || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
415
|
+
html += '</div>';
|
|
416
|
+
|
|
417
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">模式控制</div>';
|
|
418
|
+
html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:4px;margin-bottom:6px;">';
|
|
419
|
+
html += '<label style="font-size:10px;">发制冷: <input type="text" class="custom-mode-send-cool" value="' + (codes.modeSendCool || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
420
|
+
html += '<label style="font-size:10px;">发制热: <input type="text" class="custom-mode-send-heat" value="' + (codes.modeSendHeat || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
421
|
+
html += '<label style="font-size:10px;">发除湿: <input type="text" class="custom-mode-send-dry" value="' + (codes.modeSendDry || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
422
|
+
html += '<label style="font-size:10px;">发送风: <input type="text" class="custom-mode-send-fan" value="' + (codes.modeSendFan || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
423
|
+
html += '<label style="font-size:10px;">收制冷: <input type="text" class="custom-mode-recv-cool" value="' + (codes.modeRecvCool || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
424
|
+
html += '<label style="font-size:10px;">收制热: <input type="text" class="custom-mode-recv-heat" value="' + (codes.modeRecvHeat || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
425
|
+
html += '<label style="font-size:10px;">收除湿: <input type="text" class="custom-mode-recv-dry" value="' + (codes.modeRecvDry || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
426
|
+
html += '<label style="font-size:10px;">收送风: <input type="text" class="custom-mode-recv-fan" value="' + (codes.modeRecvFan || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
427
|
+
html += '</div>';
|
|
428
|
+
|
|
429
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;margin-top:4px;">制冷温度发码 (16-30度)</div>';
|
|
430
|
+
html += '<div style="display:grid;grid-template-columns:repeat(5, 1fr);gap:4px;margin-bottom:6px;">';
|
|
431
|
+
for (var t=16; t<=30; t++) {
|
|
432
|
+
html += '<label style="font-size:9px;">' + t + '°C: <input type="text" class="custom-temp-send-cool-' + t + '" value="' + (codes['tempSendCool'+t] || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
433
|
+
}
|
|
434
|
+
html += '</div>';
|
|
435
|
+
|
|
436
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">制冷温度收码 (16-30度)</div>';
|
|
437
|
+
html += '<div style="display:grid;grid-template-columns:repeat(5, 1fr);gap:4px;margin-bottom:6px;">';
|
|
438
|
+
for (var t=16; t<=30; t++) {
|
|
439
|
+
html += '<label style="font-size:9px;">' + t + '°C: <input type="text" class="custom-temp-recv-cool-' + t + '" value="' + (codes['tempRecvCool'+t] || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
440
|
+
}
|
|
441
|
+
html += '</div>';
|
|
442
|
+
|
|
443
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">制热温度发码 (16-30度)</div>';
|
|
444
|
+
html += '<div style="display:grid;grid-template-columns:repeat(5, 1fr);gap:4px;margin-bottom:6px;">';
|
|
445
|
+
for (var t=16; t<=30; t++) {
|
|
446
|
+
html += '<label style="font-size:9px;">' + t + '°C: <input type="text" class="custom-temp-send-heat-' + t + '" value="' + (codes['tempSendHeat'+t] || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
447
|
+
}
|
|
448
|
+
html += '</div>';
|
|
449
|
+
|
|
450
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">制热温度收码 (16-30度)</div>';
|
|
451
|
+
html += '<div style="display:grid;grid-template-columns:repeat(5, 1fr);gap:4px;margin-bottom:6px;">';
|
|
452
|
+
for (var t=16; t<=30; t++) {
|
|
453
|
+
html += '<label style="font-size:9px;">' + t + '°C: <input type="text" class="custom-temp-recv-heat-' + t + '" value="' + (codes['tempRecvHeat'+t] || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
454
|
+
}
|
|
334
455
|
html += '</div>';
|
|
335
456
|
} else if (deviceId === 'custom_scene') {
|
|
336
457
|
html += '<div style="display:flex;gap:8px;">';
|
|
@@ -374,18 +495,71 @@
|
|
|
374
495
|
}
|
|
375
496
|
// 保存自定义码
|
|
376
497
|
if (m.brand === 'custom') {
|
|
498
|
+
var row = $(this);
|
|
377
499
|
m.customCodes = {};
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
var
|
|
381
|
-
var
|
|
382
|
-
var
|
|
383
|
-
var
|
|
384
|
-
|
|
385
|
-
if (
|
|
386
|
-
if (
|
|
387
|
-
if (
|
|
388
|
-
if (
|
|
500
|
+
|
|
501
|
+
// 通用/开关
|
|
502
|
+
var sendOn = row.find('.custom-code-send-on').val() || row.find('.custom-ac-send-on').val() || row.find('.custom-send-on').val();
|
|
503
|
+
var sendOff = row.find('.custom-code-send-off').val() || row.find('.custom-ac-send-off').val() || row.find('.custom-send-off').val();
|
|
504
|
+
var recvOn = row.find('.custom-code-recv-on').val() || row.find('.custom-ac-recv-on').val() || row.find('.custom-recv-on').val();
|
|
505
|
+
var recvOff = row.find('.custom-code-recv-off').val() || row.find('.custom-ac-recv-off').val() || row.find('.custom-recv-off').val();
|
|
506
|
+
|
|
507
|
+
if (sendOn) m.customCodes.sendOn = sendOn;
|
|
508
|
+
if (sendOff) m.customCodes.sendOff = sendOff;
|
|
509
|
+
if (recvOn) m.customCodes.recvOn = recvOn;
|
|
510
|
+
if (recvOff) m.customCodes.recvOff = recvOff;
|
|
511
|
+
|
|
512
|
+
// 兼容旧字段名 (空调开关)
|
|
513
|
+
if (sendOn) m.customCodes.acSendOn = sendOn;
|
|
514
|
+
if (sendOff) m.customCodes.acSendOff = sendOff;
|
|
515
|
+
if (recvOn) m.customCodes.acRecvOn = recvOn;
|
|
516
|
+
if (recvOff) m.customCodes.acRecvOff = recvOff;
|
|
517
|
+
|
|
518
|
+
// 窗帘
|
|
519
|
+
var sendOpen = row.find('.custom-code-send-open').val() || row.find('.custom-send-open').val();
|
|
520
|
+
var sendClose = row.find('.custom-code-send-close').val() || row.find('.custom-send-close').val();
|
|
521
|
+
var sendStop = row.find('.custom-code-send-stop').val() || row.find('.custom-send-stop').val();
|
|
522
|
+
var recvOpen = row.find('.custom-code-recv-open').val() || row.find('.custom-recv-open').val();
|
|
523
|
+
var recvClose = row.find('.custom-code-recv-close').val() || row.find('.custom-recv-close').val();
|
|
524
|
+
var recvStop = row.find('.custom-code-recv-stop').val() || row.find('.custom-recv-stop').val();
|
|
525
|
+
|
|
526
|
+
if (sendOpen) m.customCodes.sendOpen = sendOpen;
|
|
527
|
+
if (sendClose) m.customCodes.sendClose = sendClose;
|
|
528
|
+
if (sendStop) m.customCodes.sendStop = sendStop;
|
|
529
|
+
if (recvOpen) m.customCodes.recvOpen = recvOpen;
|
|
530
|
+
if (recvClose) m.customCodes.recvClose = recvClose;
|
|
531
|
+
if (recvStop) m.customCodes.recvStop = recvStop;
|
|
532
|
+
|
|
533
|
+
// 动态提取所有空调相关的自定义码 (ac-, fan-, mode-, temp-)
|
|
534
|
+
row.find('[class^="custom-ac-"], [class^="custom-fan-"], [class^="custom-mode-"], [class^="custom-temp-"]').each(function() {
|
|
535
|
+
var cls = $(this).attr('class').split(' ')[0];
|
|
536
|
+
var val = $(this).val();
|
|
537
|
+
if (!val) return;
|
|
538
|
+
|
|
539
|
+
if (cls.startsWith('custom-temp-')) {
|
|
540
|
+
var parts = cls.split('-');
|
|
541
|
+
if (parts.length === 5) {
|
|
542
|
+
// 独立温度码: custom-temp-send-cool-16
|
|
543
|
+
var type = parts[2];
|
|
544
|
+
var mode = parts[3];
|
|
545
|
+
var temp = parts[4];
|
|
546
|
+
var key = 'temp' + type.charAt(0).toUpperCase() + type.slice(1) +
|
|
547
|
+
mode.charAt(0).toUpperCase() + mode.slice(1) + temp;
|
|
548
|
+
m.customCodes[key] = val;
|
|
549
|
+
} else {
|
|
550
|
+
// 模板码: custom-temp-send-cool
|
|
551
|
+
var key = cls.replace('custom-', '').replace(/-([a-z])/g, function(g) { return g[1].toUpperCase(); });
|
|
552
|
+
m.customCodes[key] = val;
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
// 其他码: custom-ac-send-on -> acSendOn
|
|
556
|
+
var key = cls.replace('custom-', '').replace(/-([a-z])/g, function(g) { return g[1].toUpperCase(); });
|
|
557
|
+
m.customCodes[key] = val;
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// 场景
|
|
562
|
+
var trigger = row.find('.custom-code-trigger').val() || row.find('.custom-trigger').val();
|
|
389
563
|
if (trigger) m.customCodes.trigger = trigger;
|
|
390
564
|
}
|
|
391
565
|
mappings.push(m);
|
package/nodes/symi-485-bridge.js
CHANGED
|
@@ -309,7 +309,11 @@ module.exports = function(RED) {
|
|
|
309
309
|
name: '自定义窗帘',
|
|
310
310
|
type: 'cover',
|
|
311
311
|
customMode: true,
|
|
312
|
-
|
|
312
|
+
},
|
|
313
|
+
'custom_climate': {
|
|
314
|
+
name: '自定义空调',
|
|
315
|
+
type: 'climate',
|
|
316
|
+
customMode: true,
|
|
313
317
|
},
|
|
314
318
|
'custom_scene': {
|
|
315
319
|
name: '自定义场景',
|
|
@@ -526,7 +530,7 @@ module.exports = function(RED) {
|
|
|
526
530
|
setTimeout(() => {
|
|
527
531
|
node.initializing = false;
|
|
528
532
|
node.log('[RS485 Bridge] 初始化完成,开始同步');
|
|
529
|
-
},
|
|
533
|
+
}, 10000); // 10秒初始化延迟
|
|
530
534
|
|
|
531
535
|
// Mesh设备状态变化处理(事件驱动)
|
|
532
536
|
const handleMeshStateChange = (eventData) => {
|
|
@@ -708,13 +712,12 @@ module.exports = function(RED) {
|
|
|
708
712
|
}).catch(err => {
|
|
709
713
|
node.error(`[Mesh->杜亚] 位置同步失败: ${err.message}`);
|
|
710
714
|
});
|
|
711
|
-
}, 100);
|
|
715
|
+
}, 100);
|
|
712
716
|
}
|
|
713
717
|
continue;
|
|
714
718
|
}
|
|
715
719
|
|
|
716
720
|
// 【自定义窗帘】处理Mesh面板控制同步到485(逻辑同杜亚)
|
|
717
|
-
// 必须检查isUserControl,只有NODE_ACK(subOpcode=0x05)才是用户控制
|
|
718
721
|
if (mapping.brand === 'custom' && mapping.device === 'custom_curtain' && mapping.customCodes) {
|
|
719
722
|
const now = Date.now();
|
|
720
723
|
const curtainKey = `custom_curtain_${mac}`;
|
|
@@ -1169,8 +1172,8 @@ module.exports = function(RED) {
|
|
|
1169
1172
|
// 开关类型
|
|
1170
1173
|
if (mapping.device === 'custom_switch') {
|
|
1171
1174
|
for (const [key, value] of Object.entries(state)) {
|
|
1172
|
-
if (key === 'switch' || key === 'acSwitch') {
|
|
1173
|
-
const hexCode = value ? codes.
|
|
1175
|
+
if (key === 'switch' || key === 'acSwitch' || key.startsWith('switch_')) {
|
|
1176
|
+
const hexCode = value ? codes.sendOn : codes.sendOff;
|
|
1174
1177
|
if (hexCode) {
|
|
1175
1178
|
await node.sendCustomCode(hexCode);
|
|
1176
1179
|
node.log(`[Mesh->自定义] 开关: ${value ? '开' : '关'}, 发送: ${hexCode}`);
|
|
@@ -1190,41 +1193,29 @@ module.exports = function(RED) {
|
|
|
1190
1193
|
// 1. 优先检查动作命令
|
|
1191
1194
|
if (state.curtainAction !== undefined || state.action !== undefined) {
|
|
1192
1195
|
const action = state.curtainAction || state.action;
|
|
1193
|
-
if (action === 1 || action === 'open') {
|
|
1194
|
-
hexCode = codes.
|
|
1196
|
+
if (action === 1 || action === 'open' || action === 'opening') {
|
|
1197
|
+
hexCode = codes.sendOpen;
|
|
1195
1198
|
actionName = '打开';
|
|
1196
|
-
} else if (action === 2 || action === 'close') {
|
|
1197
|
-
hexCode = codes.
|
|
1199
|
+
} else if (action === 2 || action === 'close' || action === 'closing') {
|
|
1200
|
+
hexCode = codes.sendClose;
|
|
1198
1201
|
actionName = '关闭';
|
|
1199
|
-
} else if (action === 3 || action === 'stop') {
|
|
1200
|
-
hexCode = codes.
|
|
1202
|
+
} else if (action === 3 || action === 'stop' || action === 'stopped') {
|
|
1203
|
+
hexCode = codes.sendStop;
|
|
1201
1204
|
actionName = '停止';
|
|
1202
1205
|
}
|
|
1203
1206
|
}
|
|
1204
|
-
// 2. 其次检查运行状态
|
|
1205
1207
|
else if (state.curtainStatus !== undefined) {
|
|
1206
1208
|
if (state.curtainStatus === 1) {
|
|
1207
|
-
hexCode = codes.
|
|
1208
|
-
actionName = '打开
|
|
1209
|
+
hexCode = codes.sendOpen;
|
|
1210
|
+
actionName = '打开';
|
|
1209
1211
|
} else if (state.curtainStatus === 2) {
|
|
1210
|
-
hexCode = codes.
|
|
1211
|
-
actionName = '关闭
|
|
1212
|
-
} else if (state.curtainStatus === 0 && codes.
|
|
1213
|
-
|
|
1214
|
-
hexCode = codes.stop;
|
|
1212
|
+
hexCode = codes.sendClose;
|
|
1213
|
+
actionName = '关闭';
|
|
1214
|
+
} else if (state.curtainStatus === 0 && codes.sendStop) {
|
|
1215
|
+
hexCode = codes.sendStop;
|
|
1215
1216
|
actionName = '停止';
|
|
1216
1217
|
}
|
|
1217
1218
|
}
|
|
1218
|
-
// 3. 最后检查位置(仅在极端位置时)
|
|
1219
|
-
else if (state.curtainPosition !== undefined) {
|
|
1220
|
-
if (state.curtainPosition >= 95) {
|
|
1221
|
-
hexCode = codes.open;
|
|
1222
|
-
actionName = '打开(位置>=95)';
|
|
1223
|
-
} else if (state.curtainPosition <= 5) {
|
|
1224
|
-
hexCode = codes.close;
|
|
1225
|
-
actionName = '关闭(位置<=5)';
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
1219
|
|
|
1229
1220
|
if (hexCode) {
|
|
1230
1221
|
// 防抖:500ms内不重复发送相同命令
|
|
@@ -1240,6 +1231,51 @@ module.exports = function(RED) {
|
|
|
1240
1231
|
}
|
|
1241
1232
|
}
|
|
1242
1233
|
}
|
|
1234
|
+
// 空调类型
|
|
1235
|
+
else if (mapping.device === 'custom_climate') {
|
|
1236
|
+
for (const [key, value] of Object.entries(state)) {
|
|
1237
|
+
let hexCode = null;
|
|
1238
|
+
if (key === 'acSwitch' || key === 'climateSwitch') {
|
|
1239
|
+
hexCode = value ? codes.acSendOn : codes.acSendOff;
|
|
1240
|
+
}
|
|
1241
|
+
else if (key === 'acFanSpeed' || key === 'fanSpeed') {
|
|
1242
|
+
if (value === 1) hexCode = codes.fanSendHigh;
|
|
1243
|
+
else if (value === 2) hexCode = codes.fanSendMid;
|
|
1244
|
+
else if (value === 3) hexCode = codes.fanSendLow;
|
|
1245
|
+
}
|
|
1246
|
+
else if (key === 'acMode' || key === 'climateMode') {
|
|
1247
|
+
if (value === 1) hexCode = codes.modeSendCool;
|
|
1248
|
+
else if (value === 2) hexCode = codes.modeSendHeat;
|
|
1249
|
+
else if (value === 4) hexCode = codes.modeSendDry;
|
|
1250
|
+
else if (value === 3) hexCode = codes.modeSendFan;
|
|
1251
|
+
}
|
|
1252
|
+
else if (key === 'targetTemp' || key === 'acTargetTemp') {
|
|
1253
|
+
const temp = Math.round(value);
|
|
1254
|
+
const mode = state.acMode || state.climateMode || 1; // 1=Cool, 2=Heat, 3=Fan, 4=Dry
|
|
1255
|
+
|
|
1256
|
+
// 1. 优先尝试特定温度码,如 tempSendCool16
|
|
1257
|
+
const modeSuffix = (mode === 2) ? 'Heat' : 'Cool';
|
|
1258
|
+
const type = 'Send';
|
|
1259
|
+
const specificKey = 'temp' + type + modeSuffix + temp;
|
|
1260
|
+
hexCode = codes[specificKey];
|
|
1261
|
+
|
|
1262
|
+
// 2. 如果没有特定温度码,尝试模板
|
|
1263
|
+
if (!hexCode) {
|
|
1264
|
+
let template = (mode === 2) ? codes.tempSendHeat : codes.tempSendCool;
|
|
1265
|
+
if (!template) template = codes.tempSend; // 回退到通用模板
|
|
1266
|
+
|
|
1267
|
+
if (template && template.includes('{TEMP}')) {
|
|
1268
|
+
hexCode = template.replace('{TEMP}', temp.toString(16).toUpperCase().padStart(2, '0'));
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if (hexCode) {
|
|
1274
|
+
await node.sendCustomCode(hexCode);
|
|
1275
|
+
node.log(`[Mesh->自定义] 空调 ${key}=${value}, 发送: ${hexCode}`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1243
1279
|
// 场景类型
|
|
1244
1280
|
else if (mapping.device === 'custom_scene') {
|
|
1245
1281
|
if (state.trigger && codes.trigger) {
|
|
@@ -1795,32 +1831,110 @@ module.exports = function(RED) {
|
|
|
1795
1831
|
|
|
1796
1832
|
node.debug(`[自定义码检测] 检查映射: device=${mapping.device}, meshMac=${mapping.meshMac}, codes=${JSON.stringify(codes)}`);
|
|
1797
1833
|
|
|
1798
|
-
// 开关类型:匹配
|
|
1834
|
+
// 开关类型:匹配recvOn/recvOff
|
|
1799
1835
|
if (mapping.device === 'custom_switch') {
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1836
|
+
const recvOn = (codes.recvOn || '').replace(/\s/g, '').toUpperCase();
|
|
1837
|
+
const recvOff = (codes.recvOff || '').replace(/\s/g, '').toUpperCase();
|
|
1838
|
+
|
|
1839
|
+
// 翻转模式:收开码=收关码
|
|
1840
|
+
if (recvOn && recvOff && recvOn === recvOff && hexStr.includes(recvOn)) {
|
|
1841
|
+
const device = node.gateway.deviceManager.getDeviceByMac(mapping.meshMac);
|
|
1842
|
+
const currentState = device?.state?.switch || false;
|
|
1843
|
+
matchedAction = { switch: !currentState };
|
|
1844
|
+
node.log(`[自定义开关] 翻转: ${currentState} -> ${!currentState}`);
|
|
1845
|
+
} else {
|
|
1846
|
+
if (recvOn && hexStr.includes(recvOn)) {
|
|
1847
|
+
matchedAction = { switch: true };
|
|
1848
|
+
} else if (recvOff && hexStr.includes(recvOff)) {
|
|
1849
|
+
matchedAction = { switch: false };
|
|
1850
|
+
}
|
|
1804
1851
|
}
|
|
1805
1852
|
}
|
|
1806
|
-
// 窗帘类型:匹配
|
|
1853
|
+
// 窗帘类型:匹配recvOpen/recvClose/recvStop
|
|
1807
1854
|
else if (mapping.device === 'custom_curtain') {
|
|
1808
|
-
const
|
|
1809
|
-
const
|
|
1810
|
-
const
|
|
1811
|
-
node.debug(`[自定义窗帘匹配] 帧hex=${hexStr}, open=${openCode}, close=${closeCode}, stop=${stopCode}`);
|
|
1855
|
+
const recvOpen = (codes.recvOpen || '').replace(/\s/g, '').toUpperCase();
|
|
1856
|
+
const recvClose = (codes.recvClose || '').replace(/\s/g, '').toUpperCase();
|
|
1857
|
+
const recvStop = (codes.recvStop || '').replace(/\s/g, '').toUpperCase();
|
|
1812
1858
|
|
|
1813
|
-
if (
|
|
1859
|
+
if (recvOpen && hexStr.includes(recvOpen)) {
|
|
1814
1860
|
matchedAction = { action: 'open' };
|
|
1815
|
-
node.log(`[
|
|
1816
|
-
} else if (
|
|
1861
|
+
node.log(`[自定义窗帘] 收开码`);
|
|
1862
|
+
} else if (recvClose && hexStr.includes(recvClose)) {
|
|
1817
1863
|
matchedAction = { action: 'close' };
|
|
1818
|
-
node.log(`[
|
|
1819
|
-
} else if (
|
|
1864
|
+
node.log(`[自定义窗帘] 收关码`);
|
|
1865
|
+
} else if (recvStop && hexStr.includes(recvStop)) {
|
|
1820
1866
|
matchedAction = { action: 'stop' };
|
|
1821
|
-
node.log(`[
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1867
|
+
node.log(`[自定义窗帘] 收停码`);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
// 空调类型:匹配收码
|
|
1871
|
+
else if (mapping.device === 'custom_climate') {
|
|
1872
|
+
if (codes.acRecvOn && hexStr.includes(codes.acRecvOn.replace(/\s/g, '').toUpperCase())) {
|
|
1873
|
+
matchedAction = { acSwitch: true };
|
|
1874
|
+
} else if (codes.acRecvOff && hexStr.includes(codes.acRecvOff.replace(/\s/g, '').toUpperCase())) {
|
|
1875
|
+
matchedAction = { acSwitch: false };
|
|
1876
|
+
}
|
|
1877
|
+
else if (codes.fanRecvHigh && hexStr.includes(codes.fanRecvHigh.replace(/\s/g, '').toUpperCase())) {
|
|
1878
|
+
matchedAction = { acFanSpeed: 1 };
|
|
1879
|
+
} else if (codes.fanRecvMid && hexStr.includes(codes.fanRecvMid.replace(/\s/g, '').toUpperCase())) {
|
|
1880
|
+
matchedAction = { acFanSpeed: 2 };
|
|
1881
|
+
} else if (codes.fanRecvLow && hexStr.includes(codes.fanRecvLow.replace(/\s/g, '').toUpperCase())) {
|
|
1882
|
+
matchedAction = { acFanSpeed: 3 };
|
|
1883
|
+
}
|
|
1884
|
+
else if (codes.modeRecvCool && hexStr.includes(codes.modeRecvCool.replace(/\s/g, '').toUpperCase())) {
|
|
1885
|
+
for (const m of modes) {
|
|
1886
|
+
for (let t = 16; t <= 30; t++) {
|
|
1887
|
+
const key = `tempRecv${m.suffix}${t}`;
|
|
1888
|
+
const code = (codes[key] || '').replace(/\s/g, '').toUpperCase();
|
|
1889
|
+
if (code && hexStr.includes(code)) {
|
|
1890
|
+
matchedAction = { acTargetTemp: t, acMode: m.val };
|
|
1891
|
+
foundTemp = true;
|
|
1892
|
+
break;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
if (foundTemp) break;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// 2. 如果没匹配到独立码,匹配模板
|
|
1899
|
+
if (!foundTemp) {
|
|
1900
|
+
const tempTemplates = [
|
|
1901
|
+
{ template: codes.tempRecvCool, mode: 1 },
|
|
1902
|
+
{ template: codes.tempRecvHeat, mode: 2 },
|
|
1903
|
+
{ template: codes.tempRecv, mode: 0 } // 通用
|
|
1904
|
+
];
|
|
1905
|
+
|
|
1906
|
+
for (const item of tempTemplates) {
|
|
1907
|
+
if (item.template && item.template.includes('{TEMP}')) {
|
|
1908
|
+
const template = item.template.replace(/\s/g, '').toUpperCase();
|
|
1909
|
+
const parts = template.split('{TEMP}');
|
|
1910
|
+
if (parts.length === 2) {
|
|
1911
|
+
const prefix = parts[0];
|
|
1912
|
+
const suffix = parts[1];
|
|
1913
|
+
if (hexStr.includes(prefix)) {
|
|
1914
|
+
// 尝试从 hexStr 中提取温度
|
|
1915
|
+
// 简单起见,如果包含前缀且后面有2位十六进制+后缀
|
|
1916
|
+
const startIdx = hexStr.indexOf(prefix) + prefix.length;
|
|
1917
|
+
const tempHex = hexStr.substring(startIdx, startIdx + 2);
|
|
1918
|
+
const remaining = hexStr.substring(startIdx + 2);
|
|
1919
|
+
|
|
1920
|
+
if (tempHex.length === 2 && (!suffix || remaining.includes(suffix))) {
|
|
1921
|
+
const temp = parseInt(tempHex, 16);
|
|
1922
|
+
if (temp >= 16 && temp <= 30) {
|
|
1923
|
+
matchedAction = { acTargetTemp: temp };
|
|
1924
|
+
if (item.mode > 0) matchedAction.acMode = item.mode;
|
|
1925
|
+
foundTemp = true;
|
|
1926
|
+
break;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
if (matchedAction) {
|
|
1937
|
+
node.log(`[自定义空调] 匹配: ${JSON.stringify(matchedAction)}`);
|
|
1824
1938
|
}
|
|
1825
1939
|
}
|
|
1826
1940
|
// 场景类型:匹配trigger
|
package/nodes/symi-knx-bridge.js
CHANGED
|
@@ -160,7 +160,7 @@ module.exports = function(RED) {
|
|
|
160
160
|
node.initTimer = setTimeout(() => {
|
|
161
161
|
node.initializing = false;
|
|
162
162
|
node.log('[KNX Bridge] 初始化完成,开始同步');
|
|
163
|
-
},
|
|
163
|
+
}, 10000); // 10秒初始化延迟
|
|
164
164
|
|
|
165
165
|
if (node.mappings.length === 0) {
|
|
166
166
|
node.status({ fill: 'grey', shape: 'ring', text: '请添加实体映射' });
|