fix(manager): Energieausgleich aus Enelix 1 wiederherstellen
Tests / test (push) Successful in 54s

This commit is contained in:
dh
2026-09-29 14:14:25 +00:00
parent 37400878b6
commit fdc2007bb1
5 changed files with 125 additions and 6 deletions
+1
View File
@@ -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. - 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. - 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. - 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. - 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. - Der Warmwassererwaermer verwendet konfigurierbare Temperatur-Stoergrenzen von standardmaessig 0 bis 100 Grad C.
- Vollstaendige PHP-, Struktur- und Symcon-Modultests fuer alle Module. - Vollstaendige PHP-, Struktur- und Symcon-Modultests fuer alle Module.
+67 -2
View File
@@ -180,6 +180,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterVariableFloat('Netzleistung', 'Netzleistung', '', 30); $this->RegisterVariableFloat('Netzleistung', 'Netzleistung', '', 30);
$this->RegisterAttributeString('VerbraucherCache', '{}'); $this->RegisterAttributeString('VerbraucherCache', '{}');
$this->RegisterAttributeString('VerbraucherEnergie', '{}');
$this->RegisterAttributeString('LetzteSollwerte', '{}'); $this->RegisterAttributeString('LetzteSollwerte', '{}');
$this->RegisterAttributeString('LetztePvGrenzen', '{}'); $this->RegisterAttributeString('LetztePvGrenzen', '{}');
$this->RegisterAttributeString('Netzfahrplan', '{}'); $this->RegisterAttributeString('Netzfahrplan', '{}');
@@ -603,15 +604,78 @@ class Manager extends IPSModule implements ManagerSchnittstelle
throw new InvalidArgumentException('Der Absender ist diesem Manager nicht aktiv zugeordnet.'); throw new InvalidArgumentException('Der Absender ist diesem Manager nicht aktiv zugeordnet.');
} }
$jetzt = time();
$this->aktualisiereVerbraucherEnergie($absenderID, $daten, $jetzt);
$cache = $this->leseJsonAttribut('VerbraucherCache'); $cache = $this->leseJsonAttribut('VerbraucherCache');
$cache[(string) $absenderID] = [ $cache[(string) $absenderID] = [
'EmpfangenAm' => time(), 'EmpfangenAm' => $jetzt,
'Daten' => $daten, 'Daten' => $daten,
]; ];
$this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR)); $this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR));
$this->regeln(false); $this->regeln(false);
} }
/** @param array<string, mixed> $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<string, mixed> $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<int, float> */
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 private function regeln(bool $erneuern): void
{ {
if (!$this->aktualisiereLizenzfreigabe($this->ReadPropertyString('Lizenzcode'), false)) { if (!$this->aktualisiereLizenzfreigabe($this->ReadPropertyString('Lizenzcode'), false)) {
@@ -699,7 +763,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$netzleistung, $netzleistung,
$zielnetzleistung, $zielnetzleistung,
$verbraucher, $verbraucher,
$betriebsart === ManagerRegler::BETRIEBSART_PEAK ? 'PrioritaetPeak' : 'PrioritaetPV' $betriebsart === ManagerRegler::BETRIEBSART_PEAK ? 'PrioritaetPeak' : 'PrioritaetPV',
$this->bezogeneVerbraucherEnergienWh()
); );
$this->setzeDiagnosewert('Verteilbudget', (float) $ergebnis['Verteilbudget_W']); $this->setzeDiagnosewert('Verteilbudget', (float) $ergebnis['Verteilbudget_W']);
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([ $this->setzeDiagnosewert('Verbraucherstatus', json_encode([
+5 -2
View File
@@ -310,8 +310,11 @@ Das gilt auch bei `AenderungMoeglich=false`: Ein fixes Angebot wie `[11000]`
wird mit genau `11000 W` zugeteilt. wird mit genau `11000 W` zugeteilt.
Die Verbraucher werden nach der gemeldeten PV- beziehungsweise Peak-Priorität Die Verbraucher werden nach der gemeldeten PV- beziehungsweise Peak-Priorität
und danach stabil nach Instanz-ID sortiert. Zuerst reserviert der Manager für sortiert. Bei gleicher Priorität bevorzugt der Manager wie Enelix 1 den
jeden Verbraucher den kleinsten erlaubten Leistungswert. Nur das danach 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 verbleibende Budget wird in Prioritätsreihenfolge aufgefüllt. Einzelwerte und
ganzzahlige Bereiche werden direkt verarbeitet; Lücken werden nie durch ganzzahlige Bereiche werden direkt verarbeitet; Lücken werden nie durch
unzulässige Werte geschlossen. Eine verbleibende Abweichung wird im unzulässige Werte geschlossen. Eine verbleibende Abweichung wird im
+19 -2
View File
@@ -92,6 +92,7 @@ final class ManagerRegler
/** /**
* @param list<array<string, mixed>> $verbraucher Vollstaendige, bereits gepruefte Verbraucherdaten * @param list<array<string, mixed>> $verbraucher Vollstaendige, bereits gepruefte Verbraucherdaten
* @param array<int, float|int> $bezogeneEnergieWh Bezogene Energie je Verbraucher in Wh
* @return array{ * @return array{
* Verteilbudget_W: int, * Verteilbudget_W: int,
* AktuelleVerbraucherleistung_W: int, * AktuelleVerbraucherleistung_W: int,
@@ -104,7 +105,8 @@ final class ManagerRegler
float $netzleistung, float $netzleistung,
float $zielnetzleistung, float $zielnetzleistung,
array $verbraucher, array $verbraucher,
string $prioritaetsfeld string $prioritaetsfeld,
array $bezogeneEnergieWh = []
): array { ): array {
if (!in_array($prioritaetsfeld, ['PrioritaetPV', 'PrioritaetPeak'], true)) { if (!in_array($prioritaetsfeld, ['PrioritaetPV', 'PrioritaetPeak'], true)) {
throw new InvalidArgumentException('Unbekanntes Prioritaetsfeld.'); throw new InvalidArgumentException('Unbekanntes Prioritaetsfeld.');
@@ -125,15 +127,30 @@ final class ManagerRegler
continue; 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[] = [ $steuerbar[] = [
'InstanzID' => $eintrag['InstanzID'], 'InstanzID' => $eintrag['InstanzID'],
'Prioritaet' => $daten[$prioritaetsfeld], 'Prioritaet' => $daten[$prioritaetsfeld],
'Energiegruppe' => (int) round((float) $energie / 2000.0),
'Leistungswerte_W' => $daten['Leistungswerte_W'], 'Leistungswerte_W' => $daten['Leistungswerte_W'],
]; ];
} }
usort($steuerbar, static function (array $links, array $rechts): int { 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); $verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung);
+33
View File
@@ -113,6 +113,39 @@ final class ManagerReglerTest extends TestCase
self::assertSame(500, $ergebnis['Restabweichung_W']); 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 public function testFestwertWirdUnabhaengigVonAenderungMoeglichZugewiesen(): void
{ {
$fest = $this->verbraucher(10, 0, [3000], 3000, false); $fest = $this->verbraucher(10, 0, [3000], 3000, false);