*/ private const DIAGNOSE_VARIABLEN = [ 'Istleistung', 'Leistungsquelle', 'Sollleistung', 'SollwertGueltig', 'Verfuegbar', 'AenderungMoeglich', 'Stoerung', 'Stoertext', 'Rueckmeldefehler', 'SchaltbefehlAusstehend', 'RestMindestzeit', ]; public function Create(): void { parent::Create(); $this->registriereVerbraucherBasis(); $this->RegisterPropertyInteger('Nennleistung', 0); $this->RegisterPropertyInteger('SchaltkontaktVariableID', 0); $this->RegisterPropertyBoolean('SchaltkontaktInvertiert', false); $this->RegisterPropertyInteger('RueckmeldungVariableID', 0); $this->RegisterPropertyInteger('Mindestlaufzeit', 0); $this->RegisterPropertyInteger('Mindesteinschaltdauer', 5); $this->RegisterPropertyInteger('Mindestausschaltdauer', 5); $this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false); $this->RegisterVariableBoolean('Schaltzustand', 'Schaltzustand', '~Switch', 100); $this->RegisterVariableInteger('Tageslaufzeit', 'Tageslaufzeit', '', 110); $this->RegisterAttributeInteger('RegistrierterSchaltkontakt', 0); $this->RegisterAttributeInteger('RegistrierteRueckmeldung', 0); $this->RegisterAttributeInteger('LetzteVorgabeZeit', 0); $this->RegisterAttributeInteger('LetzteManagerID', 0); $this->RegisterAttributeInteger('BestaetigtSeit', 0); $this->RegisterAttributeInteger('FreigabeAb', 0); $this->RegisterAttributeInteger('LaufzeitStandZeit', 0); $this->RegisterAttributeString('LaufzeitTag', ''); $this->RegisterAttributeString('Leistungsangebot', '[0]'); $this->RegisterAttributeBoolean('Initialisiert', false); $this->RegisterAttributeBoolean('SchaltvorgangLaeuft', false); $this->RegisterAttributeBoolean('SchaltpruefungAusstehend', false); $this->RegisterAttributeBoolean('Schaltziel', false); $this->RegisterAttributeString('Schaltfehler', ''); $this->RegisterAttributeFloat('ZustandIstleistung', 0.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('ZustandSchaltzustand', false); $this->RegisterAttributeInteger('ZustandTageslaufzeit', 0); $this->RegisterAttributeBoolean('ZustandRueckmeldefehler', false); $this->RegisterAttributeBoolean('ZustandSchaltbefehlAusstehend', false); $this->RegisterAttributeInteger('ZustandRestMindestzeit', 0); $this->RegisterTimer( 'Meldezyklus', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);" ); $this->RegisterTimer( 'RueckmeldungVerzoegert', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);" ); // Bestehende Instanzen behalten den alten Timer deaktiviert. $this->RegisterTimer('Umschaltfreigabe', 0, ''); $this->RegisterTimer( 'MindestzeitAbgelaufen', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'MindestzeitAbgelaufen', 0);" ); $this->RegisterTimer( 'VorgabeTimeout', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', 0);" ); $this->RegisterTimer( 'Tagesplanung', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Tagesplanung', 0);" ); } public function ApplyChanges(): void { parent::ApplyChanges(); $this->aktualisiereVariablen(); $this->registriereMeldungen(); try { $this->pruefeKonfiguration(); } catch (Throwable $fehler) { $this->deaktiviereTimer(); $this->setzeZustand('Verfuegbar', false); $this->setzeZustand('AenderungMoeglich', false); $this->setzeStoerung($fehler->getMessage(), false); $this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG); $this->protokolliere('Konfiguration', $fehler->getMessage()); return; } $this->SetTimerInterval('Meldezyklus', $this->ReadPropertyInteger('Meldeintervall') * 1000); $this->initialisiereZustand(); $this->aktualisiere(true); } public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void { if ((int) $nachricht !== self::VM_UPDATE || $this->ReadAttributeBoolean('SchaltvorgangLaeuft')) { return; } $relevanteIDs = array_filter([ $this->ReadPropertyInteger('SchaltkontaktVariableID'), $this->ReadPropertyInteger('RueckmeldungVariableID'), ]); if (in_array((int) $senderID, $relevanteIDs, true)) { $this->aktualisiere(true); } } public function RequestAction($ident, $wert): void { switch ($ident) { case 'Aktiv': $this->SetValue('Aktiv', (bool) $wert); if (!(bool) $wert) { $this->setzeZustand('SollwertGueltig', false); $this->SetTimerInterval('VorgabeTimeout', 0); } $this->aktualisiere(true); return; case 'Melden': if ((bool) $wert) { $this->SetTimerInterval('RueckmeldungVerzoegert', 0); } $this->aktualisiere(false); $this->sendeVerbraucherdaten(); return; case 'MindestzeitAbgelaufen': case 'VorgabeTimeout': case 'Tagesplanung': $this->SetTimerInterval((string) $ident, 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->aktualisiere(false); $sollleistung = $daten['Sollleistung_W']; 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(); $this->aktualisiereTageslaufzeit($jetzt); $vorherigerSchaltzustand = (bool) $this->leseZustand('Schaltzustand'); $schaltzustand = $this->leseSchaltzustand(); if ($schaltzustand !== $vorherigerSchaltzustand) { $this->bestaetigeZustandswechsel($schaltzustand, $jetzt); } $this->pruefeAusstehendenSchaltvorgang($schaltzustand, $jetzt); $this->pruefeVorgabeTimeout($jetzt); if ((bool) $this->leseZustand('SollwertGueltig') && !in_array( (int) $this->leseZustand('Sollleistung'), [0, $this->ReadPropertyInteger('Nennleistung')], true ) ) { $this->setzeZustand('SollwertGueltig', false); } $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') === '' && !$rueckmeldefehler; $verfuegbar = $aktiv && $fehlerfrei; $mindestlaufzeitErzwungen = $verfuegbar && $this->mussMindestlaufzeitErzwingen($jetzt); $restMindestzeit = $wartetAufRueckmeldung ? 0 : EinStufigRegler::restMindestzeit( $this->ReadAttributeInteger('FreigabeAb'), $jetzt ); $ziel = 0; if ($verfuegbar) { if ($mindestlaufzeitErzwungen) { $ziel = $this->ReadPropertyInteger('Nennleistung'); } elseif ((bool) $this->leseZustand('SollwertGueltig')) { $ziel = (int) $this->leseZustand('Sollleistung'); } } $istleistung = $this->leistungFuerSchaltzustand($schaltzustand); $zielzustand = $ziel > 0; $befehlNoetig = $ziel !== $istleistung || ($wartetAufRueckmeldung && $zielzustand !== $this->ReadAttributeBoolean('Schaltziel')); // Die lokale Deaktivierung bleibt der einzige bewusste Mindestzeit-Override. $sicheresAusschalten = !$zielzustand && !$aktiv; if ($befehlNoetig && ($sicheresAusschalten || ($restMindestzeit === 0 && !$wartetAufRueckmeldung && $fehlerfrei)) ) { $this->schalte($zielzustand, $jetzt); $schaltzustand = $this->leseSchaltzustand(); $this->pruefeAusstehendenSchaltvorgang($schaltzustand, $jetzt); $istleistung = $this->leistungFuerSchaltzustand($schaltzustand); $wartetAufRueckmeldung = $this->ReadAttributeBoolean('SchaltpruefungAusstehend'); $restMindestzeit = $wartetAufRueckmeldung ? 0 : EinStufigRegler::restMindestzeit( $this->ReadAttributeInteger('FreigabeAb'), $jetzt ); } $this->setzeZustand('Sollleistung', $ziel); $this->setzeZustand('Verfuegbar', $verfuegbar); $this->setzeZustand('SchaltbefehlAusstehend', $wartetAufRueckmeldung); $this->setzeZustand('RestMindestzeit', $restMindestzeit); $aenderungMoeglich = $verfuegbar && !$mindestlaufzeitErzwungen && $restMindestzeit === 0 && !$wartetAufRueckmeldung; $this->setzeZustand('AenderungMoeglich', $aenderungMoeglich); $angebot = EinStufigRegler::leistungsangebot( $this->ReadPropertyInteger('Nennleistung'), $istleistung, $verfuegbar, $aenderungMoeglich, $mindestlaufzeitErzwungen ); $this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR)); $this->aktualisiereStoerung(); $this->planeTimer($jetzt); $this->SetStatus( $this->ReadAttributeString('Schaltfehler') === '' && !(bool) $this->leseZustand('Rueckmeldefehler') ? 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(), false); $this->SetStatus(self::STATUS_SCHALTFEHLER); $this->protokolliere('Aktualisierung', $fehler->getMessage()); } } private function schalte(bool $einschalten, int $jetzt): void { $kontaktID = $this->ReadPropertyInteger('SchaltkontaktVariableID'); $aktorwert = $einschalten !== $this->ReadPropertyBoolean('SchaltkontaktInvertiert'); $this->WriteAttributeBoolean('Schaltziel', $einschalten); $this->WriteAttributeBoolean('SchaltpruefungAusstehend', true); $this->setzeZustand('SchaltbefehlAusstehend', true); $this->WriteAttributeBoolean('SchaltvorgangLaeuft', true); try { RequestAction($kontaktID, $aktorwert); } catch (Throwable $fehler) { $this->WriteAttributeBoolean('SchaltpruefungAusstehend', false); $this->setzeZustand('SchaltbefehlAusstehend', false); throw new RuntimeException('Schaltkontakt konnte nicht gesetzt werden: ' . $fehler->getMessage()); } finally { $this->WriteAttributeBoolean('SchaltvorgangLaeuft', false); } $istzustand = $this->leseSchaltzustand(); if ($istzustand === $einschalten) { $this->bestaetigeZustandswechsel($istzustand, $jetzt); } elseif ($this->ReadPropertyInteger('RueckmeldungVariableID') <= 0) { $this->WriteAttributeBoolean('SchaltpruefungAusstehend', false); $this->setzeZustand('SchaltbefehlAusstehend', false); throw new RuntimeException('Schaltkontakt hat den angeforderten Zustand nicht uebernommen.'); } else { // Die physische Rueckmeldung bestimmt Zustand und Beginn der Mindestzeit. $this->setzeZustand('Rueckmeldefehler', false); } $this->WriteAttributeString('Schaltfehler', ''); $this->protokolliere('Schaltbefehl', ['Eingeschaltet' => $einschalten]); } private function pruefeAusstehendenSchaltvorgang(bool $schaltzustand, int $jetzt): void { if ($this->ReadAttributeBoolean('SchaltpruefungAusstehend') && $schaltzustand === $this->ReadAttributeBoolean('Schaltziel') ) { $this->bestaetigeZustandswechsel($schaltzustand, $jetzt); } $this->setzeZustand( 'SchaltbefehlAusstehend', $this->ReadAttributeBoolean('SchaltpruefungAusstehend') ); } private function bestaetigeZustandswechsel(bool $schaltzustand, int $jetzt): void { $vorherigerZustand = (bool) $this->leseZustand('Schaltzustand'); $zustandGeaendert = $schaltzustand !== $vorherigerZustand || $this->ReadAttributeInteger('BestaetigtSeit') <= 0; $this->setzeSchaltzustand($schaltzustand); if ($zustandGeaendert) { $this->WriteAttributeInteger('BestaetigtSeit', $jetzt); $this->WriteAttributeInteger( 'FreigabeAb', EinStufigRegler::freigabezeitpunkt( $schaltzustand, $jetzt, $this->ReadPropertyInteger('Mindesteinschaltdauer'), $this->ReadPropertyInteger('Mindestausschaltdauer') ) ); } if ($schaltzustand === $this->ReadAttributeBoolean('Schaltziel')) { $this->WriteAttributeBoolean('SchaltpruefungAusstehend', false); } $this->setzeZustand( 'SchaltbefehlAusstehend', $this->ReadAttributeBoolean('SchaltpruefungAusstehend') ); $this->setzeZustand('Rueckmeldefehler', false); $this->protokolliere('Schaltzustand bestaetigt', ['Eingeschaltet' => $schaltzustand]); } private function aktualisiereTageslaufzeit(int $jetzt): void { $werte = EinStufigRegler::aktualisiereTageslaufzeit( $this->ReadAttributeString('LaufzeitTag'), (int) $this->leseZustand('Tageslaufzeit'), $this->ReadAttributeInteger('LaufzeitStandZeit'), $jetzt, (bool) $this->leseZustand('Schaltzustand'), $this->zeitzone() ); $this->WriteAttributeString('LaufzeitTag', $werte['Tag']); $this->WriteAttributeInteger('LaufzeitStandZeit', $werte['StandZeit']); $this->setzeZustand('Tageslaufzeit', $werte['Sekunden']); } private function mussMindestlaufzeitErzwingen(int $jetzt): bool { return EinStufigRegler::mussMindestlaufzeitErzwingen( (int) $this->leseZustand('Tageslaufzeit'), $this->ReadPropertyInteger('Mindestlaufzeit'), EinStufigRegler::sekundenBisTagesende($jetzt, $this->zeitzone()) ); } private function pruefeVorgabeTimeout(int $jetzt): void { if (!(bool) $this->leseZustand('SollwertGueltig')) { return; } $letzteVorgabe = $this->ReadAttributeInteger('LetzteVorgabeZeit'); if ($letzteVorgabe <= 0 || $jetzt - $letzteVorgabe >= $this->ReadPropertyInteger('VorgabeTimeout') ) { $this->setzeZustand('SollwertGueltig', false); $this->SetTimerInterval('VorgabeTimeout', 0); } } private function planeTimer(int $jetzt): void { $this->SetTimerInterval('Umschaltfreigabe', 0); $freigabeAb = $this->ReadAttributeInteger('FreigabeAb'); $wartetAufRueckmeldung = $this->ReadAttributeBoolean('SchaltpruefungAusstehend'); $this->SetTimerInterval( 'MindestzeitAbgelaufen', !$wartetAufRueckmeldung && $freigabeAb > $jetzt ? max(1, ($freigabeAb - $jetzt) * 1000) : 0 ); $this->planeVorgabeTimeout($jetzt); $bisTagesende = EinStufigRegler::sekundenBisTagesende($jetzt, $this->zeitzone()); $naechstePruefung = EinStufigRegler::sekundenBisNaechsteTagespruefung( (int) $this->leseZustand('Tageslaufzeit'), $this->ReadPropertyInteger('Mindestlaufzeit'), $bisTagesende, (bool) $this->leseZustand('Schaltzustand') ); $this->SetTimerInterval('Tagesplanung', $naechstePruefung * 1000); } 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 planeMeldung(): void { $this->SetTimerInterval('RueckmeldungVerzoegert', 100); } private function initialisiereZustand(): void { $jetzt = time(); $schaltzustand = $this->leseSchaltzustand(); if (!$this->ReadAttributeBoolean('Initialisiert')) { $this->WriteAttributeString('LaufzeitTag', date('Y-m-d', $jetzt)); $this->WriteAttributeInteger('LaufzeitStandZeit', $jetzt); $this->setzeZustand('Tageslaufzeit', 0); $this->setzeZustand('SollwertGueltig', false); $this->setzeZustand('Rueckmeldefehler', false); $this->WriteAttributeBoolean('Initialisiert', true); } 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 { $this->setzeZustand('Schaltzustand', $eingeschaltet); $this->setzeZustand('Istleistung', (float) $this->leistungFuerSchaltzustand($eingeschaltet)); $this->setzeZustand('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET); } private function leseSchaltzustand(): bool { $rueckmeldungID = $this->ReadPropertyInteger('RueckmeldungVariableID'); if ($rueckmeldungID > 0) { return (bool) GetValue($rueckmeldungID); } return $this->leseAktorSchaltzustand(); } private function leseAktorSchaltzustand(): bool { $kontaktwert = (bool) GetValue($this->ReadPropertyInteger('SchaltkontaktVariableID')); return $kontaktwert !== $this->ReadPropertyBoolean('SchaltkontaktInvertiert'); } private function leistungFuerSchaltzustand(bool $eingeschaltet): int { return $eingeschaltet ? $this->ReadPropertyInteger('Nennleistung') : 0; } private function aktualisiereStoerung(): void { $texte = []; if ($this->ReadAttributeString('Schaltfehler') !== '') { $texte[] = $this->ReadAttributeString('Schaltfehler'); } if ((bool) $this->leseZustand('Rueckmeldefehler')) { $texte[] = 'Schaltzustands-Rueckmeldung entspricht nicht dem angeforderten Zustand.'; } $this->setzeStoerung(implode("\n", $texte), $texte === []); } private function setzeStoerung(string $text, bool $fehlerfrei): void { $this->setzeZustand('Stoerung', !$fehlerfrei); $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(), ], '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' => 'Schaltzustand', 'Art' => 'Status', 'Wert' => (bool) $this->leseZustand('Schaltzustand'), 'Einheit' => '', ], [ 'Kennung' => 'SchaltbefehlAusstehend', 'Art' => 'Status', 'Wert' => (bool) $this->leseZustand('SchaltbefehlAusstehend'), 'Einheit' => '', ], [ 'Kennung' => 'Schaltbereit', 'Art' => 'Status', 'Wert' => (bool) $this->leseZustand('AenderungMoeglich'), 'Einheit' => '', ], [ 'Kennung' => 'RestMindestzeit_s', 'Art' => 'Istwert', 'Wert' => (int) $this->leseZustand('RestMindestzeit'), 'Einheit' => 's', ], [ 'Kennung' => 'Tageslaufzeit_s', 'Art' => 'Istwert', 'Wert' => (int) $this->leseZustand('Tageslaufzeit'), 'Einheit' => 's', ], [ 'Kennung' => 'Rueckmeldefehler', 'Art' => 'Stoerung', 'Wert' => (bool) $this->leseZustand('Rueckmeldefehler'), 'Einheit' => '', 'Text' => (bool) $this->leseZustand('Rueckmeldefehler') ? 'Schaltzustands-Rueckmeldung entspricht nicht dem angeforderten Zustand.' : '', ], ], ]; } /** @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'] 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->ReadPropertyInteger('Mindestlaufzeit') < 0 || $this->ReadPropertyInteger('Mindestlaufzeit') > 86400 ) { throw new InvalidArgumentException('Mindestlaufzeit muss zwischen 0 und 86400 Sekunden liegen.'); } foreach (['Mindesteinschaltdauer', 'Mindestausschaltdauer'] as $property) { if ($this->ReadPropertyInteger($property) < 0) { throw new InvalidArgumentException($property . ' darf nicht negativ sein.'); } } $kontaktID = $this->ReadPropertyInteger('SchaltkontaktVariableID'); $this->pruefeBooleanVariable($kontaktID, 'Schaltkontakt', true); $rueckmeldungID = $this->ReadPropertyInteger('RueckmeldungVariableID'); if ($rueckmeldungID > 0) { $this->pruefeBooleanVariable($rueckmeldungID, 'Rueckmeldung', false); } } private function pruefeBooleanVariable(int $variableID, string $bezeichnung, bool $aktionErforderlich): void { if ($variableID <= 0 || !IPS_VariableExists($variableID)) { throw new InvalidArgumentException($bezeichnung . ' ist nicht eingerichtet.'); } $variable = IPS_GetVariable($variableID); if ((int) $variable['VariableType'] !== 0) { throw new InvalidArgumentException($bezeichnung . ' muss eine Booleanvariable sein.'); } if ($aktionErforderlich && (int) $variable['VariableAction'] <= 0 && (int) $variable['VariableCustomAction'] <= 0 ) { throw new InvalidArgumentException($bezeichnung . ' benoetigt eine Standard- oder benutzerdefinierte Aktion.'); } } private function registriereMeldungen(): void { $alteIDs = array_unique(array_filter([ $this->ReadAttributeInteger('RegistrierterSchaltkontakt'), $this->ReadAttributeInteger('RegistrierteRueckmeldung'), ])); $neueIDs = array_unique(array_filter([ $this->ReadPropertyInteger('SchaltkontaktVariableID'), $this->ReadPropertyInteger('RueckmeldungVariableID'), ])); foreach ($alteIDs as $alteID) { if (!in_array($alteID, $neueIDs, true)) { $this->UnregisterMessage($alteID, self::VM_UPDATE); } } foreach ($neueIDs as $neueID) { if (IPS_VariableExists($neueID)) { $this->RegisterMessage($neueID, self::VM_UPDATE); } } $this->WriteAttributeInteger( 'RegistrierterSchaltkontakt', $this->ReadPropertyInteger('SchaltkontaktVariableID') ); $this->WriteAttributeInteger( 'RegistrierteRueckmeldung', $this->ReadPropertyInteger('RueckmeldungVariableID') ); } private function aktualisiereVariablen(): void { 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->RegisterVariableBoolean('Rueckmeldefehler', 'Rueckmeldefehler', '~Alert', 200); $this->RegisterVariableBoolean( 'SchaltbefehlAusstehend', 'Schaltbefehl ausstehend', '~Switch', 210 ); $this->RegisterVariableInteger('RestMindestzeit', 'Restliche Mindestzeit', '', 220); foreach (self::DIAGNOSE_VARIABLEN as $ident) { $wert = $ident === 'Leistungsquelle' ? Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET : $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); } } } } /** @param bool|int|float|string $wert */ private function setzeZustand(string $ident, $wert): void { $booleanAttribute = [ 'SollwertGueltig' => 'ZustandSollwertGueltig', 'Verfuegbar' => 'ZustandVerfuegbar', 'AenderungMoeglich' => 'ZustandAenderungMoeglich', 'Stoerung' => 'ZustandStoerung', 'Schaltzustand' => 'ZustandSchaltzustand', 'Rueckmeldefehler' => 'ZustandRueckmeldefehler', 'SchaltbefehlAusstehend' => 'ZustandSchaltbefehlAusstehend', ]; $integerAttribute = [ 'Sollleistung' => 'ZustandSollleistung', 'Tageslaufzeit' => 'ZustandTageslaufzeit', 'RestMindestzeit' => 'ZustandRestMindestzeit', ]; $floatAttribute = [ 'Istleistung' => 'ZustandIstleistung', ]; $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', 'Schaltzustand' => 'ZustandSchaltzustand', 'Rueckmeldefehler' => 'ZustandRueckmeldefehler', 'SchaltbefehlAusstehend' => 'ZustandSchaltbefehlAusstehend', ]; $integerAttribute = [ 'Sollleistung' => 'ZustandSollleistung', 'Tageslaufzeit' => 'ZustandTageslaufzeit', 'RestMindestzeit' => 'ZustandRestMindestzeit', ]; $floatAttribute = [ 'Istleistung' => 'ZustandIstleistung', ]; $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)); } private function zeitzone(): DateTimeZone { return new DateTimeZone(date_default_timezone_get()); } private function deaktiviereTimer(): void { foreach ([ 'Meldezyklus', 'RueckmeldungVerzoegert', 'Umschaltfreigabe', 'MindestzeitAbgelaufen', 'VorgabeTimeout', 'Tagesplanung', ] 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 ); } }