archivID(); if ($archivID === 0) { $this->protokolliere('Energiearchiv', 'Kein Archiv Control vorhanden.'); return; } $aktiv = $this->ReadPropertyBoolean('EnergieaufzeichnungAktiv'); $geaendert = []; $leistungstyp = $this->verdichtungstyp( $this->ReadPropertyInteger('LeistungsaufzeichnungMinuten') ); $energietyp = $this->verdichtungstyp( $this->ReadPropertyInteger('EnergieaufzeichnungMinuten') ); foreach (self::LEISTUNGS_VARIABLEN as $ident) { $variableID = $this->GetIDForIdent($ident); $loggen = $aktiv && (!str_starts_with($ident, 'SDL') || $this->ReadPropertyBoolean('SDLAktiv')) && ($ident !== 'SDLSOCArchiv' || $this->ReadPropertyInteger('SDLSOCVariableID') > 0); if (!$this->setzeArchivLogging($archivID, $variableID, $loggen)) { continue; } AC_SetAggregationType($archivID, $variableID, 0); AC_SetGraphStatus($archivID, $variableID, $loggen); $regeln = [-1 => $leistungstyp]; $loeschen = $this->ReadPropertyInteger('LeistungLoeschenMonate'); if ($loeschen > 0) { $regeln[$loeschen] = 7; } if ($aktiv && $this->setzeVerdichtung($archivID, $variableID, $regeln)) { $geaendert[] = $variableID; } } foreach (self::ENERGIE_VARIABLEN as $ident) { $variableID = $this->GetIDForIdent($ident); if (!$this->setzeArchivLogging($archivID, $variableID, $aktiv)) { continue; } AC_SetAggregationType($archivID, $variableID, 1); AC_SetGraphStatus($archivID, $variableID, $aktiv); $regeln = [-1 => $energietyp]; $verdichten = $this->ReadPropertyInteger('EnergieVerdichtenMonate'); if ($verdichten > 0) { $regeln[$verdichten] = 3; } $loeschen = $this->ReadPropertyInteger('EnergieLoeschenMonate'); if ($loeschen > 0) { $regeln[$loeschen] = 7; } if ($aktiv && $this->setzeVerdichtung($archivID, $variableID, $regeln)) { $geaendert[] = $variableID; } } foreach (IPS_GetChildrenIDs($this->InstanceID) as $variableID) { $objekt = IPS_GetObject($variableID); $ident = (string) ($objekt['ObjectIdent'] ?? ''); if ((int) ($objekt['ObjectType'] ?? -1) !== 2 || !str_starts_with($ident, 'Diagramm') ) { continue; } $istEnergie = str_contains($ident, 'Energie') || str_contains($ident, 'BatterieLaden_') || str_contains($ident, 'BatterieEntladen_'); if (!$this->setzeArchivLogging($archivID, $variableID, $aktiv)) { continue; } AC_SetAggregationType($archivID, $variableID, $istEnergie ? 1 : 0); AC_SetGraphStatus($archivID, $variableID, $aktiv); $regeln = [-1 => $istEnergie ? $energietyp : $leistungstyp]; $verdichten = $istEnergie ? $this->ReadPropertyInteger('EnergieVerdichtenMonate') : 0; if ($verdichten > 0) { $regeln[$verdichten] = 3; } $loeschen = $istEnergie ? $this->ReadPropertyInteger('EnergieLoeschenMonate') : $this->ReadPropertyInteger('LeistungLoeschenMonate'); if ($loeschen > 0) { $regeln[$loeschen] = 7; } if ($aktiv && $this->setzeVerdichtung($archivID, $variableID, $regeln)) { $geaendert[] = $variableID; } } IPS_ApplyChanges($archivID); foreach (array_unique($geaendert) as $variableID) { AC_ReAggregateVariable($archivID, $variableID); } } private function setzeArchivLogging(int $archivID, int $variableID, bool $aktiv): bool { // Archiv-Metadaten sind nur bei aktivierter Aufzeichnung aenderbar. if (!$aktiv && AC_GetLoggingStatus($archivID, $variableID)) { AC_SetGraphStatus($archivID, $variableID, false); } AC_SetLoggingStatus($archivID, $variableID, $aktiv); return $aktiv; } /** @param array $regeln */ private function setzeVerdichtung(int $archivID, int $variableID, array $regeln): bool { ksort($regeln); $aktuell = []; $eintraege = AC_GetCompaction($archivID, $variableID); if (is_array($eintraege)) { foreach ($eintraege as $eintrag) { $monat = (int) ($eintrag['MonthOffset'] ?? -999); $typ = (int) ($eintrag['CompactionType'] ?? -1); if ($monat >= -1 && $typ >= 0) { $aktuell[$monat] = $typ; } } } ksort($aktuell); if ($aktuell === $regeln) { return false; } foreach (array_keys($aktuell) as $monat) { AC_SetCompaction($archivID, $variableID, $monat, -1); } foreach ($regeln as $monat => $typ) { AC_SetCompaction($archivID, $variableID, $monat, $typ); } return true; } private function verdichtungstyp(int $minuten): int { return [1 => 0, 5 => 1, 60 => 2][$minuten] ?? 0; } private function archivID(): int { $archive = IPS_GetInstanceListByModuleID(self::ARCHIVE_MODULE_ID); return $archive === [] ? 0 : (int) $archive[0]; } private function synchronisiereEnergievisualisierungen(): void { $this->synchronisiereEnergyPie($this->ReadPropertyBoolean('EnergyPieAnzeigen')); $this->synchronisiereEnergiediagramme( $this->ReadPropertyBoolean('EnergiediagrammeAnzeigen') ); $this->synchronisiereFunFacts($this->ReadPropertyBoolean('FunFactsAnzeigen')); $this->synchronisiereEnergiefluss($this->ReadPropertyBoolean('EnergieflussAnzeigen')); } private function synchronisiereEnergyPie(bool $anzeigen): void { if ($anzeigen) { $this->erstelleEnergyPie(); return; } $this->entferneInstanzUnterobjekt('EnergyPie', self::ENERGY_PIE_MODULE_ID); } private function synchronisiereEnergiediagramme(bool $anzeigen): void { if ($anzeigen) { $this->erstelleEnergiediagramme(); return; } $this->entferneDiagrammUnterobjekt('Leistungsdiagramm'); $this->entferneDiagrammUnterobjekt('Energiediagramm'); $this->WriteAttributeString('EnergiediagrammKonfiguration', '{}'); } private function synchronisiereFunFacts(bool $anzeigen): void { if ($anzeigen) { $this->erstelleFunFacts(); return; } $variableID = $this->findeUnterobjekt('EnergieFunFacts'); if ($variableID === 0) { return; } if ((int) IPS_GetObject($variableID)['ObjectType'] !== 2) { throw new RuntimeException('Unterobjekt EnergieFunFacts ist keine Variable.'); } $this->UnregisterVariable('EnergieFunFacts'); } private function synchronisiereEnergiefluss(bool $anzeigen): void { if ($anzeigen) { $this->erstelleEnergiefluss(); return; } $instanzID = $this->findeUnterobjekt('Energiefluss'); if ($instanzID !== 0) { $objekt = IPS_GetObject($instanzID); $modulID = $objekt['ObjectType'] === 1 ? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID'] : ''; if ($modulID !== self::ENERGY_FLOW_MODULE_ID) { throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.'); } IPS_SetHidden($instanzID, true); } $this->WriteAttributeString('EnergieflussKonfiguration', '{}'); $this->WriteAttributeBoolean('EnergieflussNeuErzeugen', true); } private function entferneInstanzUnterobjekt(string $ident, string $modulID): void { $instanzID = $this->findeUnterobjekt($ident); if ($instanzID === 0) { return; } $objekt = IPS_GetObject($instanzID); $istModulID = $objekt['ObjectType'] === 1 ? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID'] : ''; if ($istModulID !== $modulID) { throw new RuntimeException('Unterobjekt ' . $ident . ' hat einen unpassenden Typ.'); } IPS_DeleteInstance($instanzID); } private function entferneDiagrammUnterobjekt(string $ident): void { $medienID = $this->findeUnterobjekt($ident); if ($medienID === 0) { return; } $objekt = IPS_GetObject($medienID); if ($objekt['ObjectType'] !== 5 || (int) IPS_GetMedia($medienID)['MediaType'] !== 4) { throw new RuntimeException('Unterobjekt ' . $ident . ' ist kein Diagramm.'); } IPS_DeleteMedia($medienID, true); } private function erstelleEnergyPie(): string { if (!in_array(self::ENERGY_PIE_MODULE_ID, IPS_GetModuleList(), true)) { throw new RuntimeException( 'Das Modul Energy Pie aus Enelix Utils ist nicht installiert.' ); } $instanzID = $this->findeUnterobjekt('EnergyPie'); if ($instanzID === 0) { $instanzID = IPS_CreateInstance(self::ENERGY_PIE_MODULE_ID); IPS_SetParent($instanzID, $this->InstanceID); IPS_SetIdent($instanzID, 'EnergyPie'); } else { $objekt = IPS_GetObject($instanzID); $modulID = $objekt['ObjectType'] === 1 ? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID'] : ''; if ($modulID !== self::ENERGY_PIE_MODULE_ID) { throw new RuntimeException('Unterobjekt EnergyPie hat einen unpassenden Typ.'); } IPS_SetHidden($instanzID, false); $this->synchronisiereEnergyPieAnteile($instanzID); return 'Bestehende Energy-Pie-Messquellen und Darstellung bleiben unveraendert.'; } IPS_SetName($instanzID, 'Energy Pie'); IPS_SetPosition($instanzID, 500); IPS_SetHidden($instanzID, false); foreach ([ 'ProduktionZaehlerID' => $this->GetIDForIdent('PVEnergie'), 'EinspeisungZaehlerID' => $this->GetIDForIdent('EinspeisungEnergie'), 'NetzbezugZaehlerID' => $this->GetIDForIdent('NetzbezugEnergie'), 'VerbrauchZaehlerID' => $this->GetIDForIdent('Hausenergie'), 'Zaehlerfaktor' => 1.0, ] as $property => $wert) { IPS_SetProperty($instanzID, $property, $wert); } $this->synchronisiereEnergyPieAnteile($instanzID, false); IPS_ApplyChanges($instanzID); return 'Energy Pie wurde erstellt oder aktualisiert.'; } private function synchronisiereEnergyPieAnteile(int $instanzID, bool $anwenden = true): void { // Physischer Netzbezug enthaelt SDL, die Hausenergie dagegen nicht. $berechenbar = !$this->ReadPropertyBoolean('SDLAktiv'); $konfiguration = json_decode(IPS_GetConfiguration($instanzID), true, 512, JSON_THROW_ON_ERROR); if (!is_array($konfiguration) || !array_key_exists('EnergieanteileBerechenbar', $konfiguration)) { IPS_SetHidden($instanzID, !$berechenbar); if (!$berechenbar) { IPS_LogMessage('Enelix Manager ' . $this->InstanceID, 'Energy Pie ausgeblendet: Enelix Utils aktualisieren, damit bei aktiver SDL keine unbestimmbaren Energieanteile angezeigt werden.'); } return; } IPS_SetHidden($instanzID, false); if (IPS_GetProperty($instanzID, 'EnergieanteileBerechenbar') === $berechenbar) { return; } IPS_SetProperty($instanzID, 'EnergieanteileBerechenbar', $berechenbar); if ($anwenden) { IPS_ApplyChanges($instanzID); } } private function erstelleEnergiefluss(): string { if (!in_array(self::ENERGY_FLOW_MODULE_ID, IPS_GetModuleList(), true)) { throw new RuntimeException('Das Symcon-Modul Energy Distribution ist nicht installiert.'); } $neuErzeugen = $this->ReadAttributeBoolean('EnergieflussNeuErzeugen'); $instanzID = $this->findeUnterobjekt('Energiefluss'); if ($instanzID === 0) { $instanzID = IPS_CreateInstance(self::ENERGY_FLOW_MODULE_ID); IPS_SetParent($instanzID, $this->InstanceID); IPS_SetIdent($instanzID, 'Energiefluss'); } else { $objekt = IPS_GetObject($instanzID); $modulID = $objekt['ObjectType'] === 1 ? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID'] : ''; if ($modulID !== self::ENERGY_FLOW_MODULE_ID) { throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.'); } if (!$neuErzeugen) { $this->visualisierungsKonfiguration('EnergieflussKonfiguration'); $this->synchronisiereSDLEnergiefluss($instanzID); IPS_SetHidden($instanzID, false); $this->aktualisiereEnergieflusswerte(); return 'Bestehende Energiefluss-Konfiguration bleibt unveraendert.'; } } $this->visualisierungsKonfiguration('EnergieflussKonfiguration'); IPS_SetName($instanzID, 'Energiefluss'); IPS_SetPosition($instanzID, 540); IPS_SetHidden($instanzID, false); $profil = $this->energieflussLeistungsprofil(); $pvID = $this->energieflussLeistungsvariable( 'EnergieflussPV', 'Energiefluss PV', $profil, 600 ); $netzID = $this->energieflussLeistungsvariable( 'EnergieflussNetz', 'Energiefluss Netz', $profil, 610 ); $hausID = $this->energieflussLeistungsvariable( 'EnergieflussHaus', 'Energiefluss Hausverbrauch', $profil, 620 ); $batterieID = $this->energieflussLeistungsvariable( 'EnergieflussBatterie', 'Energiefluss Batteriespeicher', $profil, 630 ); $verbraucher = $this->energieflussVerbraucher(); $knoten = [ $this->energieflussKnoten($pvID, 'PV-Anlage', 'solar'), $this->energieflussKnoten($netzID, 'Stromnetz', 'grid'), $this->energieflussKnoten($hausID, 'Hausverbrauch', 'consumer'), ]; $position = 640; foreach ($verbraucher as $eintrag) { $istID = $this->energieflussLeistungsvariable( 'EnergieflussIst_' . $eintrag['InstanzID'], $eintrag['Name'] . ' Istleistung', $profil, $position ); $sollID = $this->energieflussLeistungsvariable( 'EnergieflussSoll_' . $eintrag['InstanzID'], $eintrag['Name'] . ' Sollleistung', $profil, $position + 1 ); $knoten[] = $this->energieflussKnoten( $istID, $eintrag['Name'], $eintrag['Typ'], true, $sollID ); $position += 10; } $knoten[] = $this->energieflussKnoten( $batterieID, 'Batteriespeicher', 'storage' ); IPS_SetProperty($instanzID, 'Calculation', true); IPS_SetProperty( $instanzID, 'Config', json_encode($knoten, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES) ); IPS_SetProperty($instanzID, 'EnableLimits', false); IPS_SetProperty($instanzID, 'Restrictions', '[]'); IPS_ApplyChanges($instanzID); $this->WriteAttributeBoolean('EnergieflussNeuErzeugen', false); $this->synchronisiereSDLEnergiefluss($instanzID, true); $this->aktualisiereEnergieflusswerte($verbraucher); return 'Der Energiefluss wurde erstellt oder aktualisiert.'; } /** @return array */ private function energieflussKnoten( int $variableID, string $name, string $typ, bool $gruppiert = false, int $infoVariableID = 1 ): array { return [ 'VariableConfigType' => 0, 'VariableID' => $variableID, 'SecondaryVariableID' => 1, 'Name' => $name, 'Type' => $typ, 'Grouped' => $gruppiert, 'CalculationType' => 0, 'InfoVariableID' => $infoVariableID, 'Invert' => false, ]; } private function energieflussLeistungsprofil(): string { $konfiguration = $this->visualisierungsKonfiguration( 'EnergieflussKonfiguration' ); if ($konfiguration['EnergieflussLeistungseinheit'] !== 1) { return '~Power'; } $profil = 'Enelix.Power.kW'; if (!IPS_VariableProfileExists($profil)) { IPS_CreateVariableProfile($profil, 2); IPS_SetVariableProfileValues($profil, -1000000.0, 1000000.0, 0.001); IPS_SetVariableProfileDigits($profil, 3); IPS_SetVariableProfileText($profil, '', ' kW'); } return $profil; } private function energieflussLeistungsvariable( string $ident, string $name, string $profil, int $position ): int { $this->RegisterVariableFloat($ident, $name, $profil, $position); $variableID = $this->GetIDForIdent($ident); IPS_SetHidden($variableID, true); return $variableID; } /** @return array */ private function visualisierungsKonfiguration(string $attribut): array { $konfiguration = $this->leseJsonAttribut($attribut); $gueltig = isset( $konfiguration['DiagrammPVModus'], $konfiguration['DiagrammBatterieModus'], $konfiguration['LadestationenSeparatAnzeigen'], $konfiguration['VerbraucherSeparatAnzeigen'], $konfiguration['EnergieflussLeistungseinheit'] ); if ($gueltig) { return $konfiguration; } $konfiguration = [ 'DiagrammPVModus' => $this->ReadPropertyInteger('DiagrammPVModus'), 'DiagrammBatterieModus' => $this->ReadPropertyInteger('DiagrammBatterieModus'), 'LadestationenSeparatAnzeigen' => $this->ReadPropertyBoolean('LadestationenSeparatAnzeigen'), 'VerbraucherSeparatAnzeigen' => $this->ReadPropertyBoolean('VerbraucherSeparatAnzeigen'), 'EnergieflussLeistungseinheit' => $this->ReadPropertyInteger('EnergieflussLeistungseinheit'), ]; $this->WriteAttributeString( $attribut, json_encode($konfiguration, JSON_THROW_ON_ERROR) ); return $konfiguration; } /** * @return list */ private function energieflussVerbraucher( string $konfigurationsAttribut = 'EnergieflussKonfiguration' ): array { $konfiguration = $this->visualisierungsKonfiguration($konfigurationsAttribut); $cache = $this->leseJsonAttribut('VerbraucherCache'); $energien = $this->leseJsonAttribut('VerbraucherEnergie'); $ergebnis = []; foreach ($this->konfigurierteAktiveVerbraucherIDs() as $instanzID) { if (!IPS_InstanceExists($instanzID)) { continue; } $daten = $cache[(string) $instanzID]['Daten'] ?? null; $istleistung = 0.0; $sollleistung = 0.0; if (is_array($daten)) { $istleistung = $this->verbraucherleistung($daten); $sollleistung = (float) (ManagerRegler::angenommenerSollwert($daten) ?? 0); } $modulID = strtoupper( (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID'] ); $katalog = self::VERBRAUCHER_LIZENZEN[$modulID]['Katalog'] ?? ''; if ($katalog === 'battery') { continue; } if ($katalog === 'ev_charger' && !$konfiguration['LadestationenSeparatAnzeigen'] ) { continue; } if ($katalog !== 'ev_charger' && !$konfiguration['VerbraucherSeparatAnzeigen'] ) { continue; } $verbraucherEnergie = $energien[(string) $instanzID]['Energie_Wh'] ?? 0.0; $ergebnis[] = [ 'InstanzID' => $instanzID, 'Name' => IPS_GetName($instanzID), 'Typ' => $katalog === 'ev_charger' ? 'wallbox' : 'consumer', 'Istleistung_W' => $istleistung, 'Sollleistung_W' => $sollleistung, 'Energie_Wh' => is_numeric($verbraucherEnergie) ? (float) $verbraucherEnergie : 0.0, ]; } return $ergebnis; } /** * @param list>|null $verbraucher */ private function aktualisiereEnergieflusswerte(?array $verbraucher = null): void { if (!$this->ReadPropertyBoolean('EnergieflussAnzeigen')) { return; } $verbraucher = $verbraucher ?? $this->energieflussVerbraucher(); $verbraucherleistung = 0.0; foreach ($verbraucher as $eintrag) { $this->setzeEnergieflussLeistungswert( 'EnergieflussIst_' . $eintrag['InstanzID'], (float) $eintrag['Istleistung_W'] ); $this->setzeEnergieflussLeistungswert( 'EnergieflussSoll_' . $eintrag['InstanzID'], (float) $eintrag['Sollleistung_W'] ); $verbraucherleistung += max(0.0, (float) $eintrag['Istleistung_W']); } try { $leistungen = $this->leseMessleistungen(); } catch (Throwable $fehler) { $this->protokolliere('Energiefluss', $fehler->getMessage()); return; } $this->setzeEnergieflussLeistungswert('EnergieflussPV', $leistungen['PV']); $this->setzeEnergieflussLeistungswert('EnergieflussNetz', $leistungen['Netz']); $this->setzeEnergieflussLeistungswert( 'EnergieflussHaus', max(0.0, $leistungen['Haus'] - $verbraucherleistung) ); $this->setzeEnergieflussLeistungswert( 'EnergieflussBatterie', $leistungen['Batterie'] + ($this->sdlAnzeigen('SDLEnergieflussAnzeigen') ? 0.0 : $leistungen['SDL']) ); $this->setzeEnergieflussLeistungswert('EnergieflussSDL', $leistungen['SDL']); $soc = $this->aktuelleSDLSOC(); if ($soc !== null) { $this->SetValue('SDLSOCArchiv', $soc); } $flussID = $this->findeUnterobjekt('Energiefluss'); if ($flussID > 0) { $this->synchronisiereSDLEnergiefluss($flussID); } } private function setzeEnergieflussLeistungswert(string $ident, float $watt): void { $variableID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); if (!is_int($variableID) || $variableID <= 0 || !IPS_VariableExists($variableID)) { return; } $konfiguration = $this->visualisierungsKonfiguration( 'EnergieflussKonfiguration' ); $faktor = $konfiguration['EnergieflussLeistungseinheit'] === 1 ? 0.001 : 1.0; SetValue($variableID, $watt * $faktor); } private function erstelleEnergiediagramme(): string { $konfiguration = $this->visualisierungsKonfiguration( 'EnergiediagrammKonfiguration' ); $pvAnlagen = $this->diagrammPVAnlagen(); $batterien = $this->diagrammBatterien(); $verbraucher = $this->energieflussVerbraucher( 'EnergiediagrammKonfiguration' ); $this->registriereDiagrammVariablen($pvAnlagen, $batterien, $verbraucher); $this->aktualisiereDiagrammMesswerte(true); $leistung = []; $energie = []; if ($konfiguration['DiagrammPVModus'] !== 1) { $leistung[] = $this->chartDatensatz( 'PVLeistungArchiv', 'PV gesamt', '#F4C542', '~Power' ); $energie[] = $this->chartDatensatz( 'PVEnergie', 'PV gesamt', '#F4C542', '~Electricity', 'bar' ); } if ($konfiguration['DiagrammPVModus'] !== 0) { foreach ($pvAnlagen as $index => $anlage) { $farbe = $this->diagrammFarbe($index); if ($anlage['LeistungVariableID'] > 0) { $leistung[] = $this->chartDatensatz( $anlage['LeistungIdent'], $anlage['Name'], $farbe, '~Power' ); } if ($anlage['EnergieVariableID'] > 0) { $energie[] = $this->chartDatensatz( $anlage['EnergieIdent'], $anlage['Name'], $farbe, '~Electricity', 'bar' ); } } } $hausTitel = $verbraucher === [] ? 'Haus' : 'Haus ohne separat dargestellte Verbraucher'; $leistung[] = $this->chartDatensatz( 'DiagrammHausLeistung', $hausTitel, '#22A06B', '~Power' ); $leistung[] = $this->chartDatensatz( 'NetzleistungArchiv', 'Netz', '#E95D5D', '~Power' ); $energie[] = $this->chartDatensatz( 'DiagrammHausEnergie', $hausTitel, '#22A06B', '~Electricity', 'bar' ); $energie[] = $this->chartDatensatz( 'NetzbezugEnergie', 'Netzbezug', '#E95D5D', '~Electricity', 'bar' ); $energie[] = $this->chartDatensatz( 'EinspeisungEnergie', 'Netzeinspeisung', '#2F80ED', '~Electricity', 'bar' ); if ($konfiguration['DiagrammBatterieModus'] !== 1) { $leistung[] = $this->chartDatensatz( 'BatterieleistungArchiv', 'Batterie gesamt', '#8B5CF6', '~Power' ); $energie[] = $this->chartDatensatz( 'BatterieLadenEnergie', 'Batterie laden gesamt', '#8B5CF6', '~Electricity', 'bar' ); $energie[] = $this->chartDatensatz( 'BatterieEntladenEnergie', 'Batterie entladen gesamt', '#F59E0B', '~Electricity', 'bar' ); } if ($konfiguration['DiagrammBatterieModus'] !== 0) { foreach ($batterien as $index => $batterie) { $farbe = $this->diagrammFarbe($index + count($pvAnlagen)); if ($batterie['LeistungVariableID'] > 0) { $leistung[] = $this->chartDatensatz( $batterie['LeistungIdent'], $batterie['Name'], $farbe, '~Power' ); } if ($batterie['LadeenergieVariableID'] > 0) { $energie[] = $this->chartDatensatz( $batterie['LadeenergieIdent'], $batterie['Name'] . ' laden', $farbe, '~Electricity', 'bar' ); } if ($batterie['EntladeenergieVariableID'] > 0) { $energie[] = $this->chartDatensatz( $batterie['EntladeenergieIdent'], $batterie['Name'] . ' entladen', $farbe, '~Electricity', 'bar' ); } } } foreach ($verbraucher as $index => $eintrag) { $farbe = $this->diagrammFarbe( $index + count($pvAnlagen) + count($batterien) ); $leistung[] = $this->chartDatensatz( 'DiagrammVerbraucherLeistung_' . $eintrag['InstanzID'], $eintrag['Name'], $farbe, '~Power' ); $energie[] = $this->chartDatensatz( 'DiagrammVerbraucherEnergie_' . $eintrag['InstanzID'], $eintrag['Name'], $farbe, '~Electricity', 'bar' ); } $leistungID = $this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510); $energieID = $this->erstelleDiagramm('Energiediagramm', 'Energien', $energie, 520); $this->synchronisiereSDLDiagramm($leistungID, false); $this->synchronisiereSDLDiagramm($energieID, true); return 'Die Diagramme Leistungen und Energien wurden erstellt oder aktualisiert.'; } /** @return list> */ private function diagrammPVAnlagen(): array { $verwendet = []; foreach ($this->leseAnlagenKonfigurationsliste('AnlagenPVFlaechen') as $flaeche) { $wechselrichterID = trim((string) ($flaeche['WechselrichterID'] ?? '')); if ($wechselrichterID !== '') { $verwendet[$wechselrichterID] = true; } } $ergebnis = []; foreach ($this->leseAnlagenKonfigurationsliste('AnlagenWechselrichter') as $eintrag) { $id = trim((string) ($eintrag['ID'] ?? '')); if ($id === '' || ($verwendet !== [] && !isset($verwendet[$id]))) { continue; } if ($verwendet === [] && !in_array((string) ($eintrag['Typ'] ?? ''), ['pv', 'hybrid'], true) ) { continue; } $suffix = substr(hash('sha256', 'pv:' . $id), 0, 12); $leistungID = (int) ($eintrag['IstleistungVariableID'] ?? 0); $energieID = (int) ($eintrag['ErzeugungsenergieVariableID'] ?? 0); $ergebnis[] = [ 'Name' => trim((string) ($eintrag['Name'] ?? '')) ?: $id, 'LeistungVariableID' => $this->istNumerischeVariable($leistungID) ? $leistungID : 0, 'Leistungsfaktor' => (float) ($eintrag['Istleistungsfaktor'] ?? 1.0), 'EnergieVariableID' => $this->istNumerischeVariable($energieID) ? $energieID : 0, 'Energiefaktor' => (float) ($eintrag['Erzeugungsenergiefaktor'] ?? 1.0), 'LeistungIdent' => 'DiagrammPVLeistung_' . $suffix, 'EnergieIdent' => 'DiagrammPVEnergie_' . $suffix, ]; } return $ergebnis; } /** @return list> */ private function diagrammBatterien(): array { $ergebnis = []; foreach ($this->leseAnlagenKonfigurationsliste('AnlagenBatterien') as $eintrag) { $id = trim((string) ($eintrag['ID'] ?? '')); if ($id === '') { continue; } $suffix = substr(hash('sha256', 'batterie:' . $id), 0, 12); $leistungID = (int) ($eintrag['LeistungVariableID'] ?? 0); $ladenID = (int) ($eintrag['LadeenergieVariableID'] ?? 0); $entladenID = (int) ($eintrag['EntladeenergieVariableID'] ?? 0); $ergebnis[] = [ 'Name' => trim((string) ($eintrag['Name'] ?? '')) ?: $id, 'LeistungVariableID' => $this->istNumerischeVariable($leistungID) ? $leistungID : 0, 'Leistungsfaktor' => (float) ($eintrag['Leistungsfaktor'] ?? 1.0), 'LadeenergieVariableID' => $this->istNumerischeVariable($ladenID) ? $ladenID : 0, 'EntladeenergieVariableID' => $this->istNumerischeVariable($entladenID) ? $entladenID : 0, 'Energiefaktor' => (float) ($eintrag['Energiefaktor'] ?? 1.0), 'LeistungIdent' => 'DiagrammBatterieLeistung_' . $suffix, 'LadeenergieIdent' => 'DiagrammBatterieLaden_' . $suffix, 'EntladeenergieIdent' => 'DiagrammBatterieEntladen_' . $suffix, ]; } return $ergebnis; } /** * @param list> $pvAnlagen * @param list> $batterien * @param list> $verbraucher */ private function registriereDiagrammVariablen( array $pvAnlagen, array $batterien, array $verbraucher ): void { $position = 800; $this->diagrammMessvariable( 'DiagrammHausLeistung', 'Diagramm Hausleistung', '~Power', $position ); $this->diagrammMessvariable( 'DiagrammHausEnergie', 'Diagramm Hausenergie', '~Electricity', $position + 1 ); $position += 10; foreach ($pvAnlagen as $anlage) { if ($anlage['LeistungVariableID'] > 0) { $this->diagrammMessvariable( $anlage['LeistungIdent'], $anlage['Name'] . ' Leistung', '~Power', $position ); } if ($anlage['EnergieVariableID'] > 0) { $this->diagrammMessvariable( $anlage['EnergieIdent'], $anlage['Name'] . ' Energie', '~Electricity', $position + 1 ); } $position += 10; } foreach ($batterien as $batterie) { if ($batterie['LeistungVariableID'] > 0) { $this->diagrammMessvariable( $batterie['LeistungIdent'], $batterie['Name'] . ' Leistung', '~Power', $position ); } if ($batterie['LadeenergieVariableID'] > 0) { $this->diagrammMessvariable( $batterie['LadeenergieIdent'], $batterie['Name'] . ' Ladeenergie', '~Electricity', $position + 1 ); } if ($batterie['EntladeenergieVariableID'] > 0) { $this->diagrammMessvariable( $batterie['EntladeenergieIdent'], $batterie['Name'] . ' Entladeenergie', '~Electricity', $position + 2 ); } $position += 10; } foreach ($verbraucher as $eintrag) { $this->diagrammMessvariable( 'DiagrammVerbraucherLeistung_' . $eintrag['InstanzID'], $eintrag['Name'] . ' Leistung', '~Power', $position ); $this->diagrammMessvariable( 'DiagrammVerbraucherEnergie_' . $eintrag['InstanzID'], $eintrag['Name'] . ' Energie', '~Electricity', $position + 1 ); $position += 10; } } private function diagrammMessvariable( string $ident, string $name, string $profil, int $position ): int { $this->RegisterVariableFloat($ident, $name, $profil, $position); $variableID = $this->GetIDForIdent($ident); IPS_SetHidden($variableID, true); return $variableID; } private function aktualisiereDiagrammMesswerte(bool $energieAktualisieren = false): void { if ($this->findeUnterobjekt('Leistungsdiagramm') === 0 && $this->findeUnterobjekt('Energiediagramm') === 0 && $this->findeUnterobjekt('DiagrammHausLeistung') === 0 ) { return; } try { $leistungen = $this->leseMessleistungen(); $pvAnlagen = $this->diagrammPVAnlagen(); $batterien = $this->diagrammBatterien(); $verbraucher = $this->energieflussVerbraucher( 'EnergiediagrammKonfiguration' ); } catch (Throwable $fehler) { $this->protokolliere('Energiediagramme', $fehler->getMessage()); return; } $separateLeistung = 0.0; $separateEnergieKWh = 0.0; foreach ($verbraucher as $eintrag) { $leistung = max(0.0, (float) $eintrag['Istleistung_W']); $separateLeistung += $leistung; $separateEnergieKWh += max(0.0, (float) $eintrag['Energie_Wh']) / 1000.0; $this->setzeDiagrammMesswert( 'DiagrammVerbraucherLeistung_' . $eintrag['InstanzID'], $leistung ); if ($energieAktualisieren) { $this->setzeDiagrammMesswert( 'DiagrammVerbraucherEnergie_' . $eintrag['InstanzID'], max(0.0, (float) $eintrag['Energie_Wh']) / 1000.0 ); } } $this->setzeDiagrammMesswert( 'DiagrammHausLeistung', max(0.0, $leistungen['Haus'] - $separateLeistung) ); if ($energieAktualisieren) { $this->setzeDiagrammMesswert( 'DiagrammHausEnergie', max( 0.0, (float) $this->GetValue('Hausenergie') - $separateEnergieKWh ) ); } foreach ($pvAnlagen as $anlage) { if ($anlage['LeistungVariableID'] > 0) { $this->setzeDiagrammMesswert( $anlage['LeistungIdent'], max( 0.0, (float) GetValue($anlage['LeistungVariableID']) * $anlage['Leistungsfaktor'] ) ); } if ($energieAktualisieren && $anlage['EnergieVariableID'] > 0) { $this->setzeDiagrammMesswert( $anlage['EnergieIdent'], max( 0.0, (float) GetValue($anlage['EnergieVariableID']) * $anlage['Energiefaktor'] ) ); } } foreach ($batterien as $batterie) { if ($batterie['LeistungVariableID'] > 0) { $this->setzeDiagrammMesswert( $batterie['LeistungIdent'], (float) GetValue($batterie['LeistungVariableID']) * $batterie['Leistungsfaktor'] ); } if ($energieAktualisieren && $batterie['LadeenergieVariableID'] > 0) { $this->setzeDiagrammMesswert( $batterie['LadeenergieIdent'], max( 0.0, (float) GetValue($batterie['LadeenergieVariableID']) * $batterie['Energiefaktor'] ) ); } if ($energieAktualisieren && $batterie['EntladeenergieVariableID'] > 0) { $this->setzeDiagrammMesswert( $batterie['EntladeenergieIdent'], max( 0.0, (float) GetValue($batterie['EntladeenergieVariableID']) * $batterie['Energiefaktor'] ) ); } } } private function setzeDiagrammMesswert(string $ident, float $wert): void { $variableID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); if (is_int($variableID) && $variableID > 0 && IPS_VariableExists($variableID)) { SetValue($variableID, $wert); } } private function diagrammFarbe(int $index): string { $farben = [ '#A85D3C', '#007C91', '#6B5B95', '#2E7D32', '#C43E1C', '#3D5A80', '#8D6E63', '#00897B', ]; return $farben[$index % count($farben)]; } /** @param list> $datensaetze */ private function erstelleDiagramm( string $ident, string $name, array $datensaetze, int $position ): int { $medienID = $this->findeUnterobjekt($ident); $neu = $medienID === 0; if ($neu) { $medienID = IPS_CreateMedia(4); IPS_SetParent($medienID, $this->InstanceID); IPS_SetIdent($medienID, $ident); } $objekt = IPS_GetObject($medienID); if ($objekt['ObjectType'] !== 5 || (int) IPS_GetMedia($medienID)['MediaType'] !== 4) { throw new RuntimeException('Unterobjekt ' . $ident . ' ist kein Diagramm.'); } if (!$neu) { IPS_SetHidden($medienID, false); return $medienID; } IPS_SetName($medienID, $name); IPS_SetPosition($medienID, $position); IPS_SetHidden($medienID, false); IPS_SetMediaFile($medienID, 'media/' . $medienID . '.chart', false); IPS_SetMediaContent( $medienID, base64_encode(json_encode(['datasets' => $datensaetze], JSON_THROW_ON_ERROR)) ); return $medienID; } /** @return array */ private function chartDatensatz( string $ident, string $titel, string $farbe, string $profil, string $typ = 'line' ): array { return [ 'variableID' => $this->GetIDForIdent($ident), 'fillColor' => $typ === 'bar' ? $farbe : 'clear', 'strokeColor' => $farbe, 'timeOffset' => 0, 'visible' => true, 'title' => $titel, 'type' => $typ, 'side' => 'left', 'profile' => $profil, ]; } private function findeUnterobjekt(string $ident): int { $objektID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); return is_int($objektID) && $objektID > 0 ? $objektID : 0; } private function erstelleFunFacts(): string { $this->RegisterVariableString('EnergieFunFacts', 'Energy Facts', '~HTMLBox', 530); $variableID = $this->GetIDForIdent('EnergieFunFacts'); IPS_SetHidden($variableID, false); $this->aktualisiereFunFacts($variableID); return 'Energy Facts wurden erstellt oder aktualisiert.'; } private function aktualisiereFunFacts(int $variableID): void { $archivID = $this->archivID(); if ($archivID === 0) { SetValue($variableID, '
Archiv nicht verfuegbar.
'); return; } $heute = strtotime('today'); $jahr = mktime(0, 0, 0, 1, 1, (int) date('Y')); $jetzt = time(); $pvID = $this->GetIDForIdent('PVEnergie'); $hausID = $this->GetIDForIdent('Hausenergie'); $bezugID = $this->GetIDForIdent('NetzbezugEnergie'); $exportID = $this->GetIDForIdent('EinspeisungEnergie'); $pvHeute = $this->energieSumme($archivID, $pvID, $heute, $jetzt); $pvJahr = $this->energieSumme($archivID, $pvID, $jahr, $jetzt); $pvTotal = max(0.0, (float) GetValue($pvID)); [$besterTag, $besterTagZeit] = $this->maximalerTageswert($archivID, $pvID); [$maxPV, $maxPVZeit] = $this->maximaleLeistung( $archivID, $this->GetIDForIdent('PVLeistungArchiv') ); [$maxHausTag, $maxHausTagZeit] = $this->maximalerTageswert($archivID, $hausID); [$maxHaus, $maxHausZeit] = $this->maximaleLeistung( $archivID, $this->GetIDForIdent('HausverbrauchLeistungArchiv') ); [$maxBezug, $maxBezugZeit] = $this->maximalerTageswert($archivID, $bezugID); [$maxExport, $maxExportZeit] = $this->maximalerTageswert($archivID, $exportID); $werte = [ '{{PV_HEUTE}}' => $this->formatiereEnergie($pvHeute), '{{PV_JAHR}}' => $this->formatiereEnergie($pvJahr), '{{PV_TOTAL}}' => $this->formatiereEnergie($pvTotal), '{{BESTER_TAG}}' => $this->formatiereEnergie($besterTag), '{{BESTER_TAG_DATUM}}' => $this->formatiereDatum($besterTagZeit), '{{MAX_PV}}' => $this->formatiereLeistung($maxPV), '{{MAX_PV_DATUM}}' => $this->formatiereDatum($maxPVZeit), '{{MAX_HAUS_TAG}}' => $this->formatiereEnergie($maxHausTag), '{{MAX_HAUS_TAG_DATUM}}' => $this->formatiereDatum($maxHausTagZeit), '{{MAX_HAUS}}' => $this->formatiereLeistung($maxHaus), '{{MAX_HAUS_DATUM}}' => $this->formatiereDatum($maxHausZeit), '{{MAX_BEZUG}}' => $this->formatiereEnergie($maxBezug), '{{MAX_BEZUG_DATUM}}' => $this->formatiereDatum($maxBezugZeit), '{{MAX_EXPORT}}' => $this->formatiereEnergie($maxExport), '{{MAX_EXPORT_DATUM}}' => $this->formatiereDatum($maxExportZeit), '{{HEIZOEL}}' => number_format($pvTotal / 10.0, 0, ',', "'") . ' Liter', '{{FLUGKM}}' => number_format($pvTotal * 2.4, 0, ',', "'") . ' km', ]; SetValue($variableID, strtr($this->funFactsHtml(), $werte)); } private function energieSumme(int $archivID, int $variableID, int $start, int $ende): float { $werte = @AC_GetAggregatedValues($archivID, $variableID, 1, $start, $ende, 0); if (!is_array($werte)) { return 0.0; } return array_sum(array_map( static fn (array $wert): float => max(0.0, (float) ($wert['Avg'] ?? 0.0)), $werte )); } /** @return array{0: float, 1: int} */ private function maximalerTageswert(int $archivID, int $variableID): array { $werte = @AC_GetAggregatedValues($archivID, $variableID, 1, 0, time(), 0); $maximum = 0.0; $zeit = 0; if (is_array($werte)) { foreach ($werte as $wert) { $kandidat = max(0.0, (float) ($wert['Avg'] ?? 0.0)); if ($kandidat > $maximum) { $maximum = $kandidat; $zeit = (int) ($wert['TimeStamp'] ?? 0); } } } return [$maximum, $zeit]; } /** @return array{0: float, 1: int} */ private function maximaleLeistung(int $archivID, int $variableID): array { $werte = @AC_GetAggregatedValues($archivID, $variableID, 1, 0, time(), 0); $maximum = 0.0; $zeit = 0; if (is_array($werte)) { foreach ($werte as $wert) { $kandidat = max(0.0, (float) ($wert['Max'] ?? 0.0)); if ($kandidat > $maximum) { $maximum = $kandidat; $zeit = (int) ($wert['MaxTime'] ?? $wert['TimeStamp'] ?? 0); } } } return [$maximum, $zeit]; } private function formatiereEnergie(float $wert): string { if ($wert >= 1000.0) { return number_format($wert / 1000.0, 2, ',', "'") . ' MWh'; } return number_format($wert, 1, ',', "'") . ' kWh'; } private function formatiereLeistung(float $wert): string { if ($wert >= 10000.0) { return number_format($wert / 1000.0, 1, ',', "'") . ' kW'; } return number_format($wert, 0, ',', "'") . ' W'; } private function formatiereDatum(int $zeit): string { return $zeit > 0 ? date('d.m.Y', $zeit) : 'Noch keine Daten'; } private function funFactsHtml(): string { return <<<'HTML'
SolarERZEUGUNG
Heute{{PV_HEUTE}}
Dieses Jahr{{PV_JAHR}}
Total{{PV_TOTAL}}
Bester Tag{{BESTER_TAG}}{{BESTER_TAG_DATUM}}
Max. Leistung{{MAX_PV}}{{MAX_PV_DATUM}}
Netz & HausSPITZENWERTE
Max. Tagesverbrauch{{MAX_HAUS_TAG}}{{MAX_HAUS_TAG_DATUM}}
Max. Hauslast{{MAX_HAUS}}{{MAX_HAUS_DATUM}}
Max. Tagesbezug{{MAX_BEZUG}}{{MAX_BEZUG_DATUM}}
Max. Einspeisung{{MAX_EXPORT}}{{MAX_EXPORT_DATUM}}
WirkungVERGLEICHE
{{HEIZOEL}}
rechnerisch kompensiertes Heizoel
{{FLUGKM}}
rechnerisch kompensierte Flugkilometer
Vergleichswerte auf Basis von 10 kWh pro Liter Heizoel und 2,4 Flugkilometern pro erzeugter kWh.
HTML; } }