'2026-09-30T11:35:00+00:00', 'gridTargetW' => 20486.254], ['time' => '2026-09-30T11:30:00+00:00', 'gridTargetW' => 15664.283], ]; $vorgabe = ManagerRegler::interpolierteFahrplanVorgabe( $punkte, (int) strtotime('2026-09-30T11:32:30+00:00') ); self::assertEqualsWithDelta(18075.2685, $vorgabe, 0.0001); } public function testFahrplanVorgabeVerwendetRandpunktInnerhalbDerToleranz(): void { $punkte = [ ['time' => '2026-09-30T11:30:00+00:00', 'gridTargetW' => 15664.283], ['time' => '2026-09-30T11:35:00+00:00', 'gridTargetW' => 20486.254], ]; self::assertSame( 15664.283, ManagerRegler::interpolierteFahrplanVorgabe( $punkte, (int) strtotime('2026-09-30T11:29:44+00:00') ) ); self::assertSame( 20486.254, ManagerRegler::interpolierteFahrplanVorgabe( $punkte, (int) strtotime('2026-09-30T11:35:10+00:00') ) ); } public function testFahrplanVorgabeVerwirftZuGrosseZeitlicheLuecke(): void { $punkte = [ ['time' => '2026-09-30T11:00:00+00:00', 'gridTargetW' => 10000], ['time' => '2026-09-30T12:00:00+00:00', 'gridTargetW' => 20000], ]; self::assertNull( ManagerRegler::interpolierteFahrplanVorgabe( $punkte, (int) strtotime('2026-09-30T11:31:00+00:00') ) ); } public function testOhnePeakBegrenzungBleibtSolarladenAktiv(): void { self::assertSame( ManagerRegler::BETRIEBSART_PV, ManagerRegler::bestimmeBetriebsart( true, true, 25000.0, ManagerRegler::BETRIEBSART_INAKTIV, false, 0.0, 15000.0, 0.0 ) ); } public function testBetriebsartBleibtZwischenDenZielwertenErhalten(): void { self::assertSame( ManagerRegler::BETRIEBSART_PEAK, ManagerRegler::bestimmeBetriebsart( true, true, 5000.0, ManagerRegler::BETRIEBSART_PEAK, true, 0.0, 12000.0, 0.0 ) ); } public function testUmschaltdifferenzVerhindertZuKleineAenderung(): void { self::assertSame( ManagerRegler::BETRIEBSART_PV, ManagerRegler::bestimmeBetriebsart( true, true, 11000.0, ManagerRegler::BETRIEBSART_PV, true, 10000.0, 10500.0, 60.0 ) ); } public function testWechseltOberhalbDerPeakgrenzeInDenPeakbetrieb(): void { self::assertSame( ManagerRegler::BETRIEBSART_PEAK, ManagerRegler::bestimmeBetriebsart( true, true, 12500.0, ManagerRegler::BETRIEBSART_PV, true, 0.0, 12000.0, 5.0 ) ); } public function testWechseltUnterhalbDesSolarsollwertsInDenPvBetrieb(): void { self::assertSame( ManagerRegler::BETRIEBSART_PV, ManagerRegler::bestimmeBetriebsart( true, true, -500.0, ManagerRegler::BETRIEBSART_PEAK, true, 0.0, 12000.0, 5.0 ) ); } public function testVerteilungBeruecksichtigtPrioritaetEinzelwerteUndBereiche(): void { $ergebnis = ManagerRegler::verteile( 0.0, 4500.0, [ $this->verbraucher(20, 5, [0, 3000], 0), $this->verbraucher(10, 0, [0, ['Von_W' => 1000, 'Bis_W' => 2000], 4000], 0), ], 'PrioritaetPV' ); self::assertSame(4500, $ergebnis['Verteilbudget_W']); self::assertSame([10 => 4000, 20 => 0], $ergebnis['Sollwerte']); self::assertSame(500, $ergebnis['Restabweichung_W']); } public function testGleicheNaechsteLeistungBevorzugtWenigerEnergie(): void { $ergebnis = ManagerRegler::verteile( 0.0, 3000.0, [ $this->verbraucher(10, 0, [0, 3000], 0), $this->verbraucher(20, 0, [0, 3000], 0), ], 'PrioritaetPV', [10 => 3000.0, 20 => 0.0] ); self::assertSame([20 => 3000, 10 => 0], $ergebnis['Sollwerte']); self::assertSame(0, $ergebnis['Restabweichung_W']); } public function testEnergiegleichstandVergleichtExakteWhStattZweiKilowattstundenGruppen(): void { $ergebnis = ManagerRegler::verteile( 0.0, 3000.0, [ $this->verbraucher(10, 0, [0, 3000], 0), $this->verbraucher(20, 0, [0, 3000], 0), ], 'PrioritaetPV', [10 => 999.0, 20 => 0.0] ); self::assertSame([10 => 0, 20 => 3000], $ergebnis['Sollwerte']); } /** @dataProvider beispielBudgets */ public function testBestaetigteStufenfolge(int $budget, int $a, int $b): void { foreach (['PrioritaetPV', 'PrioritaetPeak'] as $feld) { $ergebnis = ManagerRegler::verteile(0.0, (float) $budget, [ $this->verbraucher(20, 2, [0, 100, 110, 500, 780, 1500], 0), $this->verbraucher(10, 2, [0, 100, 200, 400, 800], 0), ], $feld, [10 => 10000.0, 20 => 12000.0]); self::assertSame([10 => $a, 20 => $b], $ergebnis['Sollwerte']); self::assertSame($budget - $a - $b, $ergebnis['Restabweichung_W']); } } public function beispielBudgets(): array { return [ [0, 0, 0], [99, 0, 0], [100, 100, 0], [199, 100, 0], [200, 100, 100], [209, 100, 100], [210, 100, 110], [309, 100, 110], [310, 200, 110], [509, 200, 110], [510, 400, 110], [899, 400, 110], [900, 400, 500], [1179, 400, 500], [1180, 400, 780], [1579, 400, 780], [1580, 800, 780], [2299, 800, 780], [2300, 800, 1500], [3000, 800, 1500], ]; } public function testLeistungsniveauGehtVorEnergieUndSchrittdifferenz(): void { $ergebnis = ManagerRegler::verteile(0.0, 220.0, [ $this->verbraucher(10, 0, [0, 100, 200], 0), $this->verbraucher(20, 0, [0, 110, 120], 0), ], 'PrioritaetPV', [10 => 0.0, 20 => 50000.0]); self::assertSame([10 => 100, 20 => 120], $ergebnis['Sollwerte']); } public function testPicoUndGoETeilenBudgetTrotzGrossemEnergieunterschied(): void { $ergebnis = ManagerRegler::verteile(0.0, 12312.0, [ $this->verbraucher(31986, 2, array_merge([0], range(4104, 12312, 684)), 0), $this->verbraucher(43878, 2, array_merge([0], range(4104, 10944, 684)), 0), ], 'PrioritaetPV', [31986 => 41192.78, 43878 => 128082.87]); self::assertSame([31986 => 6156, 43878 => 6156], $ergebnis['Sollwerte']); self::assertSame(0, $ergebnis['Restabweichung_W']); } public function testNegativeDefizitverteilungBehaeltBisherigeEnergiegruppen(): void { $ergebnis = ManagerRegler::verteile(300.0, 0.0, [ $this->verbraucher(10, 0, [['Von_W' => -300, 'Bis_W' => 300]], 0), $this->verbraucher(20, 0, [['Von_W' => -300, 'Bis_W' => 300]], 0), ], 'PrioritaetPeak', [10 => 999.0, 20 => 0.0]); self::assertSame([10 => -300, 20 => 0], $ergebnis['Sollwerte']); self::assertSame(0, $ergebnis['Restabweichung_W']); } public function testGleicheLeistungUndEnergieEntscheidetStabilNachInstanzID(): void { $ergebnis = ManagerRegler::verteile(0.0, 100.0, [ $this->verbraucher(20, 0, [0, 100], 0), $this->verbraucher(10, 0, [0, 100], 0), ], 'PrioritaetPV'); self::assertSame([10 => 100, 20 => 0], $ergebnis['Sollwerte']); } public function testNichtPassenderSchrittBlockiertKeineAndereErreichbareStufe(): void { $ergebnis = ManagerRegler::verteile(0.0, 210.0, [ $this->verbraucher(10, 0, [0, 100, 200], 0), $this->verbraucher(20, 0, [0, 150], 0), ], 'PrioritaetPV'); self::assertSame([10 => 200, 20 => 0], $ergebnis['Sollwerte']); self::assertSame(10, $ergebnis['Restabweichung_W']); } public function testPeakVerwendetPeakPrioritaetVorLeistungUndEnergie(): void { $a = $this->verbraucher(10, 0, [0, 100, 200], 0); $b = $this->verbraucher(20, 9, [0, 300], 0); $a['Daten']['PrioritaetPeak'] = 9; $b['Daten']['PrioritaetPeak'] = 0; $ergebnis = ManagerRegler::verteile(0.0, 300.0, [$a, $b], 'PrioritaetPeak', [20 => 50000.0]); self::assertSame([20 => 300, 10 => 0], $ergebnis['Sollwerte']); } public function testGrosseBereicheWerdenGemeinsamOhneWattweiseSchleifeVerteilt(): void { $ergebnis = ManagerRegler::verteile(0.0, 1000000001.0, [ $this->verbraucher(10, 0, [['Von_W' => 0, 'Bis_W' => 1000000000]], 0), $this->verbraucher(20, 0, [['Von_W' => 0, 'Bis_W' => 1000000000]], 0), ], 'PrioritaetPV', [10 => 999.0, 20 => 0.0]); self::assertSame([10 => 500000000, 20 => 500000001], $ergebnis['Sollwerte']); self::assertSame(0, $ergebnis['Restabweichung_W']); } public function testBereicheUndEinzelwerteGegenWattweiseReferenz(): void { $angebote = [ [0, ['Von_W' => 3, 'Bis_W' => 9], 15], [0, 5, ['Von_W' => 7, 'Bis_W' => 12], 18], [0, 4, 8, ['Von_W' => 14, 'Bis_W' => 20]], ]; $energien = [10 => 10000.5, 20 => 10000.0, 30 => 10000.0]; $verbraucher = []; $diskret = []; foreach ($angebote as $index => $angebot) { $id = ($index + 1) * 10; $verbraucher[] = $this->verbraucher($id, 2, $angebot, 0); $diskret[$id] = []; foreach ($angebot as $wert) { foreach (is_int($wert) ? [$wert] : range($wert['Von_W'], $wert['Bis_W']) as $watt) { $diskret[$id][] = $watt; } } } for ($budget = 0; $budget <= 60; $budget++) { $erwartet = [10 => 0, 20 => 0, 30 => 0]; $rest = $budget; while (true) { $schritte = []; foreach ($diskret as $id => $werte) { foreach ($werte as $wert) { if ($wert > $erwartet[$id]) { if ($wert - $erwartet[$id] <= $rest) { $schritte[] = [$wert, $energien[$id], $id]; } break; } } } if ($schritte === []) { break; } sort($schritte); [$wert, , $id] = $schritte[0]; $rest -= $wert - $erwartet[$id]; $erwartet[$id] = $wert; } $ergebnis = ManagerRegler::verteile(0.0, (float) $budget, array_reverse($verbraucher), 'PrioritaetPV', $energien); $ist = $ergebnis['Sollwerte']; ksort($ist); self::assertSame($erwartet, $ist, 'Budget: ' . $budget); self::assertSame($rest, $ergebnis['Restabweichung_W']); } } public function testNichtVerfuegbareLastWirdReserviertUndNichtNeuZugeteilt(): void { $a = $this->verbraucher(10, 0, [0, 100, 200], 100); $a['Daten']['Verfuegbar'] = false; $ergebnis = ManagerRegler::verteile(100.0, 200.0, [ $a, $this->verbraucher(20, 0, [0, 100, 200], 0), ], 'PrioritaetPV'); self::assertSame(100, $ergebnis['NichtSteuerbar_W']); self::assertSame([20 => 100], $ergebnis['Sollwerte']); } public function testFestwertWirdUnabhaengigVonAenderungMoeglichZugewiesen(): void { $fest = $this->verbraucher(10, 0, [3000], 3000, false); $steuerbar = $this->verbraucher(20, 1, [0, ['Von_W' => 1, 'Bis_W' => 4000]], 0); $ergebnis = ManagerRegler::verteile(3000.0, 5000.0, [$fest, $steuerbar], 'PrioritaetPV'); self::assertSame(3000, $ergebnis['NichtSteuerbar_W']); self::assertSame([10 => 3000, 20 => 2000], $ergebnis['Sollwerte']); self::assertSame(0, $ergebnis['Restabweichung_W']); } public function testFestwerteWerdenNachIstleistungBilanziertUndAlsBefehlBeibehalten(): void { $ergebnis = ManagerRegler::verteile( -2411.0, 0.0, [ $this->verbraucher( 44234, 0, [['Von_W' => -39000, 'Bis_W' => 39000]], 0 ), $this->verbraucher(38676, 1, [0, 4600], 0), $this->verbraucher(29743, 1, [0, 6500], 0), $this->verbraucher(43878, 2, [13850], 10151, false), $this->verbraucher(46052, 2, [11000], 0, false), ], 'PrioritaetPV' ); self::assertSame(12562, $ergebnis['Verteilbudget_W']); self::assertSame(10151, $ergebnis['NichtSteuerbar_W']); self::assertSame( [ 44234 => 2411, 29743 => 0, 38676 => 0, 43878 => 13850, 46052 => 11000, ], $ergebnis['Sollwerte'] ); self::assertSame(0, $ergebnis['Restabweichung_W']); } public function testBatterieGleichtGemesseneFesteLastenAusOhneSolarladungZuSpeisen(): void { $ergebnis = ManagerRegler::verteile( -4526.0, -50.0, [ $this->verbraucher(29743, 1, [0, 6500], 6500), $this->verbraucher(38676, 1, [0, 4600], 4600), $this->verbraucher(22078, 2, [7690], 0, false), $this->verbraucher(31986, 2, [11000], 10942, false), $this->verbraucher(43878, 2, [0, 4150, 10300], 9908), $this->verbraucher( 44234, 6, [['Von_W' => -39000, 'Bis_W' => 39000]], -22769 ), ], 'PrioritaetPV' ); self::assertSame(13657, $ergebnis['Verteilbudget_W']); self::assertSame(10942, $ergebnis['NichtSteuerbar_W']); self::assertSame( [ 29743 => 0, 38676 => 0, 22078 => 7690, 31986 => 11000, 43878 => 0, 44234 => 2715, ], $ergebnis['Sollwerte'] ); self::assertSame(0, $ergebnis['Restabweichung_W']); } public function testBegrenztesFahrzeugLaedtVorBatterieOhneBatterieentladung(): void { $ergebnis = ManagerRegler::verteile( -3587.0, 0.0, [ $this->verbraucher( 10, 0, [0, 4150, 4850, 5550, 6250, 6950, 7600, 8300, 9000, 9700, 10300, 11000], 10263 ), $this->verbraucher( 20, 1, [['Von_W' => -39000, 'Bis_W' => 39000]], 0 ), ], 'PrioritaetPV' ); self::assertSame(13850, $ergebnis['Verteilbudget_W']); self::assertSame([10 => 11000, 20 => 2850], $ergebnis['Sollwerte']); self::assertGreaterThanOrEqual(0, $ergebnis['Sollwerte'][20]); self::assertSame(0, $ergebnis['Restabweichung_W']); } public function testPvBetriebNutztBeiPositivemBudgetKeineNegativeLeistungsstufe(): void { $ergebnis = ManagerRegler::verteile( 0.0, 500.0, [ $this->verbraucher(10, 0, [-1000, 5000], 0), $this->verbraucher(20, 1, [0, 3000], 0), ], 'PrioritaetPV' ); self::assertSame(500, $ergebnis['Verteilbudget_W']); self::assertSame([10 => 5000, 20 => 0], $ergebnis['Sollwerte']); self::assertGreaterThanOrEqual(0, $ergebnis['Sollwerte'][10]); self::assertSame(-4500, $ergebnis['Restabweichung_W']); } public function testPvBetriebNutztNegativeLeistungErstBeiTatsaechlichemDefizit(): void { $ergebnis = ManagerRegler::verteile( 1000.0, 0.0, [ $this->verbraucher( 10, 0, [['Von_W' => -5000, 'Bis_W' => 5000]], 0 ), $this->verbraucher(20, 1, [0, 3000], 0), ], 'PrioritaetPV' ); self::assertSame(-1000, $ergebnis['Verteilbudget_W']); self::assertSame([10 => -1000, 20 => 0], $ergebnis['Sollwerte']); self::assertSame(0, $ergebnis['Restabweichung_W']); } public function testFestgehaltenerVerbraucherBilanziertIstUndBehaeltSollwert(): void { $festgehalten = $this->verbraucher(10, 0, [0, 3000, 5000], 2000, false); $festgehalten['Daten']['Zustand'][0]['Wert'] = 3000; $ergebnis = ManagerRegler::verteile( 0.0, 3000.0, [ $festgehalten, $this->verbraucher(20, 1, [['Von_W' => 0, 'Bis_W' => 5000]], 0), ], 'PrioritaetPV' ); self::assertSame(5000, $ergebnis['Verteilbudget_W']); self::assertSame(2000, $ergebnis['NichtSteuerbar_W']); self::assertSame([10 => 3000, 20 => 3000], $ergebnis['Sollwerte']); self::assertSame(0, $ergebnis['Restabweichung_W']); } public function testFehlendeIstleistungFaelltAufAngenommenenSollwertZurueck(): void { $verbraucher = $this->verbraucher(10, 0, [3000], null, false); $verbraucher['Daten']['Leistungsquelle'] = Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN; $verbraucher['Daten']['Istleistung_W'] = null; $verbraucher['Daten']['Zustand'][0]['Wert'] = 3000; $ergebnis = ManagerRegler::verteile(3000.0, 0.0, [$verbraucher], 'PrioritaetPV'); self::assertSame(3000, $ergebnis['AktuelleVerbraucherleistung_W']); self::assertSame([10 => 3000], $ergebnis['Sollwerte']); self::assertSame(-3000, $ergebnis['Restabweichung_W']); } public function testBatterieMitTieferPrioritaetUebernimmtNichtNutzbarenRest(): void { $ergebnis = ManagerRegler::verteile( -5000.0, 0.0, [ $this->verbraucher(20, 1, [0, 3000, 6000], 0), $this->verbraucher( 10, 6, [['Von_W' => 0, 'Bis_W' => 10000]], 0 ), ], 'PrioritaetPV' ); self::assertSame([20 => 3000, 10 => 2000], $ergebnis['Sollwerte']); self::assertSame(0, $ergebnis['Restabweichung_W']); } public function testMonatlicheGrenzeMussVollstaendigSein(): void { $this->expectException(InvalidArgumentException::class); ManagerRegler::wirksameLastspitzengrenze(2, 0.0, [1 => 12000.0], 9); } /** * @param list $leistungswerte * @return array{InstanzID: int, Daten: array} */ private function verbraucher( int $instanzID, int $prioritaet, array $leistungswerte, ?int $istleistung, bool $aenderungMoeglich = true ): array { $quelle = $istleistung === null ? Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN : Nachrichtenvertrag::LEISTUNGSQUELLE_GEMESSEN; return [ 'InstanzID' => $instanzID, 'Daten' => [ 'Kopf' => [ 'Version' => Nachrichtenvertrag::VERSION, 'AbsenderID' => $instanzID, 'EmpfaengerID' => 1, 'Zeitpunkt' => 1788825600, ], 'Betriebsart' => Nachrichtenvertrag::BETRIEBSART_PV, 'PrioritaetPV' => $prioritaet, 'PrioritaetPeak' => $prioritaet, 'Leistungswerte_W' => $leistungswerte, 'AenderungMoeglich' => $aenderungMoeglich, 'Verfuegbar' => true, 'Istleistung_W' => $istleistung, 'Leistungsquelle' => $quelle, 'Zustand' => [[ 'Kennung' => 'Sollleistung_W', 'Art' => 'Sollwert', 'Wert' => $istleistung, 'Einheit' => 'W', ]], ], ]; } }