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.
- package/README.md +252 -1157
- package/lib/sync-utils.js +398 -0
- package/nodes/rs485-debug.html +4 -3
- package/nodes/symi-485-bridge.html +4 -3
- package/nodes/symi-485-bridge.js +32 -0
- package/nodes/symi-485-config.html +7 -0
- package/nodes/symi-cloud-sync.html +7 -0
- package/nodes/symi-device.html +7 -0
- package/nodes/symi-gateway.html +7 -0
- package/nodes/symi-ha-sync.html +5 -47
- package/nodes/symi-ha-sync.js +360 -179
- package/nodes/symi-knx-bridge.html +8 -0
- package/nodes/symi-knx-bridge.js +27 -24
- package/nodes/symi-mqtt-brand.html +7 -0
- package/nodes/symi-mqtt-sync.html +2 -2
- package/nodes/symi-mqtt-sync.js +164 -34
- package/nodes/symi-mqtt.html +7 -0
- package/nodes/symi-rs485-sync.html +3 -2
- package/nodes/symi-rs485-sync.js +78 -22
- package/package.json +1 -1
package/nodes/symi-rs485-sync.js
CHANGED
|
@@ -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.
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// 初始化通用同步工具类
|
|
28
|
+
node.syncUtils = new SyncUtils({
|
|
29
|
+
defaultTimeout: DEFAULT_TIMEOUT
|
|
30
|
+
});
|
|
29
31
|
|
|
30
32
|
// 状态缓存
|
|
31
|
-
node.
|
|
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.
|
|
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
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
|
353
|
-
const now = Date.now();
|
|
376
|
+
const loopKey = JSON.stringify({ a: mapping.configA, b: mapping.configB });
|
|
354
377
|
|
|
355
|
-
|
|
356
|
-
|
|
378
|
+
// 使用通用工具检查防环路
|
|
379
|
+
if (node.shouldPreventSync('b-to-a', loopKey)) {
|
|
380
|
+
node.debug(`[B->A] 防环路跳过: ${loopKey}`);
|
|
357
381
|
return;
|
|
358
382
|
}
|
|
359
383
|
|
|
360
|
-
|
|
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
|
|
475
|
-
node.
|
|
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
|
|
568
|
-
node.
|
|
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
|
});
|