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); AC_SetLoggingStatus($archivID, $variableID, $aktiv); AC_SetAggregationType($archivID, $variableID, 0); AC_SetGraphStatus($archivID, $variableID, $aktiv); $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); AC_SetLoggingStatus($archivID, $variableID, $aktiv); 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; } } IPS_ApplyChanges($archivID); foreach (array_unique($geaendert) as $variableID) { AC_ReAggregateVariable($archivID, $variableID); } } /** @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'); } 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) { return; } $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); } 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_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); } IPS_ApplyChanges($instanzID); return 'Energy Pie wurde erstellt oder aktualisiert.'; } 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.'); } $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.'); } } IPS_SetName($instanzID, 'Energiefluss'); IPS_SetPosition($instanzID, 540); IPS_SetHidden($instanzID, false); $knoten = [ $this->energieflussKnoten('PVLeistungArchiv', 'PV-Anlage', 'solar'), $this->energieflussKnoten('NetzleistungArchiv', 'Stromnetz', 'grid'), $this->energieflussKnoten('HausverbrauchLeistungArchiv', 'Hausverbrauch', 'consumer'), $this->energieflussKnoten('BatterieleistungArchiv', 'Batteriespeicher', 'battery'), ]; 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); return 'Der Energiefluss wurde erstellt oder aktualisiert.'; } /** @return array */ private function energieflussKnoten(string $ident, string $name, string $typ): array { return [ 'VariableConfigType' => 0, 'VariableID' => $this->GetIDForIdent($ident), 'SecondaryVariableID' => 1, 'Name' => $name, 'Type' => $typ, 'Grouped' => false, 'CalculationType' => 0, 'InfoVariableID' => 1, 'Invert' => false, ]; } private function erstelleEnergiediagramme(): string { $leistung = [ $this->chartDatensatz('PVLeistungArchiv', 'PV', '#F4C542', '~Power'), $this->chartDatensatz('HausverbrauchLeistungArchiv', 'Haus', '#22A06B', '~Power'), $this->chartDatensatz('NetzleistungArchiv', 'Netz', '#E95D5D', '~Power'), $this->chartDatensatz('BatterieleistungArchiv', 'Batterie', '#8B5CF6', '~Power'), ]; $energie = [ $this->chartDatensatz('PVEnergie', 'PV', '#F4C542', '~Electricity'), $this->chartDatensatz('Hausenergie', 'Haus', '#22A06B', '~Electricity'), $this->chartDatensatz('NetzbezugEnergie', 'Netzbezug', '#E95D5D', '~Electricity'), $this->chartDatensatz('EinspeisungEnergie', 'Einspeisung', '#2F80ED', '~Electricity'), $this->chartDatensatz('BatterieLadenEnergie', 'Batterie laden', '#8B5CF6', '~Electricity'), $this->chartDatensatz('BatterieEntladenEnergie', 'Batterie entladen', '#F59E0B', '~Electricity'), ]; $this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510); $this->erstelleDiagramm('Energiediagramm', 'Energien', $energie, 520); return 'Die Diagramme Leistungen und Energien wurden erstellt oder aktualisiert.'; } /** @param list> $datensaetze */ private function erstelleDiagramm( string $ident, string $name, array $datensaetze, int $position ): int { $medienID = $this->findeUnterobjekt($ident); if ($medienID === 0) { $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.'); } 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 ): array { return [ 'variableID' => $this->GetIDForIdent($ident), 'fillColor' => 'clear', 'strokeColor' => $farbe, 'timeOffset' => 0, 'visible' => true, 'title' => $titel, 'type' => 'line', '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; } }