From 4711fb0208ef4e582eab63031eddb22c33cd2845 Mon Sep 17 00:00:00 2001 From: "belevo\\mh" Date: Tue, 20 Jan 2026 08:41:06 +0100 Subject: [PATCH] no message --- PV_Forecast/form.json | 32 +++++ PV_Forecast/module.html | 78 ++++++++++++ PV_Forecast/module.json | 12 ++ PV_Forecast/module.php | 262 ++++++++++++++++++++++++++++++++++++++++ PV_Forecast/readme.md | 59 +++++++++ 5 files changed, 443 insertions(+) create mode 100644 PV_Forecast/form.json create mode 100644 PV_Forecast/module.html create mode 100644 PV_Forecast/module.json create mode 100644 PV_Forecast/module.php create mode 100644 PV_Forecast/readme.md diff --git a/PV_Forecast/form.json b/PV_Forecast/form.json new file mode 100644 index 0000000..91e2ac9 --- /dev/null +++ b/PV_Forecast/form.json @@ -0,0 +1,32 @@ +{ + "elements": [ + { + "type": "ValidationTextBox", + "name": "URL", + "caption": "Solcast URL" + }, + { + "type": "SelectVariable", + "name": "ActualVariableID", + "caption": "Ist-Produktion Variable" + }, + { + "type": "NumberSpinner", + "name": "RefreshMinutes", + "caption": "Refresh (Minuten)", + "minimum": 1, + "maximum": 120 + } + ], + "actions": [ + { + "type": "Button", + "caption": "Forecast jetzt aktualisieren", + "onClick": "IPS_RequestAction($id, 'UpdateForecast', 0);" + }, + { + "type": "Label", + "caption": "Hinweis: Istwerte werden aus dem Archiv gelesen. Variable muss im Archiv geloggt werden." + } + ] +} diff --git a/PV_Forecast/module.html b/PV_Forecast/module.html new file mode 100644 index 0000000..830de33 --- /dev/null +++ b/PV_Forecast/module.html @@ -0,0 +1,78 @@ + + + + + PV_Forecast + + + + +
+ + +
+ +
+ + + + diff --git a/PV_Forecast/module.json b/PV_Forecast/module.json new file mode 100644 index 0000000..dd88242 --- /dev/null +++ b/PV_Forecast/module.json @@ -0,0 +1,12 @@ +{ + "id": "{D32FEB32-3F6E-AA8C-CEE6-A09C2789EDC7}", + "name": "PV_Forecast ", + "type": 3, + "vendor": "Belevo AG", + "aliases": [], + "parentRequirements": [], + "childRequirements": [], + "implemented": [], + "prefix": "", + "url": "" +} diff --git a/PV_Forecast/module.php b/PV_Forecast/module.php new file mode 100644 index 0000000..4a0e0e2 --- /dev/null +++ b/PV_Forecast/module.php @@ -0,0 +1,262 @@ +RegisterPropertyString("URL", ""); + $this->RegisterPropertyInteger("ActualVariableID", 0); + $this->RegisterPropertyInteger("RefreshMinutes", 5); + + // Timer + $this->RegisterTimer("UpdateForecastTimer", 0, 'IPS_RequestAction($_IPS["TARGET"], "UpdateForecast", 0);'); + + // Hook für Visualisierung (JSON Endpoint) + $this->RegisterHook("/hook/solcastcompare"); + } + + public function ApplyChanges() + { + parent::ApplyChanges(); + + $mins = max(1, (int)$this->ReadPropertyInteger("RefreshMinutes")); + $this->SetTimerInterval("UpdateForecastTimer", $mins * 60 * 1000); + } + + public function RequestAction($Ident, $Value) + { + switch ($Ident) { + case "UpdateForecast": + $this->UpdateForecast(); + break; + + default: + throw new Exception("Unknown Ident: " . $Ident); + } + } + + private function UpdateForecast() + { + $url = trim($this->ReadPropertyString("URL")); + if ($url === "") { + $this->SendDebug("UpdateForecast", "URL ist leer", 0); + return; + } + + try { + $json = Sys_GetURLContent($url); + if ($json === false || $json === "") { + $this->SendDebug("UpdateForecast", "Leere Antwort von URL", 0); + return; + } + + $data = json_decode($json, true); + if (!is_array($data)) { + $this->SendDebug("UpdateForecast", "JSON decode fehlgeschlagen", 0); + return; + } + + // Cache in Buffer + $this->SetBuffer("ForecastRaw", $json); + $this->SetBuffer("ForecastTS", (string)time()); + + $this->SendDebug("UpdateForecast", "Forecast aktualisiert", 0); + } catch (Throwable $e) { + $this->SendDebug("UpdateForecast", "Fehler: " . $e->getMessage(), 0); + } + } + + // -------- Datenaufbereitung fürs Diagramm -------- + + private function GetForecastSeries() + { + $raw = $this->GetBuffer("ForecastRaw"); + if ($raw === "") { + return []; + } + + $data = json_decode($raw, true); + if (!is_array($data) || !isset($data["estimated_actuals"]) || !is_array($data["estimated_actuals"])) { + return []; + } + + // Solcast liefert period_end in UTC (Z) + // Wir geben dem Chart epoch-ms in UTC und lassen JS lokal anzeigen (ok). + $series = []; + foreach ($data["estimated_actuals"] as $row) { + if (!isset($row["period_end"], $row["pv_power_rooftop"])) { + continue; + } + $ts = strtotime($row["period_end"]); // UTC korrekt wegen "Z" + if ($ts === false) { + continue; + } + // pv_power_rooftop ist i.d.R. kW (Solcast rooftop_pv_power) + $val = (float)$row["pv_power_rooftop"]; + $series[] = [ $ts * 1000, $val ]; + } + + // Solcast ist oft absteigend sortiert -> aufsteigend fürs Chart + usort($series, function($a, $b) { return $a[0] <=> $b[0]; }); + + return $series; + } + + private function GetActualSeriesFromArchive($startTs, $endTs, $bucketSeconds = 1800) + { + $varId = (int)$this->ReadPropertyInteger("ActualVariableID"); + if ($varId <= 0 || !IPS_VariableExists($varId)) { + return []; + } + + $archiveId = $this->GetArchiveInstanceID(); + if ($archiveId === 0) { + return []; + } + + if (!AC_GetLoggingStatus($archiveId, $varId)) { + $this->SendDebug("Actual", "Variable wird nicht geloggt im Archiv", 0); + return []; + } + + // Rohwerte holen + $logged = AC_GetLoggedValues($archiveId, $varId, $startTs, $endTs, 0); + if (!is_array($logged) || count($logged) === 0) { + return []; + } + + // Bucket: 30 Minuten + $buckets = []; // key=bucketEndTs => [sum, count] + foreach ($logged as $row) { + if (!isset($row["TimeStamp"], $row["Value"])) continue; + + $ts = (int)$row["TimeStamp"]; + $bucketStart = intdiv($ts, $bucketSeconds) * $bucketSeconds; + $bucketEnd = $bucketStart + $bucketSeconds; // wir nehmen wie Solcast period_end + + if ($bucketEnd < $startTs || $bucketEnd > $endTs) continue; + + if (!isset($buckets[$bucketEnd])) { + $buckets[$bucketEnd] = [0.0, 0]; + } + $buckets[$bucketEnd][0] += (float)$row["Value"]; + $buckets[$bucketEnd][1] += 1; + } + + ksort($buckets); + + $series = []; + foreach ($buckets as $bucketEnd => [$sum, $count]) { + if ($count <= 0) continue; + $avg = $sum / $count; + + // Falls deine Ist-Variable in W ist und Solcast in kW: + // -> hier ggf. /1000 machen. + // Ich lasse es neutral. Wenn du willst: uncomment: + // $avg = $avg / 1000.0; + + $series[] = [ $bucketEnd * 1000, $avg ]; + } + + return $series; + } + + private function GetArchiveInstanceID() + { + $list = IPS_GetInstanceListByModuleID(self::ARCHIVE_GUID); + if (is_array($list) && count($list) > 0) { + return (int)$list[0]; + } + return 0; + } + + // -------- Visualisierung / Hook -------- + + public function GetVisualization() + { + // Forecast ggf. initial laden, wenn noch nichts da + if ($this->GetBuffer("ForecastRaw") === "") { + $this->UpdateForecast(); + } + + $htmlPath = __DIR__ . "/module.html"; + $html = file_get_contents($htmlPath); + + // Platzhalter ersetzen + $html = str_replace("{{INSTANCE_ID}}", (string)$this->InstanceID, $html); + + return $html; + } + + protected function ProcessHookData() + { + // Erwartet: /hook/solcastcompare?instance=12345&action=data + $instance = isset($_GET["instance"]) ? (int)$_GET["instance"] : 0; + $action = isset($_GET["action"]) ? (string)$_GET["action"] : ""; + + if ($instance !== $this->InstanceID) { + http_response_code(404); + echo "Wrong instance"; + return; + } + + if ($action !== "data") { + http_response_code(400); + echo "Unknown action"; + return; + } + + // Zeitraum: letzte 24h (passt zu deinem hours=24) + $end = time(); + $start = $end - 24 * 3600; + + $forecast = $this->GetForecastSeries(); + $actual = $this->GetActualSeriesFromArchive($start, $end, 1800); + + $out = [ + "meta" => [ + "forecast_cached_at" => (int)$this->GetBuffer("ForecastTS"), + "bucket_seconds" => 1800 + ], + "series" => [ + "forecast" => $forecast, + "actual" => $actual + ] + ]; + + header("Content-Type: application/json; charset=utf-8"); + echo json_encode($out); + } + + private function RegisterHook($Hook) + { + $ids = IPS_GetInstanceListByModuleID("{015A6EB8-D6E5-4B93-B496-0D3FAD4D0E6E}"); // WebHook Control GUID + if (count($ids) === 0) { + $this->SendDebug("Hook", "WebHook Control nicht gefunden", 0); + return; + } + $hookID = $ids[0]; + + $hooks = json_decode(IPS_GetProperty($hookID, "Hooks"), true); + if (!is_array($hooks)) $hooks = []; + + $found = false; + foreach ($hooks as $h) { + if ($h["Hook"] === $Hook) { + $found = true; + break; + } + } + + if (!$found) { + $hooks[] = ["Hook" => $Hook, "TargetID" => $this->InstanceID]; + IPS_SetProperty($hookID, "Hooks", json_encode($hooks)); + IPS_ApplyChanges($hookID); + } + } +} \ No newline at end of file diff --git a/PV_Forecast/readme.md b/PV_Forecast/readme.md new file mode 100644 index 0000000..bc8dd0b --- /dev/null +++ b/PV_Forecast/readme.md @@ -0,0 +1,59 @@ +# PV_Visu + +Visualisierung des Eigenverbrauchs: Tages-Quoten für PV-Produktion vs. Einspeisung und Verbrauch vs. Netz-Bezug. + +## Inhaltsverzeichnis + +1. [Funktionsumfang](#funktionsumfang) +2. [Voraussetzungen](#voraussetzungen) +3. [Installation](#installation) +4. [Instanz einrichten](#instanz-einrichten) +5. [WebFront](#webfront) +6. [PHP-Befehlsreferenz](#php-befehlsreferenz) + +## Funktionsumfang + +- Anzeige von Tages-Quoten (%) +- Produktion: Eigenverbrauch vs. Einspeisung +- Verbrauch: PV-Anteil vs. Netz-Anteil +- Zwei Balkendiagramme +- Absolute Tages-Summen (kWh) + +## Voraussetzungen + +- IP-Symcon ≥ 7.1 +- Archiv-Modul aktiviert +- Vier kWh-Zähler-Variablen + +## Installation + +1. **Module Store** → Suche nach „PV_Visu“ und installieren +2. **Alternativ**: Unter Module → Repositories folgende URL hinzufügen: + ``` + https://github.com/DeinRepo/PV_Visu.git + ``` + und Modul neu einlesen. + +## Instanz einrichten + +- **Instanz hinzufügen** → Filter: „PV_Visu“ +- Variablen zuweisen: + + | Property | Beschreibung | + | -------------- | -------------------------- | + | VarProduction | PV-Produktionszähler (kWh) | + | VarConsumption | Gesamtverbrauch (kWh) | + | VarFeedIn | Einspeisung (kWh) | + | VarGrid | Netz-Bezug (kWh) | + +## WebFront + +- **Tile-Typ:** PV_Visu +- Balken 1 (Grün): Produktion +- Balken 2 (Orange/Rot): Verbrauch + +## PHP-Befehlsreferenz + +```php +IPS_RequestAction($InstanceID, 'update', true); +```