diff --git a/CHANGELOG.md b/CHANGELOG.md index af42cbb..00eac8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert. - Easee-Ladestationen laden einen fehlenden Initialstatus ueber die Observations-API nach und starten bei noch unbekannter Ausgangsphase konservativ einphasig. - Wartende Easee-Ladestationen erhalten bei positiver Stromvorgabe einmal pro Fahrzeugverbindung einen Startbefehl; Freigabe- und Blockiergruende werden diagnostiziert. - Fehlende oder veraltete Verbraucher blockieren die Verteilung an aktuelle Verbraucher nicht mehr. +- Verbraucher gleicher Prioritaet werden wie in Enelix 1 anhand ihrer bezogenen Energie in 2-kWh-Gruppen ausgeglichen. - Die Stoerungsueberwachung fordert einen fehlenden Geraetezugang innerhalb ihres Retry-Zyklus automatisch neu an. - Der Warmwassererwaermer verwendet konfigurierbare Temperatur-Stoergrenzen von standardmaessig 0 bis 100 Grad C. - Vollstaendige PHP-, Struktur- und Symcon-Modultests fuer alle Module. diff --git a/Manager/module.php b/Manager/module.php index 9b6af31..883be22 100644 --- a/Manager/module.php +++ b/Manager/module.php @@ -180,6 +180,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle $this->RegisterVariableFloat('Netzleistung', 'Netzleistung', '', 30); $this->RegisterAttributeString('VerbraucherCache', '{}'); + $this->RegisterAttributeString('VerbraucherEnergie', '{}'); $this->RegisterAttributeString('LetzteSollwerte', '{}'); $this->RegisterAttributeString('LetztePvGrenzen', '{}'); $this->RegisterAttributeString('Netzfahrplan', '{}'); @@ -603,15 +604,78 @@ class Manager extends IPSModule implements ManagerSchnittstelle throw new InvalidArgumentException('Der Absender ist diesem Manager nicht aktiv zugeordnet.'); } + $jetzt = time(); + $this->aktualisiereVerbraucherEnergie($absenderID, $daten, $jetzt); + $cache = $this->leseJsonAttribut('VerbraucherCache'); $cache[(string) $absenderID] = [ - 'EmpfangenAm' => time(), + 'EmpfangenAm' => $jetzt, 'Daten' => $daten, ]; $this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR)); $this->regeln(false); } + /** @param array $daten */ + private function aktualisiereVerbraucherEnergie(int $instanzID, array $daten, int $jetzt): void + { + $energien = $this->leseJsonAttribut('VerbraucherEnergie'); + $bisher = $energien[(string) $instanzID] ?? []; + $energieWh = is_array($bisher) && is_numeric($bisher['Energie_Wh'] ?? null) + ? (float) $bisher['Energie_Wh'] + : 0.0; + $letzterZeitpunkt = is_array($bisher) && is_int($bisher['Zeitpunkt'] ?? null) + ? $bisher['Zeitpunkt'] + : 0; + $letzteLeistung = is_array($bisher) && is_numeric($bisher['Leistung_W'] ?? null) + ? (float) $bisher['Leistung_W'] + : 0.0; + + if ($letzterZeitpunkt > 0 && $jetzt > $letzterZeitpunkt) { + $dauer = min( + $jetzt - $letzterZeitpunkt, + $this->ReadPropertyInteger('VerbraucherTimeout') + ); + $energieWh += $letzteLeistung * $dauer / 3600.0; + } + + $energien[(string) $instanzID] = [ + 'Energie_Wh' => $energieWh, + 'Zeitpunkt' => $jetzt, + 'Leistung_W' => $this->verbraucherleistung($daten), + ]; + $this->WriteAttributeString( + 'VerbraucherEnergie', + json_encode($energien, JSON_THROW_ON_ERROR) + ); + } + + /** @param array $daten */ + private function verbraucherleistung(array $daten): float + { + if ($daten['Leistungsquelle'] !== Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN + && is_numeric($daten['Istleistung_W']) + ) { + return (float) $daten['Istleistung_W']; + } + + return (float) (ManagerRegler::angenommenerSollwert($daten) ?? 0); + } + + /** @return array */ + private function bezogeneVerbraucherEnergienWh(): array + { + $ergebnis = []; + foreach ($this->leseJsonAttribut('VerbraucherEnergie') as $instanzID => $eintrag) { + if (!is_array($eintrag) || !is_numeric($eintrag['Energie_Wh'] ?? null)) { + continue; + } + $ergebnis[(int) $instanzID] = (float) $eintrag['Energie_Wh']; + } + + return $ergebnis; + } + private function regeln(bool $erneuern): void { if (!$this->aktualisiereLizenzfreigabe($this->ReadPropertyString('Lizenzcode'), false)) { @@ -699,7 +763,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle $netzleistung, $zielnetzleistung, $verbraucher, - $betriebsart === ManagerRegler::BETRIEBSART_PEAK ? 'PrioritaetPeak' : 'PrioritaetPV' + $betriebsart === ManagerRegler::BETRIEBSART_PEAK ? 'PrioritaetPeak' : 'PrioritaetPV', + $this->bezogeneVerbraucherEnergienWh() ); $this->setzeDiagnosewert('Verteilbudget', (float) $ergebnis['Verteilbudget_W']); $this->setzeDiagnosewert('Verbraucherstatus', json_encode([ diff --git a/docs/module/Manager/README.md b/docs/module/Manager/README.md index 7e4e611..46357eb 100644 --- a/docs/module/Manager/README.md +++ b/docs/module/Manager/README.md @@ -310,8 +310,11 @@ Das gilt auch bei `AenderungMoeglich=false`: Ein fixes Angebot wie `[11000]` wird mit genau `11000 W` zugeteilt. Die Verbraucher werden nach der gemeldeten PV- beziehungsweise Peak-Priorität -und danach stabil nach Instanz-ID sortiert. Zuerst reserviert der Manager für -jeden Verbraucher den kleinsten erlaubten Leistungswert. Nur das danach +sortiert. Bei gleicher Priorität bevorzugt der Manager wie Enelix 1 den +Verbraucher mit der bisher kleineren bezogenen Energie; verglichen wird in +2-kWh-Gruppen. Erst wenn auch diese Gruppe gleich ist, entscheidet stabil die +Instanz-ID. Zuerst reserviert der Manager für jeden Verbraucher den kleinsten +erlaubten Leistungswert. Nur das danach verbleibende Budget wird in Prioritätsreihenfolge aufgefüllt. Einzelwerte und ganzzahlige Bereiche werden direkt verarbeitet; Lücken werden nie durch unzulässige Werte geschlossen. Eine verbleibende Abweichung wird im diff --git a/libs/ManagerRegler.php b/libs/ManagerRegler.php index 7f0d10e..b7f3876 100644 --- a/libs/ManagerRegler.php +++ b/libs/ManagerRegler.php @@ -92,6 +92,7 @@ final class ManagerRegler /** * @param list> $verbraucher Vollstaendige, bereits gepruefte Verbraucherdaten + * @param array $bezogeneEnergieWh Bezogene Energie je Verbraucher in Wh * @return array{ * Verteilbudget_W: int, * AktuelleVerbraucherleistung_W: int, @@ -104,7 +105,8 @@ final class ManagerRegler float $netzleistung, float $zielnetzleistung, array $verbraucher, - string $prioritaetsfeld + string $prioritaetsfeld, + array $bezogeneEnergieWh = [] ): array { if (!in_array($prioritaetsfeld, ['PrioritaetPV', 'PrioritaetPeak'], true)) { throw new InvalidArgumentException('Unbekanntes Prioritaetsfeld.'); @@ -125,15 +127,30 @@ final class ManagerRegler continue; } + $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), 'Leistungswerte_W' => $daten['Leistungswerte_W'], ]; } usort($steuerbar, static function (array $links, array $rechts): int { - return [$links['Prioritaet'], $links['InstanzID']] <=> [$rechts['Prioritaet'], $rechts['InstanzID']]; + return [ + $links['Prioritaet'], + $links['Energiegruppe'], + $links['InstanzID'], + ] <=> [ + $rechts['Prioritaet'], + $rechts['Energiegruppe'], + $rechts['InstanzID'], + ]; }); $verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung); diff --git a/tests/ManagerReglerTest.php b/tests/ManagerReglerTest.php index 5356705..4587c5a 100644 --- a/tests/ManagerReglerTest.php +++ b/tests/ManagerReglerTest.php @@ -113,6 +113,39 @@ final class ManagerReglerTest extends TestCase self::assertSame(500, $ergebnis['Restabweichung_W']); } + public function testGleichePrioritaetBevorzugtWieEnelix1DenVerbraucherMitWenigerEnergie(): 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 testEnergievergleichVerwendetWieEnelix1ZweiKilowattstundenGruppen(): 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 => 3000, 20 => 0], $ergebnis['Sollwerte']); + } + public function testFestwertWirdUnabhaengigVonAenderungMoeglichZugewiesen(): void { $fest = $this->verbraucher(10, 0, [3000], 3000, false);