RegisterPropertyInteger('ProduktionZaehlerID', 0); $this->RegisterPropertyInteger('EinspeisungZaehlerID', 0); $this->RegisterPropertyInteger('NetzbezugZaehlerID', 0); $this->RegisterPropertyInteger('VerbrauchZaehlerID', 0); $this->RegisterPropertyFloat('Zaehlerfaktor', 1.0); $this->RegisterPropertyString('Standardzeitraum', 'day'); $this->RegisterPropertyInteger('Aktualisierungsintervall', 60); $this->RegisterPropertyString('FarbeEigenverbrauch', self::COLOR_SELF_CONSUMPTION); $this->RegisterPropertyString('FarbeAutarkie', self::COLOR_AUTARKY); $this->RegisterPropertyBoolean('Debug', false); $today = date('Y-m-d'); $this->RegisterAttributeString(self::ATTR_RANGE, 'day'); $this->RegisterAttributeString(self::ATTR_DATE, $today); $this->RegisterAttributeString(self::ATTR_CUSTOM_START, $today); $this->RegisterAttributeString(self::ATTR_CUSTOM_END, $today); $visualizationType = version_compare(IPS_GetKernelVersion(), '9.0', '>=') ? 2 : 1; $this->SetVisualizationType($visualizationType); $this->RegisterTimer( 'Refresh', 0, 'IPS_RequestAction($_IPS["TARGET"], "Refresh", true);' ); } public function ApplyChanges(): void { parent::ApplyChanges(); $today = date('Y-m-d'); $defaultRange = $this->ReadPropertyString('Standardzeitraum'); if (!in_array($defaultRange, ['day', 'week', 'month', 'year', 'total'], true)) { $defaultRange = 'day'; } $this->WriteAttributeString(self::ATTR_RANGE, $defaultRange); $this->WriteAttributeString(self::ATTR_DATE, $today); if (!$this->validStoredDate($this->ReadAttributeString(self::ATTR_CUSTOM_START))) { $this->WriteAttributeString(self::ATTR_CUSTOM_START, $today); } if (!$this->validStoredDate($this->ReadAttributeString(self::ATTR_CUSTOM_END))) { $this->WriteAttributeString(self::ATTR_CUSTOM_END, $today); } $interval = $this->ReadPropertyInteger('Aktualisierungsintervall'); $this->SetTimerInterval('Refresh', max(10, min(3600, $interval)) * 1000); if ($this->requiredSourceIdsAreEmpty()) { $this->SetStatus(104); } elseif (!$this->configurationIsValid()) { $this->SetStatus(201); } elseif ($this->archiveId() === 0) { $this->SetStatus(202); } else { $this->SetStatus(102); } $this->recalculateAndPush(); } public function GetVisualizationTile(): string { $path = __DIR__ . '/module.html'; $html = @file_get_contents($path); if ($html === false) { return '
Darstellung nicht verfügbar.
'; } try { $initial = json_encode( $this->buildPayload(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR ); } catch (Throwable $error) { $initial = '{"hasData":false,"noDataHint":"Darstellung konnte nicht geladen werden."}'; } return str_replace('__ENELIX_INITIAL_STATE__', $initial, $html); } public function GetVisualizationPopup(): string { return $this->GetVisualizationTile(); } public function RequestAction($Ident, $Value): void { try { switch ((string) $Ident) { case 'SetRange': $this->setRange((string) $Value); break; case 'SetDate': $this->setDate((string) $Value); break; case 'SetCustomRange': $this->setCustomRange((string) $Value); break; case 'Prev': case 'Next': case 'Today': $this->shiftDate((string) $Ident); break; case 'Refresh': break; default: throw new InvalidArgumentException('Unbekannte Aktion.'); } $this->recalculateAndPush(); } catch (Throwable $error) { $this->debug('Aktion abgewiesen', $error->getMessage()); } } private function setRange(string $range): void { if (!in_array($range, EnergyChartCalculator::RANGES, true)) { throw new InvalidArgumentException('Unbekannter Zeitraum.'); } $this->WriteAttributeString(self::ATTR_RANGE, $range); } private function setDate(string $date): void { if (!$this->validStoredDate($date)) { throw new InvalidArgumentException('Ungültiges oder zukünftiges Datum.'); } $this->WriteAttributeString(self::ATTR_DATE, $date); } private function setCustomRange(string $json): void { try { $value = json_decode($json, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { throw new InvalidArgumentException('Ungültiger Datumsbereich.', 0, $error); } $start = is_array($value) ? (string) ($value['start'] ?? '') : ''; $end = is_array($value) ? (string) ($value['end'] ?? '') : ''; if (!$this->validStoredDate($start) || !$this->validStoredDate($end)) { throw new InvalidArgumentException('Ungültiger oder zukünftiger Datumsbereich.'); } if ($start > $end) { throw new InvalidArgumentException('Das Startdatum liegt nach dem Enddatum.'); } $this->WriteAttributeString(self::ATTR_CUSTOM_START, $start); $this->WriteAttributeString(self::ATTR_CUSTOM_END, $end); $this->WriteAttributeString(self::ATTR_RANGE, 'custom'); } private function shiftDate(string $action): void { $range = $this->ReadAttributeString(self::ATTR_RANGE); if ($range === 'total' || $range === 'custom') { return; } if ($action === 'Today') { $this->WriteAttributeString(self::ATTR_DATE, date('Y-m-d')); return; } $date = $this->ReadAttributeString(self::ATTR_DATE); $base = strtotime($date . ' 00:00:00'); if ($base === false) { $base = strtotime(date('Y-m-d') . ' 00:00:00'); } $previous = $action === 'Prev'; $modifier = [ 'day' => $previous ? '-1 day' : '+1 day', 'week' => $previous ? '-1 week' : '+1 week', 'month' => $previous ? 'first day of previous month' : 'first day of next month', 'year' => $previous ? 'first day of january previous year' : 'first day of january next year', ][$range] ?? 'today'; $shifted = strtotime($modifier, $base); if ($shifted === false || $shifted > time()) { $shifted = time(); } $this->WriteAttributeString(self::ATTR_DATE, date('Y-m-d', $shifted)); } private function recalculateAndPush(): void { try { $payload = $this->buildPayload(); $this->UpdateVisualizationValue(json_encode($payload, JSON_THROW_ON_ERROR)); } catch (Throwable $error) { $this->debug('Aktualisierung fehlgeschlagen', $error->getMessage()); } } /** * @return array */ private function buildPayload(): array { $range = $this->ReadAttributeString(self::ATTR_RANGE); if (!in_array($range, EnergyChartCalculator::RANGES, true)) { $range = $this->ReadPropertyString('Standardzeitraum'); } if (!in_array($range, EnergyChartCalculator::RANGES, true)) { $range = 'day'; } $date = $this->ReadAttributeString(self::ATTR_DATE); if (!$this->validStoredDate($date)) { $date = date('Y-m-d'); } $customStart = $this->ReadAttributeString(self::ATTR_CUSTOM_START); $customEnd = $this->ReadAttributeString(self::ATTR_CUSTOM_END); if (!$this->validStoredDate($customStart)) { $customStart = date('Y-m-d'); } if (!$this->validStoredDate($customEnd)) { $customEnd = date('Y-m-d'); } [$start, $end] = EnergyChartCalculator::rangeBounds( $range, $date, $customStart, $customEnd ); $payload = [ 'range' => $range, 'date' => $date, 'customStart' => $customStart, 'customEnd' => $customEnd, 'tStart' => $start, 'tEnd' => $end, 'hasData' => false, 'noDataHint' => 'Energiezähler konfigurieren.', 'colors' => [ 'selfConsumption' => EnergyChartCalculator::normalizeColor( $this->ReadPropertyString('FarbeEigenverbrauch'), self::COLOR_SELF_CONSUMPTION ), 'autarky' => EnergyChartCalculator::normalizeColor( $this->ReadPropertyString('FarbeAutarkie'), self::COLOR_AUTARKY ), ], 'values' => EnergyChartCalculator::metrics(0.0, 0.0, 0.0), ]; if (!$this->configurationIsValid()) { return $payload; } $archiveId = $this->archiveId(); if ($archiveId === 0) { $payload['noDataHint'] = 'Das IP-Symcon-Archiv ist nicht verfügbar.'; return $payload; } $factor = $this->ReadPropertyFloat('Zaehlerfaktor'); $total = $range === 'total'; $production = $this->readEnergy( $archiveId, $this->ReadPropertyInteger('ProduktionZaehlerID'), $start, $end, $total ); $feedIn = $this->readEnergy( $archiveId, $this->ReadPropertyInteger('EinspeisungZaehlerID'), $start, $end, $total ); $grid = $this->readEnergy( $archiveId, $this->ReadPropertyInteger('NetzbezugZaehlerID'), $start, $end, $total ); $consumption = null; $consumptionAvailable = true; $consumptionId = $this->ReadPropertyInteger('VerbrauchZaehlerID'); if ($consumptionId > 0) { $consumptionResult = $this->readEnergy($archiveId, $consumptionId, $start, $end, $total); $consumptionAvailable = $consumptionResult['available']; if ($consumptionAvailable) { $consumption = $consumptionResult['value'] * $factor; } } $payload['hasData'] = $production['available'] && $feedIn['available'] && $grid['available'] && $consumptionAvailable; $payload['values'] = EnergyChartCalculator::metrics( $production['value'] * $factor, $feedIn['value'] * $factor, $grid['value'] * $factor, $consumption ); if (!$payload['hasData']) { $lastLog = max( $this->lastLogTimestamp($archiveId, $this->ReadPropertyInteger('ProduktionZaehlerID')), $this->lastLogTimestamp($archiveId, $this->ReadPropertyInteger('EinspeisungZaehlerID')), $this->lastLogTimestamp($archiveId, $this->ReadPropertyInteger('NetzbezugZaehlerID')), $this->lastLogTimestamp($archiveId, $consumptionId) ); $payload['noDataHint'] = $lastLog > 0 ? 'Letzter Archivwert: ' . date('d.m.Y H:i', $lastLog) : 'Für diesen Zeitraum sind keine Archivwerte vorhanden.'; } else { $payload['noDataHint'] = ''; } return $payload; } /** * @return array{value: float, available: bool} */ private function readEnergy( int $archiveId, int $variableId, int $start, int $end, bool $total ): array { if (!$this->isNumericVariable($variableId)) { return ['value' => 0.0, 'available' => false]; } if ($total) { $value = GetValue($variableId); return [ 'value' => is_numeric($value) ? max(0.0, (float) $value) : 0.0, 'available' => is_numeric($value), ]; } $before = @AC_GetLoggedValues($archiveId, $variableId, 0, $start, 1); $within = @AC_GetLoggedValues($archiveId, $variableId, $start, min($end, time()), 0); $before = is_array($before) ? $before : []; $within = is_array($within) ? $within : []; usort($before, static fn (array $left, array $right): int => ((int) $left['TimeStamp']) <=> ((int) $right['TimeStamp']) ); usort($within, static fn (array $left, array $right): int => ((int) $left['TimeStamp']) <=> ((int) $right['TimeStamp']) ); if ($before === [] && $within === []) { return ['value' => 0.0, 'available' => false]; } $startValue = $before !== [] ? (float) $before[count($before) - 1]['Value'] : (float) $within[0]['Value']; $endValue = $within !== [] ? (float) $within[count($within) - 1]['Value'] : $startValue; return [ 'value' => max(0.0, $endValue - $startValue), 'available' => true, ]; } private function lastLogTimestamp(int $archiveId, int $variableId): int { if (!$this->isNumericVariable($variableId)) { return 0; } $values = @AC_GetLoggedValues($archiveId, $variableId, 0, time(), 1); return is_array($values) && isset($values[0]['TimeStamp']) ? (int) $values[0]['TimeStamp'] : 0; } private function configurationIsValid(): bool { $requiredIds = [ $this->ReadPropertyInteger('ProduktionZaehlerID'), $this->ReadPropertyInteger('EinspeisungZaehlerID'), $this->ReadPropertyInteger('NetzbezugZaehlerID'), ]; foreach ($requiredIds as $variableId) { if (!$this->isNumericVariable($variableId)) { return false; } } $consumptionId = $this->ReadPropertyInteger('VerbrauchZaehlerID'); if ($consumptionId > 0 && !$this->isNumericVariable($consumptionId)) { return false; } return $this->ReadPropertyFloat('Zaehlerfaktor') > 0.0 && $this->ReadPropertyInteger('Aktualisierungsintervall') >= 10 && $this->ReadPropertyInteger('Aktualisierungsintervall') <= 3600 && in_array( $this->ReadPropertyString('Standardzeitraum'), ['day', 'week', 'month', 'year', 'total'], true ) && EnergyChartCalculator::normalizeColor( $this->ReadPropertyString('FarbeEigenverbrauch'), '' ) !== '' && EnergyChartCalculator::normalizeColor( $this->ReadPropertyString('FarbeAutarkie'), '' ) !== ''; } private function requiredSourceIdsAreEmpty(): bool { return $this->ReadPropertyInteger('ProduktionZaehlerID') === 0 && $this->ReadPropertyInteger('EinspeisungZaehlerID') === 0 && $this->ReadPropertyInteger('NetzbezugZaehlerID') === 0; } private function archiveId(): int { $archives = IPS_GetInstanceListByModuleID(self::ARCHIVE_MODULE_ID); return isset($archives[0]) ? (int) $archives[0] : 0; } private function isNumericVariable(int $variableId): bool { if ($variableId <= 0 || !IPS_VariableExists($variableId)) { return false; } $variable = IPS_GetVariable($variableId); return in_array((int) ($variable['VariableType'] ?? -1), [1, 2], true); } private function validStoredDate(string $date): bool { return EnergyChartCalculator::isValidDate($date) && $date <= date('Y-m-d'); } private function debug(string $title, string $message): void { if ($this->ReadPropertyBoolean('Debug')) { $this->SendDebug($title, $message, 0); } } }