This commit is contained in:
@@ -18,6 +18,8 @@ Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
|
||||
- Stufenlose Leistungsbereiche fuer Batteriespeicher.
|
||||
- Ladefreigabe neuer Ladestationsinstanzen standardmaessig aktiviert.
|
||||
- Easee-Ladestationen laden einen fehlenden Initialstatus ueber die Observations-API nach und starten bei noch unbekannter Ausgangsphase konservativ einphasig.
|
||||
- Fehlende oder veraltete Verbraucher blockieren die Verteilung an aktuelle Verbraucher nicht mehr.
|
||||
- Die Stoerungsueberwachung fordert einen fehlenden Geraetezugang innerhalb ihres Retry-Zyklus automatisch neu an.
|
||||
- Vollstaendige PHP-, Struktur- und Symcon-Modultests fuer alle Module.
|
||||
|
||||
### Hinweise
|
||||
|
||||
+17
-20
@@ -689,22 +689,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
);
|
||||
$this->setzeDiagnosewert('WirksameLastspitzengrenze', $lastspitzengrenze ?? 0.0);
|
||||
$this->regleEinspeisung($netzleistung, $stoerungen, $erneuern);
|
||||
if (!$synchronisiert) {
|
||||
$this->setzeDiagnosewert('Verteilbudget', 0.0);
|
||||
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
|
||||
'Betriebsart' => $betriebsart,
|
||||
'Synchronisiert' => false,
|
||||
'Verbraucher' => $verbraucher,
|
||||
'NichtGeregelt' => $nichtGeregelt,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
$this->setzeDiagnosewert(
|
||||
'NichtGeregelteGeraete',
|
||||
$this->nichtGeregelteGeraeteText($nichtGeregelt)
|
||||
);
|
||||
$this->aktualisiereStoerungen($verbraucher, $stoerungen);
|
||||
$this->SetStatus(self::STATUS_AKTIV);
|
||||
return;
|
||||
}
|
||||
|
||||
$fahrplanVorgabe = $this->aktuelleNetzfahrplanVorgabe();
|
||||
$zielnetzleistung = $betriebsart === ManagerRegler::BETRIEBSART_PEAK
|
||||
@@ -719,14 +703,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->setzeDiagnosewert('Verteilbudget', (float) $ergebnis['Verteilbudget_W']);
|
||||
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
|
||||
'Betriebsart' => $betriebsart,
|
||||
'Synchronisiert' => true,
|
||||
'Synchronisiert' => $synchronisiert,
|
||||
'Restabweichung_W' => $ergebnis['Restabweichung_W'],
|
||||
'Verbraucher' => $verbraucher,
|
||||
'NichtGeregelt' => [],
|
||||
'NichtGeregelt' => $nichtGeregelt,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
$this->setzeDiagnosewert(
|
||||
'NichtGeregelteGeraete',
|
||||
'Alle aktiven Verbraucher werden geregelt.'
|
||||
$nichtGeregelt === []
|
||||
? 'Alle aktiven Verbraucher werden geregelt.'
|
||||
: $this->nichtGeregelteGeraeteText($nichtGeregelt)
|
||||
);
|
||||
|
||||
$letzteSollwerte = $this->leseJsonAttribut('LetzteSollwerte');
|
||||
@@ -1450,7 +1436,18 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
|
||||
$token = trim($this->ReadAttributeString('PrognoseInstallationsToken'));
|
||||
if ($token === '') {
|
||||
throw new RuntimeException('Der Geraetezugang fuer die Stoerueberwachung fehlt.');
|
||||
$this->aktualisiereLizenzfreigabe(
|
||||
$this->ReadPropertyString('Lizenzcode'),
|
||||
true
|
||||
);
|
||||
$token = trim($this->ReadAttributeString('PrognoseInstallationsToken'));
|
||||
}
|
||||
if ($token === '') {
|
||||
$lizenzfehler = trim($this->ReadAttributeString('LizenzLetzterFehler'));
|
||||
throw new RuntimeException(
|
||||
'Der Geraetezugang fuer die Stoerueberwachung fehlt.'
|
||||
. ($lizenzfehler !== '' ? ' Lizenzpruefung: ' . $lizenzfehler : '')
|
||||
);
|
||||
}
|
||||
$snapshot['capturedAt'] = gmdate('Y-m-d\TH:i:s\Z', $jetzt);
|
||||
$this->uebertrageStoerungsSnapshot(
|
||||
|
||||
@@ -43,12 +43,16 @@ Ein Betriebsartwechsel wird synchronisiert:
|
||||
3. `null` ist nur eine Ankuendigung und kein Schaltbefehl.
|
||||
4. Jeder Verbraucher berechnet sein zustandsabhaengiges Leistungsangebot neu
|
||||
und meldet es mit derselben Betriebsart zurueck.
|
||||
5. Erst wenn alle aktiven Verbraucher synchronisiert sind, verteilt der
|
||||
Manager konkrete Sollleistungen.
|
||||
5. Der Manager verteilt konkrete Sollleistungen an alle bereits
|
||||
synchronisierten Verbraucher.
|
||||
6. Fehlende, veraltete oder noch nicht umgeschaltete Verbraucher bleiben von
|
||||
der Verteilung ausgeschlossen und werden als Stoerung ausgewiesen.
|
||||
|
||||
Der Manager verwendet damit nie ein PV-Angebot fuer Peak oder umgekehrt. Eine
|
||||
technische Schaltsperre, eine ausstehende Rueckmeldung oder eine Mindestzeit
|
||||
kann das Angebot weiterhin auf die aktuell gehaltene Leistung begrenzen.
|
||||
Der Manager verwendet damit nie ein PV-Angebot fuer Peak oder umgekehrt. Ein
|
||||
nicht antwortender Verbraucher blockiert die Regelung der uebrigen Anlage
|
||||
nicht; sein aktueller Einfluss ist bereits in der Netzleistungsmessung
|
||||
enthalten. Eine technische Schaltsperre oder eine Mindestzeit kann das Angebot
|
||||
weiterhin auf die aktuell gehaltene Leistung begrenzen.
|
||||
|
||||
| Verbraucher | PV-Angebot | Peak-Angebot |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -78,8 +78,11 @@ zulaessig ist.
|
||||
`Sollleistung_W=null`.
|
||||
4. Jeder Verbraucher berechnet und meldet seine PowerSteps fuer diese
|
||||
Betriebsart.
|
||||
5. Erst wenn alle aktiven Verbraucher synchronisiert sind, verteilt der Manager
|
||||
Sollleistungen.
|
||||
5. Der Manager verteilt Sollleistungen an alle bereits synchronisierten
|
||||
Verbraucher.
|
||||
6. Fehlende, veraltete oder noch nicht umgeschaltete Verbraucher werden nicht
|
||||
angesteuert und als Stoerung ausgewiesen; sie blockieren die aktuellen
|
||||
Verbraucher nicht.
|
||||
|
||||
Damit kann jeder Verbrauchertyp unterschiedliche Angebote fuer PV und Peak
|
||||
melden, ohne dass der Manager seine interne Geraetelogik kennen muss.
|
||||
|
||||
@@ -254,6 +254,25 @@ final class ManagerModulstrukturTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testManagerRegeltAktuelleVerbraucherTrotzFehlenderRueckmeldungen(): void
|
||||
{
|
||||
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||
|
||||
self::assertStringNotContainsString("if (!\$synchronisiert) {", $inhalt);
|
||||
self::assertStringContainsString(
|
||||
"'Synchronisiert' => \$synchronisiert",
|
||||
$inhalt
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
"'NichtGeregelt' => \$nichtGeregelt",
|
||||
$inhalt
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
"\$ergebnis = ManagerRegler::verteile(",
|
||||
$inhalt
|
||||
);
|
||||
}
|
||||
|
||||
public function testManagerUebertraegtStoerungsvollsnapshotsMitRetry(): void
|
||||
{
|
||||
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||
@@ -280,6 +299,13 @@ final class ManagerModulstrukturTest extends TestCase
|
||||
'"name":"StoermeldeAnschluss"',
|
||||
str_replace(' ', '', (string) file_get_contents(__DIR__ . '/../Manager/form.json'))
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
"if (\$token === '') {\n"
|
||||
. " \$this->aktualisiereLizenzfreigabe(\n"
|
||||
. " \$this->ReadPropertyString('Lizenzcode'),\n"
|
||||
. " true",
|
||||
$inhalt
|
||||
);
|
||||
self::assertGreaterThanOrEqual(
|
||||
3,
|
||||
substr_count(
|
||||
|
||||
@@ -125,6 +125,34 @@ final class ManagerReglerTest extends TestCase
|
||||
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||
}
|
||||
|
||||
public function testVeralteterVerbraucherBlockiertUeberschussverteilungNicht(): void
|
||||
{
|
||||
$ergebnis = ManagerRegler::verteile(
|
||||
-2411.0,
|
||||
0.0,
|
||||
[
|
||||
$this->verbraucher(
|
||||
44234,
|
||||
0,
|
||||
[['Von_W' => -39000, 'Bis_W' => 39000]],
|
||||
0
|
||||
),
|
||||
$this->verbraucher(38676, 1, [0, 4600], 0),
|
||||
$this->verbraucher(29743, 1, [0, 6500], 0),
|
||||
$this->verbraucher(43878, 2, [13850], 10151, false),
|
||||
$this->verbraucher(46052, 2, [11000], 0, false),
|
||||
],
|
||||
'PrioritaetPV'
|
||||
);
|
||||
|
||||
self::assertSame(12562, $ergebnis['Verteilbudget_W']);
|
||||
self::assertSame(
|
||||
[44234 => 2411, 29743 => 0, 38676 => 0],
|
||||
$ergebnis['Sollwerte']
|
||||
);
|
||||
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||
}
|
||||
|
||||
public function testFehlendeIstleistungFaelltAufAngenommenenSollwertZurueck(): void
|
||||
{
|
||||
$verbraucher = $this->verbraucher(10, 0, [0, 3000], null, false);
|
||||
|
||||
@@ -444,4 +444,46 @@ return static function (TestContext $test): void {
|
||||
$test->objectByIdent('Sammelstoerung', $managerId)
|
||||
));
|
||||
});
|
||||
|
||||
$test->runCase('Veralteter Verbraucher blockiert aktuelle Verbraucher nicht', static function (
|
||||
TestContext $test
|
||||
) use (
|
||||
$managerId,
|
||||
$netzleistungId,
|
||||
$einStufigId,
|
||||
$warmwasserId,
|
||||
$pufferspeicherId,
|
||||
$melden,
|
||||
$verbraucherstatus
|
||||
): void {
|
||||
IPS_SetProperty($managerId, 'VerbraucherTimeout', 1);
|
||||
IPS_ApplyChanges($managerId);
|
||||
SetValue($netzleistungId, -2400.0);
|
||||
$melden();
|
||||
IPS_RequestAction($managerId, 'Regeln', true);
|
||||
$test->waitUntil(
|
||||
static fn (): bool => ($verbraucherstatus()['Synchronisiert'] ?? false) === true
|
||||
);
|
||||
|
||||
IPS_Sleep(1100);
|
||||
IPS_RequestAction($warmwasserId, 'Melden', true);
|
||||
IPS_RequestAction($pufferspeicherId, 'Melden', true);
|
||||
IPS_RequestAction($managerId, 'Regeln', true);
|
||||
|
||||
$daten = $verbraucherstatus();
|
||||
$test->assertSame(false, $daten['Synchronisiert'] ?? null);
|
||||
$test->assertSame(2, count($daten['Verbraucher'] ?? []));
|
||||
$test->assertSame(1, count($daten['NichtGeregelt'] ?? []));
|
||||
$test->assertSame(
|
||||
$einStufigId,
|
||||
$daten['NichtGeregelt'][0]['InstanzID'] ?? null
|
||||
);
|
||||
$test->assertSame(
|
||||
'Meldung ist veraltet',
|
||||
$daten['NichtGeregelt'][0]['Grund'] ?? null
|
||||
);
|
||||
$test->assertTrue(
|
||||
(float) GetValue($test->objectByIdent('Verteilbudget', $managerId)) > 0.0
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user