diff --git a/Warmwassererwaermer/form.json b/Warmwassererwaermer/form.json index 0dd702a..13fb5cf 100644 --- a/Warmwassererwaermer/form.json +++ b/Warmwassererwaermer/form.json @@ -33,16 +33,10 @@ }, { "type": "NumberSpinner", - "name": "Interval", - "caption": "Regelintervall", + "name": "LastwechselSperrzeit", + "caption": "Mindestzeit zwischen Lastwechseln", "suffix": " s", "minimum": 1 - }, - { - "type": "NumberSpinner", - "name": "IdleCounterMax", - "caption": "Zyklen bis Idle", - "minimum": 1 } ] }, diff --git a/Warmwassererwaermer/module.php b/Warmwassererwaermer/module.php index b349e8c..2d66109 100644 --- a/Warmwassererwaermer/module.php +++ b/Warmwassererwaermer/module.php @@ -56,8 +56,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->RegisterPropertyBoolean('BoilertemperaturGlaetten', false); $this->RegisterPropertyInteger('Boilervolumen', 300); $this->RegisterPropertyString('Zeitplan', '[]'); - $this->RegisterPropertyInteger('Interval', 5); - $this->RegisterPropertyInteger('IdleCounterMax', 2); + $this->RegisterPropertyInteger('LastwechselSperrzeit', 5); $this->RegisterPropertyInteger('TemperaturMaxAlter', 30); $this->RegisterPropertyFloat('Hysterese', 5.0); $this->RegisterPropertyBoolean('LegionellenfunktionAktiv', true); @@ -74,7 +73,9 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->RegisterAttributeBoolean('InitialwerteGesetzt', false); $this->RegisterAttributeInteger('RegistrierteFuehlerID', 0); $this->RegisterAttributeInteger('LetzteBerechnung', 0); + $this->RegisterAttributeInteger('LetzteTemperaturberechnung', 0); $this->RegisterAttributeInteger('LetzteVorgabeZeit', 0); + $this->RegisterAttributeInteger('LetzterLastwechsel', 0); $this->RegisterAttributeInteger('LetzteManagerID', 0); $this->RegisterAttributeInteger('LetzterLegionellenabschluss', 0); $this->RegisterAttributeFloat('Glaettungswert', 0.0); @@ -99,9 +100,9 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->RegisterAttributeInteger('ZustandIdleCounter', 0); $this->RegisterTimer( - 'Regelzyklus', + 'LastwechselFreigabe', 0, - "IPS_RequestAction(\$_IPS['TARGET'], 'Regelzyklus', false);" + "IPS_RequestAction(\$_IPS['TARGET'], 'LastwechselFreigabe', false);" ); $this->RegisterTimer( 'Meldezyklus', @@ -125,7 +126,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch try { $this->pruefeKonfiguration(); } catch (Throwable $fehler) { - $this->SetTimerInterval('Regelzyklus', 0); + $this->SetTimerInterval('LastwechselFreigabe', 0); $this->SetTimerInterval('Meldezyklus', 0); $this->SetTimerInterval('RueckmeldungVerzoegert', 0); $this->setzeZustand('Verfuegbar', false); @@ -136,7 +137,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch } $this->registriereFuehlermeldung(); - $this->SetTimerInterval('Regelzyklus', $this->ReadPropertyInteger('Interval') * 1000); + $this->SetTimerInterval('LastwechselFreigabe', 0); $this->SetTimerInterval('Meldezyklus', $this->ReadPropertyInteger('Meldeintervall') * 1000); if ($this->ReadAttributeInteger('LetzterLegionellenabschluss') <= 0) { $this->WriteAttributeInteger('LetzterLegionellenabschluss', time()); @@ -168,7 +169,8 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->setzeTemperaturSollwert((string) $ident, $wert); return; - case 'Regelzyklus': + case 'LastwechselFreigabe': + $this->SetTimerInterval('LastwechselFreigabe', 0); $this->regelzyklus(true); return; @@ -176,6 +178,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch if ((bool) $wert) { $this->SetTimerInterval('RueckmeldungVerzoegert', 0); } + $this->regelzyklus(false); $this->sendeVerbraucherdaten(); return; @@ -233,8 +236,15 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $temperaturGueltig = $this->aktualisiereTemperatur(); $this->aktualisiereLegionellenstatus($temperaturGueltig, $jetzt); + $restzeit = $this->aktualisiereLastwechselstatus($jetzt); [$angebot, $aenderungMoeglich, $verfuegbar, $zwangsleistung] = $this->bestimmeLeistungsangebot($temperaturGueltig, $jetzt); + $aktuelleLeistung = (int) round((float) $this->leseZustand('Istleistung')); + $lastwechselGesperrt = $restzeit > 0 && $verfuegbar && $zwangsleistung !== 0; + if ($lastwechselGesperrt) { + $angebot = [$aktuelleLeistung]; + $aenderungMoeglich = false; + } $this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR)); $this->setzeZustand('Verfuegbar', $verfuegbar); $this->setzeZustand('AenderungMoeglich', $aenderungMoeglich); @@ -247,7 +257,9 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch } $ziel = 0; - if ($verfuegbar) { + if ($lastwechselGesperrt) { + $ziel = $aktuelleLeistung; + } elseif ($verfuegbar) { if ($zwangsleistung !== null) { $ziel = $zwangsleistung; } elseif ((bool) $this->leseZustand('SollwertGueltig')) { @@ -259,7 +271,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch } } - $this->schalteLeistung($ziel); + $this->schalteLeistung($ziel, $jetzt); $this->setzeZustand('Sollleistung', $ziel); $this->aktualisiereStoerungen($temperaturGueltig); if ($meldungPlanen) { @@ -296,18 +308,23 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch } $temperatur = (float) $rohwert; + $jetzt = time(); + $letzteTemperaturberechnung = $this->ReadAttributeInteger('LetzteTemperaturberechnung'); + $this->WriteAttributeInteger('LetzteTemperaturberechnung', $jetzt); if ($this->ReadPropertyBoolean('BoilertemperaturGlaetten')) { if (!$this->ReadAttributeBoolean('GlaettungInitialisiert')) { $this->WriteAttributeFloat('Glaettungswert', $temperatur); $this->WriteAttributeBoolean('GlaettungInitialisiert', true); - } else { + } elseif ($letzteTemperaturberechnung > 0 && $jetzt > $letzteTemperaturberechnung) { $temperatur = WarmwasserRegler::pt1( $this->ReadAttributeFloat('Glaettungswert'), $temperatur, - $this->ReadPropertyInteger('Interval'), + $jetzt - $letzteTemperaturberechnung, $this->ReadPropertyInteger('ZeitKonstante') ); $this->WriteAttributeFloat('Glaettungswert', $temperatur); + } else { + $temperatur = $this->ReadAttributeFloat('Glaettungswert'); } } else { $this->WriteAttributeBoolean('GlaettungInitialisiert', false); @@ -403,14 +420,13 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->setzeZustand('LegionellenbetriebAktiv', $fruehestensFaellig); } - private function schalteLeistung(int $zielLeistung): void + private function schalteLeistung(int $zielLeistung, int $jetzt): void { $stufen = WarmwasserRegler::dekodiereLeistungsstufen($this->ReadPropertyString('LeistungsStufen')); $zielStufe = WarmwasserRegler::stufeFuerLeistung($stufen, $zielLeistung); $bisherigeLeistung = (int) round((float) $this->leseZustand('Istleistung')); if ($zielLeistung === $bisherigeLeistung && $this->kontakteEntsprechenZiel($stufen, $zielLeistung)) { $this->WriteAttributeString('Schaltfehler', ''); - $this->verarbeiteIdle(false); return; } @@ -436,6 +452,8 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch } $this->setzeZustand('Istleistung', 0.0); $this->setzeZustand('AktiveStufe', 0); + $this->WriteAttributeInteger('LetzterLastwechsel', $jetzt); + $this->aktualisiereLastwechselstatus($jetzt); throw new RuntimeException('Leistungsstufe konnte nicht sicher geschaltet werden: ' . $fehler->getMessage()); } @@ -443,7 +461,8 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->setzeZustand('Istleistung', (float) $zielLeistung); $this->setzeZustand('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET); $this->setzeZustand('AktiveStufe', $zielStufe); - $this->verarbeiteIdle(true); + $this->WriteAttributeInteger('LetzterLastwechsel', $jetzt); + $this->aktualisiereLastwechselstatus($jetzt); $this->protokolliere('Leistungsstufe', ['Leistung_W' => $zielLeistung, 'Stufe' => $zielStufe]); } @@ -465,16 +484,18 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch return true; } - private function verarbeiteIdle(bool $geaendert): void + private function aktualisiereLastwechselstatus(int $jetzt): int { - $counter = (int) $this->leseZustand('IdleCounter'); - if ($geaendert) { - $counter = $this->ReadPropertyInteger('IdleCounterMax'); - } elseif ($counter > 0) { - --$counter; - } - $this->setzeZustand('IdleCounter', $counter); - $this->setzeZustand('Idle', $counter === 0); + $restzeit = WarmwasserRegler::restzeitBisLastwechsel( + $this->ReadAttributeInteger('LetzterLastwechsel'), + $jetzt, + $this->ReadPropertyInteger('LastwechselSperrzeit') + ); + $this->setzeZustand('IdleCounter', $restzeit); + $this->setzeZustand('Idle', $restzeit === 0); + $this->SetTimerInterval('LastwechselFreigabe', $restzeit > 0 ? $restzeit * 1000 : 0); + + return $restzeit; } private function aktualisiereEnergie(int $jetzt): void @@ -674,7 +695,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch } } foreach ([ - 'Meldeintervall', 'VorgabeTimeout', 'Interval', 'IdleCounterMax', 'TemperaturMaxAlter', + 'Meldeintervall', 'VorgabeTimeout', 'LastwechselSperrzeit', 'TemperaturMaxAlter', 'ZeitKonstante', 'Boilervolumen', 'LegionellenMinimalintervallTage', 'LegionellenMaximalintervallTage', ] as $property) { @@ -825,8 +846,8 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch 220 ); $this->RegisterVariableInteger('LegioCounter', 'Zeit seit Legionellenabschluss', '', 230); - $this->RegisterVariableBoolean('Idle', 'Idle', '~Switch', 240); - $this->RegisterVariableInteger('IdleCounter', 'Idle Counter', '', 250); + $this->RegisterVariableBoolean('Idle', 'Lastwechsel freigegeben', '~Switch', 240); + $this->RegisterVariableInteger('IdleCounter', 'Restzeit bis Lastwechsel', '', 250); foreach (self::DIAGNOSE_VARIABLEN as $ident) { $wert = $ident === 'Leistungsquelle' diff --git a/docs/migration/Boiler-x-Stufig.md b/docs/migration/Boiler-x-Stufig.md index bd3eb6a..479fe34 100644 --- a/docs/migration/Boiler-x-Stufig.md +++ b/docs/migration/Boiler-x-Stufig.md @@ -5,13 +5,13 @@ | Enelix 1 | Enelix 2 | Hinweis | | --- | --- | --- | | `LeistungsStufen` | `LeistungsStufen` | Felder `Stufe`, `Leistung`, `Schaltkontakt_Stufe` bleiben erhalten. | -| `ZeitKonstante` | `ZeitKonstante` | PT1-Berechnung verwendet das reale Regelintervall. | +| `ZeitKonstante` | `ZeitKonstante` | PT1-Berechnung verwendet die tatsaechlich verstrichene Zeit. | | `Boilerfuehler_PT1` | `Boilerfuehler_PT1` | Alter und Datentyp werden geprueft. | | `Boilertemperatur_glätten` | `BoilertemperaturGlaetten` | ASCII-sicherer Ident. | | `Boilervolumen` | `Boilervolumen` | Wird fuer die Zeitplanprognose verwendet. | | `Zeitplan` | `Zeitplan` | JSON-Liste mit `Uhrzeit` und `Solltemperatur`. | -| `Interval` | `Interval` | Regelintervall in Sekunden. | -| `IdleCounterMax` | `IdleCounterMax` | Verhalten bleibt erhalten. | +| `Interval` | entfaellt | Die Regelung wird ereignisbasiert ausgeloest. | +| `IdleCounterMax` | `LastwechselSperrzeit` | Zykluszaehler wird durch eine Sperrzeit in Sekunden ersetzt; Standard `5`. | Die neuen Properties `TemperaturMaxAlter`, `Hysterese`, die Parameter der Legionellenfunktion und `DiagnosevariablenAnzeigen` ersetzen feste Werte und diff --git a/docs/module/Wassererwaermer/README.md b/docs/module/Wassererwaermer/README.md index 4c4f526..b56cdbb 100644 --- a/docs/module/Wassererwaermer/README.md +++ b/docs/module/Wassererwaermer/README.md @@ -19,13 +19,16 @@ Legionellenprogramm koennen die lokale Warmwassersicherheit priorisieren. - Mindesttemperatur, vorausberechneter Zeitplan und Legionellenprogramm koennen die maximale Stufe lokal erzwingen. Diese Phase wird mit `AenderungMoeglich=false` gemeldet. +- Lastwechsel werden fuer `LastwechselSperrzeit` Sekunden gesperrt. Danach + erfolgt genau eine ereignisgesteuerte Neuberechnung; sicherheitsbedingtes + Ausschalten bleibt sofort moeglich. - `BoilertemperaturGlaetten` aktiviert eine PT1-Glaettung mit konfigurierbarer - Zeitkonstante. + Zeitkonstante und der tatsaechlich verstrichenen Zeit. ## Properties Zu den sechs gemeinsamen Verbraucher-Properties aus -[`Schnittstelle.md`](../../Schnittstelle.md) kommen 14 Modul-Properties hinzu. +[`Schnittstelle.md`](../../Schnittstelle.md) kommen 13 Modul-Properties hinzu. | Ident | Typ | Standard | Beschreibung | | --- | --- | ---: | --- | @@ -35,8 +38,7 @@ Zu den sechs gemeinsamen Verbraucher-Properties aus | `BoilertemperaturGlaetten` | Boolean | `false` | PT1-Glaettung aktiv. | | `Boilervolumen` | Integer | `300` | Volumen in Litern fuer die Zeitplanprognose. | | `Zeitplan` | JSON-Liste | `[]` | Uhrzeit und Solltemperatur. | -| `Interval` | Integer | `5` | Regelintervall in Sekunden. | -| `IdleCounterMax` | Integer | `2` | Zyklen bis zum Idle-Status. | +| `LastwechselSperrzeit` | Integer | `5` | Mindestzeit zwischen zwei Lastwechseln in Sekunden. | | `TemperaturMaxAlter` | Integer | `30` | Maximal zulaessiges Messwertalter in Sekunden. | | `Hysterese` | Float | `5.0` | Temperaturhysterese in K. | | `LegionellenfunktionAktiv` | Boolean | `true` | Zweistufige periodische Aufheizung aktiv. | @@ -74,8 +76,8 @@ Mit `DiagnosevariablenAnzeigen` werden diese 16 Variablen angelegt: - `NachladenAktiv` - `LegionellenbetriebAktiv` - `LegioCounter` -- `Idle` -- `IdleCounter` +- `Idle` (Lastwechsel freigegeben) +- `IdleCounter` (verbleibende Sperrzeit in Sekunden) Die Regelung und Managerkommunikation verwenden fuer diese Werte eine interne Zustandsablage und funktionieren auch ohne angelegte Diagnosevariablen weiter. @@ -96,7 +98,7 @@ verzoegert, damit keine synchrone Manager-Verbraucher-Endlosschleife entsteht. `WarmwasserReglerTest.php` prueft Leistungsstufen, PT1, Energieberechnung, Zeitplan, thermische Prognose und die zweistufigen Legionellengrenzen. `WarmwassererwaermerModulstrukturTest.php` prueft Metadaten, Formular, -20 Properties, bedarfsgesteuerte Diagnosevariablen, Schnittstellenvertrag und +19 Properties, bedarfsgesteuerte Diagnosevariablen, Schnittstellenvertrag und Break-before-make-Schaltung. ## Migration diff --git a/libs/WarmwasserRegler.php b/libs/WarmwasserRegler.php index 28a4be6..7d19f03 100644 --- a/libs/WarmwasserRegler.php +++ b/libs/WarmwasserRegler.php @@ -109,6 +109,23 @@ final class WarmwasserRegler return $bisher + $alpha * ($eingang - $bisher); } + public static function restzeitBisLastwechsel( + int $letzterLastwechsel, + int $jetzt, + int $sperrzeitSekunden + ): int { + if ($sperrzeitSekunden < 0) { + throw new InvalidArgumentException('Die Lastwechsel-Sperrzeit darf nicht negativ sein.'); + } + if ($letzterLastwechsel <= 0) { + return 0; + } + + $verstrichen = max(0, $jetzt - $letzterLastwechsel); + + return max(0, $sperrzeitSekunden - $verstrichen); + } + public static function energieKWh(float $leistungW, int $dauerSekunden): float { if ($dauerSekunden < 0) { diff --git a/tests/WarmwasserReglerTest.php b/tests/WarmwasserReglerTest.php index ad9241b..b00cfda 100644 --- a/tests/WarmwasserReglerTest.php +++ b/tests/WarmwasserReglerTest.php @@ -53,6 +53,14 @@ final class WarmwasserReglerTest extends TestCase self::assertEqualsWithDelta(20.4, WarmwasserRegler::pt1(20.0, 30.0, 5, 120), 0.001); } + public function testLastwechselRestzeitWirdInSekundenBerechnet(): void + { + self::assertSame(0, WarmwasserRegler::restzeitBisLastwechsel(0, 100, 5)); + self::assertSame(5, WarmwasserRegler::restzeitBisLastwechsel(100, 100, 5)); + self::assertSame(2, WarmwasserRegler::restzeitBisLastwechsel(100, 103, 5)); + self::assertSame(0, WarmwasserRegler::restzeitBisLastwechsel(100, 105, 5)); + } + public function testEnergieWirdInKWhBerechnet(): void { self::assertEqualsWithDelta(3.0, WarmwasserRegler::energieKWh(3000.0, 3600), 0.000001); diff --git a/tests/WarmwassererwaermerModulstrukturTest.php b/tests/WarmwassererwaermerModulstrukturTest.php index dfde680..25e6044 100644 --- a/tests/WarmwassererwaermerModulstrukturTest.php +++ b/tests/WarmwassererwaermerModulstrukturTest.php @@ -29,14 +29,14 @@ final class WarmwassererwaermerModulstrukturTest extends TestCase self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $metadaten['id']); } - public function testAlle20PropertiesSindRegistriertUndImFormularDokumentiert(): void + public function testAlle19PropertiesSindRegistriertUndImFormularDokumentiert(): void { $basis = (string) file_get_contents(__DIR__ . '/../libs/VerbraucherBasisTrait.php'); $modul = (string) file_get_contents(self::MODULPFAD . 'module.php'); preg_match_all('/RegisterProperty(?:Integer|Float|Boolean|String)\(\'([^\']+)\'/', $basis . $modul, $treffer); $properties = array_values(array_unique($treffer[1])); - self::assertCount(20, $properties); + self::assertCount(19, $properties); $formular = (string) file_get_contents(self::MODULPFAD . 'form.json'); foreach ($properties as $property) { @@ -44,6 +44,18 @@ final class WarmwassererwaermerModulstrukturTest extends TestCase } } + public function testRegelungIstEreignisbasiertUndSperrtLastwechselFuenfSekunden(): void + { + $inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php'); + + self::assertStringContainsString("RegisterPropertyInteger('LastwechselSperrzeit', 5)", $inhalt); + self::assertStringNotContainsString("RegisterPropertyInteger('Interval'", $inhalt); + self::assertStringNotContainsString("RegisterPropertyInteger('IdleCounterMax'", $inhalt); + self::assertStringNotContainsString("ReadPropertyInteger('Interval')", $inhalt); + self::assertStringContainsString("'LastwechselFreigabe'", $inhalt); + self::assertStringContainsString('LetzteTemperaturberechnung', $inhalt); + } + public function testModulVerwendetAusschliesslichDenVertrag30(): void { $inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php');