From cf92e7623ba969f35f75876e46cdb08b1f71eb5a Mon Sep 17 00:00:00 2001 From: dh_Agent Date: Wed, 30 Sep 2026 14:38:42 +0200 Subject: [PATCH] fix(manager): Fahrplanvorgabe linear interpolieren --- Manager/module.php | 27 +++------------ libs/ManagerRegler.php | 65 +++++++++++++++++++++++++++++++++++++ tests/ManagerReglerTest.php | 53 ++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 23 deletions(-) diff --git a/Manager/module.php b/Manager/module.php index eb6e07a..1090d94 100644 --- a/Manager/module.php +++ b/Manager/module.php @@ -2757,29 +2757,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle return null; } - $jetzt = time(); - $naechsterAbstand = PHP_INT_MAX; - $vorgabe = null; - foreach ($fahrplan['points'] 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']); - if ($zeitpunkt === false) { - continue; - } - $abstand = abs($zeitpunkt - $jetzt); - if ($abstand < $naechsterAbstand) { - $naechsterAbstand = $abstand; - $vorgabe = (float) $punkt['gridTargetW']; - } - } - - return $naechsterAbstand <= 1800 ? $vorgabe : null; + return ManagerRegler::interpolierteFahrplanVorgabe( + $fahrplan['points'], + time() + ); } private function synchronisiereAnlagentopologie(): void diff --git a/libs/ManagerRegler.php b/libs/ManagerRegler.php index c7aff6b..579b2c8 100644 --- a/libs/ManagerRegler.php +++ b/libs/ManagerRegler.php @@ -19,6 +19,71 @@ final class ManagerRegler public const BETRIEBSART_PV = Nachrichtenvertrag::BETRIEBSART_PV; public const BETRIEBSART_PEAK = Nachrichtenvertrag::BETRIEBSART_PEAK; + /** + * @param list> $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 */ diff --git a/tests/ManagerReglerTest.php b/tests/ManagerReglerTest.php index d10210a..5164685 100644 --- a/tests/ManagerReglerTest.php +++ b/tests/ManagerReglerTest.php @@ -11,6 +11,59 @@ use PHPUnit\Framework\TestCase; final class ManagerReglerTest extends TestCase { + public function testFahrplanVorgabeWirdZwischenPunktenLinearInterpoliert(): void + { + $punkte = [ + ['time' => '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(