node-red-contrib-symi-mesh 1.6.8 → 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.
@@ -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: '请添加实体映射' });