diff --git a/Energy_Pie/README.md b/Energy_Pie/README.md new file mode 100644 index 0000000..e3abe65 --- /dev/null +++ b/Energy_Pie/README.md @@ -0,0 +1,67 @@ +# Manager_1 +Beschreibung des Moduls. + +### Inhaltsverzeichnis + +1. [Funktionsumfang](#1-funktionsumfang) +2. [Voraussetzungen](#2-voraussetzungen) +3. [Software-Installation](#3-software-installation) +4. [Einrichten der Instanzen in IP-Symcon](#4-einrichten-der-instanzen-in-ip-symcon) +5. [Statusvariablen und Profile](#5-statusvariablen-und-profile) +6. [WebFront](#6-webfront) +7. [PHP-Befehlsreferenz](#7-php-befehlsreferenz) + +### 1. Funktionsumfang + +* + +### 2. Voraussetzungen + +- IP-Symcon ab Version 7.1 + +### 3. Software-Installation + +* Über den Module Store das 'Manager_1'-Modul installieren. +* Alternativ über das Module Control folgende URL hinzufügen + +### 4. Einrichten der Instanzen in IP-Symcon + + Unter 'Instanz hinzufügen' kann das 'Manager_1'-Modul mithilfe des Schnellfilters gefunden werden. + - Weitere Informationen zum Hinzufügen von Instanzen in der [Dokumentation der Instanzen](https://www.symcon.de/service/dokumentation/konzepte/instanzen/#Instanz_hinzufügen) + +__Konfigurationsseite__: + +Name | Beschreibung +-------- | ------------------ + | + | + +### 5. Statusvariablen und Profile + +Die Statusvariablen/Kategorien werden automatisch angelegt. Das Löschen einzelner kann zu Fehlfunktionen führen. + +#### Statusvariablen + +Name | Typ | Beschreibung +------ | ------- | ------------ + | | + | | + +#### Profile + +Name | Typ +------ | ------- + | + | + +### 6. WebFront + +Die Funktionalität, die das Modul im WebFront bietet. + +### 7. PHP-Befehlsreferenz + +`boolean GEF_BeispielFunktion(integer $InstanzID);` +Erklärung der Funktion. + +Beispiel: +`GEF_BeispielFunktion(12345);` \ No newline at end of file diff --git a/Energy_Pie/form.json b/Energy_Pie/form.json new file mode 100644 index 0000000..a8b8127 --- /dev/null +++ b/Energy_Pie/form.json @@ -0,0 +1,8 @@ +{ + "elements": [ + { "type": "SelectVariable", "name": "VarProduction", "caption": "Produktion (kWh)" }, + { "type": "SelectVariable", "name": "VarConsumption", "caption": "Verbrauch (kWh)" }, + { "type": "SelectVariable", "name": "VarFeedIn", "caption": "Einspeisung (kWh)" }, + { "type": "SelectVariable", "name": "VarGrid", "caption": "Bezug Netz (kWh)" } + ] +} \ No newline at end of file diff --git a/Energy_Pie/module.html b/Energy_Pie/module.html new file mode 100644 index 0000000..fd5f16f --- /dev/null +++ b/Energy_Pie/module.html @@ -0,0 +1,217 @@ +
+ + +
+ + + + + + + +
+ +
+
+ +
+
+ +
+
+ + + + diff --git a/Energy_Pie/module.json b/Energy_Pie/module.json new file mode 100644 index 0000000..8d3dea1 --- /dev/null +++ b/Energy_Pie/module.json @@ -0,0 +1,13 @@ +{ + "id": "{97BFC43B-9BE5-AB7D-EC5A-E31BB54878E0}", + "name": "Energy_Pie", + "type": 3, + "vendor": "Belevo AG", + "aliases": [], + "parentRequirements": [], + "childRequirements": [], + "implemented": [], + "prefix": "", + "url": "" +} + diff --git a/Energy_Pie/module.php b/Energy_Pie/module.php new file mode 100644 index 0000000..75d6e1e --- /dev/null +++ b/Energy_Pie/module.php @@ -0,0 +1,252 @@ +RegisterPropertyInteger('VarProduction', 0); + $this->RegisterPropertyInteger('VarConsumption', 0); + $this->RegisterPropertyInteger('VarFeedIn', 0); + $this->RegisterPropertyInteger('VarGrid', 0); + // Persisted UI state + $this->RegisterAttributeString(self::ATTR_RANGE, 'day'); + $this->RegisterAttributeString(self::ATTR_DATE, date('Y-m-d')); + // Enable individual visualization (HTML-SDK) + $this->SetVisualizationType(1); + // IMPORTANT: Timer calls global helper below (must exist!) + $this->RegisterTimer('AutoPush', 0, 'IPS_RequestAction($_IPS["TARGET"], "Refresh", 1);'); + } + public function ApplyChanges(): void + { + parent::ApplyChanges(); + // ensure range valid + $range = $this->ReadAttributeString(self::ATTR_RANGE); + if (!in_array($range, ['day', 'week', 'month', 'year', 'total'], true)) { + $this->WriteAttributeString(self::ATTR_RANGE, 'day'); + } + // ensure date valid (not empty/invalid/future) + $date = $this->ReadAttributeString(self::ATTR_DATE); + if ($date === '' || !$this->isValidDate($date) || strtotime($date . ' 00:00:00') > time()) { + $this->WriteAttributeString(self::ATTR_DATE, date('Y-m-d')); + } + // Fullscreen-Fix: push periodically (adjust as you like) + // 2000ms = alle 2 Sekunden (stabil, aber nicht ganz so brutal wie 1000ms) + $this->SetTimerInterval('AutoPush', 2000); + $this->RecalculateAndPush(); + } + public function GetVisualizationTile(): string + { + $path = __DIR__ . '/module.html'; + if (!file_exists($path)) { + return '
module.html fehlt
'; + } + return file_get_contents($path); + } + public function RequestAction($Ident, $Value): void + { + switch ($Ident) { + case 'SetRange': + $range = (string)$Value; + if (!in_array($range, ['day', 'week', 'month', 'year', 'total'], true)) { + return; + } + $this->WriteAttributeString(self::ATTR_RANGE, $range); + $this->RecalculateAndPush(); + break; + case 'SetDate': + $date = (string)$Value; + if (!$this->isValidDate($date)) { + return; + } + $this->WriteAttributeString(self::ATTR_DATE, $date); + $this->RecalculateAndPush(); + break; + case 'Prev': + case 'Next': + case 'Today': + $this->ShiftDate($Ident); + $this->RecalculateAndPush(); + break; + case 'Refresh': + $this->RecalculateAndPush(); + break; + default: + // ignore unknown + return; + } + } + private function RecalculateAndPush(): void + { + $range = $this->ReadAttributeString(self::ATTR_RANGE); + $date = $this->ReadAttributeString(self::ATTR_DATE); + [$tStart, $tEnd] = $this->getRange($range, $date); + $dbgProd = []; + $dbgFeed = []; + $dbgGrid = []; + $prod = $this->readDelta($this->ReadPropertyInteger('VarProduction'), $tStart, $tEnd, $dbgProd); + $feed = $this->readDelta($this->ReadPropertyInteger('VarFeedIn'), $tStart, $tEnd, $dbgFeed); + $grid = $this->readDelta($this->ReadPropertyInteger('VarGrid'), $tStart, $tEnd, $dbgGrid); + $hasData = (($dbgProd['count'] ?? 0) > 0) || (($dbgFeed['count'] ?? 0) > 0) || (($dbgGrid['count'] ?? 0) > 0); + $noDataHint = (!$hasData && $range !== 'total') ? 'Letzter Zeitpunkt' : ''; + + + // House = Prod - Feed + Grid + $house = $prod - $feed + $grid; + if ($house < 0) $house = 0.0; + $payload = [ + 'range' => $range, + 'date' => $date, + 'tStart' => $tStart, + 'tEnd' => $tEnd, + 'hasData' => $hasData, + 'noDataHint' => $noDataHint, + 'values' => [ + 'Produktion' => (float)$prod, + 'Einspeisung' => (float)$feed, + 'Netz' => (float)$grid, + 'Hausverbrauch' => (float)$house + ], + // kannst du später entfernen + 'debug' => [ + 'prod' => $dbgProd, + 'feed' => $dbgFeed, + 'grid' => $dbgGrid + ] + ]; + $this->UpdateVisualizationValue(json_encode($payload, JSON_THROW_ON_ERROR)); + } + private function getRange(string $range, string $dateYmd): array + { + $now = time(); + if ($range === 'total') { + return [0, $now]; + } + $base = strtotime($dateYmd . ' 00:00:00') ?: strtotime(date('Y-m-d') . ' 00:00:00'); + switch ($range) { + case 'day': + return [$base, $base + 86400]; + case 'week': + $dow = (int)date('N', $base); // 1=Mon..7=Sun + $start = $base - (($dow - 1) * 86400); + return [$start, $start + 7 * 86400]; + case 'month': + $start = strtotime(date('Y-m-01 00:00:00', $base)); + $end = strtotime(date('Y-m-01 00:00:00', strtotime('+1 month', $start))); + return [$start, $end]; + + case 'year': + $start = strtotime(date('Y-01-01 00:00:00', $base)); + $end = strtotime(date('Y-01-01 00:00:00', strtotime('+1 year', $start))); + return [$start, $end]; + + default: + return [$base, $base + 86400]; + } + } + public function GetVisualizationPopup(): string + { + // Popup (Fullscreen) soll das gleiche HTML wie das Tile anzeigen + $this->RecalculateAndPush(); + return $this->GetVisualizationTile(); + } + private function readDelta(int $varId, int $tStart, int $tEnd, array &$dbg): float + { + $dbg = [ + 'varId' => $varId, + 'archiveId' => 0, + 'count' => 0, + 'first' => null, + 'last' => null, + 'vStart' => null, + 'vEnd' => null + ]; + if ($varId <= 0 || !IPS_VariableExists($varId)) { + return 0.0; + } + $archiveID = IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0] ?? 0; + $dbg['archiveId'] = $archiveID; + if ($archiveID <= 0) { + return 0.0; + } + $values = @AC_GetLoggedValues($archiveID, $varId, $tStart - 86400, $tEnd + 86400, 0); + if (empty($values)) { + return 0.0; + } + usort($values, static fn($a, $b) => (int)$a['TimeStamp'] <=> (int)$b['TimeStamp']); + $dbg['count'] = count($values); + $dbg['first'] = (float)$values[0]['Value']; + $dbg['last'] = (float)$values[count($values) - 1]['Value']; + $vStart = null; + $vEnd = null; + foreach ($values as $v) { + $ts = (int)$v['TimeStamp']; + if ($ts <= $tStart) $vStart = (float)$v['Value']; + if ($ts <= $tEnd) $vEnd = (float)$v['Value']; + if ($ts > $tEnd) break; + } + if ($vStart === null) $vStart = $dbg['first']; + if ($vEnd === null) $vEnd = $dbg['last']; + $dbg['vStart'] = $vStart; + $dbg['vEnd'] = $vEnd; + $diff = $vEnd - $vStart; + return ($diff < 0) ? 0.0 : (float)$diff; + } + private function getLastLogTimestamp(int $varId): int + { + if ($varId <= 0 || !IPS_VariableExists($varId)) { + return 0; + } + $archiveID = IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0] ?? 0; + if ($archiveID <= 0) { + return 0; + } + $values = @AC_GetLoggedValues($archiveID, $varId, 0, time(), 1); + if (empty($values)) { + return 0; + } + return (int)$values[0]['TimeStamp']; + } + private function ShiftDate(string $action): void + { + $range = $this->ReadAttributeString(self::ATTR_RANGE); + if ($range === 'total') { + 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') ?: strtotime(date('Y-m-d') . ' 00:00:00'); + $sign = ($action === 'Prev') ? -1 : 1; + switch ($range) { + case 'day': + $base = strtotime(($sign === -1 ? '-1 day' : '+1 day'), $base); + break; + case 'week': + $base = strtotime(($sign === -1 ? '-7 day' : '+7 day'), $base); + break; + case 'month': + $base = strtotime(($sign === -1 ? '-1 month' : '+1 month'), $base); + break; + case 'year': + $base = strtotime(($sign === -1 ? '-1 year' : '+1 year'), $base); + break; + } + $this->WriteAttributeString(self::ATTR_DATE, date('Y-m-d', $base)); + } + private function isValidDate(string $ymd): bool + { + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $ymd)) { + return false; + } + [$y, $m, $d] = array_map('intval', explode('-', $ymd)); + return checkdate($m, $d, $y); + } +} + +?>