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.
|
- 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
@@ -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([
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user