fix(manager): Regelung bei veralteten Verbrauchern fortsetzen
Tests / test (push) Successful in 46s

This commit is contained in:
dh
2026-09-29 08:46:44 +00:00
parent aefe46352f
commit d1fc62d43c
7 changed files with 129 additions and 27 deletions
+2
View File
@@ -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
View File
@@ -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(
+9 -5
View File
@@ -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 |
| --- | --- | --- |
+5 -2
View File
@@ -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.
+26
View File
@@ -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(
+28
View File
@@ -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);
+42
View File
@@ -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
);
});
};