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 CHANGED
@@ -603,7 +603,7 @@ Tab分隔,每行一个实体:
603
603
 
604
604
  #### 注意事项
605
605
 
606
- 1. **20秒初始化延迟**:部署后前20秒不同步,等待Mesh设备发现完成
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
- - **打开码**:Mesh窗帘打开 ↔ RS485打开命令
755
- - **关闭码**:Mesh窗帘关闭 ↔ RS485关闭命令
756
- - **停止码**:Mesh窗帘停止 ↔ RS485停止命令
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
- 打开码: 55 02 02 03 01 49 44
763
- 关闭码: 55 02 02 03 02 09 45
764
- 停止码: 55 02 02 03 03 C8 85
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帧与自定义码匹配,触发对应Mesh设备
770
- 3. **防死循环**:500ms内不重复触发,避免Mesh→RS485→Mesh循环
796
+ 1. **Mesh→RS485**:Mesh设备状态变化时,发送对应的发码
797
+ 2. **RS485→Mesh**:收到的RS485帧与收码匹配,触发对应Mesh设备
798
+ 3. **翻转逻辑**:开关的收开码=收关码时,收到后翻转当前状态
799
+ 4. **防死循环**:500ms内不重复触发,避免Mesh→RS485→Mesh循环
771
800
 
772
- > 自定义码支持任意长度的十六进制字符串,支持空格分隔(如 `55 01 01` 或 `550101`)
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.6.9
1356
+ **版本**: 1.7.0
1312
1357
  **协议**: 蓝牙MESH网关(初级版)串口协议V1.0
1313
- **最后更新**: 2025-12-20
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
- // 杜亚窗帘使用2字节地址
163
+ // 地址输入框:杜亚2字节地址,自定义模式隐藏,其他显示
164
164
  var addrHtml = '';
165
- if (m.brand === 'duya') {
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-code-on').off('change').on('change', function() {
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.on = $(this).val();
288
+ mappings[idx].customCodes.sendOn = $(this).val();
287
289
  });
288
- container.find('.custom-code-off').off('change').on('change', function() {
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.off = $(this).val();
293
+ mappings[idx].customCodes.sendOff = $(this).val();
292
294
  });
293
- container.find('.custom-code-open').off('change').on('change', function() {
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.open = $(this).val();
298
+ mappings[idx].customCodes.recvOn = $(this).val();
297
299
  });
298
- container.find('.custom-code-close').off('change').on('change', function() {
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.close = $(this).val();
303
+ mappings[idx].customCodes.recvOff = $(this).val();
302
304
  });
303
- container.find('.custom-code-stop').off('change').on('change', function() {
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.stop = $(this).val();
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
- container.find('.custom-code-trigger').off('change').on('change', function() {
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:flex;gap:8px;flex-wrap:wrap;">';
326
- html += '<label style="font-size:11px;">打开码: <input type="text" class="custom-code-on" value="' + (codes.on || '') + '" placeholder="55 01 01..." style="width:150px;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:11px;font-family:monospace;"></label>';
327
- html += '<label style="font-size:11px;">关闭码: <input type="text" class="custom-code-off" value="' + (codes.off || '') + '" placeholder="55 01 02..." style="width:150px;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:11px;font-family:monospace;"></label>';
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:flex;gap:8px;flex-wrap:wrap;">';
331
- html += '<label style="font-size:11px;">打开码: <input type="text" class="custom-code-open" value="' + (codes.open || '') + '" placeholder="55 02 02 03 01..." style="width:150px;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:11px;font-family:monospace;"></label>';
332
- html += '<label style="font-size:11px;">关闭码: <input type="text" class="custom-code-close" value="' + (codes.close || '') + '" placeholder="55 02 02 03 02..." style="width:150px;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:11px;font-family:monospace;"></label>';
333
- html += '<label style="font-size:11px;">停止码: <input type="text" class="custom-code-stop" value="' + (codes.stop || '') + '" placeholder="55 02 02 03 03..." style="width:150px;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:11px;font-family:monospace;"></label>';
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
- var on = $(this).find('.custom-code-on').val();
379
- var off = $(this).find('.custom-code-off').val();
380
- var open = $(this).find('.custom-code-open').val();
381
- var close = $(this).find('.custom-code-close').val();
382
- var stop = $(this).find('.custom-code-stop').val();
383
- var trigger = $(this).find('.custom-code-trigger').val();
384
- if (on) m.customCodes.on = on;
385
- if (off) m.customCodes.off = off;
386
- if (open) m.customCodes.open = open;
387
- if (close) m.customCodes.close = close;
388
- if (stop) m.customCodes.stop = stop;
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);
@@ -309,7 +309,11 @@ module.exports = function(RED) {
309
309
  name: '自定义窗帘',
310
310
  type: 'cover',
311
311
  customMode: true,
312
- // 用户需要在映射中配置: customCodes.open, customCodes.close, customCodes.stop
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
- }, 20000);
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); // 延迟100ms发送位置命令
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.on : codes.off;
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.open;
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.close;
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.stop;
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.open;
1208
- actionName = '打开(运行中)';
1209
+ hexCode = codes.sendOpen;
1210
+ actionName = '打开';
1209
1211
  } else if (state.curtainStatus === 2) {
1210
- hexCode = codes.close;
1211
- actionName = '关闭(运行中)';
1212
- } else if (state.curtainStatus === 0 && codes.stop) {
1213
- // 0=已停止,发送停止码
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
- // 开关类型:匹配on/off
1834
+ // 开关类型:匹配recvOn/recvOff
1799
1835
  if (mapping.device === 'custom_switch') {
1800
- if (codes.on && hexStr.includes(codes.on.replace(/\s/g, '').toUpperCase())) {
1801
- matchedAction = { switch: true };
1802
- } else if (codes.off && hexStr.includes(codes.off.replace(/\s/g, '').toUpperCase())) {
1803
- matchedAction = { switch: false };
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
- // 窗帘类型:匹配open/close/stop(只设置action,避免重复发码)
1853
+ // 窗帘类型:匹配recvOpen/recvClose/recvStop
1807
1854
  else if (mapping.device === 'custom_curtain') {
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}`);
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 (openCode && hexStr.includes(openCode)) {
1859
+ if (recvOpen && hexStr.includes(recvOpen)) {
1814
1860
  matchedAction = { action: 'open' };
1815
- node.log(`[自定义窗帘匹配] 匹配到打开码!`);
1816
- } else if (closeCode && hexStr.includes(closeCode)) {
1861
+ node.log(`[自定义窗帘] 收开码`);
1862
+ } else if (recvClose && hexStr.includes(recvClose)) {
1817
1863
  matchedAction = { action: 'close' };
1818
- node.log(`[自定义窗帘匹配] 匹配到关闭码!`);
1819
- } else if (stopCode && hexStr.includes(stopCode)) {
1864
+ node.log(`[自定义窗帘] 收关码`);
1865
+ } else if (recvStop && hexStr.includes(recvStop)) {
1820
1866
  matchedAction = { action: 'stop' };
1821
- node.log(`[自定义窗帘匹配] 匹配到停止码!`);
1822
- } else {
1823
- node.debug(`[自定义窗帘匹配] 未匹配到任何码`);
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
@@ -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
- }, 20000); // 20秒初始化延迟
163
+ }, 10000); // 10秒初始化延迟
164
164
 
165
165
  if (node.mappings.length === 0) {
166
166
  node.status({ fill: 'grey', shape: 'ring', text: '请添加实体映射' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.6.9",
3
+ "version": "1.7.0",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "nodes/symi-gateway.js",
6
6
  "scripts": {