Files
Enelix-EMS/libs/LadestationRegler.php
T
dh 5885681669
Tests / test (push) Waiting to run
fix(charging): validate status and budget phase detection
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.
2026-10-07 18:24:47 +00:00

213 lines
7.1 KiB
PHP

<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS;
use InvalidArgumentException;
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;
public static function erkennePhasenzahlAusLeistung(
float $leistung,
int $angeforderterStrom
): int {
if (!is_finite($leistung) || $leistung < 0.0) {
throw new InvalidArgumentException('Istleistung muss endlich und darf nicht negativ sein.');
}
if ($angeforderterStrom < 6 || $angeforderterStrom > 32) {
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;
}
/**
* 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, LadeendeBestaetigt?: bool} $rohstatus
* @return array<string, int|float|bool>
*/
public static function normalisiereStatus(
array $rohstatus,
bool $warVerbunden,
bool $warGeladen = false,
bool $ladungErwartet = true,
bool $ladeleistungWurdeErkannt = false,
int $bekanntePhasenzahl = self::PHASEN_UNBEKANNT
): array {
$verbunden = $rohstatus['Rohstatus'] !== 1;
$leistung = max(0.0, $rohstatus['Istleistung_W']);
$phasen = self::PHASEN_UNBEKANNT;
$ladestrom = 0.0;
$geladen = false;
if ($verbunden) {
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 ($warVerbunden
&& in_array($bekanntePhasenzahl, [self::PHASEN_EIN, self::PHASEN_DREI], true)
) {
$phasen = $bekanntePhasenzahl;
}
$nenner = $phasen === self::PHASEN_DREI
? self::LEISTUNG_PRO_AMPERE_DREIPHASIG
: self::LEISTUNG_PRO_AMPERE_EINPHASIG;
$ladestrom = isset($rohstatus['Iststrom_A'])
? max(0.0, (float) $rohstatus['Iststrom_A'])
: ($phasen === self::PHASEN_UNBEKANNT ? 0.0 : $leistung / $nenner);
$geladen = ($rohstatus['LadeendeBestaetigt'] ?? false) === true
&& $ladestrom < 1.0 && $leistung < 100.0;
}
if (!$verbunden) {
$fahrzeugstatus = 1;
} elseif ($geladen) {
$fahrzeugstatus = 4;
} elseif ($leistung > 0.0) {
$fahrzeugstatus = 3;
} else {
$fahrzeugstatus = 2;
}
return [
'FahrzeugVerbunden' => $verbunden,
'FahrzeugGeladen' => $geladen,
'Fahrzeugstatus' => $fahrzeugstatus,
'Phasenzahl' => $phasen,
'Istleistung_W' => $leistung,
'Ladestrom_A' => $ladestrom,
];
}
public static function ermittleFahrzeugMaximalstrom(
int $vorgegebenerStrom,
float $gemessenerStrom,
int $konfigurierterMaximalstrom
): ?int {
if ($konfigurierterMaximalstrom < 6 || $konfigurierterMaximalstrom > 32) {
throw new InvalidArgumentException('MaximalerLadestrom muss zwischen 6 und 32 A liegen.');
}
if ($vorgegebenerStrom < 6 || $gemessenerStrom < 5.5) {
return null;
}
$kandidat = max(
6,
min($konfigurierterMaximalstrom, (int) floor($gemessenerStrom + 2.5))
);
if ($kandidat > $vorgegebenerStrom - 2 || $kandidat >= $konfigurierterMaximalstrom) {
return null;
}
return $kandidat;
}
/** @return list<int> */
public static function leistungsangebot(
bool $aktiv,
bool $ladefreigabe,
bool $verbunden,
bool $geladen,
int $phasenzahl,
int $maximalerLadestrom,
bool $solarladen,
bool $peakbetrieb
): array {
if (!$aktiv || !$ladefreigabe || !$verbunden || $geladen) {
return [0];
}
if ($phasenzahl === self::PHASEN_UNBEKANNT) {
// 6 A mit konservativem Dreiphasenbudget, niemals ein direkter Probebefehl.
return $solarladen && $peakbetrieb ? [0] : [0, self::PHASENERKENNUNGSLEISTUNG];
}
$stufen = self::leistungsstufen($phasenzahl, $maximalerLadestrom);
if ($stufen === []) {
return [0];
}
if ($solarladen) {
return $peakbetrieb ? [0] : array_merge([0], $stufen);
}
if ($peakbetrieb) {
return array_merge([0], $stufen);
}
return [$stufen[count($stufen) - 1]];
}
public static function stromFuerLeistung(int $leistung, int $phasenzahl): int
{
if ($leistung <= 0) {
return 0;
}
if ($phasenzahl === self::PHASEN_UNBEKANNT) {
return $leistung >= self::PHASENERKENNUNGSLEISTUNG ? 6 : 0;
}
$stufen = self::stufenFuerPhasen($phasenzahl);
if ($leistung < $stufen[0]) {
return 0;
}
$strom = 6;
foreach ($stufen as $index => $schwelle) {
if ($leistung < $schwelle) {
break;
}
$strom = 6 + $index;
}
return min(32, $strom);
}
/** @return list<int> */
private static function leistungsstufen(int $phasenzahl, int $maximalerLadestrom): array
{
if ($maximalerLadestrom < 6 || $maximalerLadestrom > 32) {
throw new InvalidArgumentException('MaximalerLadestrom muss zwischen 6 und 32 A liegen.');
}
return array_slice(self::stufenFuerPhasen($phasenzahl), 0, $maximalerLadestrom - 5);
}
/** @return list<int> */
private static function stufenFuerPhasen(int $phasenzahl): array
{
if ($phasenzahl === self::PHASEN_EIN) {
$leistungProAmpere = self::LEISTUNG_PRO_AMPERE_EINPHASIG;
} elseif ($phasenzahl === self::PHASEN_DREI) {
$leistungProAmpere = self::LEISTUNG_PRO_AMPERE_DREIPHASIG;
} else {
throw new InvalidArgumentException('Phasenzahl muss 1 oder 3 sein.');
}
return array_map(
static fn (int $strom): int => $strom * $leistungProAmpere,
range(6, 32)
);
}
}