iobroker.smartloadmanager 0.1.2 → 0.4.1
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 +24 -0
- package/admin/i18n/de.json +130 -0
- package/admin/i18n/en.json +130 -0
- package/admin/i18n/es.json +130 -0
- package/admin/i18n/fr.json +130 -0
- package/admin/i18n/it.json +130 -0
- package/admin/i18n/nl.json +130 -0
- package/admin/i18n/pl.json +130 -0
- package/admin/i18n/pt.json +130 -0
- package/admin/i18n/ru.json +130 -0
- package/admin/i18n/uk.json +130 -0
- package/admin/i18n/zh-cn.json +130 -0
- package/admin/jsonConfig.json +196 -5
- package/docs/de/README.md +292 -0
- package/docs/de/release-0.2.0-pruefung.md +74 -0
- package/docs/en/README.md +275 -0
- package/io-package.json +107 -25
- package/lib/forecast.js +123 -0
- package/main.js +399 -130
- package/package.json +18 -15
- package/admin/i18n/de/translations.json +0 -124
- package/admin/i18n/en/translations.json +0 -116
- package/admin/i18n/es/translations.json +0 -124
- package/admin/i18n/fr/translations.json +0 -124
- package/admin/i18n/it/translations.json +0 -124
- package/admin/i18n/nl/translations.json +0 -124
- package/admin/i18n/pl/translations.json +0 -124
- package/admin/i18n/pt/translations.json +0 -124
- package/admin/i18n/ru/translations.json +0 -124
- package/admin/i18n/uk/translations.json +0 -124
- package/admin/i18n/zh-cn/translations.json +0 -124
- package/lib/adapter-config.d.ts +0 -20
package/main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const utils = require('@iobroker/adapter-core');
|
|
4
|
+
const { evaluateForecast, minimumStateTimeSatisfied, parseForecast } = require('./lib/forecast');
|
|
4
5
|
|
|
5
6
|
class ZeroFeedIn extends utils.Adapter {
|
|
6
7
|
constructor(options) {
|
|
@@ -21,11 +22,17 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
21
22
|
// 🔮 Forecast (PV forecast integration)
|
|
22
23
|
this.forecastEnabled = false;
|
|
23
24
|
this.forecastJsonDatapoint = null;
|
|
25
|
+
this.forecastNotificationTimes = new Map();
|
|
26
|
+
this.forecastLogSignature = '';
|
|
24
27
|
this.forecast = {
|
|
25
28
|
power: 0,
|
|
26
29
|
surplus: 0,
|
|
27
30
|
minutes: 0,
|
|
28
31
|
lastUpdate: 0,
|
|
32
|
+
// eslint-disable-next-line jsdoc/check-tag-names
|
|
33
|
+
points: /** @type {Array<{timestamp: number, power: number, surplus: number}>} */ ([]),
|
|
34
|
+
// eslint-disable-next-line jsdoc/check-tag-names
|
|
35
|
+
unit: /** @type {'W' | 'kW' | null} */ (null),
|
|
29
36
|
};
|
|
30
37
|
|
|
31
38
|
this.on('ready', this.onReady.bind(this));
|
|
@@ -81,6 +88,20 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
81
88
|
// 🔮 Forecast config (PV forecast integration)
|
|
82
89
|
this.forecastEnabled = !!this.config.enableForecast;
|
|
83
90
|
this.forecastJsonDatapoint = this.config.forecastJsonDatapoint || null;
|
|
91
|
+
this.log.debug(
|
|
92
|
+
`[Forecast] ${this.forecastEnabled ? 'Enabled' : 'Disabled'}; source: ${this.forecastJsonDatapoint || 'not configured'}; decision notifications: ${this.config.notifyForecast ? 'enabled' : 'disabled'}.`,
|
|
93
|
+
);
|
|
94
|
+
if (this.forecastEnabled && !this.forecastJsonDatapoint) {
|
|
95
|
+
this.log.debug('[Forecast] No JSON data point configured; forecast control cannot run.');
|
|
96
|
+
}
|
|
97
|
+
if (this.config.notifyForecast && !this.forecastEnabled) {
|
|
98
|
+
this.log.debug(
|
|
99
|
+
'[Forecast] Decision notifications do not enable forecast control. Enable forecast control and select a JSON data point first.',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (this.forecastEnabled && this.forecastJsonDatapoint) {
|
|
103
|
+
this.forecast.unit = await this.detectForecastPowerUnit();
|
|
104
|
+
}
|
|
84
105
|
|
|
85
106
|
this.consumerList = Array.isArray(this.config.consumer)
|
|
86
107
|
? this.config.consumer.filter(
|
|
@@ -100,9 +121,22 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
100
121
|
v.enabled = state.val;
|
|
101
122
|
this.log.debug(`"${v.name}": Active status taken from object tree (${state.val})`);
|
|
102
123
|
}
|
|
124
|
+
const switchingState = v.datapoint
|
|
125
|
+
? await this.getForeignStateAsync(v.datapoint)
|
|
126
|
+
: v.numericDatapoint
|
|
127
|
+
? await this.getForeignStateAsync(v.numericDatapoint)
|
|
128
|
+
: null;
|
|
129
|
+
v.lastStateChange = switchingState?.lc || 0;
|
|
130
|
+
v.lastForecastBridgeAt = null;
|
|
103
131
|
}
|
|
104
132
|
|
|
105
133
|
this.log.info(`Loaded consumers: ${this.consumerList.map(v => v.name).join(', ')}`);
|
|
134
|
+
if (this.forecastEnabled) {
|
|
135
|
+
const forecastConsumers = this.consumerList.filter(v => v.enabled && v.useForecast);
|
|
136
|
+
this.log.debug(
|
|
137
|
+
`[Forecast] Enabled consumers using forecast: ${forecastConsumers.map(v => v.name).join(', ') || 'none; activate useForecast for the desired consumers'}.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
106
140
|
|
|
107
141
|
await this.checkAndCreateConsumerObjects();
|
|
108
142
|
await this.createConsumerStates();
|
|
@@ -126,6 +160,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
126
160
|
}
|
|
127
161
|
|
|
128
162
|
await this.checkConsumers();
|
|
163
|
+
this.ruleTimer = this.safeSetInterval(() => this.checkConsumers(), 15000);
|
|
129
164
|
} catch (error) {
|
|
130
165
|
this.log.error(`Error in onReady: ${error.message}`);
|
|
131
166
|
// additionally send alert
|
|
@@ -488,6 +523,19 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
488
523
|
// =====================================================================
|
|
489
524
|
// ========================= Forecast JSON parse =======================
|
|
490
525
|
// =====================================================================
|
|
526
|
+
async clearForecast(reason) {
|
|
527
|
+
this.forecastLogSignature = '';
|
|
528
|
+
this.forecast.points = [];
|
|
529
|
+
this.forecast.power = 0;
|
|
530
|
+
this.forecast.surplus = 0;
|
|
531
|
+
this.forecast.minutes = 0;
|
|
532
|
+
this.forecast.lastUpdate = 0;
|
|
533
|
+
for (const field of ['power', 'surplus', 'minutes', 'lastUpdate']) {
|
|
534
|
+
await this.setStateAsync(`${this.namespace}.forecast.${field}`, { val: 0, ack: true });
|
|
535
|
+
}
|
|
536
|
+
this.log.debug(`[Forecast] ${reason}; forecast control is unavailable until valid data arrives.`);
|
|
537
|
+
}
|
|
538
|
+
|
|
491
539
|
async processPvforecastJson() {
|
|
492
540
|
try {
|
|
493
541
|
if (!this.forecastEnabled || !this.forecastJsonDatapoint) {
|
|
@@ -496,46 +544,30 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
496
544
|
|
|
497
545
|
const state = await this.getForeignStateAsync(this.forecastJsonDatapoint);
|
|
498
546
|
if (!state || !state.val) {
|
|
547
|
+
await this.clearForecast('JSON data point is missing or empty');
|
|
499
548
|
return;
|
|
500
549
|
}
|
|
501
550
|
|
|
502
|
-
let data;
|
|
503
551
|
try {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (!Array.isArray(data) || data.length === 0) {
|
|
552
|
+
this.forecast.points = parseForecast(String(state.val), {
|
|
553
|
+
unit: this.forecast.unit,
|
|
554
|
+
baseload: this.config.baseload || 0,
|
|
555
|
+
});
|
|
556
|
+
} catch (error) {
|
|
557
|
+
await this.clearForecast(`Invalid pvforecast JSON: ${error.message}`);
|
|
511
558
|
return;
|
|
512
559
|
}
|
|
513
560
|
|
|
514
|
-
const
|
|
515
|
-
let best = null;
|
|
516
|
-
|
|
517
|
-
for (const entry of data) {
|
|
518
|
-
if (!entry || typeof entry.t !== 'number' || typeof entry.y !== 'number') {
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
if (entry.t < now) {
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
|
-
best = entry;
|
|
525
|
-
break;
|
|
526
|
-
}
|
|
527
|
-
|
|
561
|
+
const best = this.forecast.points[0];
|
|
528
562
|
if (!best) {
|
|
563
|
+
await this.clearForecast('JSON contains no valid future forecast points');
|
|
529
564
|
return;
|
|
530
565
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const power = Math.round(best.y * 1000);
|
|
534
|
-
const minutes = Math.max(0, Math.round((best.t - now) / 60000));
|
|
535
|
-
const surplus = power - (this.config.baseload || 0);
|
|
566
|
+
const power = best.power;
|
|
567
|
+
const minutes = Math.max(0, Math.round((best.timestamp - Date.now()) / 60000));
|
|
536
568
|
|
|
537
569
|
this.forecast.power = power;
|
|
538
|
-
this.forecast.surplus = surplus
|
|
570
|
+
this.forecast.surplus = best.surplus;
|
|
539
571
|
this.forecast.minutes = minutes;
|
|
540
572
|
this.forecast.lastUpdate = Date.now();
|
|
541
573
|
|
|
@@ -550,12 +582,39 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
550
582
|
ack: true,
|
|
551
583
|
});
|
|
552
584
|
|
|
553
|
-
this.
|
|
554
|
-
|
|
555
|
-
|
|
585
|
+
const signature = `${this.forecast.points.length}:${best.timestamp}:${power}:${best.surplus}`;
|
|
586
|
+
if (signature !== this.forecastLogSignature) {
|
|
587
|
+
this.log.debug(
|
|
588
|
+
`[Forecast] ${this.forecast.points.length} values parsed (${this.forecast.unit || 'auto'}) - next: ${power}W in ${minutes}min`,
|
|
589
|
+
);
|
|
590
|
+
this.forecastLogSignature = signature;
|
|
591
|
+
}
|
|
592
|
+
} catch (error) {
|
|
593
|
+
this.log.debug(`[Forecast] Error processing pvforecast JSON: ${error.message}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Detect the power unit used by pvforecast.
|
|
599
|
+
*
|
|
600
|
+
* @returns {Promise<'W'|'kW'|null>} Detected unit or null for value-based detection.
|
|
601
|
+
*/
|
|
602
|
+
async detectForecastPowerUnit() {
|
|
603
|
+
const configured = this.config.forecastPowerUnit;
|
|
604
|
+
if (configured === 'W' || configured === 'kW') {
|
|
605
|
+
return configured;
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
const summaryPowerId = (this.forecastJsonDatapoint || '').replace(/\.JSONData$/, '.power.now');
|
|
609
|
+
const object = await this.getForeignObjectAsync(summaryPowerId);
|
|
610
|
+
const unit = object?.common?.unit;
|
|
611
|
+
if (unit === 'W' || unit === 'kW') {
|
|
612
|
+
return unit;
|
|
613
|
+
}
|
|
556
614
|
} catch (error) {
|
|
557
|
-
this.log.
|
|
615
|
+
this.log.debug(`[Forecast] Unit detection failed: ${error.message}`);
|
|
558
616
|
}
|
|
617
|
+
return null;
|
|
559
618
|
}
|
|
560
619
|
|
|
561
620
|
// =====================================================================
|
|
@@ -569,32 +628,57 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
569
628
|
return this.forecast?.minutes || 0;
|
|
570
629
|
}
|
|
571
630
|
|
|
572
|
-
|
|
631
|
+
getForecastDecision(v, requiredPower) {
|
|
632
|
+
const maxStartMinutes =
|
|
633
|
+
this.config.forecastMaxStartMinutes === undefined ? 30 : Number(this.config.forecastMaxStartMinutes);
|
|
634
|
+
return evaluateForecast(this.forecast.points, {
|
|
635
|
+
requiredPower,
|
|
636
|
+
safetyFactor: (Number(this.config.forecastSafetyPercent) || 80) / 100,
|
|
637
|
+
minDurationMinutes: Number(v.forecastMinMinutes) || Number(this.config.forecastDefaultMinMinutes) || 30,
|
|
638
|
+
maxStartMinutes,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
isForecastAllowingSwitchOn(v, currentSurplus) {
|
|
573
643
|
if (!this.forecastEnabled || !v.useForecast) {
|
|
574
644
|
return false;
|
|
575
645
|
}
|
|
576
|
-
const
|
|
577
|
-
|
|
646
|
+
const requiredPower = Number(v.performance) || 0;
|
|
647
|
+
const minimumCurrentPercent =
|
|
648
|
+
this.config.forecastMinCurrentPercent === undefined ? 50 : Number(this.config.forecastMinCurrentPercent);
|
|
649
|
+
const minimumCurrent = requiredPower * (minimumCurrentPercent / 100);
|
|
650
|
+
if (currentSurplus < minimumCurrent) {
|
|
578
651
|
return false;
|
|
579
652
|
}
|
|
580
|
-
|
|
581
|
-
|
|
653
|
+
const minimumOffMinutes = v.minimumOffMinutes === undefined ? 5 : Number(v.minimumOffMinutes);
|
|
654
|
+
if (!minimumStateTimeSatisfied(v.lastStateChange, minimumOffMinutes)) {
|
|
655
|
+
return false;
|
|
582
656
|
}
|
|
583
|
-
return
|
|
657
|
+
return this.getForecastDecision(v, requiredPower).available;
|
|
584
658
|
}
|
|
585
659
|
|
|
586
|
-
isForecastDelayingSwitchOff(v,
|
|
660
|
+
isForecastDelayingSwitchOff(v, currentSurplus, requiredPower) {
|
|
587
661
|
if (!this.forecastEnabled || !v.useForecast) {
|
|
588
662
|
return false;
|
|
589
663
|
}
|
|
590
|
-
const
|
|
591
|
-
|
|
664
|
+
const bridgeMinutes =
|
|
665
|
+
this.config.forecastBridgeMinutes === undefined ? 5 : Number(this.config.forecastBridgeMinutes);
|
|
666
|
+
const maxGridImport = Math.max(
|
|
667
|
+
0,
|
|
668
|
+
this.config.forecastMaxGridImport === undefined ? 100 : Number(this.config.forecastMaxGridImport),
|
|
669
|
+
);
|
|
670
|
+
if (currentSurplus < -maxGridImport) {
|
|
592
671
|
return false;
|
|
593
672
|
}
|
|
594
|
-
|
|
595
|
-
|
|
673
|
+
const decision = this.getForecastDecision(
|
|
674
|
+
v,
|
|
675
|
+
Number(requiredPower) || Number(v.switchOnPoint) || Number(v.performance) || 0,
|
|
676
|
+
);
|
|
677
|
+
if (!decision.available || decision.startMinutes === null || decision.startMinutes > bridgeMinutes) {
|
|
678
|
+
return false;
|
|
596
679
|
}
|
|
597
|
-
|
|
680
|
+
v.lastForecastBridgeAt ||= Date.now();
|
|
681
|
+
return Date.now() - v.lastForecastBridgeAt <= bridgeMinutes * 60000;
|
|
598
682
|
}
|
|
599
683
|
|
|
600
684
|
async setForecastState(v, idx, active, reason, power, minutes) {
|
|
@@ -609,17 +693,29 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
609
693
|
ack: true,
|
|
610
694
|
});
|
|
611
695
|
} catch (err) {
|
|
612
|
-
this.log.
|
|
696
|
+
this.log.debug(`[Forecast] Failed to set forecastStatus: ${err.message}`);
|
|
613
697
|
}
|
|
614
698
|
}
|
|
615
699
|
|
|
616
700
|
async sendForecastNotification(type, message) {
|
|
617
701
|
try {
|
|
618
702
|
if (this.config.notifyForecast) {
|
|
703
|
+
const now = Date.now();
|
|
704
|
+
for (const [key, sentAt] of this.forecastNotificationTimes) {
|
|
705
|
+
if (now - sentAt >= 5 * 60000) {
|
|
706
|
+
this.forecastNotificationTimes.delete(key);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const key = `${type}:${message}`;
|
|
710
|
+
if (this.forecastNotificationTimes.has(key)) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
this.log.debug(`[Forecast] ${message}`);
|
|
619
714
|
await this.sendNotification('smartloadmanager', 'notify', message);
|
|
715
|
+
this.forecastNotificationTimes.set(key, now);
|
|
620
716
|
}
|
|
621
717
|
} catch (err) {
|
|
622
|
-
this.log.
|
|
718
|
+
this.log.debug(`[Forecast] Failed to send forecast notification: ${err.message}`);
|
|
623
719
|
}
|
|
624
720
|
}
|
|
625
721
|
|
|
@@ -636,7 +732,11 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
636
732
|
const onMinutes = this.parseTimeToMinutes(onTime);
|
|
637
733
|
const offMinutes = this.parseTimeToMinutes(offTime);
|
|
638
734
|
|
|
639
|
-
const within =
|
|
735
|
+
const within =
|
|
736
|
+
onMinutes === offMinutes ||
|
|
737
|
+
(onMinutes < offMinutes
|
|
738
|
+
? currentMinutes >= onMinutes && currentMinutes < offMinutes
|
|
739
|
+
: currentMinutes >= onMinutes || currentMinutes < offMinutes);
|
|
640
740
|
|
|
641
741
|
if (name) {
|
|
642
742
|
this.log.debug(
|
|
@@ -673,40 +773,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
673
773
|
this.log.debug(`Feed-in value updated: ${state.val} (ack=${state.ack})`);
|
|
674
774
|
await this.checkConsumers();
|
|
675
775
|
|
|
676
|
-
|
|
677
|
-
clearTimeout(this.percentTimer);
|
|
678
|
-
}
|
|
679
|
-
this.percentTimer = this.safeSetTimeout(
|
|
680
|
-
async () => {
|
|
681
|
-
for (const v of this.consumerList.filter(v => v.ruletype === 'percent')) {
|
|
682
|
-
await this.controlPercentConsumer(v);
|
|
683
|
-
}
|
|
684
|
-
this.percentTimer = null;
|
|
685
|
-
},
|
|
686
|
-
(this.config.delaySecondsProzent || this.DEFAULT_TIMEOUT / 1000) * 1000,
|
|
687
|
-
);
|
|
688
|
-
|
|
689
|
-
if (this.batteryTimer) {
|
|
690
|
-
clearTimeout(this.batteryTimer);
|
|
691
|
-
}
|
|
692
|
-
this.batteryTimer = this.safeSetTimeout(
|
|
693
|
-
async () => {
|
|
694
|
-
if (!this.feedInDatapoint) {
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
const feedInState = await this.getForeignStateAsync(this.feedInDatapoint);
|
|
698
|
-
let feedIn = Number(feedInState?.val) || 0;
|
|
699
|
-
if (this.config.feedinNegativ) {
|
|
700
|
-
feedIn = -feedIn;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
for (const v of this.consumerList.filter(v => v.ruletype === 'battery')) {
|
|
704
|
-
await this.controlBattery(v, feedIn);
|
|
705
|
-
}
|
|
706
|
-
this.batteryTimer = null;
|
|
707
|
-
},
|
|
708
|
-
(this.config.batteryDelaySeconds || this.DEFAULT_TIMEOUT / 1000) * 1000,
|
|
709
|
-
);
|
|
776
|
+
this.scheduleRegulation();
|
|
710
777
|
} else {
|
|
711
778
|
this.log.debug(`Feed-in value change ignored (ack=${state.ack}) - waiting for confirmed value`);
|
|
712
779
|
}
|
|
@@ -724,6 +791,16 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
724
791
|
const v = this.consumerList[index];
|
|
725
792
|
if (v) {
|
|
726
793
|
v.enabled = state.val;
|
|
794
|
+
if (!v.enabled && v.timer) {
|
|
795
|
+
clearTimeout(v.timer);
|
|
796
|
+
v.timer = null;
|
|
797
|
+
v.timerEnd = null;
|
|
798
|
+
v.pendingAction = null;
|
|
799
|
+
if (v.timerTick) {
|
|
800
|
+
clearInterval(v.timerTick);
|
|
801
|
+
v.timerTick = null;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
727
804
|
this.log.info(`Consumer "${v.name}" became ${state.val ? 'activated' : 'deactivated'}.`);
|
|
728
805
|
await this.setStateAsync(id, { val: state.val, ack: true });
|
|
729
806
|
}
|
|
@@ -737,10 +814,29 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
737
814
|
const mode = state.val;
|
|
738
815
|
const v = this.consumerList[index];
|
|
739
816
|
|
|
740
|
-
if (
|
|
741
|
-
|
|
742
|
-
}
|
|
743
|
-
|
|
817
|
+
if (!v || ![0, 1, 2].includes(mode)) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
await this.setStateAsync(id, { val: mode, ack: true });
|
|
821
|
+
if (v.timer) {
|
|
822
|
+
clearTimeout(v.timer);
|
|
823
|
+
v.timer = null;
|
|
824
|
+
v.timerEnd = null;
|
|
825
|
+
v.pendingAction = null;
|
|
826
|
+
}
|
|
827
|
+
if (mode === 2) {
|
|
828
|
+
await this.checkConsumers();
|
|
829
|
+
} else if (v.ruletype === 'percent') {
|
|
830
|
+
await this.setForeignStateAsync(v.datapoint, { val: mode === 1 ? 100 : 0, ack: false });
|
|
831
|
+
} else if (v.ruletype === 'battery') {
|
|
832
|
+
if (v.batterySetpoint) {
|
|
833
|
+
await this.setForeignStateAsync(v.batterySetpoint, {
|
|
834
|
+
val: mode === 1 ? v.performance || 1000 : 0,
|
|
835
|
+
ack: false,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
} else {
|
|
839
|
+
await this.switchConsumerWithDelay(v, mode === 1, 0);
|
|
744
840
|
}
|
|
745
841
|
}
|
|
746
842
|
|
|
@@ -833,13 +929,18 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
833
929
|
}
|
|
834
930
|
|
|
835
931
|
this.log.debug(`"${v.name}" Status: isOn=${isOn}`);
|
|
932
|
+
const minimumOffSatisfied = minimumStateTimeSatisfied(
|
|
933
|
+
v.lastStateChange,
|
|
934
|
+
v.minimumOffMinutes === undefined ? 5 : Number(v.minimumOffMinutes),
|
|
935
|
+
);
|
|
836
936
|
|
|
837
937
|
// Zusatz-Checks heating
|
|
838
938
|
if (v.ruletype === 'heating' && withinWindow && !isOn) {
|
|
839
939
|
const tempState = v.temperatureDatapoint
|
|
840
940
|
? await this.getForeignStateAsync(v.temperatureDatapoint)
|
|
841
941
|
: null;
|
|
842
|
-
const currentTemp =
|
|
942
|
+
const currentTemp =
|
|
943
|
+
tempState?.val !== undefined && tempState.val !== null ? Number(tempState.val) : null;
|
|
843
944
|
|
|
844
945
|
if (currentTemp !== null && v.maxTemperature !== undefined && currentTemp >= v.maxTemperature) {
|
|
845
946
|
this.log.warn(
|
|
@@ -889,7 +990,12 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
889
990
|
}
|
|
890
991
|
|
|
891
992
|
if (withinWindow) {
|
|
892
|
-
if (
|
|
993
|
+
if (
|
|
994
|
+
!gridUsage &&
|
|
995
|
+
Math.max(v.performance, Number(v.switchOnPoint) || 0) <= feedIn - baseload &&
|
|
996
|
+
!isOn &&
|
|
997
|
+
minimumOffSatisfied
|
|
998
|
+
) {
|
|
893
999
|
this.log.debug(
|
|
894
1000
|
`Switch-on possible: surplus ${
|
|
895
1001
|
feedIn - baseload
|
|
@@ -899,7 +1005,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
899
1005
|
feedIn -= v.performance;
|
|
900
1006
|
this.log.debug(`Remaining surplus after switch-on: ${feedIn - baseload} W`);
|
|
901
1007
|
await this.setForecastState(v, idx, false, '', null, null);
|
|
902
|
-
} else if (!gridUsage && !isOn && this.isForecastAllowingSwitchOn(v)) {
|
|
1008
|
+
} else if (!gridUsage && !isOn && this.isForecastAllowingSwitchOn(v, feedIn - baseload)) {
|
|
903
1009
|
const forecastPower = this.getForecastPower(v);
|
|
904
1010
|
const minutes = this.getForecastMinutes(v);
|
|
905
1011
|
this.log.debug(
|
|
@@ -916,7 +1022,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
916
1022
|
this.log.debug(`Not switched on: grid usage active.`);
|
|
917
1023
|
} else if (!isOn) {
|
|
918
1024
|
this.log.debug(
|
|
919
|
-
|
|
1025
|
+
minimumOffSatisfied
|
|
1026
|
+
? `Not switched on: surplus too low (${feedIn - baseload} W < ${v.performance} W).`
|
|
1027
|
+
: `Not switched on: minimum off time (${v.minimumOffMinutes === undefined ? 5 : Number(v.minimumOffMinutes)} min) is active.`,
|
|
920
1028
|
);
|
|
921
1029
|
} else {
|
|
922
1030
|
this.log.debug(`"${v.name}" is already switched on – no action needed.`);
|
|
@@ -1025,8 +1133,12 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1025
1133
|
const tempState = v.temperatureDatapoint
|
|
1026
1134
|
? await this.getForeignStateAsync(v.temperatureDatapoint)
|
|
1027
1135
|
: null;
|
|
1028
|
-
const currentTemp =
|
|
1029
|
-
|
|
1136
|
+
const currentTemp =
|
|
1137
|
+
tempState?.val !== undefined && tempState.val !== null ? Number(tempState.val) : null;
|
|
1138
|
+
if (
|
|
1139
|
+
(v.temperatureDatapoint && (currentTemp === null || !Number.isFinite(currentTemp))) ||
|
|
1140
|
+
(currentTemp !== null && currentTemp >= v.maxTemperature)
|
|
1141
|
+
) {
|
|
1030
1142
|
this.log.warn(
|
|
1031
1143
|
`"${v.name}" switched off due to maxTemperature reached: ${currentTemp}°C >= ${v.maxTemperature}°C`,
|
|
1032
1144
|
);
|
|
@@ -1037,7 +1149,14 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1037
1149
|
|
|
1038
1150
|
// Timer-Abbruch bei wegfallender Bedingung (Ausschalten)
|
|
1039
1151
|
const surplus = feedIn - baseload;
|
|
1040
|
-
if (
|
|
1152
|
+
if (
|
|
1153
|
+
v.timer &&
|
|
1154
|
+
v.timerEnd &&
|
|
1155
|
+
v.pendingAction === false &&
|
|
1156
|
+
withinWindow &&
|
|
1157
|
+
!gridUsage &&
|
|
1158
|
+
surplus >= v.switchOffPoint
|
|
1159
|
+
) {
|
|
1041
1160
|
clearTimeout(v.timer);
|
|
1042
1161
|
v.timer = null;
|
|
1043
1162
|
v.timerEnd = null;
|
|
@@ -1053,8 +1172,27 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1053
1172
|
// - Netzbezug aktiv ODER
|
|
1054
1173
|
// - Überschuss < switchOffPoint
|
|
1055
1174
|
const shouldSwitchOff = !withinWindow || gridUsage || surplus < v.switchOffPoint;
|
|
1175
|
+
if (!shouldSwitchOff) {
|
|
1176
|
+
v.lastForecastBridgeAt = null;
|
|
1177
|
+
}
|
|
1056
1178
|
|
|
1057
1179
|
if (!v.alwaysOffAtTime && shouldSwitchOff) {
|
|
1180
|
+
const maxGridImport = Math.max(
|
|
1181
|
+
0,
|
|
1182
|
+
this.config.forecastMaxGridImport === undefined
|
|
1183
|
+
? 100
|
|
1184
|
+
: Number(this.config.forecastMaxGridImport),
|
|
1185
|
+
);
|
|
1186
|
+
const minimumOnMinutes = v.minimumOnMinutes === undefined ? 10 : Number(v.minimumOnMinutes);
|
|
1187
|
+
const hardGridImport = surplus < -maxGridImport;
|
|
1188
|
+
if (
|
|
1189
|
+
withinWindow &&
|
|
1190
|
+
!hardGridImport &&
|
|
1191
|
+
!minimumStateTimeSatisfied(v.lastStateChange, minimumOnMinutes)
|
|
1192
|
+
) {
|
|
1193
|
+
this.log.debug(`"${v.name}" remains on: minimum run time (${minimumOnMinutes} min) is active.`);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1058
1196
|
// 🔮 Forecast delay switch-off (PV forecast integration)
|
|
1059
1197
|
if (this.isForecastDelayingSwitchOff(v, surplus)) {
|
|
1060
1198
|
const forecastPower = this.getForecastPower(v);
|
|
@@ -1084,14 +1222,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1084
1222
|
}
|
|
1085
1223
|
|
|
1086
1224
|
// --- Batterie unverändert ---
|
|
1087
|
-
|
|
1088
|
-
for (const v of batteryConsumers) {
|
|
1089
|
-
if (!v.enabled) {
|
|
1090
|
-
this.log.debug(`"${v.name}" is deactivated – battery control skipped.`);
|
|
1091
|
-
continue;
|
|
1092
|
-
}
|
|
1093
|
-
await this.controlBattery(v, feedIn);
|
|
1094
|
-
}
|
|
1225
|
+
this.scheduleRegulation();
|
|
1095
1226
|
|
|
1096
1227
|
const runningTimers = this.consumerList
|
|
1097
1228
|
.filter(c => c.timer && c.timerEnd)
|
|
@@ -1119,8 +1250,55 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1119
1250
|
// =====================================================================
|
|
1120
1251
|
// ========================= controlPercentConsumer ====================
|
|
1121
1252
|
// =====================================================================
|
|
1253
|
+
scheduleRegulation() {
|
|
1254
|
+
if (!this.percentTimer && this.consumerList.some(v => v.ruletype === 'percent')) {
|
|
1255
|
+
this.percentTimer = this.safeSetTimeout(
|
|
1256
|
+
async () => {
|
|
1257
|
+
for (const v of this.consumerList.filter(v => v.ruletype === 'percent')) {
|
|
1258
|
+
await this.controlPercentConsumer(v);
|
|
1259
|
+
}
|
|
1260
|
+
this.percentTimer = null;
|
|
1261
|
+
},
|
|
1262
|
+
Math.max(0, Number(this.config.delaySecondsProzent) || 0) * 1000,
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
if (!this.batteryTimer && this.consumerList.some(v => v.ruletype === 'battery')) {
|
|
1267
|
+
this.batteryTimer = this.safeSetTimeout(
|
|
1268
|
+
async () => {
|
|
1269
|
+
if (!this.feedInDatapoint) {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const feedInState = await this.getForeignStateAsync(this.feedInDatapoint);
|
|
1273
|
+
let feedIn = Number(feedInState?.val) || 0;
|
|
1274
|
+
if (this.config.feedinNegativ) {
|
|
1275
|
+
feedIn = -feedIn;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
for (const v of this.consumerList.filter(v => v.ruletype === 'battery')) {
|
|
1279
|
+
await this.controlBattery(v, feedIn);
|
|
1280
|
+
}
|
|
1281
|
+
this.batteryTimer = null;
|
|
1282
|
+
},
|
|
1283
|
+
Math.max(0, Number(this.config.batteryDelaySeconds) || 0) * 1000,
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
async isAutomaticConsumer(v) {
|
|
1289
|
+
if (!v.enabled) {
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
const idx = this.consumerList.indexOf(v);
|
|
1293
|
+
const mode = await this.getStateAsync(`${this.namespace}.consumer.${idx}_${this.name2id(v.name)}.controlMode`);
|
|
1294
|
+
return mode?.val === 2;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1122
1297
|
async controlPercentConsumer(v) {
|
|
1123
1298
|
try {
|
|
1299
|
+
if (!(await this.isAutomaticConsumer(v))) {
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1124
1302
|
const withinWindow = this.timeWithinWindow(v.switchOnTime, v.switchOffTime);
|
|
1125
1303
|
if (!withinWindow) {
|
|
1126
1304
|
this.log.debug(`[Percent] ${v.name} outside time window, set to 0%`);
|
|
@@ -1137,25 +1315,36 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1137
1315
|
|
|
1138
1316
|
const feedInValue = Number(feedInState?.val) || 0;
|
|
1139
1317
|
|
|
1140
|
-
let surplus = this.config.feedinNegativ
|
|
1141
|
-
? feedInValue < 0
|
|
1142
|
-
? -feedInValue
|
|
1143
|
-
: 0
|
|
1144
|
-
: feedInValue > 0
|
|
1145
|
-
? feedInValue
|
|
1146
|
-
: 0;
|
|
1318
|
+
let surplus = this.config.feedinNegativ ? -feedInValue : feedInValue;
|
|
1147
1319
|
|
|
1148
1320
|
surplus -= this.config.baseload || 0;
|
|
1321
|
+
const currentSurplus = surplus;
|
|
1149
1322
|
if (surplus < 0) {
|
|
1150
1323
|
surplus = 0;
|
|
1151
1324
|
}
|
|
1152
1325
|
|
|
1153
1326
|
const maxPerformance = v.maxPerformance || v.performance || 1000;
|
|
1154
|
-
let newPercent = Math.round((surplus / maxPerformance) * 100);
|
|
1155
|
-
newPercent = Math.min(100, Math.max(newPercent, v.minPercentStart || 0));
|
|
1156
|
-
|
|
1157
1327
|
const state = await this.getForeignStateAsync(v.datapoint);
|
|
1158
1328
|
const currentPercent = Number(state?.val) || 0;
|
|
1329
|
+
const currentPower = (currentPercent / 100) * maxPerformance;
|
|
1330
|
+
let newPercent = Math.min(
|
|
1331
|
+
100,
|
|
1332
|
+
Math.max(0, Math.round(((currentPower + currentSurplus) / maxPerformance) * 100)),
|
|
1333
|
+
);
|
|
1334
|
+
if (newPercent < (Number(v.minPercentStart) || 0)) {
|
|
1335
|
+
newPercent = 0;
|
|
1336
|
+
}
|
|
1337
|
+
if (newPercent >= currentPercent) {
|
|
1338
|
+
v.lastForecastBridgeAt = null;
|
|
1339
|
+
}
|
|
1340
|
+
if (newPercent < currentPercent && this.isForecastDelayingSwitchOff(v, currentSurplus, currentPower)) {
|
|
1341
|
+
newPercent = currentPercent;
|
|
1342
|
+
this.log.debug(`[Forecast] ${v.name}: holding ${currentPercent}% during a short forecast-backed dip`);
|
|
1343
|
+
await this.sendForecastNotification(
|
|
1344
|
+
'percent',
|
|
1345
|
+
`${v.name}: holding ${currentPercent}% during a forecast-backed dip.`,
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1159
1348
|
|
|
1160
1349
|
if (newPercent !== currentPercent) {
|
|
1161
1350
|
await this.setForeignStateAsync(v.datapoint, newPercent);
|
|
@@ -1186,6 +1375,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1186
1375
|
// =====================================================================
|
|
1187
1376
|
async controlBattery(v, feedIn) {
|
|
1188
1377
|
try {
|
|
1378
|
+
if (!(await this.isAutomaticConsumer(v))) {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1189
1381
|
this.log.debug(`[Battery] Checking for ${v.name}`);
|
|
1190
1382
|
|
|
1191
1383
|
const withinWindow = this.timeWithinWindow(v.switchOnTime, v.switchOffTime);
|
|
@@ -1194,13 +1386,13 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1194
1386
|
if (v.batterySetpoint) {
|
|
1195
1387
|
await this.setForeignStateAsync(v.batterySetpoint, {
|
|
1196
1388
|
val: 0,
|
|
1197
|
-
ack:
|
|
1389
|
+
ack: false,
|
|
1198
1390
|
});
|
|
1199
1391
|
}
|
|
1200
1392
|
if (this.batteryControlModeDatapoint) {
|
|
1201
1393
|
await this.setForeignStateAsync(this.batteryControlModeDatapoint, {
|
|
1202
1394
|
val: 0,
|
|
1203
|
-
ack:
|
|
1395
|
+
ack: false,
|
|
1204
1396
|
});
|
|
1205
1397
|
}
|
|
1206
1398
|
return;
|
|
@@ -1211,7 +1403,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1211
1403
|
if (this.batteryControlModeDatapoint) {
|
|
1212
1404
|
await this.setForeignStateAsync(this.batteryControlModeDatapoint, {
|
|
1213
1405
|
val: 0,
|
|
1214
|
-
ack:
|
|
1406
|
+
ack: false,
|
|
1215
1407
|
});
|
|
1216
1408
|
}
|
|
1217
1409
|
return;
|
|
@@ -1219,34 +1411,64 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1219
1411
|
|
|
1220
1412
|
let soc = null;
|
|
1221
1413
|
let targetSoc = null;
|
|
1414
|
+
let invalidSoc = false;
|
|
1222
1415
|
if (v.batterySOC && v.batteryTargetSOC) {
|
|
1223
1416
|
const socState = await this.getForeignStateAsync(v.batterySOC);
|
|
1224
1417
|
const targetSocState = await this.getForeignStateAsync(v.batteryTargetSOC);
|
|
1225
1418
|
soc = Number(socState?.val);
|
|
1226
1419
|
targetSoc = Number(targetSocState?.val);
|
|
1227
|
-
if (
|
|
1420
|
+
if (
|
|
1421
|
+
!socState ||
|
|
1422
|
+
socState.val === null ||
|
|
1423
|
+
!targetSocState ||
|
|
1424
|
+
targetSocState.val === null ||
|
|
1425
|
+
!Number.isFinite(soc) ||
|
|
1426
|
+
!Number.isFinite(targetSoc)
|
|
1427
|
+
) {
|
|
1228
1428
|
this.log.warn(`[Battery] ${v.name}: invalid SOC or targetSOC`);
|
|
1229
1429
|
soc = null;
|
|
1230
1430
|
targetSoc = null;
|
|
1431
|
+
invalidSoc = true;
|
|
1231
1432
|
}
|
|
1232
1433
|
}
|
|
1233
1434
|
|
|
1234
|
-
|
|
1435
|
+
// Callers provide feed-in normalized to positive export.
|
|
1436
|
+
const surplus = feedIn - (this.config.baseload || 0);
|
|
1437
|
+
const oldPower = Number((await this.getForeignStateAsync(v.batterySetpoint))?.val) || 0;
|
|
1235
1438
|
let powerToSet = 0;
|
|
1236
1439
|
let modeToSet = 0;
|
|
1237
1440
|
|
|
1238
|
-
|
|
1441
|
+
const availablePower = Math.max(0, oldPower + surplus);
|
|
1442
|
+
if (availablePower <= 0 || invalidSoc) {
|
|
1239
1443
|
powerToSet = 0;
|
|
1240
1444
|
modeToSet = 0;
|
|
1241
1445
|
} else if (soc !== null && targetSoc !== null && soc >= targetSoc) {
|
|
1242
1446
|
powerToSet = 0;
|
|
1243
1447
|
modeToSet = 1;
|
|
1244
1448
|
} else {
|
|
1245
|
-
powerToSet = Math.min(
|
|
1449
|
+
powerToSet = Math.min(availablePower, v.performance || 1000);
|
|
1246
1450
|
modeToSet = 2;
|
|
1247
1451
|
}
|
|
1248
1452
|
|
|
1249
|
-
|
|
1453
|
+
if (powerToSet >= oldPower) {
|
|
1454
|
+
v.lastForecastBridgeAt = null;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
if (
|
|
1458
|
+
powerToSet < oldPower &&
|
|
1459
|
+
!invalidSoc &&
|
|
1460
|
+
(soc === null || targetSoc === null || soc < targetSoc) &&
|
|
1461
|
+
this.isForecastDelayingSwitchOff(v, surplus, oldPower)
|
|
1462
|
+
) {
|
|
1463
|
+
powerToSet = Math.min(oldPower, v.performance || oldPower);
|
|
1464
|
+
modeToSet = 2;
|
|
1465
|
+
this.log.debug(`[Forecast] ${v.name}: holding ${powerToSet}W charging power during a short dip`);
|
|
1466
|
+
await this.sendForecastNotification(
|
|
1467
|
+
'battery',
|
|
1468
|
+
`${v.name}: holding ${powerToSet}W charging power during a forecast-backed dip.`,
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1250
1472
|
const oldMode = this.batteryControlModeDatapoint
|
|
1251
1473
|
? Number((await this.getForeignStateAsync(this.batteryControlModeDatapoint))?.val) || 0
|
|
1252
1474
|
: -1;
|
|
@@ -1254,7 +1476,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1254
1476
|
if (powerToSet !== oldPower) {
|
|
1255
1477
|
await this.setForeignStateAsync(v.batterySetpoint, {
|
|
1256
1478
|
val: powerToSet,
|
|
1257
|
-
ack:
|
|
1479
|
+
ack: false,
|
|
1258
1480
|
});
|
|
1259
1481
|
this.log.info(`Battery ${v.name}: charging power set to ${powerToSet}W`);
|
|
1260
1482
|
if (this.config.notifyBattery) {
|
|
@@ -1271,7 +1493,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1271
1493
|
if (this.batteryControlModeDatapoint && modeToSet !== oldMode) {
|
|
1272
1494
|
await this.setForeignStateAsync(this.batteryControlModeDatapoint, {
|
|
1273
1495
|
val: modeToSet,
|
|
1274
|
-
ack:
|
|
1496
|
+
ack: false,
|
|
1275
1497
|
});
|
|
1276
1498
|
this.log.debug(`[Battery] ${v.name} mode changed from ${oldMode} to ${modeToSet}`);
|
|
1277
1499
|
if (this.config.notifyBattery) {
|
|
@@ -1307,11 +1529,6 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1307
1529
|
val: targetState,
|
|
1308
1530
|
ack: false,
|
|
1309
1531
|
});
|
|
1310
|
-
// Danach sofort bestätigen (falls keine Rückmeldung vom Gerät kommt)
|
|
1311
|
-
await this.setForeignStateAsync(consumer.datapoint, {
|
|
1312
|
-
val: targetState,
|
|
1313
|
-
ack: true,
|
|
1314
|
-
});
|
|
1315
1532
|
}
|
|
1316
1533
|
|
|
1317
1534
|
if (consumer.numericDatapoint) {
|
|
@@ -1320,13 +1537,12 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1320
1537
|
val: numVal,
|
|
1321
1538
|
ack: false,
|
|
1322
1539
|
});
|
|
1323
|
-
await this.setForeignStateAsync(consumer.numericDatapoint, {
|
|
1324
|
-
val: numVal,
|
|
1325
|
-
ack: true,
|
|
1326
|
-
});
|
|
1327
1540
|
this.log.debug(`Numeric datapoint for "${consumer.name}" set to ${numVal}`);
|
|
1328
1541
|
}
|
|
1329
1542
|
|
|
1543
|
+
consumer.lastStateChange = Date.now();
|
|
1544
|
+
consumer.lastForecastBridgeAt = null;
|
|
1545
|
+
|
|
1330
1546
|
const msg = `${consumer.name} wurde ${targetState ? 'eingeschaltet' : 'ausgeschaltet'}`;
|
|
1331
1547
|
|
|
1332
1548
|
if (consumer.ruletype === 'heating') {
|
|
@@ -1398,12 +1614,57 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1398
1614
|
};
|
|
1399
1615
|
|
|
1400
1616
|
const doSwitch = async () => {
|
|
1617
|
+
if (!consumer.enabled) {
|
|
1618
|
+
consumer.timer = null;
|
|
1619
|
+
consumer.timerEnd = null;
|
|
1620
|
+
consumer.pendingAction = null;
|
|
1621
|
+
await stopTimerTicker();
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
if (delay > 0 && turnOn) {
|
|
1625
|
+
const mode = await this.getStateAsync(`${this.namespace}.${channelId}.controlMode`);
|
|
1626
|
+
if (mode?.val === 2) {
|
|
1627
|
+
const meter = this.feedInDatapoint ? await this.getForeignStateAsync(this.feedInDatapoint) : null;
|
|
1628
|
+
const rawPower = Number(meter?.val);
|
|
1629
|
+
const surplus = (this.config.feedinNegativ ? -rawPower : rawPower) - (this.config.baseload || 0);
|
|
1630
|
+
const withinWindow = this.timeWithinWindow(consumer.switchOnTime, consumer.switchOffTime);
|
|
1631
|
+
const required = Math.max(Number(consumer.performance) || 0, Number(consumer.switchOnPoint) || 0);
|
|
1632
|
+
if (
|
|
1633
|
+
!meter ||
|
|
1634
|
+
meter.val === null ||
|
|
1635
|
+
!Number.isFinite(surplus) ||
|
|
1636
|
+
!withinWindow ||
|
|
1637
|
+
(surplus < required && !this.isForecastAllowingSwitchOn(consumer, surplus))
|
|
1638
|
+
) {
|
|
1639
|
+
consumer.timer = null;
|
|
1640
|
+
consumer.timerEnd = null;
|
|
1641
|
+
consumer.pendingAction = null;
|
|
1642
|
+
await stopTimerTicker();
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
} else if (mode?.val !== 1) {
|
|
1646
|
+
consumer.timer = null;
|
|
1647
|
+
consumer.timerEnd = null;
|
|
1648
|
+
consumer.pendingAction = null;
|
|
1649
|
+
await stopTimerTicker();
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1401
1653
|
// nur bei heating: zusätzliche Sicherheitschecks
|
|
1402
1654
|
if (consumer.ruletype === 'heating' && turnOn) {
|
|
1403
1655
|
const tempState = consumer.temperatureDatapoint
|
|
1404
1656
|
? await this.getForeignStateAsync(consumer.temperatureDatapoint)
|
|
1405
1657
|
: null;
|
|
1406
|
-
const currentTemp =
|
|
1658
|
+
const currentTemp =
|
|
1659
|
+
tempState?.val !== undefined && tempState.val !== null ? Number(tempState.val) : null;
|
|
1660
|
+
|
|
1661
|
+
if (consumer.temperatureDatapoint && (currentTemp === null || !Number.isFinite(currentTemp))) {
|
|
1662
|
+
consumer.timer = null;
|
|
1663
|
+
consumer.timerEnd = null;
|
|
1664
|
+
consumer.pendingAction = null;
|
|
1665
|
+
await stopTimerTicker();
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1407
1668
|
|
|
1408
1669
|
if (
|
|
1409
1670
|
currentTemp !== null &&
|
|
@@ -1674,6 +1935,10 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1674
1935
|
// =====================================================================
|
|
1675
1936
|
async onUnload(callback) {
|
|
1676
1937
|
try {
|
|
1938
|
+
if (this.ruleTimer) {
|
|
1939
|
+
clearInterval(this.ruleTimer);
|
|
1940
|
+
this.ruleTimer = null;
|
|
1941
|
+
}
|
|
1677
1942
|
if (this.percentTimer) {
|
|
1678
1943
|
clearTimeout(this.percentTimer);
|
|
1679
1944
|
}
|
|
@@ -1681,6 +1946,10 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1681
1946
|
clearTimeout(this.batteryTimer);
|
|
1682
1947
|
}
|
|
1683
1948
|
for (const v of this.consumerList) {
|
|
1949
|
+
if (v.timerTick) {
|
|
1950
|
+
clearInterval(v.timerTick);
|
|
1951
|
+
v.timerTick = null;
|
|
1952
|
+
}
|
|
1684
1953
|
if (v.timer) {
|
|
1685
1954
|
clearTimeout(v.timer);
|
|
1686
1955
|
v.timer = null;
|