This commit is contained in:
@@ -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.
|
||||
|
||||
+67
-2
@@ -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<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
|
||||
{
|
||||
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([
|
||||
|
||||
@@ -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
|
||||
|
||||
+19
-2
@@ -92,6 +92,7 @@ final class ManagerRegler
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $verbraucher Vollstaendige, bereits gepruefte Verbraucherdaten
|
||||
* @param array<int, float|int> $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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user