*/ private const DIAGNOSE_VARIABLEN = [ 'Istleistung', 'Leistungsquelle', 'Sollleistung', 'SollwertGueltig', 'Verfuegbar', 'AenderungMoeglich', 'Stoerung', 'Stoertext', 'AnlaufAusstehend', 'WiederholsperreAktiv', 'SperrerholungAktiv', 'RestMindestlaufzeit', 'RestMindestsperrzeit', 'Sperrzeit24h', ]; public function Create(): void { parent::Create(); $this->registriereVerbraucherBasis(); $this->RegisterPropertyInteger('Kontaktart', WaermepumpenRegler::KONTAKTART_SPERRE_ERHOEHUNG); $this->RegisterPropertyInteger('Kontakt1VariableID', 0); $this->RegisterPropertyInteger('Kontakt2VariableID', 0); $this->RegisterPropertyBoolean('Kontakt1Invertiert', false); $this->RegisterPropertyBoolean('Kontakt2Invertiert', false); $this->RegisterPropertyInteger('Nennleistung', 0); $this->RegisterPropertyInteger('Rueckmeldungsart', WaermepumpenRegler::RUECKMELDUNG_LEISTUNG); $this->RegisterPropertyInteger('IstleistungVariableID', 0); $this->RegisterPropertyFloat('Laufschwelle', 100.0); $this->RegisterPropertyInteger('Anlaufwartezeit', 30); $this->RegisterPropertyInteger('Wiederholsperre', 300); $this->RegisterPropertyInteger('Mindestlaufzeit', 1200); $this->RegisterPropertyInteger('Mindestsperrzeit', 1200); $this->RegisterPropertyInteger('MaximaleSperrzeit', 7200); $this->RegisterPropertyInteger('MaximaleSperrzeit24h', 21600); $this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false); $this->RegisterVariableInteger('Waermepumpenzustand', 'Waermepumpenzustand', '', 100); $this->RegisterVariableBoolean('WaermepumpeLaeuft', 'Waermepumpe laeuft', '~Switch', 110); $this->RegisterVariableBoolean('SperreAktiv', 'Sperre aktiv', '~Switch', 120); $this->RegisterVariableBoolean('ErhoehungAktiv', 'Erhoehung aktiv', '~Switch', 130); $this->RegisterVariableInteger('SGReadyZustand', 'SG-Ready-Zustand', '', 140); $this->RegisterVariableInteger('Laufzeit', 'Bestaetigte Laufzeit', '', 150); foreach (['Kontakt1', 'Kontakt2', 'Istleistung'] as $name) { $this->RegisterAttributeInteger('Registrierte' . $name . 'ID', 0); } $this->RegisterAttributeInteger('LetzteManagerID', 0); $this->RegisterAttributeInteger('LetzteVorgabeZeit', 0); $this->RegisterAttributeString('Betriebsart', Nachrichtenvertrag::BETRIEBSART_PV); $this->RegisterAttributeString('Leistungsangebot', '[0]'); $this->RegisterAttributeBoolean('Initialisiert', false); $this->RegisterAttributeBoolean('SchaltvorgangLaeuft', false); $this->RegisterAttributeString('Schaltfehler', ''); $this->RegisterAttributeInteger('Steuerzustand', WaermepumpenRegler::STEUERZUSTAND_NORMAL); $this->RegisterAttributeBoolean('LetzterLaufstatus', false); $this->RegisterAttributeInteger('Laufbeginn', 0); $this->RegisterAttributeInteger('LaufzeitStand', 0); $this->RegisterAttributeInteger('Sperrbeginn', 0); $this->RegisterAttributeString('Sperrhistorie', '[]'); $this->RegisterAttributeBoolean('Sperrerholung', false); $this->RegisterAttributeInteger('ErholungsLaufbeginn', 0); $this->RegisterAttributeInteger('AnlaufDeadline', 0); $this->RegisterAttributeInteger('WiederholsperreBis', 0); $this->RegisterAttributeFloat('ZustandIstleistung', 0.0); $this->RegisterAttributeInteger('ZustandLeistungsquelle', 0); $this->RegisterAttributeInteger('ZustandSollleistung', 0); $this->RegisterAttributeBoolean('ZustandSollwertGueltig', false); $this->RegisterAttributeBoolean('ZustandVerfuegbar', false); $this->RegisterAttributeBoolean('ZustandAenderungMoeglich', false); $this->RegisterAttributeBoolean('ZustandStoerung', false); $this->RegisterAttributeString('ZustandStoertext', ''); $this->RegisterAttributeBoolean('ZustandAnlaufAusstehend', false); $this->RegisterAttributeBoolean('ZustandWiederholsperreAktiv', false); $this->RegisterAttributeBoolean('ZustandSperrerholungAktiv', false); $this->RegisterAttributeInteger('ZustandRestMindestlaufzeit', 0); $this->RegisterAttributeInteger('ZustandRestMindestsperrzeit', 0); $this->RegisterAttributeInteger('ZustandSperrzeit24h', 0); $this->RegisterTimer( 'Meldezyklus', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);" ); $this->RegisterTimer( 'MeldungVerzoegert', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);" ); $this->RegisterTimer( 'Zustandspruefung', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Aktualisieren', 0);" ); $this->RegisterTimer( 'VorgabeTimeout', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', 0);" ); } public function ApplyChanges(): void { parent::ApplyChanges(); $this->aktualisiereVariablen(); $this->bereinigeUngueltigenSollwert(); $this->registriereMeldungen(); try { $this->pruefeKonfiguration(); } catch (Throwable $fehler) { $this->deaktiviereTimer(); $this->setzeZustand('Verfuegbar', false); $this->setzeZustand('AenderungMoeglich', false); $this->setzeStoerung($fehler->getMessage()); $this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG); $this->protokolliere('Konfiguration', $fehler->getMessage()); return; } $this->SetTimerInterval('Meldezyklus', $this->ReadPropertyInteger('Meldeintervall') * 1000); $this->initialisiereZustand(time()); $this->aktualisiere(true); } public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void { if ((int) $nachricht !== self::VM_UPDATE || $this->ReadAttributeBoolean('SchaltvorgangLaeuft')) { return; } $ids = [ $this->ReadPropertyInteger('Kontakt1VariableID'), $this->ReadPropertyInteger('Kontakt2VariableID'), $this->ReadPropertyInteger('IstleistungVariableID'), ]; if (in_array((int) $senderID, $ids, true)) { $this->aktualisiere(true); } } public function RequestAction($ident, $wert): void { switch ($ident) { case 'Aktiv': $this->SetValue('Aktiv', (bool) $wert); if (!(bool) $wert) { $this->verwerfeSollwert(); $this->SetTimerInterval('VorgabeTimeout', 0); } $this->aktualisiere(true); return; case 'Melden': if ((bool) $wert) { $this->SetTimerInterval('MeldungVerzoegert', 0); } $this->aktualisiere(false); $this->sendeVerbraucherdaten(); return; case 'Aktualisieren': $this->SetTimerInterval('Zustandspruefung', 0); $this->aktualisiere(true); return; case 'VorgabeTimeout': $this->SetTimerInterval('VorgabeTimeout', 0); $this->aktualisiere(true); return; case 'ManagerdatenEmpfangen': if (!is_string($wert)) { throw new InvalidArgumentException('Managerdaten muessen als JSON uebergeben werden.'); } $daten = json_decode($wert, true, 512, JSON_THROW_ON_ERROR); if (!is_array($daten)) { throw new InvalidArgumentException('Managerdaten muessen ein JSON-Objekt sein.'); } $this->ManagerdatenEmpfangen($daten); return; } throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident); } /** @param array $daten */ public function ManagerdatenEmpfangen(array $daten): void { Nachrichtenvertrag::pruefeManagerdaten($daten); if ($daten['Kopf']['EmpfaengerID'] !== $this->InstanceID) { throw new InvalidArgumentException('Managerdaten sind an eine andere Instanz adressiert.'); } $managerID = $daten['Kopf']['AbsenderID']; if (!in_array($managerID, $this->zugeordneteManagerIDs(), true)) { throw new InvalidArgumentException('Der Manager hat diesen Verbraucher nicht aktiv zugeordnet.'); } $this->WriteAttributeString('Betriebsart', $daten['Betriebsart']); $this->aktualisiere(false); $sollleistung = $daten['Sollleistung_W']; if ($sollleistung === null) { if ((bool) $this->leseZustand('SollwertGueltig') && !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true) ) { $this->verwerfeSollwert(); } $this->planeMeldung(); return; } if (!in_array($sollleistung, $this->leseLeistungsangebot(), true)) { throw new InvalidArgumentException( 'Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.' ); } $this->WriteAttributeInteger('LetzteManagerID', $managerID); $this->WriteAttributeInteger('LetzteVorgabeZeit', time()); $this->setzeZustand('Sollleistung', $sollleistung); $this->setzeZustand('SollwertGueltig', true); $this->planeVorgabeTimeout(time()); $this->aktualisiere(false); $this->planeMeldung(); } private function aktualisiere(bool $meldungPlanen): void { try { $jetzt = time(); $rueckmeldung = $this->leseRueckmeldung(); $laeuft = WaermepumpenRegler::laeuft( $rueckmeldung, $this->ReadPropertyInteger('Rueckmeldungsart'), $this->ReadPropertyFloat('Laufschwelle') ); $istleistung = WaermepumpenRegler::istleistung( $rueckmeldung, $this->ReadPropertyInteger('Rueckmeldungsart'), $this->ReadPropertyInteger('Nennleistung') ); $this->pruefeKontaktzustand(); $this->aktualisiereLaufzustand($laeuft, $jetzt); $this->pruefeVorgabeTimeout($jetzt); $steuerzustand = $this->ReadAttributeInteger('Steuerzustand'); $this->aktualisiereSperrhistorie($steuerzustand, $jetzt); $sperrzeit24h = $this->sperrsekunden24h($steuerzustand, $jetzt); $maxAmStueckErreicht = $steuerzustand === WaermepumpenRegler::STEUERZUSTAND_SPERRE && $this->ReadPropertyInteger('MaximaleSperrzeit') > 0 && $jetzt - $this->ReadAttributeInteger('Sperrbeginn') >= $this->ReadPropertyInteger('MaximaleSperrzeit'); $max24hErreicht = $this->ReadPropertyInteger('MaximaleSperrzeit24h') > 0 && $sperrzeit24h >= $this->ReadPropertyInteger('MaximaleSperrzeit24h'); if (($maxAmStueckErreicht || $max24hErreicht) && $steuerzustand === WaermepumpenRegler::STEUERZUSTAND_SPERRE ) { $this->WriteAttributeBoolean('Sperrerholung', true); $this->WriteAttributeInteger('ErholungsLaufbeginn', $laeuft ? $jetzt : 0); $this->WriteAttributeInteger('Laufbeginn', $laeuft ? $jetzt : 0); $this->schalte(WaermepumpenRegler::STEUERZUSTAND_NORMAL, $jetzt); $steuerzustand = WaermepumpenRegler::STEUERZUSTAND_NORMAL; $this->verwerfeSollwert(); } $this->aktualisiereSperrerholung($laeuft, $jetzt); $sperrerholung = $this->ReadAttributeBoolean('Sperrerholung'); $restMindestlaufzeit = $this->restMindestlaufzeit($laeuft, $jetzt); $restMindestsperrzeit = $steuerzustand === WaermepumpenRegler::STEUERZUSTAND_SPERRE ? WaermepumpenRegler::restzeit( $this->ReadAttributeInteger('Sperrbeginn'), $this->ReadPropertyInteger('Mindestsperrzeit'), $jetzt ) : 0; $wiederholsperreAktiv = $jetzt < $this->ReadAttributeInteger('WiederholsperreBis'); $startenZulaessig = !$wiederholsperreAktiv && !$sperrerholung && $restMindestsperrzeit === 0; $sperrenZulaessig = !$sperrerholung && $restMindestlaufzeit === 0 && !$max24hErreicht; $aktiv = (bool) $this->GetValue('Aktiv'); $fehlerfrei = $this->ReadAttributeString('Schaltfehler') === ''; $verfuegbar = $aktiv && $fehlerfrei; $angebot = WaermepumpenRegler::leistungsangebot( $this->ReadPropertyInteger('Nennleistung'), $this->ReadAttributeString('Betriebsart'), $laeuft, $verfuegbar, $restMindestlaufzeit > 0, $restMindestsperrzeit > 0, $sperrenZulaessig, $startenZulaessig, $sperrerholung ); $this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR)); if ((bool) $this->leseZustand('SollwertGueltig') && !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true) ) { $this->setzeZustand('Sollleistung', $angebot[0]); } $zielzustand = $this->bestimmeZielzustand( $aktiv, $laeuft, $restMindestlaufzeit, $restMindestsperrzeit, $sperrenZulaessig, $sperrerholung ); if ($zielzustand !== $steuerzustand) { $this->schalte($zielzustand, $jetzt); $steuerzustand = $zielzustand; } $this->aktualisiereAnlauf($laeuft, $steuerzustand, $jetzt); $steuerzustand = $this->ReadAttributeInteger('Steuerzustand'); $sperrerholung = $this->ReadAttributeBoolean('Sperrerholung'); $wiederholsperreAktiv = $jetzt < $this->ReadAttributeInteger('WiederholsperreBis'); $startenZulaessig = !$wiederholsperreAktiv && !$sperrerholung && $restMindestsperrzeit === 0; $sperrenZulaessig = !$sperrerholung && $restMindestlaufzeit === 0 && !$max24hErreicht; $angebot = WaermepumpenRegler::leistungsangebot( $this->ReadPropertyInteger('Nennleistung'), $this->ReadAttributeString('Betriebsart'), $laeuft, $verfuegbar, $restMindestlaufzeit > 0, $restMindestsperrzeit > 0, $sperrenZulaessig, $startenZulaessig, $sperrerholung ); $this->WriteAttributeString( 'Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR) ); $aenderungMoeglich = $verfuegbar && count($angebot) > 1 && !$wiederholsperreAktiv && !$sperrerholung; $this->setzeZustand('Istleistung', $istleistung); $this->setzeZustand( 'Leistungsquelle', $this->ReadPropertyInteger('Rueckmeldungsart') === WaermepumpenRegler::RUECKMELDUNG_LEISTUNG ? Nachrichtenvertrag::LEISTUNGSQUELLE_GEMESSEN : Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET ); $this->setzeZustand('Verfuegbar', $verfuegbar); $this->setzeZustand('AenderungMoeglich', $aenderungMoeglich); $this->setzeZustand('AnlaufAusstehend', $this->ReadAttributeInteger('AnlaufDeadline') > $jetzt); $this->setzeZustand('WiederholsperreAktiv', $wiederholsperreAktiv); $this->setzeZustand('SperrerholungAktiv', $sperrerholung); $this->setzeZustand('RestMindestlaufzeit', $restMindestlaufzeit); $this->setzeZustand('RestMindestsperrzeit', $restMindestsperrzeit); $this->setzeZustand('Sperrzeit24h', $sperrzeit24h); $this->aktualisiereFachvariablen($laeuft, $steuerzustand); $this->aktualisiereStoerung(); $this->planeTimer($jetzt); $this->SetStatus($fehlerfrei ? self::STATUS_AKTIV : self::STATUS_SCHALTFEHLER); if ($meldungPlanen) { $this->planeMeldung(); } } catch (Throwable $fehler) { $this->WriteAttributeString('Schaltfehler', $fehler->getMessage()); $this->setzeZustand('Verfuegbar', false); $this->setzeZustand('AenderungMoeglich', false); $this->setzeStoerung($fehler->getMessage()); $this->SetStatus(self::STATUS_SCHALTFEHLER); $this->protokolliere('Aktualisierung', $fehler->getMessage()); } } private function bestimmeZielzustand( bool $aktiv, bool $laeuft, int $restMindestlaufzeit, int $restMindestsperrzeit, bool $sperrenZulaessig, bool $sperrerholung ): int { $aktuell = $this->ReadAttributeInteger('Steuerzustand'); if (!$aktiv) { return WaermepumpenRegler::STEUERZUSTAND_NORMAL; } if ($aktuell === WaermepumpenRegler::STEUERZUSTAND_SPERRE && $restMindestsperrzeit > 0 ) { return WaermepumpenRegler::STEUERZUSTAND_SPERRE; } if ($sperrerholung) { return WaermepumpenRegler::STEUERZUSTAND_NORMAL; } $pv = $this->ReadAttributeString('Betriebsart') === Nachrichtenvertrag::BETRIEBSART_PV; if (!(bool) $this->leseZustand('SollwertGueltig')) { return WaermepumpenRegler::STEUERZUSTAND_NORMAL; } $sollleistung = (int) $this->leseZustand('Sollleistung'); if ($pv) { return $sollleistung > 0 ? WaermepumpenRegler::STEUERZUSTAND_ERHOEHUNG : WaermepumpenRegler::STEUERZUSTAND_NORMAL; } if ($laeuft && $restMindestlaufzeit > 0) { return WaermepumpenRegler::STEUERZUSTAND_NORMAL; } return $sollleistung === 0 && $sperrenZulaessig ? WaermepumpenRegler::STEUERZUSTAND_SPERRE : WaermepumpenRegler::STEUERZUSTAND_NORMAL; } private function aktualisiereAnlauf(bool $laeuft, int $steuerzustand, int $jetzt): void { if ($laeuft) { $this->WriteAttributeInteger('AnlaufDeadline', 0); $this->WriteAttributeInteger('WiederholsperreBis', 0); return; } $pv = $this->ReadAttributeString('Betriebsart') === Nachrichtenvertrag::BETRIEBSART_PV; $anlaufAngefordert = $pv && $steuerzustand === WaermepumpenRegler::STEUERZUSTAND_ERHOEHUNG && (bool) $this->leseZustand('SollwertGueltig') && (int) $this->leseZustand('Sollleistung') === $this->ReadPropertyInteger('Nennleistung'); if (!$anlaufAngefordert) { $this->WriteAttributeInteger('AnlaufDeadline', 0); return; } $deadline = $this->ReadAttributeInteger('AnlaufDeadline'); if ($deadline <= 0) { $this->WriteAttributeInteger( 'AnlaufDeadline', $jetzt + $this->ReadPropertyInteger('Anlaufwartezeit') ); return; } if ($jetzt < $deadline) { return; } $this->schalte(WaermepumpenRegler::STEUERZUSTAND_NORMAL, $jetzt); $this->WriteAttributeInteger('AnlaufDeadline', 0); $this->WriteAttributeInteger( 'WiederholsperreBis', $jetzt + $this->ReadPropertyInteger('Wiederholsperre') ); $this->setzeZustand('Sollleistung', 0); $this->setzeZustand('SollwertGueltig', true); } private function aktualisiereLaufzustand(bool $laeuft, int $jetzt): void { $stand = $this->ReadAttributeInteger('LaufzeitStand'); $vorher = $this->ReadAttributeBoolean('LetzterLaufstatus'); if ($stand > 0 && $stand <= $jetzt && $vorher) { $this->SetValue('Laufzeit', (int) $this->GetValue('Laufzeit') + $jetzt - $stand); } if ($laeuft !== $vorher) { $this->WriteAttributeInteger('Laufbeginn', $laeuft ? $jetzt : 0); if ($this->ReadAttributeBoolean('Sperrerholung')) { $this->WriteAttributeInteger('ErholungsLaufbeginn', $laeuft ? $jetzt : 0); } } $this->WriteAttributeBoolean('LetzterLaufstatus', $laeuft); $this->WriteAttributeInteger('LaufzeitStand', $jetzt); $this->SetValue('WaermepumpeLaeuft', $laeuft); } private function restMindestlaufzeit(bool $laeuft, int $jetzt): int { if (!$laeuft) { return 0; } $beginn = $this->ReadAttributeBoolean('Sperrerholung') ? $this->ReadAttributeInteger('ErholungsLaufbeginn') : $this->ReadAttributeInteger('Laufbeginn'); return WaermepumpenRegler::restzeit( $beginn, $this->ReadPropertyInteger('Mindestlaufzeit'), $jetzt ); } private function aktualisiereSperrerholung(bool $laeuft, int $jetzt): void { if (!$this->ReadAttributeBoolean('Sperrerholung')) { return; } $beginn = $this->ReadAttributeInteger('ErholungsLaufbeginn'); if (!$laeuft) { if ($beginn > 0) { $this->WriteAttributeInteger('ErholungsLaufbeginn', 0); } return; } if ($beginn <= 0) { $this->WriteAttributeInteger('ErholungsLaufbeginn', $jetzt); return; } if (WaermepumpenRegler::restzeit( $beginn, $this->ReadPropertyInteger('Mindestlaufzeit'), $jetzt ) === 0) { $this->WriteAttributeBoolean('Sperrerholung', false); $this->WriteAttributeInteger('ErholungsLaufbeginn', 0); } } private function schalte(int $steuerzustand, int $jetzt): void { $werte = WaermepumpenRegler::kontaktwerte( $this->ReadPropertyInteger('Kontaktart'), $steuerzustand ); $this->WriteAttributeBoolean('SchaltvorgangLaeuft', true); try { $this->setzeKontakt(1, false); $this->setzeKontakt(2, false); if ($werte['Kontakt1']) { $this->setzeKontakt(1, true); } if ($werte['Kontakt2']) { $this->setzeKontakt(2, true); } } catch (Throwable $fehler) { throw new RuntimeException('Kontakte konnten nicht gesetzt werden: ' . $fehler->getMessage()); } finally { $this->WriteAttributeBoolean('SchaltvorgangLaeuft', false); } if ($this->leseLogischenKontakt(1) !== $werte['Kontakt1'] || $this->leseLogischenKontakt(2) !== $werte['Kontakt2'] ) { throw new RuntimeException('Mindestens ein Kontakt hat den Schaltbefehl nicht uebernommen.'); } $alt = $this->ReadAttributeInteger('Steuerzustand'); if ($alt === WaermepumpenRegler::STEUERZUSTAND_SPERRE && $steuerzustand !== WaermepumpenRegler::STEUERZUSTAND_SPERRE ) { $this->schliesseSperrintervall($jetzt); } if ($alt !== WaermepumpenRegler::STEUERZUSTAND_SPERRE && $steuerzustand === WaermepumpenRegler::STEUERZUSTAND_SPERRE ) { $this->WriteAttributeInteger('Sperrbeginn', $jetzt); } $this->WriteAttributeInteger('Steuerzustand', $steuerzustand); $this->WriteAttributeString('Schaltfehler', ''); $this->protokolliere('Steuerzustand', ['Alt' => $alt, 'Neu' => $steuerzustand]); } private function setzeKontakt(int $nummer, bool $logischerWert): void { $id = $this->ReadPropertyInteger('Kontakt' . $nummer . 'VariableID'); $invertiert = $this->ReadPropertyBoolean('Kontakt' . $nummer . 'Invertiert'); RequestAction($id, $logischerWert !== $invertiert); } private function leseLogischenKontakt(int $nummer): bool { $id = $this->ReadPropertyInteger('Kontakt' . $nummer . 'VariableID'); $invertiert = $this->ReadPropertyBoolean('Kontakt' . $nummer . 'Invertiert'); return (bool) GetValue($id) !== $invertiert; } private function pruefeKontaktzustand(): void { $erwartet = WaermepumpenRegler::kontaktwerte( $this->ReadPropertyInteger('Kontaktart'), $this->ReadAttributeInteger('Steuerzustand') ); if ($this->leseLogischenKontakt(1) !== $erwartet['Kontakt1'] || $this->leseLogischenKontakt(2) !== $erwartet['Kontakt2'] ) { $this->schalte($this->ReadAttributeInteger('Steuerzustand'), time()); return; } $this->WriteAttributeString('Schaltfehler', ''); } private function aktualisiereSperrhistorie(int $steuerzustand, int $jetzt): void { $historie = $this->leseSperrhistorie(); $historie = WaermepumpenRegler::bereinigeSperrhistorie($historie, $jetzt); $this->WriteAttributeString('Sperrhistorie', json_encode($historie, JSON_THROW_ON_ERROR)); if ($steuerzustand === WaermepumpenRegler::STEUERZUSTAND_SPERRE && $this->ReadAttributeInteger('Sperrbeginn') <= 0 ) { $this->WriteAttributeInteger('Sperrbeginn', $jetzt); } } private function schliesseSperrintervall(int $jetzt): void { $beginn = $this->ReadAttributeInteger('Sperrbeginn'); if ($beginn > 0 && $beginn <= $jetzt) { $historie = $this->leseSperrhistorie(); $historie[] = ['Von' => $beginn, 'Bis' => $jetzt]; $historie = WaermepumpenRegler::bereinigeSperrhistorie($historie, $jetzt); $this->WriteAttributeString('Sperrhistorie', json_encode($historie, JSON_THROW_ON_ERROR)); } $this->WriteAttributeInteger('Sperrbeginn', 0); } private function sperrsekunden24h(int $steuerzustand, int $jetzt): int { return WaermepumpenRegler::sperrsekunden24h( $this->leseSperrhistorie(), $jetzt, $steuerzustand === WaermepumpenRegler::STEUERZUSTAND_SPERRE ? $this->ReadAttributeInteger('Sperrbeginn') : null ); } /** @return list */ private function leseSperrhistorie(): array { $historie = json_decode( $this->ReadAttributeString('Sperrhistorie'), true, 512, JSON_THROW_ON_ERROR ); return is_array($historie) ? array_values($historie) : []; } private function aktualisiereFachvariablen(bool $laeuft, int $steuerzustand): void { $sperre = $steuerzustand === WaermepumpenRegler::STEUERZUSTAND_SPERRE; $erhoehung = $steuerzustand === WaermepumpenRegler::STEUERZUSTAND_ERHOEHUNG; $zustand = $sperre ? 4 : ($erhoehung ? 3 : ($laeuft ? 2 : 1)); $this->SetValue('Waermepumpenzustand', $zustand); $this->SetValue('SperreAktiv', $sperre); $this->SetValue('ErhoehungAktiv', $erhoehung); $this->SetValue( 'SGReadyZustand', ($this->leseLogischenKontakt(1) ? 1 : 0) + ($this->leseLogischenKontakt(2) ? 2 : 0) ); } private function initialisiereZustand(int $jetzt): void { if (!$this->ReadAttributeBoolean('Initialisiert')) { $kontakt1 = $this->leseLogischenKontakt(1); $kontakt2 = $this->leseLogischenKontakt(2); if (!$kontakt1 && !$kontakt2) { $zustand = WaermepumpenRegler::STEUERZUSTAND_NORMAL; } elseif ($kontakt1 && !$kontakt2) { $zustand = WaermepumpenRegler::STEUERZUSTAND_SPERRE; } elseif ($kontakt2 && !$kontakt1) { $zustand = WaermepumpenRegler::STEUERZUSTAND_ERHOEHUNG; } else { $zustand = WaermepumpenRegler::STEUERZUSTAND_NORMAL; } $this->WriteAttributeInteger('Steuerzustand', $zustand); $this->WriteAttributeInteger( 'Sperrbeginn', $zustand === WaermepumpenRegler::STEUERZUSTAND_SPERRE ? $jetzt : 0 ); $laeuft = WaermepumpenRegler::laeuft( $this->leseRueckmeldung(), $this->ReadPropertyInteger('Rueckmeldungsart'), $this->ReadPropertyFloat('Laufschwelle') ); $this->WriteAttributeBoolean('LetzterLaufstatus', $laeuft); $this->WriteAttributeInteger('Laufbeginn', $laeuft ? $jetzt : 0); $this->WriteAttributeInteger('LaufzeitStand', $jetzt); $this->WriteAttributeBoolean('Initialisiert', true); } } /** @return bool|int|float */ private function leseRueckmeldung() { return GetValue($this->ReadPropertyInteger('IstleistungVariableID')); } private function pruefeKonfiguration(): void { foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) { if ($this->ReadPropertyInteger($property) < 0) { throw new InvalidArgumentException($property . ' muss mindestens 0 sein.'); } } foreach (['Meldeintervall', 'VorgabeTimeout'] as $property) { if ($this->ReadPropertyInteger($property) <= 0) { throw new InvalidArgumentException($property . ' muss groesser als 0 sein.'); } } if ($this->ReadPropertyInteger('Nennleistung') <= 0) { throw new InvalidArgumentException('Nennleistung muss groesser als 0 sein.'); } if ($this->ReadPropertyFloat('Laufschwelle') < 0) { throw new InvalidArgumentException('Laufschwelle darf nicht negativ sein.'); } $kontaktart = $this->ReadPropertyInteger('Kontaktart'); WaermepumpenRegler::kontaktwerte( $kontaktart, WaermepumpenRegler::STEUERZUSTAND_NORMAL ); $rueckmeldungsart = $this->ReadPropertyInteger('Rueckmeldungsart'); WaermepumpenRegler::laeuft(false, $rueckmeldungsart, 0.0); $kontakt1 = $this->ReadPropertyInteger('Kontakt1VariableID'); $kontakt2 = $this->ReadPropertyInteger('Kontakt2VariableID'); $this->pruefeVariable($kontakt1, 'Kontakt 1', [0], true); $this->pruefeVariable($kontakt2, 'Kontakt 2', [0], true); if ($kontakt1 === $kontakt2) { throw new InvalidArgumentException('Kontakt 1 und Kontakt 2 muessen verschieden sein.'); } $rueckmeldung = $this->ReadPropertyInteger('IstleistungVariableID'); $typen = $rueckmeldungsart === WaermepumpenRegler::RUECKMELDUNG_BETRIEB ? [0] : [1, 2]; $this->pruefeVariable($rueckmeldung, 'Rueckmeldung', $typen, false); foreach ([ 'Anlaufwartezeit', 'Wiederholsperre', 'Mindestlaufzeit', 'Mindestsperrzeit', 'MaximaleSperrzeit', 'MaximaleSperrzeit24h', ] as $property) { if ($this->ReadPropertyInteger($property) < 0) { throw new InvalidArgumentException($property . ' darf nicht negativ sein.'); } } if ($this->ReadPropertyInteger('MaximaleSperrzeit24h') > 86400) { throw new InvalidArgumentException('MaximaleSperrzeit24h darf hoechstens 86400 Sekunden betragen.'); } } /** @param list $typen */ private function pruefeVariable( int $variableID, string $bezeichnung, array $typen, bool $aktionErforderlich ): void { if ($variableID <= 0 || !IPS_VariableExists($variableID)) { throw new InvalidArgumentException($bezeichnung . ' ist nicht eingerichtet.'); } $variable = IPS_GetVariable($variableID); if (!in_array((int) $variable['VariableType'], $typen, true)) { throw new InvalidArgumentException($bezeichnung . ' hat einen ungueltigen Variablentyp.'); } if ($aktionErforderlich && (int) $variable['VariableAction'] <= 0 && (int) $variable['VariableCustomAction'] <= 0 ) { throw new InvalidArgumentException( $bezeichnung . ' benoetigt eine Standard- oder benutzerdefinierte Aktion.' ); } } private function registriereMeldungen(): void { $definitionen = [ 'Kontakt1' => $this->ReadPropertyInteger('Kontakt1VariableID'), 'Kontakt2' => $this->ReadPropertyInteger('Kontakt2VariableID'), 'Istleistung' => $this->ReadPropertyInteger('IstleistungVariableID'), ]; foreach ($definitionen as $name => $neueID) { $attribut = 'Registrierte' . $name . 'ID'; $alteID = $this->ReadAttributeInteger($attribut); if ($alteID > 0 && $alteID !== $neueID) { $this->UnregisterMessage($alteID, self::VM_UPDATE); } if ($neueID > 0 && IPS_VariableExists($neueID)) { $this->RegisterMessage($neueID, self::VM_UPDATE); } $this->WriteAttributeInteger($attribut, $neueID); } } private function pruefeVorgabeTimeout(int $jetzt): void { if (!(bool) $this->leseZustand('SollwertGueltig')) { return; } $ablauf = $this->ReadAttributeInteger('LetzteVorgabeZeit') + $this->ReadPropertyInteger('VorgabeTimeout'); if ($this->ReadAttributeInteger('LetzteVorgabeZeit') <= 0 || $jetzt >= $ablauf) { $this->verwerfeSollwert(); $this->SetTimerInterval('VorgabeTimeout', 0); } } private function planeVorgabeTimeout(int $jetzt): void { if (!(bool) $this->leseZustand('SollwertGueltig')) { $this->SetTimerInterval('VorgabeTimeout', 0); return; } $ablauf = $this->ReadAttributeInteger('LetzteVorgabeZeit') + $this->ReadPropertyInteger('VorgabeTimeout'); $this->SetTimerInterval('VorgabeTimeout', max(1, ($ablauf - $jetzt) * 1000)); } private function planeTimer(int $jetzt): void { $zeitpunkte = []; foreach (['AnlaufDeadline', 'WiederholsperreBis'] as $attribut) { $zeitpunkt = $this->ReadAttributeInteger($attribut); if ($zeitpunkt > $jetzt) { $zeitpunkte[] = $zeitpunkt; } } $laufbeginn = $this->ReadAttributeInteger('Laufbeginn'); if ($this->ReadAttributeBoolean('LetzterLaufstatus') && $laufbeginn > 0) { $zeitpunkte[] = $laufbeginn + $this->ReadPropertyInteger('Mindestlaufzeit'); } if ($this->ReadAttributeInteger('Steuerzustand') === WaermepumpenRegler::STEUERZUSTAND_SPERRE ) { $sperrbeginn = $this->ReadAttributeInteger('Sperrbeginn'); if ($sperrbeginn > 0) { $zeitpunkte[] = $sperrbeginn + $this->ReadPropertyInteger('Mindestsperrzeit'); if ($this->ReadPropertyInteger('MaximaleSperrzeit') > 0) { $zeitpunkte[] = $sperrbeginn + $this->ReadPropertyInteger('MaximaleSperrzeit'); } $maximum24h = $this->ReadPropertyInteger('MaximaleSperrzeit24h'); if ($maximum24h > 0) { $rest24h = $maximum24h - $this->sperrsekunden24h( WaermepumpenRegler::STEUERZUSTAND_SPERRE, $jetzt ); if ($rest24h > 0) { $zeitpunkte[] = $jetzt + $rest24h; } } } } $zeitpunkte = array_values(array_filter( $zeitpunkte, static fn (int $zeitpunkt): bool => $zeitpunkt > $jetzt )); $this->SetTimerInterval( 'Zustandspruefung', $zeitpunkte === [] ? 0 : max(1, (min($zeitpunkte) - $jetzt) * 1000) ); $this->planeVorgabeTimeout($jetzt); } private function planeMeldung(): void { $this->SetTimerInterval('MeldungVerzoegert', 100); } private function aktualisiereStoerung(): void { $text = $this->ReadAttributeString('Schaltfehler'); $this->setzeStoerung($text); } private function setzeStoerung(string $text): void { $this->setzeZustand('Stoerung', $text !== ''); $this->setzeZustand('Stoertext', $text); } private function sendeVerbraucherdaten(): void { foreach ($this->zugeordneteManagerIDs() as $managerID) { $daten = $this->baueVerbraucherdaten($managerID); Nachrichtenvertrag::pruefeVerbraucherdaten($daten); try { IPS_RequestAction( $managerID, 'VerbraucherdatenEmpfangen', json_encode($daten, JSON_THROW_ON_ERROR) ); } catch (Throwable $fehler) { $this->protokolliere('Managerkommunikation', $fehler->getMessage()); } } } /** @return array */ private function baueVerbraucherdaten(int $managerID): array { return [ 'Kopf' => [ 'Version' => Nachrichtenvertrag::VERSION, 'AbsenderID' => $this->InstanceID, 'EmpfaengerID' => $managerID, 'Zeitpunkt' => time(), ], 'Betriebsart' => $this->ReadAttributeString('Betriebsart'), 'PrioritaetPV' => $this->ReadPropertyInteger('PrioritaetPV'), 'PrioritaetPeak' => $this->ReadPropertyInteger('PrioritaetPeak'), 'Leistungswerte_W' => $this->leseLeistungsangebot(), 'AenderungMoeglich' => (bool) $this->leseZustand('AenderungMoeglich'), 'Verfuegbar' => (bool) $this->leseZustand('Verfuegbar'), 'Istleistung_W' => (float) $this->leseZustand('Istleistung'), 'Leistungsquelle' => (int) $this->leseZustand('Leistungsquelle'), 'Zustand' => $this->zustandsdaten(), ]; } /** @return list> */ private function zustandsdaten(): array { return [ [ 'Kennung' => 'Sollleistung_W', 'Art' => 'Sollwert', 'Wert' => (bool) $this->leseZustand('SollwertGueltig') ? (int) $this->leseZustand('Sollleistung') : null, 'Einheit' => 'W', ], [ 'Kennung' => 'WaermepumpeLaeuft', 'Art' => 'Status', 'Wert' => (bool) $this->GetValue('WaermepumpeLaeuft'), 'Einheit' => '', ], [ 'Kennung' => 'Steuerzustand', 'Art' => 'Status', 'Wert' => $this->ReadAttributeInteger('Steuerzustand'), 'Einheit' => '', ], [ 'Kennung' => 'RestMindestlaufzeit_s', 'Art' => 'Istwert', 'Wert' => (int) $this->leseZustand('RestMindestlaufzeit'), 'Einheit' => 's', ], [ 'Kennung' => 'RestMindestsperrzeit_s', 'Art' => 'Istwert', 'Wert' => (int) $this->leseZustand('RestMindestsperrzeit'), 'Einheit' => 's', ], [ 'Kennung' => 'Sperrzeit24h_s', 'Art' => 'Istwert', 'Wert' => (int) $this->leseZustand('Sperrzeit24h'), 'Einheit' => 's', ], [ 'Kennung' => 'SperrerholungAktiv', 'Art' => 'Status', 'Wert' => (bool) $this->leseZustand('SperrerholungAktiv'), 'Einheit' => '', ], [ 'Kennung' => 'Schaltfehler', 'Art' => 'Stoerung', 'Wert' => (bool) $this->leseZustand('Stoerung'), 'Einheit' => '', 'Text' => (string) $this->leseZustand('Stoertext'), ], ]; } /** @return list */ private function zugeordneteManagerIDs(): array { $ergebnis = []; foreach (IPS_GetInstanceListByModuleID(self::MANAGER_MODULE_ID) as $managerID) { try { $automatisch = (bool) IPS_GetProperty($managerID, 'AutomatischeSuche'); $property = $automatisch ? 'AutomatischeVerbraucherZuordnung' : 'VerbraucherZuordnung'; $zuordnung = $this->leseManagerZuordnung($managerID, $property); if ($automatisch && $zuordnung === []) { $zuordnung = $this->leseManagerZuordnung($managerID, 'VerbraucherZuordnung'); } } catch (Throwable $fehler) { continue; } foreach ($zuordnung as $eintrag) { if (!is_array($eintrag) || ($eintrag['Aktiv'] ?? false) !== true) { continue; } $instanzID = $eintrag['InstanzID'] ?? $eintrag['Verbraucher'] ?? null; if ($instanzID === $this->InstanceID) { $ergebnis[] = (int) $managerID; break; } } } return array_values(array_unique($ergebnis)); } /** @return array */ private function leseManagerZuordnung(int $managerID, string $property): array { $zuordnung = json_decode( (string) IPS_GetProperty($managerID, $property), true, 512, JSON_THROW_ON_ERROR ); return is_array($zuordnung) ? $zuordnung : []; } /** @return list */ private function leseLeistungsangebot(): array { $angebot = json_decode( $this->ReadAttributeString('Leistungsangebot'), true, 512, JSON_THROW_ON_ERROR ); return is_array($angebot) ? array_values(array_map('intval', $angebot)) : []; } private function aktualisiereVariablen(): void { foreach (self::DIAGNOSE_VARIABLEN as $ident) { $variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) { $this->setzeZustand($ident, GetValue($variablenID)); } } if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) { $this->registriereVerbraucherDiagnose(); $this->RegisterVariableBoolean('AnlaufAusstehend', 'Anlauf ausstehend', '~Switch', 200); $this->RegisterVariableBoolean( 'WiederholsperreAktiv', 'Wiederholsperre aktiv', '~Switch', 210 ); $this->RegisterVariableBoolean( 'SperrerholungAktiv', 'Sperrerholung aktiv', '~Switch', 220 ); $this->RegisterVariableInteger( 'RestMindestlaufzeit', 'Restliche Mindestlaufzeit', '', 230 ); $this->RegisterVariableInteger( 'RestMindestsperrzeit', 'Restliche Mindestsperrzeit', '', 240 ); $this->RegisterVariableInteger('Sperrzeit24h', 'Sperrzeit letzte 24 h', '', 250); foreach (self::DIAGNOSE_VARIABLEN as $ident) { $this->setzeZustand($ident, $this->leseZustand($ident)); } return; } foreach (self::DIAGNOSE_VARIABLEN as $ident) { $variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) { $this->UnregisterVariable($ident); } } } /** @param bool|int|float|string $wert */ private function setzeZustand(string $ident, $wert): void { $booleanAttribute = [ 'SollwertGueltig' => 'ZustandSollwertGueltig', 'Verfuegbar' => 'ZustandVerfuegbar', 'AenderungMoeglich' => 'ZustandAenderungMoeglich', 'Stoerung' => 'ZustandStoerung', 'AnlaufAusstehend' => 'ZustandAnlaufAusstehend', 'WiederholsperreAktiv' => 'ZustandWiederholsperreAktiv', 'SperrerholungAktiv' => 'ZustandSperrerholungAktiv', ]; $integerAttribute = [ 'Leistungsquelle' => 'ZustandLeistungsquelle', 'Sollleistung' => 'ZustandSollleistung', 'RestMindestlaufzeit' => 'ZustandRestMindestlaufzeit', 'RestMindestsperrzeit' => 'ZustandRestMindestsperrzeit', 'Sperrzeit24h' => 'ZustandSperrzeit24h', ]; 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 === 'Istleistung') { $wert = (float) $wert; $this->WriteAttributeFloat('ZustandIstleistung', $wert); } elseif ($ident === 'Stoertext') { $wert = (string) $wert; $this->WriteAttributeString('ZustandStoertext', $wert); } else { 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|float|string */ private function leseZustand(string $ident) { $attribute = [ 'Istleistung' => ['Float', 'ZustandIstleistung'], 'Leistungsquelle' => ['Integer', 'ZustandLeistungsquelle'], 'Sollleistung' => ['Integer', 'ZustandSollleistung'], 'SollwertGueltig' => ['Boolean', 'ZustandSollwertGueltig'], 'Verfuegbar' => ['Boolean', 'ZustandVerfuegbar'], 'AenderungMoeglich' => ['Boolean', 'ZustandAenderungMoeglich'], 'Stoerung' => ['Boolean', 'ZustandStoerung'], 'Stoertext' => ['String', 'ZustandStoertext'], 'AnlaufAusstehend' => ['Boolean', 'ZustandAnlaufAusstehend'], 'WiederholsperreAktiv' => ['Boolean', 'ZustandWiederholsperreAktiv'], 'SperrerholungAktiv' => ['Boolean', 'ZustandSperrerholungAktiv'], 'RestMindestlaufzeit' => ['Integer', 'ZustandRestMindestlaufzeit'], 'RestMindestsperrzeit' => ['Integer', 'ZustandRestMindestsperrzeit'], 'Sperrzeit24h' => ['Integer', 'ZustandSperrzeit24h'], ]; if (!isset($attribute[$ident])) { throw new LogicException('Unbekannter interner Zustand: ' . $ident); } [$typ, $name] = $attribute[$ident]; $methode = 'ReadAttribute' . $typ; return $this->{$methode}($name); } private function deaktiviereTimer(): void { foreach ([ 'Meldezyklus', 'MeldungVerzoegert', 'Zustandspruefung', 'VorgabeTimeout', ] as $timer) { $this->SetTimerInterval($timer, 0); } } /** @param mixed $daten */ private function protokolliere(string $bezeichnung, $daten): void { if (!$this->ReadPropertyBoolean('LoggingEin')) { return; } $this->SendDebug( $bezeichnung, is_string($daten) ? $daten : json_encode($daten, JSON_THROW_ON_ERROR), 0 ); } }