fix(charging): validate status and budget phase detection
Tests / test (push) Waiting to run

Do not latch full on transient zero current; require explicit completion. Validate go-e errors and Pico status/timestamps, keep historical events diagnostic, and preserve faults until a fresh successful poll. Budget the 6 A phase probe through the manager and correct stale phase counts conservatively.

Add adapter, regulator, isolated runtime and Symcon regression cases plus migration documentation. Verified: composer check (402 tests, 1875 assertions), 92 pure-function assertions in Symcon 8.0, git diff --check. No plant deployment or hardware commands.
This commit is contained in:
dh
2026-10-07 18:24:47 +00:00
parent 372b997876
commit 5885681669
10 changed files with 836 additions and 98 deletions
+80 -29
View File
@@ -34,6 +34,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
'AenderungMoeglich',
'Stoerung',
'Stoertext',
'Geraetehinweis',
'Fahrzeugstatus',
'FahrzeugMaximalstrom',
'LetzterGeraetebefehl',
@@ -88,6 +89,9 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->RegisterAttributeBoolean('ZustandAenderungMoeglich', false);
$this->RegisterAttributeBoolean('ZustandStoerung', false);
$this->RegisterAttributeString('ZustandStoertext', '');
$this->RegisterAttributeString('ZustandGeraetehinweis', '');
$this->RegisterAttributeInteger('GeraetestatusGueltigBis', 0);
$this->RegisterAttributeBoolean('GeraetLadebereit', false);
$this->RegisterAttributeString('Leistungsangebot', '[0]');
$this->RegisterAttributeString('Betriebsart', Nachrichtenvertrag::BETRIEBSART_PV);
$this->RegisterAttributeInteger('LetzteManagerID', 0);
@@ -149,12 +153,13 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->WriteAttributeInteger('AusschaltenGesperrtBis', 0);
$this->WriteAttributeInteger('EinschaltenGesperrtBis', 0);
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
if ($this->ReadAttributeInteger('PhasenerkennungVersion') < 1) {
$this->WriteAttributeInteger('GeraetestatusGueltigBis', 0);
if ($this->ReadAttributeInteger('PhasenerkennungVersion') < 2) {
$this->WriteAttributeInteger(
'ZustandPhasenzahl',
LadestationRegler::PHASEN_UNBEKANNT
);
$this->WriteAttributeInteger('PhasenerkennungVersion', 1);
$this->WriteAttributeInteger('PhasenerkennungVersion', 2);
}
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
$this->WriteAttributeInteger(
@@ -273,8 +278,10 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->aktualisiere(false, false, false);
$sollleistung = $daten['Sollleistung_W'];
if ($sollleistung === null) {
if ((bool) $this->leseZustand('SollwertGueltig')
&& !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true)
if (($this->leseLeistungsangebot() === [0]
&& $this->ReadAttributeInteger('LetzterGesetzterStrom') !== 0)
|| ((bool) $this->leseZustand('SollwertGueltig')
&& !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true))
) {
$this->verwerfeSollwert();
$this->aktualisiere(false, false);
@@ -308,9 +315,14 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
if ($statusLesen) {
$this->leseGeraetestatus();
}
if ($this->ReadAttributeInteger('GeraetestatusGueltigBis') <= time()) {
$fehlertext = (string) $this->leseZustand('Stoertext');
throw new RuntimeException($fehlertext !== '' ? $fehlertext : 'Kein frischer gueltiger Geraetestatus.');
}
$aktiv = (bool) $this->GetValue('Aktiv');
$ladefreigabe = $this->ReadAttributeBoolean('ZustandLadefreigabe');
$ladefreigabe = $this->ReadAttributeBoolean('ZustandLadefreigabe')
&& $this->ReadAttributeBoolean('GeraetLadebereit');
$verbunden = $this->ReadAttributeBoolean('ZustandFahrzeugVerbunden');
$geladen = $this->ReadAttributeBoolean('ZustandFahrzeugGeladen');
$phasenzahl = $this->ReadAttributeInteger('ZustandPhasenzahl');
@@ -384,25 +396,33 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->setzeZustand('AenderungMoeglich', $verfuegbar && count($angebot) > 1);
$sollleistung = $this->bestimmeWirksameSollleistung($angebot);
$phasenerkennungAktiv = $aktiv
&& $ladefreigabe
&& $verbunden
&& !$geladen
&& $phasenzahl === LadestationRegler::PHASEN_UNBEKANNT;
$regeluebergangAktiv = $aktiv
&& $ladefreigabe
&& $verbunden
&& !$geladen
&& $this->ReadAttributeBoolean('RegeluebergangAktiv');
if ($steuerungAusfuehren && !$phasenerkennungAktiv && !$regeluebergangAktiv) {
if ($steuerungAusfuehren && (!$regeluebergangAktiv || $angebot === [0])) {
$this->setzeLadestrom($sollleistung);
}
$this->setzeStoerung('');
$this->SetStatus(self::STATUS_AKTIV);
} catch (Throwable $fehler) {
$this->WriteAttributeInteger('GeraetestatusGueltigBis', 0);
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
$this->WriteAttributeString('Leistungsangebot', '[0]');
$this->setzeSichtbareVariable('LeistungsangebotDiagnose', '[0]');
$this->verwerfeSollwert();
$this->setzeZustand('Verfuegbar', false);
$this->setzeZustand('AenderungMoeglich', false);
$this->setzeStoerung($fehler->getMessage());
$text = $fehler->getMessage();
if ($steuerungAusfuehren && $this->ReadAttributeInteger('LetzterGesetzterStrom') !== 0) {
try {
$this->setzeLadestrom(0);
} catch (Throwable $stoppfehler) {
$text .= ' Nullvorgabe nicht bestaetigt: ' . $stoppfehler->getMessage();
}
}
$this->setzeStoerung($text);
$this->SetStatus(self::STATUS_KOMMUNIKATIONSFEHLER);
$this->protokolliere('Geraetekommunikation', $fehler->getMessage());
}
@@ -447,7 +467,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->WriteAttributeInteger('LeistungswechselGesperrtBis', 0);
$this->WriteAttributeInteger('AusschaltenGesperrtBis', 0);
$this->WriteAttributeInteger('EinschaltenGesperrtBis', 0);
} elseif ($bekanntePhasenzahl === LadestationRegler::PHASEN_UNBEKANNT) {
} else {
$direktErkanntePhasen = (int) ($rohstatus['Phasenzahl']
?? LadestationRegler::PHASEN_UNBEKANNT);
if (in_array(
@@ -455,30 +475,25 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
[LadestationRegler::PHASEN_EIN, LadestationRegler::PHASEN_DREI],
true
)) {
$bekanntePhasenzahl = $direktErkanntePhasen;
$bekanntePhasenzahl = max($bekanntePhasenzahl, $direktErkanntePhasen);
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
} elseif ($rohstatus['Istleistung_W'] > 7500.0) {
$bekanntePhasenzahl = LadestationRegler::PHASEN_DREI;
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
} elseif ((bool) $this->GetValue('Aktiv')
&& $this->ReadAttributeBoolean('ZustandLadefreigabe')
) {
} elseif ($bekanntePhasenzahl === LadestationRegler::PHASEN_UNBEKANNT) {
$erkennungSeit = $this->ReadAttributeInteger('PhasenerkennungSeit');
if ($erkennungSeit <= 0) {
$this->sendeLadestromAnGeraet(self::PHASENERKENNUNGSSTROM);
$erkennungSeit = time();
$this->WriteAttributeInteger('PhasenerkennungSeit', $erkennungSeit);
} elseif ((time() - $erkennungSeit)
>= $this->ReadPropertyInteger('Phasenerkennungszeit')
if ($erkennungSeit > 0
&& $this->ReadAttributeInteger('LetzterGesetzterStrom') === self::PHASENERKENNUNGSSTROM
&& (time() - $erkennungSeit) >= $this->ReadPropertyInteger('Phasenerkennungszeit')
) {
$bekanntePhasenzahl = LadestationRegler::erkennePhasenzahlAusLeistung(
(float) $rohstatus['Istleistung_W'],
$this->ReadAttributeInteger('LetzterGesetzterStrom')
);
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
if ($bekanntePhasenzahl !== LadestationRegler::PHASEN_UNBEKANNT) {
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
}
}
} else {
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
}
}
@@ -490,6 +505,10 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->ReadAttributeBoolean('LadeleistungSeitFreigabeErkannt'),
$bekanntePhasenzahl
);
if ($status['Phasenzahl'] !== $this->ReadAttributeInteger('ZustandPhasenzahl')) {
// Eine alte Leistungsstufe darf nach einer Phasenkorrektur nicht festgehalten werden.
$this->WriteAttributeInteger('LeistungswechselGesperrtBis', 0);
}
if (!$status['FahrzeugVerbunden']) {
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
} elseif ($status['Ladestrom_A'] >= 3.5) {
@@ -504,6 +523,13 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
foreach ($status as $ident => $wert) {
$this->setzeZustand($ident, $wert);
}
$this->setzeZustand('Geraetehinweis', (string) ($rohstatus['Geraetehinweis'] ?? ''));
$this->WriteAttributeBoolean('GeraetLadebereit', (bool) ($rohstatus['Ladebereit'] ?? true));
$cacheAlter = min(LadestationAdapter::STATUS_MAXIMALALTER, max(15, 3 * $this->ReadPropertyInteger('Abfrageintervall')));
$this->WriteAttributeInteger('GeraetestatusGueltigBis', min(
time() + $cacheAlter,
(int) ($rohstatus['GueltigBis'] ?? PHP_INT_MAX)
));
}
/** @param array<string, int|float|bool> $status */
@@ -573,6 +599,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
}
if (!$this->ReadAttributeBoolean('ZustandSolarladen') && $angebot !== [0]) {
if ($this->ReadAttributeInteger('ZustandPhasenzahl') === LadestationRegler::PHASEN_UNBEKANNT
&& $this->zugeordneteManagerIDs() !== []
) {
return 0;
}
return $angebot[count($angebot) - 1];
}
@@ -594,6 +625,12 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
if (!$stromUnveraendert) {
$this->sendeLadestromAnGeraet($strom);
}
if ($phasenzahl === LadestationRegler::PHASEN_UNBEKANNT
&& $strom === self::PHASENERKENNUNGSSTROM
&& $this->ReadAttributeInteger('PhasenerkennungSeit') === 0
) {
$this->WriteAttributeInteger('PhasenerkennungSeit', time());
}
if ($leistung === $bisherigeLeistung) {
return;
}
@@ -647,6 +684,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$strom
);
$this->WriteAttributeInteger('LetzterGesetzterStrom', $strom);
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
}
/**
@@ -729,6 +767,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
private function sendeVerbraucherdaten(): void
{
if ($this->ReadAttributeInteger('GeraetestatusGueltigBis') <= time()
&& (bool) $this->leseZustand('Verfuegbar')
) {
$this->aktualisiere(false, false, false);
}
foreach ($this->zugeordneteManagerIDs() as $managerID) {
$daten = $this->baueVerbraucherdaten($managerID);
Nachrichtenvertrag::pruefeVerbraucherdaten($daten);
@@ -822,6 +865,12 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
'Einheit' => '',
'Text' => (string) $this->leseZustand('Stoertext'),
],
[
'Kennung' => 'Geraetehinweis',
'Art' => 'Status',
'Wert' => (string) $this->leseZustand('Geraetehinweis'),
'Einheit' => '',
],
],
];
}
@@ -955,6 +1004,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 200);
$this->RegisterVariableString('LetzterGeraetebefehl', 'Letzter Geraetebefehl', '', 210);
$this->RegisterVariableString('LeistungsangebotDiagnose', 'Leistungsangebot', '', 220);
$this->RegisterVariableString('Geraetehinweis', 'Geraetehinweis', '', 240);
$this->RegisterVariableInteger(
'FahrzeugMaximalstrom',
'Erkannter Fahrzeug-Maximalstrom',
@@ -1037,7 +1087,8 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->WriteAttributeFloat('ZustandIstleistung', (float) $wert);
break;
case 'Stoertext':
$this->WriteAttributeString('ZustandStoertext', (string) $wert);
case 'Geraetehinweis':
$this->WriteAttributeString('Zustand' . $ident, (string) $wert);
break;
default:
throw new LogicException('Unbekannter Zustand: ' . $ident);
@@ -1069,8 +1120,8 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
if ($ident === 'Ladestrom' || $ident === 'Istleistung') {
return $this->ReadAttributeFloat('Zustand' . $ident);
}
if ($ident === 'Stoertext') {
return $this->ReadAttributeString('ZustandStoertext');
if ($ident === 'Stoertext' || $ident === 'Geraetehinweis') {
return $this->ReadAttributeString('Zustand' . $ident);
}
if ($ident === 'Leistungsquelle') {
return Nachrichtenvertrag::LEISTUNGSQUELLE_GEMESSEN;