From d1fc62d43c3012281d032d70989c7f45c0e5162a Mon Sep 17 00:00:00 2001 From: dh_Agent Date: Tue, 29 Sep 2026 08:46:44 +0000 Subject: [PATCH] fix(manager): Regelung bei veralteten Verbrauchern fortsetzen --- CHANGELOG.md | 2 ++ Manager/module.php | 37 ++++++++++++-------------- README.md | 14 ++++++---- docs/Schnittstelle.md | 7 +++-- tests/ManagerModulstrukturTest.php | 26 ++++++++++++++++++ tests/ManagerReglerTest.php | 28 ++++++++++++++++++++ tests/Symcon/modules/Manager.php | 42 ++++++++++++++++++++++++++++++ 7 files changed, 129 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c9419b..563cb15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Manager/module.php b/Manager/module.php index b4cd7a1..7fa93bb 100644 --- a/Manager/module.php +++ b/Manager/module.php @@ -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( diff --git a/README.md b/README.md index 3646ade..de1fe02 100644 --- a/README.md +++ b/README.md @@ -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 | | --- | --- | --- | diff --git a/docs/Schnittstelle.md b/docs/Schnittstelle.md index f959724..4eec288 100644 --- a/docs/Schnittstelle.md +++ b/docs/Schnittstelle.md @@ -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. diff --git a/tests/ManagerModulstrukturTest.php b/tests/ManagerModulstrukturTest.php index 1356df1..0b20388 100644 --- a/tests/ManagerModulstrukturTest.php +++ b/tests/ManagerModulstrukturTest.php @@ -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( diff --git a/tests/ManagerReglerTest.php b/tests/ManagerReglerTest.php index a5d1b48..31c840d 100644 --- a/tests/ManagerReglerTest.php +++ b/tests/ManagerReglerTest.php @@ -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); diff --git a/tests/Symcon/modules/Manager.php b/tests/Symcon/modules/Manager.php index e4b0e6a..ae6d5aa 100644 --- a/tests/Symcon/modules/Manager.php +++ b/tests/Symcon/modules/Manager.php @@ -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 + ); + }); };