Verbraucher 1-Stufig um Mindestzeiten erweitern
Tests / test (push) Successful in 42s

This commit is contained in:
dh
2026-09-17 10:23:39 +00:00
parent 49fe18c2eb
commit 5c5ef73fb8
14 changed files with 487 additions and 184 deletions
+154 -52
View File
@@ -33,6 +33,8 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
'Stoerung',
'Stoertext',
'Rueckmeldefehler',
'SchaltbefehlAusstehend',
'RestMindestzeit',
];
public function Create(): void
@@ -45,7 +47,8 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterPropertyBoolean('SchaltkontaktInvertiert', false);
$this->RegisterPropertyInteger('RueckmeldungVariableID', 0);
$this->RegisterPropertyInteger('Mindestlaufzeit', 0);
$this->RegisterPropertyInteger('Umschaltabstand', 5);
$this->RegisterPropertyInteger('Mindesteinschaltdauer', 5);
$this->RegisterPropertyInteger('Mindestausschaltdauer', 5);
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
$this->RegisterVariableBoolean('Schaltzustand', 'Schaltzustand', '~Switch', 100);
@@ -55,7 +58,8 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterAttributeInteger('RegistrierteRueckmeldung', 0);
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
$this->RegisterAttributeInteger('LetzteManagerID', 0);
$this->RegisterAttributeInteger('GesperrtBis', 0);
$this->RegisterAttributeInteger('BestaetigtSeit', 0);
$this->RegisterAttributeInteger('FreigabeAb', 0);
$this->RegisterAttributeInteger('LaufzeitStandZeit', 0);
$this->RegisterAttributeString('LaufzeitTag', '');
$this->RegisterAttributeString('Leistungsangebot', '[0]');
@@ -75,6 +79,8 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterAttributeBoolean('ZustandSchaltzustand', false);
$this->RegisterAttributeInteger('ZustandTageslaufzeit', 0);
$this->RegisterAttributeBoolean('ZustandRueckmeldefehler', false);
$this->RegisterAttributeBoolean('ZustandSchaltbefehlAusstehend', false);
$this->RegisterAttributeInteger('ZustandRestMindestzeit', 0);
$this->RegisterTimer(
'Meldezyklus',
@@ -86,10 +92,12 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);"
);
// Bestehende Instanzen behalten den alten Timer deaktiviert.
$this->RegisterTimer('Umschaltfreigabe', 0, '');
$this->RegisterTimer(
'Umschaltfreigabe',
'MindestzeitAbgelaufen',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Umschaltfreigabe', 0);"
"IPS_RequestAction(\$_IPS['TARGET'], 'MindestzeitAbgelaufen', 0);"
);
$this->RegisterTimer(
'VorgabeTimeout',
@@ -162,7 +170,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->sendeVerbraucherdaten();
return;
case 'Umschaltfreigabe':
case 'MindestzeitAbgelaufen':
case 'VorgabeTimeout':
case 'Tagesplanung':
$this->SetTimerInterval((string) $ident, 0);
@@ -223,14 +231,8 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$schaltzustand = $this->leseSchaltzustand();
if ($schaltzustand !== $vorherigerSchaltzustand) {
$this->setzeSchaltzustand($schaltzustand);
if ($this->ReadAttributeBoolean('Initialisiert')
&& !$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
) {
$this->starteUmschaltsperre($jetzt);
}
$this->bestaetigeZustandswechsel($schaltzustand, $jetzt);
}
$this->pruefeAusstehendenSchaltvorgang($schaltzustand, $jetzt);
$this->pruefeVorgabeTimeout($jetzt);
if ((bool) $this->leseZustand('SollwertGueltig')
@@ -244,12 +246,23 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
}
$aktiv = (bool) $this->GetValue('Aktiv');
$wartetAufRueckmeldung = $this->ReadAttributeBoolean('SchaltpruefungAusstehend');
$rueckmeldefehler = $this->ReadPropertyInteger('RueckmeldungVariableID') > 0
&& !$wartetAufRueckmeldung
&& $schaltzustand !== $this->leseAktorSchaltzustand();
$this->setzeZustand('Rueckmeldefehler', $rueckmeldefehler);
$fehlerfrei = $this->ReadAttributeString('Schaltfehler') === ''
&& !(bool) $this->leseZustand('Rueckmeldefehler');
&& !$rueckmeldefehler;
$verfuegbar = $aktiv && $fehlerfrei;
$mindestlaufzeitErzwungen = $verfuegbar
&& $this->mussMindestlaufzeitErzwingen($jetzt);
$gesperrt = $jetzt < $this->ReadAttributeInteger('GesperrtBis');
$restMindestzeit = $wartetAufRueckmeldung
? 0
: EinStufigRegler::restMindestzeit(
$this->ReadAttributeInteger('FreigabeAb'),
$jetzt
);
$ziel = 0;
if ($verfuegbar) {
@@ -261,20 +274,37 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
}
$istleistung = $this->leistungFuerSchaltzustand($schaltzustand);
if ($ziel !== $istleistung && (!$aktiv || (!$gesperrt && $fehlerfrei))) {
$this->schalte($ziel > 0, $jetzt);
$zielzustand = $ziel > 0;
$befehlNoetig = $ziel !== $istleistung
|| ($wartetAufRueckmeldung
&& $zielzustand !== $this->ReadAttributeBoolean('Schaltziel'));
// Die lokale Deaktivierung bleibt der einzige bewusste Mindestzeit-Override.
$sicheresAusschalten = !$zielzustand && !$aktiv;
if ($befehlNoetig
&& ($sicheresAusschalten
|| ($restMindestzeit === 0 && !$wartetAufRueckmeldung && $fehlerfrei))
) {
$this->schalte($zielzustand, $jetzt);
$schaltzustand = $this->leseSchaltzustand();
$this->setzeSchaltzustand($schaltzustand);
$this->pruefeAusstehendenSchaltvorgang($schaltzustand, $jetzt);
$istleistung = $this->leistungFuerSchaltzustand($schaltzustand);
$gesperrt = $jetzt < $this->ReadAttributeInteger('GesperrtBis');
$wartetAufRueckmeldung = $this->ReadAttributeBoolean('SchaltpruefungAusstehend');
$restMindestzeit = $wartetAufRueckmeldung
? 0
: EinStufigRegler::restMindestzeit(
$this->ReadAttributeInteger('FreigabeAb'),
$jetzt
);
}
$this->setzeZustand('Sollleistung', $ziel);
$this->setzeZustand('Verfuegbar', $verfuegbar);
$this->setzeZustand('SchaltbefehlAusstehend', $wartetAufRueckmeldung);
$this->setzeZustand('RestMindestzeit', $restMindestzeit);
$aenderungMoeglich = $verfuegbar
&& !$mindestlaufzeitErzwungen
&& !$gesperrt
&& !$this->ReadAttributeBoolean('SchaltpruefungAusstehend');
&& $restMindestzeit === 0
&& !$wartetAufRueckmeldung;
$this->setzeZustand('AenderungMoeglich', $aenderungMoeglich);
$angebot = EinStufigRegler::leistungsangebot(
@@ -314,58 +344,77 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->WriteAttributeBoolean('Schaltziel', $einschalten);
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', true);
$this->setzeZustand('SchaltbefehlAusstehend', true);
$this->WriteAttributeBoolean('SchaltvorgangLaeuft', true);
try {
RequestAction($kontaktID, $aktorwert);
} catch (Throwable $fehler) {
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
$this->setzeZustand('SchaltbefehlAusstehend', false);
throw new RuntimeException('Schaltkontakt konnte nicht gesetzt werden: ' . $fehler->getMessage());
} finally {
$this->WriteAttributeBoolean('SchaltvorgangLaeuft', false);
}
$this->starteUmschaltsperre($jetzt);
$istzustand = $this->leseSchaltzustand();
if ($istzustand === $einschalten) {
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
$this->setzeZustand('Rueckmeldefehler', false);
$this->bestaetigeZustandswechsel($istzustand, $jetzt);
} elseif ($this->ReadPropertyInteger('RueckmeldungVariableID') <= 0) {
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
$this->setzeZustand('SchaltbefehlAusstehend', false);
throw new RuntimeException('Schaltkontakt hat den angeforderten Zustand nicht uebernommen.');
} elseif ($this->ReadPropertyInteger('Umschaltabstand') === 0) {
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
$this->setzeZustand('Rueckmeldefehler', true);
} else {
// Die physische Rueckmeldung bestimmt Zustand und Beginn der Mindestzeit.
$this->setzeZustand('Rueckmeldefehler', false);
}
$this->WriteAttributeString('Schaltfehler', '');
$this->protokolliere('Lastwechsel', ['Eingeschaltet' => $einschalten]);
$this->protokolliere('Schaltbefehl', ['Eingeschaltet' => $einschalten]);
}
private function pruefeAusstehendenSchaltvorgang(bool $schaltzustand, int $jetzt): void
{
if (!$this->ReadAttributeBoolean('SchaltpruefungAusstehend')) {
if ((bool) $this->leseZustand('Rueckmeldefehler')
&& $schaltzustand === $this->ReadAttributeBoolean('Schaltziel')
) {
$this->setzeZustand('Rueckmeldefehler', false);
}
return;
}
if ($schaltzustand === $this->ReadAttributeBoolean('Schaltziel')) {
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
$this->setzeZustand('Rueckmeldefehler', false);
return;
}
if ($jetzt >= $this->ReadAttributeInteger('GesperrtBis')) {
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
$this->setzeZustand('Rueckmeldefehler', true);
if ($this->ReadAttributeBoolean('SchaltpruefungAusstehend')
&& $schaltzustand === $this->ReadAttributeBoolean('Schaltziel')
) {
$this->bestaetigeZustandswechsel($schaltzustand, $jetzt);
}
$this->setzeZustand(
'SchaltbefehlAusstehend',
$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
);
}
private function starteUmschaltsperre(int $jetzt): void
private function bestaetigeZustandswechsel(bool $schaltzustand, int $jetzt): void
{
$gesperrtBis = $jetzt + $this->ReadPropertyInteger('Umschaltabstand');
$this->WriteAttributeInteger('GesperrtBis', $gesperrtBis);
$vorherigerZustand = (bool) $this->leseZustand('Schaltzustand');
$zustandGeaendert = $schaltzustand !== $vorherigerZustand
|| $this->ReadAttributeInteger('BestaetigtSeit') <= 0;
$this->setzeSchaltzustand($schaltzustand);
if ($zustandGeaendert) {
$this->WriteAttributeInteger('BestaetigtSeit', $jetzt);
$this->WriteAttributeInteger(
'FreigabeAb',
EinStufigRegler::freigabezeitpunkt(
$schaltzustand,
$jetzt,
$this->ReadPropertyInteger('Mindesteinschaltdauer'),
$this->ReadPropertyInteger('Mindestausschaltdauer')
)
);
}
if ($schaltzustand === $this->ReadAttributeBoolean('Schaltziel')) {
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
}
$this->setzeZustand(
'SchaltbefehlAusstehend',
$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
);
$this->setzeZustand('Rueckmeldefehler', false);
$this->protokolliere('Schaltzustand bestaetigt', ['Eingeschaltet' => $schaltzustand]);
}
private function aktualisiereTageslaufzeit(int $jetzt): void
@@ -410,10 +459,14 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
private function planeTimer(int $jetzt): void
{
$gesperrtBis = $this->ReadAttributeInteger('GesperrtBis');
$this->SetTimerInterval('Umschaltfreigabe', 0);
$freigabeAb = $this->ReadAttributeInteger('FreigabeAb');
$wartetAufRueckmeldung = $this->ReadAttributeBoolean('SchaltpruefungAusstehend');
$this->SetTimerInterval(
'Umschaltfreigabe',
$gesperrtBis > $jetzt ? max(1, ($gesperrtBis - $jetzt) * 1000) : 0
'MindestzeitAbgelaufen',
!$wartetAufRueckmeldung && $freigabeAb > $jetzt
? max(1, ($freigabeAb - $jetzt) * 1000)
: 0
);
$this->planeVorgabeTimeout($jetzt);
@@ -457,7 +510,19 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->setzeZustand('Rueckmeldefehler', false);
$this->WriteAttributeBoolean('Initialisiert', true);
}
$this->setzeSchaltzustand($schaltzustand);
if ($this->ReadAttributeInteger('BestaetigtSeit') <= 0) {
// Migration bestehender Instanzen ohne bestaetigten Zeitbezug.
$this->WriteAttributeBoolean('Schaltziel', $schaltzustand);
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
$this->bestaetigeZustandswechsel($schaltzustand, $jetzt);
} else {
$this->setzeSchaltzustand($schaltzustand);
$this->setzeZustand(
'SchaltbefehlAusstehend',
$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
);
}
}
private function setzeSchaltzustand(bool $eingeschaltet): void
@@ -474,6 +539,11 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
return (bool) GetValue($rueckmeldungID);
}
return $this->leseAktorSchaltzustand();
}
private function leseAktorSchaltzustand(): bool
{
$kontaktwert = (bool) GetValue($this->ReadPropertyInteger('SchaltkontaktVariableID'));
return $kontaktwert !== $this->ReadPropertyBoolean('SchaltkontaktInvertiert');
@@ -551,6 +621,24 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
'Wert' => (bool) $this->leseZustand('Schaltzustand'),
'Einheit' => '',
],
[
'Kennung' => 'SchaltbefehlAusstehend',
'Art' => 'Status',
'Wert' => (bool) $this->leseZustand('SchaltbefehlAusstehend'),
'Einheit' => '',
],
[
'Kennung' => 'Schaltbereit',
'Art' => 'Status',
'Wert' => (bool) $this->leseZustand('AenderungMoeglich'),
'Einheit' => '',
],
[
'Kennung' => 'RestMindestzeit_s',
'Art' => 'Istwert',
'Wert' => (int) $this->leseZustand('RestMindestzeit'),
'Einheit' => 's',
],
[
'Kennung' => 'Tageslaufzeit_s',
'Art' => 'Istwert',
@@ -636,8 +724,10 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
) {
throw new InvalidArgumentException('Mindestlaufzeit muss zwischen 0 und 86400 Sekunden liegen.');
}
if ($this->ReadPropertyInteger('Umschaltabstand') < 0) {
throw new InvalidArgumentException('Umschaltabstand darf nicht negativ sein.');
foreach (['Mindesteinschaltdauer', 'Mindestausschaltdauer'] as $property) {
if ($this->ReadPropertyInteger($property) < 0) {
throw new InvalidArgumentException($property . ' darf nicht negativ sein.');
}
}
$kontaktID = $this->ReadPropertyInteger('SchaltkontaktVariableID');
@@ -713,6 +803,13 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->registriereVerbraucherDiagnose();
$this->RegisterVariableBoolean('Rueckmeldefehler', 'Rueckmeldefehler', '~Alert', 200);
$this->RegisterVariableBoolean(
'SchaltbefehlAusstehend',
'Schaltbefehl ausstehend',
'~Switch',
210
);
$this->RegisterVariableInteger('RestMindestzeit', 'Restliche Mindestzeit', '', 220);
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
$wert = $ident === 'Leistungsquelle'
? Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET
@@ -739,10 +836,12 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
'Stoerung' => 'ZustandStoerung',
'Schaltzustand' => 'ZustandSchaltzustand',
'Rueckmeldefehler' => 'ZustandRueckmeldefehler',
'SchaltbefehlAusstehend' => 'ZustandSchaltbefehlAusstehend',
];
$integerAttribute = [
'Sollleistung' => 'ZustandSollleistung',
'Tageslaufzeit' => 'ZustandTageslaufzeit',
'RestMindestzeit' => 'ZustandRestMindestzeit',
];
$floatAttribute = [
'Istleistung' => 'ZustandIstleistung',
@@ -783,10 +882,12 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
'Stoerung' => 'ZustandStoerung',
'Schaltzustand' => 'ZustandSchaltzustand',
'Rueckmeldefehler' => 'ZustandRueckmeldefehler',
'SchaltbefehlAusstehend' => 'ZustandSchaltbefehlAusstehend',
];
$integerAttribute = [
'Sollleistung' => 'ZustandSollleistung',
'Tageslaufzeit' => 'ZustandTageslaufzeit',
'RestMindestzeit' => 'ZustandRestMindestzeit',
];
$floatAttribute = [
'Istleistung' => 'ZustandIstleistung',
@@ -838,6 +939,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
'Meldezyklus',
'RueckmeldungVerzoegert',
'Umschaltfreigabe',
'MindestzeitAbgelaufen',
'VorgabeTimeout',
'Tagesplanung',
] as $timer) {