node-red-contrib-symi-mesh 1.8.4 → 1.8.5

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.
@@ -1,5 +1,11 @@
1
1
  /**
2
2
  * MQTT同步节点 - 实现第三方MQTT品牌设备与Mesh设备的双向同步
3
+ * 版本: 1.8.5
4
+ *
5
+ * v1.8.5 更新:
6
+ * - 使用通用 SyncUtils 类统一防环路逻辑
7
+ * - 添加三合一面板子实体同步支持 (空调/新风/地暖)
8
+ * - 支持 meshSubEntity 映射字段
3
9
  *
4
10
  * 架构设计:
5
11
  * - 使用symi-mqtt配置节点共享MQTT连接(与Mesh网关MQTT一致)
@@ -11,6 +17,7 @@
11
17
 
12
18
  module.exports = function(RED) {
13
19
  const mqtt = require('mqtt');
20
+ const { SyncUtils, THREE_IN_ONE_SUB_ENTITIES, getSubEntityByHyqwType } = require('../lib/sync-utils');
14
21
 
15
22
  // ===== 常量定义 =====
16
23
  const SYNC_DEBOUNCE_MS = 2000;
@@ -110,6 +117,11 @@ module.exports = function(RED) {
110
117
  node._cleanupTimer = null;
111
118
  node._closing = false;
112
119
  node._lastErrorLog = 0; // 错误日志限流
120
+
121
+ // 初始化通用同步工具
122
+ node._syncUtils = new SyncUtils({
123
+ defaultTimeout: SYNC_DEBOUNCE_MS
124
+ });
113
125
 
114
126
  if (!node._mqttConfig) {
115
127
  node.status({ fill: 'red', shape: 'ring', text: '未选择Mesh MQTT' });
@@ -263,36 +275,41 @@ module.exports = function(RED) {
263
275
  function syncToMesh(deviceType, deviceId, fn, fv) {
264
276
  if (!node._gateway?.deviceManager) return;
265
277
 
266
- const mapping = node.mappings.find(m =>
267
- parseInt(m.brandDeviceType) === deviceType && parseInt(m.brandDeviceId) === deviceId
268
- );
278
+ // 查找映射 - 支持三合一子实体
279
+ const mapping = node.mappings.find(m => {
280
+ const typeMatch = parseInt(m.brandDeviceType) === deviceType;
281
+ const idMatch = parseInt(m.brandDeviceId) === deviceId;
282
+ return typeMatch && idMatch;
283
+ });
269
284
  if (!mapping || !mapping.meshMac) return;
270
285
 
271
286
  const meshMac = mapping.meshMac;
272
287
  const meshChannel = parseInt(mapping.meshChannel) || 1;
288
+ const meshSubEntity = mapping.meshSubEntity; // 三合一子实体类型
273
289
 
274
- // 防死循环 - 检查两个方向的时间戳
275
- const now = Date.now();
276
- const mqttSyncKey = `mqtt_${deviceType}_${deviceId}_${fn}`;
277
- const meshSyncKey = `mesh_${meshMac}_${meshChannel}_${fn}`;
278
- const lastMqttSync = node._syncTimestamps.get(mqttSyncKey) || 0;
279
- const lastMeshSync = node._syncTimestamps.get(meshSyncKey) || 0;
280
-
281
- // 如果任一方向在防抖时间内有同步,跳过
282
- if (now - lastMqttSync < SYNC_DEBOUNCE_MS || now - lastMeshSync < SYNC_DEBOUNCE_MS) return;
290
+ // 使用 SyncUtils 防环路
291
+ const syncKey = `${meshMac}_${meshChannel}_${meshSubEntity || 'default'}_${fn}`;
292
+ if (node._syncUtils.shouldPreventSync('target-to-mesh', syncKey)) {
293
+ return;
294
+ }
283
295
 
284
296
  const device = node._gateway.deviceManager.getDeviceByMac(meshMac);
285
297
  if (!device) return;
286
298
 
287
- // 记录时间戳 - 同时标记两个方向
288
- node._syncTimestamps.set(mqttSyncKey, now);
289
- node._syncTimestamps.set(meshSyncKey, now);
299
+ // 记录同步时间
300
+ node._syncUtils.recordSyncTime('target-to-mesh', syncKey);
290
301
 
291
302
  const typeInfo = node._brandProtocol.deviceTypes[deviceType];
292
303
  if (!typeInfo) return;
293
304
 
294
305
  let property = '', value = null;
295
306
  try {
307
+ // 三合一子实体特殊处理
308
+ if (meshSubEntity && THREE_IN_ONE_SUB_ENTITIES[meshSubEntity]) {
309
+ syncThreeInOneToMesh(device, meshMac, meshSubEntity, fn, fv);
310
+ return;
311
+ }
312
+
296
313
  // 窗帘设备特殊处理 (typeId=14)
297
314
  if (deviceType === 14) {
298
315
  if (fn === 1) {
@@ -392,11 +409,88 @@ module.exports = function(RED) {
392
409
  node.warn(`[Brand→Mesh] 同步失败: ${e.message}`);
393
410
  }
394
411
  }
412
+
413
+ // ===== 三合一子实体同步到Mesh =====
414
+ function syncThreeInOneToMesh(device, meshMac, subEntity, fn, fv) {
415
+ const subConfig = THREE_IN_ONE_SUB_ENTITIES[subEntity];
416
+ if (!subConfig) return;
417
+
418
+ let property = '', value = null;
419
+
420
+ // 根据子实体类型和功能码处理
421
+ if (subEntity === 'aircon') {
422
+ // 空调子实体
423
+ if (fn === 1) {
424
+ property = 'climateSwitch'; value = fv === 1;
425
+ } else if (fn === 2) {
426
+ property = 'targetTemp'; value = fv;
427
+ } else if (fn === 3) {
428
+ property = 'climateMode'; value = AC_MODE_MAP[fv] || 'cool';
429
+ } else if (fn === 4) {
430
+ property = 'fanMode'; value = FAN_SPEED_MAP[fv] || 'auto';
431
+ }
432
+ } else if (subEntity === 'fresh_air') {
433
+ // 新风子实体
434
+ if (fn === 1) {
435
+ property = 'freshAirSwitch'; value = fv === 1;
436
+ } else if (fn === 3) {
437
+ property = 'freshAirSpeed'; value = FAN_SPEED_MAP[fv] || 'auto';
438
+ }
439
+ } else if (subEntity === 'floor_heating') {
440
+ // 地暖子实体
441
+ if (fn === 1) {
442
+ property = 'floorHeatingSwitch'; value = fv === 1;
443
+ } else if (fn === 2) {
444
+ property = 'floorHeatingTemp'; value = fv;
445
+ }
446
+ }
447
+
448
+ if (property && node._gateway) {
449
+ // 使用网关发送三合一控制命令
450
+ const attrMap = {
451
+ 'climateSwitch': 0x02,
452
+ 'targetTemp': 0x1B,
453
+ 'climateMode': 0x1D,
454
+ 'fanMode': 0x1C,
455
+ 'freshAirSwitch': 0x68,
456
+ 'freshAirSpeed': 0x6A,
457
+ 'floorHeatingSwitch': 0x6B,
458
+ 'floorHeatingTemp': 0x6C
459
+ };
460
+
461
+ const attrType = attrMap[property];
462
+ if (attrType && device.networkAddress) {
463
+ let param;
464
+ if (property.includes('Switch')) {
465
+ param = [value ? 0x02 : 0x01];
466
+ } else if (property.includes('Temp')) {
467
+ param = [value];
468
+ } else if (property.includes('Mode') || property.includes('Speed')) {
469
+ const reverseMap = { auto: 4, low: 3, medium: 2, high: 1, cool: 1, heat: 2, fan_only: 3, dry: 4 };
470
+ param = [reverseMap[value] || 1];
471
+ }
472
+
473
+ if (param) {
474
+ node._gateway.sendControl(device.networkAddress, attrType, param);
475
+ node.send({
476
+ topic: 'mqtt-sync/brand-to-mesh',
477
+ payload: {
478
+ direction: 'Brand→Mesh(三合一)',
479
+ subEntity, property, value,
480
+ meshMac, attrType: `0x${attrType.toString(16)}`,
481
+ timestamp: Date.now()
482
+ }
483
+ });
484
+ }
485
+ }
486
+ }
487
+ }
395
488
 
396
489
  // ===== 同步到MQTT =====
397
490
  function syncToMqtt(meshMac, channel, property, value) {
398
491
  if (!node._connected || !node._mqttClient) return;
399
492
 
493
+ // 查找映射 - 支持三合一子实体
400
494
  const mapping = node.mappings.find(m =>
401
495
  m.meshMac?.toLowerCase() === meshMac?.toLowerCase() &&
402
496
  parseInt(m.meshChannel) === channel
@@ -405,14 +499,28 @@ module.exports = function(RED) {
405
499
 
406
500
  const deviceType = parseInt(mapping.brandDeviceType);
407
501
  const deviceId = parseInt(mapping.brandDeviceId);
502
+ const meshSubEntity = mapping.meshSubEntity; // 三合一子实体类型
408
503
  const typeInfo = node._brandProtocol.deviceTypes[deviceType];
409
504
  if (!typeInfo) return;
410
505
 
506
+ // 使用 SyncUtils 防环路
507
+ const syncKey = `${meshMac}_${channel}_${meshSubEntity || 'default'}_${property}`;
508
+ if (node._syncUtils.shouldPreventSync('mesh-to-target', syncKey)) {
509
+ return;
510
+ }
511
+
411
512
  // 根据设备类型和属性确定功能码
412
513
  let fn, fv;
413
514
 
515
+ // 三合一子实体特殊处理
516
+ if (meshSubEntity && THREE_IN_ONE_SUB_ENTITIES[meshSubEntity]) {
517
+ const result = getThreeInOneFnFv(meshSubEntity, property, value);
518
+ if (!result) return;
519
+ fn = result.fn;
520
+ fv = result.fv;
521
+ }
414
522
  // 窗帘设备特殊处理 (typeId=14)
415
- if (deviceType === 14) {
523
+ else if (deviceType === 14) {
416
524
  if (property === 'curtainAction' || property === 'action') {
417
525
  fn = 1;
418
526
  if (value === 'open' || value === 1) fv = 1;
@@ -440,7 +548,7 @@ module.exports = function(RED) {
440
548
  }
441
549
  // 空调设备 (typeId=12)
442
550
  else if (deviceType === 12) {
443
- if (property === 'switch' || property === 'on' || property === 'isOn') {
551
+ if (property === 'switch' || property === 'on' || property === 'isOn' || property === 'climateSwitch') {
444
552
  fn = 1;
445
553
  fv = value ? 1 : 0;
446
554
  } else if (property === 'temperature' || property === 'targetTemp') {
@@ -458,10 +566,10 @@ module.exports = function(RED) {
458
566
  }
459
567
  // 地暖设备 (typeId=16)
460
568
  else if (deviceType === 16) {
461
- if (property === 'switch' || property === 'on' || property === 'isOn') {
569
+ if (property === 'switch' || property === 'on' || property === 'isOn' || property === 'floorHeatingSwitch') {
462
570
  fn = 1;
463
571
  fv = value ? 1 : 0;
464
- } else if (property === 'temperature' || property === 'targetTemp') {
572
+ } else if (property === 'temperature' || property === 'targetTemp' || property === 'floorHeatingTemp') {
465
573
  fn = 2;
466
574
  fv = parseInt(value) || 20;
467
575
  } else {
@@ -470,10 +578,10 @@ module.exports = function(RED) {
470
578
  }
471
579
  // 新风设备 (typeId=36)
472
580
  else if (deviceType === 36) {
473
- if (property === 'switch' || property === 'on' || property === 'isOn') {
581
+ if (property === 'switch' || property === 'on' || property === 'isOn' || property === 'freshAirSwitch') {
474
582
  fn = 1;
475
583
  fv = value ? 1 : 0;
476
- } else if (property === 'fanSpeed' || property === 'fanMode') {
584
+ } else if (property === 'fanSpeed' || property === 'fanMode' || property === 'freshAirSpeed') {
477
585
  fn = 3;
478
586
  fv = FAN_SPEED_REVERSE[value] ?? 0;
479
587
  } else {
@@ -490,19 +598,8 @@ module.exports = function(RED) {
490
598
  }
491
599
  }
492
600
 
493
- // 防死循环 - 检查两个方向的时间戳
494
- const now = Date.now();
495
- const meshSyncKey = `mesh_${meshMac}_${channel}_${fn}`;
496
- const mqttSyncKey = `mqtt_${deviceType}_${deviceId}_${fn}`;
497
- const lastMeshSync = node._syncTimestamps.get(meshSyncKey) || 0;
498
- const lastMqttSync = node._syncTimestamps.get(mqttSyncKey) || 0;
499
-
500
- // 如果任一方向在防抖时间内有同步,跳过
501
- if (now - lastMeshSync < SYNC_DEBOUNCE_MS || now - lastMqttSync < SYNC_DEBOUNCE_MS) return;
502
-
503
- // 记录时间戳 - 同时标记两个方向
504
- node._syncTimestamps.set(meshSyncKey, now);
505
- node._syncTimestamps.set(mqttSyncKey, now);
601
+ // 记录同步时间
602
+ node._syncUtils.recordSyncTime('mesh-to-target', syncKey);
506
603
 
507
604
  const topic = node._brandProtocol.getDownTopic(node);
508
605
  const payload = node._brandProtocol.buildMessage(deviceType, deviceId, fn, fv);
@@ -516,6 +613,7 @@ module.exports = function(RED) {
516
613
  payload: {
517
614
  direction: 'Mesh→Brand',
518
615
  meshMac, channel, property, value,
616
+ meshSubEntity: meshSubEntity || null,
519
617
  brandType: deviceType, brandId: deviceId, fn, fv,
520
618
  timestamp: Date.now()
521
619
  }
@@ -524,6 +622,34 @@ module.exports = function(RED) {
524
622
  node.warn(`[Mesh→Brand] 同步失败: ${e.message}`);
525
623
  }
526
624
  }
625
+
626
+ // ===== 获取三合一子实体的功能码和值 =====
627
+ function getThreeInOneFnFv(subEntity, property, value) {
628
+ if (subEntity === 'aircon') {
629
+ if (property === 'climateSwitch' || property === 'switch') {
630
+ return { fn: 1, fv: value ? 1 : 0 };
631
+ } else if (property === 'targetTemp' || property === 'temperature') {
632
+ return { fn: 2, fv: parseInt(value) || 24 };
633
+ } else if (property === 'climateMode' || property === 'mode') {
634
+ return { fn: 3, fv: AC_MODE_REVERSE[value] ?? 0 };
635
+ } else if (property === 'fanMode' || property === 'fanSpeed') {
636
+ return { fn: 4, fv: FAN_SPEED_REVERSE[value] ?? 0 };
637
+ }
638
+ } else if (subEntity === 'fresh_air') {
639
+ if (property === 'freshAirSwitch' || property === 'switch') {
640
+ return { fn: 1, fv: value ? 1 : 0 };
641
+ } else if (property === 'freshAirSpeed' || property === 'fanSpeed') {
642
+ return { fn: 3, fv: FAN_SPEED_REVERSE[value] ?? 0 };
643
+ }
644
+ } else if (subEntity === 'floor_heating') {
645
+ if (property === 'floorHeatingSwitch' || property === 'switch') {
646
+ return { fn: 1, fv: value ? 1 : 0 };
647
+ } else if (property === 'floorHeatingTemp' || property === 'temperature') {
648
+ return { fn: 2, fv: parseInt(value) || 20 };
649
+ }
650
+ }
651
+ return null;
652
+ }
527
653
 
528
654
  // ===== 网关事件监听 =====
529
655
  function setupGatewayListeners() {
@@ -584,6 +710,10 @@ module.exports = function(RED) {
584
710
  node._gateway.deviceManager.removeListener('device-state-changed', node._stateChangeHandler);
585
711
  }
586
712
  if (node._mqttClient) { try { node._mqttClient.end(true); } catch (e) {} node._mqttClient = null; }
713
+ // 清理 SyncUtils
714
+ if (node._syncUtils) {
715
+ node._syncUtils.destroy();
716
+ }
587
717
  node._syncTimestamps.clear();
588
718
  node._deviceStates.clear();
589
719
  node._discoveredDevices.clear();
@@ -13,6 +13,13 @@
13
13
  return this.name || 'MQTT配置';
14
14
  },
15
15
  oneditprepare: function() {
16
+ // 设置编辑面板更宽更高
17
+ var panel = $('#dialog-form').parent();
18
+ if (panel.length) {
19
+ if (panel.width() < 1000) panel.css('width', '1000px');
20
+ if (panel.height() < 1000) panel.css('min-height', '1000px');
21
+ }
22
+
16
23
  // 自动填充默认值到输入框
17
24
  if (!this.mqttBroker || this.mqttBroker === '') {
18
25
  $('#node-config-input-mqttBroker').val('mqtt://localhost:1883');
@@ -24,10 +24,11 @@
24
24
  return 'RS485同步';
25
25
  },
26
26
  oneditprepare: function() {
27
- // 设置编辑面板更宽
27
+ // 设置编辑面板更宽更高
28
28
  var panel = $('#dialog-form').parent();
29
29
  if (panel.length) {
30
- panel.css('min-width', '700px');
30
+ if (panel.width() < 1000) panel.css('width', '1000px');
31
+ if (panel.height() < 1000) panel.css('min-height', '1000px');
31
32
  }
32
33
 
33
34
  var node = this;
@@ -1,6 +1,8 @@
1
1
  module.exports = function(RED) {
2
2
  'use strict';
3
3
 
4
+ const { SyncUtils, StateCache, DEFAULT_TIMEOUT } = require('../lib/sync-utils');
5
+
4
6
  function SymiRS485SyncNode(config) {
5
7
  RED.nodes.createNode(this, config);
6
8
  const node = this;
@@ -22,13 +24,13 @@ module.exports = function(RED) {
22
24
  node._pollTimer = null;
23
25
  node._pollIndex = 0;
24
26
 
25
- // 防抖时间戳,防止循环同步
26
- node._lastSyncA = {};
27
- node._lastSyncB = {};
28
- const DEBOUNCE_MS = 2000;
27
+ // 初始化通用同步工具类
28
+ node.syncUtils = new SyncUtils({
29
+ defaultTimeout: DEFAULT_TIMEOUT
30
+ });
29
31
 
30
32
  // 状态缓存
31
- node._stateCache = {};
33
+ node.stateCache = new StateCache(node.context(), 'rs485_sync_state_cache');
32
34
 
33
35
  if (!node.rs485ConfigA || !node.rs485ConfigB) {
34
36
  node.status({ fill: 'red', shape: 'ring', text: '未配置RS485连接' });
@@ -42,6 +44,19 @@ module.exports = function(RED) {
42
44
 
43
45
  node.status({ fill: 'yellow', shape: 'dot', text: '初始化...' });
44
46
 
47
+ // 检查是否应该阻止同步(防死循环)
48
+ node.shouldPreventSync = function(direction, key) {
49
+ // A->B 对应 mesh-to-target, B->A 对应 target-to-mesh
50
+ const syncDirection = direction === 'a-to-b' ? 'mesh-to-target' : 'target-to-mesh';
51
+ return node.syncUtils.shouldPreventSync(syncDirection, key);
52
+ };
53
+
54
+ // 记录同步时间(用于防死循环)
55
+ node.recordSyncTime = function(direction, key) {
56
+ const syncDirection = direction === 'a-to-b' ? 'mesh-to-target' : 'target-to-mesh';
57
+ node.syncUtils.recordSyncTime(syncDirection, key);
58
+ };
59
+
45
60
  // 计算校验和(中弘协议)
46
61
  function checksum(data) {
47
62
  let sum = 0;
@@ -229,7 +244,7 @@ module.exports = function(RED) {
229
244
  const addr = cfg.address || 1;
230
245
  // 获取当前缓存的状态
231
246
  const cacheKey = `symi_${addr}`;
232
- const cached = node._stateCache[cacheKey] || { power: 0, temp: 24, mode: 0x01, fan: 0x01 };
247
+ const cached = node.stateCache.get(cacheKey) || { power: 0, temp: 24, mode: 0x01, fan: 0x01 };
233
248
 
234
249
  let power = cached.power;
235
250
  let temp = cached.temp;
@@ -293,15 +308,24 @@ module.exports = function(RED) {
293
308
 
294
309
  // 同步状态:从A到B
295
310
  function syncAtoB(mapping, state) {
296
- const key = JSON.stringify({ a: mapping.configA, b: mapping.configB });
297
- const now = Date.now();
311
+ const loopKey = JSON.stringify({ a: mapping.configA, b: mapping.configB });
312
+
313
+ // 使用通用工具检查防环路
314
+ if (node.shouldPreventSync('a-to-b', loopKey)) {
315
+ node.debug(`[A->B] 防环路跳过: ${loopKey}`);
316
+ return;
317
+ }
298
318
 
299
- if (node._lastSyncB[key] && (now - node._lastSyncB[key]) < DEBOUNCE_MS) {
300
- node.debug(`[A->B] 防抖跳过`);
319
+ // 使用状态缓存检查是否有实际变化
320
+ const cacheKey = `mapping_${loopKey}`;
321
+ if (!node.stateCache.hasChanged(cacheKey, state)) {
322
+ node.debug(`[A->B] 状态未变化跳过: ${JSON.stringify(state)}`);
301
323
  return;
302
324
  }
325
+ node.stateCache.update(cacheKey, state);
303
326
 
304
- node._lastSyncA[key] = now;
327
+ // 记录同步时间
328
+ node.recordSyncTime('a-to-b', loopKey);
305
329
 
306
330
  node.log(`[A->B] 同步状态: ${JSON.stringify(state)}`);
307
331
 
@@ -349,15 +373,24 @@ module.exports = function(RED) {
349
373
 
350
374
  // 同步状态:从B到A
351
375
  function syncBtoA(mapping, state) {
352
- const key = JSON.stringify({ a: mapping.configA, b: mapping.configB });
353
- const now = Date.now();
376
+ const loopKey = JSON.stringify({ a: mapping.configA, b: mapping.configB });
354
377
 
355
- if (node._lastSyncA[key] && (now - node._lastSyncA[key]) < DEBOUNCE_MS) {
356
- node.debug(`[B->A] 防抖跳过`);
378
+ // 使用通用工具检查防环路
379
+ if (node.shouldPreventSync('b-to-a', loopKey)) {
380
+ node.debug(`[B->A] 防环路跳过: ${loopKey}`);
357
381
  return;
358
382
  }
359
383
 
360
- node._lastSyncB[key] = now;
384
+ // 使用状态缓存检查是否有实际变化
385
+ const cacheKey = `mapping_${loopKey}`;
386
+ if (!node.stateCache.hasChanged(cacheKey, state)) {
387
+ node.debug(`[B->A] 状态未变化跳过: ${JSON.stringify(state)}`);
388
+ return;
389
+ }
390
+ node.stateCache.update(cacheKey, state);
391
+
392
+ // 记录同步时间
393
+ node.recordSyncTime('b-to-a', loopKey);
361
394
 
362
395
  node.log(`[B->A] 同步状态: ${JSON.stringify(state)}`);
363
396
 
@@ -451,6 +484,11 @@ module.exports = function(RED) {
451
484
  mode: climate.mode,
452
485
  fanMode: climate.fanMode
453
486
  };
487
+
488
+ // 更新本地状态缓存,供B侧同步参考
489
+ const addrKey = `zhonghong_${climate.outdoorAddr}_${climate.indoorAddr}`;
490
+ node.stateCache.update(addrKey, state);
491
+
454
492
  node.log(`[A->B] 内机${climate.outdoorAddr}-${climate.indoorAddr}: 开=${state.power}, 温=${state.targetTemp}, 模式=${state.mode}`);
455
493
  syncAtoB(mapping, state);
456
494
  }
@@ -471,13 +509,13 @@ module.exports = function(RED) {
471
509
  } else if (mapping.protocolA === 'symi_climate') {
472
510
  state = parseSymiClimateFrame(frame, mapping.configA);
473
511
  if (state) {
474
- const cacheKey = `symi_${mapping.configA.address || 1}`;
475
- node._stateCache[cacheKey] = {
512
+ const addrKey = `symi_${mapping.configA.address || 1}`;
513
+ node.stateCache.update(addrKey, {
476
514
  power: state.power ? 1 : 0,
477
515
  temp: state.targetTemp,
478
516
  mode: state.mode,
479
517
  fan: state.fanMode
480
- };
518
+ });
481
519
  state.mode = SYMI_TO_ZH_MODE[state.mode] || state.mode;
482
520
  state.fanMode = SYMI_TO_ZH_FAN[state.fanMode] || state.fanMode;
483
521
  }
@@ -495,6 +533,10 @@ module.exports = function(RED) {
495
533
  }
496
534
 
497
535
  if (state && state.type === 'status') {
536
+ // 处理三合一子实体同步(空调、新风、地暖)
537
+ if (state.meshSubEntity) {
538
+ node.log(`[A->B] 三合一子实体同步: ${state.meshSubEntity}`);
539
+ }
498
540
  syncAtoB(mapping, state);
499
541
  }
500
542
  }
@@ -545,6 +587,12 @@ module.exports = function(RED) {
545
587
  mode: climate.mode,
546
588
  fanMode: climate.fanMode
547
589
  };
590
+
591
+ // 更新本地状态缓存,供A侧同步参考
592
+ const addrKey = `zhonghong_${climate.outdoorAddr}_${climate.indoorAddr}`;
593
+ node.stateCache.update(addrKey, state);
594
+
595
+ node.log(`[B->A] 内机${climate.outdoorAddr}-${climate.indoorAddr}: 开=${state.power}, 温=${state.targetTemp}, 模式=${state.mode}`);
548
596
  syncBtoA(mapping, state);
549
597
  }
550
598
  }
@@ -564,13 +612,13 @@ module.exports = function(RED) {
564
612
  } else if (mapping.protocolB === 'symi_climate') {
565
613
  state = parseSymiClimateFrame(frame, mapping.configB);
566
614
  if (state) {
567
- const cacheKey = `symi_${mapping.configB.address || 1}`;
568
- node._stateCache[cacheKey] = {
615
+ const addrKey = `symi_${mapping.configB.address || 1}`;
616
+ node.stateCache.update(addrKey, {
569
617
  power: state.power ? 1 : 0,
570
618
  temp: state.targetTemp,
571
619
  mode: state.mode,
572
620
  fan: state.fanMode
573
- };
621
+ });
574
622
  state.mode = SYMI_TO_ZH_MODE[state.mode] || state.mode;
575
623
  state.fanMode = SYMI_TO_ZH_FAN[state.fanMode] || state.fanMode;
576
624
  }
@@ -588,6 +636,10 @@ module.exports = function(RED) {
588
636
  }
589
637
 
590
638
  if (state && state.type === 'status') {
639
+ // 处理三合一子实体同步
640
+ if (state.meshSubEntity) {
641
+ node.log(`[B->A] 三合一子实体同步: ${state.meshSubEntity}`);
642
+ }
591
643
  syncBtoA(mapping, state);
592
644
  }
593
645
  }
@@ -756,6 +808,10 @@ module.exports = function(RED) {
756
808
 
757
809
  node.rs485ConfigA.deregister(node);
758
810
  node.rs485ConfigB.deregister(node);
811
+
812
+ if (node.syncUtils) {
813
+ node.syncUtils.destroy();
814
+ }
759
815
 
760
816
  done();
761
817
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.8.4",
3
+ "version": "1.8.5",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "nodes/symi-gateway.js",
6
6
  "scripts": {