> $punkte */ public static function interpolierteFahrplanVorgabe( array $punkte, int $jetzt, int $maximalerPunktabstandSekunden = 1800 ): ?float { if ($maximalerPunktabstandSekunden < 0) { throw new InvalidArgumentException('Maximaler Fahrplanpunktabstand darf nicht negativ sein.'); } $gueltigePunkte = []; foreach ($punkte as $punkt) { if (!is_array($punkt) || !is_string($punkt['time'] ?? null) || (!is_int($punkt['gridTargetW'] ?? null) && !is_float($punkt['gridTargetW'] ?? null)) ) { continue; } $zeitpunkt = strtotime($punkt['time']); $vorgabe = (float) $punkt['gridTargetW']; if ($zeitpunkt === false || !is_finite($vorgabe)) { continue; } $gueltigePunkte[$zeitpunkt] = $vorgabe; } if ($gueltigePunkte === []) { return null; } ksort($gueltigePunkte, SORT_NUMERIC); $vorherigerZeitpunkt = null; $vorherigeVorgabe = null; foreach ($gueltigePunkte as $zeitpunkt => $vorgabe) { if ($zeitpunkt === $jetzt) { return $vorgabe; } if ($zeitpunkt > $jetzt) { if ($vorherigerZeitpunkt === null) { return $zeitpunkt - $jetzt <= $maximalerPunktabstandSekunden ? $vorgabe : null; } if ($jetzt - $vorherigerZeitpunkt > $maximalerPunktabstandSekunden || $zeitpunkt - $jetzt > $maximalerPunktabstandSekunden ) { return null; } $anteil = ($jetzt - $vorherigerZeitpunkt) / ($zeitpunkt - $vorherigerZeitpunkt); return $vorherigeVorgabe + (($vorgabe - $vorherigeVorgabe) * $anteil); } $vorherigerZeitpunkt = $zeitpunkt; $vorherigeVorgabe = $vorgabe; } return $vorherigerZeitpunkt !== null && $jetzt - $vorherigerZeitpunkt <= $maximalerPunktabstandSekunden ? $vorherigeVorgabe : null; } /** * @param array $monatsgrenzen Monatsnummer 1 bis 12 als Schluessel */ public static function wirksameLastspitzengrenze( int $modus, float $konstanteGrenze, array $monatsgrenzen, int $monat ): ?float { if ($modus === 0) { return null; } if ($modus === 1) { return $konstanteGrenze; } if ($modus !== 2) { throw new InvalidArgumentException('Lastspitzenmodus muss 0, 1 oder 2 sein.'); } if ($monat < 1 || $monat > 12 || !array_key_exists($monat, $monatsgrenzen)) { throw new InvalidArgumentException('Fuer den aktuellen Monat fehlt eine Lastspitzengrenze.'); } return (float) $monatsgrenzen[$monat]; } public static function bestimmeBetriebsart( bool $aktiv, bool $netzleistungGueltig, float $netzleistung, string $bisherigeBetriebsart, bool $peakAktiv, float $sollwertSolarladen, float $wirksameLastspitzengrenze, float $umschaltdifferenzProzent ): string { if (!$aktiv || !$netzleistungGueltig) { return self::BETRIEBSART_INAKTIV; } if (!$peakAktiv) { return self::BETRIEBSART_PV; } if ($umschaltdifferenzProzent < 0 || $umschaltdifferenzProzent > 100) { throw new InvalidArgumentException('Umschaltdifferenz muss zwischen 0 und 100 Prozent liegen.'); } $kandidat = $bisherigeBetriebsart; if ($netzleistung < $sollwertSolarladen) { $kandidat = self::BETRIEBSART_PV; } elseif ($netzleistung > $wirksameLastspitzengrenze) { $kandidat = self::BETRIEBSART_PEAK; } elseif (!in_array($kandidat, [self::BETRIEBSART_PV, self::BETRIEBSART_PEAK], true)) { $kandidat = self::BETRIEBSART_PV; } if ($kandidat === $bisherigeBetriebsart || !in_array($bisherigeBetriebsart, [self::BETRIEBSART_PV, self::BETRIEBSART_PEAK], true) || $umschaltdifferenzProzent === 0.0 ) { return $kandidat; } $pvKorrektur = $sollwertSolarladen - $netzleistung; $peakKorrektur = $wirksameLastspitzengrenze - $netzleistung; $bezugswert = max(abs($pvKorrektur), abs($peakKorrektur), 1.0); $differenzProzent = abs($pvKorrektur - $peakKorrektur) / $bezugswert * 100.0; return $differenzProzent >= $umschaltdifferenzProzent ? $kandidat : $bisherigeBetriebsart; } /** * @param list> $verbraucher Vollstaendige, bereits gepruefte Verbraucherdaten * @param array $bezogeneEnergieWh Bezogene Energie je Verbraucher in Wh * @return array{ * Verteilbudget_W: int, * AktuelleVerbraucherleistung_W: int, * NichtSteuerbar_W: int, * Sollwerte: array, * Restabweichung_W: int * } */ public static function verteile( float $netzleistung, float $zielnetzleistung, array $verbraucher, string $prioritaetsfeld, array $bezogeneEnergieWh = [] ): array { if (!in_array($prioritaetsfeld, ['PrioritaetPV', 'PrioritaetPeak'], true)) { throw new InvalidArgumentException('Unbekanntes Prioritaetsfeld.'); } $aktuelleLeistung = 0; $nichtSteuerbar = 0; $steuerbar = []; foreach ($verbraucher as $eintrag) { self::pruefeVerteilEintrag($eintrag, $prioritaetsfeld); $leistung = self::aktuelleLeistung($eintrag['Daten']); $aktuelleLeistung += $leistung; $daten = $eintrag['Daten']; if (!$daten['Verfuegbar'] || $daten['Leistungswerte_W'] === []) { $nichtSteuerbar += $leistung; continue; } $festwert = self::istFestwert($daten['Leistungswerte_W']); $festgehalten = !$daten['AenderungMoeglich']; if ($festwert || $festgehalten) { $nichtSteuerbar += $leistung; } $energie = $bezogeneEnergieWh[$eintrag['InstanzID']] ?? 0.0; if ((!is_int($energie) && !is_float($energie)) || (is_float($energie) && !is_finite($energie)) ) { throw new InvalidArgumentException('Bezogene Energie muss eine endliche Zahl sein.'); } $steuerbar[] = [ 'InstanzID' => $eintrag['InstanzID'], 'Prioritaet' => $daten[$prioritaetsfeld], 'Energiegruppe' => (int) round((float) $energie / 2000.0), 'Energie_Wh' => (float) $energie, 'Leistungswerte_W' => $daten['Leistungswerte_W'], 'Festwert' => $festwert, 'Festgehalten' => $festgehalten, 'Sollwert' => self::angenommenerSollwert($daten), ]; } usort($steuerbar, static function (array $links, array $rechts): int { return [ $links['Prioritaet'], $links['Energiegruppe'], $links['InstanzID'], ] <=> [ $rechts['Prioritaet'], $rechts['Energiegruppe'], $rechts['InstanzID'], ]; }); $verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung); $positiveRichtung = $verteilbudget - $nichtSteuerbar >= 0; $sollwerte = []; $ausgangssumme = 0; foreach ($steuerbar as $eintrag) { if ($eintrag['Festwert']) { $sollwerte[$eintrag['InstanzID']] = self::kleinsterLeistungswert( $eintrag['Leistungswerte_W'] ); continue; } if ($eintrag['Festgehalten']) { if ($eintrag['Sollwert'] !== null) { $sollwerte[$eintrag['InstanzID']] = $eintrag['Sollwert']; } continue; } $ausgangswert = self::richtungsneutralerLeistungswert( $eintrag['Leistungswerte_W'], $positiveRichtung ); $sollwerte[$eintrag['InstanzID']] = $ausgangswert; $ausgangssumme += $ausgangswert; } $rest = $verteilbudget - $nichtSteuerbar - $ausgangssumme; $gruppen = []; foreach ($steuerbar as $eintrag) { if ($eintrag['Festwert'] || $eintrag['Festgehalten']) { continue; } $instanzID = $eintrag['InstanzID']; $ausgangswert = $sollwerte[$instanzID]; if ($positiveRichtung) { $gruppen[$eintrag['Prioritaet']][] = $eintrag; continue; } else { if ($rest >= 0) { continue; } $neuerSollwert = self::kleinsterLeistungswertAb( $eintrag['Leistungswerte_W'], $ausgangswert + $rest ); } $rest -= $neuerSollwert - $ausgangswert; $sollwerte[$instanzID] = $neuerSollwert; } foreach ($gruppen as $gruppe) { self::verteilePositiveGruppe($gruppe, $sollwerte, $rest); } $zugeteilt = $nichtSteuerbar; foreach ($steuerbar as $eintrag) { if (!$eintrag['Festwert'] && !$eintrag['Festgehalten']) { $zugeteilt += $sollwerte[$eintrag['InstanzID']]; } } return [ 'Verteilbudget_W' => $verteilbudget, 'AktuelleVerbraucherleistung_W' => $aktuelleLeistung, 'NichtSteuerbar_W' => $nichtSteuerbar, 'Sollwerte' => $sollwerte, 'Restabweichung_W' => $verteilbudget - $zugeteilt, ]; } /** * @param list> $gruppe Verbraucher derselben Prioritaet * @param array $sollwerte */ private static function verteilePositiveGruppe(array $gruppe, array &$sollwerte, int &$rest): void { while ($rest > 0) { $kandidaten = self::naechsteLeistungsschritte($gruppe, $sollwerte, $rest); if ($kandidaten === []) { return; } // Ganze Leistungsniveaus gemeinsam vergeben, ohne Bereiche Watt fuer Watt zu durchlaufen. $unten = $kandidaten[0]['NaechsteLeistung_W'] - 1; $oben = $unten; foreach ($kandidaten as $eintrag) { $oben = max($oben, $sollwerte[$eintrag['InstanzID']] + $rest); } while ($unten < $oben) { $mitte = $unten + intdiv($oben - $unten + 1, 2); $bedarf = 0; foreach ($kandidaten as $eintrag) { $bisher = $sollwerte[$eintrag['InstanzID']]; $bedarf += max(0, self::groessterLeistungswertBis( $eintrag['Leistungswerte_W'], $mitte ) - $bisher); if ($bedarf > $rest) { break; } } if ($bedarf <= $rest) { $unten = $mitte; } else { $oben = $mitte - 1; } } foreach ($kandidaten as $eintrag) { $id = $eintrag['InstanzID']; $neu = max($sollwerte[$id], self::groessterLeistungswertBis( $eintrag['Leistungswerte_W'], $unten )); $rest -= $neu - $sollwerte[$id]; $sollwerte[$id] = $neu; } // Reicht es nicht fuer das ganze naechste Niveau, entscheidet Leistung, Energie, ID. $kandidaten = self::naechsteLeistungsschritte($gruppe, $sollwerte, $rest); if ($kandidaten === []) { return; } $erster = $kandidaten[0]; $id = $erster['InstanzID']; $rest -= $erster['NaechsteLeistung_W'] - $sollwerte[$id]; $sollwerte[$id] = $erster['NaechsteLeistung_W']; } } /** * @param list> $gruppe * @param array $sollwerte * @return list> */ private static function naechsteLeistungsschritte(array $gruppe, array $sollwerte, int $rest): array { $kandidaten = []; foreach ($gruppe as $eintrag) { $bisher = $sollwerte[$eintrag['InstanzID']]; foreach ($eintrag['Leistungswerte_W'] as $angebot) { $naechste = is_int($angebot) ? $angebot : max($angebot['Von_W'], $bisher + 1); if ($naechste <= $bisher || (!is_int($angebot) && $naechste > $angebot['Bis_W'])) { continue; } if ($naechste - $bisher <= $rest) { $eintrag['NaechsteLeistung_W'] = $naechste; $kandidaten[] = $eintrag; } break; } } usort($kandidaten, static function (array $links, array $rechts): int { return [$links['NaechsteLeistung_W'], $links['Energie_Wh'], $links['InstanzID']] <=> [$rechts['NaechsteLeistung_W'], $rechts['Energie_Wh'], $rechts['InstanzID']]; }); return $kandidaten; } /** * @param array $daten */ public static function angenommenerSollwert(array $daten): ?int { foreach ($daten['Zustand'] as $zustand) { if ($zustand['Kennung'] === 'Sollleistung_W') { return is_int($zustand['Wert']) ? $zustand['Wert'] : null; } } return null; } /** * @param array $daten */ private static function aktuelleLeistung(array $daten): int { if ($daten['Leistungsquelle'] !== Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN && is_numeric($daten['Istleistung_W']) ) { return (int) round((float) $daten['Istleistung_W']); } return self::angenommenerSollwert($daten) ?? 0; } /** * @param array $eintrag */ private static function pruefeVerteilEintrag(array $eintrag, string $prioritaetsfeld): void { if (!isset($eintrag['InstanzID'], $eintrag['Daten']) || !is_int($eintrag['InstanzID']) || $eintrag['InstanzID'] <= 0 || !is_array($eintrag['Daten']) ) { throw new InvalidArgumentException('Verbrauchereintrag ist ungueltig.'); } Nachrichtenvertrag::pruefeVerbraucherdaten($eintrag['Daten']); if (!array_key_exists($prioritaetsfeld, $eintrag['Daten'])) { throw new InvalidArgumentException('Prioritaet fehlt im Verbrauchereintrag.'); } } /** * Ein einzelner Leistungswert bleibt als Befehl erhalten. Fuer die * Leistungsbilanz zaehlt jedoch die gemessene Istleistung. * * @param list $leistungswerte */ private static function istFestwert(array $leistungswerte): bool { return count($leistungswerte) === 1 && is_int($leistungswerte[0]); } /** * @param list $leistungswerte */ private static function richtungsneutralerLeistungswert( array $leistungswerte, bool $positiveRichtung ): int { $negativ = null; $positiv = null; foreach ($leistungswerte as $eintrag) { if (is_int($eintrag)) { if ($eintrag === 0) { return 0; } $negativ = $eintrag < 0 ? max($negativ ?? $eintrag, $eintrag) : $negativ; $positiv = $eintrag > 0 ? min($positiv ?? $eintrag, $eintrag) : $positiv; continue; } if ($eintrag['Von_W'] <= 0 && $eintrag['Bis_W'] >= 0) { return 0; } $negativ = $eintrag['Bis_W'] < 0 ? max($negativ ?? $eintrag['Bis_W'], $eintrag['Bis_W']) : $negativ; $positiv = $eintrag['Von_W'] > 0 ? min($positiv ?? $eintrag['Von_W'], $eintrag['Von_W']) : $positiv; } if ($positiveRichtung) { return $positiv ?? $negativ; } return $negativ ?? $positiv; } /** * @param list $leistungswerte */ private static function kleinsterLeistungswert(array $leistungswerte): int { $erster = $leistungswerte[0]; return is_int($erster) ? $erster : $erster['Von_W']; } /** * @param list $leistungswerte */ private static function groessterLeistungswertBis(array $leistungswerte, int $obergrenze): int { $gewaehlt = self::kleinsterLeistungswert($leistungswerte); foreach ($leistungswerte as $eintrag) { if (is_int($eintrag)) { if ($eintrag > $obergrenze) { break; } $gewaehlt = $eintrag; continue; } if ($eintrag['Von_W'] > $obergrenze) { break; } $gewaehlt = min($eintrag['Bis_W'], $obergrenze); } return $gewaehlt; } /** * @param list $leistungswerte */ private static function kleinsterLeistungswertAb(array $leistungswerte, int $untergrenze): int { foreach ($leistungswerte as $eintrag) { if (is_int($eintrag)) { if ($eintrag >= $untergrenze) { return $eintrag; } continue; } if ($eintrag['Bis_W'] < $untergrenze) { continue; } return max($eintrag['Von_W'], $untergrenze); } return self::richtungsneutralerLeistungswert($leistungswerte, false); } }