2 Commits
Author SHA1 Message Date
dh 8d83b083bf feat(batterie): Nachladeleistung begrenzen
Tests / test (push) Successful in 48s
2026-10-01 15:56:23 +00:00
dh 6cd7bf19f5 test(ladestation): Regeluebergang abwarten 2026-10-01 15:56:14 +00:00
10 changed files with 193 additions and 16 deletions
+11 -4
View File
@@ -82,19 +82,26 @@ Messwert, ist er veraltet oder liegt der Ladezustand ausserhalb von 0 bis
ReserveLadezustand entspricht der bisherigen Peakshaving-Reserve.
MindestLadezustand schuetzt vor weiterer Entladung.
LadezustandHysterese ersetzt die bisher fest im Code hinterlegte
2-Prozent-Hysterese.
2-Prozent-Hysterese. NachladenMitMaximalleistung verwendet standardmaessig die
dynamische maximale Ladeleistung. Wird der Schalter deaktiviert, begrenzt
MaximaleNachladeleistung das schutzbedingte Nachladen in W. Der wirksame Wert
wird nie hoeher als die dynamische Ladegrenze der Batterie.
Die bisherigen betriebsartabhaengigen Angebote bleiben erhalten:
- Im PV-Betrieb steht oberhalb der Reserve der durchgaengige Bereich von der
maximalen Entlade- bis zur maximalen Ladeleistung zur Verfuegung.
- Unterhalb der Reserve wird die maximale Ladeleistung als fester Wert angeboten.
- Unterhalb der Reserve wird die wirksame Nachladeleistung als fester Wert
angeboten.
- Bei vollem Speicher wird der durchgaengige Bereich von der maximalen
Entladeleistung bis 0 W angeboten.
- Im Peakbetrieb wird oberhalb der Reserve der aus Netz- und aktueller
Batterieleistung berechnete Entladewert angeboten.
- Innerhalb der Reserve-Hysterese und unterhalb des Mindestladezustands werden
Laden und Entladen wie bisher eingeschraenkt.
Laden und Entladen wie bisher eingeschraenkt; die positive Grenze ist dabei
hoechstens die konfigurierte Nachladeleistung.
- Oberhalb der Reserve ohne aktive Hysterese bleibt der normale Ladebereich
unveraendert und wird nicht durch die Nachladegrenze reduziert.
## Manager und Zeitverhalten
@@ -133,7 +140,7 @@ verwirft die aktuelle Vorgabe und schreibt eine Leistung von 0.
1. Batterietyp und Steuerung auswaehlen.
2. Alle fuenf Messwertvariablen zuordnen.
3. Die automatisch eingeblendeten Registervariablen zuordnen.
4. Reserve, Mindestladezustand und Hysterese pruefen.
4. Reserve, Mindestladezustand, Hysterese und Nachladeleistung pruefen.
5. Diagnosevariablen und Logging fuer die Erstinbetriebnahme aktivieren.
6. Die Batterie im Manager zuordnen.
7. Aktiv einschalten.
+14
View File
@@ -137,6 +137,20 @@
"minimum": 0,
"maximum": 100,
"digits": 1
},
{
"type": "CheckBox",
"name": "NachladenMitMaximalleistung",
"caption": "Beim Nachladen maximale Ladeleistung verwenden",
"onChange": "IPS_RequestAction($id, 'FormNachladenMitMaximalleistung', $NachladenMitMaximalleistung);"
},
{
"type": "NumberSpinner",
"name": "MaximaleNachladeleistung",
"caption": "Maximale Nachladeleistung",
"suffix": " W",
"minimum": 1,
"visible": false
}
]
},
+47 -2
View File
@@ -86,6 +86,8 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterPropertyFloat('ReserveLadezustand', 20.0);
$this->RegisterPropertyFloat('MindestLadezustand', 10.0);
$this->RegisterPropertyFloat('LadezustandHysterese', 2.0);
$this->RegisterPropertyBoolean('NachladenMitMaximalleistung', true);
$this->RegisterPropertyInteger('MaximaleNachladeleistung', 0);
$this->RegisterPropertyInteger('MesswertMaxAlter', 60);
$this->RegisterPropertyInteger('Aenderungssperre', 4);
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
@@ -185,6 +187,11 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
$formular['elements'],
$this->ReadPropertyInteger('Batterietyp')
);
$this->setzeFormfeldSichtbar(
$formular['elements'],
'MaximaleNachladeleistung',
!$this->ReadPropertyBoolean('NachladenMitMaximalleistung')
);
return json_encode($formular, JSON_THROW_ON_ERROR);
}
@@ -251,6 +258,14 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
$this->aktualisiereFormRegisterfelder((int) $wert);
return;
case 'FormNachladenMitMaximalleistung':
$this->UpdateFormField(
'MaximaleNachladeleistung',
'visible',
!(bool) $wert
);
return;
case 'ManagerdatenEmpfangen':
if (!is_string($wert)) {
throw new InvalidArgumentException(
@@ -382,7 +397,10 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
$messwerte['Netzleistung'],
$messwerte['Istleistung'],
$messwerte['MaxLaden'],
$messwerte['MaxEntladen']
$messwerte['MaxEntladen'],
$this->ReadPropertyBoolean('NachladenMitMaximalleistung')
? null
: (float) $this->ReadPropertyInteger('MaximaleNachladeleistung')
)
: [0];
@@ -776,6 +794,14 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
}
}
if (!$this->ReadPropertyBoolean('NachladenMitMaximalleistung')
&& $this->ReadPropertyInteger('MaximaleNachladeleistung') <= 0
) {
throw new InvalidArgumentException(
'MaximaleNachladeleistung muss groesser als 0 W sein.'
);
}
BatterieRegler::hystereseAktiv(
50.0,
$this->ReadPropertyFloat('ReserveLadezustand'),
@@ -793,7 +819,10 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
0.0,
0.0,
1.0,
1.0
1.0,
$this->ReadPropertyBoolean('NachladenMitMaximalleistung')
? null
: (float) $this->ReadPropertyInteger('MaximaleNachladeleistung')
);
foreach (self::MESSWERT_PROPERTIES as $property) {
@@ -1096,6 +1125,22 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
$this->setzeZustand('Stoertext', $text);
}
/** @param array<int, mixed> $elemente */
private function setzeFormfeldSichtbar(
array &$elemente,
string $name,
bool $sichtbar
): void {
foreach ($elemente as &$element) {
if (($element['name'] ?? '') === $name) {
$element['visible'] = $sichtbar;
}
if (isset($element['items']) && is_array($element['items'])) {
$this->setzeFormfeldSichtbar($element['items'], $name, $sichtbar);
}
}
}
/** @param array<int, mixed> $elemente */
private function setzeRegisterfelderSichtbar(
array &$elemente,
+1
View File
@@ -18,6 +18,7 @@ Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
- Ladestationen halten beim Aktivieren und Umschalten von Solarladen den bisherigen Strom bis zur neuen Manager-Vorgabe; die Stand-alone-Phasenprobe verwendet nur noch 6 A.
- Fahrzeugstatus ist bei beiden Ladestationsvarianten eine Diagnosevariable, Ist- und Sollleistung gehoeren zu den optionalen Visualisierungseinstellungen.
- Stufenlose Leistungsbereiche fuer Batteriespeicher.
- Schutzbedingtes Nachladen der Batterie kann wahlweise auf die dynamische Maximalleistung oder eine benutzerdefinierte Watt-Grenze begrenzt werden.
- Ladefreigabe neuer Ladestationsinstanzen standardmaessig aktiviert.
- Stand-alone-Ladestationen erkennen nach einer einstellbaren Beobachtungszeit den niedrigeren Fahrzeug-Maximalstrom und begrenzen ihr Leistungsangebot ohne Ladeunterbruch.
- Easee-Ladestationen laden einen fehlenden Initialstatus ueber die Observations-API nach und starten bei noch unbekannter Ausgangsphase konservativ einphasig.
+8 -3
View File
@@ -53,6 +53,8 @@ EinstellungenInVisu und LoggingEin stammen aus VerbraucherBasisTrait.
| ReserveLadezustand | Float | 20 | Peakshaving-Reserve in Prozent |
| MindestLadezustand | Float | 10 | Untere Entladegrenze in Prozent |
| LadezustandHysterese | Float | 2 | Hysterese oberhalb der Reserve |
| NachladenMitMaximalleistung | Boolean | true | Verwendet fuer schutzbedingtes Nachladen die dynamische maximale Ladeleistung |
| MaximaleNachladeleistung | Integer | 0 | Benutzerdefinierte Nachladegrenze in W; bei deaktivierter Maximalleistung muss der Wert groesser als 0 sein |
| MesswertMaxAlter | Integer | 60 | Maximales Messwertalter in Sekunden |
| Aenderungssperre | Integer | 4 | Sperrzeit nach Sollwertaenderung |
| DiagnosevariablenAnzeigen | Boolean | false | Technische Variablen einblenden |
@@ -81,9 +83,12 @@ wird.
PV und Peak verwenden weiterhin die bisherige SoC-Logik. Feste Schutz- oder
Peakvorgaben bleiben Einzelwerte; frei regelbare Angebote werden als Bereiche
gemeldet. Die Hysterese wird als persistenter Modulzustand gefuehrt. Bei
Wechselrichtersteuerung, lokaler Deaktivierung oder ungueltigen Messwerten
lautet das Angebot [0].
gemeldet. Bei schutzbedingtem Nachladen innerhalb oder unterhalb der Reserve
begrenzt MaximaleNachladeleistung die positive Angebotsgrenze, wenn
NachladenMitMaximalleistung deaktiviert ist. Der normale Ladebereich oberhalb
der Reserve bleibt unveraendert. Die Hysterese wird als persistenter Modulzustand
gefuehrt. Bei Wechselrichtersteuerung, lokaler Deaktivierung oder ungueltigen
Messwerten lautet das Angebot [0].
## Variablen
+10 -5
View File
@@ -52,7 +52,8 @@ final class BatterieRegler
float $netzleistungW,
float $istleistungW,
float $maxLadenW,
float $maxEntladenW
float $maxEntladenW,
?float $maxNachladenW = null
): array {
self::pruefeLadezustandsgrenzen($reserve, 0.0);
if ($minimum < 0.0 || $minimum > 100.0 || $minimum > $reserve) {
@@ -61,12 +62,16 @@ final class BatterieRegler
);
}
self::pruefeLeistungsgrenzen($maxLadenW, $maxEntladenW);
if ($maxNachladenW !== null && $maxNachladenW < 0.0) {
throw new InvalidArgumentException('Maximale Nachladeleistung darf nicht negativ sein.');
}
if (!$aktiv || !$enelixSteuert) {
return [0];
}
$maxLaden = (int) floor($maxLadenW);
$maxNachladen = (int) floor(min($maxLadenW, $maxNachladenW ?? $maxLadenW));
$maxEntladen = (int) floor($maxEntladenW);
if ($peakbetrieb) {
if ($ladezustand > $reserve && !$hystereseAktiv) {
@@ -80,10 +85,10 @@ final class BatterieRegler
return self::leistungsbereich(-$maxEntladen, 0);
}
if ($ladezustand > $minimum) {
return self::leistungsbereich(-$maxEntladen, $maxLaden);
return self::leistungsbereich(-$maxEntladen, $maxNachladen);
}
return self::leistungsbereich(0, $maxLaden);
return self::leistungsbereich(0, $maxNachladen);
}
if ($ladezustand > 99.0) {
@@ -93,10 +98,10 @@ final class BatterieRegler
return self::leistungsbereich(-$maxEntladen, $maxLaden);
}
if ($ladezustand >= $reserve && $hystereseAktiv) {
return self::leistungsbereich(0, $maxLaden);
return self::leistungsbereich(0, $maxNachladen);
}
return [$maxLaden];
return [$maxNachladen];
}
/**
+24
View File
@@ -83,6 +83,30 @@ final class BatterieModulstrukturTest extends TestCase
}
}
public function testNachladebegrenzungIstImFormularKonfigurierbar(): void
{
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
self::assertStringContainsString(
"RegisterPropertyBoolean('NachladenMitMaximalleistung', true)",
$modul
);
self::assertStringContainsString(
"RegisterPropertyInteger('MaximaleNachladeleistung', 0)",
$modul
);
self::assertStringContainsString(
"'FormNachladenMitMaximalleistung'",
$formular
);
self::assertStringContainsString(
'"name": "MaximaleNachladeleistung"',
$formular
);
self::assertStringContainsString('"suffix": " W"', $formular);
}
public function testLoggingDiagnoseUndAktionsbuttonsSindVorhanden(): void
{
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
+32
View File
@@ -82,6 +82,38 @@ final class BatterieReglerTest extends TestCase
);
}
public function testNachladeleistungBegrenztNurSchutzbedingtesLaden(): void
{
self::assertSame(
[400],
BatterieRegler::leistungsangebot(
true, true, false, 19.0, 20.0, 10.0, true,
0.0, 0.0, 1000.0, 1000.0, 400.0
)
);
self::assertSame(
[['Von_W' => 0, 'Bis_W' => 400]],
BatterieRegler::leistungsangebot(
true, true, true, 10.0, 20.0, 10.0, true,
0.0, 0.0, 1000.0, 1000.0, 400.0
)
);
self::assertSame(
[['Von_W' => -1000, 'Bis_W' => 400]],
BatterieRegler::leistungsangebot(
true, true, true, 15.0, 20.0, 10.0, true,
0.0, 0.0, 1000.0, 1000.0, 400.0
)
);
self::assertSame(
[['Von_W' => -1000, 'Bis_W' => 1000]],
BatterieRegler::leistungsangebot(
true, true, false, 50.0, 20.0, 10.0, false,
0.0, 0.0, 1000.0, 1000.0, 400.0
)
);
}
public function testWechselrichtersteuerungBietetNurNull(): void
{
self::assertSame(
+26
View File
@@ -163,6 +163,32 @@ PHP
}
);
$test->runCase(
'Benutzerdefinierte Nachladeleistung begrenzt das Schutzangebot',
static function (TestContext $test) use (
$batterieID,
$ladezustandID,
$managerdatenSenden
): void {
$test->configureInstance($batterieID, [
'Batteriemanagement' => 2,
'NachladenMitMaximalleistung' => false,
'MaximaleNachladeleistung' => 400,
]);
IPS_RequestAction($batterieID, 'Aktiv', true);
SetValue($ladezustandID, 19.0);
$managerdatenSenden('PV', null);
$angebotID = $test->objectByIdent('LeistungsangebotDiagnose', $batterieID);
$test->waitUntil(
static fn (): bool => json_decode(
(string) GetValue($angebotID),
true
) === [400]
);
}
);
$test->runCase(
'Wechselrichtersteuerung setzt Leistung auf null und Automatikmodus',
static function (TestContext $test) use (
+20 -2
View File
@@ -209,10 +209,25 @@ return static function (TestContext $test): void {
'Sollleistung_W' => $leistung,
], JSON_THROW_ON_ERROR));
};
$warteAufGeraetebefehl = static function (
int $stationId,
string $urlTeil
) use ($test): string {
$deadline = microtime(true) + 3.0;
do {
$befehl = (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId));
if (str_contains($befehl, $urlTeil)) {
return $befehl;
}
IPS_Sleep(50);
} while (microtime(true) < $deadline);
return $befehl;
};
$test->runCase('Alle drei Geraetevarianten erkennen Fahrzeug und Phasen', static function (
TestContext $test
) use ($definitionen, $stationen): void {
) use ($definitionen, $stationen, $warteAufGeraetebefehl): void {
foreach ($stationen as $name => $stationId) {
IPS_RequestAction($stationId, 'Aktiv', true);
$test->assertSame(true, GetValue($test->objectByIdent('FahrzeugVerbunden', $stationId)));
@@ -222,7 +237,10 @@ return static function (TestContext $test): void {
);
$test->assertSame(false, GetValue($test->objectByIdent('FahrzeugGeladen', $stationId)));
IPS_RequestAction($stationId, 'Solarladen', false);
$befehl = (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId));
$befehl = $warteAufGeraetebefehl(
$stationId,
$definitionen[$name]['BefehlUrlTeil']
);
$diagnose = json_encode([
'Aktiv' => GetValue($test->objectByIdent('Aktiv', $stationId)),
'Ladefreigabe' => GetValue($test->objectByIdent('Ladefreigabe', $stationId)),