Compare commits
2
Commits
ace1821213
...
fdc2007bb1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc2007bb1 | ||
|
|
37400878b6 |
@@ -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
|
||||
|
||||
@@ -53,11 +53,16 @@ final class EaseeLadestatus
|
||||
? self::phasenzahl((int) ($beobachtungen['110'] ?? 0))
|
||||
: 0;
|
||||
|
||||
$leistung = max(0.0, (float) ($beobachtungen['120'] ?? 0.0) * 1000.0);
|
||||
$laedt = $betriebszustand === 3;
|
||||
$leistung = $laedt
|
||||
? max(0.0, (float) ($beobachtungen['120'] ?? 0.0) * 1000.0)
|
||||
: 0.0;
|
||||
$stroeme = [];
|
||||
foreach ([182, 183, 184, 185] as $id) {
|
||||
if (array_key_exists((string) $id, $beobachtungen)) {
|
||||
$stroeme[] = abs((float) $beobachtungen[(string) $id]);
|
||||
if ($laedt) {
|
||||
foreach ([182, 183, 184, 185] as $id) {
|
||||
if (array_key_exists((string) $id, $beobachtungen)) {
|
||||
$stroeme[] = abs((float) $beobachtungen[(string) $id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$ladestrom = $stroeme === [] ? 0.0 : max($stroeme);
|
||||
|
||||
+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);
|
||||
|
||||
@@ -77,6 +77,33 @@ final class EaseeLadestatusTest extends TestCase
|
||||
self::assertSame(0, $status['Phasenzahl']);
|
||||
}
|
||||
|
||||
public function testStillstehendeWallboxSetztVeralteteLeistungUndStromAufNull(): void
|
||||
{
|
||||
$status = EaseeLadestatus::ausBeobachtungen([
|
||||
'109' => 2,
|
||||
'110' => 30,
|
||||
'120' => 10.294,
|
||||
'182' => 14.9,
|
||||
'183' => 14.8,
|
||||
'184' => 14.7,
|
||||
], 32);
|
||||
|
||||
self::assertEqualsWithDelta(0.0, $status['Istleistung_W'], 0.01);
|
||||
self::assertEqualsWithDelta(0.0, $status['Ladestrom_A'], 0.01);
|
||||
}
|
||||
|
||||
public function testFertigGeladeneWallboxSetztVeralteteLeistungAufNull(): void
|
||||
{
|
||||
$status = EaseeLadestatus::ausBeobachtungen([
|
||||
'109' => 4,
|
||||
'110' => 30,
|
||||
'120' => 10.294,
|
||||
], 32);
|
||||
|
||||
self::assertTrue($status['FahrzeugGeladen']);
|
||||
self::assertEqualsWithDelta(0.0, $status['Istleistung_W'], 0.01);
|
||||
}
|
||||
|
||||
public function testApiUndKabelgrenzenBegrenzenDenKonfiguriertenStrom(): void
|
||||
{
|
||||
$status = EaseeLadestatus::ausBeobachtungen([
|
||||
|
||||
@@ -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