From 8d83b083bf9ad7912926fcf4a539eff67071f89a Mon Sep 17 00:00:00 2001 From: dh_Agent Date: Thu, 1 Oct 2026 15:56:23 +0000 Subject: [PATCH] feat(batterie): Nachladeleistung begrenzen --- Batterie/README.md | 15 ++++++--- Batterie/form.json | 14 +++++++++ Batterie/module.php | 49 +++++++++++++++++++++++++++-- CHANGELOG.md | 1 + docs/module/Batterie/README.md | 11 +++++-- libs/BatterieRegler.php | 15 ++++++--- tests/BatterieModulstrukturTest.php | 24 ++++++++++++++ tests/BatterieReglerTest.php | 32 +++++++++++++++++++ tests/Symcon/modules/Batterie.php | 26 +++++++++++++++ 9 files changed, 173 insertions(+), 14 deletions(-) diff --git a/Batterie/README.md b/Batterie/README.md index 989ca72..1ae27ae 100644 --- a/Batterie/README.md +++ b/Batterie/README.md @@ -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. diff --git a/Batterie/form.json b/Batterie/form.json index c7cbcc5..1aa40b5 100644 --- a/Batterie/form.json +++ b/Batterie/form.json @@ -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 } ] }, diff --git a/Batterie/module.php b/Batterie/module.php index ada1b24..97c7a90 100644 --- a/Batterie/module.php +++ b/Batterie/module.php @@ -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 $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 $elemente */ private function setzeRegisterfelderSichtbar( array &$elemente, diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7161f..a316bca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/module/Batterie/README.md b/docs/module/Batterie/README.md index 58d8bf8..19706d5 100644 --- a/docs/module/Batterie/README.md +++ b/docs/module/Batterie/README.md @@ -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 diff --git a/libs/BatterieRegler.php b/libs/BatterieRegler.php index 0a6c047..25f6ab4 100644 --- a/libs/BatterieRegler.php +++ b/libs/BatterieRegler.php @@ -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]; } /** diff --git a/tests/BatterieModulstrukturTest.php b/tests/BatterieModulstrukturTest.php index 7df7eff..da8e329 100644 --- a/tests/BatterieModulstrukturTest.php +++ b/tests/BatterieModulstrukturTest.php @@ -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'); diff --git a/tests/BatterieReglerTest.php b/tests/BatterieReglerTest.php index 3956e1f..f34687a 100644 --- a/tests/BatterieReglerTest.php +++ b/tests/BatterieReglerTest.php @@ -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( diff --git a/tests/Symcon/modules/Batterie.php b/tests/Symcon/modules/Batterie.php index 76f42d2..1b8bd4a 100644 --- a/tests/Symcon/modules/Batterie.php +++ b/tests/Symcon/modules/Batterie.php @@ -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 (