*/ private const DIAGNOSE_VARIABLEN = [ 'Istleistung', 'Leistungsquelle', 'Sollleistung', 'SollwertGueltig', 'Verfuegbar', 'AenderungMoeglich', 'Stoerung', 'Stoertext', 'AktiveStufe', 'BezogeneEnergie', 'TemperaturGueltig', 'NachladenAktiv', 'LegionellenbetriebAktiv', 'LegioCounter', ]; public function Create(): void { parent::Create(); $this->registriereVerbraucherBasis(); // Aus Boiler_x_Stufig uebernommene und sicher ergaenzte Konfiguration. $this->RegisterPropertyString('LeistungsStufen', '[]'); $this->RegisterPropertyInteger('ZeitKonstante', 120); $this->RegisterPropertyInteger('Boilerfuehler_PT1', 0); $this->RegisterPropertyBoolean('BoilertemperaturGlaetten', false); $this->RegisterPropertyInteger('Boilervolumen', 300); $this->RegisterPropertyString('Zeitplan', '[]'); $this->RegisterPropertyInteger('LastwechselSperrzeit', 5); $this->RegisterPropertyInteger('TemperaturMaxAlter', 30); $this->RegisterPropertyFloat('Hysterese', 5.0); $this->RegisterPropertyBoolean('LegionellenfunktionAktiv', true); $this->RegisterPropertyInteger('LegionellenMinimalintervallTage', 4); $this->RegisterPropertyInteger('LegionellenMaximalintervallTage', 7); $this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false); $this->RegisterVariableFloat('Boilertemperatur', 'Boilertemperatur', '~Temperature', 100); $this->RegisterVariableFloat('Mindesttemperatur', 'Mindesttemperatur', '~Temperature', 130); $this->RegisterVariableFloat('Maximaltemperatur', 'Maximaltemperatur', '~Temperature', 140); $this->EnableAction('Mindesttemperatur'); $this->EnableAction('Maximaltemperatur'); $this->RegisterAttributeBoolean('InitialwerteGesetzt', false); $this->RegisterAttributeInteger('RegistrierteFuehlerID', 0); $this->RegisterAttributeInteger('LetzteBerechnung', 0); $this->RegisterAttributeInteger('LetzteTemperaturberechnung', 0); $this->RegisterAttributeInteger('LetzteVorgabeZeit', 0); $this->RegisterAttributeInteger('LetzterLastwechsel', 0); $this->RegisterAttributeInteger('LetzteManagerID', 0); $this->RegisterAttributeInteger('LetzterLegionellenabschluss', 0); $this->RegisterAttributeFloat('Glaettungswert', 0.0); $this->RegisterAttributeBoolean('GlaettungInitialisiert', false); $this->RegisterAttributeString('Leistungsangebot', '[]'); $this->RegisterAttributeString('Betriebsart', Nachrichtenvertrag::BETRIEBSART_PV); $this->RegisterAttributeString('Schaltfehler', ''); $this->RegisterAttributeFloat('ZustandIstleistung', 0.0); $this->RegisterAttributeInteger('ZustandSollleistung', 0); $this->RegisterAttributeBoolean('ZustandStoerung', false); $this->RegisterAttributeString('ZustandStoertext', ''); $this->RegisterAttributeInteger('ZustandAktiveStufe', 0); $this->RegisterAttributeFloat('ZustandBezogeneEnergie', 0.0); $this->RegisterAttributeFloat('ZustandLegionellentemperatur', 65.0); $this->RegisterAttributeBoolean('ZustandSollwertGueltig', false); $this->RegisterAttributeBoolean('ZustandVerfuegbar', false); $this->RegisterAttributeBoolean('ZustandAenderungMoeglich', false); $this->RegisterAttributeBoolean('ZustandTemperaturGueltig', false); $this->RegisterAttributeBoolean('ZustandNachladenAktiv', false); $this->RegisterAttributeBoolean('MaximaltemperaturErreicht', false); $this->RegisterAttributeBoolean('ZustandLegionellenbetriebAktiv', false); $this->RegisterAttributeInteger('ZustandLegioCounter', 0); $this->RegisterTimer( 'LastwechselFreigabe', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'LastwechselFreigabe', false);" ); $this->RegisterTimer( 'Meldezyklus', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);" ); $this->RegisterTimer( 'RueckmeldungVerzoegert', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);" ); } public function ApplyChanges(): void { parent::ApplyChanges(); $this->initialisiereTemperaturen(); $this->aktualisiereVariablen(); try { $this->pruefeKonfiguration(); } catch (Throwable $fehler) { $this->SetTimerInterval('LastwechselFreigabe', 0); $this->SetTimerInterval('Meldezyklus', 0); $this->SetTimerInterval('RueckmeldungVerzoegert', 0); $this->setzeZustand('Verfuegbar', false); $this->setzeZustand('AenderungMoeglich', false); $this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG); $this->protokolliere('Konfiguration', $fehler->getMessage()); return; } $this->registriereFuehlermeldung(); $this->SetTimerInterval('LastwechselFreigabe', 0); $this->SetTimerInterval('Meldezyklus', $this->ReadPropertyInteger('Meldeintervall') * 1000); if ($this->ReadAttributeInteger('LetzterLegionellenabschluss') <= 0) { $this->WriteAttributeInteger('LetzterLegionellenabschluss', time()); } $this->regelzyklus(true); } public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void { if ((int) $senderID === $this->ReadPropertyInteger('Boilerfuehler_PT1') && (int) $nachricht === self::VM_UPDATE ) { $this->regelzyklus(true); } } public function RequestAction($ident, $wert): void { switch ($ident) { case 'Aktiv': $this->SetValue('Aktiv', (bool) $wert); $this->regelzyklus(true); return; case 'Mindesttemperatur': case 'Maximaltemperatur': case 'Legionellentemperatur': $this->setzeTemperaturSollwert((string) $ident, $wert); return; case 'LastwechselFreigabe': $this->SetTimerInterval('LastwechselFreigabe', 0); $this->regelzyklus(true); return; case 'Melden': if ((bool) $wert) { $this->SetTimerInterval('RueckmeldungVerzoegert', 0); } $this->regelzyklus(false); $this->sendeVerbraucherdaten(); 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->regelzyklus(false); $angebot = $this->leseLeistungsangebot(); $sollleistung = $daten['Sollleistung_W']; if ($sollleistung === null) { if ((bool) $this->leseZustand('SollwertGueltig') && !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true) ) { $this->setzeZustand('SollwertGueltig', false); $this->regelzyklus(false); } $this->SetTimerInterval('RueckmeldungVerzoegert', 100); return; } if (!in_array($sollleistung, $angebot, 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->regelzyklus(false); // Verhindert eine synchrone Manager-Verbraucher-Endlosschleife. $this->SetTimerInterval('RueckmeldungVerzoegert', 100); } private function regelzyklus(bool $meldungPlanen): void { try { $jetzt = time(); $this->aktualisiereEnergie($jetzt); $temperaturGueltig = $this->aktualisiereTemperatur(); $this->aktualisiereLegionellenstatus($temperaturGueltig, $jetzt); $restzeit = $this->aktualisiereLastwechselstatus($jetzt); [$angebot, $aenderungMoeglich, $verfuegbar, $zwangsleistung] = $this->bestimmeLeistungsangebot($temperaturGueltig, $jetzt); $aktuelleLeistung = (int) round((float) $this->leseZustand('Istleistung')); $lastwechselGesperrt = $restzeit > 0 && $verfuegbar && $zwangsleistung !== 0; if ($lastwechselGesperrt) { $angebot = [$aktuelleLeistung]; $aenderungMoeglich = false; } $this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR)); $this->setzeZustand('Verfuegbar', $verfuegbar); $this->setzeZustand('AenderungMoeglich', $aenderungMoeglich); $letzteVorgabe = $this->ReadAttributeInteger('LetzteVorgabeZeit'); if ($letzteVorgabe <= 0 || $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout') ) { $this->setzeZustand('SollwertGueltig', false); } $ziel = 0; if ($lastwechselGesperrt) { $ziel = $aktuelleLeistung; } elseif ($verfuegbar) { if ($zwangsleistung !== null) { $ziel = $zwangsleistung; } elseif ((bool) $this->leseZustand('SollwertGueltig')) { $ziel = (int) $this->leseZustand('Sollleistung'); if (!in_array($ziel, $angebot, true)) { $this->setzeZustand('SollwertGueltig', false); $ziel = 0; } } } $lastwechselErfolgt = $this->schalteLeistung($ziel, $jetzt); if ($lastwechselErfolgt) { $this->WriteAttributeString('Leistungsangebot', json_encode([$ziel], JSON_THROW_ON_ERROR)); $this->setzeZustand('AenderungMoeglich', false); } $this->setzeZustand('Sollleistung', $ziel); $this->aktualisiereStoerungen($temperaturGueltig); if ($meldungPlanen) { $this->SetTimerInterval('RueckmeldungVerzoegert', 100); } } catch (Throwable $fehler) { $this->WriteAttributeString('Schaltfehler', $fehler->getMessage()); $this->setzeZustand('Verfuegbar', false); $this->setzeZustand('AenderungMoeglich', false); $this->setzeZustand('Stoerung', true); $this->setzeZustand('Stoertext', $fehler->getMessage()); $this->SetStatus(self::STATUS_SCHALTFEHLER); $this->protokolliere('Regelzyklus', $fehler->getMessage()); } } private function aktualisiereTemperatur(): bool { $fuehlerID = $this->ReadPropertyInteger('Boilerfuehler_PT1'); if ($fuehlerID <= 0 || !IPS_VariableExists($fuehlerID)) { $this->setzeZustand('TemperaturGueltig', false); return false; } $variable = IPS_GetVariable($fuehlerID); if (time() - (int) $variable['VariableUpdated'] > $this->ReadPropertyInteger('TemperaturMaxAlter')) { $this->setzeZustand('TemperaturGueltig', false); return false; } $rohwert = GetValue($fuehlerID); if (!is_int($rohwert) && !is_float($rohwert)) { $this->setzeZustand('TemperaturGueltig', false); return false; } $temperatur = (float) $rohwert; $jetzt = time(); $letzteTemperaturberechnung = $this->ReadAttributeInteger('LetzteTemperaturberechnung'); $this->WriteAttributeInteger('LetzteTemperaturberechnung', $jetzt); if ($this->ReadPropertyBoolean('BoilertemperaturGlaetten')) { if (!$this->ReadAttributeBoolean('GlaettungInitialisiert')) { $this->WriteAttributeFloat('Glaettungswert', $temperatur); $this->WriteAttributeBoolean('GlaettungInitialisiert', true); } elseif ($letzteTemperaturberechnung > 0 && $jetzt > $letzteTemperaturberechnung) { $temperatur = WarmwasserRegler::pt1( $this->ReadAttributeFloat('Glaettungswert'), $temperatur, $jetzt - $letzteTemperaturberechnung, $this->ReadPropertyInteger('ZeitKonstante') ); $this->WriteAttributeFloat('Glaettungswert', $temperatur); } else { $temperatur = $this->ReadAttributeFloat('Glaettungswert'); } } else { $this->WriteAttributeBoolean('GlaettungInitialisiert', false); } $this->SetValue('Boilertemperatur', $temperatur); $this->setzeZustand('TemperaturGueltig', true); return true; } /** * @return array{0: list, 1: bool, 2: bool, 3: int|null} */ private function bestimmeLeistungsangebot(bool $temperaturGueltig, int $jetzt): array { $stufen = WarmwasserRegler::dekodiereLeistungsstufen($this->ReadPropertyString('LeistungsStufen')); if (!(bool) $this->GetValue('Aktiv') || !$temperaturGueltig || $stufen === []) { $this->setzeZustand('NachladenAktiv', false); return [[], false, false, 0]; } $alleLeistungen = WarmwasserRegler::leistungswerte($stufen); $maximalleistung = max($alleLeistungen); $temperatur = (float) $this->GetValue('Boilertemperatur'); $minimum = (float) $this->GetValue('Mindesttemperatur'); $maximum = (float) $this->GetValue('Maximaltemperatur'); [$minimum, $maximum] = WarmwasserRegler::legionellenTemperaturgrenzen( $this->ReadPropertyBoolean('LegionellenfunktionAktiv'), max(0, $jetzt - $this->ReadAttributeInteger('LetzterLegionellenabschluss')), $this->ReadPropertyInteger('LegionellenMinimalintervallTage'), $this->ReadPropertyInteger('LegionellenMaximalintervallTage'), $minimum, $maximum, (float) $this->ReadAttributeFloat('ZustandLegionellentemperatur') ); $hysterese = $this->ReadPropertyFloat('Hysterese'); $naechster = WarmwasserRegler::naechsterZeitplaneintrag( $this->ReadPropertyString('Zeitplan'), new DateTimeImmutable('now') ); if ($naechster !== null && $temperatur < $naechster['Solltemperatur'] && !WarmwasserRegler::kannZielErreichen( (float) $this->ReadPropertyInteger('Boilervolumen'), $temperatur, $naechster['Solltemperatur'], max(0, $naechster['Zeitpunkt'] - $jetzt), $maximalleistung ) ) { $minimum = $naechster['Solltemperatur']; } $stufeAktiv = (float) $this->leseZustand('Istleistung') > 0.0; $nachladen = $temperatur < $minimum || ($temperatur < $minimum + $hysterese && $stufeAktiv); $this->setzeZustand('NachladenAktiv', $nachladen); $peakbetrieb = $this->ReadAttributeString('Betriebsart') === Nachrichtenvertrag::BETRIEBSART_PEAK; $angebot = WarmwasserRegler::leistungsangebot( $peakbetrieb, $temperatur, $minimum, $maximum, $hysterese, $stufeAktiv, $alleLeistungen ); if ($angebot === [0]) { return [$angebot, false, true, 0]; } if ($angebot === [$maximalleistung]) { return [$angebot, false, true, $maximalleistung]; } return [$angebot, true, true, null]; } private function aktualisiereLegionellenstatus(bool $temperaturGueltig, int $jetzt): void { $aktiviert = $this->ReadPropertyBoolean('LegionellenfunktionAktiv'); $letzterAbschluss = $this->ReadAttributeInteger('LetzterLegionellenabschluss'); $sekundenSeitAbschluss = max(0, $jetzt - $letzterAbschluss); if ($aktiviert && $temperaturGueltig && (float) $this->GetValue('Boilertemperatur') >= $this->ReadAttributeFloat('ZustandLegionellentemperatur') ) { $this->WriteAttributeInteger('LetzterLegionellenabschluss', $jetzt); $sekundenSeitAbschluss = 0; } $fruehestensFaellig = $aktiviert && $sekundenSeitAbschluss >= $this->ReadPropertyInteger('LegionellenMinimalintervallTage') * 86400; $this->setzeZustand('LegioCounter', $sekundenSeitAbschluss); $this->setzeZustand('LegionellenbetriebAktiv', $fruehestensFaellig); } private function schalteLeistung(int $zielLeistung, int $jetzt): bool { $stufen = WarmwasserRegler::dekodiereLeistungsstufen($this->ReadPropertyString('LeistungsStufen')); $zielStufe = WarmwasserRegler::stufeFuerLeistung($stufen, $zielLeistung); $bisherigeLeistung = (int) round((float) $this->leseZustand('Istleistung')); if ($zielLeistung === $bisherigeLeistung && $this->kontakteEntsprechenZiel($stufen, $zielLeistung)) { $this->WriteAttributeString('Schaltfehler', ''); return false; } try { // Break-before-make: zuerst alle Kontakte aus, danach genau eine Stufe ein. foreach ($stufen as $stufe) { RequestAction($stufe['KontaktID'], false); } if ($zielLeistung > 0) { foreach ($stufen as $stufe) { if ($stufe['Leistung_W'] === $zielLeistung) { RequestAction($stufe['KontaktID'], true); break; } } } } catch (Throwable $fehler) { foreach ($stufen as $stufe) { try { RequestAction($stufe['KontaktID'], false); } catch (Throwable $ignoriert) { } } $this->setzeZustand('Istleistung', 0.0); $this->setzeZustand('AktiveStufe', 0); $this->WriteAttributeInteger('LetzterLastwechsel', $jetzt); $this->aktualisiereLastwechselstatus($jetzt); throw new RuntimeException('Leistungsstufe konnte nicht sicher geschaltet werden: ' . $fehler->getMessage()); } $this->WriteAttributeString('Schaltfehler', ''); $this->setzeZustand('Istleistung', (float) $zielLeistung); $this->setzeZustand('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET); $this->setzeZustand('AktiveStufe', $zielStufe); $this->WriteAttributeInteger('LetzterLastwechsel', $jetzt); $this->aktualisiereLastwechselstatus($jetzt); $this->protokolliere('Leistungsstufe', ['Leistung_W' => $zielLeistung, 'Stufe' => $zielStufe]); return true; } /** * @param list $stufen */ private function kontakteEntsprechenZiel(array $stufen, int $zielLeistung): bool { foreach ($stufen as $stufe) { if (!IPS_VariableExists($stufe['KontaktID'])) { return false; } $sollEin = $zielLeistung > 0 && $stufe['Leistung_W'] === $zielLeistung; if ((bool) GetValue($stufe['KontaktID']) !== $sollEin) { return false; } } return true; } private function aktualisiereLastwechselstatus(int $jetzt): int { $restzeit = WarmwasserRegler::restzeitBisLastwechsel( $this->ReadAttributeInteger('LetzterLastwechsel'), $jetzt, $this->ReadPropertyInteger('LastwechselSperrzeit') ); $this->SetTimerInterval('LastwechselFreigabe', $restzeit > 0 ? $restzeit * 1000 : 0); return $restzeit; } private function aktualisiereEnergie(int $jetzt): void { $letzteBerechnung = $this->ReadAttributeInteger('LetzteBerechnung'); $this->WriteAttributeInteger('LetzteBerechnung', $jetzt); if ($letzteBerechnung <= 0 || $jetzt <= $letzteBerechnung) { return; } $zuwachs = WarmwasserRegler::energieKWh( (float) $this->leseZustand('Istleistung'), $jetzt - $letzteBerechnung ); $this->setzeZustand('BezogeneEnergie', (float) $this->leseZustand('BezogeneEnergie') + $zuwachs); } private function aktualisiereStoerungen(bool $temperaturGueltig): void { $stoerungen = []; if (!$temperaturGueltig) { $stoerungen[] = 'Boilertemperatur fehlt oder ist veraltet.'; } $uebertemperatur = $this->istUebertemperatur($temperaturGueltig); if ($uebertemperatur) { $stoerungen[] = 'Uebertemperatur erkannt.'; } if ($this->ReadAttributeString('Schaltfehler') !== '') { $stoerungen[] = $this->ReadAttributeString('Schaltfehler'); } $this->setzeZustand('Stoerung', $stoerungen !== []); $this->setzeZustand('Stoertext', implode("\n", array_values(array_unique($stoerungen)))); if ($this->ReadAttributeString('Schaltfehler') !== '') { $this->SetStatus(self::STATUS_SCHALTFEHLER); } elseif (!$temperaturGueltig) { $this->SetStatus(self::STATUS_TEMPERATUR_UNGUELTIG); } else { $this->SetStatus(self::STATUS_AKTIV); } } private function istUebertemperatur(bool $temperaturGueltig): bool { if (!$temperaturGueltig) { return false; } $grenze = (float) $this->GetValue('Maximaltemperatur'); if ($this->ReadPropertyBoolean('LegionellenfunktionAktiv')) { $grenze = max($grenze, $this->ReadAttributeFloat('ZustandLegionellentemperatur')); } return (float) $this->GetValue('Boilertemperatur') > $grenze + $this->ReadPropertyFloat('Hysterese'); } 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 { $temperaturGueltig = (bool) $this->leseZustand('TemperaturGueltig'); $schaltfehler = $this->ReadAttributeString('Schaltfehler'); $uebertemperatur = $this->istUebertemperatur($temperaturGueltig); 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' => Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET, 'Zustand' => [ [ 'Kennung' => 'Sollleistung_W', 'Art' => 'Sollwert', 'Wert' => (bool) $this->leseZustand('SollwertGueltig') ? (int) $this->leseZustand('Sollleistung') : null, 'Einheit' => 'W', ], [ 'Kennung' => 'Wassertemperatur_C', 'Art' => 'Istwert', 'Wert' => $temperaturGueltig ? (float) $this->GetValue('Boilertemperatur') : null, 'Einheit' => 'C', ], [ 'Kennung' => 'Maximaltemperatur_C', 'Art' => 'Sollwert', 'Wert' => (float) $this->GetValue('Maximaltemperatur'), 'Einheit' => 'C', ], [ 'Kennung' => 'AktiveStufe', 'Art' => 'Status', 'Wert' => (int) $this->leseZustand('AktiveStufe'), 'Einheit' => '', ], [ 'Kennung' => 'NachladenAktiv', 'Art' => 'Status', 'Wert' => (bool) $this->leseZustand('NachladenAktiv'), 'Einheit' => '', ], [ 'Kennung' => 'LegionellenbetriebAktiv', 'Art' => 'Status', 'Wert' => (bool) $this->leseZustand('LegionellenbetriebAktiv'), 'Einheit' => '', ], [ 'Kennung' => 'Fuehlerfehler', 'Art' => 'Stoerung', 'Wert' => !$temperaturGueltig, 'Einheit' => '', 'Text' => $temperaturGueltig ? '' : 'Boilertemperatur fehlt oder ist veraltet.', ], [ 'Kennung' => 'Uebertemperatur', 'Art' => 'Stoerung', 'Wert' => $uebertemperatur, 'Einheit' => '', 'Text' => $uebertemperatur ? 'Uebertemperatur erkannt.' : '', ], [ 'Kennung' => 'Schaltfehler', 'Art' => 'Stoerung', 'Wert' => $schaltfehler !== '', 'Einheit' => '', 'Text' => $schaltfehler, ], ], ]; } /** @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 : []; } 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', 'LastwechselSperrzeit', 'TemperaturMaxAlter', 'ZeitKonstante', 'Boilervolumen', 'LegionellenMinimalintervallTage', 'LegionellenMaximalintervallTage', ] as $property) { if ($this->ReadPropertyInteger($property) <= 0) { throw new InvalidArgumentException($property . ' muss groesser als 0 sein.'); } } if ($this->ReadPropertyInteger('LegionellenMinimalintervallTage') > $this->ReadPropertyInteger('LegionellenMaximalintervallTage') ) { throw new InvalidArgumentException( 'Das Legionellen-Minimalintervall darf nicht groesser als das Maximalintervall sein.' ); } if ($this->ReadPropertyFloat('Hysterese') <= 0) { throw new InvalidArgumentException('Hysterese muss groesser als 0 sein.'); } $minimum = (float) $this->GetValue('Mindesttemperatur'); $maximum = (float) $this->GetValue('Maximaltemperatur'); $legionellen = (float) $this->ReadAttributeFloat('ZustandLegionellentemperatur'); if ($minimum < 0 || $minimum >= $maximum || $legionellen < $maximum) { throw new InvalidArgumentException( 'Temperaturen muessen Mindesttemperatur < Maximaltemperatur <= Legionellentemperatur erfuellen.' ); } $fuehlerID = $this->ReadPropertyInteger('Boilerfuehler_PT1'); if ($fuehlerID <= 0 || !IPS_VariableExists($fuehlerID)) { throw new InvalidArgumentException('Boilerfuehler_PT1 ist nicht eingerichtet.'); } $fuehler = IPS_GetVariable($fuehlerID); if (!in_array((int) $fuehler['VariableType'], [1, 2], true)) { throw new InvalidArgumentException('Boilerfuehler_PT1 muss eine Integer- oder Floatvariable sein.'); } $stufen = WarmwasserRegler::dekodiereLeistungsstufen($this->ReadPropertyString('LeistungsStufen')); if ($stufen === []) { throw new InvalidArgumentException('Mindestens eine Leistungsstufe ist erforderlich.'); } foreach ($stufen as $stufe) { if (!IPS_VariableExists($stufe['KontaktID'])) { throw new InvalidArgumentException('Schaltkontakt ' . $stufe['KontaktID'] . ' existiert nicht.'); } $kontakt = IPS_GetVariable($stufe['KontaktID']); if ((int) $kontakt['VariableType'] !== 0) { throw new InvalidArgumentException('Alle Schaltkontakte muessen Booleanvariablen sein.'); } } WarmwasserRegler::naechsterZeitplaneintrag( $this->ReadPropertyString('Zeitplan'), new DateTimeImmutable('now') ); } private function registriereFuehlermeldung(): void { $alt = $this->ReadAttributeInteger('RegistrierteFuehlerID'); $neu = $this->ReadPropertyInteger('Boilerfuehler_PT1'); if ($alt > 0 && $alt !== $neu) { $this->UnregisterMessage($alt, self::VM_UPDATE); } if ($neu > 0 && $neu !== $alt) { $this->RegisterMessage($neu, self::VM_UPDATE); } $this->WriteAttributeInteger('RegistrierteFuehlerID', $neu); } private function initialisiereTemperaturen(): void { if ($this->ReadAttributeBoolean('InitialwerteGesetzt')) { return; } $this->SetValue('Mindesttemperatur', 45.0); $this->SetValue('Maximaltemperatur', 60.0); $this->WriteAttributeFloat('ZustandLegionellentemperatur', 65.0); $this->setzeZustand('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET); $this->WriteAttributeBoolean('InitialwerteGesetzt', true); } /** @param mixed $wert */ private function setzeTemperaturSollwert(string $ident, $wert): void { if (!$this->ReadPropertyBoolean('EinstellungenInVisu')) { throw new RuntimeException('Temperatureinstellungen sind in der Visualisierung nicht freigegeben.'); } if ($ident === 'Legionellentemperatur' && !$this->ReadPropertyBoolean('LegionellenfunktionAktiv') ) { throw new RuntimeException('Die Legionellenfunktion ist nicht aktiviert.'); } if (!is_int($wert) && !is_float($wert)) { throw new InvalidArgumentException('Temperatursollwert muss numerisch sein.'); } $temperatur = (float) $wert; if ($temperatur < 0 || $temperatur > 100) { throw new InvalidArgumentException('Temperatursollwert muss zwischen 0 und 100 Grad C liegen.'); } $werte = [ 'Mindesttemperatur' => (float) $this->GetValue('Mindesttemperatur'), 'Maximaltemperatur' => (float) $this->GetValue('Maximaltemperatur'), 'Legionellentemperatur' => $this->ReadAttributeFloat('ZustandLegionellentemperatur'), ]; $werte[$ident] = $temperatur; if ($werte['Mindesttemperatur'] >= $werte['Maximaltemperatur'] || $werte['Maximaltemperatur'] > $werte['Legionellentemperatur'] ) { throw new InvalidArgumentException( 'Temperaturen muessen Mindesttemperatur < Maximaltemperatur <= Legionellentemperatur erfuellen.' ); } if ($ident === 'Legionellentemperatur') { $this->WriteAttributeFloat('ZustandLegionellentemperatur', $temperatur); $variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) { SetValue($variablenID, $temperatur); } } else { $this->SetValue($ident, $temperatur); } $this->regelzyklus(true); } private function aktualisiereVariablen(): void { foreach (['Idle', 'IdleCounter'] as $ident) { $variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) { $this->UnregisterVariable($ident); } } foreach (self::DIAGNOSE_VARIABLEN as $ident) { if ($ident === 'Leistungsquelle') { continue; } $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->RegisterVariableInteger('AktiveStufe', 'Aktive Stufe', '', 110); $this->RegisterVariableFloat('BezogeneEnergie', 'Bezogene Energie', '~Electricity', 120); $this->RegisterVariableBoolean('TemperaturGueltig', 'Temperatur gueltig', '~Switch', 200); $this->RegisterVariableBoolean('NachladenAktiv', 'Nachladen aktiv', '~Switch', 210); $this->RegisterVariableBoolean( 'LegionellenbetriebAktiv', 'Legionellenbetrieb aktiv', '~Switch', 220 ); $this->RegisterVariableInteger('LegioCounter', 'Zeit seit Legionellenabschluss', '', 230); foreach (self::DIAGNOSE_VARIABLEN as $ident) { $wert = $ident === 'Leistungsquelle' ? Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET : $this->leseZustand($ident); $this->setzeZustand($ident, $wert); } } else { foreach (self::DIAGNOSE_VARIABLEN as $ident) { $variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) { $this->UnregisterVariable($ident); } } } $einstellungen = $this->ReadPropertyBoolean('EinstellungenInVisu'); foreach (['Mindesttemperatur', 'Maximaltemperatur'] as $ident) { IPS_SetHidden($this->GetIDForIdent($ident), !$einstellungen); if ($einstellungen) { $this->EnableAction($ident); } else { $this->DisableAction($ident); } } $legionellenID = @IPS_GetObjectIDByIdent('Legionellentemperatur', $this->InstanceID); if (is_int($legionellenID) && $legionellenID > 0 && IPS_VariableExists($legionellenID)) { $this->WriteAttributeFloat('ZustandLegionellentemperatur', (float) GetValue($legionellenID)); } if ($this->ReadPropertyBoolean('LegionellenfunktionAktiv')) { $this->RegisterVariableFloat('Legionellentemperatur', 'Legionellentemperatur', '~Temperature', 150); $legionellenID = $this->GetIDForIdent('Legionellentemperatur'); SetValue($legionellenID, $this->ReadAttributeFloat('ZustandLegionellentemperatur')); IPS_SetHidden($legionellenID, !$einstellungen); if ($einstellungen) { $this->EnableAction('Legionellentemperatur'); } else { $this->DisableAction('Legionellentemperatur'); } } elseif (is_int($legionellenID) && $legionellenID > 0 && IPS_VariableExists($legionellenID)) { $this->UnregisterVariable('Legionellentemperatur'); } } /** @param bool|int|float|string $wert */ private function setzeZustand(string $ident, $wert): void { $booleanAttribute = [ 'SollwertGueltig' => 'ZustandSollwertGueltig', 'Verfuegbar' => 'ZustandVerfuegbar', 'AenderungMoeglich' => 'ZustandAenderungMoeglich', 'Stoerung' => 'ZustandStoerung', 'TemperaturGueltig' => 'ZustandTemperaturGueltig', 'NachladenAktiv' => 'ZustandNachladenAktiv', 'LegionellenbetriebAktiv' => 'ZustandLegionellenbetriebAktiv', ]; $integerAttribute = [ 'Sollleistung' => 'ZustandSollleistung', 'AktiveStufe' => 'ZustandAktiveStufe', 'LegioCounter' => 'ZustandLegioCounter', ]; $floatAttribute = [ 'Istleistung' => 'ZustandIstleistung', 'BezogeneEnergie' => 'ZustandBezogeneEnergie', ]; $stringAttribute = [ 'Stoertext' => 'ZustandStoertext', ]; 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 (isset($floatAttribute[$ident])) { $wert = (float) $wert; $this->WriteAttributeFloat($floatAttribute[$ident], $wert); } elseif (isset($stringAttribute[$ident])) { $wert = (string) $wert; $this->WriteAttributeString($stringAttribute[$ident], $wert); } elseif ($ident !== 'Leistungsquelle') { throw new LogicException('Unbekannter interner Zustand: ' . $ident); } $variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) { SetValue($variablenID, $wert); } } /** @return bool|int|float|string */ private function leseZustand(string $ident) { $booleanAttribute = [ 'SollwertGueltig' => 'ZustandSollwertGueltig', 'Verfuegbar' => 'ZustandVerfuegbar', 'AenderungMoeglich' => 'ZustandAenderungMoeglich', 'Stoerung' => 'ZustandStoerung', 'TemperaturGueltig' => 'ZustandTemperaturGueltig', 'NachladenAktiv' => 'ZustandNachladenAktiv', 'LegionellenbetriebAktiv' => 'ZustandLegionellenbetriebAktiv', ]; $integerAttribute = [ 'Sollleistung' => 'ZustandSollleistung', 'AktiveStufe' => 'ZustandAktiveStufe', 'LegioCounter' => 'ZustandLegioCounter', ]; $floatAttribute = [ 'Istleistung' => 'ZustandIstleistung', 'BezogeneEnergie' => 'ZustandBezogeneEnergie', ]; $stringAttribute = [ 'Stoertext' => 'ZustandStoertext', ]; if (isset($booleanAttribute[$ident])) { return $this->ReadAttributeBoolean($booleanAttribute[$ident]); } if (isset($integerAttribute[$ident])) { return $this->ReadAttributeInteger($integerAttribute[$ident]); } if (isset($floatAttribute[$ident])) { return $this->ReadAttributeFloat($floatAttribute[$ident]); } if (isset($stringAttribute[$ident])) { return $this->ReadAttributeString($stringAttribute[$ident]); } throw new LogicException('Unbekannter interner Zustand: ' . $ident); } /** @return list */ private function leseLeistungsangebot(): array { $angebot = json_decode($this->ReadAttributeString('Leistungsangebot'), true, 512, JSON_THROW_ON_ERROR); if (!is_array($angebot)) { return []; } return array_values(array_map('intval', $angebot)); } /** @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 ); } }