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
+4 -1
View File
@@ -21,6 +21,9 @@ jobs:
- name: Abhaengigkeiten installieren - name: Abhaengigkeiten installieren
run: composer install --no-interaction --no-progress --prefer-dist run: composer install --no-interaction --no-progress --prefer-dist
- name: Pruefungen ausfuehren - name: Verbraucher 1-Stufig separat pruefen
run: composer check:verbraucher-einstufig
- name: Gesamttest ausfuehren
run: composer check run: composer check
+7
View File
@@ -39,6 +39,13 @@ composer install
composer check composer check
``` ```
Der Gesamttest schliesst alle Modulsuiten ein. Der einstufige Verbraucher kann
zusaetzlich unabhaengig ausgefuehrt werden:
```bash
composer check:verbraucher-einstufig
```
Neue oder geaenderte Funktionen werden zusammen mit ihren PHPUnit-Tests eingecheckt. Die CI-Pruefung muss vor der Uebernahme in einen Freigabebranch erfolgreich sein. Neue oder geaenderte Funktionen werden zusammen mit ihren PHPUnit-Tests eingecheckt. Die CI-Pruefung muss vor der Uebernahme in einen Freigabebranch erfolgreich sein.
| Git-Branch | IP-Symcon-Kanal | | Git-Branch | IP-Symcon-Kanal |
+9 -2
View File
@@ -33,8 +33,15 @@
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Umschaltabstand", "name": "Mindesteinschaltdauer",
"caption": "Zeit bis zum naechsten Lastwechsel", "caption": "Mindest-Einschaltdauer",
"suffix": " s",
"minimum": 0
},
{
"type": "NumberSpinner",
"name": "Mindestausschaltdauer",
"caption": "Mindest-Ausschaltdauer",
"suffix": " s", "suffix": " s",
"minimum": 0 "minimum": 0
} }
+154 -52
View File
@@ -33,6 +33,8 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
'Stoerung', 'Stoerung',
'Stoertext', 'Stoertext',
'Rueckmeldefehler', 'Rueckmeldefehler',
'SchaltbefehlAusstehend',
'RestMindestzeit',
]; ];
public function Create(): void public function Create(): void
@@ -45,7 +47,8 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterPropertyBoolean('SchaltkontaktInvertiert', false); $this->RegisterPropertyBoolean('SchaltkontaktInvertiert', false);
$this->RegisterPropertyInteger('RueckmeldungVariableID', 0); $this->RegisterPropertyInteger('RueckmeldungVariableID', 0);
$this->RegisterPropertyInteger('Mindestlaufzeit', 0); $this->RegisterPropertyInteger('Mindestlaufzeit', 0);
$this->RegisterPropertyInteger('Umschaltabstand', 5); $this->RegisterPropertyInteger('Mindesteinschaltdauer', 5);
$this->RegisterPropertyInteger('Mindestausschaltdauer', 5);
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false); $this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
$this->RegisterVariableBoolean('Schaltzustand', 'Schaltzustand', '~Switch', 100); $this->RegisterVariableBoolean('Schaltzustand', 'Schaltzustand', '~Switch', 100);
@@ -55,7 +58,8 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterAttributeInteger('RegistrierteRueckmeldung', 0); $this->RegisterAttributeInteger('RegistrierteRueckmeldung', 0);
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0); $this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
$this->RegisterAttributeInteger('LetzteManagerID', 0); $this->RegisterAttributeInteger('LetzteManagerID', 0);
$this->RegisterAttributeInteger('GesperrtBis', 0); $this->RegisterAttributeInteger('BestaetigtSeit', 0);
$this->RegisterAttributeInteger('FreigabeAb', 0);
$this->RegisterAttributeInteger('LaufzeitStandZeit', 0); $this->RegisterAttributeInteger('LaufzeitStandZeit', 0);
$this->RegisterAttributeString('LaufzeitTag', ''); $this->RegisterAttributeString('LaufzeitTag', '');
$this->RegisterAttributeString('Leistungsangebot', '[0]'); $this->RegisterAttributeString('Leistungsangebot', '[0]');
@@ -75,6 +79,8 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterAttributeBoolean('ZustandSchaltzustand', false); $this->RegisterAttributeBoolean('ZustandSchaltzustand', false);
$this->RegisterAttributeInteger('ZustandTageslaufzeit', 0); $this->RegisterAttributeInteger('ZustandTageslaufzeit', 0);
$this->RegisterAttributeBoolean('ZustandRueckmeldefehler', false); $this->RegisterAttributeBoolean('ZustandRueckmeldefehler', false);
$this->RegisterAttributeBoolean('ZustandSchaltbefehlAusstehend', false);
$this->RegisterAttributeInteger('ZustandRestMindestzeit', 0);
$this->RegisterTimer( $this->RegisterTimer(
'Meldezyklus', 'Meldezyklus',
@@ -86,10 +92,12 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
0, 0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);" "IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);"
); );
// Bestehende Instanzen behalten den alten Timer deaktiviert.
$this->RegisterTimer('Umschaltfreigabe', 0, '');
$this->RegisterTimer( $this->RegisterTimer(
'Umschaltfreigabe', 'MindestzeitAbgelaufen',
0, 0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Umschaltfreigabe', 0);" "IPS_RequestAction(\$_IPS['TARGET'], 'MindestzeitAbgelaufen', 0);"
); );
$this->RegisterTimer( $this->RegisterTimer(
'VorgabeTimeout', 'VorgabeTimeout',
@@ -162,7 +170,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->sendeVerbraucherdaten(); $this->sendeVerbraucherdaten();
return; return;
case 'Umschaltfreigabe': case 'MindestzeitAbgelaufen':
case 'VorgabeTimeout': case 'VorgabeTimeout':
case 'Tagesplanung': case 'Tagesplanung':
$this->SetTimerInterval((string) $ident, 0); $this->SetTimerInterval((string) $ident, 0);
@@ -223,14 +231,8 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$schaltzustand = $this->leseSchaltzustand(); $schaltzustand = $this->leseSchaltzustand();
if ($schaltzustand !== $vorherigerSchaltzustand) { if ($schaltzustand !== $vorherigerSchaltzustand) {
$this->setzeSchaltzustand($schaltzustand); $this->bestaetigeZustandswechsel($schaltzustand, $jetzt);
if ($this->ReadAttributeBoolean('Initialisiert')
&& !$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
) {
$this->starteUmschaltsperre($jetzt);
}
} }
$this->pruefeAusstehendenSchaltvorgang($schaltzustand, $jetzt); $this->pruefeAusstehendenSchaltvorgang($schaltzustand, $jetzt);
$this->pruefeVorgabeTimeout($jetzt); $this->pruefeVorgabeTimeout($jetzt);
if ((bool) $this->leseZustand('SollwertGueltig') if ((bool) $this->leseZustand('SollwertGueltig')
@@ -244,12 +246,23 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
} }
$aktiv = (bool) $this->GetValue('Aktiv'); $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') === '' $fehlerfrei = $this->ReadAttributeString('Schaltfehler') === ''
&& !(bool) $this->leseZustand('Rueckmeldefehler'); && !$rueckmeldefehler;
$verfuegbar = $aktiv && $fehlerfrei; $verfuegbar = $aktiv && $fehlerfrei;
$mindestlaufzeitErzwungen = $verfuegbar $mindestlaufzeitErzwungen = $verfuegbar
&& $this->mussMindestlaufzeitErzwingen($jetzt); && $this->mussMindestlaufzeitErzwingen($jetzt);
$gesperrt = $jetzt < $this->ReadAttributeInteger('GesperrtBis'); $restMindestzeit = $wartetAufRueckmeldung
? 0
: EinStufigRegler::restMindestzeit(
$this->ReadAttributeInteger('FreigabeAb'),
$jetzt
);
$ziel = 0; $ziel = 0;
if ($verfuegbar) { if ($verfuegbar) {
@@ -261,20 +274,37 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
} }
$istleistung = $this->leistungFuerSchaltzustand($schaltzustand); $istleistung = $this->leistungFuerSchaltzustand($schaltzustand);
if ($ziel !== $istleistung && (!$aktiv || (!$gesperrt && $fehlerfrei))) { $zielzustand = $ziel > 0;
$this->schalte($ziel > 0, $jetzt); $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(); $schaltzustand = $this->leseSchaltzustand();
$this->setzeSchaltzustand($schaltzustand); $this->pruefeAusstehendenSchaltvorgang($schaltzustand, $jetzt);
$istleistung = $this->leistungFuerSchaltzustand($schaltzustand); $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('Sollleistung', $ziel);
$this->setzeZustand('Verfuegbar', $verfuegbar); $this->setzeZustand('Verfuegbar', $verfuegbar);
$this->setzeZustand('SchaltbefehlAusstehend', $wartetAufRueckmeldung);
$this->setzeZustand('RestMindestzeit', $restMindestzeit);
$aenderungMoeglich = $verfuegbar $aenderungMoeglich = $verfuegbar
&& !$mindestlaufzeitErzwungen && !$mindestlaufzeitErzwungen
&& !$gesperrt && $restMindestzeit === 0
&& !$this->ReadAttributeBoolean('SchaltpruefungAusstehend'); && !$wartetAufRueckmeldung;
$this->setzeZustand('AenderungMoeglich', $aenderungMoeglich); $this->setzeZustand('AenderungMoeglich', $aenderungMoeglich);
$angebot = EinStufigRegler::leistungsangebot( $angebot = EinStufigRegler::leistungsangebot(
@@ -314,58 +344,77 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->WriteAttributeBoolean('Schaltziel', $einschalten); $this->WriteAttributeBoolean('Schaltziel', $einschalten);
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', true); $this->WriteAttributeBoolean('SchaltpruefungAusstehend', true);
$this->setzeZustand('SchaltbefehlAusstehend', true);
$this->WriteAttributeBoolean('SchaltvorgangLaeuft', true); $this->WriteAttributeBoolean('SchaltvorgangLaeuft', true);
try { try {
RequestAction($kontaktID, $aktorwert); RequestAction($kontaktID, $aktorwert);
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false); $this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
$this->setzeZustand('SchaltbefehlAusstehend', false);
throw new RuntimeException('Schaltkontakt konnte nicht gesetzt werden: ' . $fehler->getMessage()); throw new RuntimeException('Schaltkontakt konnte nicht gesetzt werden: ' . $fehler->getMessage());
} finally { } finally {
$this->WriteAttributeBoolean('SchaltvorgangLaeuft', false); $this->WriteAttributeBoolean('SchaltvorgangLaeuft', false);
} }
$this->starteUmschaltsperre($jetzt);
$istzustand = $this->leseSchaltzustand(); $istzustand = $this->leseSchaltzustand();
if ($istzustand === $einschalten) { if ($istzustand === $einschalten) {
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false); $this->bestaetigeZustandswechsel($istzustand, $jetzt);
$this->setzeZustand('Rueckmeldefehler', false);
} elseif ($this->ReadPropertyInteger('RueckmeldungVariableID') <= 0) { } elseif ($this->ReadPropertyInteger('RueckmeldungVariableID') <= 0) {
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false); $this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
$this->setzeZustand('SchaltbefehlAusstehend', false);
throw new RuntimeException('Schaltkontakt hat den angeforderten Zustand nicht uebernommen.'); throw new RuntimeException('Schaltkontakt hat den angeforderten Zustand nicht uebernommen.');
} elseif ($this->ReadPropertyInteger('Umschaltabstand') === 0) { } else {
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false); // Die physische Rueckmeldung bestimmt Zustand und Beginn der Mindestzeit.
$this->setzeZustand('Rueckmeldefehler', true); $this->setzeZustand('Rueckmeldefehler', false);
} }
$this->WriteAttributeString('Schaltfehler', ''); $this->WriteAttributeString('Schaltfehler', '');
$this->protokolliere('Lastwechsel', ['Eingeschaltet' => $einschalten]); $this->protokolliere('Schaltbefehl', ['Eingeschaltet' => $einschalten]);
} }
private function pruefeAusstehendenSchaltvorgang(bool $schaltzustand, int $jetzt): void private function pruefeAusstehendenSchaltvorgang(bool $schaltzustand, int $jetzt): void
{ {
if (!$this->ReadAttributeBoolean('SchaltpruefungAusstehend')) { if ($this->ReadAttributeBoolean('SchaltpruefungAusstehend')
if ((bool) $this->leseZustand('Rueckmeldefehler') && $schaltzustand === $this->ReadAttributeBoolean('Schaltziel')
&& $schaltzustand === $this->ReadAttributeBoolean('Schaltziel') ) {
) { $this->bestaetigeZustandswechsel($schaltzustand, $jetzt);
$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);
} }
$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'); $vorherigerZustand = (bool) $this->leseZustand('Schaltzustand');
$this->WriteAttributeInteger('GesperrtBis', $gesperrtBis); $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 private function aktualisiereTageslaufzeit(int $jetzt): void
@@ -410,10 +459,14 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
private function planeTimer(int $jetzt): void private function planeTimer(int $jetzt): void
{ {
$gesperrtBis = $this->ReadAttributeInteger('GesperrtBis'); $this->SetTimerInterval('Umschaltfreigabe', 0);
$freigabeAb = $this->ReadAttributeInteger('FreigabeAb');
$wartetAufRueckmeldung = $this->ReadAttributeBoolean('SchaltpruefungAusstehend');
$this->SetTimerInterval( $this->SetTimerInterval(
'Umschaltfreigabe', 'MindestzeitAbgelaufen',
$gesperrtBis > $jetzt ? max(1, ($gesperrtBis - $jetzt) * 1000) : 0 !$wartetAufRueckmeldung && $freigabeAb > $jetzt
? max(1, ($freigabeAb - $jetzt) * 1000)
: 0
); );
$this->planeVorgabeTimeout($jetzt); $this->planeVorgabeTimeout($jetzt);
@@ -457,7 +510,19 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->setzeZustand('Rueckmeldefehler', false); $this->setzeZustand('Rueckmeldefehler', false);
$this->WriteAttributeBoolean('Initialisiert', true); $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 private function setzeSchaltzustand(bool $eingeschaltet): void
@@ -474,6 +539,11 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
return (bool) GetValue($rueckmeldungID); return (bool) GetValue($rueckmeldungID);
} }
return $this->leseAktorSchaltzustand();
}
private function leseAktorSchaltzustand(): bool
{
$kontaktwert = (bool) GetValue($this->ReadPropertyInteger('SchaltkontaktVariableID')); $kontaktwert = (bool) GetValue($this->ReadPropertyInteger('SchaltkontaktVariableID'));
return $kontaktwert !== $this->ReadPropertyBoolean('SchaltkontaktInvertiert'); return $kontaktwert !== $this->ReadPropertyBoolean('SchaltkontaktInvertiert');
@@ -551,6 +621,24 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
'Wert' => (bool) $this->leseZustand('Schaltzustand'), 'Wert' => (bool) $this->leseZustand('Schaltzustand'),
'Einheit' => '', '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', 'Kennung' => 'Tageslaufzeit_s',
'Art' => 'Istwert', 'Art' => 'Istwert',
@@ -636,8 +724,10 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
) { ) {
throw new InvalidArgumentException('Mindestlaufzeit muss zwischen 0 und 86400 Sekunden liegen.'); throw new InvalidArgumentException('Mindestlaufzeit muss zwischen 0 und 86400 Sekunden liegen.');
} }
if ($this->ReadPropertyInteger('Umschaltabstand') < 0) { foreach (['Mindesteinschaltdauer', 'Mindestausschaltdauer'] as $property) {
throw new InvalidArgumentException('Umschaltabstand darf nicht negativ sein.'); if ($this->ReadPropertyInteger($property) < 0) {
throw new InvalidArgumentException($property . ' darf nicht negativ sein.');
}
} }
$kontaktID = $this->ReadPropertyInteger('SchaltkontaktVariableID'); $kontaktID = $this->ReadPropertyInteger('SchaltkontaktVariableID');
@@ -713,6 +803,13 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) { if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->registriereVerbraucherDiagnose(); $this->registriereVerbraucherDiagnose();
$this->RegisterVariableBoolean('Rueckmeldefehler', 'Rueckmeldefehler', '~Alert', 200); $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) { foreach (self::DIAGNOSE_VARIABLEN as $ident) {
$wert = $ident === 'Leistungsquelle' $wert = $ident === 'Leistungsquelle'
? Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET ? Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET
@@ -739,10 +836,12 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
'Stoerung' => 'ZustandStoerung', 'Stoerung' => 'ZustandStoerung',
'Schaltzustand' => 'ZustandSchaltzustand', 'Schaltzustand' => 'ZustandSchaltzustand',
'Rueckmeldefehler' => 'ZustandRueckmeldefehler', 'Rueckmeldefehler' => 'ZustandRueckmeldefehler',
'SchaltbefehlAusstehend' => 'ZustandSchaltbefehlAusstehend',
]; ];
$integerAttribute = [ $integerAttribute = [
'Sollleistung' => 'ZustandSollleistung', 'Sollleistung' => 'ZustandSollleistung',
'Tageslaufzeit' => 'ZustandTageslaufzeit', 'Tageslaufzeit' => 'ZustandTageslaufzeit',
'RestMindestzeit' => 'ZustandRestMindestzeit',
]; ];
$floatAttribute = [ $floatAttribute = [
'Istleistung' => 'ZustandIstleistung', 'Istleistung' => 'ZustandIstleistung',
@@ -783,10 +882,12 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
'Stoerung' => 'ZustandStoerung', 'Stoerung' => 'ZustandStoerung',
'Schaltzustand' => 'ZustandSchaltzustand', 'Schaltzustand' => 'ZustandSchaltzustand',
'Rueckmeldefehler' => 'ZustandRueckmeldefehler', 'Rueckmeldefehler' => 'ZustandRueckmeldefehler',
'SchaltbefehlAusstehend' => 'ZustandSchaltbefehlAusstehend',
]; ];
$integerAttribute = [ $integerAttribute = [
'Sollleistung' => 'ZustandSollleistung', 'Sollleistung' => 'ZustandSollleistung',
'Tageslaufzeit' => 'ZustandTageslaufzeit', 'Tageslaufzeit' => 'ZustandTageslaufzeit',
'RestMindestzeit' => 'ZustandRestMindestzeit',
]; ];
$floatAttribute = [ $floatAttribute = [
'Istleistung' => 'ZustandIstleistung', 'Istleistung' => 'ZustandIstleistung',
@@ -838,6 +939,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
'Meldezyklus', 'Meldezyklus',
'RueckmeldungVerzoegert', 'RueckmeldungVerzoegert',
'Umschaltfreigabe', 'Umschaltfreigabe',
'MindestzeitAbgelaufen',
'VorgabeTimeout', 'VorgabeTimeout',
'Tagesplanung', 'Tagesplanung',
] as $timer) { ] as $timer) {
+6
View File
@@ -20,10 +20,16 @@
}, },
"scripts": { "scripts": {
"lint": "find . -path ./vendor -prune -o -name '*.php' -print0 | xargs -0 -n1 php -l", "lint": "find . -path ./vendor -prune -o -name '*.php' -print0 | xargs -0 -n1 php -l",
"lint:verbraucher-einstufig": "find VerbraucherEinStufig libs/EinStufigRegler.php tests/VerbraucherEinStufig -name '*.php' -print0 | xargs -0 -n1 php -l",
"test": "phpunit", "test": "phpunit",
"test:verbraucher-einstufig": "phpunit --configuration phpunit.verbraucher-einstufig.xml",
"check": [ "check": [
"@lint", "@lint",
"@test" "@test"
],
"check:verbraucher-einstufig": [
"@lint:verbraucher-einstufig",
"@test:verbraucher-einstufig"
] ]
}, },
"config": { "config": {
@@ -2,56 +2,73 @@
## Status ## Status
Akzeptiert. Akzeptiert, am 17. September 2026 um getrennte Mindestzeiten erweitert.
## Kontext ## Kontext
Das Enelix-1-Modul berechnete seinen Zustand in einem festen Intervall und Das Enelix-1-Modul berechnete seinen Zustand in einem festen Intervall und
bildete die Lastwechselsperre ueber `Interval`, `IdleCounterMax` und weitere bildete Lastwechselsperren ueber `Interval`, `IdleCounterMax` und weitere
zyklusabhaengige Zaehler ab. Dieses Verhalten passt nicht zur zyklusabhaengige Zaehler ab. Ein erster Enelix-2-Stand ersetzte dies durch
ereignisbasierten Enelix-2-Kommunikation und machte reale Zeitabstaende von einen allgemeinen `Umschaltabstand`. Dieser konnte unterschiedliche
mehreren Properties abhaengig. Geraeteanforderungen fuer Ein- und Aus-Zustand nicht ausdruecken.
Bei einem asynchron schaltenden Geraet kann ausserdem der Aktorbefehl nicht als
physische Zustandsbestaetigung gelten.
## Entscheidung ## Entscheidung
Der neue `VerbraucherEinStufig` verwendet keine zyklische Regelberechnung. Der `VerbraucherEinStufig` verwendet keine zyklische Regelberechnung.
Schaltkontakt, Rueckmeldung, Manager-Vorgabe, Freigabe, Vorgabeablauf und Schaltkontakt, Rueckmeldung, Manager-Vorgabe, Freigabe, Vorgabeablauf und
Tagesplanung loesen die Regelung direkt aus. Tagesplanung loesen die Regelung direkt aus.
Als einziger modulspezifischer Wert fuer die Lastwechselbegrenzung wird Der allgemeine `Umschaltabstand` entfaellt. An seine Stelle treten:
`Umschaltabstand` in Sekunden registriert. Der Standardwert ist 5 Sekunden.
Das Ende der Sperre wird durch einen einmaligen Timer ausgeloest. - `Mindesteinschaltdauer` in Sekunden, Standard 5,
- `Mindestausschaltdauer` in Sekunden, Standard 5.
Die passende Mindestzeit beginnt mit dem bestaetigten Zustandswechsel. Ohne
separate Rueckmeldung bestaetigt die Aktorvariable den Wechsel unmittelbar.
Mit separater Rueckmeldung beginnt die Mindestzeit erst, wenn deren Wert den
Zielzustand erreicht.
Bis dahin bleiben Istleistung und Leistungsangebot beim rueckgemeldeten
Zustand. `SchaltbefehlAusstehend=true`,
`AenderungMoeglich=false` und `Schaltbereit=false` machen die laufende
Umschaltung fuer den Manager sichtbar.
Das gemeinsame `Meldeintervall` bleibt erhalten, weil Vertrag `3.0` Das gemeinsame `Meldeintervall` bleibt erhalten, weil Vertrag `3.0`
zusaetzlich zu Ereignismeldungen eine periodische Vollmeldung fordert. Dieser zusaetzlich zu Ereignismeldungen eine periodische Vollmeldung fordert. Dieser
Timer ist kein Regelzyklus. Timer ist kein Regelzyklus.
Die Tagesmindestlaufzeit wird in realen Sekunden und lokaler Symcon-Zeitzone Die Tagesmindestlaufzeit wird in realen Sekunden und lokaler Symcon-Zeitzone
gezaehlt. Das Modul erzwingt den Betrieb erst zum spaetestmoeglichen Zeitpunkt, gezaehlt. Bei separater Rueckmeldung zaehlt ausschliesslich der bestaetigte
an dem die fehlende Laufzeit vor Mitternacht noch erreicht werden kann. Ein-Zustand.
## Alternativen ## Alternativen
- Ein fester Regelzyklus wurde verworfen, weil Reaktionszeit und Sperrzeit - Ein fester Regelzyklus wurde verworfen, weil Reaktionszeit und Zeitregeln
wieder voneinander abhaengen wuerden. wieder voneinander abhaengen wuerden.
- Getrennte Ein- und Ausschaltzeiten wurden verworfen, weil sie fuer diese - Ein allgemeiner Umschaltabstand wurde verworfen, weil Ein- und Aus-Zustand
Adaption nicht freigegeben sind. unterschiedliche Mindestzeiten benoetigen koennen.
- Die feste Nachtphase aus Enelix 1 wurde verworfen, weil sie weder - Der Aktorbefehl als sofortige physische Bestaetigung wurde bei vorhandener
konfigurierbar noch Teil der Enelix-2-Spezifikation ist. Rueckmeldung verworfen.
- Ein zusaetzlicher Rueckmelde-Timeout wurde nicht eingefuehrt. Ein
ausstehender Befehl bleibt transparent sichtbar, bis ein neues Ereignis den
Zustand klaert.
## Folgen ## Folgen
- Lastwechsel reagieren ohne Polling auf relevante Ereignisse. - Lastwechsel reagieren ohne Polling auf relevante Ereignisse.
- `Interval` und `IdleCounterMax` entfallen. - `Interval`, `IdleCounterMax` und `Umschaltabstand` entfallen.
- Der Manager sieht waehrend einer Sperre nur die aktuelle Leistung und - Waerend Mindestzeiten und ausstehenden Rueckmeldungen wird nur die
`AenderungMoeglich=false`. bestaetigte Istleistung angeboten.
- Ein optionaler Rueckmeldekontakt muss innerhalb des Umschaltabstands folgen. - Der Manager erhaelt Status und Restmindestzeit als Zustandseintraege.
- Tageslaufzeit und Mindestlaufzeit sind unabhaengig von einer Zyklusdauer. - Eine unerwartete Abweichung zwischen Aktor und Rueckmeldung ausserhalb eines
- Das Modul bleibt vollstaendig im Repository Enelix EMS; Enelix Utils erhaelt laufenden Schaltvorgangs wird als Stoerung gemeldet.
keine geraetespezifische Logik. - Tageslaufzeit und Mindestzeiten sind unabhaengig von einer Zyklusdauer.
- Das Modul bleibt vollstaendig im Repository Enelix EMS.
## Offene Punkte ## Offene Punkte
- Eine spaetere sperrbare Variante wird separat spezifiziert. - Eine spaetere sperrbare Variante wird separat spezifiziert.
- Unterschiedliche Ein- und Ausschaltabstaende benoetigen eine neue - Ein konfigurierbarer Rueckmelde-Timeout benoetigt eine eigene Entscheidung.
Architekturentscheidung.
+24 -23
View File
@@ -1,27 +1,28 @@
# Migration von Verbraucher_1_Stufig # Migration von Verbraucher_1_Stufig
Das Enelix-1-Modul wird nicht in-place aktualisiert. Das neue Modul verwendet Das Enelix-1-Modul wird nicht in-place aktualisiert. Das neue Modul verwendet
eine neue Modul-ID, den Vertrag `3.0` und eine ereignisbasierte Regelung. eine neue Modul-ID, Vertrag `3.0` und eine ereignisbasierte Regelung.
## Eigenschaften ## Eigenschaften
| Enelix 1 | Enelix 2 | Migration | | Enelix 1 / bisheriges Enelix 2 | Aktuelles Enelix 2 | Migration |
| --- | --- | --- | | --- | --- | --- |
| `BoilerLeistung` | `Nennleistung` | Wert in ganzen Watt uebernehmen. | | `BoilerLeistung` | `Nennleistung` | Wert in ganzen Watt uebernehmen. |
| `Schaltkontakt1` | `SchaltkontaktVariableID` | Boolean-Aktor uebernehmen und Aktion pruefen. | | `Schaltkontakt1` | `SchaltkontaktVariableID` | Boolean-Aktor uebernehmen und Aktion pruefen. |
| nicht vorhanden | `SchaltkontaktInvertiert` | Nur bei umgekehrter Aktorlogik aktivieren. | | nicht vorhanden | `SchaltkontaktInvertiert` | Nur bei umgekehrter Aktorlogik aktivieren. |
| nicht vorhanden | `RueckmeldungVariableID` | Optional eine echte Boolean-Rueckmeldung zuordnen. | | nicht vorhanden | `RueckmeldungVariableID` | Optional eine echte Boolean-Rueckmeldung zuordnen. |
| `Mindesttlaufzeit` | `Mindestlaufzeit` | Alter Wert war in Minuten gedacht; fuer gleiches Verhalten mit 60 multiplizieren und als Sekunden eintragen. | | `Mindesttlaufzeit` | `Mindestlaufzeit` | Alten Minutenwert fuer gleiches Verhalten mit 60 multiplizieren. |
| `Zeit_Zwischen_Zustandswechseln` | `Umschaltabstand` | Alte Minuten nicht uebernehmen. Neuen Sekundenwert bewusst festlegen; Standard ist 5 s. | | `Zeit_Zwischen_Zustandswechseln` | `Mindesteinschaltdauer`, `Mindestausschaltdauer` | Alte Minuten nicht ungeprueft uebernehmen; beide Sekundenwerte geraetespezifisch setzen. |
| `Interval` | entfaellt | Keine zyklische Regelberechnung mehr. | | `Umschaltabstand` aus fruehem Enelix 2 | `Mindesteinschaltdauer`, `Mindestausschaltdauer` | Der alte Wert wird nicht automatisch uebertragen; beide Werte haben Standard 5 s. |
| `IdleCounterMax` | entfaellt | Keine zyklusbasierte Idle-Erkennung mehr. | | `Interval` | entfaellt | Keine zyklische Regelberechnung. |
| `IdleCounterMax` | entfaellt | Keine zyklusbasierte Idle-Erkennung. |
## Variablen und Verhalten ## Variablen und Verhalten
| Enelix 1 | Enelix 2 | | Alt | Aktuell |
| --- | --- | | --- | --- |
| `DailyOnTime` | `Tageslaufzeit` in echten Sekunden | | `DailyOnTime` | `Tageslaufzeit` in bestaetigten echten Sekunden |
| `IstNacht` | entfaellt; Mindestlaufzeit wird am spaetestmoeglichen Start des lokalen Kalendertags erzwungen | | `IstNacht` | entfaellt; Tagesmindestlaufzeit wird spaetestmoeglich erzwungen |
| `IsTimerActive` | entfaellt; interne einmalige Timer | | `IsTimerActive` | entfaellt; interne einmalige Timer |
| `Aktuelle_Leistung` | gemeinsames `Istleistung` | | `Aktuelle_Leistung` | gemeinsames `Istleistung` |
| `Power` | gemeinsames `Sollleistung` | | `Power` | gemeinsames `Sollleistung` |
@@ -32,25 +33,25 @@ eine neue Modul-ID, den Vertrag `3.0` und eine ereignisbasierte Regelung.
| `Leistung_Delta` | entfaellt; Leistungsverteilung liegt im Manager | | `Leistung_Delta` | entfaellt; Leistungsverteilung liegt im Manager |
| `Idle`, `IdleCounter` | entfallen | | `Idle`, `IdleCounter` | entfallen |
Die feste Enelix-1-Nachtzeit von 22:00 bis 07:00 wird nicht uebernommen. Bei konfigurierter Rueckmeldung bestimmt nicht mehr der ausgegebene
Enelix 2 plant nur den Zeitpunkt, ab dem die verbleibende Mindestlaufzeit vor Aktorbefehl, sondern erst der rueckgemeldete Zustand die Istleistung, die
dem lokalen Tagesende zwingend eingeschaltet werden muss. Tageslaufzeit und den Start der jeweiligen Mindestzeit.
## Inbetriebnahme ## Inbetriebnahme
1. Bestehende Instanzkonfiguration und aktuelle Aktorlogik dokumentieren. 1. Bestehende Instanzkonfiguration und Aktorlogik dokumentieren.
2. Neue Instanz **Verbraucher 1-Stufig** anlegen. 2. Neue Instanz **Verbraucher 1-Stufig** anlegen oder die bestehende
Enelix-2-Instanz aktualisieren.
3. Leistung, Aktor, Invertierung und optional Rueckmeldung konfigurieren. 3. Leistung, Aktor, Invertierung und optional Rueckmeldung konfigurieren.
4. Mindestlaufzeit von Minuten in Sekunden umrechnen. 4. Tagesmindestlaufzeit gegebenenfalls von Minuten in Sekunden umrechnen.
5. `Umschaltabstand` neu in Sekunden festlegen; nicht den alten Minutenwert 5. Mindest-Ein- und Mindest-Aus-Zeit getrennt anhand der Geraeteanforderungen
kopieren. setzen.
6. Neue Instanz im Enelix-2-Manager aktiv zuordnen. 6. Neue Instanz im Enelix-2-Manager aktiv zuordnen.
7. Bei ausgeschalteter lokaler EMS-Freigabe Ein/Aus und Rueckmeldung 7. Ein- und Ausschalten zuerst ohne reale Last oder unter Aufsicht pruefen.
beaufsichtigt pruefen. 8. Bei separater Rueckmeldung kontrollieren, dass
8. Alte Instanz erst deaktivieren, danach `Aktiv` an der neuen Instanz `SchaltbefehlAusstehend` erst mit deren Zustandswechsel verschwindet.
einschalten. 9. Mindestens je einen verhinderten Ein- und Ausschaltvorgang testen.
9. Mindestens einen Timeout, einen verhinderten Lastwechsel und einen 10. Tageslaufzeit und Tageswechsel kontrollieren.
Tageswechsel kontrollieren.
## Rueckkehr ## Rueckkehr
+118 -63
View File
@@ -3,9 +3,10 @@
> Status: implementiert fuer IP-Symcon 8 und den Enelix-2-Vertrag `3.0`. > Status: implementiert fuer IP-Symcon 8 und den Enelix-2-Vertrag `3.0`.
Das Modul schaltet einen elektrischen Ein-/Aus-Verbraucher. Im freien Zustand Das Modul schaltet einen elektrischen Ein-/Aus-Verbraucher. Im freien Zustand
meldet es dem Manager das Leistungsangebot `[0, Nennleistung]`. Waehrend der meldet es dem Manager das Leistungsangebot `[0, Nennleistung]`. Waerend einer
Umschaltsperre und bei lokal erzwungener Mindestlaufzeit wird nur der aktuell Mindest-Ein- oder Mindest-Aus-Zeit, waehrend einer ausstehenden Rueckmeldung
zulaessige Leistungswert angeboten. und bei lokal erzwungener Tagesmindestlaufzeit wird nur der aktuelle
bestaetigte Leistungswert angeboten.
## Architekturentscheidung ## Architekturentscheidung
@@ -14,18 +15,19 @@ Die Regelung ist ereignisbasiert. Neuberechnungen werden ausgeloest durch:
- eine neue Manager-Vorgabe, - eine neue Manager-Vorgabe,
- eine Aenderung an Schaltkontakt oder optionaler Rueckmeldung, - eine Aenderung an Schaltkontakt oder optionaler Rueckmeldung,
- das Ein- oder Ausschalten der lokalen EMS-Freigabe, - das Ein- oder Ausschalten der lokalen EMS-Freigabe,
- das Ende des Umschaltabstands, - das Ende der zustandsabhaengigen Mindestzeit,
- das Ablaufen einer Manager-Vorgabe, - das Ablaufen einer Manager-Vorgabe,
- den naechsten relevanten Zeitpunkt der Tageslaufzeitplanung. - den naechsten relevanten Zeitpunkt der Tageslaufzeitplanung.
Es gibt keinen Regelzyklus, kein `Interval` und keinen `IdleCounterMax`. Es gibt keinen Regelzyklus, kein `Interval`, keinen `IdleCounterMax` und
`Meldeintervall` bleibt als vertraglich geforderte periodische Vollmeldung keinen allgemeinen `Umschaltabstand`. `Meldeintervall` bleibt als
bestehen und steuert keine Regelberechnung. vertraglich geforderte periodische Vollmeldung bestehen und steuert keine
Regelberechnung.
## Properties ## Properties
Zu den sechs gemeinsamen Verbraucher-Properties aus Zu den sechs gemeinsamen Verbraucher-Properties aus
[`Schnittstelle.md`](../../Schnittstelle.md) kommen sieben [`Schnittstelle.md`](../../Schnittstelle.md) kommen acht
Modul-Properties hinzu. Modul-Properties hinzu.
| Ident | Typ | Standard | Beschreibung | | Ident | Typ | Standard | Beschreibung |
@@ -33,14 +35,19 @@ Modul-Properties hinzu.
| `Nennleistung` | Integer | `0` | Positive elektrische Leistung in W. | | `Nennleistung` | Integer | `0` | Positive elektrische Leistung in W. |
| `SchaltkontaktVariableID` | Integer | `0` | Boolean-Aktor mit Standard- oder benutzerdefinierter Aktion. | | `SchaltkontaktVariableID` | Integer | `0` | Boolean-Aktor mit Standard- oder benutzerdefinierter Aktion. |
| `SchaltkontaktInvertiert` | Boolean | `false` | Kehrt die Ein-/Aus-Semantik des Aktors um. | | `SchaltkontaktInvertiert` | Boolean | `false` | Kehrt die Ein-/Aus-Semantik des Aktors um. |
| `RueckmeldungVariableID` | Integer | `0` | Optionale Boolean-Rueckmeldung; `true` bedeutet eingeschaltet. | | `RueckmeldungVariableID` | Integer | `0` | Optionale Boolean-Rueckmeldung; `true` bedeutet physisch eingeschaltet. |
| `Mindestlaufzeit` | Integer | `0` | Geforderte Laufzeit pro lokalem Kalendertag in Sekunden, maximal 86400. | | `Mindestlaufzeit` | Integer | `0` | Geforderte Laufzeit pro lokalem Kalendertag in Sekunden, maximal 86400. |
| `Umschaltabstand` | Integer | `5` | Sekunden bis zum naechsten erlaubten Lastwechsel. | | `Mindesteinschaltdauer` | Integer | `5` | Mindestzeit in Sekunden, die ein bestaetigter Ein-Zustand gehalten wird. |
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Legt gemeinsame Diagnosevariablen und `Rueckmeldefehler` an. | | `Mindestausschaltdauer` | Integer | `5` | Mindestzeit in Sekunden, die ein bestaetigter Aus-Zustand gehalten wird. |
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Legt gemeinsame und modulspezifische Diagnosevariablen an. |
Beide Mindestzeiten duerfen `0` sein. Die lokale Aktion `Aktiv=false`
bleibt der einzige bewusste Override und schaltet sicher aus, auch wenn die
Mindesteinschaltdauer noch laeuft.
`EinstellungenInVisu` ist Teil der gemeinsamen Verbraucherbasis. Das Modul `EinstellungenInVisu` ist Teil der gemeinsamen Verbraucherbasis. Das Modul
besitzt derzeit keine zusaetzlichen bedienbaren Einstellvariablen und wertet besitzt derzeit keine zusaetzlichen bedienbaren Einstellvariablen und wertet
diese Property deshalb noch nicht weiter aus. diese Property deshalb nicht weiter aus.
## Variablen ## Variablen
@@ -49,10 +56,10 @@ Immer sichtbar:
| Ident | Typ / Zugriff | Beschreibung | | Ident | Typ / Zugriff | Beschreibung |
| --- | --- | --- | | --- | --- | --- |
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; Startwert `false`. | | `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; Startwert `false`. |
| `Schaltzustand` | Boolean / Anzeige | Rueckgemeldeter oder aus dem Aktor berechneter Zustand. | | `Schaltzustand` | Boolean / Anzeige | Bestaetigter oder aus dem Aktor berechneter Zustand. |
| `Tageslaufzeit` | Integer / Anzeige | Laufzeit des lokalen Kalendertags in Sekunden. | | `Tageslaufzeit` | Integer / Anzeige | Bestaetigte Laufzeit des lokalen Kalendertags in Sekunden. |
Mit `DiagnosevariablenAnzeigen` werden diese Variablen zusaetzlich angelegt: Mit `DiagnosevariablenAnzeigen` werden zusaetzlich angelegt:
- `Istleistung` - `Istleistung`
- `Leistungsquelle` - `Leistungsquelle`
@@ -63,47 +70,83 @@ Mit `DiagnosevariablenAnzeigen` werden diese Variablen zusaetzlich angelegt:
- `Stoerung` - `Stoerung`
- `Stoertext` - `Stoertext`
- `Rueckmeldefehler` - `Rueckmeldefehler`
- `SchaltbefehlAusstehend`
- `RestMindestzeit`
Die Regellogik arbeitet mit persistenten Attributen und ist nicht von der Die Regellogik arbeitet mit persistenten Attributen und ist nicht von der
Sichtbarkeit der Diagnosevariablen abhaengig. Sichtbarkeit der Diagnosevariablen abhaengig.
## Zeit- und Schaltverhalten ## Mindest-Ein- und Mindest-Aus-Zeit
Nach jedem Lastwechsel wird `AenderungMoeglich=false` gemeldet und das Nach einer bestaetigten Einschaltung wird fuer
Leistungsangebot auf den aktuellen Wert eingeschraenkt. Nach `Mindesteinschaltdauer` Sekunden kein regulaeres Ausschalten angeboten. Nach
`Umschaltabstand` Sekunden gibt ein einmaliger Timer den naechsten einer bestaetigten Ausschaltung blockiert `Mindestausschaltdauer` ein
Lastwechsel frei. Der Standardwert betraegt 5 Sekunden. regulaeres Einschalten.
Waerend einer Mindestzeit gilt:
- `AenderungMoeglich=false`,
- `Schaltbereit=false`,
- `RestMindestzeit_s` enthaelt die verbleibenden Sekunden,
- `Leistungswerte_W` enthaelt nur die bestaetigte Istleistung.
Ein einmaliger Timer loest am Ende der Mindestzeit genau eine Neuberechnung
aus. Der fruehere allgemeine `Umschaltabstand` ist vollstaendig entfallen.
## Rueckmeldung und Schaltbereitschaft
Ohne separate Rueckmeldung wird der logische Zustand der Aktorvariable als
sofortige Schaltbestaetigung verwendet.
Mit `RueckmeldungVariableID` ist ausschliesslich deren Booleanwert fuer
`Schaltzustand`, `Istleistung_W`, Tageslaufzeit und den Beginn der
Mindestzeit massgeblich. Nach einem Aktorbefehl und vor der passenden
Rueckmeldung meldet das Modul:
- die bisherige bestaetigte Istleistung,
- `SchaltbefehlAusstehend=true`,
- `Schaltbereit=false`,
- `AenderungMoeglich=false`,
- nur den bisherigen Leistungswert in `Leistungswerte_W`.
Sobald die Rueckmeldung den Zielzustand bestaetigt, beginnt die passende
Mindest-Ein- oder Mindest-Aus-Zeit. Es gibt bewusst keinen zusaetzlichen
zyklischen Rueckmelde-Timeout. Bleibt die Rueckmeldung aus, bleibt der
Schaltbefehl sichtbar ausstehend.
Weichen Aktor- und Rueckmeldezustand ohne ausstehenden Schaltbefehl voneinander
ab, wird `Rueckmeldefehler=true`, `Verfuegbar=false` und eine Stoerung
gemeldet.
## Tagesmindestlaufzeit
Die Tageslaufzeit wird mit der in IP-Symcon eingestellten lokalen Zeitzone Die Tageslaufzeit wird mit der in IP-Symcon eingestellten lokalen Zeitzone
gefuehrt. Sommer- und Winterzeit sowie der Wechsel um Mitternacht werden gefuehrt. Sommer- und Winterzeit sowie der Wechsel um Mitternacht werden
beruecksichtigt. Ist die konfigurierte Mindestlaufzeit noch nicht erreicht, beruecksichtigt. Nur der bestaetigte Ein-Zustand zaehlt.
wird der Verbraucher erst zum spaetestmoeglichen Zeitpunkt lokal
eingeschaltet, an dem die Restlaufzeit bis Mitternacht noch erfuellt werden
kann. In dieser Phase meldet er `AenderungMoeglich=false`.
Bei Aktivierung sehr kurz vor Tagesende kann eine unmoeglich hohe Restlaufzeit Ist die konfigurierte Tagesmindestlaufzeit noch nicht erreicht, wird der
naturgemaess nicht mehr vollstaendig nachgeholt werden. Der Verbraucher wird Verbraucher zum spaetestmoeglichen Zeitpunkt angefordert, an dem die
dann sofort eingeschaltet und die tatsaechliche Laufzeit erfasst. Restlaufzeit bis Mitternacht noch erfuellt werden kann. Eine noch aktive
Mindestausschaltdauer hat dabei Vorrang; eine unmoegliche Restlaufzeit kann
## Rueckmeldung und Fehler nicht rueckwirkend nachgeholt werden.
Ohne separate Rueckmeldung wird der Schaltzustand aus der Aktorvariable
berechnet. Mit `RueckmeldungVariableID` wartet das Modul bis zum Ende des
Umschaltabstands auf den angeforderten Zustand. Liegt er dann nicht an, werden
`Rueckmeldefehler=true`, `Stoerung=true` und `Verfuegbar=false` gemeldet.
Die Istleistung ist immer berechnet: `0` oder `Nennleistung`. Deshalb wird
`Leistungsquelle=1` gemeldet, auch wenn der Boolean-Schaltzustand ueber eine
physische Rueckmeldung erfasst wird.
## Managerkommunikation ## Managerkommunikation
Das Modul implementiert Das Modul implementiert
`VerbraucherSchnittstelle::ManagerdatenEmpfangen()` und verwendet `VerbraucherSchnittstelle::ManagerdatenEmpfangen()` und verwendet
ausschliesslich den Vertrag `3.0`. Der Verbraucher besitzt keine ausschliesslich Vertrag `3.0`. Der Verbraucher besitzt keine
Manager-ID-Property. Er akzeptiert nur Manager, in deren manueller oder Manager-ID-Property. Er akzeptiert nur Manager, in deren manueller oder
automatischer Verbraucherzuordnung seine Instanz aktiv eingetragen ist. automatischer Verbraucherzuordnung seine Instanz aktiv eingetragen ist.
Neben den gemeinsamen Vertragsfeldern werden diese Zustaende gemeldet:
- `Schaltzustand`
- `SchaltbefehlAusstehend`
- `Schaltbereit`
- `RestMindestzeit_s`
- `Tageslaufzeit_s`
- `Rueckmeldefehler`
Rueckmeldungen erfolgen bei relevanten Ereignissen, kurz verzoegert nach einer Rueckmeldungen erfolgen bei relevanten Ereignissen, kurz verzoegert nach einer
Manager-Vorgabe und zusaetzlich alle `Meldeintervall` Sekunden. Eine Vorgabe Manager-Vorgabe und zusaetzlich alle `Meldeintervall` Sekunden. Eine Vorgabe
wird nach `VorgabeTimeout` Sekunden ohne Erneuerung ungueltig. wird nach `VorgabeTimeout` Sekunden ohne Erneuerung ungueltig.
@@ -116,16 +159,14 @@ wird nach `VorgabeTimeout` Sekunden ohne Erneuerung ungueltig.
2. Unter **Instanz hinzufuegen** nach **Verbraucher 1-Stufig** suchen und eine 2. Unter **Instanz hinzufuegen** nach **Verbraucher 1-Stufig** suchen und eine
Instanz anlegen. Instanz anlegen.
3. `Nennleistung` in ganzen Watt eintragen. 3. `Nennleistung` in ganzen Watt eintragen.
4. Als `Schaltkontakt` eine Booleanvariable mit funktionsfaehiger Aktion 4. Als Schaltkontakt eine Booleanvariable mit funktionsfaehiger Aktion
auswaehlen. Bei umgekehrter Aktorlogik `SchaltkontaktInvertiert` auswaehlen und bei Bedarf `SchaltkontaktInvertiert` aktivieren.
aktivieren.
5. Optional eine separate Boolean-Rueckmeldung auswaehlen. Dort muss 5. Optional eine separate Boolean-Rueckmeldung auswaehlen. Dort muss
`true` dem physisch eingeschalteten Verbraucher entsprechen. `true` dem physisch eingeschalteten Verbraucher entsprechen.
6. Prioritaeten, `Mindestlaufzeit`, `Umschaltabstand`, 6. Tagesmindestlaufzeit, Mindest-Einschaltdauer und Mindest-Ausschaltdauer
`Meldeintervall` und `VorgabeTimeout` einstellen und die Konfiguration passend zum angeschlossenen Geraet festlegen.
uebernehmen. 7. Die Instanz im Manager manuell aktiv zuordnen oder bei automatischer Suche
7. Die neue Instanz im Manager manuell aktiv zuordnen oder bei automatischer in der gefundenen Liste aktivieren.
Suche in der gefundenen Liste aktivieren.
8. Fuer die Erstpruefung `DiagnosevariablenAnzeigen` und bei Bedarf 8. Fuer die Erstpruefung `DiagnosevariablenAnzeigen` und bei Bedarf
`LoggingEin` einschalten. `LoggingEin` einschalten.
9. Unter Aufsicht `Aktiv` einschalten und ueber den Manager je eine Ein- und 9. Unter Aufsicht `Aktiv` einschalten und ueber den Manager je eine Ein- und
@@ -134,23 +175,37 @@ wird nach `VorgabeTimeout` Sekunden ohne Erneuerung ungueltig.
## Abnahmecheckliste ## Abnahmecheckliste
- `Aktiv=false` schaltet den Ausgang aus. - `Aktiv=false` schaltet den Ausgang aus.
- `Aktiv=true` und eine Manager-Vorgabe `Nennleistung` schalten den Ausgang - Eine Ein-Vorgabe setzt zuerst den Aktor; mit separater Rueckmeldung bleibt
ein. `SchaltbefehlAusstehend=true`, bis diese einschaltet.
- Direkt nach einem Wechsel ist `AenderungMoeglich=false`; nach 5 Sekunden - Die Mindesteinschaltdauer beginnt erst mit der Ein-Rueckmeldung.
wird es mit Standardkonfiguration wieder `true`. - Vor Ablauf der Mindesteinschaltdauer wird kein Ausschalten angeboten.
- Eine Gegenanforderung waehrend der Sperre wird vom Leistungsangebot - Nach bestaetigter Ausschaltung beginnt die Mindestausschaltdauer.
ausgeschlossen. - Vor Ablauf der Mindestausschaltdauer wird kein Einschalten angeboten.
- Die optionale Rueckmeldung folgt dem Ausgang innerhalb des - `RestMindestzeit_s` erreicht 0 und `Schaltbereit` wird danach true.
Umschaltabstands; andernfalls erscheint eine Stoerung. - `Tageslaufzeit` steigt nur bei bestaetigtem Ein-Zustand.
- `Tageslaufzeit` steigt nur im eingeschalteten Zustand und beginnt am - Ein unerwarteter Unterschied zwischen Aktor und Rueckmeldung erzeugt eine
lokalen Tageswechsel wieder bei 0. Stoerung.
- Nach Ablauf von `VorgabeTimeout` wird ohne neue Manager-Vorgabe - Nach einem Neustart wird keine alte Manager-Vorgabe ungeprueft fortgesetzt.
ausgeschaltet, sobald der Umschaltabstand dies zulaesst.
- Nach Neustart oder erneuter Konfiguration wird der reale Schaltzustand ## Tests
eingelesen; alte Manager-Vorgaben werden nicht ungeprueft fortgesetzt.
Nur dieses Modul:
```bash
composer check:verbraucher-einstufig
```
Gesamtes Repository inklusive dieser Modulsuite:
```bash
composer check
```
Die separate Suite und ihre Struktur sind unter
[`tests/VerbraucherEinStufig`](../../../tests/VerbraucherEinStufig/README.md)
dokumentiert.
## Migration ## Migration
Die Zuordnung der Enelix-1-Felder, Einheiten und bewusst entfallenen Die Zuordnung der Enelix-1- und bisherigen Enelix-2-Zeitwerte steht in
Kommunikationsvariablen steht in
[`docs/migration/Verbraucher-1-Stufig.md`](../../migration/Verbraucher-1-Stufig.md). [`docs/migration/Verbraucher-1-Stufig.md`](../../migration/Verbraucher-1-Stufig.md).
+22
View File
@@ -34,6 +34,28 @@ final class EinStufigRegler
return [0, $nennleistung]; return [0, $nennleistung];
} }
public static function freigabezeitpunkt(
bool $eingeschaltet,
int $bestaetigtSeit,
int $mindesteinschaltdauer,
int $mindestausschaltdauer
): int {
if ($bestaetigtSeit < 0 || $mindesteinschaltdauer < 0 || $mindestausschaltdauer < 0) {
throw new InvalidArgumentException('Zeitwerte duerfen nicht negativ sein.');
}
return $bestaetigtSeit + ($eingeschaltet ? $mindesteinschaltdauer : $mindestausschaltdauer);
}
public static function restMindestzeit(int $freigabezeitpunkt, int $jetzt): int
{
if ($freigabezeitpunkt < 0 || $jetzt < 0) {
throw new InvalidArgumentException('Zeitwerte duerfen nicht negativ sein.');
}
return max(0, $freigabezeitpunkt - $jetzt);
}
public static function mussMindestlaufzeitErzwingen( public static function mussMindestlaufzeitErzwingen(
int $tageslaufzeit, int $tageslaufzeit,
int $mindestlaufzeit, int $mindestlaufzeit,
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
failOnWarning="true"
failOnRisky="true">
<testsuites>
<testsuite name="Verbraucher 1-Stufig">
<directory>tests/VerbraucherEinStufig</directory>
</testsuite>
</testsuites>
</phpunit>
+1 -1
View File
@@ -35,7 +35,7 @@ final class ManagerModulstrukturTest extends TestCase
512, 512,
JSON_THROW_ON_ERROR JSON_THROW_ON_ERROR
); );
$json = json_encode($formular, JSON_THROW_ON_ERROR); $json = json_encode($formular, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
foreach ([ foreach ([
'NetzleistungVariableID', 'NetzleistungVariableID',
@@ -43,6 +43,25 @@ final class EinStufigReglerTest extends TestCase
EinStufigRegler::leistungsangebot(3500, 1200, true, true, false); EinStufigRegler::leistungsangebot(3500, 1200, true, true, false);
} }
public function testFreigabeVerwendetZustandsabhaengigeMindestdauer(): void
{
self::assertSame(1010, EinStufigRegler::freigabezeitpunkt(true, 1000, 10, 20));
self::assertSame(1020, EinStufigRegler::freigabezeitpunkt(false, 1000, 10, 20));
}
public function testRestMindestzeitWirdNieNegativ(): void
{
self::assertSame(7, EinStufigRegler::restMindestzeit(1007, 1000));
self::assertSame(0, EinStufigRegler::restMindestzeit(1000, 1007));
}
public function testNegativeMindestdauerWirdAbgewiesen(): void
{
$this->expectException(InvalidArgumentException::class);
EinStufigRegler::freigabezeitpunkt(true, 1000, -1, 0);
}
public function testMindestlaufzeitWirdErstZumSpaetestmoeglichenStartErzwungen(): void public function testMindestlaufzeitWirdErstZumSpaetestmoeglichenStartErzwungen(): void
{ {
self::assertFalse(EinStufigRegler::mussMindestlaufzeitErzwingen(0, 3600, 7200)); self::assertFalse(EinStufigRegler::mussMindestlaufzeitErzwingen(0, 3600, 7200));
@@ -8,7 +8,7 @@ use PHPUnit\Framework\TestCase;
final class VerbraucherEinStufigModulstrukturTest extends TestCase final class VerbraucherEinStufigModulstrukturTest extends TestCase
{ {
private const MODULPFAD = __DIR__ . '/../VerbraucherEinStufig/'; private const MODULPFAD = __DIR__ . '/../../VerbraucherEinStufig/';
public function testModulIstVollstaendigUndMetadatenSindGueltig(): void public function testModulIstVollstaendigUndMetadatenSindGueltig(): void
{ {
@@ -43,7 +43,7 @@ final class VerbraucherEinStufigModulstrukturTest extends TestCase
public function testPropertiesSindRegistriertUndImFormularVorhanden(): void public function testPropertiesSindRegistriertUndImFormularVorhanden(): void
{ {
$basis = (string) file_get_contents(__DIR__ . '/../libs/VerbraucherBasisTrait.php'); $basis = (string) file_get_contents(__DIR__ . '/../../libs/VerbraucherBasisTrait.php');
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php'); $modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
preg_match_all( preg_match_all(
'/RegisterProperty(?:Integer|Float|Boolean|String)\(\'([^\']+)\'/', '/RegisterProperty(?:Integer|Float|Boolean|String)\(\'([^\']+)\'/',
@@ -65,7 +65,8 @@ final class VerbraucherEinStufigModulstrukturTest extends TestCase
'SchaltkontaktInvertiert', 'SchaltkontaktInvertiert',
'RueckmeldungVariableID', 'RueckmeldungVariableID',
'Mindestlaufzeit', 'Mindestlaufzeit',
'Umschaltabstand', 'Mindesteinschaltdauer',
'Mindestausschaltdauer',
'DiagnosevariablenAnzeigen', 'DiagnosevariablenAnzeigen',
], ],
$properties $properties
@@ -77,17 +78,21 @@ final class VerbraucherEinStufigModulstrukturTest extends TestCase
} }
} }
public function testNurUmschaltabstandErsetztAlteZyklusProperties(): void public function testGetrennteMindestzeitenErsetzenUmschaltabstand(): void
{ {
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php'); $modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
self::assertStringContainsString( self::assertStringContainsString(
"RegisterPropertyInteger('Umschaltabstand', 5)", "RegisterPropertyInteger('Mindesteinschaltdauer', 5)",
$modul $modul
); );
self::assertStringContainsString(
"RegisterPropertyInteger('Mindestausschaltdauer', 5)",
$modul
);
self::assertStringNotContainsString("RegisterPropertyInteger('Umschaltabstand'", $modul);
self::assertStringNotContainsString("RegisterPropertyInteger('Interval'", $modul); self::assertStringNotContainsString("RegisterPropertyInteger('Interval'", $modul);
self::assertStringNotContainsString("RegisterPropertyInteger('IdleCounterMax'", $modul); self::assertStringNotContainsString("RegisterPropertyInteger('IdleCounterMax'", $modul);
self::assertStringNotContainsString("RegisterTimer('Regelzyklus'", $modul);
} }
public function testRegelungReagiertAufEreignisseUndEinmalTimer(): void public function testRegelungReagiertAufEreignisseUndEinmalTimer(): void
@@ -96,13 +101,33 @@ final class VerbraucherEinStufigModulstrukturTest extends TestCase
self::assertStringContainsString('public function MessageSink', $modul); self::assertStringContainsString('public function MessageSink', $modul);
self::assertStringContainsString('RegisterMessage($neueID, self::VM_UPDATE)', $modul); self::assertStringContainsString('RegisterMessage($neueID, self::VM_UPDATE)', $modul);
foreach (['Umschaltfreigabe', 'VorgabeTimeout', 'Tagesplanung'] as $timer) { foreach (['MindestzeitAbgelaufen', 'VorgabeTimeout', 'Tagesplanung'] as $timer) {
self::assertStringContainsString("'$timer'", $modul); self::assertStringContainsString("'$timer'", $modul);
} }
self::assertStringContainsString( self::assertStringContainsString(
"SetTimerInterval('Meldezyklus', \$this->ReadPropertyInteger('Meldeintervall') * 1000)", "SetTimerInterval('Meldezyklus', \$this->ReadPropertyInteger('Meldeintervall') * 1000)",
$modul $modul
); );
self::assertStringNotContainsString("case 'Umschaltfreigabe':", $modul);
self::assertStringNotContainsString("RegisterTimer('Regelzyklus'", $modul);
}
public function testRueckmeldungBestimmtZustandUndSchaltbereitschaft(): void
{
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
self::assertStringContainsString(
"ReadPropertyInteger('RueckmeldungVariableID')",
$modul
);
self::assertStringContainsString('bestaetigeZustandswechsel', $modul);
self::assertStringContainsString("'Kennung' => 'SchaltbefehlAusstehend'", $modul);
self::assertStringContainsString("'Kennung' => 'Schaltbereit'", $modul);
self::assertStringContainsString("'Kennung' => 'RestMindestzeit_s'", $modul);
self::assertStringContainsString(
'setzeZustand(\'AenderungMoeglich\', $aenderungMoeglich)',
$modul
);
} }
public function testModulVerwendetAusschliesslichDenVertrag30(): void public function testModulVerwendetAusschliesslichDenVertrag30(): void
@@ -117,25 +142,19 @@ final class VerbraucherEinStufigModulstrukturTest extends TestCase
self::assertStringNotContainsString("RegisterPropertyInteger('ManagerID'", $modul); self::assertStringNotContainsString("RegisterPropertyInteger('ManagerID'", $modul);
} }
public function testFachvariablenUndRueckmeldestoerungSindEnthalten(): void public function testFachvariablenUndRueckmeldestatusSindEnthalten(): void
{ {
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php'); $modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
self::assertStringContainsString( self::assertStringContainsString("RegisterVariableBoolean('Schaltzustand'", $modul);
"RegisterVariableBoolean('Schaltzustand'", self::assertStringContainsString("RegisterVariableInteger('Tageslaufzeit'", $modul);
$modul
);
self::assertStringContainsString(
"RegisterVariableInteger('Tageslaufzeit'",
$modul
);
self::assertStringContainsString("'Kennung' => 'Rueckmeldefehler'", $modul); self::assertStringContainsString("'Kennung' => 'Rueckmeldefehler'", $modul);
self::assertStringContainsString("'Kennung' => 'Tageslaufzeit_s'", $modul); self::assertStringContainsString("'Kennung' => 'Tageslaufzeit_s'", $modul);
} }
public function testManagerKenntDieModulIDGenauEinmal(): void public function testManagerKenntDieModulIDGenauEinmal(): void
{ {
$manager = (string) file_get_contents(__DIR__ . '/../Manager/module.php'); $manager = (string) file_get_contents(__DIR__ . '/../../Manager/module.php');
$modulID = '{15879A4E-D0C2-4495-83DE-46E1E462591E}'; $modulID = '{15879A4E-D0C2-4495-83DE-46E1E462591E}';
self::assertSame(1, substr_count($manager, $modulID)); self::assertSame(1, substr_count($manager, $modulID));
@@ -149,4 +168,20 @@ final class VerbraucherEinStufigModulstrukturTest extends TestCase
self::assertStringContainsString('registriereVerbraucherDiagnose', $modul); self::assertStringContainsString('registriereVerbraucherDiagnose', $modul);
self::assertStringContainsString('UnregisterVariable($ident)', $modul); self::assertStringContainsString('UnregisterVariable($ident)', $modul);
} }
public function testSeparateSuiteIstImGesamttestEnthalten(): void
{
self::assertFileExists(__DIR__ . '/../../phpunit.verbraucher-einstufig.xml');
$composer = json_decode(
(string) file_get_contents(__DIR__ . '/../../composer.json'),
true,
512,
JSON_THROW_ON_ERROR
);
self::assertArrayHasKey('check:verbraucher-einstufig', $composer['scripts']);
$gesamt = (string) file_get_contents(__DIR__ . '/../../phpunit.xml.dist');
self::assertStringContainsString('<directory>tests</directory>', $gesamt);
}
} }
+16
View File
@@ -0,0 +1,16 @@
# Tests fuer Verbraucher 1-Stufig
Die Tests des Moduls koennen unabhaengig vom restlichen Repository ausgefuehrt
werden:
```bash
composer check:verbraucher-einstufig
```
Der Befehl prueft nur den Modulcode, die reine Regellogik und diese Tests.
Der allgemeine Befehl bleibt der externe Gesamteinstieg und schliesst die
Modulsuite durch das rekursive Verzeichnis `tests` automatisch ein:
```bash
composer check
```