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
+116 -2
View File
@@ -10,6 +10,7 @@ use PHPUnit\Framework\TestCase;
final class LadestationAdapterTest extends TestCase
{
private const JETZT = 1791392400;
/**
* @dataProvider statusFaelle
* @param array<string, string> $konfiguration
@@ -33,7 +34,8 @@ final class LadestationAdapterTest extends TestCase
$rohstatus = LadestationAdapter::dekodiereStatus(
$geraetetyp,
$transport->statusLesen($geraetetyp, $konfiguration)
$transport->statusLesen($geraetetyp, $konfiguration),
self::JETZT
);
self::assertSame($erwarteteUrl, $aufrufe[0]['URL']);
@@ -52,6 +54,7 @@ final class LadestationAdapterTest extends TestCase
[
'Rohstatus' => 2,
'Istleistung_W' => 11000.0,
'LadeendeBestaetigt' => false,
'Sollstrom_A' => 16,
'Maximalstrom_A' => 16,
'Iststrom_A' => 16.0,
@@ -66,6 +69,7 @@ final class LadestationAdapterTest extends TestCase
[
'Rohstatus' => 2,
'Istleistung_W' => 2300.0,
'LadeendeBestaetigt' => false,
'Sollstrom_A' => 16,
'Maximalstrom_A' => 16,
'Iststrom_A' => 9.8,
@@ -79,9 +83,12 @@ final class LadestationAdapterTest extends TestCase
'State' => 2,
'ActiveChargingPower' => 11.0,
'MaxAllowedChargingCurrent' => 16,
'LastSeen' => gmdate('Y-m-d\TH:i:s\Z', self::JETZT),
'ValueDate' => gmdate('Y-m-d\TH:i:s\Z', self::JETZT),
], JSON_THROW_ON_ERROR),
'https://api.smart-me.com/pico/charging/pico-id',
['Rohstatus' => 2, 'Istleistung_W' => 11000.0],
['Rohstatus' => 2, 'Istleistung_W' => 11000.0, 'LadeendeBestaetigt' => false,
'Ladebereit' => true, 'GueltigBis' => self::JETZT + 120, 'Geraetehinweis' => ''],
];
}
@@ -99,6 +106,112 @@ final class LadestationAdapterTest extends TestCase
self::assertSame(0, $status['Sollstrom_A']);
}
/** @dataProvider fehlerhafteGoEStatus */
public function testGoEFehlerUndUnbekannteZustaendeWerdenAbgewiesen($car, $err): void
{
$daten = json_decode($this->goEAntwort(2, 0, [0, 0, 0]), true, 512, JSON_THROW_ON_ERROR);
$daten['car'] = $car;
$daten['err'] = $err;
$this->expectException(\RuntimeException::class);
LadestationAdapter::dekodiereStatus(2, json_encode($daten, JSON_THROW_ON_ERROR));
}
public function fehlerhafteGoEStatus(): iterable
{
yield [2, 11];
yield [2, null];
yield [2, 'ungueltig'];
yield [0, 0];
yield [5, 0];
yield [2.5, 0];
}
public function testGoELadeendeIstExplizitUndNichtAusLeistungAbgeleitet(): void
{
foreach ([1 => false, 2 => false, 3 => false, 4 => true] as $car => $ende) {
$status = LadestationAdapter::dekodiereStatus(2, $this->goEAntwort($car, 0, [0, 0, 0]));
self::assertSame($ende, $status['LadeendeBestaetigt']);
}
}
public function testPicoVerwendetFrischePhasenstroemeUndStationsgrenze(): void
{
$status = LadestationAdapter::dekodiereStatus(3, $this->picoAntwort([
'ActiveChargingPower' => 4.104, 'CurrentL1' => 6.0, 'CurrentL2' => 6.0,
'CurrentL3' => 6.0, 'MaxStationCurrent' => 16, 'MaxDynamicCurrent' => 6,
'MaxAllowedChargingCurrent' => 0,
]), self::JETZT);
self::assertSame(3, $status['Phasenzahl']);
self::assertSame(6.0, $status['Iststrom_A']);
self::assertSame(16, $status['Maximalstrom_A']);
self::assertSame(6, $status['Sollstrom_A']);
self::assertFalse($status['LadeendeBestaetigt']);
}
public function testPicoIdleAkzeptiertLeeresMessdatumNurBeiFrischemKontaktUndNullleistung(): void
{
$antwort = $this->picoAntwort(['State' => 1, 'ValueDate' => '0001-01-01T00:00:00']);
$status = LadestationAdapter::dekodiereStatus(3, $antwort, self::JETZT);
self::assertSame(1, $status['Rohstatus']);
self::assertSame(0.0, $status['Istleistung_W']);
}
public function testPicoHistorischesEreignisIstHinweisUndKeineDauersperre(): void
{
$status = LadestationAdapter::dekodiereStatus(3, $this->picoAntwort([
'LastWarningOrError' => 19, 'LastWarningOrErrorTime' => '2020-01-01T00:00:00Z',
]), self::JETZT);
self::assertTrue($status['Ladebereit']);
self::assertStringContainsString('19 (historisch)', $status['Geraetehinweis']);
}
public function testPicoAutorisierungIstKeineLadefreigabe(): void
{
$status = LadestationAdapter::dekodiereStatus(3, $this->picoAntwort(['State' => 6]), self::JETZT);
self::assertFalse($status['Ladebereit']);
self::assertStringContainsString('Autorisierung', $status['Geraetehinweis']);
}
public function testPicoAkzeptiertDokumentierteCamelCaseAntwortUndSekundenbruchteile(): void
{
$daten = json_decode($this->picoAntwort(), true, 512, JSON_THROW_ON_ERROR);
$camel = [];
foreach ($daten as $name => $wert) { $camel[lcfirst($name)] = $wert; }
$camel['lastSeen'] = gmdate('Y-m-d\TH:i:s', self::JETZT) . '.1234567Z';
$status = LadestationAdapter::dekodiereStatus(3, json_encode($camel, JSON_THROW_ON_ERROR), self::JETZT);
self::assertSame(self::JETZT + 120, $status['GueltigBis']);
}
/** @dataProvider fehlerhaftePicoStatus */
public function testPicoVeralteteOderUngueltigeStatusdatenWerdenAbgewiesen(array $aenderungen): void
{
$this->expectException(\RuntimeException::class);
LadestationAdapter::dekodiereStatus(3, $this->picoAntwort($aenderungen), self::JETZT);
}
public function fehlerhaftePicoStatus(): iterable
{
yield 'Kontakt alt' => [['LastSeen' => gmdate('Y-m-d\TH:i:s\Z', self::JETZT - 121)]];
yield 'Messung alt' => [['ValueDate' => gmdate('Y-m-d\TH:i:s\Z', self::JETZT - 121)]];
yield 'Zukunft' => [['LastSeen' => gmdate('Y-m-d\TH:i:s\Z', self::JETZT + 16)]];
yield 'Zeitzone fehlt' => [['LastSeen' => '2026-10-07T19:40:00']];
yield 'Datum fehlt' => [['LastSeen' => null, 'ValueDate' => null]];
yield 'Offline' => [['State' => 7]];
yield 'Boot' => [['State' => 0]];
yield 'Installation' => [['State' => 5]];
yield 'Negativer Strom' => [['CurrentL1' => -1, 'CurrentL2' => 0, 'CurrentL3' => 0]];
yield 'Unendliche Leistung' => [['ActiveChargingPower' => '1e999']];
}
private function picoAntwort(array $aenderungen = []): string
{
return json_encode(array_replace([
'State' => 4, 'ActiveChargingPower' => 0.0,
'ValueDate' => gmdate('Y-m-d\TH:i:s\Z', self::JETZT),
'LastSeen' => gmdate('Y-m-d\TH:i:s\Z', self::JETZT),
], $aenderungen), JSON_THROW_ON_ERROR);
}
/**
* @dataProvider steuerFaelle
* @param array<string, string> $konfiguration
@@ -200,6 +313,7 @@ final class LadestationAdapterTest extends TestCase
return json_encode([
'car' => $status,
'err' => 0,
'nrg' => $nrg,
'amp' => 16,
'ama' => 16,
+53 -7
View File
@@ -71,7 +71,7 @@ final class LadestationReglerTest extends TestCase
], false);
self::assertSame(LadestationRegler::PHASEN_UNBEKANNT, $status['Phasenzahl']);
self::assertSame([0], LadestationRegler::leistungsangebot(
self::assertSame([0, 4104], LadestationRegler::leistungsangebot(
true,
true,
true,
@@ -94,15 +94,15 @@ final class LadestationReglerTest extends TestCase
self::assertSame(2, $status['Fahrzeugstatus']);
}
public function testGeladenWirdErstNachZuvorGemessenerLadeleistungErkannt(): void
public function testNiedrigstromNachLadungIstKeinBestaetigtesLadeende(): void
{
$status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2,
'Istleistung_W' => 0.0,
], true, false, true, true);
self::assertTrue($status['FahrzeugGeladen']);
self::assertSame(4, $status['Fahrzeugstatus']);
self::assertFalse($status['FahrzeugGeladen']);
self::assertSame(2, $status['Fahrzeugstatus']);
}
public function testAbsichtlichGestoppteLadungWirdNichtAlsGeladenErkannt(): void
@@ -116,15 +116,61 @@ final class LadestationReglerTest extends TestCase
self::assertSame(2, $status['Fahrzeugstatus']);
}
public function testErkanntesLadeendeBleibtBisZumAbsteckenErhalten(): void
public function testAltesLadeendeOhneAktuelleBestaetigungWirdVerworfen(): void
{
$status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2,
'Istleistung_W' => 0.0,
], true, true, false);
self::assertTrue($status['FahrzeugGeladen']);
self::assertSame(4, $status['Fahrzeugstatus']);
self::assertFalse($status['FahrzeugGeladen']);
self::assertSame(2, $status['Fahrzeugstatus']);
}
public function testExplizitesLadeendeWirdBeiErneuterLadungZurueckgenommen(): void
{
$ende = ['Rohstatus' => 4, 'Istleistung_W' => 0.0, 'LadeendeBestaetigt' => true];
self::assertTrue(LadestationRegler::normalisiereStatus($ende, true)['FahrzeugGeladen']);
$ende['Istleistung_W'] = 1380.0;
self::assertFalse(LadestationRegler::normalisiereStatus($ende, true, true)['FahrzeugGeladen']);
$ende['Rohstatus'] = 1;
$ende['Istleistung_W'] = 0.0;
self::assertFalse(LadestationRegler::normalisiereStatus($ende, true, true)['FahrzeugGeladen']);
}
public function testFrischePhasenmessungKorrigiertGespeicherteEinphasigkeit(): void
{
$status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2, 'Istleistung_W' => 4104.0, 'Iststrom_A' => 6.0, 'Phasenzahl' => 3,
], true, false, true, true, 1);
self::assertSame(3, $status['Phasenzahl']);
}
public function testNullUndSchwacheLeistungBeweisenKeinePhase(): void
{
foreach ([0.0, 100.0, 800.0, 1264.0] as $leistung) {
self::assertSame(0, LadestationRegler::erkennePhasenzahlAusLeistung($leistung, 6));
}
}
public function testEinzelneAktivePhaseVerkleinertKeinBestaetigtesDreiphasenbudget(): void
{
$rohstatus = ['Rohstatus' => 2, 'Istleistung_W' => 1380.0, 'Phasenzahl' => 1];
self::assertSame(3, LadestationRegler::normalisiereStatus($rohstatus, true, false, true, true, 3)['Phasenzahl']);
self::assertSame(1, LadestationRegler::normalisiereStatus($rohstatus, false, false, true, true, 3)['Phasenzahl']);
$rohstatus['Istleistung_W'] = 11000.0;
self::assertSame(3, LadestationRegler::normalisiereStatus($rohstatus, false)['Phasenzahl']);
}
public function testUnbekanntePhasenHabenNurKonservativesStartbudget(): void
{
self::assertSame([0], LadestationRegler::leistungsangebot(true, true, true, false, 0, 16, true, true));
self::assertSame([0, 4104], LadestationRegler::leistungsangebot(true, true, true, false, 0, 16, false, true));
self::assertSame([0], LadestationRegler::leistungsangebot(false, true, true, false, 0, 16, false, false));
self::assertSame([0], LadestationRegler::leistungsangebot(true, false, true, false, 0, 16, false, false));
self::assertSame(0, LadestationRegler::stromFuerLeistung(4103, 0));
self::assertSame(6, LadestationRegler::stromFuerLeistung(4104, 0));
self::assertSame(6, LadestationRegler::stromFuerLeistung(22000, 0));
}
public function testDirekterPhasenstromHatVorrangVorNominalerLeistungsumrechnung(): void
+255
View File
@@ -0,0 +1,255 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS\Tests;
use PHPUnit\Framework\TestCase;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
final class LadestationStandAloneRuntimeTest extends TestCase
{
private function station(bool $solar = true, int $type = 2): \LadestationStandAlone
{
require_once __DIR__ . '/Support/LadestationRuntime.php';
require_once __DIR__ . '/../LadestationStandAlone/module.php';
$station = new \LadestationStandAlone(10001);
$station->Create();
$station->testProperties = array_replace($station->testProperties, [
'Geraetetyp' => $type, 'Geraeteadresse' => '192.0.2.10',
'GeraeteID' => 'test-pico', 'Seriennummer' => 'test-pico',
'Benutzername' => 'test', 'Passwort' => hash('sha256', __FILE__),
'EinstellungenInVisu' => true, 'DiagnosevariablenAnzeigen' => true,
'Testmodus' => true, 'Testantwort' => $type === 3 ? $this->pico() : $this->goe(0),
'Solarladen' => $solar, 'ZeitZwischenZustandswechseln' => 0,
]);
\LadestationRuntime::$managers[20001] = [
'AutomatischeSuche' => false,
'VerbraucherZuordnung' => json_encode([['InstanzID' => 10001, 'Aktiv' => true]], JSON_THROW_ON_ERROR),
];
$station->SetValue('Aktiv', true);
$station->ApplyChanges();
\LadestationRuntime::$commands = [];
return $station;
}
private function goe(float $power, array $currents = [0, 0, 0], int $car = 2, int $error = 0): string
{
$nrg = array_fill(0, 12, 0);
foreach ($currents as $i => $value) { $nrg[$i + 4] = $value; }
$nrg[11] = $power;
return json_encode(['car' => $car, 'err' => $error, 'nrg' => $nrg], JSON_THROW_ON_ERROR);
}
private function pico(array $changes = []): string
{
return json_encode(array_replace([
'State' => 4, 'ActiveChargingPower' => 0.0,
'LastSeen' => gmdate('Y-m-d\TH:i:s\Z'), 'ValueDate' => gmdate('Y-m-d\TH:i:s\Z'),
], $changes), JSON_THROW_ON_ERROR);
}
private function poll(\LadestationStandAlone $station, string $reply): void
{
$station->testProperties['Testantwort'] = $reply;
$station->RequestAction('Statuszyklus', false);
}
private function target(\LadestationStandAlone $station, ?int $power, string $mode = 'PV'): void
{
$station->ManagerdatenEmpfangen([
'Kopf' => ['Version' => '4.0', 'AbsenderID' => 20001, 'EmpfaengerID' => 10001, 'Zeitpunkt' => time()],
'Betriebsart' => $mode, 'Sollleistung_W' => $power,
]);
}
private function offer(\LadestationStandAlone $station): array
{
return json_decode($station->testAttributes['Leistungsangebot'], true, 512, JSON_THROW_ON_ERROR);
}
public function testKurzerNullwertNachLadenSperrtKeinenNeustart(): void
{
$station = $this->station();
$this->poll($station, $this->goe(10944, [16, 16, 16]));
$this->target($station, 10944);
$this->poll($station, $this->goe(10944, [16, 16, 16]));
$this->poll($station, $this->goe(0));
self::assertFalse($station->GetValue('FahrzeugGeladen'));
self::assertContains(10944, $this->offer($station));
$this->poll($station, $this->goe(10944, [16, 16, 16]));
self::assertFalse($station->GetValue('FahrzeugGeladen'));
}
public function testExplizitesLadeendeWirdBeiNeuLadendemFahrzeugAufgehoben(): void
{
$station = $this->station();
$this->poll($station, $this->goe(0, [0, 0, 0], 4));
self::assertTrue($station->GetValue('FahrzeugGeladen'));
self::assertSame([0], $this->offer($station));
$this->poll($station, $this->goe(4104, [6, 6, 6]));
self::assertFalse($station->GetValue('FahrzeugGeladen'));
self::assertContains(4104, $this->offer($station));
}
public function testPhasenprobeBrauchtManagerbudgetUndNullleistungBleibtUnbekannt(): void
{
$station = $this->station();
self::assertSame(0, $station->testAttributes['LetzterGesetzterStrom']);
self::assertSame([0, 4104], $this->offer($station));
$this->target($station, 4104);
self::assertSame(6, $station->testAttributes['LetzterGesetzterStrom']);
$station->testAttributes['PhasenerkennungSeit'] = time() - 70;
$this->poll($station, $this->goe(0));
self::assertSame(0, $station->GetValue('Phasenzahl'));
self::assertFalse($station->GetValue('FahrzeugGeladen'));
}
public function testPhasenprobeOhneManagerbudgetStartetAuchOhneSolarNicht(): void
{
$station = $this->station(false);
self::assertSame(0, $station->testAttributes['LetzterGesetzterStrom']);
$this->target($station, 4104, 'Peak');
self::assertSame(6, $station->testAttributes['LetzterGesetzterStrom']);
}
public function testSolarImPeakStartetKeineProbeUndBeendetLaufendeProbe(): void
{
$station = $this->station();
$this->target($station, null, 'Peak');
$this->poll($station, $this->goe(0));
self::assertSame([0], $this->offer($station));
self::assertSame(0, $station->testAttributes['LetzterGesetzterStrom']);
$this->target($station, 4104, 'PV');
self::assertSame(6, $station->testAttributes['LetzterGesetzterStrom']);
$this->target($station, null, 'Peak');
self::assertSame(0, $station->testAttributes['LetzterGesetzterStrom']);
}
public function testDeaktivierungStopptAuchWaerendUnbekannterPhasen(): void
{
$station = $this->station();
$this->target($station, 4104);
$station->RequestAction('Ladefreigabe', false);
self::assertSame(0, $station->testAttributes['LetzterGesetzterStrom']);
self::assertSame([0], $this->offer($station));
}
public function testFrischeDreiphasigkeitKorrigiertAltenEinphasenwertUndLeistungssperre(): void
{
$station = $this->station();
$this->poll($station, $this->goe(1380, [6, 0, 0]));
$this->target($station, 1380);
$station->testAttributes['LeistungswechselGesperrtBis'] = time() + 60;
$this->poll($station, $this->goe(4104, [6, 6, 6]));
self::assertSame(3, $station->GetValue('Phasenzahl'));
self::assertNotContains(1380, $this->offer($station));
self::assertContains(4104, $this->offer($station));
}
public function testGeraetefehlerBleibtBisFrischerErfolgreicherAbfrageGesperrt(): void
{
$station = $this->station();
$this->target($station, 4104);
$this->poll($station, $this->goe(0, [0, 0, 0], 2, 11));
self::assertSame(202, $station->testStatus);
self::assertSame([0], $this->offer($station));
self::assertSame(0, $station->testAttributes['LetzterGesetzterStrom']);
$this->target($station, null, 'PV');
self::assertTrue($station->GetValue('Stoerung'));
self::assertSame([0], $this->offer($station));
try {
$this->target($station, 4104);
self::fail('Positiver Auftrag trotz Fehler angenommen');
} catch (\InvalidArgumentException $expected) {
self::assertSame(0, $station->testAttributes['LetzterGesetzterStrom']);
}
$this->poll($station, $this->goe(0));
self::assertSame(102, $station->testStatus);
self::assertFalse($station->GetValue('Stoerung'));
}
public function testDreiphasenbudgetBleibtBeiEinzelnerBelasteterPhaseBisZumAusstecken(): void
{
$station = $this->station();
$this->poll($station, $this->goe(4104, [6, 6, 6]));
$this->poll($station, $this->goe(1380, [6, 0, 0]));
self::assertSame(3, $station->GetValue('Phasenzahl'));
$this->poll($station, $this->goe(0, [0, 0, 0], 1));
$this->poll($station, $this->goe(1380, [6, 0, 0]));
self::assertSame(1, $station->GetValue('Phasenzahl'));
}
public function testAbgelaufenerStatusWirdNichtDurchManagerdatenErneuert(): void
{
$station = $this->station();
$this->target($station, 4104);
$station->testAttributes['GeraetestatusGueltigBis'] = time() - 1;
$this->target($station, 0);
self::assertSame([0], $this->offer($station));
self::assertFalse($station->GetValue('Verfuegbar'));
self::assertSame(0, $station->testAttributes['LetzterGesetzterStrom']);
}
public function testMeldungMitAbgelaufenemCacheMeldetNichtVerfuegbarOhneHttpAbruf(): void
{
$station = $this->station();
$station->testAttributes['GeraetestatusGueltigBis'] = time() - 1;
$station->testProperties['Testantwort'] = 'UNGUELTIG';
$station->RequestAction('Melden', false);
self::assertSame([], \LadestationRuntime::$commands);
$message = end(\LadestationRuntime::$messages);
$data = json_decode($message[2], true, 512, JSON_THROW_ON_ERROR);
self::assertFalse($data['Verfuegbar']);
self::assertSame([0], $data['Leistungswerte_W']);
}
public function testReineModusmeldungStopptBeiAbgelaufenemCache(): void
{
$station = $this->station();
$this->target($station, 4104);
$station->testAttributes['GeraetestatusGueltigBis'] = time() - 1;
$this->target($station, null);
self::assertTrue($station->GetValue('Stoerung'));
self::assertSame(0, $station->testAttributes['LetzterGesetzterStrom']);
}
public function testPicoVeralteteMessungSperrtUndFrischeMessungStelltWiederHer(): void
{
$station = $this->station(true, 3);
$this->poll($station, $this->pico(['ValueDate' => '2020-01-01T00:00:00Z']));
self::assertTrue($station->GetValue('Stoerung'));
self::assertSame([0], $this->offer($station));
$this->target($station, null);
self::assertTrue($station->GetValue('Stoerung'));
$this->poll($station, $this->pico());
self::assertFalse($station->GetValue('Stoerung'));
}
public function testPicoAutorisierungUndHistorieWerdenGetrenntBehandelt(): void
{
$station = $this->station(true, 3);
$this->poll($station, $this->pico(['State' => 6, 'LastWarningOrError' => 19]));
self::assertSame([0], $this->offer($station));
self::assertFalse($station->GetValue('Stoerung'));
self::assertStringContainsString('Autorisierung', $station->GetValue('Geraetehinweis'));
$this->poll($station, $this->pico(['LastWarningOrError' => 19]));
self::assertSame([0, 4104], $this->offer($station));
self::assertStringContainsString('historisch', $station->GetValue('Geraetehinweis'));
}
public function testUpdateEntferntAlteUnbelegtePhasenUndVollErkennung(): void
{
$station = $this->station();
$station->testAttributes['PhasenerkennungVersion'] = 1;
$station->testAttributes['ZustandPhasenzahl'] = 1;
$station->testAttributes['ZustandFahrzeugGeladen'] = true;
$station->ApplyChanges();
self::assertSame(2, $station->testAttributes['PhasenerkennungVersion']);
self::assertSame(0, $station->GetValue('Phasenzahl'));
self::assertFalse($station->GetValue('FahrzeugGeladen'));
self::assertSame([0, 4104], $this->offer($station));
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
// Used only by isolated PHPUnit processes; never load inside a Symcon kernel.
if (function_exists('IPS_GetKernelVersion')) {
throw new RuntimeException('Der isolierte Test darf nicht in Symcon laufen.');
}
final class LadestationRuntime
{
public static array $variables = [];
public static array $children = [];
public static array $managers = [];
public static array $messages = [];
public static array $commands = [];
public static int $nextID = 40000;
}
class IPSModule
{
public int $InstanceID;
public array $testProperties = [];
public array $testAttributes = [];
public array $testTimers = [];
public int $testStatus = 0;
public function __construct(int $id) { $this->InstanceID = $id; }
public function Create(): void {}
public function ApplyChanges(): void {}
public function EnableAction(string $ident): void {}
public function SetStatus(int $status): void { $this->testStatus = $status; }
public function SetTimerInterval(string $ident, int $interval): void { $this->testTimers[$ident] = $interval; }
public function RegisterTimer(string $ident, int $interval, string $script): void { $this->SetTimerInterval($ident, $interval); }
public function SendDebug(string $sender, string $text, int $format): void {}
public function __call(string $method, array $arguments)
{
if (preg_match('/^(Register|Read|Write)(Property|Attribute)(Boolean|Integer|Float|String)$/', $method, $parts)) {
$storage = $parts[2] === 'Property' ? 'testProperties' : 'testAttributes';
$ident = $arguments[0];
if ($parts[1] === 'Register') {
$this->{$storage}[$ident] = $this->{$storage}[$ident] ?? $arguments[1];
return null;
}
if ($parts[1] === 'Write') {
$this->{$storage}[$ident] = $arguments[1];
return null;
}
if (!array_key_exists($ident, $this->{$storage})) {
throw new RuntimeException('Unregistered test property/attribute: ' . $ident);
}
return $this->{$storage}[$ident];
}
if (preg_match('/^RegisterVariable(Boolean|Integer|Float|String)$/', $method, $parts)) {
$ident = $arguments[0];
$id = LadestationRuntime::$children[$this->InstanceID][$ident] ?? ++LadestationRuntime::$nextID;
LadestationRuntime::$children[$this->InstanceID][$ident] = $id;
LadestationRuntime::$variables[$id] = LadestationRuntime::$variables[$id]
?? ['Boolean' => false, 'Integer' => 0, 'Float' => 0.0, 'String' => ''][$parts[1]];
return $id;
}
throw new BadMethodCallException('Unsupported test call: ' . $method);
}
public function GetIDForIdent(string $ident): int
{
return LadestationRuntime::$children[$this->InstanceID][$ident] ?? 0;
}
public function SetValue(string $ident, $value): void
{
$id = $this->GetIDForIdent($ident);
if (!$id) { throw new RuntimeException('Unregistered test variable: ' . $ident); }
LadestationRuntime::$variables[$id] = $value;
if ($ident === 'LetzterGeraetebefehl') {
LadestationRuntime::$commands[] = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
}
}
public function GetValue(string $ident) { return LadestationRuntime::$variables[$this->GetIDForIdent($ident)]; }
public function UnregisterVariable(string $ident): void
{
unset(LadestationRuntime::$variables[$this->GetIDForIdent($ident)], LadestationRuntime::$children[$this->InstanceID][$ident]);
}
}
function IPS_VariableExists(int $id): bool { return array_key_exists($id, LadestationRuntime::$variables); }
function IPS_GetInstanceListByModuleID(string $id): array { return array_keys(LadestationRuntime::$managers); }
function IPS_GetProperty(int $id, string $property) { return LadestationRuntime::$managers[$id][$property]; }
function IPS_RequestAction(int $id, string $ident, $value): bool
{
LadestationRuntime::$messages[] = [$id, $ident, $value];
return true;
}
+18 -6
View File
@@ -12,7 +12,7 @@ return static function (TestContext $test): void {
$nrg[4 + $index] = $strom;
}
$nrg[11] = $leistung;
return json_encode(['car' => $status, 'nrg' => $nrg], JSON_THROW_ON_ERROR);
return json_encode(['car' => $status, 'err' => 0, 'nrg' => $nrg], JSON_THROW_ON_ERROR);
};
$definitionen = [
@@ -37,9 +37,11 @@ return static function (TestContext $test): void {
'Benutzername' => 'api-user',
'Passwort' => $testPasswort,
'Testantwort' => json_encode([
'State' => 2,
'State' => 4,
'ActiveChargingPower' => 11.0,
'MaxAllowedChargingCurrent' => 16,
'MaxStationCurrent' => 16,
'LastSeen' => gmdate('Y-m-d\TH:i:s\Z'),
'ValueDate' => gmdate('Y-m-d\TH:i:s\Z'),
], JSON_THROW_ON_ERROR),
'Phasenzahl' => 3,
'BefehlUrlTeil' => 'current=16000',
@@ -452,9 +454,9 @@ return static function (TestContext $test): void {
));
});
$test->runCase('Neue Ladestation wird erst nach Freigabe mit Mindeststrom erkannt', static function (
$test->runCase('Phasenprobe braucht Managerbudget und deutet Nullleistung nicht als einphasig', static function (
TestContext $test
) use ($phasenerkennung, $anforderungSenden): void {
) use ($phasenerkennung, $anforderungSenden, $goEAntwort): void {
$stationId = $phasenerkennung;
$test->assertSame(
0,
@@ -471,13 +473,23 @@ return static function (TestContext $test): void {
0,
GetValue($test->objectByIdent('Phasenzahl', $stationId))
);
$test->assertSame('[0,4104]', GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId)));
$test->assertTrue(!str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
'amp=6'
), 'Die Freigabe allein ersetzt kein Managerbudget.');
$anforderungSenden($stationId, 4104);
$test->assertTrue(str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
'amp=6'
), 'Nach der Freigabe muss die Mindeststrom-Phasenprobe starten.');
), 'Nach Freigabe und Budget muss die Mindeststrom-Phasenprobe starten.');
IPS_Sleep(2100);
IPS_RequestAction($stationId, 'Statuszyklus', false);
$test->assertSame(0, GetValue($test->objectByIdent('Phasenzahl', $stationId)));
$test->assertSame(false, GetValue($test->objectByIdent('FahrzeugGeladen', $stationId)));
IPS_SetProperty($stationId, 'Testantwort', $goEAntwort(2, 1380, [6, 0, 0]));
IPS_ApplyChanges($stationId);
$anforderungSenden($stationId, 3680);
IPS_RequestAction($stationId, 'Statuszyklus', false);