Add opt-in SDL measurement sources and independent visualization toggles. Exclude balancing power from household telemetry and new household energy. Preserve custom views and cover measurement, migration and Symcon 8.0 behavior.
This commit is contained in:
@@ -24,6 +24,8 @@ final class EnergieMessungTest extends TestCase
|
||||
self::assertEqualsWithDelta(0.15, $werte['EinspeisungEnergie'], 0.000001);
|
||||
self::assertEqualsWithDelta(0.075, $werte['BatterieLadenEnergie'], 0.000001);
|
||||
self::assertSame(0.0, $werte['BatterieEntladenEnergie']);
|
||||
self::assertSame(0.0, $werte['SDLLadenEnergie']);
|
||||
self::assertSame(0.0, $werte['SDLEntladenEnergie']);
|
||||
}
|
||||
|
||||
public function testTrenntBezugUndBatterieentladung(): void
|
||||
@@ -50,23 +52,100 @@ final class EnergieMessungTest extends TestCase
|
||||
self::assertSame(0.0, EnergieMessung::hausleistung(500.0, -1200.0, 0.0));
|
||||
}
|
||||
|
||||
public function testBeruecksichtigtSDLVorDerBegrenzungDerHausleistung(): void
|
||||
{
|
||||
self::assertSame(1700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0, 1000.0));
|
||||
self::assertSame(3700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0, -1000.0));
|
||||
self::assertSame(1000.0, EnergieMessung::hausleistung(0.0, -1000.0, 0.0, -2000.0));
|
||||
self::assertSame(0.0, EnergieMessung::hausleistung(500.0, 0.0, 0.0, 1000.0));
|
||||
self::assertSame(
|
||||
EnergieMessung::hausleistung(4200.0, -800.0, 700.0),
|
||||
EnergieMessung::hausleistung(4200.0, -800.0, 700.0, 0.0)
|
||||
);
|
||||
}
|
||||
|
||||
public function testBerechnetHausenergieAusDenZaehlerdeltas(): void
|
||||
{
|
||||
self::assertSame(8.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0));
|
||||
self::assertSame(0.0, EnergieMessung::hausenergie(1.0, 0.0, 5.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
public function testBereinigtHausenergieUmSDLLadungUndEntladung(): void
|
||||
{
|
||||
self::assertSame(5.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 3.0, 0.0));
|
||||
self::assertSame(11.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 0.0, 3.0));
|
||||
self::assertSame(7.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 3.0, 2.0));
|
||||
self::assertSame(1.0, EnergieMessung::hausenergie(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0));
|
||||
self::assertSame(0.0, EnergieMessung::hausenergie(1.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0));
|
||||
self::assertSame(
|
||||
EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0),
|
||||
EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 0.0, 0.0)
|
||||
);
|
||||
}
|
||||
|
||||
public function testIntegriertSDLGetrenntVonBatterieUndHaus(): void
|
||||
{
|
||||
$leistungen = [
|
||||
'PV' => 7200.0,
|
||||
'Haus' => 3600.0,
|
||||
'Netz' => -1800.0,
|
||||
'Batterie' => 900.0,
|
||||
'SDL' => 1800.0,
|
||||
];
|
||||
$laden = EnergieMessung::energieDeltas($leistungen, 300);
|
||||
self::assertEqualsWithDelta(0.15, $laden['SDLLadenEnergie'], 0.000001);
|
||||
self::assertSame(0.0, $laden['SDLEntladenEnergie']);
|
||||
self::assertEqualsWithDelta(0.075, $laden['BatterieLadenEnergie'], 0.000001);
|
||||
self::assertEqualsWithDelta(0.3, $laden['Hausenergie'], 0.000001);
|
||||
|
||||
$leistungen['SDL'] = -1800.0;
|
||||
$entladen = EnergieMessung::energieDeltas($leistungen, 300);
|
||||
self::assertSame(0.0, $entladen['SDLLadenEnergie']);
|
||||
self::assertEqualsWithDelta(0.15, $entladen['SDLEntladenEnergie'], 0.000001);
|
||||
self::assertSame($laden['Hausenergie'], $entladen['Hausenergie']);
|
||||
self::assertSame($laden['BatterieLadenEnergie'], $entladen['BatterieLadenEnergie']);
|
||||
|
||||
$leistungen['SDL'] = 0.0;
|
||||
$mitNull = EnergieMessung::energieDeltas($leistungen, 300);
|
||||
unset($leistungen['SDL']);
|
||||
self::assertSame($mitNull, EnergieMessung::energieDeltas($leistungen, 300));
|
||||
}
|
||||
|
||||
public function testIgnoriertNichtEndlicheSDLWerte(): void
|
||||
{
|
||||
foreach ([INF, -INF, NAN] as $ungueltig) {
|
||||
self::assertSame(2700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0, $ungueltig));
|
||||
self::assertSame(8.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, $ungueltig, $ungueltig));
|
||||
$werte = EnergieMessung::energieDeltas([
|
||||
'PV' => 0.0,
|
||||
'Haus' => 1000.0,
|
||||
'Netz' => 1000.0,
|
||||
'Batterie' => 0.0,
|
||||
'SDL' => $ungueltig,
|
||||
], 3600);
|
||||
|
||||
self::assertSame(0.0, $werte['SDLLadenEnergie']);
|
||||
self::assertSame(0.0, $werte['SDLEntladenEnergie']);
|
||||
self::assertSame(1.0, $werte['Hausenergie']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testIgnoriertUngueltigeDauerUndNichtEndlicheWerte(): void
|
||||
{
|
||||
$werte = EnergieMessung::energieDeltas([
|
||||
'PV' => INF,
|
||||
'Haus' => NAN,
|
||||
'Netz' => 1000.0,
|
||||
'Batterie' => -1000.0,
|
||||
], -1);
|
||||
foreach ([-1, 0] as $dauer) {
|
||||
foreach ([-1000.0, 1000.0] as $sdl) {
|
||||
$werte = EnergieMessung::energieDeltas([
|
||||
'PV' => INF,
|
||||
'Haus' => NAN,
|
||||
'Netz' => 1000.0,
|
||||
'Batterie' => -1000.0,
|
||||
'SDL' => $sdl,
|
||||
], $dauer);
|
||||
|
||||
foreach ($werte as $wert) {
|
||||
self::assertSame(0.0, $wert);
|
||||
foreach ($werte as $wert) {
|
||||
self::assertSame(0.0, $wert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,12 @@ final class ManagerModulstrukturTest extends TestCase
|
||||
'EinspeisebegrenzungAktiv',
|
||||
'Einspeisegrenze',
|
||||
'PrognoseAktiv',
|
||||
'SDLAktiv',
|
||||
'SDLLeistungVariableID',
|
||||
'SDLLeistungsfaktor',
|
||||
'SDLSOCVariableID',
|
||||
'SDLEnergieflussAnzeigen',
|
||||
'SDLDiagrammeAnzeigen',
|
||||
'PrognoseLizenzInformation',
|
||||
'NetzfahrplanAktiv',
|
||||
'NetzbezugEnergieVariableID',
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/** Minimal Symcon runtime, loaded only inside isolated ManagerSDLTest processes. */
|
||||
final class ManagerSDLRuntime
|
||||
{
|
||||
public static array $variables = [];
|
||||
public static array $children = [];
|
||||
public static array $properties = [];
|
||||
public static array $media = [];
|
||||
public static array $writes = [];
|
||||
public static array $hidden = [];
|
||||
private static int $nextID = 10000;
|
||||
|
||||
public static function reset(): void
|
||||
{
|
||||
self::$variables = [];
|
||||
self::$children = [];
|
||||
self::$properties = [];
|
||||
self::$media = [];
|
||||
self::$writes = [];
|
||||
self::$hidden = [];
|
||||
self::$nextID = 10000;
|
||||
}
|
||||
|
||||
public static function variable(int $id, $value, int $type = 2): int
|
||||
{
|
||||
self::$variables[$id] = ['VariableType' => $type, 'Value' => $value];
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
public static function child(int $parent, string $ident, int $type = 2): int
|
||||
{
|
||||
if (!isset(self::$children[$parent][$ident])) {
|
||||
$id = self::$nextID++;
|
||||
self::$children[$parent][$ident] = $id;
|
||||
self::variable($id, $type === 3 ? '' : 0.0, $type);
|
||||
}
|
||||
|
||||
return self::$children[$parent][$ident];
|
||||
}
|
||||
}
|
||||
|
||||
class IPSModule
|
||||
{
|
||||
public int $InstanceID;
|
||||
public array $testProperties = [];
|
||||
public array $testAttributes = [];
|
||||
|
||||
public function __construct(int $instanceID)
|
||||
{
|
||||
$this->InstanceID = $instanceID;
|
||||
}
|
||||
|
||||
public function Create(): 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 or attribute: ' . $ident);
|
||||
}
|
||||
|
||||
return $this->{$storage}[$ident];
|
||||
}
|
||||
if (preg_match('/^RegisterVariable(Boolean|Integer|Float|String)$/', $method, $parts)) {
|
||||
$types = ['Boolean' => 0, 'Integer' => 1, 'Float' => 2, 'String' => 3];
|
||||
|
||||
return ManagerSDLRuntime::child($this->InstanceID, $arguments[0], $types[$parts[1]]);
|
||||
}
|
||||
|
||||
throw new BadMethodCallException('Unsupported Symcon test call: ' . $method);
|
||||
}
|
||||
|
||||
public function EnableAction(string $ident): void
|
||||
{
|
||||
}
|
||||
|
||||
public function RegisterTimer(string $ident, int $interval, string $script): void
|
||||
{
|
||||
}
|
||||
|
||||
public function GetIDForIdent(string $ident): int
|
||||
{
|
||||
if (!isset(ManagerSDLRuntime::$children[$this->InstanceID][$ident])) {
|
||||
throw new RuntimeException('Unknown test child: ' . $ident);
|
||||
}
|
||||
|
||||
return ManagerSDLRuntime::$children[$this->InstanceID][$ident];
|
||||
}
|
||||
|
||||
public function SetValue(string $ident, $value): void
|
||||
{
|
||||
ManagerSDLRuntime::$variables[$this->GetIDForIdent($ident)]['Value'] = $value;
|
||||
}
|
||||
|
||||
public function GetValue(string $ident)
|
||||
{
|
||||
return GetValue($this->GetIDForIdent($ident));
|
||||
}
|
||||
}
|
||||
|
||||
function IPS_VariableExists(int $variableID): bool
|
||||
{
|
||||
return isset(ManagerSDLRuntime::$variables[$variableID]);
|
||||
}
|
||||
|
||||
function IPS_GetVariable(int $variableID): array
|
||||
{
|
||||
if (!IPS_VariableExists($variableID)) {
|
||||
throw new RuntimeException('Unknown test variable: ' . $variableID);
|
||||
}
|
||||
|
||||
return ManagerSDLRuntime::$variables[$variableID];
|
||||
}
|
||||
|
||||
function GetValue(int $variableID)
|
||||
{
|
||||
return IPS_GetVariable($variableID)['Value'];
|
||||
}
|
||||
|
||||
function IPS_GetObjectIDByIdent(string $ident, int $parentID)
|
||||
{
|
||||
return ManagerSDLRuntime::$children[$parentID][$ident] ?? false;
|
||||
}
|
||||
|
||||
function IPS_GetProperty(int $instanceID, string $property)
|
||||
{
|
||||
return ManagerSDLRuntime::$properties[$instanceID][$property];
|
||||
}
|
||||
|
||||
function IPS_SetProperty(int $instanceID, string $property, $value): void
|
||||
{
|
||||
ManagerSDLRuntime::$properties[$instanceID][$property] = $value;
|
||||
ManagerSDLRuntime::$writes[] = ['property', $instanceID, $property];
|
||||
}
|
||||
|
||||
function IPS_ApplyChanges(int $instanceID): void
|
||||
{
|
||||
ManagerSDLRuntime::$writes[] = ['apply', $instanceID];
|
||||
}
|
||||
|
||||
function IPS_GetMediaContent(int $mediaID): string
|
||||
{
|
||||
return ManagerSDLRuntime::$media[$mediaID];
|
||||
}
|
||||
|
||||
function IPS_SetMediaContent(int $mediaID, string $content): void
|
||||
{
|
||||
ManagerSDLRuntime::$media[$mediaID] = $content;
|
||||
ManagerSDLRuntime::$writes[] = ['media', $mediaID];
|
||||
}
|
||||
|
||||
function IPS_SetHidden(int $objectID, bool $hidden): void
|
||||
{
|
||||
ManagerSDLRuntime::$hidden[$objectID] = $hidden;
|
||||
}
|
||||
@@ -431,6 +431,97 @@ return static function (TestContext $test): void {
|
||||
}
|
||||
});
|
||||
|
||||
$test->runCase('SDL korrigiert Hauslast und ergaenzt bestehende Anzeigen idempotent', static function (
|
||||
TestContext $test
|
||||
) use ($managerId): void {
|
||||
$leistungID = $test->createVariable(2, 'SDL Istleistung', -1.2);
|
||||
$socID = $test->createVariable(2, 'SDL SOC', 47.5);
|
||||
$flussID = $test->objectByIdent('Energiefluss', $managerId);
|
||||
$chartID = $test->objectByIdent('Leistungsdiagramm', $managerId);
|
||||
$flussVorher = json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR);
|
||||
$chartVorher = json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR);
|
||||
try {
|
||||
IPS_SetProperty($managerId, 'SDLAktiv', true);
|
||||
IPS_SetProperty($managerId, 'SDLLeistungVariableID', $leistungID);
|
||||
IPS_SetProperty($managerId, 'SDLLeistungsfaktor', -1000.0);
|
||||
IPS_SetProperty($managerId, 'SDLSOCVariableID', $socID);
|
||||
IPS_SetProperty($managerId, 'SDLEnergieflussAnzeigen', true);
|
||||
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', true);
|
||||
IPS_ApplyChanges($managerId);
|
||||
$test->assertSame(1500.0, GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId)));
|
||||
$test->assertSame(1200.0, GetValue($test->objectByIdent('SDLLeistungArchiv', $managerId)));
|
||||
$test->assertSame(47.5, GetValue($test->objectByIdent('SDLSOCArchiv', $managerId)));
|
||||
$test->assertSame(1.2, GetValue($test->objectByIdent('EnergieflussSDL', $managerId)));
|
||||
$test->assertSame('Aktiv', GetValue($test->objectByIdent('SDLStatus', $managerId)));
|
||||
$fluss = json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR);
|
||||
$test->assertSame($flussVorher, array_slice($fluss, 0, count($flussVorher)));
|
||||
$test->assertSame(count($flussVorher) + 1, count($fluss));
|
||||
$test->assertSame($test->objectByIdent('SDLSOCArchiv', $managerId), $fluss[count($fluss) - 1]['InfoVariableID']);
|
||||
$chart = json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR);
|
||||
$test->assertSame($chartVorher['datasets'], array_slice($chart['datasets'], 0, count($chartVorher['datasets'])));
|
||||
$test->assertSame('right', $chart['datasets'][count($chart['datasets']) - 1]['side']);
|
||||
$test->assertSame('~Intensity.100', $chart['datasets'][count($chart['datasets']) - 1]['profile']);
|
||||
IPS_ApplyChanges($managerId);
|
||||
$test->assertSame($fluss, json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR));
|
||||
$test->assertSame($chart, json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR));
|
||||
|
||||
foreach ([0 => ['~Power', 1200.0], 1 => ['Enelix.Power.kW', 1.2]] as $einheit => $erwartet) {
|
||||
IPS_SetProperty($managerId, 'EnergieflussAnzeigen', false);
|
||||
IPS_ApplyChanges($managerId);
|
||||
IPS_SetProperty($managerId, 'EnergieflussLeistungseinheit', $einheit);
|
||||
IPS_SetProperty($managerId, 'EnergieflussAnzeigen', true);
|
||||
IPS_ApplyChanges($managerId);
|
||||
$flussID = $test->objectByIdent('Energiefluss', $managerId);
|
||||
$sdlID = $test->objectByIdent('EnergieflussSDL', $managerId);
|
||||
$test->assertSame($erwartet[0], IPS_GetVariable($sdlID)['VariableProfile']);
|
||||
$test->assertSame($erwartet[1], GetValue($sdlID));
|
||||
}
|
||||
|
||||
IPS_SetProperty($managerId, 'SDLEnergieflussAnzeigen', false);
|
||||
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', false);
|
||||
IPS_ApplyChanges($managerId);
|
||||
$test->assertSame($flussVorher, json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR));
|
||||
$test->assertSame($chartVorher, json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR));
|
||||
$test->assertSame(1500.0, GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId)));
|
||||
$test->assertEquals(1.9, GetValue($test->objectByIdent('EnergieflussBatterie', $managerId)), 0.000001);
|
||||
|
||||
IPS_SetProperty($managerId, 'SDLSOCVariableID', 0);
|
||||
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', true);
|
||||
IPS_ApplyChanges($managerId);
|
||||
$chart = json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR);
|
||||
$test->assertSame(count($chartVorher['datasets']) + 1, count($chart['datasets']));
|
||||
IPS_SetProperty($managerId, 'EnergieaufzeichnungAktiv', false);
|
||||
$test->assertTrue(IPS_ApplyChanges($managerId));
|
||||
$archivID = IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
|
||||
foreach (['SDLLeistungArchiv', 'SDLSOCArchiv', 'SDLLadenEnergie', 'SDLEntladenEnergie'] as $ident) {
|
||||
$test->assertSame(false, AC_GetLoggingStatus($archivID, $test->objectByIdent($ident, $managerId)));
|
||||
}
|
||||
} finally {
|
||||
IPS_SetProperty($managerId, 'EnergieaufzeichnungAktiv', true);
|
||||
IPS_SetProperty($managerId, 'SDLAktiv', false);
|
||||
IPS_SetProperty($managerId, 'SDLEnergieflussAnzeigen', false);
|
||||
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', false);
|
||||
IPS_ApplyChanges($managerId);
|
||||
}
|
||||
$test->assertSame(2700.0, GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId)));
|
||||
});
|
||||
$test->runCase('SDL weist fehlende und doppelte Leistungsquellen zurueck', static function (
|
||||
TestContext $test
|
||||
) use ($managerId, $batterieLeistungId): void {
|
||||
try {
|
||||
IPS_SetProperty($managerId, 'SDLAktiv', true);
|
||||
foreach ([0, $batterieLeistungId] as $quelle) {
|
||||
IPS_SetProperty($managerId, 'SDLLeistungVariableID', $quelle);
|
||||
IPS_ApplyChanges($managerId);
|
||||
$test->assertInstanceStatus($managerId, 202);
|
||||
}
|
||||
} finally {
|
||||
IPS_SetProperty($managerId, 'SDLAktiv', false);
|
||||
IPS_SetProperty($managerId, 'SDLLeistungVariableID', 0);
|
||||
IPS_ApplyChanges($managerId);
|
||||
}
|
||||
});
|
||||
|
||||
$zustandswert = static function (array $daten, int $instanzId, string $kennung) {
|
||||
foreach ($daten['Verbraucher'] ?? [] as $verbraucher) {
|
||||
if (($verbraucher['InstanzID'] ?? 0) !== $instanzId) {
|
||||
|
||||
Reference in New Issue
Block a user