From 7cf67d43920f28d7179037ed2809a83ffd671d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=A4fliger?= Date: Thu, 17 Sep 2026 14:07:52 +0000 Subject: [PATCH] Erweitere explizite EMS-Modultests --- Warmwassererwaermer/module.php | 14 +- tests/Symcon/bin/run-symcon-tests.sh | 3 +- tests/Symcon/modules/Manager.php | 238 ++++++++++++------ tests/Symcon/modules/VerbraucherEinStufig.php | 167 +++++++++--- tests/Symcon/modules/Warmwassererwaermer.php | 187 +++++++++++--- 5 files changed, 462 insertions(+), 147 deletions(-) diff --git a/Warmwassererwaermer/module.php b/Warmwassererwaermer/module.php index a6e8610..cb40eab 100644 --- a/Warmwassererwaermer/module.php +++ b/Warmwassererwaermer/module.php @@ -92,6 +92,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->RegisterAttributeBoolean('ZustandAenderungMoeglich', false); $this->RegisterAttributeBoolean('ZustandTemperaturGueltig', false); $this->RegisterAttributeBoolean('ZustandNachladenAktiv', false); + $this->RegisterAttributeBoolean('MaximaltemperaturErreicht', false); $this->RegisterAttributeBoolean('ZustandLegionellenbetriebAktiv', false); $this->RegisterAttributeInteger('ZustandLegioCounter', 0); @@ -362,7 +363,13 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch (float) $this->ReadAttributeFloat('ZustandLegionellentemperatur') ); $hysterese = $this->ReadPropertyFloat('Hysterese'); - $aktuelleLeistung = (int) round((float) $this->leseZustand('Istleistung')); + $maximaltemperaturErreicht = $this->ReadAttributeBoolean('MaximaltemperaturErreicht'); + if ($temperatur >= $maximum) { + $maximaltemperaturErreicht = true; + } elseif ($temperatur <= $maximum - $hysterese) { + $maximaltemperaturErreicht = false; + } + $this->WriteAttributeBoolean('MaximaltemperaturErreicht', $maximaltemperaturErreicht); $nachladen = (bool) $this->leseZustand('NachladenAktiv'); if ($temperatur < $minimum) { @@ -390,12 +397,9 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch if ($nachladen || $zeitplanErfordertHeizen) { return [[$maximalleistung], false, true, $maximalleistung]; } - if ($temperatur >= $maximum) { + if ($maximaltemperaturErreicht) { return [[0], false, true, 0]; } - if ($aktuelleLeistung > 0 && $temperatur >= $maximum - $hysterese) { - return [[$aktuelleLeistung], false, true, $aktuelleLeistung]; - } return [$alleLeistungen, true, true, null]; } diff --git a/tests/Symcon/bin/run-symcon-tests.sh b/tests/Symcon/bin/run-symcon-tests.sh index e38dabd..c05ab75 100755 --- a/tests/Symcon/bin/run-symcon-tests.sh +++ b/tests/Symcon/bin/run-symcon-tests.sh @@ -22,11 +22,12 @@ encode_base64() { mode_base64="$(encode_base64 "$mode")" selection_base64="$(encode_base64 "$selection")" -php_code="require_once IPS_GetKernelDir() . 'modules/Enelix-EMS/tests/Symcon/bootstrap.php'; " +php_code="ob_start(); require_once IPS_GetKernelDir() . 'modules/Enelix-EMS/tests/Symcon/bootstrap.php'; " php_code+="\$mode = base64_decode('$mode_base64'); " php_code+="\$selection = array_values(array_filter(explode(',', base64_decode('$selection_base64')), " php_code+="static fn (string \$value): bool => \$value !== '')); " php_code+="\$result = enelixEmsRunSymconTests(\$mode, \$selection); " +php_code+="ob_end_clean(); " php_code+="echo implode(':', [" php_code+="base64_encode(json_encode(\$result, JSON_THROW_ON_ERROR)), " php_code+="base64_encode(json_encode(\$result['report'], JSON_THROW_ON_ERROR)), " diff --git a/tests/Symcon/modules/Manager.php b/tests/Symcon/modules/Manager.php index 6f49715..6f9f8bd 100644 --- a/tests/Symcon/modules/Manager.php +++ b/tests/Symcon/modules/Manager.php @@ -16,6 +16,7 @@ return static function (TestContext $test): void { 'Mindesteinschaltdauer' => 0, 'Mindestausschaltdauer' => 0, 'PrioritaetPV' => 0, + 'PrioritaetPeak' => 0, 'DiagnosevariablenAnzeigen' => true, ] ); @@ -38,106 +39,195 @@ return static function (TestContext $test): void { 'LastwechselSperrzeit' => 1, 'LegionellenfunktionAktiv' => false, 'PrioritaetPV' => 1, + 'PrioritaetPeak' => 1, 'DiagnosevariablenAnzeigen' => true, ] ); + $zuordnung = [ + ['InstanzID' => $einStufigId, 'Aktiv' => true], + ['InstanzID' => $warmwasserId, 'Aktiv' => true], + ]; $managerId = $test->createInstance( '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}', 'Manager', [ 'NetzleistungVariableID' => $netzleistungId, 'AutomatischeSuche' => false, - 'VerbraucherZuordnung' => '[]', - 'DiagnosevariablenAnzeigen' => true, + 'VerbraucherZuordnung' => json_encode($zuordnung, JSON_THROW_ON_ERROR), + 'KeepAlive' => 30, + 'VerbraucherTimeout' => 10, + 'Lastspitzenmodus' => 1, + 'Lastspitzengrenze' => 0.0, 'SollwertSolarladen' => 0.0, + 'Umschaltdifferenz' => 0.0, + 'DiagnosevariablenAnzeigen' => true, ] ); - $setAssignment = static function (array $ids) use ($test, $managerId): void { - $assignment = array_map( - static fn (int $id): array => ['InstanzID' => $id, 'Aktiv' => true], - $ids - ); - $test->configureInstance($managerId, [ - 'VerbraucherZuordnung' => json_encode($assignment, JSON_THROW_ON_ERROR), - ]); - }; - $reportConsumers = static function (array $ids): void { - foreach ($ids as $id) { - IPS_RequestAction($id, 'Aktiv', true); - IPS_RequestAction($id, 'Melden', true); + $melden = static function () use ($einStufigId, $warmwasserId): void { + foreach ([$einStufigId, $warmwasserId] as $instanzId) { + IPS_RequestAction($instanzId, 'Aktiv', true); + IPS_RequestAction($instanzId, 'Melden', true); } }; - - $test->runCase('Manager funktioniert ohne Verbraucher', static function (TestContext $test) use ( + $regeln = static function (float $netzleistung) use ( + $netzleistungId, + $managerId, + $einStufigId, + $warmwasserId + ): void { + IPS_RequestAction($einStufigId, 'Melden', true); + IPS_RequestAction($warmwasserId, 'Melden', true); + SetValue($netzleistungId, $netzleistung); + IPS_RequestAction($managerId, 'Regeln', true); + }; + $kontaktzustand = static fn (): array => [ + (bool) GetValue($einKontaktId), + (bool) GetValue($warmKontakte[0]), + (bool) GetValue($warmKontakte[1]), + ]; + $warteAufKontakte = static function (array $erwartet, string $schritt) use ( + $test, + $kontaktzustand, $managerId ): void { - IPS_RequestAction($managerId, 'Aktiv', true); - IPS_RequestAction($managerId, 'Regeln', true); - $test->assertInstanceStatus($managerId); - $test->assertSame('PV', GetValue($test->objectByIdent('Betriebsart', $managerId))); - }); + $deadline = microtime(true) + 5.0; + do { + if ($kontaktzustand() === $erwartet) { + return; + } + IPS_Sleep(50); + } while (microtime(true) < $deadline); - $test->runCase('Manager steuert Verbraucher 1-Stufig', static function (TestContext $test) use ( - $setAssignment, - $reportConsumers, - $netzleistungId, - $managerId, - $einStufigId, - $einKontaktId - ): void { - $setAssignment([$einStufigId]); - SetValue($netzleistungId, -1000.0); - $reportConsumers([$einStufigId]); - IPS_RequestAction($managerId, 'Regeln', true); - $test->waitUntil(static fn (): bool => GetValue($einKontaktId) === true); - $test->assertSame(true, GetValue($einKontaktId)); - }); - - $test->runCase('Manager steuert Wassererwärmer', static function (TestContext $test) use ( - $setAssignment, - $reportConsumers, - $netzleistungId, - $managerId, - $warmwasserId, - $warmKontakte - ): void { - $setAssignment([$warmwasserId]); - SetValue($netzleistungId, -2000.0); - $reportConsumers([$warmwasserId]); - IPS_RequestAction($managerId, 'Regeln', true); - $test->waitUntil(static fn (): bool => GetValue($warmKontakte[1]) === true); - $test->assertSame([false, true], array_map( - static fn (int $id): bool => (bool) GetValue($id), - $warmKontakte + $test->fail(sprintf( + '%s: Kontaktbild %s statt %s; Betriebsart %s; Verteilbudget %.0f W.', + $schritt, + json_encode($kontaktzustand(), JSON_THROW_ON_ERROR), + json_encode($erwartet, JSON_THROW_ON_ERROR), + (string) GetValue($test->objectByIdent('Betriebsart', $managerId)), + (float) GetValue($test->objectByIdent('Verteilbudget', $managerId)) )); - }); + }; + $verbraucherstatus = static function () use ($test, $managerId): array { + $json = (string) GetValue($test->objectByIdent('Verbraucherstatus', $managerId)); + $daten = json_decode($json, true, 512, JSON_THROW_ON_ERROR); - $test->runCase('Manager steuert beide Verbrauchertypen gemeinsam', static function ( + return is_array($daten) ? $daten : []; + }; + $zustandswert = static function (array $daten, int $instanzId, string $kennung) { + foreach ($daten['Verbraucher'] ?? [] as $verbraucher) { + if (($verbraucher['InstanzID'] ?? 0) !== $instanzId) { + continue; + } + foreach ($verbraucher['Daten']['Zustand'] ?? [] as $zustand) { + if (($zustand['Kennung'] ?? '') === $kennung) { + return $zustand['Wert'] ?? null; + } + } + } + + return null; + }; + + $test->runCase('Manager und alle vorhandenen Verbrauchertypen sind verknüpft', static function ( TestContext $test - ) use ( - $setAssignment, - $reportConsumers, - $netzleistungId, - $managerId, - $einStufigId, - $warmwasserId, - $einKontaktId, - $warmKontakte - ): void { - $setAssignment([$einStufigId, $warmwasserId]); - SetValue($netzleistungId, -3000.0); - $reportConsumers([$einStufigId, $warmwasserId]); + ) use ($managerId, $einStufigId, $warmwasserId, $melden): void { + IPS_RequestAction($managerId, 'Aktiv', true); + $melden(); IPS_RequestAction($managerId, 'Regeln', true); - $test->waitUntil(static fn (): bool => ( - GetValue($einKontaktId) === true && GetValue($warmKontakte[1]) === true - )); + + foreach ([$managerId, $einStufigId, $warmwasserId] as $instanzId) { + $test->assertInstanceStatus($instanzId); + } $test->assertSame( 2, GetValue($test->objectByIdent('VerbraucherAnzahl', $managerId)) ); - $test->assertSame(true, GetValue($einKontaktId)); - $test->assertSame(true, GetValue($warmKontakte[1])); + $test->assertSame( + 2, + GetValue($test->objectByIdent('VerbraucherVerfuegbar', $managerId)) + ); + }); + + $test->runCase('Netzleistung wird in beide Richtungen in PV und Peak geregelt', static function ( + TestContext $test + ) use ($managerId, $regeln, $warteAufKontakte): void { + $regeln(-3000.0); + $warteAufKontakte([true, false, true], 'Netz -3000 W'); + $test->assertSame('PV', GetValue($test->objectByIdent('Betriebsart', $managerId))); + + $regeln(-1000.0); + $warteAufKontakte([true, false, true], 'Netz -1000 W'); + $regeln(0.0); + $warteAufKontakte([true, false, true], 'Netz 0 W'); + + IPS_Sleep(1100); + $regeln(1000.0); + $warteAufKontakte([true, true, false], 'Netz +1000 W'); + $test->assertSame('Peak', GetValue($test->objectByIdent('Betriebsart', $managerId))); + + IPS_Sleep(1100); + $regeln(3000.0); + $warteAufKontakte([false, false, false], 'Netz +3000 W'); + }); + + $test->runCase('Keep-alive ist aktiv und erneuert unveränderte Sollwerte', static function ( + TestContext $test + ) use ($managerId, $einStufigId): void { + $sollwertId = $test->objectByIdent('Sollleistung', $einStufigId); + $vorher = (int) IPS_GetVariable($sollwertId)['VariableUpdated']; + IPS_Sleep(1100); + IPS_RequestAction($managerId, 'Regeln', true); + $test->waitUntil( + static fn (): bool => (int) IPS_GetVariable($sollwertId)['VariableUpdated'] > $vorher + ); + }); + + $test->runCase('Alle definierten Nachrichtenauslöser aktualisieren den Manager', static function ( + TestContext $test + ) use ( + $managerId, + $einStufigId, + $warmwasserId, + $temperaturId, + $melden, + $verbraucherstatus, + $zustandswert + ): void { + $melden(); + IPS_RequestAction($managerId, 'Regeln', false); + $daten = $verbraucherstatus(); + $test->assertSame(2, count($daten['Verbraucher'] ?? [])); + + SetValue($temperaturId, 53.0); + $test->waitUntil(static function () use ( + $managerId, + $warmwasserId, + $verbraucherstatus, + $zustandswert + ): bool { + IPS_RequestAction($managerId, 'Regeln', false); + $wert = $zustandswert( + $verbraucherstatus(), + $warmwasserId, + 'Wassertemperatur_C' + ); + return is_numeric($wert) && abs((float) $wert - 53.0) < 0.01; + }, 4000, 100); + + IPS_RequestAction($einStufigId, 'Melden', true); + IPS_RequestAction($warmwasserId, 'Melden', true); + IPS_RequestAction($managerId, 'Regeln', true); + $daten = $verbraucherstatus(); + $test->assertTrue( + is_bool($zustandswert($daten, $einStufigId, 'Schaltzustand')) + ); + $test->assertTrue( + is_int($zustandswert($daten, $warmwasserId, 'AktiveStufe')) + ); + $test->assertSame(false, GetValue( + $test->objectByIdent('Sammelstoerung', $managerId) + )); }); }; diff --git a/tests/Symcon/modules/VerbraucherEinStufig.php b/tests/Symcon/modules/VerbraucherEinStufig.php index 929f6fa..1be20f2 100644 --- a/tests/Symcon/modules/VerbraucherEinStufig.php +++ b/tests/Symcon/modules/VerbraucherEinStufig.php @@ -6,14 +6,18 @@ use Belevo\EnelixEMS\SymconTest\TestContext; return static function (TestContext $test): void { $kontaktId = $test->createActionVariable('Schaltkontakt'); + $rueckmeldungId = $test->createVariable(0, 'Rückmeldung', false); $verbraucherId = $test->createInstance( '{15879A4E-D0C2-4495-83DE-46E1E462591E}', 'Verbraucher 1-Stufig', [ 'Nennleistung' => 1200, 'SchaltkontaktVariableID' => $kontaktId, - 'Mindesteinschaltdauer' => 0, - 'Mindestausschaltdauer' => 0, + 'RueckmeldungVariableID' => $rueckmeldungId, + 'Mindestlaufzeit' => 0, + 'Mindesteinschaltdauer' => 2, + 'Mindestausschaltdauer' => 2, + 'VorgabeTimeout' => 30, 'DiagnosevariablenAnzeigen' => true, ] ); @@ -28,21 +32,10 @@ return static function (TestContext $test): void { ] ); - $test->runCase('Instanz und Variablen werden angelegt', static function (TestContext $test) use ( + $anforderungSenden = static function (int $leistung) use ( + $managerId, $verbraucherId ): void { - $test->assertInstanceStatus($verbraucherId); - foreach (['Aktiv', 'Schaltzustand', 'Tageslaufzeit', 'Istleistung', 'Sollleistung'] as $ident) { - $test->objectByIdent($ident, $verbraucherId); - } - }); - - $test->runCase('Sollwert schaltet den Verbraucher ein', static function (TestContext $test) use ( - $managerId, - $verbraucherId, - $kontaktId - ): void { - IPS_RequestAction($verbraucherId, 'Aktiv', true); $paket = [ 'Kopf' => [ 'Version' => '3.0', @@ -50,41 +43,149 @@ return static function (TestContext $test): void { 'EmpfaengerID' => $verbraucherId, 'Zeitpunkt' => time(), ], - 'Sollleistung_W' => 1200, + 'Sollleistung_W' => $leistung, ]; IPS_RequestAction( $verbraucherId, 'ManagerdatenEmpfangen', json_encode($paket, JSON_THROW_ON_ERROR) ); + }; + $test->runCase('Instanz und Diagnosevariablen werden angelegt', static function ( + TestContext $test + ) use ($verbraucherId): void { + $test->assertInstanceStatus($verbraucherId); + foreach ([ + 'Aktiv', 'Schaltzustand', 'Tageslaufzeit', 'Istleistung', 'Sollleistung', + 'AenderungMoeglich', 'Rueckmeldefehler', 'SchaltbefehlAusstehend', + 'RestMindestzeit', + ] as $ident) { + $test->objectByIdent($ident, $verbraucherId); + } + }); + + $test->runCase('Nur angebotene Leistungsanforderungen werden angenommen', static function ( + TestContext $test + ) use ( + $verbraucherId, + $kontaktId, + $rueckmeldungId, + $anforderungSenden + ): void { + IPS_RequestAction($verbraucherId, 'Aktiv', true); + IPS_Sleep(2100); + + $anforderungSenden(600); + $test->assertSame(false, GetValue($kontaktId)); + $test->assertSame( + 0, + (int) GetValue($test->objectByIdent('Sollleistung', $verbraucherId)) + ); + $anforderungSenden(1200); $test->waitUntil(static fn (): bool => GetValue($kontaktId) === true); - $test->assertSame(true, GetValue($kontaktId)); + $test->assertSame(false, GetValue( + $test->objectByIdent('Schaltzustand', $verbraucherId) + )); + $test->assertSame(true, GetValue( + $test->objectByIdent('SchaltbefehlAusstehend', $verbraucherId) + )); + + SetValue($rueckmeldungId, true); + $test->waitUntil(static fn (): bool => GetValue( + $test->objectByIdent('Schaltzustand', $verbraucherId) + ) === true); $test->assertSame( 1200, (int) round((float) GetValue($test->objectByIdent('Istleistung', $verbraucherId))) ); }); - $test->runCase('Sollwert null schaltet den Verbraucher aus', static function (TestContext $test) use ( - $managerId, + $test->runCase('Mindesteinschaltdauer verhindert zu frühes Ausschalten', static function ( + TestContext $test + ) use ( $verbraucherId, - $kontaktId + $kontaktId, + $rueckmeldungId, + $anforderungSenden ): void { - $paket = [ - 'Kopf' => [ - 'Version' => '3.0', - 'AbsenderID' => $managerId, - 'EmpfaengerID' => $verbraucherId, - 'Zeitpunkt' => time(), - ], - 'Sollleistung_W' => 0, - ]; - IPS_RequestAction( - $verbraucherId, - 'ManagerdatenEmpfangen', - json_encode($paket, JSON_THROW_ON_ERROR) + $anforderungSenden(0); + $test->assertSame(true, GetValue($kontaktId)); + $test->assertTrue( + (int) GetValue($test->objectByIdent('RestMindestzeit', $verbraucherId)) > 0 ); + + IPS_Sleep(2100); + $anforderungSenden(0); $test->waitUntil(static fn (): bool => GetValue($kontaktId) === false); + $test->assertSame(true, GetValue( + $test->objectByIdent('SchaltbefehlAusstehend', $verbraucherId) + )); + + SetValue($rueckmeldungId, false); + $test->waitUntil(static fn (): bool => GetValue( + $test->objectByIdent('Schaltzustand', $verbraucherId) + ) === false); + }); + + $test->runCase('Mindestausschaltdauer verhindert zu frühes Einschalten', static function ( + TestContext $test + ) use ( + $kontaktId, + $rueckmeldungId, + $anforderungSenden + ): void { + $anforderungSenden(1200); $test->assertSame(false, GetValue($kontaktId)); + + IPS_Sleep(2100); + $anforderungSenden(1200); + $test->waitUntil(static fn (): bool => GetValue($kontaktId) === true); + SetValue($rueckmeldungId, true); + }); + + $test->runCase('Tägliche Mindestlaufzeit wird spätestens rechtzeitig erzwungen', static function ( + TestContext $test + ) use ( + $verbraucherId, + $kontaktId, + $rueckmeldungId, + $anforderungSenden + ): void { + IPS_Sleep(2100); + $anforderungSenden(0); + $test->waitUntil(static fn (): bool => GetValue($kontaktId) === false); + SetValue($rueckmeldungId, false); + + IPS_SetProperty($verbraucherId, 'Mindestlaufzeit', 86400); + IPS_ApplyChanges($verbraucherId); + $test->waitUntil(static fn (): bool => GetValue($kontaktId) === true); + $test->assertSame(1200, (int) GetValue( + $test->objectByIdent('Sollleistung', $verbraucherId) + )); + SetValue($rueckmeldungId, true); + + IPS_SetProperty($verbraucherId, 'Mindestlaufzeit', 0); + IPS_ApplyChanges($verbraucherId); + }); + + $test->runCase('Physische Rückmeldung bestätigt und überwacht den Zustand', static function ( + TestContext $test + ) use ($verbraucherId, $kontaktId, $rueckmeldungId): void { + $test->assertSame(true, GetValue($kontaktId)); + SetValue($rueckmeldungId, false); + IPS_RequestAction($verbraucherId, 'Melden', true); + $test->waitUntil(static fn (): bool => GetValue( + $test->objectByIdent('Rueckmeldefehler', $verbraucherId) + ) === true); + $test->assertSame(true, GetValue( + $test->objectByIdent('Stoerung', $verbraucherId) + )); + + SetValue($rueckmeldungId, true); + IPS_RequestAction($verbraucherId, 'Melden', true); + $test->waitUntil(static fn (): bool => GetValue( + $test->objectByIdent('Rueckmeldefehler', $verbraucherId) + ) === false); + $test->assertInstanceStatus($verbraucherId); }); }; diff --git a/tests/Symcon/modules/Warmwassererwaermer.php b/tests/Symcon/modules/Warmwassererwaermer.php index b19e17b..917b9e2 100644 --- a/tests/Symcon/modules/Warmwassererwaermer.php +++ b/tests/Symcon/modules/Warmwassererwaermer.php @@ -6,6 +6,7 @@ use Belevo\EnelixEMS\SymconTest\TestContext; return static function (TestContext $test): void { $temperaturId = $test->createVariable(2, 'Boilertemperatur', 52.0, null, '~Temperature'); + $netzleistungId = $test->createVariable(2, 'Netzleistung', 0.0); $kontakte = [ $test->createActionVariable('Heizstufe 1'), $test->createActionVariable('Heizstufe 2'), @@ -25,9 +26,15 @@ return static function (TestContext $test): void { 'Wassererwärmer', [ 'Boilerfuehler_PT1' => $temperaturId, + 'Boilervolumen' => 300, 'LeistungsStufen' => json_encode($stufen, JSON_THROW_ON_ERROR), 'LastwechselSperrzeit' => 1, - 'LegionellenfunktionAktiv' => false, + 'Hysterese' => 5.0, + 'TemperaturMaxAlter' => 120, + 'LegionellenfunktionAktiv' => true, + 'LegionellenMinimalintervallTage' => 4, + 'LegionellenMaximalintervallTage' => 7, + 'EinstellungenInVisu' => true, 'DiagnosevariablenAnzeigen' => true, ] ); @@ -35,63 +42,175 @@ return static function (TestContext $test): void { '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}', 'Hilfsmanager', [ + 'NetzleistungVariableID' => $netzleistungId, 'AutomatischeSuche' => false, 'VerbraucherZuordnung' => json_encode([ ['InstanzID' => $verbraucherId, 'Aktiv' => true], ], JSON_THROW_ON_ERROR), + 'Lastspitzenmodus' => 1, + 'Lastspitzengrenze' => 0.0, + 'SollwertSolarladen' => 0.0, + 'Umschaltdifferenz' => 0.0, + 'KeepAlive' => 5, + 'DiagnosevariablenAnzeigen' => true, ] ); - $test->runCase('Instanz und Diagnosevariablen werden angelegt', static function ( + $kontaktzustand = static fn (): array => array_map( + static fn (int $id): bool => (bool) GetValue($id), + $kontakte + ); + $warteAufKontakte = static function (array $erwartet) use ( + $test, + $kontaktzustand + ): void { + $test->waitUntil( + static fn (): bool => $kontaktzustand() === $erwartet, + 4000, + 50 + ); + $test->assertSame($erwartet, $kontaktzustand()); + }; + $temperaturSetzen = static function (float $wert) use ( + $temperaturId, + $verbraucherId + ): void { + SetValue($temperaturId, $wert); + IPS_RequestAction($verbraucherId, 'Melden', true); + }; + $managerRegeln = static function (float $netzleistung) use ( + $netzleistungId, + $verbraucherId, + $managerId + ): void { + SetValue($netzleistungId, $netzleistung); + IPS_RequestAction($verbraucherId, 'Melden', true); + IPS_RequestAction($managerId, 'Regeln', true); + }; + + $test->runCase('Instanzen und Diagnosevariablen werden angelegt', static function ( TestContext $test - ) use ($verbraucherId): void { + ) use ($verbraucherId, $managerId): void { $test->assertInstanceStatus($verbraucherId); + $test->assertInstanceStatus($managerId); foreach ([ 'Aktiv', 'Boilertemperatur', 'Mindesttemperatur', 'Maximaltemperatur', - 'Istleistung', 'Sollleistung', 'AktiveStufe', 'TemperaturGueltig', + 'Legionellentemperatur', 'Istleistung', 'Sollleistung', 'AktiveStufe', + 'TemperaturGueltig', 'NachladenAktiv', 'LegionellenbetriebAktiv', ] as $ident) { $test->objectByIdent($ident, $verbraucherId); } }); - $test->runCase('Temperaturmessung wird übernommen', static function (TestContext $test) use ( - $verbraucherId - ): void { + $test->runCase('Temperaturmessung und Verbraucher werden aktiviert', static function ( + TestContext $test + ) use ($verbraucherId, $managerId): void { IPS_RequestAction($verbraucherId, 'Aktiv', true); + IPS_RequestAction($managerId, 'Aktiv', true); IPS_RequestAction($verbraucherId, 'Melden', true); - $temperature = (float) GetValue($test->objectByIdent('Boilertemperatur', $verbraucherId)); - $test->assertEquals(52.0, $temperature, 0.01); + $test->assertEquals( + 52.0, + (float) GetValue($test->objectByIdent('Boilertemperatur', $verbraucherId)), + 0.01 + ); $test->assertSame( true, GetValue($test->objectByIdent('TemperaturGueltig', $verbraucherId)) ); }); - $test->runCase('Sollwert wählt genau eine Leistungsstufe', static function ( + $test->runCase('Unter Minimum wird in PV und Peak bis über die Hysterese geheizt', static function ( TestContext $test - ) use ($managerId, $verbraucherId, $kontakte): void { - $paket = [ - 'Kopf' => [ - 'Version' => '3.0', - 'AbsenderID' => $managerId, - 'EmpfaengerID' => $verbraucherId, - 'Zeitpunkt' => time(), - ], - 'Sollleistung_W' => 2000, - ]; - IPS_RequestAction( - $verbraucherId, - 'ManagerdatenEmpfangen', - json_encode($paket, JSON_THROW_ON_ERROR) - ); - $test->waitUntil(static fn (): bool => GetValue($kontakte[1]) === true); - $test->assertSame( - [false, true, false], - array_map(static fn (int $id): bool => (bool) GetValue($id), $kontakte) - ); - $test->assertSame( - 2, - GetValue($test->objectByIdent('AktiveStufe', $verbraucherId)) - ); + ) use ( + $temperaturSetzen, + $managerRegeln, + $warteAufKontakte, + $managerId + ): void { + $temperaturSetzen(44.0); + $managerRegeln(-3000.0); + $warteAufKontakte([false, false, true]); + $test->assertSame('PV', GetValue($test->objectByIdent('Betriebsart', $managerId))); + + $managerRegeln(3000.0); + $warteAufKontakte([false, false, true]); + $test->assertSame('Peak', GetValue($test->objectByIdent('Betriebsart', $managerId))); + + $temperaturSetzen(49.9); + $managerRegeln(3000.0); + $warteAufKontakte([false, false, true]); + + IPS_Sleep(1100); + $temperaturSetzen(50.0); + $managerRegeln(3000.0); + $warteAufKontakte([false, false, false]); + }); + + $test->runCase('Zwischen Minimum und Maximum wählt PV Stufen und Peak null', static function ( + TestContext $test + ) use ($temperaturSetzen, $managerRegeln, $warteAufKontakte): void { + IPS_Sleep(1100); + $temperaturSetzen(52.0); + $managerRegeln(-3000.0); + $warteAufKontakte([false, false, true]); + + IPS_Sleep(1100); + $managerRegeln(3000.0); + $warteAufKontakte([false, false, false]); + }); + + $test->runCase('Maximum sperrt bis unter die obere Hysterese', static function ( + TestContext $test + ) use ($temperaturSetzen, $managerRegeln, $warteAufKontakte): void { + $temperaturSetzen(60.0); + $managerRegeln(-3000.0); + $warteAufKontakte([false, false, false]); + + $temperaturSetzen(58.0); + $managerRegeln(-3000.0); + $warteAufKontakte([false, false, false]); + + $temperaturSetzen(55.0); + $managerRegeln(-3000.0); + $warteAufKontakte([false, false, true]); + }); + + $test->runCase('Legionellenziel erlaubt Peak-Stufen und lädt in PV maximal', static function ( + TestContext $test + ) use ( + $verbraucherId, + $temperaturSetzen, + $managerRegeln, + $warteAufKontakte + ): void { + IPS_RequestAction($verbraucherId, 'Maximaltemperatur', 65.0); + $temperaturSetzen(55.0); + $managerRegeln(-3000.0); + $warteAufKontakte([false, false, true]); + + IPS_Sleep(1100); + $managerRegeln(2000.0); + $warteAufKontakte([true, false, false]); + }); + + $test->runCase('Zeitplan erzwingt rechtzeitiges Aufheizen wie ein Minimum', static function ( + TestContext $test + ) use ( + $verbraucherId, + $temperaturSetzen, + $managerRegeln, + $warteAufKontakte + ): void { + IPS_Sleep(1100); + IPS_RequestAction($verbraucherId, 'Maximaltemperatur', 60.0); + $temperaturSetzen(52.0); + $managerRegeln(3000.0); + $warteAufKontakte([false, false, false]); + + IPS_SetProperty($verbraucherId, 'Zeitplan', json_encode([ + ['Uhrzeit' => date('H:i', time() + 60), 'Solltemperatur' => 60], + ], JSON_THROW_ON_ERROR)); + IPS_ApplyChanges($verbraucherId); + $warteAufKontakte([false, false, true]); }); };