RegisterPropertyString('Batterieliste', '[]'); $this->RegisterPropertyInteger('SDLReserveLaden', 0); $this->RegisterPropertyInteger('SDLReserveEntladen', 0); $this->RegisterPropertyFloat('Reservezeit', 0.5); $this->RegisterPropertyInteger('Aktualisierungsintervall', 2); $this->RegisterPropertyInteger('MesswertMaxAlter', 30); $this->RegisterPropertyInteger('VorgabeTimeout', 30); $this->RegisterPropertyFloat('EigenverbrauchStart', 50.0); $this->RegisterPropertyFloat('SDLStart', 50.0); $this->RegisterPropertyBoolean('DiagnoseAnzeigen', false); $this->RegisterPropertyBoolean('LoggingEin', false); $this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10); $this->EnableAction('Aktiv'); $this->RegisterVariableInteger( 'EVManagement', 'EV Managementregister', '', 20 ); $this->EnableAction('EVManagement'); $this->RegisterVariableInteger('EVModus', 'EV Modusregister', '', 30); $this->EnableAction('EVModus'); $this->RegisterVariableFloat( 'EVLadeleistung', 'EV Laderegister', '', 40 ); $this->EnableAction('EVLadeleistung'); $this->RegisterVariableFloat( 'EVEntladeleistung', 'EV Entladeregister', '', 50 ); $this->EnableAction('EVEntladeleistung'); $this->RegisterVariableFloat( 'SDLSollleistung', 'SDL Sollleistung', '', 60 ); $this->EnableAction('SDLSollleistung'); $this->RegisterVariableFloat( 'GesamtLadezustand', 'Gesamtladezustand', '', 100 ); $this->RegisterVariableFloat( 'GesamtIstleistung', 'Gesamte Istleistung', '', 110 ); $this->RegisterVariableFloat( 'GesamtMaxLaden', 'Gesamte maximale Ladeleistung', '', 120 ); $this->RegisterVariableFloat( 'GesamtMaxEntladen', 'Gesamte maximale Entladeleistung', '', 130 ); $this->RegisterVariableFloat( 'EigenverbrauchLadezustand', 'Eigenverbrauch Ladezustand', '', 200 ); $this->RegisterVariableFloat( 'EigenverbrauchSollleistung', 'Eigenverbrauch Sollleistung', '', 210 ); $this->RegisterVariableFloat( 'EigenverbrauchIstleistung', 'Eigenverbrauch Istleistung', '', 220 ); $this->RegisterVariableFloat( 'EigenverbrauchMaxLaden', 'Eigenverbrauch maximal Laden', '', 230 ); $this->RegisterVariableFloat( 'EigenverbrauchMaxEntladen', 'Eigenverbrauch maximal Entladen', '', 240 ); $this->RegisterVariableFloat( 'SDLLadezustand', 'SDL Ladezustand', '', 300 ); $this->RegisterVariableFloat( 'SDLIstleistung', 'SDL Istleistung', '', 310 ); $this->RegisterVariableFloat( 'SDLMaxLaden', 'SDL maximal Laden', '', 320 ); $this->RegisterVariableFloat( 'SDLMaxEntladen', 'SDL maximal Entladen', '', 330 ); $this->RegisterVariableBoolean('Stoerung', 'Stoerung', '~Switch', 400); $this->RegisterVariableString('Stoertext', 'Stoertext', '', 410); $this->RegisterVariableString( 'Verteilstatus', 'Verteilstatus', '', 420 ); $this->RegisterAttributeString('RegistrierteMesswerte', '[]'); $this->RegisterAttributeString('LetzteZielwerte', '{}'); $this->RegisterAttributeFloat('EigenverbrauchEnergie_kWh', 0.0); $this->RegisterAttributeFloat('SDLEnergie_kWh', 0.0); $this->RegisterAttributeBoolean('KontenInitialisiert', false); $this->RegisterAttributeInteger('LetzteIntegration', 0); $this->RegisterAttributeInteger('LetzteEVVorgabe', 0); $this->RegisterAttributeInteger('LetzteSDLVorgabe', 0); $this->RegisterAttributeFloat('EigenverbrauchAuftrag_W', 0.0); $this->RegisterTimer( 'Aktualisierung', 0, 'IPS_RequestAction($_IPS["TARGET"], "Aktualisieren", 0);' ); $this->RegisterTimer( 'EVUebernahme', 0, 'IPS_RequestAction($_IPS["TARGET"], "EVUebernehmen", 0);' ); } public function ApplyChanges(): void { parent::ApplyChanges(); $this->SetTimerInterval('Aktualisierung', 0); $this->SetTimerInterval('EVUebernahme', 0); $this->registriereMesswertmeldungen(); try { $this->pruefeKonfiguration(); $this->SetTimerInterval( 'Aktualisierung', $this->ReadPropertyInteger('Aktualisierungsintervall') * 1000 ); $this->aktualisiere(); } catch (Throwable $fehler) { $this->sichererZustand($fehler->getMessage()); $this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG); } } public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void { if ((int) $nachricht !== self::VM_UPDATE) { return; } $ids = json_decode( $this->ReadAttributeString('RegistrierteMesswerte'), true ); if (is_array($ids) && in_array((int) $senderID, $ids, true)) { $this->aktualisiere(); } } public function RequestAction($ident, $wert): void { switch ($ident) { case 'Aktiv': $this->SetValue('Aktiv', (bool) $wert); if (!(bool) $wert) { $this->WriteAttributeFloat('EigenverbrauchAuftrag_W', 0.0); $this->SetValue('SDLSollleistung', 0.0); $this->sichererZustand(''); $this->SetStatus(self::STATUS_INAKTIV); return; } $this->aktualisiere(); return; case 'EVManagement': $this->SetValue('EVManagement', (int) $wert); $this->uebernehmeEVVorgabe(); return; case 'EVModus': $this->SetValue('EVModus', (int) $wert); $this->planeEVUebernahme(); return; case 'EVLadeleistung': $this->SetValue( 'EVLadeleistung', max(0.0, (float) $wert) ); $this->planeEVUebernahme(); return; case 'EVEntladeleistung': $this->SetValue( 'EVEntladeleistung', max(0.0, (float) $wert) ); $this->planeEVUebernahme(); return; case 'SDLSollleistung': $this->SetValue('SDLSollleistung', (float) $wert); $this->WriteAttributeInteger('LetzteSDLVorgabe', time()); $this->aktualisiere(); return; case 'EVUebernehmen': $this->SetTimerInterval('EVUebernahme', 0); $this->uebernehmeEVVorgabe(); return; case 'Aktualisieren': $this->aktualisiere(); return; case 'KontenReset': $this->WriteAttributeBoolean('KontenInitialisiert', false); $this->WriteAttributeInteger('LetzteIntegration', time()); $this->aktualisiere(); return; } throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident); } private function planeEVUebernahme(): void { $this->SetTimerInterval('EVUebernahme', 50); } private function uebernehmeEVVorgabe(): void { $management = (int) $this->GetValue('EVManagement'); $modus = (int) $this->GetValue('EVModus'); $ziel = 0.0; if ($management === 1) { $ziel = $modus === 1 ? -(float) $this->GetValue('EVEntladeleistung') : (float) $this->GetValue('EVLadeleistung'); } $this->WriteAttributeFloat('EigenverbrauchAuftrag_W', $ziel); $this->WriteAttributeInteger('LetzteEVVorgabe', time()); $this->SetValue('EigenverbrauchSollleistung', $ziel); $this->aktualisiere(); } private function aktualisiere(): void { $semaphore = 'EnelixVirtuelleBatterie_' . $this->InstanceID; if (!IPS_SemaphoreEnter($semaphore, 1000)) { return; } try { if (!(bool) $this->GetValue('Aktiv')) { $this->sichererZustand(''); $this->SetStatus(self::STATUS_INAKTIV); return; } $this->pruefeKonfiguration(); [$batterien, $fehler] = $this->leseBatterien(); if ($batterien === []) { throw new RuntimeException( 'Keine Batterie besitzt aktuelle gueltige Messwerte.' ); } $aggregat = VirtuelleBatterieRegler::aggregiere( $batterien, (float) $this->ReadPropertyInteger('SDLReserveLaden'), (float) $this->ReadPropertyInteger('SDLReserveEntladen'), $this->ReadPropertyFloat('Reservezeit') ); $this->initialisiereKonten($aggregat); $jetzt = time(); $timeout = $this->ReadPropertyInteger('VorgabeTimeout'); $ev = $jetzt - $this->ReadAttributeInteger('LetzteEVVorgabe') <= $timeout ? $this->ReadAttributeFloat('EigenverbrauchAuftrag_W') : 0.0; $sdl = $jetzt - $this->ReadAttributeInteger('LetzteSDLVorgabe') <= $timeout ? (float) $this->GetValue('SDLSollleistung') : 0.0; $auftrag = VirtuelleBatterieRegler::kombiniere( $ev, $sdl, $aggregat ); $verteilung = VirtuelleBatterieRegler::verteile( $auftrag['Gesamt_W'], $batterien ); $this->schreibeVerteilung($verteilung); $zuordnung = VirtuelleBatterieRegler::ordneIstleistungZu( $aggregat['Istleistung_W'], $auftrag['SDL_W'] ); $integrationsfehler = $this->integriereKonten( $zuordnung, $aggregat, $jetzt ); $this->SetValue( 'GesamtLadezustand', $aggregat['Ladezustand_Prozent'] ); $this->SetValue( 'GesamtIstleistung', $aggregat['Istleistung_W'] ); $this->SetValue('GesamtMaxLaden', $aggregat['MaxLaden_W']); $this->SetValue( 'GesamtMaxEntladen', $aggregat['MaxEntladen_W'] ); $this->SetValue( 'EigenverbrauchSollleistung', $auftrag['Eigenverbrauch_W'] ); $this->SetValue( 'EigenverbrauchIstleistung', $zuordnung['Eigenverbrauch_W'] ); $this->SetValue( 'EigenverbrauchMaxLaden', $aggregat['EigenverbrauchMaxLaden_W'] ); $this->SetValue( 'EigenverbrauchMaxEntladen', $aggregat['EigenverbrauchMaxEntladen_W'] ); $this->SetValue('SDLIstleistung', $zuordnung['SDL_W']); $this->SetValue('SDLMaxLaden', $aggregat['SDLMaxLaden_W']); $this->SetValue( 'SDLMaxEntladen', $aggregat['SDLMaxEntladen_W'] ); $this->aktualisiereKontenanzeigen($aggregat); $status = [ 'Eigenverbrauch_W' => round($auftrag['Eigenverbrauch_W'], 1), 'SDL_W' => round($auftrag['SDL_W'], 1), 'Gesamt_W' => round($auftrag['Gesamt_W'], 1), 'Batterien' => count($batterien), 'Verteilung_W' => $verteilung, ]; $this->SetValue( 'Verteilstatus', json_encode($status, JSON_THROW_ON_ERROR) ); $fehler = array_values(array_filter(array_merge( $fehler, $integrationsfehler === '' ? [] : [$integrationsfehler] ))); $this->setzeFehler(implode(' ', $fehler)); $this->SetStatus( $fehler === [] ? self::STATUS_AKTIV : self::STATUS_MESSWERTE_UNGUELTIG ); $this->protokolliere('Verteilung', $status); } catch (InvalidArgumentException $fehler) { $this->sichererZustand($fehler->getMessage()); $this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG); } catch (Throwable $fehler) { $this->sichererZustand($fehler->getMessage()); $this->SetStatus(self::STATUS_AUSGABE_FEHLER); } finally { IPS_SemaphoreLeave($semaphore); } } /** * @return array{0:list>, 1:list} */ private function leseBatterien(): array { $konfiguration = $this->leseBatterieliste(); $batterien = []; $fehler = []; $maxAlter = $this->ReadPropertyInteger('MesswertMaxAlter'); foreach ($konfiguration as $index => $eintrag) { $name = trim((string) ($eintrag['Name'] ?? '')); if ($name === '') { $name = 'Batterie ' . ($index + 1); } $socID = (int) ($eintrag['LadezustandVariableID'] ?? 0); $leistungID = (int) ($eintrag['IstleistungVariableID'] ?? 0); if (!$this->istAktuellerMesswert($socID, $maxAlter) || !$this->istAktuellerMesswert($leistungID, $maxAlter) ) { $fehler[] = $name . ': Messwerte fehlen oder sind veraltet.'; continue; } $batterien[] = [ 'Index' => $index, 'Name' => $name, 'Kapazitaet_kWh' => (float) ($eintrag['Kapazitaet_kWh'] ?? 0.0), 'MaxLaden_W' => (float) ($eintrag['MaxLaden_W'] ?? 0.0), 'MaxEntladen_W' => (float) ($eintrag['MaxEntladen_W'] ?? 0.0), 'Ladezustand_Prozent' => (float) GetValue($socID), 'Istleistung_W' => (float) GetValue($leistungID), ]; } return [$batterien, $fehler]; } /** @return list> */ private function leseBatterieliste(): array { $liste = json_decode( $this->ReadPropertyString('Batterieliste'), true, 512, JSON_THROW_ON_ERROR ); if (!is_array($liste)) { throw new InvalidArgumentException( 'Batterieliste muss eine JSON-Liste sein.' ); } return array_values($liste); } private function pruefeKonfiguration(): void { foreach ([ 'Aktualisierungsintervall', 'MesswertMaxAlter', 'VorgabeTimeout', ] as $property) { if ($this->ReadPropertyInteger($property) <= 0) { throw new InvalidArgumentException( $property . ' muss groesser als 0 sein.' ); } } if ($this->ReadPropertyFloat('Reservezeit') < 0.0) { throw new InvalidArgumentException( 'Reservezeit darf nicht negativ sein.' ); } $liste = $this->leseBatterieliste(); if ($liste === []) { throw new InvalidArgumentException( 'Mindestens eine physische Batterie ist erforderlich.' ); } $zielIDs = []; foreach ($liste as $index => $eintrag) { $name = 'Batterie ' . ($index + 1); if ((float) ($eintrag['Kapazitaet_kWh'] ?? 0.0) <= 0.0) { throw new InvalidArgumentException( $name . ': Kapazitaet muss groesser als 0 sein.' ); } foreach ([ 'LadezustandVariableID', 'IstleistungVariableID', ] as $feld) { $this->pruefeNumerischeVariable( (int) ($eintrag[$feld] ?? 0), $name . ' ' . $feld, false ); } $zielID = (int) ($eintrag['SollleistungVariableID'] ?? 0); $this->pruefeNumerischeVariable( $zielID, $name . ' SollleistungVariableID', true ); if (isset($zielIDs[$zielID])) { throw new InvalidArgumentException( 'Jede Batterie benoetigt eine eigene Sollleistungsvariable.' ); } $zielIDs[$zielID] = true; } } private function pruefeNumerischeVariable( int $id, string $name, bool $aktion ): void { if ($id <= 0 || !IPS_VariableExists($id)) { throw new InvalidArgumentException($name . ' ist nicht eingerichtet.'); } $variable = IPS_GetVariable($id); if (!in_array((int) $variable['VariableType'], [1, 2], true)) { throw new InvalidArgumentException($name . ' muss numerisch sein.'); } if ($aktion && (int) ($variable['VariableAction'] ?? 0) <= 0 && (int) ($variable['VariableCustomAction'] ?? 0) <= 0 ) { throw new InvalidArgumentException( $name . ' benoetigt eine Variablenaktion.' ); } } private function istAktuellerMesswert(int $id, int $maxAlter): bool { if ($id <= 0 || !IPS_VariableExists($id)) { return false; } $variable = IPS_GetVariable($id); if (!in_array((int) $variable['VariableType'], [1, 2], true)) { return false; } $aktualisiert = (int) ($variable['VariableUpdated'] ?? 0); return $aktualisiert > 0 && time() - $aktualisiert <= $maxAlter; } /** @param array $verteilung */ private function schreibeVerteilung(array $verteilung): void { $alt = json_decode( $this->ReadAttributeString('LetzteZielwerte'), true ); if (!is_array($alt)) { $alt = []; } $neu = []; foreach ($this->leseBatterieliste() as $index => $eintrag) { $zielID = (int) ($eintrag['SollleistungVariableID'] ?? 0); $ziel = (float) ($verteilung[$index] ?? 0.0); $neu[(string) $zielID] = $ziel; if (!isset($alt[(string) $zielID]) || abs((float) $alt[(string) $zielID] - $ziel) >= 0.5 ) { RequestAction($zielID, $ziel); } } $this->WriteAttributeString( 'LetzteZielwerte', json_encode($neu, JSON_THROW_ON_ERROR) ); } private function sichererZustand(string $fehler): void { try { $this->schreibeVerteilung([]); } catch (Throwable $ausgabeFehler) { if ($fehler === '') { $fehler = $ausgabeFehler->getMessage(); } else { $fehler .= ' ' . $ausgabeFehler->getMessage(); } } $this->SetValue('EigenverbrauchSollleistung', 0.0); $this->SetValue('EigenverbrauchIstleistung', 0.0); $this->SetValue('SDLIstleistung', 0.0); $this->setzeFehler($fehler); } /** @param array $aggregat */ private function initialisiereKonten(array $aggregat): void { if ($this->ReadAttributeBoolean('KontenInitialisiert')) { return; } $evProzent = min( 100.0, max(0.0, $this->ReadPropertyFloat('EigenverbrauchStart')) ); $sdlProzent = min( 100.0, max(0.0, $this->ReadPropertyFloat('SDLStart')) ); $this->WriteAttributeFloat( 'EigenverbrauchEnergie_kWh', $aggregat['EigenverbrauchKapazitaet_kWh'] * $evProzent / 100.0 ); $this->WriteAttributeFloat( 'SDLEnergie_kWh', $aggregat['SDLKapazitaet_kWh'] * $sdlProzent / 100.0 ); $this->WriteAttributeInteger('LetzteIntegration', time()); $this->WriteAttributeBoolean('KontenInitialisiert', true); } /** * @param array{Eigenverbrauch_W:float, SDL_W:float} $zuordnung * @param array $aggregat */ private function integriereKonten( array $zuordnung, array $aggregat, int $jetzt ): string { $letzteZeit = $this->ReadAttributeInteger('LetzteIntegration'); $this->WriteAttributeInteger('LetzteIntegration', $jetzt); if ($letzteZeit <= 0 || $jetzt <= $letzteZeit) { return ''; } $dauer = $jetzt - $letzteZeit; $maxLuecke = max( $this->ReadPropertyInteger('MesswertMaxAlter'), $this->ReadPropertyInteger('Aktualisierungsintervall') * 3 ); if ($dauer > $maxLuecke) { return 'Integrationsluecke erkannt; Konten wurden nicht fortgeschrieben.'; } $evKapazitaet = $aggregat['EigenverbrauchKapazitaet_kWh']; $sdlKapazitaet = $aggregat['SDLKapazitaet_kWh']; $evEnergie = $this->ReadAttributeFloat('EigenverbrauchEnergie_kWh') + $zuordnung['Eigenverbrauch_W'] * $dauer / 3600000.0; $sdlEnergie = $this->ReadAttributeFloat('SDLEnergie_kWh') + $zuordnung['SDL_W'] * $dauer / 3600000.0; $this->WriteAttributeFloat( 'EigenverbrauchEnergie_kWh', min($evKapazitaet, max(0.0, $evEnergie)) ); $this->WriteAttributeFloat( 'SDLEnergie_kWh', min($sdlKapazitaet, max(0.0, $sdlEnergie)) ); return ''; } /** @param array $aggregat */ private function aktualisiereKontenanzeigen(array $aggregat): void { $evKapazitaet = $aggregat['EigenverbrauchKapazitaet_kWh']; $sdlKapazitaet = $aggregat['SDLKapazitaet_kWh']; $this->SetValue( 'EigenverbrauchLadezustand', $evKapazitaet > 0.0 ? $this->ReadAttributeFloat('EigenverbrauchEnergie_kWh') / $evKapazitaet * 100.0 : 0.0 ); $this->SetValue( 'SDLLadezustand', $sdlKapazitaet > 0.0 ? $this->ReadAttributeFloat('SDLEnergie_kWh') / $sdlKapazitaet * 100.0 : 0.0 ); } private function registriereMesswertmeldungen(): void { $alteIDs = json_decode( $this->ReadAttributeString('RegistrierteMesswerte'), true ); if (!is_array($alteIDs)) { $alteIDs = []; } $neueIDs = []; try { foreach ($this->leseBatterieliste() as $eintrag) { foreach ([ 'LadezustandVariableID', 'IstleistungVariableID', ] as $feld) { $id = (int) ($eintrag[$feld] ?? 0); if ($id > 0 && IPS_VariableExists($id)) { $neueIDs[] = $id; } } } } catch (Throwable $fehler) { $neueIDs = []; } $neueIDs = array_values(array_unique($neueIDs)); foreach ($alteIDs as $id) { if (!in_array((int) $id, $neueIDs, true)) { $this->UnregisterMessage((int) $id, self::VM_UPDATE); } } foreach ($neueIDs as $id) { if (!in_array($id, $alteIDs, true)) { $this->RegisterMessage($id, self::VM_UPDATE); } } $this->WriteAttributeString( 'RegistrierteMesswerte', json_encode($neueIDs, JSON_THROW_ON_ERROR) ); } private function setzeFehler(string $text): void { $this->SetValue('Stoerung', $text !== ''); $this->SetValue('Stoertext', $text); } /** @param mixed $daten */ private function protokolliere(string $titel, $daten): void { if (!$this->ReadPropertyBoolean('LoggingEin')) { return; } $text = is_string($daten) ? $daten : json_encode($daten, JSON_THROW_ON_ERROR); $this->SendDebug($titel, $text, 0); } }