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.
256 lines
11 KiB
PHP
256 lines
11 KiB
PHP
<?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));
|
|
}
|
|
}
|