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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user