fix(ladestation): Ladeanlauf und Easee-Verbindung stabilisieren
Tests / test (push) Successful in 1m23s

This commit is contained in:
dh
2026-09-29 12:25:35 +00:00
parent a74f14aa19
commit b3b868cd2a
5 changed files with 91 additions and 10 deletions
+29 -3
View File
@@ -16,6 +16,7 @@ class EaseeGateway extends IPSModule
private const SIGNALR_BASE_URL = 'https://streams.easee.com/hubs/chargers'; private const SIGNALR_BASE_URL = 'https://streams.easee.com/hubs/chargers';
private const API_BASE_URL = 'https://api.easee.com'; private const API_BASE_URL = 'https://api.easee.com';
private const RECORD_SEPARATOR = "\x1e"; private const RECORD_SEPARATOR = "\x1e";
private const CACHE_MAXIMALALTER_SEKUNDEN = 60;
public function Create(): void public function Create(): void
{ {
@@ -112,7 +113,10 @@ class EaseeGateway extends IPSModule
return; return;
case 'RefreshToken': case 'RefreshToken':
if ($this->erneuereZugangsdaten(false)) { if (
$this->erneuereZugangsdaten(false)
&& $this->GetBuffer('SignalRReady') !== '1'
) {
$this->verbindeSignalR(); $this->verbindeSignalR();
} }
return; return;
@@ -346,7 +350,7 @@ class EaseeGateway extends IPSModule
$this->SetBuffer('ReceiveBuffer', ''); $this->SetBuffer('ReceiveBuffer', '');
$this->SetBuffer('SubscribedThisConnection', '{}'); $this->SetBuffer('SubscribedThisConnection', '{}');
$this->SetBuffer('PendingInvocations', '{}'); $this->SetBuffer('PendingInvocations', '{}');
$this->WriteAttributeString('ObservationCache', '{}'); $this->markiereBeobachtungscacheVeraltet();
$this->setzeVerbunden(false); $this->setzeVerbunden(false);
if (!$this->konfiguriereWebSocketParent($url)) { if (!$this->konfiguriereWebSocketParent($url)) {
@@ -572,12 +576,30 @@ class EaseeGateway extends IPSModule
); );
} }
private function markiereBeobachtungscacheVeraltet(): void
{
$cache = $this->leseAttributArray('ObservationCache');
foreach ($cache as &$zustand) {
if (is_array($zustand)) {
$zustand['updated'] = 0;
}
}
unset($zustand);
$this->WriteAttributeString(
'ObservationCache',
json_encode($cache, JSON_THROW_ON_ERROR)
);
}
private function stelleAktuellenStatusSicher(string $seriennummer): void private function stelleAktuellenStatusSicher(string $seriennummer): void
{ {
$cache = $this->leseAttributArray('ObservationCache'); $cache = $this->leseAttributArray('ObservationCache');
$zustand = $cache[$seriennummer] ?? []; $zustand = $cache[$seriennummer] ?? [];
$cacheIstAktuell = is_array($zustand)
&& array_key_exists('109', $zustand)
&& (int) ($zustand['updated'] ?? 0) >= time() - self::CACHE_MAXIMALALTER_SEKUNDEN;
if ( if (
(is_array($zustand) && array_key_exists('109', $zustand)) $cacheIstAktuell
|| $this->ReadPropertyBoolean('Testmodus') || $this->ReadPropertyBoolean('Testmodus')
|| $this->GetBuffer('SignalRReady') !== '1' || $this->GetBuffer('SignalRReady') !== '1'
) { ) {
@@ -848,7 +870,11 @@ class EaseeGateway extends IPSModule
private function setzeVerbunden(bool $verbunden): void private function setzeVerbunden(bool $verbunden): void
{ {
$warVerbunden = (bool) $this->GetValue('Connected');
$this->SetValue('Connected', $verbunden); $this->SetValue('Connected', $verbunden);
if ($warVerbunden === $verbunden) {
return;
}
$this->SendDataToChildren(json_encode([ $this->SendDataToChildren(json_encode([
'DataID' => self::CHILD_EVENT_DATA_ID, 'DataID' => self::CHILD_EVENT_DATA_ID,
'Buffer' => json_encode([ 'Buffer' => json_encode([
+10 -1
View File
@@ -87,6 +87,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->RegisterAttributeInteger('LetzteManagerID', 0); $this->RegisterAttributeInteger('LetzteManagerID', 0);
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0); $this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
$this->RegisterAttributeInteger('LetzterGesetzterStrom', -1); $this->RegisterAttributeInteger('LetzterGesetzterStrom', -1);
$this->RegisterAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
$this->RegisterTimer( $this->RegisterTimer(
'Statuszyklus', 'Statuszyklus',
@@ -120,6 +121,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
// Ein persistiertes Ladeende nach Neueinrichtung oder Modulupdate einmal neu pruefen. // Ein persistiertes Ladeende nach Neueinrichtung oder Modulupdate einmal neu pruefen.
$this->setzeZustand('FahrzeugGeladen', false); $this->setzeZustand('FahrzeugGeladen', false);
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1); $this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
@@ -307,8 +309,14 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$rohstatus, $rohstatus,
$this->ReadAttributeBoolean('ZustandFahrzeugVerbunden'), $this->ReadAttributeBoolean('ZustandFahrzeugVerbunden'),
$this->ReadAttributeBoolean('ZustandFahrzeugGeladen'), $this->ReadAttributeBoolean('ZustandFahrzeugGeladen'),
$this->ReadAttributeInteger('LetzterGesetzterStrom') >= 6 $this->ReadAttributeInteger('LetzterGesetzterStrom') >= 6,
$this->ReadAttributeBoolean('LadeleistungSeitFreigabeErkannt')
); );
if (!$status['FahrzeugVerbunden']) {
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
} elseif ($status['Ladestrom_A'] >= 3.5) {
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', true);
}
foreach ($status as $ident => $wert) { foreach ($status as $ident => $wert) {
$this->setzeZustand($ident, $wert); $this->setzeZustand($ident, $wert);
@@ -342,6 +350,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
if ($strom === $this->ReadAttributeInteger('LetzterGesetzterStrom')) { if ($strom === $this->ReadAttributeInteger('LetzterGesetzterStrom')) {
return; return;
} }
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
$transport = new LadestationTransport( $transport = new LadestationTransport(
fn (array $anfrage): string => $this->fuehreHttpAnfrageAus($anfrage) fn (array $anfrage): string => $this->fuehreHttpAnfrageAus($anfrage)
+9 -4
View File
@@ -28,8 +28,9 @@ final class LadestationRegler
]; ];
/** /**
* Die Schwelle, die Voll-Erkennung und der Zuschlag von 2.5 A stammen * Die Schwelle und der Zuschlag von 2.5 A stammen aus der Enelix-1-Ladestation.
* aus der Enelix-1-Ladestation und bleiben bewusst kompatibel. * Ein Ladeende wird erst nach zuvor gemessener Ladeleistung angenommen, damit
* die Anlaufzeit nach einer neuen Stromvorgabe nicht als volles Fahrzeug gilt.
* *
* @param array{Rohstatus: int, Istleistung_W: float} $rohstatus * @param array{Rohstatus: int, Istleistung_W: float} $rohstatus
* @return array<string, int|float|bool> * @return array<string, int|float|bool>
@@ -38,7 +39,8 @@ final class LadestationRegler
array $rohstatus, array $rohstatus,
bool $warVerbunden, bool $warVerbunden,
bool $warGeladen = false, bool $warGeladen = false,
bool $ladungErwartet = true bool $ladungErwartet = true,
bool $ladeleistungWurdeErkannt = false
): array { ): array {
$verbunden = $rohstatus['Rohstatus'] !== 1; $verbunden = $rohstatus['Rohstatus'] !== 1;
$leistung = max(0.0, $rohstatus['Istleistung_W']); $leistung = max(0.0, $rohstatus['Istleistung_W']);
@@ -52,7 +54,10 @@ final class LadestationRegler
$ladestrom = $leistung / $nenner; $ladestrom = $leistung / $nenner;
$ermittelterMaximalstrom = 2.5 + $ladestrom; $ermittelterMaximalstrom = 2.5 + $ladestrom;
$geladen = $warGeladen $geladen = $warGeladen
|| ($warVerbunden && $ladungErwartet && $ermittelterMaximalstrom < 6.0); || ($warVerbunden
&& $ladungErwartet
&& $ladeleistungWurdeErkannt
&& $ermittelterMaximalstrom < 6.0);
} }
if (!$verbunden) { if (!$verbunden) {
+13 -2
View File
@@ -33,12 +33,23 @@ final class LadestationReglerTest extends TestCase
self::assertEqualsWithDelta(10.0, $status['Ladestrom_A'], 0.01); self::assertEqualsWithDelta(10.0, $status['Ladestrom_A'], 0.01);
} }
public function testGeladenWirdNachVerbindungUnterSechsAmpereErkannt(): void public function testAnlaufOhneGemesseneLadeleistungGiltNichtAlsLadeende(): void
{ {
$status = LadestationRegler::normalisiereStatus([ $status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2, 'Rohstatus' => 2,
'Istleistung_W' => 0.0, 'Istleistung_W' => 0.0,
], true); ], true, false, true, false);
self::assertFalse($status['FahrzeugGeladen']);
self::assertSame(2, $status['Fahrzeugstatus']);
}
public function testGeladenWirdErstNachZuvorGemessenerLadeleistungErkannt(): void
{
$status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2,
'Istleistung_W' => 0.0,
], true, false, true, true);
self::assertTrue($status['FahrzeugGeladen']); self::assertTrue($status['FahrzeugGeladen']);
self::assertSame(4, $status['Fahrzeugstatus']); self::assertSame(4, $status['Fahrzeugstatus']);
@@ -228,4 +228,34 @@ return static function (TestContext $test): void {
'alw=0' 'alw=0'
), 'Peak mit Solarladen muss die Ladestation stoppen.'); ), 'Peak mit Solarladen muss die Ladestation stoppen.');
}); });
$test->runCase('Sollwert bleibt waehrend der Ladeanlaufzeit gueltig', static function (
TestContext $test
) use ($goEAntwort, $stationen, $anforderungSenden): void {
$stationId = $stationen['go-e Gemini'];
IPS_SetProperty($stationId, 'Testantwort', $goEAntwort(2, 0));
IPS_ApplyChanges($stationId);
IPS_RequestAction($stationId, 'Aktiv', true);
IPS_RequestAction($stationId, 'Solarladen', true);
$anforderungSenden($stationId, 3850);
IPS_RequestAction($stationId, 'Statuszyklus', false);
$test->assertSame(
false,
GetValue($test->objectByIdent('FahrzeugGeladen', $stationId))
);
$test->assertSame(
true,
GetValue($test->objectByIdent('SollwertGueltig', $stationId))
);
$test->assertSame(
3850,
GetValue($test->objectByIdent('Sollleistung', $stationId))
);
$test->assertSame(
'[0,1450,1690,1930,2170,2410,2650,2890,3130,3370,3610,3850]',
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId))
);
});
}; };