Files
Enelix-EMS/tests/ManagerSDLTest.php
T
2026-10-06 17:40:45 +00:00

488 lines
20 KiB
PHP

<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS\Tests;
use InvalidArgumentException;
use Manager;
use ManagerSDLRuntime;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use RuntimeException;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
final class ManagerSDLTest extends TestCase
{
private Manager $manager;
protected function setUp(): void
{
require_once __DIR__ . '/Support/ManagerSDLRuntime.php';
require_once __DIR__ . '/../Manager/module.php';
ManagerSDLRuntime::reset();
$this->manager = new Manager(42);
$this->manager->Create();
foreach ([101 => 1000.0, 102 => 5000.0, 103 => 200.0, 104 => 1500.0, 105 => 70.0] as $id => $value) {
ManagerSDLRuntime::variable($id, $value);
}
$this->properties([
'NetzleistungVariableID' => 101,
'AnlagenWechselrichter' => $this->json([
['ID' => 'pv', 'Typ' => 'pv', 'IstleistungVariableID' => 102],
]),
'AnlagenBatterien' => $this->json([
['ID' => 'battery', 'LeistungVariableID' => 103],
]),
'SDLLeistungVariableID' => 104,
'SDLSOCVariableID' => 105,
]);
foreach ([
'SDLLeistungArchiv', 'SDLSOCArchiv', 'SDLLadenEnergie', 'SDLEntladenEnergie',
] as $ident) {
ManagerSDLRuntime::child(42, $ident);
}
ManagerSDLRuntime::child(42, 'SDLStatus', 3);
}
public function testSDLByDefaultInactiveDoesNotRequireSources(): void
{
self::assertFalse($this->manager->testProperties['SDLAktiv']);
self::assertFalse($this->manager->testProperties['SDLEnergieflussAnzeigen']);
self::assertFalse($this->manager->testProperties['SDLDiagrammeAnzeigen']);
unset(ManagerSDLRuntime::$variables[104], ManagerSDLRuntime::$variables[105]);
$this->invoke('pruefeSDLKonfiguration');
$powers = $this->invoke('leseMessleistungen');
self::assertSame(0.0, $powers['SDL']);
self::assertSame(5800.0, $powers['Haus']);
self::assertNull($this->invoke('aktuelleSDLSOC'));
}
/** @dataProvider signedPowerProvider */
public function testSDLPowerIsExcludedFromHouseConsumption(float $raw, float $factor, float $sdl, float $house): void
{
$this->properties(['SDLAktiv' => true, 'SDLLeistungsfaktor' => $factor]);
ManagerSDLRuntime::variable(104, $raw);
$this->invoke('pruefeSDLKonfiguration');
self::assertSame([
'PV' => 5000.0, 'Haus' => $house, 'Netz' => 1000.0, 'Batterie' => 200.0, 'SDL' => $sdl,
], $this->invoke('leseMessleistungen'));
}
public static function signedPowerProvider(): array
{
return [
'charging' => [1500.0, 1.0, 1500.0, 4300.0],
'discharging' => [-1500.0, 1.0, -1500.0, 7300.0],
'kilowatts' => [2.5, 1000.0, 2500.0, 3300.0],
'inverted source' => [1500.0, -1.0, -1500.0, 7300.0],
];
}
/** @dataProvider duplicatePowerProvider */
public function testRejectsSDLPowerAlsoUsedByAnotherEnergySource(int $duplicateID): void
{
$this->properties(['SDLAktiv' => true, 'SDLLeistungVariableID' => $duplicateID]);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('nicht zugleich');
$this->invoke('pruefeSDLKonfiguration');
}
public static function duplicatePowerProvider(): array
{
return ['grid' => [101], 'PV' => [102], 'battery' => [103]];
}
public function testRejectsSOCUsingThePowerVariable(): void
{
$this->properties(['SDLAktiv' => true, 'SDLSOCVariableID' => 104]);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('separate numerische Variable');
$this->invoke('pruefeSDLKonfiguration');
}
/** @dataProvider invalidFactorProvider */
public function testRejectsInvalidSDLFactors(float $factor): void
{
$this->properties(['SDLAktiv' => true, 'SDLLeistungsfaktor' => $factor]);
$this->expectException(InvalidArgumentException::class);
$this->invoke('pruefeSDLKonfiguration');
}
public static function invalidFactorProvider(): array
{
return ['zero' => [0.0], 'infinite' => [INF], 'not a number' => [NAN]];
}
public function testMissingSDLPowerFailsWithoutReturningHouseConsumption(): void
{
$this->properties(['SDLAktiv' => true]);
unset(ManagerSDLRuntime::$variables[104]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('nicht verfuegbar');
$this->invoke('leseMessleistungen');
}
/** @dataProvider invalidPowerProvider */
public function testNonfiniteOrNonnumericPowerDoesNotBecomeZero($value): void
{
$this->properties(['SDLAktiv' => true]);
ManagerSDLRuntime::variable(104, $value);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('keine endliche Zahl');
$this->invoke('leseMessleistungen');
}
public static function invalidPowerProvider(): array
{
return ['infinite' => [INF], 'negative infinite' => [-INF], 'not a number' => [NAN], 'string' => ['1500']];
}
public function testScaledSDLPowerOverflowIsRejected(): void
{
$this->properties(['SDLAktiv' => true, 'SDLLeistungsfaktor' => 2.0]);
ManagerSDLRuntime::variable(104, PHP_FLOAT_MAX);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('skalierte SDL-Istleistung');
$this->invoke('leseMessleistungen');
}
public function testSDLPowerMustBeANumericVariable(): void
{
$this->properties(['SDLAktiv' => true]);
ManagerSDLRuntime::variable(104, '1500', 3);
$this->expectException(InvalidArgumentException::class);
$this->invoke('pruefeSDLKonfiguration');
}
/** @dataProvider socProvider */
public function testSDLSOCBoundaries(float $value, ?float $expected): void
{
$this->properties(['SDLAktiv' => true]);
ManagerSDLRuntime::variable(105, $value);
self::assertSame($expected, $this->invoke('aktuelleSDLSOC'));
if ($expected === null) {
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
}
}
public static function socProvider(): array
{
return [
'empty' => [0.0, 0.0], 'full' => [100.0, 100.0], 'normal' => [37.5, 37.5],
'negative' => [-0.1, null], 'overfull' => [100.1, null], 'infinite' => [INF, null], 'NaN' => [NAN, null],
];
}
public function testSDLSOCMayBeOmittedAndDisappearingSourceIsIgnored(): void
{
$this->properties(['SDLAktiv' => true, 'SDLSOCVariableID' => 0]);
$this->invoke('pruefeSDLKonfiguration');
self::assertNull($this->invoke('aktuelleSDLSOC'));
$this->properties(['SDLSOCVariableID' => 105]);
unset(ManagerSDLRuntime::$variables[105]);
self::assertNull($this->invoke('aktuelleSDLSOC'));
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
}
public function testPowerReadingPreservesSOCErrorUntilRecovery(): void
{
$this->properties(['SDLAktiv' => true]);
ManagerSDLRuntime::variable(105, 101.0);
$this->invoke('aktuelleSDLLeistung');
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
$this->invoke('aktuelleSDLLeistung');
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
ManagerSDLRuntime::variable(105, 47.5);
$this->invoke('aktuelleSDLLeistung');
self::assertSame('Aktiv', $this->manager->GetValue('SDLStatus'));
}
public function testSDLSOCDoesNotChangeWeightedNormalBatterySOC(): void
{
ManagerSDLRuntime::variable(106, 20.0);
ManagerSDLRuntime::variable(107, 80.0);
$this->properties([
'SDLAktiv' => true,
'AnlagenBatterien' => $this->json([
['ID' => 'small', 'SOCVariableID' => 106, 'Nutzkapazitaet_kWh' => 10.0],
['ID' => 'large', 'SOCVariableID' => 107, 'Nutzkapazitaet_kWh' => 30.0],
]),
]);
self::assertSame(65.0, $this->invoke('aktuelleBatterieSoc'));
self::assertSame(70.0, $this->invoke('aktuelleSDLSOC'));
ManagerSDLRuntime::variable(105, 0.0);
self::assertSame(65.0, $this->invoke('aktuelleBatterieSoc'));
}
public function testCounterDeltasExcludeSDLChargingAndIncludeDischarging(): void
{
$this->configureCounters();
$baseline = $this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]);
self::assertCount(8, $baseline);
self::assertSame(array_fill_keys(array_keys($baseline), 0.0), $baseline);
$this->advanceCounters();
$deltas = $this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]);
self::assertEqualsWithDelta(2.1, $deltas['Hausenergie'], 0.000001);
self::assertEqualsWithDelta(2.0, $deltas['PVEnergie'], 0.000001);
self::assertSame(0.6, $deltas['SDLLadenEnergie']);
self::assertSame(0.2, $deltas['SDLEntladenEnergie']);
self::assertEqualsWithDelta(0.4, $deltas['BatterieLadenEnergie'], 0.000001);
self::assertEqualsWithDelta(0.1, $deltas['BatterieEntladenEnergie'], 0.000001);
}
public function testMissingSDLIntervalDoesNotPolluteHouseEnergyOrTheNextInterval(): void
{
$this->configureCounters();
$this->invoke('leseEnergieZaehlerDeltas');
$this->advanceCounters();
$deltas = $this->invoke('leseEnergieZaehlerDeltas', [0.0, 0.0, false]);
self::assertSame(0.0, $deltas['Hausenergie']);
self::assertEqualsWithDelta(2.0, $deltas['PVEnergie'], 0.000001);
self::assertEqualsWithDelta(1.0, $deltas['NetzbezugEnergie'], 0.000001);
ManagerSDLRuntime::variable(201, 102.5);
$next = $this->invoke('leseEnergieZaehlerDeltas', [0.1, 0.0, true]);
self::assertEqualsWithDelta(0.4, $next['Hausenergie'], 0.000001);
}
public function testCounterResetSuppressesHouseEnergyForActiveSDL(): void
{
$this->configureCounters();
$this->invoke('leseEnergieZaehlerDeltas');
$this->advanceCounters();
ManagerSDLRuntime::variable(202, 0.0);
$deltas = $this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]);
self::assertSame(0.0, $deltas['Hausenergie']);
self::assertSame(0.0, $deltas['NetzbezugEnergie']);
self::assertSame(0.6, $deltas['SDLLadenEnergie']);
}
public function testWithoutConfiguredCountersReturnsNull(): void
{
$this->properties(['SDLAktiv' => true]);
self::assertNull($this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]));
}
public function testChangingSDLSourceResetsBaselinesButKeepsCollectedEnergy(): void
{
$this->properties(['SDLAktiv' => true]);
$this->invoke('synchronisiereSDLMesskonfiguration');
$this->manager->testAttributes['MessungLetzterZeitstempel'] = 123;
$this->manager->testAttributes['MessungLetzteLeistungsaufzeichnung'] = 100;
$this->manager->testAttributes['MessungZaehlerstaende'] = '{"pv:pv":5.0}';
$this->manager->testAttributes['MessungEnergiePuffer'] = '{"Hausenergie":1.5}';
$this->invoke('synchronisiereSDLMesskonfiguration');
self::assertSame(123, $this->manager->testAttributes['MessungLetzterZeitstempel']);
$this->properties(['SDLLeistungsfaktor' => -1.0]);
$this->invoke('synchronisiereSDLMesskonfiguration');
self::assertSame(0, $this->manager->testAttributes['MessungLetzterZeitstempel']);
self::assertSame(0, $this->manager->testAttributes['MessungLetzteLeistungsaufzeichnung']);
self::assertSame('{}', $this->manager->testAttributes['MessungZaehlerstaende']);
self::assertSame('{"Hausenergie":1.5}', $this->manager->testAttributes['MessungEnergiePuffer']);
}
/** @dataProvider chartProvider */
public function testChartMergePreservesCustomizationsAndOnlyRemovesSDL(bool $energy): void
{
$this->properties(['SDLAktiv' => true, 'SDLDiagrammeAnzeigen' => true]);
$powerIdent = $energy ? 'SDLLadenEnergie' : 'SDLLeistungArchiv';
$custom = ['variableID' => 999, 'title' => 'Custom series', 'strokeColor' => '#ABCDEF'];
$sdl = ['variableID' => $this->manager->GetIDForIdent($powerIdent), 'title' => 'Own SDL name', 'visible' => false];
ManagerSDLRuntime::$media[301] = base64_encode($this->json([
'datasets' => [$custom, $sdl], 'customOption' => ['keep' => true],
]));
$this->invoke('synchronisiereSDLDiagramm', [301, $energy]);
$chart = $this->chart();
self::assertSame($custom, $chart['datasets'][0]);
self::assertSame($sdl, $chart['datasets'][1]);
self::assertSame(['keep' => true], $chart['customOption']);
self::assertCount($energy ? 4 : 3, $chart['datasets']);
$soc = $chart['datasets'][count($chart['datasets']) - 1];
self::assertSame($this->manager->GetIDForIdent('SDLSOCArchiv'), $soc['variableID']);
self::assertSame('right', $soc['side']);
if ($energy) {
self::assertSame('bar', $chart['datasets'][2]['type']);
self::assertSame($this->manager->GetIDForIdent('SDLEntladenEnergie'), $chart['datasets'][2]['variableID']);
}
$writes = ManagerSDLRuntime::$writes;
$this->invoke('synchronisiereSDLDiagramm', [301, $energy]);
self::assertSame($writes, ManagerSDLRuntime::$writes);
$this->properties(['SDLDiagrammeAnzeigen' => false]);
$this->invoke('synchronisiereSDLDiagramm', [301, $energy]);
self::assertSame([$custom], $this->chart()['datasets']);
self::assertSame(['keep' => true], $this->chart()['customOption']);
}
public static function chartProvider(): array
{
return ['power chart' => [false], 'energy chart' => [true]];
}
public function testEnergyFlowMergePreservesCustomNodesAndIsIdempotent(): void
{
$this->properties(['SDLAktiv' => true, 'SDLEnergieflussAnzeigen' => true]);
$custom = ['VariableID' => 999, 'Name' => 'Custom consumer', 'Type' => 'consumer', 'Invert' => true];
ManagerSDLRuntime::$properties[302]['Config'] = $this->json([$custom]);
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
$nodes = $this->flow();
self::assertSame($custom, $nodes[0]);
self::assertCount(2, $nodes);
self::assertSame('storage', $nodes[1]['Type']);
self::assertSame($this->manager->GetIDForIdent('EnergieflussSDL'), $nodes[1]['VariableID']);
self::assertSame($this->manager->GetIDForIdent('SDLSOCArchiv'), $nodes[1]['InfoVariableID']);
self::assertTrue(ManagerSDLRuntime::$hidden[$nodes[1]['VariableID']]);
$nodes[1]['Name'] = 'My SDL';
$nodes[1]['Invert'] = true;
ManagerSDLRuntime::$properties[302]['Config'] = $this->json($nodes);
$writes = ManagerSDLRuntime::$writes;
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
self::assertSame($nodes, $this->flow());
self::assertSame($writes, ManagerSDLRuntime::$writes);
$this->properties(['SDLSOCVariableID' => 0]);
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
self::assertSame(1, $this->flow()[1]['InfoVariableID']);
self::assertSame('My SDL', $this->flow()[1]['Name']);
$this->properties(['SDLAktiv' => false]);
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
self::assertSame([$custom], $this->flow());
}
private function configureCounters(): void
{
$this->properties([
'SDLAktiv' => true,
'NetzbezugEnergieVariableID' => 202,
'NetzeinspeisungEnergieVariableID' => 203,
'AnlagenWechselrichter' => $this->json([
['ID' => 'pv', 'Typ' => 'pv', 'IstleistungVariableID' => 102, 'ErzeugungsenergieVariableID' => 201],
]),
'AnlagenBatterien' => $this->json([
['ID' => 'battery', 'LeistungVariableID' => 103, 'LadeenergieVariableID' => 204, 'EntladeenergieVariableID' => 205],
]),
]);
foreach ([201 => 100.0, 202 => 10.0, 203 => 5.0, 204 => 10.0, 205 => 8.0] as $id => $value) {
ManagerSDLRuntime::variable($id, $value);
}
}
public function testEnergyPieAttributionTracksSDLWithoutChangingCustomSettings(): void
{
$custom = [
'ProduktionZaehlerID' => 801,
'NetzbezugZaehlerID' => 802,
'EinspeisungZaehlerID' => 803,
'VerbrauchZaehlerID' => 804,
'FarbeEigenverbrauch' => '#123456',
'FarbeAutarkie' => '#ABCDEF',
'Standardzeitraum' => 'month',
'EnergieanteileBerechenbar' => true,
];
ManagerSDLRuntime::$properties[303] = $custom;
$this->properties(['SDLAktiv' => true]);
$this->invoke('synchronisiereEnergyPieAnteile', [303]);
self::assertSame(array_replace($custom, ['EnergieanteileBerechenbar' => false]),
ManagerSDLRuntime::$properties[303]);
$writes = ManagerSDLRuntime::$writes;
$this->invoke('synchronisiereEnergyPieAnteile', [303]);
self::assertSame($writes, ManagerSDLRuntime::$writes);
$this->properties(['SDLAktiv' => false]);
$this->invoke('synchronisiereEnergyPieAnteile', [303]);
self::assertSame($custom, ManagerSDLRuntime::$properties[303]);
}
public function testOlderEnergyPieStaysVisibleWithoutSDLAndKeepsConfiguration(): void
{
$custom = ['ProduktionZaehlerID' => 801, 'FarbeEigenverbrauch' => '#123456'];
ManagerSDLRuntime::$properties[303] = $custom;
$this->properties(['SDLAktiv' => false]);
$this->invoke('synchronisiereEnergyPieAnteile', [303]);
self::assertSame($custom, ManagerSDLRuntime::$properties[303]);
self::assertFalse(ManagerSDLRuntime::$hidden[303]);
self::assertSame([], ManagerSDLRuntime::$logs);
}
public function testOlderEnergyPieIsHiddenWithSDLAndRestoredAfterUtilsUpdate(): void
{
$custom = ['ProduktionZaehlerID' => 801, 'FarbeEigenverbrauch' => '#123456'];
ManagerSDLRuntime::$properties[303] = $custom;
$this->properties(['SDLAktiv' => true]);
$this->invoke('synchronisiereEnergyPieAnteile', [303]);
self::assertSame($custom, ManagerSDLRuntime::$properties[303]);
self::assertTrue(ManagerSDLRuntime::$hidden[303]);
self::assertCount(1, ManagerSDLRuntime::$logs);
self::assertStringContainsString('Enelix Utils aktualisieren', ManagerSDLRuntime::$logs[0][1]);
ManagerSDLRuntime::$properties[303]['EnergieanteileBerechenbar'] = true;
$this->invoke('synchronisiereEnergyPieAnteile', [303]);
self::assertFalse(ManagerSDLRuntime::$hidden[303]);
self::assertSame(array_replace($custom, ['EnergieanteileBerechenbar' => false]),
ManagerSDLRuntime::$properties[303]);
self::assertCount(1, ManagerSDLRuntime::$logs);
}
private function advanceCounters(): void
{
foreach ([201 => 102.0, 202 => 11.0, 203 => 5.2, 204 => 10.4, 205 => 8.1] as $id => $value) {
ManagerSDLRuntime::variable($id, $value);
}
}
private function properties(array $properties): void
{
$this->manager->testProperties = array_replace($this->manager->testProperties, $properties);
}
private function invoke(string $name, array $arguments = [])
{
$method = new ReflectionMethod(Manager::class, $name);
if (PHP_VERSION_ID < 80100) {
$method->setAccessible(true);
}
return $method->invokeArgs($this->manager, $arguments);
}
private function json(array $value): string
{
return json_encode($value, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION);
}
private function chart(): array
{
return json_decode(base64_decode(ManagerSDLRuntime::$media[301]), true, 512, JSON_THROW_ON_ERROR);
}
private function flow(): array
{
return json_decode(ManagerSDLRuntime::$properties[302]['Config'], true, 512, JSON_THROW_ON_ERROR);
}
}