Lastwechsel ereignisbasiert sperren
Tests / test (push) Failing after 39s

This commit is contained in:
dh
2026-09-17 09:14:15 +00:00
parent 1b4a788f22
commit 355e7163fb
7 changed files with 100 additions and 46 deletions
+2 -8
View File
@@ -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
}
]
},
+47 -26
View File
@@ -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'
+3 -3
View File
@@ -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
+9 -7
View File
@@ -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
+17
View File
@@ -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) {
+8
View File
@@ -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);
+14 -2
View File
@@ -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');