*/ private const REGISTER_PROPERTIES = [ 'Management' => 'ManagementRegisterVariableID', 'Modus' => 'ModusRegisterVariableID', 'Leistung' => 'LeistungsRegisterVariableID', 'Ladeleistung' => 'LadeleistungRegisterVariableID', 'Entladeleistung' => 'EntladeleistungRegisterVariableID', ]; /** @var list */ private const MESSWERT_PROPERTIES = [ 'MaxLadeleistungVariableID', 'MaxEntladeleistungVariableID', 'LadezustandVariableID', 'NetzleistungVariableID', 'IstleistungVariableID', ]; /** @var list */ private const DIAGNOSE_VARIABLEN = [ 'Istleistung', 'Leistungsquelle', 'Sollleistung', 'SollwertGueltig', 'Verfuegbar', 'AenderungMoeglich', 'Stoerung', 'Stoertext', 'Ladezustand', 'LadezustandGueltig', 'Netzleistung', 'MaxLadeleistung', 'MaxEntladeleistung', 'HystereseAktiv', 'Steuerungsmodus', 'BezogeneEnergie', 'LeistungsangebotDiagnose', 'LetzterRegisterbefehl', ]; public function Create(): void { parent::Create(); $this->registriereVerbraucherBasis(); $this->RegisterPropertyInteger('Batterietyp', BatterieRegler::TYP_UNKONFIGURIERT); $this->RegisterPropertyInteger( 'Batteriemanagement', BatterieRegler::STEUERUNG_WECHSELRICHTER ); $this->RegisterPropertyInteger('MaxLadeleistungVariableID', 0); $this->RegisterPropertyInteger('MaxEntladeleistungVariableID', 0); $this->RegisterPropertyInteger('LadezustandVariableID', 0); $this->RegisterPropertyInteger('NetzleistungVariableID', 0); $this->RegisterPropertyInteger('IstleistungVariableID', 0); $this->RegisterPropertyInteger('ManagementRegisterVariableID', 0); $this->RegisterPropertyInteger('ModusRegisterVariableID', 0); $this->RegisterPropertyInteger('LeistungsRegisterVariableID', 0); $this->RegisterPropertyInteger('LadeleistungRegisterVariableID', 0); $this->RegisterPropertyInteger('EntladeleistungRegisterVariableID', 0); $this->RegisterPropertyFloat('ReserveLadezustand', 20.0); $this->RegisterPropertyFloat('MindestLadezustand', 10.0); $this->RegisterPropertyFloat('LadezustandHysterese', 2.0); $this->RegisterPropertyBoolean('NachladenMitMaximalleistung', true); $this->RegisterPropertyInteger('MaximaleNachladeleistung', 0); $this->RegisterPropertyInteger('MesswertMaxAlter', 60); $this->RegisterPropertyInteger('Aenderungssperre', 4); $this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false); $this->RegisterVariableInteger('Ladestatus', 'Ladestatus', '', 100); $this->RegisterAttributeString('RegistrierteMesswerte', '[]'); $this->RegisterAttributeInteger('LetzteVorgabeZeit', 0); $this->RegisterAttributeInteger('LetzteManagerID', 0); $this->RegisterAttributeInteger('AenderungFreigabeAb', 0); $this->RegisterAttributeInteger('EnergieStandZeit', 0); $this->RegisterAttributeInteger('LetzterSollwert', 0); $this->RegisterAttributeString('Leistungsangebot', '[0]'); $this->RegisterAttributeString( 'Betriebsart', Nachrichtenvertrag::BETRIEBSART_PV ); $this->RegisterAttributeString('LetzterRegisterbefehl', '{}'); $this->RegisterAttributeString('Registerfehler', ''); $this->RegisterAttributeBoolean('RegisterInitialisiert', false); $this->RegisterAttributeBoolean('RegisterSchreiben', false); $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->RegisterAttributeFloat('ZustandLadezustand', 0.0); $this->RegisterAttributeBoolean('ZustandLadezustandGueltig', false); $this->RegisterAttributeFloat('ZustandNetzleistung', 0.0); $this->RegisterAttributeFloat('ZustandMaxLadeleistung', 0.0); $this->RegisterAttributeFloat('ZustandMaxEntladeleistung', 0.0); $this->RegisterAttributeBoolean('ZustandHystereseAktiv', false); $this->RegisterAttributeInteger('ZustandSteuerungsmodus', 0); $this->RegisterAttributeFloat('ZustandBezogeneEnergie', 0.0); $this->RegisterTimer( 'Meldezyklus', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);" ); $this->RegisterTimer( 'RueckmeldungVerzoegert', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);" ); $this->RegisterTimer( 'VorgabeTimeout', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', 0);" ); $this->RegisterTimer( 'AenderungFreigabe', 0, "IPS_RequestAction(\$_IPS['TARGET'], 'AenderungFreigabe', 0);" ); } public function ApplyChanges(): void { parent::ApplyChanges(); $this->aktualisiereVariablen(); $this->registriereMesswertmeldungen(); $this->WriteAttributeBoolean('RegisterInitialisiert', false); 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->aktualisiere(true); } public function GetConfigurationForm(): string { $formular = json_decode( (string) file_get_contents(__DIR__ . '/form.json'), true, 512, JSON_THROW_ON_ERROR ); $this->setzeRegisterfelderSichtbar( $formular['elements'], $this->ReadPropertyInteger('Batterietyp') ); $this->setzeFormfeldSichtbar( $formular['elements'], 'MaximaleNachladeleistung', !$this->ReadPropertyBoolean('NachladenMitMaximalleistung') ); return json_encode($formular, JSON_THROW_ON_ERROR); } public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void { if ((int) $nachricht !== self::VM_UPDATE || $this->ReadAttributeBoolean('RegisterSchreiben') ) { return; } $messwerte = json_decode( $this->ReadAttributeString('RegistrierteMesswerte'), true ); if (is_array($messwerte) && in_array((int) $senderID, $messwerte, 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 'VorgabeTimeout': $this->SetTimerInterval('VorgabeTimeout', 0); $this->setzeZustand('SollwertGueltig', false); $this->aktualisiere(true); return; case 'AenderungFreigabe': $this->SetTimerInterval('AenderungFreigabe', 0); $this->aktualisiere(true); return; case 'Aktualisieren': $this->aktualisiere(true); return; case 'SichererZustand': $this->setzeZustand('SollwertGueltig', false); $this->schreibeRegister(0, true); $this->aktualisiere(true); return; case 'FormBatterietyp': $this->aktualisiereFormRegisterfelder((int) $wert); return; case 'FormNachladenMitMaximalleistung': $this->UpdateFormField( 'MaximaleNachladeleistung', 'visible', !(bool) $wert ); 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 diese Batterie nicht aktiv zugeordnet.' ); } $this->WriteAttributeString('Betriebsart', $daten['Betriebsart']); $this->aktualisiere(false); $sollleistung = $daten['Sollleistung_W']; if ($sollleistung === null) { if ((bool) $this->leseZustand('SollwertGueltig') && !Nachrichtenvertrag::enthaeltLeistungswert( $this->leseLeistungsangebot(), (int) $this->leseZustand('Sollleistung') ) ) { $this->setzeZustand('SollwertGueltig', false); $this->aktualisiere(false); } $this->planeMeldung(); return; } if (!Nachrichtenvertrag::enthaeltLeistungswert( $this->leseLeistungsangebot(), $sollleistung )) { throw new InvalidArgumentException( 'Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.' ); } if (!(bool) $this->leseZustand('AenderungMoeglich') && $sollleistung !== (int) $this->leseZustand('Sollleistung') ) { throw new RuntimeException( 'Die Aenderungssperre der Batterie ist noch aktiv.' ); } $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->aktualisiereEnergie($jetzt); $messwerte = $this->leseMesswerte(); $messwerteGueltig = $messwerte['Gueltig']; $this->setzeZustand('Ladezustand', $messwerte['Ladezustand']); $this->setzeZustand('LadezustandGueltig', $messwerteGueltig); $this->setzeZustand('Netzleistung', $messwerte['Netzleistung']); $this->setzeZustand('MaxLadeleistung', $messwerte['MaxLaden']); $this->setzeZustand('MaxEntladeleistung', $messwerte['MaxEntladen']); $this->setzeZustand('Istleistung', $messwerte['Istleistung']); $this->setzeSichtbareVariable( 'Leistungsquelle', $messwerteGueltig ? Nachrichtenvertrag::LEISTUNGSQUELLE_GEMESSEN : Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN ); $this->aktualisiereLadestatus( $messwerte['Istleistung'], $messwerteGueltig ); if ($messwerteGueltig) { $hysterese = BatterieRegler::hystereseAktiv( $messwerte['Ladezustand'], $this->ReadPropertyFloat('ReserveLadezustand'), $this->ReadPropertyFloat('LadezustandHysterese'), (bool) $this->leseZustand('HystereseAktiv') ); $this->setzeZustand('HystereseAktiv', $hysterese); } $enelixSteuert = $this->ReadPropertyInteger('Batteriemanagement') === BatterieRegler::STEUERUNG_ENELIX; $aktiv = (bool) $this->GetValue('Aktiv'); $verfuegbar = $aktiv && $enelixSteuert && $messwerteGueltig; $angebot = $messwerteGueltig ? BatterieRegler::leistungsangebot( $aktiv, $enelixSteuert, $this->ReadAttributeString('Betriebsart') === Nachrichtenvertrag::BETRIEBSART_PEAK, $messwerte['Ladezustand'], $this->ReadPropertyFloat('ReserveLadezustand'), $this->ReadPropertyFloat('MindestLadezustand'), (bool) $this->leseZustand('HystereseAktiv'), $messwerte['Netzleistung'], $messwerte['Istleistung'], $messwerte['MaxLaden'], $messwerte['MaxEntladen'], $this->ReadPropertyBoolean('NachladenMitMaximalleistung') ? null : (float) $this->ReadPropertyInteger('MaximaleNachladeleistung') ) : [0]; $this->WriteAttributeString( 'Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR) ); $this->setzeSichtbareVariable( 'LeistungsangebotDiagnose', json_encode($angebot, JSON_THROW_ON_ERROR) ); if ((bool) $this->leseZustand('SollwertGueltig') && !Nachrichtenvertrag::enthaeltLeistungswert( $angebot, (int) $this->leseZustand('Sollleistung') ) ) { $this->setzeZustand('SollwertGueltig', false); } $this->pruefeVorgabeTimeout($jetzt); $ziel = $verfuegbar && (bool) $this->leseZustand('SollwertGueltig') ? (int) $this->leseZustand('Sollleistung') : 0; $this->schreibeRegister($ziel, false); $this->setzeZustand('Sollleistung', $ziel); $this->setzeZustand('Verfuegbar', $verfuegbar); $aenderungMoeglich = $verfuegbar && $jetzt >= $this->ReadAttributeInteger('AenderungFreigabeAb'); $this->setzeZustand('AenderungMoeglich', $aenderungMoeglich); $this->setzeZustand( 'Steuerungsmodus', $this->ReadPropertyInteger('Batteriemanagement') ); $this->setzeStoerung( $messwerteGueltig ? '' : 'Batteriemesswerte fehlen, sind veraltet oder ungueltig.' ); $this->SetStatus( $messwerteGueltig ? self::STATUS_AKTIV : self::STATUS_MESSWERTE_UNGUELTIG ); $this->planeAenderungsfreigabe($jetzt); $this->planeVorgabeTimeout($jetzt); if ($meldungPlanen) { $this->planeMeldung(); } } catch (Throwable $fehler) { $this->WriteAttributeString('Registerfehler', $fehler->getMessage()); $this->setzeZustand('Verfuegbar', false); $this->setzeZustand('AenderungMoeglich', false); $this->setzeStoerung($fehler->getMessage()); $this->SetStatus(self::STATUS_REGISTERFEHLER); $this->protokolliere('Aktualisierung', $fehler->getMessage()); if ($meldungPlanen) { $this->planeMeldung(); } } } /** @return array{Gueltig: bool, MaxLaden: float, MaxEntladen: float, Ladezustand: float, Netzleistung: float, Istleistung: float} */ private function leseMesswerte(): array { $werte = [ 'MaxLaden' => 0.0, 'MaxEntladen' => 0.0, 'Ladezustand' => 0.0, 'Netzleistung' => 0.0, 'Istleistung' => 0.0, ]; $zuordnung = [ 'MaxLadeleistungVariableID' => 'MaxLaden', 'MaxEntladeleistungVariableID' => 'MaxEntladen', 'LadezustandVariableID' => 'Ladezustand', 'NetzleistungVariableID' => 'Netzleistung', 'IstleistungVariableID' => 'Istleistung', ]; $gueltig = true; foreach ($zuordnung as $property => $schluessel) { $variablenID = $this->ReadPropertyInteger($property); if (!$this->istMesswertAktuell($variablenID)) { $gueltig = false; continue; } $werte[$schluessel] = (float) GetValue($variablenID); } if ($werte['MaxLaden'] <= 0.0 || $werte['MaxEntladen'] <= 0.0 || $werte['Ladezustand'] < 0.0 || $werte['Ladezustand'] > 100.0 ) { $gueltig = false; } return ['Gueltig' => $gueltig] + $werte; } private function istMesswertAktuell(int $variablenID): bool { if ($variablenID <= 0 || !IPS_VariableExists($variablenID)) { return false; } $variable = IPS_GetVariable($variablenID); $aktualisiert = (int) ($variable['VariableUpdated'] ?? 0); return $aktualisiert > 0 && time() - $aktualisiert <= $this->ReadPropertyInteger('MesswertMaxAlter'); } private function schreibeRegister(int $sollleistungW, bool $erzwingen): void { $werte = BatterieRegler::registerwerte( $this->ReadPropertyInteger('Batterietyp'), $this->ReadPropertyInteger('Batteriemanagement'), $sollleistungW ); $befehl = json_encode($werte, JSON_THROW_ON_ERROR); if (!$erzwingen && $this->ReadAttributeBoolean('RegisterInitialisiert') && $befehl === $this->ReadAttributeString('LetzterRegisterbefehl') ) { return; } $this->WriteAttributeBoolean('RegisterSchreiben', true); try { foreach ($werte as $register => $wert) { $property = self::REGISTER_PROPERTIES[$register]; RequestAction($this->ReadPropertyInteger($property), $wert); } } catch (Throwable $fehler) { throw new RuntimeException( 'Batterieregister konnten nicht geschrieben werden: ' . $fehler->getMessage() ); } finally { $this->WriteAttributeBoolean('RegisterSchreiben', false); } $alterSollwert = $this->ReadAttributeInteger('LetzterSollwert'); $this->WriteAttributeString('LetzterRegisterbefehl', $befehl); $this->WriteAttributeBoolean('RegisterInitialisiert', true); $this->setzeSichtbareVariable('LetzterRegisterbefehl', $befehl); $this->WriteAttributeString('Registerfehler', ''); if ($alterSollwert !== $sollleistungW && $this->ReadPropertyInteger('Batteriemanagement') === BatterieRegler::STEUERUNG_ENELIX ) { $this->WriteAttributeInteger( 'AenderungFreigabeAb', time() + $this->ReadPropertyInteger('Aenderungssperre') ); } $this->WriteAttributeInteger('LetzterSollwert', $sollleistungW); $this->protokolliere('Registerbefehl', $werte); } private function aktualisiereEnergie(int $jetzt): void { $standZeit = $this->ReadAttributeInteger('EnergieStandZeit'); if ($standZeit > 0 && $jetzt >= $standZeit) { $energie = (float) $this->leseZustand('BezogeneEnergie') + (float) $this->leseZustand('Istleistung') * ($jetzt - $standZeit) / 3600000.0; $this->setzeZustand('BezogeneEnergie', $energie); } $this->WriteAttributeInteger('EnergieStandZeit', $jetzt); } private function aktualisiereLadestatus( float $istleistungW, bool $gueltig ): void { $status = 0; if ($gueltig) { $status = $istleistungW > 0.0 ? 2 : ($istleistungW < 0.0 ? 3 : 1); } $this->SetValue('Ladestatus', $status); } 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 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 planeAenderungsfreigabe(int $jetzt): void { $freigabe = $this->ReadAttributeInteger('AenderungFreigabeAb'); $this->SetTimerInterval( 'AenderungFreigabe', $freigabe > $jetzt ? max(1, ($freigabe - $jetzt) * 1000) : 0 ); } private function planeMeldung(): void { $this->SetTimerInterval('RueckmeldungVerzoegert', 100); } 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 { $messwerteGueltig = (bool) $this->leseZustand('LadezustandGueltig'); $registerfehler = $this->ReadAttributeString('Registerfehler'); 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' => $messwerteGueltig ? (float) $this->leseZustand('Istleistung') : null, 'Leistungsquelle' => $messwerteGueltig ? Nachrichtenvertrag::LEISTUNGSQUELLE_GEMESSEN : Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN, 'Zustand' => [ [ 'Kennung' => 'Sollleistung_W', 'Art' => 'Sollwert', 'Wert' => (bool) $this->leseZustand('SollwertGueltig') ? (int) $this->leseZustand('Sollleistung') : null, 'Einheit' => 'W', ], [ 'Kennung' => 'Ladezustand_Prozent', 'Art' => 'Istwert', 'Wert' => $messwerteGueltig ? (float) $this->leseZustand('Ladezustand') : null, 'Einheit' => '%', ], [ 'Kennung' => 'HystereseAktiv', 'Art' => 'Status', 'Wert' => (bool) $this->leseZustand('HystereseAktiv'), 'Einheit' => '', ], [ 'Kennung' => 'Batteriesteuerung', 'Art' => 'Status', 'Wert' => $this->ReadPropertyInteger('Batteriemanagement'), 'Einheit' => '', ], [ 'Kennung' => 'Messwertfehler', 'Art' => 'Stoerung', 'Wert' => !$messwerteGueltig, 'Einheit' => '', 'Text' => $messwerteGueltig ? '' : 'Batteriemesswerte fehlen, sind veraltet oder ungueltig.', ], [ 'Kennung' => 'Registerfehler', 'Art' => 'Stoerung', 'Wert' => $registerfehler !== '', 'Einheit' => '', 'Text' => $registerfehler, ], ], ]; } /** @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', 'Aenderungssperre'] as $property) { if ($this->ReadPropertyInteger($property) < 0) { throw new InvalidArgumentException( $property . ' darf nicht negativ sein.' ); } } foreach (['Meldeintervall', 'VorgabeTimeout', 'MesswertMaxAlter'] as $property) { if ($this->ReadPropertyInteger($property) <= 0) { throw new InvalidArgumentException( $property . ' muss groesser als 0 sein.' ); } } if (!$this->ReadPropertyBoolean('NachladenMitMaximalleistung') && $this->ReadPropertyInteger('MaximaleNachladeleistung') <= 0 ) { throw new InvalidArgumentException( 'MaximaleNachladeleistung muss groesser als 0 W sein.' ); } BatterieRegler::hystereseAktiv( 50.0, $this->ReadPropertyFloat('ReserveLadezustand'), $this->ReadPropertyFloat('LadezustandHysterese'), false ); BatterieRegler::leistungsangebot( true, true, false, 50.0, $this->ReadPropertyFloat('ReserveLadezustand'), $this->ReadPropertyFloat('MindestLadezustand'), false, 0.0, 0.0, 1.0, 1.0, $this->ReadPropertyBoolean('NachladenMitMaximalleistung') ? null : (float) $this->ReadPropertyInteger('MaximaleNachladeleistung') ); foreach (self::MESSWERT_PROPERTIES as $property) { $this->pruefeNumerischeVariable( $this->ReadPropertyInteger($property), $property, false ); } $registerIDs = []; foreach (BatterieRegler::erforderlicheRegister( $this->ReadPropertyInteger('Batterietyp') ) as $register) { $property = self::REGISTER_PROPERTIES[$register]; $variablenID = $this->ReadPropertyInteger($property); $this->pruefeNumerischeVariable($variablenID, $property, true); if (isset($registerIDs[$variablenID])) { throw new InvalidArgumentException( 'Jedes Batterieregister muss eine eigene Variable verwenden.' ); } $registerIDs[$variablenID] = true; } BatterieRegler::registerwerte( $this->ReadPropertyInteger('Batterietyp'), $this->ReadPropertyInteger('Batteriemanagement'), 0 ); } private function pruefeNumerischeVariable( int $variablenID, string $bezeichnung, bool $aktionErforderlich ): void { if ($variablenID <= 0 || !IPS_VariableExists($variablenID)) { throw new InvalidArgumentException( $bezeichnung . ' ist nicht eingerichtet.' ); } $variable = IPS_GetVariable($variablenID); if (!in_array((int) $variable['VariableType'], [1, 2], true)) { throw new InvalidArgumentException( $bezeichnung . ' muss eine Integer- oder Floatvariable sein.' ); } if ($aktionErforderlich && (int) $variable['VariableAction'] <= 0 && (int) $variable['VariableCustomAction'] <= 0 ) { throw new InvalidArgumentException( $bezeichnung . ' benoetigt eine Standard- oder benutzerdefinierte Aktion.' ); } } private function registriereMesswertmeldungen(): void { $alteIDs = json_decode( $this->ReadAttributeString('RegistrierteMesswerte'), true ); if (!is_array($alteIDs)) { $alteIDs = []; } $neueIDs = []; foreach (self::MESSWERT_PROPERTIES as $property) { $variablenID = $this->ReadPropertyInteger($property); if ($variablenID > 0) { $neueIDs[] = $variablenID; } } $neueIDs = array_values(array_unique($neueIDs)); foreach ($alteIDs as $alteID) { if (!in_array((int) $alteID, $neueIDs, true)) { $this->UnregisterMessage((int) $alteID, self::VM_UPDATE); } } foreach ($neueIDs as $neueID) { if (IPS_VariableExists($neueID)) { $this->RegisterMessage($neueID, self::VM_UPDATE); } } $this->WriteAttributeString( 'RegistrierteMesswerte', json_encode($neueIDs, JSON_THROW_ON_ERROR) ); } private function aktualisiereVariablen(): void { if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) { $this->registriereVerbraucherDiagnose(); $this->RegisterVariableFloat('Ladezustand', 'Ladezustand', '', 200); $this->RegisterVariableBoolean( 'LadezustandGueltig', 'Ladezustand gueltig', '~Switch', 210 ); $this->RegisterVariableFloat('Netzleistung', 'Netzleistung', '', 220); $this->RegisterVariableFloat( 'MaxLadeleistung', 'Maximale Ladeleistung', '', 230 ); $this->RegisterVariableFloat( 'MaxEntladeleistung', 'Maximale Entladeleistung', '', 240 ); $this->RegisterVariableBoolean( 'HystereseAktiv', 'Hysterese aktiv', '~Switch', 250 ); $this->RegisterVariableInteger( 'Steuerungsmodus', 'Steuerungsmodus', '', 260 ); $this->RegisterVariableFloat( 'BezogeneEnergie', 'Bezogene Energie', '~Electricity', 270 ); $this->RegisterVariableString( 'LeistungsangebotDiagnose', 'Leistungsangebot', '', 280 ); $this->RegisterVariableString( 'LetzterRegisterbefehl', 'Letzter Registerbefehl', '', 290 ); $this->synchronisiereDiagnosevariablen(); 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); } } } private function synchronisiereDiagnosevariablen(): void { foreach (self::DIAGNOSE_VARIABLEN as $ident) { if ($ident === 'Leistungsquelle') { continue; } if ($ident === 'LeistungsangebotDiagnose') { $wert = $this->ReadAttributeString('Leistungsangebot'); } elseif ($ident === 'LetzterRegisterbefehl') { $wert = $this->ReadAttributeString('LetzterRegisterbefehl'); } else { $wert = $this->leseZustand($ident); } $this->setzeSichtbareVariable($ident, $wert); } } /** @param bool|int|float|string $wert */ private function setzeZustand(string $ident, $wert): void { $booleanAttribute = [ 'SollwertGueltig' => 'ZustandSollwertGueltig', 'Verfuegbar' => 'ZustandVerfuegbar', 'AenderungMoeglich' => 'ZustandAenderungMoeglich', 'Stoerung' => 'ZustandStoerung', 'LadezustandGueltig' => 'ZustandLadezustandGueltig', 'HystereseAktiv' => 'ZustandHystereseAktiv', ]; $integerAttribute = [ 'Sollleistung' => 'ZustandSollleistung', 'Steuerungsmodus' => 'ZustandSteuerungsmodus', ]; $floatAttribute = [ 'Istleistung' => 'ZustandIstleistung', 'Ladezustand' => 'ZustandLadezustand', 'Netzleistung' => 'ZustandNetzleistung', 'MaxLadeleistung' => 'ZustandMaxLadeleistung', 'MaxEntladeleistung' => 'ZustandMaxEntladeleistung', '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); } else { throw new LogicException( 'Unbekannter interner Batteriezustand: ' . $ident ); } $this->setzeSichtbareVariable($ident, $wert); } /** @return bool|int|float|string */ private function leseZustand(string $ident) { $booleanAttribute = [ 'SollwertGueltig' => 'ZustandSollwertGueltig', 'Verfuegbar' => 'ZustandVerfuegbar', 'AenderungMoeglich' => 'ZustandAenderungMoeglich', 'Stoerung' => 'ZustandStoerung', 'LadezustandGueltig' => 'ZustandLadezustandGueltig', 'HystereseAktiv' => 'ZustandHystereseAktiv', ]; $integerAttribute = [ 'Sollleistung' => 'ZustandSollleistung', 'Steuerungsmodus' => 'ZustandSteuerungsmodus', ]; $floatAttribute = [ 'Istleistung' => 'ZustandIstleistung', 'Ladezustand' => 'ZustandLadezustand', 'Netzleistung' => 'ZustandNetzleistung', 'MaxLadeleistung' => 'ZustandMaxLadeleistung', 'MaxEntladeleistung' => 'ZustandMaxEntladeleistung', '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 Batteriezustand: ' . $ident ); } /** @param bool|int|float|string $wert */ private function setzeSichtbareVariable(string $ident, $wert): void { $variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID) ) { SetValue($variablenID, $wert); } } /** @return list */ private function leseLeistungsangebot(): array { $angebot = json_decode( $this->ReadAttributeString('Leistungsangebot'), true, 512, JSON_THROW_ON_ERROR ); if (!is_array($angebot)) { return [0]; } return array_values($angebot); } private function setzeStoerung(string $text): void { $this->setzeZustand('Stoerung', $text !== ''); $this->setzeZustand('Stoertext', $text); } /** @param array $elemente */ private function setzeFormfeldSichtbar( array &$elemente, string $name, bool $sichtbar ): void { foreach ($elemente as &$element) { if (($element['name'] ?? '') === $name) { $element['visible'] = $sichtbar; } if (isset($element['items']) && is_array($element['items'])) { $this->setzeFormfeldSichtbar($element['items'], $name, $sichtbar); } } } /** @param array $elemente */ private function setzeRegisterfelderSichtbar( array &$elemente, int $batterietyp ): void { $sichtbar = $this->registerfeldSichtbarkeit($batterietyp); foreach ($elemente as &$element) { if (isset($element['name'], $sichtbar[$element['name']])) { $element['visible'] = $sichtbar[$element['name']]; } if (isset($element['items']) && is_array($element['items'])) { $this->setzeRegisterfelderSichtbar( $element['items'], $batterietyp ); } } } private function aktualisiereFormRegisterfelder(int $batterietyp): void { foreach ($this->registerfeldSichtbarkeit($batterietyp) as $name => $sichtbar) { $this->UpdateFormField($name, 'visible', $sichtbar); } } /** @return array */ private function registerfeldSichtbarkeit(int $batterietyp): array { $konfiguriert = in_array($batterietyp, [ BatterieRegler::TYP_HERSTELLERUNABHAENGIG, BatterieRegler::TYP_GOODWE, BatterieRegler::TYP_SOLAREDGE, BatterieRegler::TYP_SIGENERGY, ], true); $goodwe = $batterietyp === BatterieRegler::TYP_GOODWE; $getrennt = in_array($batterietyp, [ BatterieRegler::TYP_HERSTELLERUNABHAENGIG, BatterieRegler::TYP_SOLAREDGE, BatterieRegler::TYP_SIGENERGY, ], true); return [ 'ManagementRegisterVariableID' => $konfiguriert, 'ModusRegisterVariableID' => $konfiguriert, 'LeistungsRegisterVariableID' => $goodwe, 'LadeleistungRegisterVariableID' => $getrennt, 'EntladeleistungRegisterVariableID' => $getrennt, ]; } private function deaktiviereTimer(): void { foreach ([ 'Meldezyklus', 'RueckmeldungVerzoegert', 'VorgabeTimeout', 'AenderungFreigabe', ] 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 ); } }