From 4514ccb5578f71fe1c8234de1aee33cb81ac0dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=A4fliger?= Date: Thu, 17 Sep 2026 08:29:49 +0000 Subject: [PATCH] Wassererwaermer-Konfiguration vereinfachen --- Warmwassererwaermer/form.json | 146 +++------ Warmwassererwaermer/module.php | 307 +++++++++--------- docs/module/Wassererwaermer/README.md | 23 +- libs/VerbraucherBasisTrait.php | 12 +- libs/WarmwasserRegler.php | 26 ++ tests/VerbraucherBasisTraitTest.php | 25 +- tests/WarmwasserReglerTest.php | 20 ++ .../WarmwassererwaermerModulstrukturTest.php | 30 +- 8 files changed, 312 insertions(+), 277 deletions(-) diff --git a/Warmwassererwaermer/form.json b/Warmwassererwaermer/form.json index 05d5965..0dd702a 100644 --- a/Warmwassererwaermer/form.json +++ b/Warmwassererwaermer/form.json @@ -1,9 +1,5 @@ { "elements": [ - { - "type": "Label", - "caption": "Mehrstufiger Warmwassererwaermer nach Enelix-2-Vertrag 3.0" - }, { "type": "ExpansionPanel", "caption": "Manager und Zeitverhalten", @@ -12,13 +8,13 @@ { "type": "NumberSpinner", "name": "PrioritaetPV", - "caption": "Prioritaet PV", + "caption": "Priorität PV", "minimum": 0 }, { "type": "NumberSpinner", "name": "PrioritaetPeak", - "caption": "Prioritaet Peak", + "caption": "Priorität Peak", "minimum": 0 }, { @@ -52,7 +48,7 @@ }, { "type": "ExpansionPanel", - "caption": "Leistungsstufen", + "caption": "Speichereinstellungen", "expanded": true, "items": [ { @@ -94,38 +90,37 @@ } } ] + }, + { + "type": "SelectVariable", + "name": "Boilerfuehler_PT1", + "caption": "Boilerfühler" + }, + { + "type": "CheckBox", + "name": "LegionellenfunktionAktiv", + "caption": "Legionellenschaltung aktiv" + }, + { + "type": "NumberSpinner", + "name": "LegionellenMinimalintervallTage", + "caption": "Frühestens nach", + "suffix": " Tagen", + "minimum": 1 + }, + { + "type": "NumberSpinner", + "name": "LegionellenMaximalintervallTage", + "caption": "Spätestens nach", + "suffix": " Tagen", + "minimum": 1 } ] }, { "type": "ExpansionPanel", - "caption": "Temperatur und Speicher", - "expanded": true, + "caption": "Erweiterte Speichereinstellungen", "items": [ - { - "type": "SelectVariable", - "name": "Boilerfuehler_PT1", - "caption": "Boilerfuehler" - }, - { - "type": "NumberSpinner", - "name": "TemperaturMaxAlter", - "caption": "Maximales Messwertalter", - "suffix": " s", - "minimum": 1 - }, - { - "type": "CheckBox", - "name": "BoilertemperaturGlaetten", - "caption": "Boilertemperatur mit PT1 glaetten" - }, - { - "type": "NumberSpinner", - "name": "ZeitKonstante", - "caption": "PT1-Zeitkonstante", - "suffix": " s", - "minimum": 1 - }, { "type": "NumberSpinner", "name": "Boilervolumen", @@ -141,21 +136,10 @@ "minimum": 0.1, "digits": 1 }, - { - "type": "CheckBox", - "name": "EinstellungenInVisu", - "caption": "Temperatursollwerte in der Visualisierung anzeigen und bedienen" - } - ] - }, - { - "type": "ExpansionPanel", - "caption": "Zeitplan", - "items": [ { "type": "List", "name": "Zeitplan", - "caption": "Naechste Solltemperatur vorausberechnen", + "caption": "Zeitplan für Solltemperaturen", "add": true, "delete": true, "sortable": true, @@ -179,7 +163,7 @@ "minimum": 0, "maximum": 100, "digits": 1, - "suffix": " C" + "suffix": " °C" } } ] @@ -188,60 +172,36 @@ }, { "type": "ExpansionPanel", - "caption": "Legionellenfunktion", + "caption": "Erweiterte sonstige Einstellungen", "items": [ + { + "type": "NumberSpinner", + "name": "TemperaturMaxAlter", + "caption": "Maximales Messwertalter", + "suffix": " s", + "minimum": 1 + }, { "type": "CheckBox", - "name": "LegionellenfunktionAktiv", - "caption": "Legionellenfunktion aktiv" + "name": "BoilertemperaturGlaetten", + "caption": "Boilertemperatur mit PT1 glätten" }, { "type": "NumberSpinner", - "name": "LegionellenIntervallTage", - "caption": "Intervall", - "suffix": " Tage", - "minimum": 1 - }, - { - "type": "NumberSpinner", - "name": "LegionellenHaltezeit", - "caption": "Haltezeit auf Legionellentemperatur", - "suffix": " s", - "minimum": 0 - }, - { - "type": "NumberSpinner", - "name": "LegionellenMaximaldauer", - "caption": "Maximaldauer", + "name": "ZeitKonstante", + "caption": "PT1-Zeitkonstante", "suffix": " s", "minimum": 1 }, { - "type": "NumberSpinner", - "name": "NachtstartStunde", - "caption": "Nachtbeginn", - "suffix": " Uhr", - "minimum": 0, - "maximum": 23 + "type": "CheckBox", + "name": "EinstellungenInVisu", + "caption": "Temperatursollwerte in der Visualisierung anzeigen und bedienen" }, - { - "type": "NumberSpinner", - "name": "NachtendeStunde", - "caption": "Nachtende", - "suffix": " Uhr", - "minimum": 0, - "maximum": 23 - } - ] - }, - { - "type": "ExpansionPanel", - "caption": "Diagnose", - "items": [ { "type": "CheckBox", "name": "DiagnosevariablenAnzeigen", - "caption": "Diagnosevariablen anzeigen" + "caption": "Diagnosevariablen anlegen" }, { "type": "CheckBox", @@ -251,18 +211,6 @@ ] } ], - "actions": [ - { - "type": "Button", - "caption": "Regelung jetzt ausfuehren", - "onClick": "IPS_RequestAction($id, 'Regelzyklus', false);" - }, - { - "type": "Button", - "caption": "Aktuellen Status einmalig ins Debug-Log schreiben", - "onClick": "IPS_RequestAction($id, 'LogStatus', true);" - } - ], "status": [ { "code": 102, @@ -277,7 +225,7 @@ { "code": 202, "icon": "error", - "caption": "Konfiguration ungueltig" + "caption": "Konfiguration ungültig" }, { "code": 203, diff --git a/Warmwassererwaermer/module.php b/Warmwassererwaermer/module.php index 8ed6303..9f014d3 100644 --- a/Warmwassererwaermer/module.php +++ b/Warmwassererwaermer/module.php @@ -55,11 +55,8 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->RegisterPropertyInteger('TemperaturMaxAlter', 30); $this->RegisterPropertyFloat('Hysterese', 5.0); $this->RegisterPropertyBoolean('LegionellenfunktionAktiv', true); - $this->RegisterPropertyInteger('LegionellenIntervallTage', 7); - $this->RegisterPropertyInteger('LegionellenHaltezeit', 0); - $this->RegisterPropertyInteger('LegionellenMaximaldauer', 86400); - $this->RegisterPropertyInteger('NachtstartStunde', 22); - $this->RegisterPropertyInteger('NachtendeStunde', 7); + $this->RegisterPropertyInteger('LegionellenMinimalintervallTage', 4); + $this->RegisterPropertyInteger('LegionellenMaximalintervallTage', 7); $this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false); $this->RegisterVariableFloat('Boilertemperatur', 'Boilertemperatur', '~Temperature', 100); @@ -73,31 +70,25 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->EnableAction('Maximaltemperatur'); $this->EnableAction('Legionellentemperatur'); - $this->RegisterVariableBoolean('TemperaturGueltig', 'Temperatur gueltig', '~Switch', 200); - $this->RegisterVariableBoolean('NachladenAktiv', 'Nachladen aktiv', '~Switch', 210); - $this->RegisterVariableBoolean( - 'LegionellenbetriebAktiv', - 'Legionellenbetrieb aktiv', - '~Switch', - 220 - ); - $this->RegisterVariableInteger('LegioCounter', 'Zeit seit Legionellenabschluss', '', 230); - $this->RegisterVariableBoolean('Idle', 'Idle', '~Switch', 240); - $this->RegisterVariableInteger('IdleCounter', 'Idle Counter', '', 250); - $this->RegisterAttributeBoolean('InitialwerteGesetzt', false); $this->RegisterAttributeInteger('RegistrierteFuehlerID', 0); $this->RegisterAttributeInteger('LetzteBerechnung', 0); $this->RegisterAttributeInteger('LetzteVorgabeZeit', 0); $this->RegisterAttributeInteger('LetzteManagerID', 0); $this->RegisterAttributeInteger('LetzterLegionellenabschluss', 0); - $this->RegisterAttributeInteger('LegionellenStart', 0); - $this->RegisterAttributeInteger('LegionellenSeitZiel', 0); $this->RegisterAttributeFloat('Glaettungswert', 0.0); $this->RegisterAttributeBoolean('GlaettungInitialisiert', false); $this->RegisterAttributeString('Leistungsangebot', '[]'); $this->RegisterAttributeString('Schaltfehler', ''); - $this->RegisterAttributeString('Legionellenfehler', ''); + $this->RegisterAttributeBoolean('ZustandSollwertGueltig', false); + $this->RegisterAttributeBoolean('ZustandVerfuegbar', false); + $this->RegisterAttributeBoolean('ZustandAenderungMoeglich', false); + $this->RegisterAttributeBoolean('ZustandTemperaturGueltig', false); + $this->RegisterAttributeBoolean('ZustandNachladenAktiv', false); + $this->RegisterAttributeBoolean('ZustandLegionellenbetriebAktiv', false); + $this->RegisterAttributeInteger('ZustandLegioCounter', 0); + $this->RegisterAttributeBoolean('ZustandIdle', true); + $this->RegisterAttributeInteger('ZustandIdleCounter', 0); $this->RegisterTimer( 'Regelzyklus', @@ -121,7 +112,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch parent::ApplyChanges(); $this->initialisiereTemperaturen(); - $this->aktualisiereSichtbarkeit(); + $this->aktualisiereVariablen(); try { $this->pruefeKonfiguration(); @@ -129,8 +120,8 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->SetTimerInterval('Regelzyklus', 0); $this->SetTimerInterval('Meldezyklus', 0); $this->SetTimerInterval('RueckmeldungVerzoegert', 0); - $this->SetValue('Verfuegbar', false); - $this->SetValue('AenderungMoeglich', false); + $this->setzeZustand('Verfuegbar', false); + $this->setzeZustand('AenderungMoeglich', false); $this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG); $this->protokolliere('Konfiguration', $fehler->getMessage()); return; @@ -191,9 +182,6 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->ManagerdatenEmpfangen($daten); return; - case 'LogStatus': - $this->protokolliereStatus(true); - return; } throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident); @@ -222,7 +210,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->WriteAttributeInteger('LetzteManagerID', $managerID); $this->WriteAttributeInteger('LetzteVorgabeZeit', time()); $this->SetValue('Sollleistung', $sollleistung); - $this->SetValue('SollwertGueltig', true); + $this->setzeZustand('SollwertGueltig', true); $this->regelzyklus(false); // Verhindert eine synchrone Manager-Verbraucher-Endlosschleife. @@ -240,24 +228,24 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch [$angebot, $aenderungMoeglich, $verfuegbar, $zwangsleistung] = $this->bestimmeLeistungsangebot($temperaturGueltig, $jetzt); $this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR)); - $this->SetValue('Verfuegbar', $verfuegbar); - $this->SetValue('AenderungMoeglich', $aenderungMoeglich); + $this->setzeZustand('Verfuegbar', $verfuegbar); + $this->setzeZustand('AenderungMoeglich', $aenderungMoeglich); $letzteVorgabe = $this->ReadAttributeInteger('LetzteVorgabeZeit'); if ($letzteVorgabe <= 0 || $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout') ) { - $this->SetValue('SollwertGueltig', false); + $this->setzeZustand('SollwertGueltig', false); } $ziel = 0; if ($verfuegbar) { if ($zwangsleistung !== null) { $ziel = $zwangsleistung; - } elseif ((bool) $this->GetValue('SollwertGueltig')) { + } elseif ((bool) $this->leseZustand('SollwertGueltig')) { $ziel = (int) $this->GetValue('Sollleistung'); if (!in_array($ziel, $angebot, true)) { - $this->SetValue('SollwertGueltig', false); + $this->setzeZustand('SollwertGueltig', false); $ziel = 0; } } @@ -271,8 +259,8 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch } } catch (Throwable $fehler) { $this->WriteAttributeString('Schaltfehler', $fehler->getMessage()); - $this->SetValue('Verfuegbar', false); - $this->SetValue('AenderungMoeglich', false); + $this->setzeZustand('Verfuegbar', false); + $this->setzeZustand('AenderungMoeglich', false); $this->SetValue('Stoerung', true); $this->SetValue('Stoertext', $fehler->getMessage()); $this->SetStatus(self::STATUS_SCHALTFEHLER); @@ -284,18 +272,18 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch { $fuehlerID = $this->ReadPropertyInteger('Boilerfuehler_PT1'); if ($fuehlerID <= 0 || !IPS_VariableExists($fuehlerID)) { - $this->SetValue('TemperaturGueltig', false); + $this->setzeZustand('TemperaturGueltig', false); return false; } $variable = IPS_GetVariable($fuehlerID); if (time() - (int) $variable['VariableUpdated'] > $this->ReadPropertyInteger('TemperaturMaxAlter')) { - $this->SetValue('TemperaturGueltig', false); + $this->setzeZustand('TemperaturGueltig', false); return false; } $rohwert = GetValue($fuehlerID); if (!is_int($rohwert) && !is_float($rohwert)) { - $this->SetValue('TemperaturGueltig', false); + $this->setzeZustand('TemperaturGueltig', false); return false; } @@ -318,7 +306,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch } $this->SetValue('Boilertemperatur', $temperatur); - $this->SetValue('TemperaturGueltig', true); + $this->setzeZustand('TemperaturGueltig', true); return true; } @@ -330,7 +318,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch { $stufen = WarmwasserRegler::dekodiereLeistungsstufen($this->ReadPropertyString('LeistungsStufen')); if (!(bool) $this->GetValue('Aktiv') || !$temperaturGueltig || $stufen === []) { - $this->SetValue('NachladenAktiv', false); + $this->setzeZustand('NachladenAktiv', false); return [[], false, false, 0]; } @@ -339,16 +327,25 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $temperatur = (float) $this->GetValue('Boilertemperatur'); $minimum = (float) $this->GetValue('Mindesttemperatur'); $maximum = (float) $this->GetValue('Maximaltemperatur'); + [$minimum, $maximum] = WarmwasserRegler::legionellenTemperaturgrenzen( + $this->ReadPropertyBoolean('LegionellenfunktionAktiv'), + max(0, $jetzt - $this->ReadAttributeInteger('LetzterLegionellenabschluss')), + $this->ReadPropertyInteger('LegionellenMinimalintervallTage'), + $this->ReadPropertyInteger('LegionellenMaximalintervallTage'), + $minimum, + $maximum, + (float) $this->GetValue('Legionellentemperatur') + ); $hysterese = $this->ReadPropertyFloat('Hysterese'); $aktuelleLeistung = (int) round((float) $this->GetValue('Istleistung')); - $nachladen = (bool) $this->GetValue('NachladenAktiv'); + $nachladen = (bool) $this->leseZustand('NachladenAktiv'); if ($temperatur < $minimum) { $nachladen = true; } elseif ($temperatur >= $minimum + $hysterese) { $nachladen = false; } - $this->SetValue('NachladenAktiv', $nachladen); + $this->setzeZustand('NachladenAktiv', $nachladen); $zeitplanErfordertHeizen = false; $naechster = WarmwasserRegler::naechsterZeitplaneintrag( @@ -365,7 +362,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch ); } - if ((bool) $this->GetValue('LegionellenbetriebAktiv') || $nachladen || $zeitplanErfordertHeizen) { + if ($nachladen || $zeitplanErfordertHeizen) { return [[$maximalleistung], false, true, $maximalleistung]; } if ($temperatur >= $maximum) { @@ -380,54 +377,21 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch private function aktualisiereLegionellenstatus(bool $temperaturGueltig, int $jetzt): void { + $aktiviert = $this->ReadPropertyBoolean('LegionellenfunktionAktiv'); $letzterAbschluss = $this->ReadAttributeInteger('LetzterLegionellenabschluss'); - $this->SetValue('LegioCounter', max(0, $jetzt - $letzterAbschluss)); + $sekundenSeitAbschluss = max(0, $jetzt - $letzterAbschluss); - if (!$this->ReadPropertyBoolean('LegionellenfunktionAktiv')) { - $this->SetValue('LegionellenbetriebAktiv', false); - $this->WriteAttributeInteger('LegionellenStart', 0); - $this->WriteAttributeInteger('LegionellenSeitZiel', 0); - $this->WriteAttributeString('Legionellenfehler', ''); - return; - } - - $aktiv = (bool) $this->GetValue('LegionellenbetriebAktiv'); - $faellig = $jetzt - $letzterAbschluss >= $this->ReadPropertyInteger('LegionellenIntervallTage') * 86400; - if (!$aktiv && $faellig && $this->istNachtstunde((int) date('G', $jetzt))) { - $aktiv = true; - $this->WriteAttributeInteger('LegionellenStart', $jetzt); - $this->WriteAttributeInteger('LegionellenSeitZiel', 0); - } - - if ($aktiv && $jetzt - $this->ReadAttributeInteger('LegionellenStart') - > $this->ReadPropertyInteger('LegionellenMaximaldauer') - ) { - $aktiv = false; - $this->WriteAttributeInteger('LetzterLegionellenabschluss', $jetzt); - $this->WriteAttributeString( - 'Legionellenfehler', - 'Legionellenprogramm innerhalb der Maximaldauer nicht abgeschlossen.' - ); - } - - if ($aktiv && $temperaturGueltig + if ($aktiviert && $temperaturGueltig && (float) $this->GetValue('Boilertemperatur') >= (float) $this->GetValue('Legionellentemperatur') ) { - $seitZiel = $this->ReadAttributeInteger('LegionellenSeitZiel'); - if ($seitZiel <= 0) { - $seitZiel = $jetzt; - $this->WriteAttributeInteger('LegionellenSeitZiel', $seitZiel); - } - if ($jetzt - $seitZiel >= $this->ReadPropertyInteger('LegionellenHaltezeit')) { - $aktiv = false; - $this->WriteAttributeInteger('LetzterLegionellenabschluss', $jetzt); - $this->WriteAttributeInteger('LegionellenStart', 0); - $this->WriteAttributeInteger('LegionellenSeitZiel', 0); - $this->WriteAttributeString('Legionellenfehler', ''); - } + $this->WriteAttributeInteger('LetzterLegionellenabschluss', $jetzt); + $sekundenSeitAbschluss = 0; } - $this->SetValue('LegionellenbetriebAktiv', $aktiv); + $fruehestensFaellig = $aktiviert + && $sekundenSeitAbschluss >= $this->ReadPropertyInteger('LegionellenMinimalintervallTage') * 86400; + $this->setzeZustand('LegioCounter', $sekundenSeitAbschluss); + $this->setzeZustand('LegionellenbetriebAktiv', $fruehestensFaellig); } private function schalteLeistung(int $zielLeistung): void @@ -468,7 +432,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->WriteAttributeString('Schaltfehler', ''); $this->SetValue('Istleistung', (float) $zielLeistung); - $this->SetValue('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET); + $this->setzeZustand('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET); $this->SetValue('AktiveStufe', $zielStufe); $this->verarbeiteIdle(true); $this->protokolliere('Leistungsstufe', ['Leistung_W' => $zielLeistung, 'Stufe' => $zielStufe]); @@ -494,14 +458,14 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch private function verarbeiteIdle(bool $geaendert): void { - $counter = (int) $this->GetValue('IdleCounter'); + $counter = (int) $this->leseZustand('IdleCounter'); if ($geaendert) { $counter = $this->ReadPropertyInteger('IdleCounterMax'); } elseif ($counter > 0) { --$counter; } - $this->SetValue('IdleCounter', $counter); - $this->SetValue('Idle', $counter === 0); + $this->setzeZustand('IdleCounter', $counter); + $this->setzeZustand('Idle', $counter === 0); } private function aktualisiereEnergie(int $jetzt): void @@ -537,10 +501,6 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch if ($this->ReadAttributeString('Schaltfehler') !== '') { $stoerungen[] = $this->ReadAttributeString('Schaltfehler'); } - if ($this->ReadAttributeString('Legionellenfehler') !== '') { - $stoerungen[] = $this->ReadAttributeString('Legionellenfehler'); - } - $this->SetValue('Stoerung', $stoerungen !== []); $this->SetValue('Stoertext', implode("\n", array_values(array_unique($stoerungen)))); if ($this->ReadAttributeString('Schaltfehler') !== '') { @@ -572,9 +532,8 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch /** @return array */ private function baueVerbraucherdaten(int $managerID): array { - $temperaturGueltig = (bool) $this->GetValue('TemperaturGueltig'); + $temperaturGueltig = (bool) $this->leseZustand('TemperaturGueltig'); $schaltfehler = $this->ReadAttributeString('Schaltfehler'); - $legionellenfehler = $this->ReadAttributeString('Legionellenfehler'); $uebertemperatur = $temperaturGueltig && (float) $this->GetValue('Boilertemperatur') > max( @@ -592,15 +551,15 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch 'PrioritaetPV' => $this->ReadPropertyInteger('PrioritaetPV'), 'PrioritaetPeak' => $this->ReadPropertyInteger('PrioritaetPeak'), 'Leistungswerte_W' => $this->leseLeistungsangebot(), - 'AenderungMoeglich' => (bool) $this->GetValue('AenderungMoeglich'), - 'Verfuegbar' => (bool) $this->GetValue('Verfuegbar'), + 'AenderungMoeglich' => (bool) $this->leseZustand('AenderungMoeglich'), + 'Verfuegbar' => (bool) $this->leseZustand('Verfuegbar'), 'Istleistung_W' => (float) $this->GetValue('Istleistung'), 'Leistungsquelle' => Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET, 'Zustand' => [ [ 'Kennung' => 'Sollleistung_W', 'Art' => 'Sollwert', - 'Wert' => (bool) $this->GetValue('SollwertGueltig') + 'Wert' => (bool) $this->leseZustand('SollwertGueltig') ? (int) $this->GetValue('Sollleistung') : null, 'Einheit' => 'W', @@ -626,13 +585,13 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch [ 'Kennung' => 'NachladenAktiv', 'Art' => 'Status', - 'Wert' => (bool) $this->GetValue('NachladenAktiv'), + 'Wert' => (bool) $this->leseZustand('NachladenAktiv'), 'Einheit' => '', ], [ 'Kennung' => 'LegionellenbetriebAktiv', 'Art' => 'Status', - 'Wert' => (bool) $this->GetValue('LegionellenbetriebAktiv'), + 'Wert' => (bool) $this->leseZustand('LegionellenbetriebAktiv'), 'Einheit' => '', ], [ @@ -656,13 +615,6 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch 'Einheit' => '', 'Text' => $schaltfehler, ], - [ - 'Kennung' => 'Legionellenfehler', - 'Art' => 'Stoerung', - 'Wert' => $legionellenfehler !== '', - 'Einheit' => '', - 'Text' => $legionellenfehler, - ], ], ]; } @@ -709,25 +661,23 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch } foreach ([ 'Meldeintervall', 'VorgabeTimeout', 'Interval', 'IdleCounterMax', 'TemperaturMaxAlter', - 'ZeitKonstante', 'Boilervolumen', 'LegionellenIntervallTage', 'LegionellenMaximaldauer', + 'ZeitKonstante', 'Boilervolumen', 'LegionellenMinimalintervallTage', + 'LegionellenMaximalintervallTage', ] as $property) { if ($this->ReadPropertyInteger($property) <= 0) { throw new InvalidArgumentException($property . ' muss groesser als 0 sein.'); } } - if ($this->ReadPropertyInteger('LegionellenHaltezeit') < 0) { - throw new InvalidArgumentException('LegionellenHaltezeit darf nicht negativ sein.'); + if ($this->ReadPropertyInteger('LegionellenMinimalintervallTage') + > $this->ReadPropertyInteger('LegionellenMaximalintervallTage') + ) { + throw new InvalidArgumentException( + 'Das Legionellen-Minimalintervall darf nicht groesser als das Maximalintervall sein.' + ); } if ($this->ReadPropertyFloat('Hysterese') <= 0) { throw new InvalidArgumentException('Hysterese muss groesser als 0 sein.'); } - foreach (['NachtstartStunde', 'NachtendeStunde'] as $property) { - $stunde = $this->ReadPropertyInteger($property); - if ($stunde < 0 || $stunde > 23) { - throw new InvalidArgumentException($property . ' muss zwischen 0 und 23 liegen.'); - } - } - $minimum = (float) $this->GetValue('Mindesttemperatur'); $maximum = (float) $this->GetValue('Maximaltemperatur'); $legionellen = (float) $this->GetValue('Legionellentemperatur'); @@ -786,8 +736,8 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->SetValue('Mindesttemperatur', 45.0); $this->SetValue('Maximaltemperatur', 60.0); $this->SetValue('Legionellentemperatur', 65.0); - $this->SetValue('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET); - $this->SetValue('Idle', true); + $this->setzeZustand('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET); + $this->setzeZustand('Idle', true); $this->WriteAttributeBoolean('InitialwerteGesetzt', true); } @@ -823,11 +773,33 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch $this->regelzyklus(true); } - private function aktualisiereSichtbarkeit(): void + private function aktualisiereVariablen(): void { - $diagnose = $this->ReadPropertyBoolean('DiagnosevariablenAnzeigen'); - foreach (self::DIAGNOSE_VARIABLEN as $ident) { - IPS_SetHidden($this->GetIDForIdent($ident), !$diagnose); + if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) { + $this->registriereVerbraucherDiagnose(); + $this->RegisterVariableBoolean('TemperaturGueltig', 'Temperatur gueltig', '~Switch', 200); + $this->RegisterVariableBoolean('NachladenAktiv', 'Nachladen aktiv', '~Switch', 210); + $this->RegisterVariableBoolean( + 'LegionellenbetriebAktiv', + 'Legionellenbetrieb aktiv', + '~Switch', + 220 + ); + $this->RegisterVariableInteger('LegioCounter', 'Zeit seit Legionellenabschluss', '', 230); + $this->RegisterVariableBoolean('Idle', 'Idle', '~Switch', 240); + $this->RegisterVariableInteger('IdleCounter', 'Idle Counter', '', 250); + + $this->setzeZustand('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET); + foreach (array_slice(self::DIAGNOSE_VARIABLEN, 1) as $ident) { + $this->setzeZustand($ident, $this->leseZustand($ident)); + } + } else { + foreach (self::DIAGNOSE_VARIABLEN as $ident) { + $variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); + if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) { + $this->UnregisterVariable($ident); + } + } } $einstellungen = $this->ReadPropertyBoolean('EinstellungenInVisu'); @@ -841,6 +813,66 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch } } + /** @param bool|int $wert */ + private function setzeZustand(string $ident, $wert): void + { + $booleanAttribute = [ + 'SollwertGueltig' => 'ZustandSollwertGueltig', + 'Verfuegbar' => 'ZustandVerfuegbar', + 'AenderungMoeglich' => 'ZustandAenderungMoeglich', + 'TemperaturGueltig' => 'ZustandTemperaturGueltig', + 'NachladenAktiv' => 'ZustandNachladenAktiv', + 'LegionellenbetriebAktiv' => 'ZustandLegionellenbetriebAktiv', + 'Idle' => 'ZustandIdle', + ]; + $integerAttribute = [ + 'LegioCounter' => 'ZustandLegioCounter', + 'IdleCounter' => 'ZustandIdleCounter', + ]; + + if (isset($booleanAttribute[$ident])) { + $wert = (bool) $wert; + $this->WriteAttributeBoolean($booleanAttribute[$ident], $wert); + } elseif (isset($integerAttribute[$ident])) { + $wert = (int) $wert; + $this->WriteAttributeInteger($integerAttribute[$ident], $wert); + } elseif ($ident !== 'Leistungsquelle') { + throw new LogicException('Unbekannter interner Zustand: ' . $ident); + } + + $variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); + if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) { + SetValue($variablenID, $wert); + } + } + + /** @return bool|int */ + private function leseZustand(string $ident) + { + $booleanAttribute = [ + 'SollwertGueltig' => 'ZustandSollwertGueltig', + 'Verfuegbar' => 'ZustandVerfuegbar', + 'AenderungMoeglich' => 'ZustandAenderungMoeglich', + 'TemperaturGueltig' => 'ZustandTemperaturGueltig', + 'NachladenAktiv' => 'ZustandNachladenAktiv', + 'LegionellenbetriebAktiv' => 'ZustandLegionellenbetriebAktiv', + 'Idle' => 'ZustandIdle', + ]; + $integerAttribute = [ + 'LegioCounter' => 'ZustandLegioCounter', + 'IdleCounter' => 'ZustandIdleCounter', + ]; + + if (isset($booleanAttribute[$ident])) { + return $this->ReadAttributeBoolean($booleanAttribute[$ident]); + } + if (isset($integerAttribute[$ident])) { + return $this->ReadAttributeInteger($integerAttribute[$ident]); + } + + throw new LogicException('Unbekannter interner Zustand: ' . $ident); + } + /** @return list */ private function leseLeistungsangebot(): array { @@ -852,37 +884,6 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch return array_values(array_map('intval', $angebot)); } - private function istNachtstunde(int $stunde): bool - { - $start = $this->ReadPropertyInteger('NachtstartStunde'); - $ende = $this->ReadPropertyInteger('NachtendeStunde'); - if ($start === $ende) { - return true; - } - if ($start < $ende) { - return $stunde >= $start && $stunde < $ende; - } - - return $stunde >= $start || $stunde < $ende; - } - - private function protokolliereStatus(bool $erzwingen): void - { - if (!$erzwingen && !$this->ReadPropertyBoolean('LoggingEin')) { - return; - } - $this->SendDebug('Status', json_encode([ - 'Aktiv' => (bool) $this->GetValue('Aktiv'), - 'Boilertemperatur_C' => (float) $this->GetValue('Boilertemperatur'), - 'TemperaturGueltig' => (bool) $this->GetValue('TemperaturGueltig'), - 'Leistungsangebot_W' => $this->leseLeistungsangebot(), - 'Sollleistung_W' => (int) $this->GetValue('Sollleistung'), - 'Istleistung_W' => (float) $this->GetValue('Istleistung'), - 'AktiveStufe' => (int) $this->GetValue('AktiveStufe'), - 'Stoertext' => (string) $this->GetValue('Stoertext'), - ], JSON_THROW_ON_ERROR), 0); - } - /** @param mixed $daten */ private function protokolliere(string $bezeichnung, $daten): void { diff --git a/docs/module/Wassererwaermer/README.md b/docs/module/Wassererwaermer/README.md index 0a4485e..f7ed2ba 100644 --- a/docs/module/Wassererwaermer/README.md +++ b/docs/module/Wassererwaermer/README.md @@ -25,7 +25,7 @@ Legionellenprogramm koennen die lokale Warmwassersicherheit priorisieren. ## Properties Zu den sechs gemeinsamen Verbraucher-Properties aus -[`Schnittstelle.md`](../../Schnittstelle.md) kommen 17 Modul-Properties hinzu. +[`Schnittstelle.md`](../../Schnittstelle.md) kommen 14 Modul-Properties hinzu. | Ident | Typ | Standard | Beschreibung | | --- | --- | ---: | --- | @@ -39,16 +39,12 @@ Zu den sechs gemeinsamen Verbraucher-Properties aus | `IdleCounterMax` | Integer | `2` | Zyklen bis zum Idle-Status. | | `TemperaturMaxAlter` | Integer | `30` | Maximal zulaessiges Messwertalter in Sekunden. | | `Hysterese` | Float | `5.0` | Temperaturhysterese in K. | -| `LegionellenfunktionAktiv` | Boolean | `true` | Periodische Aufheizung aktiv. | -| `LegionellenIntervallTage` | Integer | `7` | Intervall seit erfolgreichem Abschluss. | -| `LegionellenHaltezeit` | Integer | `0` | Haltezeit auf Zieltemperatur in Sekunden. | -| `LegionellenMaximaldauer` | Integer | `86400` | Abbruchzeit in Sekunden. | -| `NachtstartStunde` | Integer | `22` | Fruehester Programmstart. | -| `NachtendeStunde` | Integer | `7` | Ende des Nachtfensters. | -| `DiagnosevariablenAnzeigen` | Boolean | `false` | Blendet zehn Diagnosevariablen ein. | +| `LegionellenfunktionAktiv` | Boolean | `true` | Zweistufige periodische Aufheizung aktiv. | +| `LegionellenMinimalintervallTage` | Integer | `4` | Ab diesem Intervall wird die Legionellentemperatur als Maximaltemperatur angeboten. | +| `LegionellenMaximalintervallTage` | Integer | `7` | Ab diesem Intervall wird die Legionellentemperatur zwingend nachgeladen. | +| `DiagnosevariablenAnzeigen` | Boolean | `false` | Legt zehn Diagnosevariablen bei Bedarf an. | -`LoggingEin` aktiviert laufende Debug-Ausgaben. Der Formular-Knopf schreibt -unabhaengig davon genau einen aktuellen Statusdatensatz ins Debug-Log. +`LoggingEin` aktiviert laufende Debug-Ausgaben. ## Variablen @@ -87,9 +83,10 @@ verzoegert, damit keine synchrone Manager-Verbraucher-Endlosschleife entsteht. ## Tests `WarmwasserReglerTest.php` prueft Leistungsstufen, PT1, Energieberechnung, -Zeitplan und thermische Prognose. `WarmwassererwaermerModulstrukturTest.php` -prueft Metadaten, Formular, 23 Properties, Sichtbarkeit, Schnittstellenvertrag -und Break-before-make-Schaltung. +Zeitplan, thermische Prognose und die zweistufigen Legionellengrenzen. +`WarmwassererwaermerModulstrukturTest.php` prueft Metadaten, Formular, +20 Properties, bedarfsgesteuerte Diagnosevariablen, Schnittstellenvertrag und +Break-before-make-Schaltung. ## Migration diff --git a/libs/VerbraucherBasisTrait.php b/libs/VerbraucherBasisTrait.php index 5fc25a5..1d04891 100644 --- a/libs/VerbraucherBasisTrait.php +++ b/libs/VerbraucherBasisTrait.php @@ -24,12 +24,16 @@ trait VerbraucherBasisTrait $this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10); $this->EnableAction('Aktiv'); $this->RegisterVariableFloat('Istleistung', 'Istleistung', '', 20); - $this->RegisterVariableInteger('Leistungsquelle', 'Leistungsquelle', '', 30); $this->RegisterVariableInteger('Sollleistung', 'Sollleistung', '', 40); - $this->RegisterVariableBoolean('SollwertGueltig', 'Sollwert gueltig', '~Switch', 50); - $this->RegisterVariableBoolean('Verfuegbar', 'Verfuegbar', '~Switch', 60); - $this->RegisterVariableBoolean('AenderungMoeglich', 'Aenderung moeglich', '~Switch', 70); $this->RegisterVariableBoolean('Stoerung', 'Stoerung', '', 80); $this->RegisterVariableString('Stoertext', 'Stoertext', '', 90); } + + protected function registriereVerbraucherDiagnose(): void + { + $this->RegisterVariableInteger('Leistungsquelle', 'Leistungsquelle', '', 30); + $this->RegisterVariableBoolean('SollwertGueltig', 'Sollwert gueltig', '~Switch', 50); + $this->RegisterVariableBoolean('Verfuegbar', 'Verfuegbar', '~Switch', 60); + $this->RegisterVariableBoolean('AenderungMoeglich', 'Aenderung moeglich', '~Switch', 70); + } } diff --git a/libs/WarmwasserRegler.php b/libs/WarmwasserRegler.php index bb3df20..28a4be6 100644 --- a/libs/WarmwasserRegler.php +++ b/libs/WarmwasserRegler.php @@ -138,6 +138,32 @@ final class WarmwasserRegler return $verfuegbareWaermeJ >= $erforderlicheWaermeJ; } + /** + * @return array{0: float, 1: float} + */ + public static function legionellenTemperaturgrenzen( + bool $aktiv, + int $sekundenSeitAbschluss, + int $minimalintervallTage, + int $maximalintervallTage, + float $mindesttemperatur, + float $maximaltemperatur, + float $legionellentemperatur + ): array { + if (!$aktiv) { + return [$mindesttemperatur, $maximaltemperatur]; + } + + if ($sekundenSeitAbschluss >= $minimalintervallTage * 86400) { + $maximaltemperatur = $legionellentemperatur; + } + if ($sekundenSeitAbschluss >= $maximalintervallTage * 86400) { + $mindesttemperatur = $legionellentemperatur; + } + + return [$mindesttemperatur, $maximaltemperatur]; + } + /** * @return array{Zeitpunkt: int, Solltemperatur: float}|null */ diff --git a/tests/VerbraucherBasisTraitTest.php b/tests/VerbraucherBasisTraitTest.php index 2786789..eb19b89 100644 --- a/tests/VerbraucherBasisTraitTest.php +++ b/tests/VerbraucherBasisTraitTest.php @@ -29,17 +29,29 @@ final class VerbraucherBasisTraitTest extends TestCase [ 'Aktiv', 'Istleistung', - 'Leistungsquelle', 'Sollleistung', - 'SollwertGueltig', - 'Verfuegbar', - 'AenderungMoeglich', 'Stoerung', 'Stoertext', ], array_keys($modul->variablen) ); self::assertSame(['Aktiv'], $modul->aktionen); + + $modul->registriereDiagnose(); + self::assertSame( + [ + 'Aktiv', + 'Istleistung', + 'Sollleistung', + 'Stoerung', + 'Stoertext', + 'Leistungsquelle', + 'SollwertGueltig', + 'Verfuegbar', + 'AenderungMoeglich', + ], + array_keys($modul->variablen) + ); } } @@ -61,6 +73,11 @@ final class VerbraucherBasisDummy $this->registriereVerbraucherBasis(); } + public function registriereDiagnose(): void + { + $this->registriereVerbraucherDiagnose(); + } + public function RegisterPropertyInteger(string $ident, int $standard): void { $this->properties[$ident] = $standard; diff --git a/tests/WarmwasserReglerTest.php b/tests/WarmwasserReglerTest.php index 52dc2b8..ad9241b 100644 --- a/tests/WarmwasserReglerTest.php +++ b/tests/WarmwasserReglerTest.php @@ -79,4 +79,24 @@ final class WarmwasserReglerTest extends TestCase ); self::assertSame(55.0, $eintrag['Solltemperatur']); } + + public function testLegionellenschaltungHebtTemperaturgrenzenZweistufigAn(): void + { + self::assertSame( + [45.0, 60.0], + WarmwasserRegler::legionellenTemperaturgrenzen(true, 3 * 86400, 4, 7, 45.0, 60.0, 65.0) + ); + self::assertSame( + [45.0, 65.0], + WarmwasserRegler::legionellenTemperaturgrenzen(true, 4 * 86400, 4, 7, 45.0, 60.0, 65.0) + ); + self::assertSame( + [65.0, 65.0], + WarmwasserRegler::legionellenTemperaturgrenzen(true, 7 * 86400, 4, 7, 45.0, 60.0, 65.0) + ); + self::assertSame( + [45.0, 60.0], + WarmwasserRegler::legionellenTemperaturgrenzen(false, 8 * 86400, 4, 7, 45.0, 60.0, 65.0) + ); + } } diff --git a/tests/WarmwassererwaermerModulstrukturTest.php b/tests/WarmwassererwaermerModulstrukturTest.php index bc83fbb..c779858 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 testAlle23PropertiesSindRegistriertUndImFormularDokumentiert(): void + public function testAlle20PropertiesSindRegistriertUndImFormularDokumentiert(): 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(23, $properties); + self::assertCount(20, $properties); $formular = (string) file_get_contents(self::MODULPFAD . 'form.json'); foreach ($properties as $property) { @@ -55,7 +55,7 @@ final class WarmwassererwaermerModulstrukturTest extends TestCase self::assertStringNotContainsString("RegisterPropertyInteger('ManagerID'", $inhalt); } - public function testZehnDiagnosevariablenSindOptionalUndLoggingIstGetrennt(): void + public function testZehnDiagnosevariablenWerdenBedarfsgesteuertAngelegtUndGeloescht(): void { $inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php'); preg_match('/private const DIAGNOSE_VARIABLEN = \[(.*?)\];/s', $inhalt, $treffer); @@ -64,8 +64,30 @@ final class WarmwassererwaermerModulstrukturTest extends TestCase self::assertCount(10, $variablen[1]); self::assertStringContainsString("ReadPropertyBoolean('DiagnosevariablenAnzeigen')", $inhalt); + self::assertStringContainsString('registriereVerbraucherDiagnose', $inhalt); + self::assertStringContainsString('UnregisterVariable($ident)', $inhalt); self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $inhalt); - self::assertStringContainsString("case 'LogStatus':", $inhalt); + self::assertStringNotContainsString("case 'LogStatus':", $inhalt); + } + + public function testFormularIstWieGewuenschtGegliedertUndHatKeineAktionsknoepfe(): void + { + $formular = json_decode( + (string) file_get_contents(self::MODULPFAD . 'form.json'), + true, + 512, + JSON_THROW_ON_ERROR + ); + self::assertSame( + [ + 'Manager und Zeitverhalten', + 'Speichereinstellungen', + 'Erweiterte Speichereinstellungen', + 'Erweiterte sonstige Einstellungen', + ], + array_column($formular['elements'], 'caption') + ); + self::assertArrayNotHasKey('actions', $formular); } public function testTemperatursollwerteSindGeprueftBedienbar(): void