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
+22 -15
View File
@@ -11,6 +11,7 @@ final class LadestationRegler
public const PHASEN_UNBEKANNT = 0;
public const PHASEN_EIN = 1;
public const PHASEN_DREI = 3;
public const PHASENERKENNUNGSLEISTUNG = 4104;
private const LEISTUNG_PRO_AMPERE_EINPHASIG = 230;
private const LEISTUNG_PRO_AMPERE_DREIPHASIG = 684;
@@ -26,17 +27,20 @@ final class LadestationRegler
throw new InvalidArgumentException('Erkennungsstrom muss zwischen 6 und 32 A liegen.');
}
if ($leistung < 5.5 * self::LEISTUNG_PRO_AMPERE_EINPHASIG) {
return self::PHASEN_UNBEKANNT;
}
$trennleistung = 2 * self::LEISTUNG_PRO_AMPERE_EINPHASIG * $angeforderterStrom;
return $leistung > $trennleistung ? self::PHASEN_DREI : self::PHASEN_EIN;
}
/**
* Die Schwelle und der Zuschlag von 2.5 A stammen aus der Enelix-1-Ladestation.
* Ein Ladeende wird erst nach zuvor gemessener Ladeleistung angenommen, damit
* die Anlaufzeit nach einer neuen Stromvorgabe nicht als volles Fahrzeug gilt.
* Ladepausen sind kein Ladeende. Nur ein expliziter aktueller Geraetestatus
* darf "geladen" melden; alte, aus Niedrigstrom abgeleitete Werte verfallen.
* Die bisherigen Verlaufsparameter bleiben fuer Aufrufer kompatibel.
*
* @param array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int} $rohstatus
* @param array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int, LadeendeBestaetigt?: bool} $rohstatus
* @return array<string, int|float|bool>
*/
public static function normalisiereStatus(
@@ -54,14 +58,17 @@ final class LadestationRegler
$geladen = false;
if ($verbunden) {
if (in_array($bekanntePhasenzahl, [self::PHASEN_EIN, self::PHASEN_DREI], true)) {
$phasen = $bekanntePhasenzahl;
if ($leistung > 7500.0 || ($warVerbunden && $bekanntePhasenzahl === self::PHASEN_DREI)) {
// Einzelne momentan unbelastete Phasen duerfen das Budget nicht verkleinern.
$phasen = self::PHASEN_DREI;
} elseif (isset($rohstatus['Phasenzahl'])
&& in_array($rohstatus['Phasenzahl'], [self::PHASEN_EIN, self::PHASEN_DREI], true)
) {
$phasen = (int) $rohstatus['Phasenzahl'];
} elseif ($leistung > 7500.0) {
$phasen = self::PHASEN_DREI;
} elseif ($warVerbunden
&& in_array($bekanntePhasenzahl, [self::PHASEN_EIN, self::PHASEN_DREI], true)
) {
$phasen = $bekanntePhasenzahl;
}
$nenner = $phasen === self::PHASEN_DREI
@@ -70,12 +77,8 @@ final class LadestationRegler
$ladestrom = isset($rohstatus['Iststrom_A'])
? max(0.0, (float) $rohstatus['Iststrom_A'])
: ($phasen === self::PHASEN_UNBEKANNT ? 0.0 : $leistung / $nenner);
$ermittelterMaximalstrom = 2.5 + $ladestrom;
$geladen = $warGeladen
|| ($warVerbunden
&& $ladungErwartet
&& $ladeleistungWurdeErkannt
&& $ermittelterMaximalstrom < 6.0);
$geladen = ($rohstatus['LadeendeBestaetigt'] ?? false) === true
&& $ladestrom < 1.0 && $leistung < 100.0;
}
if (!$verbunden) {
@@ -137,7 +140,8 @@ final class LadestationRegler
}
if ($phasenzahl === self::PHASEN_UNBEKANNT) {
return [0];
// 6 A mit konservativem Dreiphasenbudget, niemals ein direkter Probebefehl.
return $solarladen && $peakbetrieb ? [0] : [0, self::PHASENERKENNUNGSLEISTUNG];
}
$stufen = self::leistungsstufen($phasenzahl, $maximalerLadestrom);
@@ -159,6 +163,9 @@ final class LadestationRegler
if ($leistung <= 0) {
return 0;
}
if ($phasenzahl === self::PHASEN_UNBEKANNT) {
return $leistung >= self::PHASENERKENNUNGSLEISTUNG ? 6 : 0;
}
$stufen = self::stufenFuerPhasen($phasenzahl);
if ($leistung < $stufen[0]) {