From f611da975d5184fad5c08c8042230386b5ca2f10 Mon Sep 17 00:00:00 2001 From: dh_Agent Date: Tue, 29 Sep 2026 16:47:03 +0000 Subject: [PATCH] fix(easee): Ereignisverbindung konsistent neu aufbauen --- EaseeGateway/module.php | 97 +++++++++++++++++---- tests/Symcon/modules/EaseeGateway.php | 32 +++++++ tests/Symcon/modules/LadestationGateway.php | 26 ++++++ 3 files changed, 136 insertions(+), 19 deletions(-) diff --git a/EaseeGateway/module.php b/EaseeGateway/module.php index 7a5ebb3..09cb293 100644 --- a/EaseeGateway/module.php +++ b/EaseeGateway/module.php @@ -51,6 +51,11 @@ class EaseeGateway extends IPSModule 0, "IPS_RequestAction(\$_IPS['TARGET'], 'RefreshToken', false);" ); + $this->RegisterTimer( + 'ReconnectTimer', + 0, + "IPS_RequestAction(\$_IPS['TARGET'], 'ReconnectAfterLoss', false);" + ); $this->RequireParent(self::WEBSOCKET_MODULE_ID); } @@ -61,6 +66,7 @@ class EaseeGateway extends IPSModule $this->SetTimerInterval('MaintainConnectionTimer', 0); $this->SetTimerInterval('TokenRefreshTimer', 0); + $this->SetTimerInterval('ReconnectTimer', 0); $this->SetBuffer('SignalRReady', '0'); $this->SetBuffer('ReceiveBuffer', ''); $this->SetBuffer('Subscriptions', '{}'); @@ -112,6 +118,17 @@ class EaseeGateway extends IPSModule $this->pflegeVerbindung(); return; + case 'ReconnectAfterLoss': + $this->SetTimerInterval('ReconnectTimer', 0); + if ( + $this->ReadPropertyBoolean('Active') + && !$this->ReadPropertyBoolean('Testmodus') + && $this->GetBuffer('SignalRReady') !== '1' + ) { + $this->verbindeSignalR(); + } + return; + case 'RefreshToken': if ( $this->erneuereZugangsdaten(false) @@ -146,6 +163,15 @@ class EaseeGateway extends IPSModule $daten['value'] ?? null ); return; + + case 'TestSignalRFrame': + if (!$this->ReadPropertyBoolean('Testmodus') || !is_string($wert)) { + throw new InvalidArgumentException( + 'TestSignalRFrame ist nur im Testmodus zulaessig.' + ); + } + $this->verarbeiteSignalRRahmen($wert); + return; } throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident); @@ -281,8 +307,10 @@ class EaseeGateway extends IPSModule if ($letzterEmpfang > 0 && ($jetzt - $letzterEmpfang) <= 90) { return; } - $this->SetBuffer('SignalRReady', '0'); - $this->setzeVerbunden(false); + $this->planeWiederverbindung( + 'Seit mehr als 90 Sekunden keine Easee-Ereignisdaten empfangen.' + ); + return; } if (($jetzt - $letzteAushandlung) >= 45) { @@ -295,16 +323,22 @@ class EaseeGateway extends IPSModule private function verbindeSignalR(): bool { + $this->SetTimerInterval('ReconnectTimer', 0); if ($this->ReadPropertyBoolean('Testmodus')) { - $this->SetStatus(102); - $this->setzeVerbunden(true); + $this->setzeEreignisverbindung(true); return true; } if (!$this->stelleAccessTokenSicher()) { + $this->SetBuffer('SignalRReady', '0'); + $this->setzeVerbunden(false); $this->SetStatus(202); return false; } + $this->setzeEreignisverbindung( + false, + 'Easee-Ereignisverbindung wird aufgebaut.' + ); $this->SetBuffer('LastNegotiation', (string) time()); $token = $this->GetBuffer('AccessToken'); $antwort = $this->httpAnfrage( @@ -323,11 +357,11 @@ class EaseeGateway extends IPSModule ); } if (!$antwort['success']) { - $this->SetStatus(203); - $this->setzeLetztenFehler( + $this->setzeEreignisverbindung( + false, 'Easee-Ereignisverbindung fehlgeschlagen: HTTP ' - . $antwort['httpCode'] - . ($antwort['error'] !== '' ? ' / ' . $antwort['error'] : '') + . $antwort['httpCode'] + . ($antwort['error'] !== '' ? ' / ' . $antwort['error'] : '') ); return false; } @@ -337,8 +371,10 @@ class EaseeGateway extends IPSModule ? (string) ($daten['connectionToken'] ?? $daten['connectionId'] ?? '') : ''; if ($verbindungstoken === '') { - $this->SetStatus(203); - $this->setzeLetztenFehler('Easee liefert kein Verbindungstoken.'); + $this->setzeEreignisverbindung( + false, + 'Easee liefert kein Verbindungstoken.' + ); return false; } @@ -413,10 +449,7 @@ class EaseeGateway extends IPSModule private function verarbeiteSignalRRahmen(string $rahmen): void { if ($rahmen === '{}') { - $this->SetBuffer('SignalRReady', '1'); - $this->SetStatus(102); - $this->setzeLetztenFehler(''); - $this->setzeVerbunden(true); + $this->setzeEreignisverbindung(true); $this->sendeAlleAbonnements(); return; } @@ -427,8 +460,9 @@ class EaseeGateway extends IPSModule return; } if (isset($nachricht['error'])) { - $this->SetStatus(203); - $this->setzeLetztenFehler('Easee-Ereignisfehler: ' . $nachricht['error']); + $this->planeWiederverbindung( + 'Easee-Ereignisfehler: ' . $nachricht['error'] + ); return; } @@ -437,9 +471,9 @@ class EaseeGateway extends IPSModule return; } if ($typ === 7) { - $this->SetBuffer('SignalRReady', '0'); - $this->setzeVerbunden(false); - $this->setzeLetztenFehler('Easee-Ereignisverbindung wurde beendet.'); + $this->planeWiederverbindung( + 'Easee-Ereignisverbindung wurde beendet.' + ); return; } @@ -868,6 +902,31 @@ class EaseeGateway extends IPSModule ]; } + private function setzeEreignisverbindung( + bool $verbunden, + string $fehler = '' + ): void { + $this->SetBuffer('SignalRReady', $verbunden ? '1' : '0'); + $this->SetStatus($verbunden ? 102 : 203); + $this->setzeLetztenFehler($verbunden ? '' : $fehler); + if ($verbunden) { + $this->SetTimerInterval('ReconnectTimer', 0); + } + $this->setzeVerbunden($verbunden); + } + + private function planeWiederverbindung(string $fehler): void + { + $this->setzeEreignisverbindung(false, $fehler); + $this->SetBuffer('LastNegotiation', '0'); + if ( + $this->ReadPropertyBoolean('Active') + && !$this->ReadPropertyBoolean('Testmodus') + ) { + $this->SetTimerInterval('ReconnectTimer', 1000); + } + } + private function setzeVerbunden(bool $verbunden): void { $warVerbunden = (bool) $this->GetValue('Connected'); diff --git a/tests/Symcon/modules/EaseeGateway.php b/tests/Symcon/modules/EaseeGateway.php index e825613..4433f6d 100644 --- a/tests/Symcon/modules/EaseeGateway.php +++ b/tests/Symcon/modules/EaseeGateway.php @@ -24,6 +24,38 @@ return static function (TestContext $test): void { ); }); + $test->runCase('SignalR-Abbruch synchronisiert den Gatewaystatus', static function ( + TestContext $test + ) use ($gateway): void { + IPS_RequestAction( + $gateway, + 'TestSignalRFrame', + json_encode(['type' => 7], JSON_THROW_ON_ERROR) + ); + + $test->assertInstanceStatus($gateway, 203); + $test->assertSame( + false, + GetValue($test->objectByIdent('Connected', $gateway)) + ); + $test->assertSame( + 'Easee-Ereignisverbindung wurde beendet.', + GetValue($test->objectByIdent('LastError', $gateway)) + ); + + IPS_RequestAction($gateway, 'TestSignalRFrame', '{}'); + + $test->assertInstanceStatus($gateway); + $test->assertSame( + true, + GetValue($test->objectByIdent('Connected', $gateway)) + ); + $test->assertSame( + '', + GetValue($test->objectByIdent('LastError', $gateway)) + ); + }); + $test->runCase('Abonnement liefert injizierten API-Zustand', static function ( TestContext $test ) use ($gateway): void { diff --git a/tests/Symcon/modules/LadestationGateway.php b/tests/Symcon/modules/LadestationGateway.php index 9df9062..b548786 100644 --- a/tests/Symcon/modules/LadestationGateway.php +++ b/tests/Symcon/modules/LadestationGateway.php @@ -184,6 +184,32 @@ return static function (TestContext $test): void { $test->assertInstanceStatus($station); }); + $test->runCase('SignalR-Abbruch erreicht die Ladestation', static function ( + TestContext $test + ) use ($gateway, $station): void { + IPS_RequestAction( + $gateway, + 'TestSignalRFrame', + json_encode(['type' => 7], JSON_THROW_ON_ERROR) + ); + + $test->assertInstanceStatus($gateway, 203); + $test->assertInstanceStatus($station, 202); + $test->assertSame( + false, + GetValue($test->objectByIdent('GatewayVerbunden', $station)) + ); + + IPS_RequestAction($gateway, 'TestSignalRFrame', '{}'); + + $test->assertInstanceStatus($gateway); + $test->assertInstanceStatus($station); + $test->assertSame( + true, + GetValue($test->objectByIdent('GatewayVerbunden', $station)) + ); + }); + $test->runCase('Gateway-Ausfall verwirft Fahrzeug- und Phasenstatus', static function ( TestContext $test ) use ($gateway, $station): void {