diff --git a/Batterie/README.md b/Batterie/README.md index 7bb28a7..989ca72 100644 --- a/Batterie/README.md +++ b/Batterie/README.md @@ -8,7 +8,7 @@ IP-Symcon-Registervariablen. ## Funktionen - positive Leistung bedeutet Laden, negative Leistung bedeutet Entladen -- 250-W-Grundraster mit 50-W-Feinwerten zwischen -200 W und +200 W +- stufenlose Leistungsbereiche in ganzen Watt innerhalb der dynamischen Grenzen - unterschiedliche Leistungsangebote fuer PV- und Peakbetrieb - Reserve- und Mindestladezustand mit konfigurierbarer Hysterese - direkte Registeransteuerung fuer herstellerunabhaengige Batterien, GoodWe, @@ -86,9 +86,11 @@ LadezustandHysterese ersetzt die bisher fest im Code hinterlegte Die bisherigen betriebsartabhaengigen Angebote bleiben erhalten: -- Im PV-Betrieb steht oberhalb der Reserve das gesamte Raster zur Verfuegung. -- Unterhalb der Reserve wird die maximale Ladeleistung angeboten. -- Bei vollem Speicher werden nur Entladung und 0 angeboten. +- 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. +- 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 diff --git a/Batterie/module.php b/Batterie/module.php index fafc151..ada1b24 100644 --- a/Batterie/module.php +++ b/Batterie/module.php @@ -293,10 +293,9 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle $sollleistung = $daten['Sollleistung_W']; if ($sollleistung === null) { if ((bool) $this->leseZustand('SollwertGueltig') - && !in_array( - (int) $this->leseZustand('Sollleistung'), + && !Nachrichtenvertrag::enthaeltLeistungswert( $this->leseLeistungsangebot(), - true + (int) $this->leseZustand('Sollleistung') ) ) { $this->setzeZustand('SollwertGueltig', false); @@ -306,7 +305,10 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle return; } - if (!in_array($sollleistung, $this->leseLeistungsangebot(), true)) { + if (!Nachrichtenvertrag::enthaeltLeistungswert( + $this->leseLeistungsangebot(), + $sollleistung + )) { throw new InvalidArgumentException( 'Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.' ); @@ -394,10 +396,9 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle ); if ((bool) $this->leseZustand('SollwertGueltig') - && !in_array( - (int) $this->leseZustand('Sollleistung'), + && !Nachrichtenvertrag::enthaeltLeistungswert( $angebot, - true + (int) $this->leseZustand('Sollleistung') ) ) { $this->setzeZustand('SollwertGueltig', false); @@ -1073,7 +1074,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle } } - /** @return list */ + /** @return list */ private function leseLeistungsangebot(): array { $angebot = json_decode( @@ -1086,7 +1087,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle return [0]; } - return array_values(array_map('intval', $angebot)); + return array_values($angebot); } private function setzeStoerung(string $text): void diff --git a/docs/Schnittstelle-Batterie.md b/docs/Schnittstelle-Batterie.md index 310b7ae..c1fcad0 100644 --- a/docs/Schnittstelle-Batterie.md +++ b/docs/Schnittstelle-Batterie.md @@ -29,8 +29,8 @@ Sollleistung_W verwendet folgende Semantik: - null: nur Betriebsart synchronisieren und neues Angebot anfordern Ein konkreter Wert muss im zuletzt fuer dieselbe Betriebsart gemeldeten -Leistungsangebot enthalten sein. Waehren der Aenderungssperre wird nur die -Wiederholung des aktuellen Sollwerts akzeptiert. +Einzelwert oder Leistungsbereich enthalten sein. Waehrend der Aenderungssperre +wird nur die Wiederholung des aktuellen Sollwerts akzeptiert. ## Batterie an Manager @@ -56,10 +56,10 @@ Beispiel: "EmpfaengerID": 10001, "Zeitpunkt": 1788825602 }, - "Betriebsart": "Peak", + "Betriebsart": "PV", "PrioritaetPV": 0, "PrioritaetPeak": 0, - "Leistungswerte_W": [-1500], + "Leistungswerte_W": [{"Von_W": -5000, "Bis_W": 5000}], "AenderungMoeglich": true, "Verfuegbar": true, "Istleistung_W": -250.0, @@ -68,7 +68,7 @@ Beispiel: { "Kennung": "Sollleistung_W", "Art": "Sollwert", - "Wert": -1500, + "Wert": -537, "Einheit": "W" }, { @@ -83,7 +83,9 @@ Beispiel: Ist die Leistungsmessung ungueltig, wird Istleistung_W als null und Leistungsquelle als 0 gemeldet. Bei einem gueltigen Messwert ist -Leistungsquelle 2. +Leistungsquelle 2. Frei regelbare Lade- und Entladeangebote werden als +inklusive Bereiche in ganzen Watt gemeldet. Feste Schutz- und Peakvorgaben +bleiben einzelne Leistungswerte. ## Ereignisse diff --git a/docs/migration/Batterie.md b/docs/migration/Batterie.md index 07dc19d..4106a5e 100644 --- a/docs/migration/Batterie.md +++ b/docs/migration/Batterie.md @@ -11,7 +11,6 @@ Verhaltensreferenz. - positive Sollleistung fuer Laden und negative Sollleistung fuer Entladen - dynamische maximale Lade- und Entladeleistung aus Variablen - Ladezustand, Netzleistung und aktuelle Batterieleistung als Eingaben -- 250-W-Grundraster und 50-W-Feinwerte um 0 - Reserve-, Mindestladezustands- und Hystereselogik - getrennte Angebote fuer PV und Peak - Wechselrichter- oder Enelix-Steuerung @@ -25,6 +24,7 @@ Verhaltensreferenz. | zyklischer Timer Do_UserCalc | Messwert- und Managerereignisse | | IdleCounter in Zyklen | Aenderungssperre in Sekunden mit Einmaltimer | | PowerSteps als Modulvariable | Leistungswerte_W direkt im Vertrag | +| 250-W-Grundraster mit 50-W-Feinwerten | inklusive Leistungsbereiche in ganzen Watt | | interne Stellwertvariablen | ausgewaehlte Registervariablen mit RequestAction | | fest verdrahtete Herstellerhilfsvariablen | dynamisch sichtbare Registerauswahl | | fest codierte 2-%-Hysterese | Property LadezustandHysterese | diff --git a/docs/module/Batterie/README.md b/docs/module/Batterie/README.md index c99a783..58d8bf8 100644 --- a/docs/module/Batterie/README.md +++ b/docs/module/Batterie/README.md @@ -73,13 +73,17 @@ zuletzt Management geschrieben. ## Leistungsangebot -Das Raster besteht wie in Enelix 1 aus 250-W-Grundschritten. Um 0 werden -zusaetzlich -200, -150, -100, -50, 0, 50, 100, 150 und 200 W angeboten, -soweit diese Werte innerhalb der dynamischen Leistungsgrenzen liegen. +Die Batterie meldet innerhalb der dynamischen Lade- und Entladegrenzen +inklusive Leistungsbereiche in ganzen Watt. Der Manager kann dadurch jeden +ganzzahligen Sollwert innerhalb des aktuell erlaubten Bereichs vorgeben. Die +Grenzen werden abgerundet, damit keine dynamische Maximalleistung ueberschritten +wird. -PV und Peak verwenden die bisherige SoC-Logik. Die Hysterese wird als -persistenter Modulzustand gefuehrt. Bei Wechselrichtersteuerung, lokaler -Deaktivierung oder ungueltigen Messwerten lautet das Angebot [0]. +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]. ## Variablen @@ -115,7 +119,7 @@ Transport und gemeinsame Felder folgen ## Tests -- BatterieReglerTest: Raster, Hysterese, PV/Peak und Herstellerabbildung +- BatterieReglerTest: Leistungsbereiche, Hysterese, PV/Peak und Herstellerabbildung - BatterieModulstrukturTest: Metadaten, Formular, Ereignismodell und Manager-ID - Symcon/modules/Batterie.php: reale Modulinstanz und Registeraktionen unter IP-Symcon 8.0 diff --git a/libs/BatterieRegler.php b/libs/BatterieRegler.php index 17d0835..0a6c047 100644 --- a/libs/BatterieRegler.php +++ b/libs/BatterieRegler.php @@ -17,38 +17,6 @@ final class BatterieRegler public const STEUERUNG_WECHSELRICHTER = 1; public const STEUERUNG_ENELIX = 2; - /** - * Erzeugt das bisherige Raster: 250-W-Grundschritte und - * 50-W-Feinschritte im Bereich von -200 W bis +200 W. - * - * @return list - */ - public static function leistungsraster(float $maxLadenW, float $maxEntladenW): array - { - self::pruefeLeistungsgrenzen($maxLadenW, $maxEntladenW); - - $maxLaden = (int) floor($maxLadenW / 50.0) * 50; - $maxEntladen = (int) floor($maxEntladenW / 50.0) * 50; - $werte = [0]; - - for ($leistung = -$maxEntladen; $leistung <= 0; $leistung += 250) { - $werte[] = $leistung; - } - for ($leistung = 0; $leistung <= $maxLaden; $leistung += 250) { - $werte[] = $leistung; - } - for ($leistung = -200; $leistung <= 200; $leistung += 50) { - if ($leistung >= -$maxEntladen && $leistung <= $maxLaden) { - $werte[] = $leistung; - } - } - - $werte = array_values(array_unique($werte)); - sort($werte, SORT_NUMERIC); - - return $werte; - } - public static function hystereseAktiv( float $ladezustand, float $reserve, @@ -68,9 +36,10 @@ final class BatterieRegler } /** - * Bildet das zustands- und betriebsartabhaengige Angebot aus Enelix 1 ab. + * Bildet das zustands- und betriebsartabhaengige Angebot als + * durchgaengige Leistungsbereiche in ganzen Watt ab. * - * @return list + * @return list */ public static function leistungsangebot( bool $aktiv, @@ -97,7 +66,8 @@ final class BatterieRegler return [0]; } - $raster = self::leistungsraster($maxLadenW, $maxEntladenW); + $maxLaden = (int) floor($maxLadenW); + $maxEntladen = (int) floor($maxEntladenW); if ($peakbetrieb) { if ($ladezustand > $reserve && !$hystereseAktiv) { $ziel = $netzleistungW > 0.0 @@ -107,26 +77,26 @@ final class BatterieRegler return [self::begrenzeLeistung($ziel, $maxLadenW, $maxEntladenW)]; } if ($ladezustand > $reserve && $hystereseAktiv) { - return self::filtere($raster, static fn (int $wert): bool => $wert <= 0); + return self::leistungsbereich(-$maxEntladen, 0); } if ($ladezustand > $minimum) { - return $raster; + return self::leistungsbereich(-$maxEntladen, $maxLaden); } - return self::filtere($raster, static fn (int $wert): bool => $wert >= 0); + return self::leistungsbereich(0, $maxLaden); } if ($ladezustand > 99.0) { - return self::filtere($raster, static fn (int $wert): bool => $wert <= 0); + return self::leistungsbereich(-$maxEntladen, 0); } if ($ladezustand > $reserve && !$hystereseAktiv) { - return $raster; + return self::leistungsbereich(-$maxEntladen, $maxLaden); } if ($ladezustand >= $reserve && $hystereseAktiv) { - return self::filtere($raster, static fn (int $wert): bool => $wert >= 0); + return self::leistungsbereich(0, $maxLaden); } - return [(int) round($maxLadenW)]; + return [$maxLaden]; } /** @@ -220,12 +190,14 @@ final class BatterieRegler return 1; } - /** @param list $werte - * @return list - */ - private static function filtere(array $werte, callable $filter): array + /** @return list */ + private static function leistungsbereich(int $vonW, int $bisW): array { - return array_values(array_filter($werte, $filter)); + if ($vonW === $bisW) { + return [$vonW]; + } + + return [['Von_W' => $vonW, 'Bis_W' => $bisW]]; } private static function begrenzeLeistung( @@ -233,7 +205,10 @@ final class BatterieRegler float $maxLadenW, float $maxEntladenW ): int { - return (int) round(min($maxLadenW, max(-$maxEntladenW, $leistungW))); + $maxLaden = (int) floor($maxLadenW); + $maxEntladen = (int) floor($maxEntladenW); + + return (int) round(min($maxLaden, max(-$maxEntladen, $leistungW))); } private static function pruefeLeistungsgrenzen(float $maxLadenW, float $maxEntladenW): void diff --git a/libs/Nachrichtenvertrag.php b/libs/Nachrichtenvertrag.php index cd87d2a..21b1526 100644 --- a/libs/Nachrichtenvertrag.php +++ b/libs/Nachrichtenvertrag.php @@ -70,6 +70,33 @@ final class Nachrichtenvertrag self::pruefeZustand($daten['Zustand']); } + /** + * @param list $leistungswerte + */ + public static function enthaeltLeistungswert(array $leistungswerte, int $leistung): bool + { + foreach ($leistungswerte as $eintrag) { + if (is_int($eintrag)) { + if ($eintrag === $leistung) { + return true; + } + continue; + } + + if (is_array($eintrag) + && isset($eintrag['Von_W'], $eintrag['Bis_W']) + && is_int($eintrag['Von_W']) + && is_int($eintrag['Bis_W']) + && $leistung >= $eintrag['Von_W'] + && $leistung <= $eintrag['Bis_W'] + ) { + return true; + } + } + + return false; + } + /** * @param mixed $kopf */ diff --git a/tests/BatterieReglerTest.php b/tests/BatterieReglerTest.php index 208dc93..3956e1f 100644 --- a/tests/BatterieReglerTest.php +++ b/tests/BatterieReglerTest.php @@ -10,14 +10,14 @@ use PHPUnit\Framework\TestCase; final class BatterieReglerTest extends TestCase { - public function testLeistungsrasterBehaeltGrobeUndFeineSchritte(): void + public function testLeistungsbereichBleibtInnerhalbDynamischerGrenzen(): void { self::assertSame( - [ - -1000, -750, -500, -250, -200, -150, -100, -50, - 0, 50, 100, 150, 200, 250, 500, 750, 1000, - ], - BatterieRegler::leistungsraster(1000.0, 1000.0) + [['Von_W' => -999, 'Bis_W' => 1000]], + BatterieRegler::leistungsangebot( + true, true, false, 50.0, 20.0, 10.0, false, + 0.0, 0.0, 1000.9, 999.9 + ) ); } @@ -28,12 +28,10 @@ final class BatterieReglerTest extends TestCase self::assertFalse(BatterieRegler::hystereseAktiv(22.1, 20.0, 2.0, true)); } - public function testPvAngebotEntsprichtDemBisherigenSocVerhalten(): void + public function testPvAngebotVerwendetStufenloseBereicheMitBisherigerSocLogik(): void { - $voll = BatterieRegler::leistungsraster(1000.0, 1000.0); - self::assertSame( - $voll, + [['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 @@ -47,7 +45,7 @@ final class BatterieReglerTest extends TestCase ) ); self::assertSame( - [-1000, -750, -500, -250, -200, -150, -100, -50, 0], + [['Von_W' => -1000, 'Bis_W' => 0]], BatterieRegler::leistungsangebot( true, true, false, 100.0, 20.0, 10.0, false, 0.0, 0.0, 1000.0, 1000.0 @@ -66,6 +64,24 @@ final class BatterieReglerTest extends TestCase ); } + public function testPeakEinschraenkungenWerdenAlsBereicheGemeldet(): void + { + self::assertSame( + [['Von_W' => -1000, 'Bis_W' => 0]], + BatterieRegler::leistungsangebot( + true, true, true, 21.0, 20.0, 10.0, true, + 0.0, 0.0, 1000.0, 1000.0 + ) + ); + self::assertSame( + [['Von_W' => 0, 'Bis_W' => 1000]], + BatterieRegler::leistungsangebot( + true, true, true, 10.0, 20.0, 10.0, true, + 0.0, 0.0, 1000.0, 1000.0 + ) + ); + } + public function testWechselrichtersteuerungBietetNurNull(): void { self::assertSame( @@ -186,6 +202,9 @@ final class BatterieReglerTest extends TestCase { $this->expectException(InvalidArgumentException::class); - BatterieRegler::leistungsraster(-1.0, 1000.0); + BatterieRegler::leistungsangebot( + true, true, false, 50.0, 20.0, 10.0, false, + 0.0, 0.0, -1.0, 1000.0 + ); } } diff --git a/tests/NachrichtenvertragTest.php b/tests/NachrichtenvertragTest.php index 17899b1..3f98786 100644 --- a/tests/NachrichtenvertragTest.php +++ b/tests/NachrichtenvertragTest.php @@ -43,6 +43,17 @@ final class NachrichtenvertragTest extends TestCase self::assertTrue(true); } + public function testLeistungswertWirdInEinzelwertenUndBereichenGefunden(): void + { + $angebot = [0, ['Von_W' => 1000, 'Bis_W' => 2000], 3000]; + + self::assertTrue(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 0)); + self::assertTrue(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 1537)); + self::assertTrue(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 2000)); + self::assertFalse(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 999)); + self::assertFalse(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 2500)); + } + public function testNegativePrioritaetWirdAbgelehnt(): void { $daten = $this->verbraucherdaten(); diff --git a/tests/Symcon/modules/Batterie.php b/tests/Symcon/modules/Batterie.php index 093c5ae..76f42d2 100644 --- a/tests/Symcon/modules/Batterie.php +++ b/tests/Symcon/modules/Batterie.php @@ -119,10 +119,10 @@ PHP ): void { IPS_RequestAction($batterieID, 'Aktiv', true); $managerdatenSenden('PV', null); - $managerdatenSenden('PV', 500); + $managerdatenSenden('PV', 537); $test->waitUntil( - static fn (): bool => (float) GetValue($leistungID) === 500.0 + static fn (): bool => (float) GetValue($leistungID) === 537.0 && (int) GetValue($modusID) === 11 && (int) GetValue($managementID) === 2 ); @@ -136,9 +136,9 @@ PHP $modusID, $leistungID ): void { - $managerdatenSenden('PV', -500); + $managerdatenSenden('PV', -843); $test->waitUntil( - static fn (): bool => (float) GetValue($leistungID) === 500.0 + static fn (): bool => (float) GetValue($leistungID) === 843.0 && (int) GetValue($modusID) === 12 ); }