node-red-contrib-symi-mesh 1.8.3 → 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,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.3",
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": {