From 27536d3d1699b796cad6f94eeef6cd6a0c784f50 Mon Sep 17 00:00:00 2001 From: "belevo\\mh" Date: Tue, 20 Jan 2026 10:25:14 +0100 Subject: [PATCH] no message --- PV_Forecast/form.json | 34 +++++++-- PV_Forecast/module.php | 164 +++++++++++++++++++++++++---------------- 2 files changed, 129 insertions(+), 69 deletions(-) diff --git a/PV_Forecast/form.json b/PV_Forecast/form.json index 5292c4e..c746a10 100644 --- a/PV_Forecast/form.json +++ b/PV_Forecast/form.json @@ -1,5 +1,15 @@ { "elements": [ + { + "type": "ValidationTextBox", + "name": "URL", + "caption": "Solcast URL" + }, + { + "type": "SelectVariable", + "name": "ActualVariableID", + "caption": "Ist-Produktion Variable (Leistung)" + }, { "type": "Select", "name": "RefreshMode", @@ -14,14 +24,28 @@ "name": "RefreshMinutes", "caption": "Intervall (Minuten)", "minimum": 1, - "maximum": 240, - "visible": true + "maximum": 240 }, { - "type": "TimeSpinner", + "type": "ValidationTextBox", "name": "RefreshTime", - "caption": "Tägliche Uhrzeit", - "visible": true + "caption": "Tägliche Uhrzeit (HH:MM, z.B. 06:00)" + }, + { + "type": "CheckBox", + "name": "ActualIsWatt", + "caption": "Istwerte sind in Watt (in kW umrechnen)" + } + ], + "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.php b/PV_Forecast/module.php index 366acc0..d10a942 100644 --- a/PV_Forecast/module.php +++ b/PV_Forecast/module.php @@ -10,42 +10,40 @@ class PV_Forecast extends IPSModule $this->RegisterPropertyString("URL", ""); $this->RegisterPropertyInteger("ActualVariableID", 0); + + // Scheduler $this->RegisterPropertyString("RefreshMode", "interval"); // interval | daily $this->RegisterPropertyInteger("RefreshMinutes", 5); - $this->RegisterPropertyInteger("RefreshTime", 6 * 3600); // 06:00 in Sekunden + $this->RegisterPropertyString("RefreshTime", "06:00"); // HH:MM $this->RegisterPropertyBoolean("ActualIsWatt", true); + // Timer fires RequestAction(UpdateForecast) $this->RegisterTimer("UpdateForecastTimer", 0, 'IPS_RequestAction($_IPS["TARGET"], "UpdateForecast", 0);'); + // WebHook endpoint $this->RegisterHook("/hook/solcastcompare"); } - public function ApplyChanges() - { - parent::ApplyChanges(); + public function ApplyChanges() + { + parent::ApplyChanges(); - $this->SetVisualizationType(1); + // Tile Visualization aktivieren + $this->SetVisualizationType(1); - $mode = $this->ReadPropertyString("RefreshMode"); + $mode = $this->ReadPropertyString("RefreshMode"); - if ($mode === "interval") { - // 🔁 Intervall-Modus - $mins = max(1, (int)$this->ReadPropertyInteger("RefreshMinutes")); - $this->SetTimerInterval( - "UpdateForecastTimer", - $mins * 60 * 1000 - ); - } else { - // ⏰ Tageszeit-Modus - $this->SetTimerInterval( - "UpdateForecastTimer", - 60 * 1000 // jede Minute prüfen - ); - } - - $this->RegisterHook("/hook/solcastcompare"); + if ($mode === "interval") { + $mins = max(1, (int)$this->ReadPropertyInteger("RefreshMinutes")); + $this->SetTimerInterval("UpdateForecastTimer", $mins * 60 * 1000); + } else { + // daily: jede Minute prüfen + $this->SetTimerInterval("UpdateForecastTimer", 60 * 1000); } + // Hook (optional, falls Auto-Register bei dir klappt) + $this->RegisterHook("/hook/solcastcompare"); + } public function RequestAction($Ident, $Value) { @@ -53,11 +51,59 @@ class PV_Forecast extends IPSModule $this->HandleScheduledUpdate(); return; } - throw new Exception("Unknown Ident: " . $Ident); } - private function UpdateForecast() + // ----------------- Scheduler ----------------- + + private function HandleScheduledUpdate(): void + { + $mode = $this->ReadPropertyString("RefreshMode"); + + if ($mode === "interval") { + $this->UpdateForecast(); + return; + } + + // daily + $timeStr = trim($this->ReadPropertyString("RefreshTime")); // "06:00" + $targetSec = $this->ParseHHMMToSeconds($timeStr); + if ($targetSec === null) { + $this->SendDebug("Scheduler", "Ungueltige RefreshTime: " . $timeStr, 0); + return; + } + + $now = time(); + $nowSec = ((int)date("H", $now) * 3600) + ((int)date("i", $now) * 60); + + // Schon heute gelaufen? + $lastRun = (int)$this->GetBuffer("LastDailyRun"); + $today0 = strtotime("today"); + + if ($nowSec >= $targetSec && $lastRun < $today0) { + $this->SendDebug("Scheduler", "Taegliches Update ausgeloest (" . $timeStr . ")", 0); + $this->UpdateForecast(); + $this->SetBuffer("LastDailyRun", (string)$now); + } + } + + private function ParseHHMMToSeconds(string $hhmm): ?int + { + // akzeptiert "6:00" oder "06:00" + if (!preg_match('/^(\d{1,2}):(\d{2})$/', $hhmm, $m)) { + return null; + } + $h = (int)$m[1]; + $min = (int)$m[2]; + if ($h < 0 || $h > 23 || $min < 0 || $min > 59) { + return null; + } + return $h * 3600 + $min * 60; + } + + // ----------------- Forecast Fetch ----------------- + + private function UpdateForecast(): void { $url = trim($this->ReadPropertyString("URL")); if ($url === "") { @@ -82,6 +128,8 @@ class PV_Forecast extends IPSModule $this->SendDebug("UpdateForecast", "Forecast aktualisiert", 0); } + // ----------------- Tile Visualization ----------------- + public function GetVisualizationTile(): string { if ($this->GetBuffer("ForecastRaw") === "") { @@ -93,6 +141,8 @@ class PV_Forecast extends IPSModule return $html; } + // ----------------- WebHook: data for chart ----------------- + protected function ProcessHookData() { $instance = isset($_GET["instance"]) ? (int)$_GET["instance"] : 0; @@ -140,10 +190,14 @@ class PV_Forecast extends IPSModule echo json_encode($out); } + // ----------------- Series: Forecast (Solcast) ----------------- + private function GetForecastSeriesFiltered(int $startTs, int $endTs): array { $raw = $this->GetBuffer("ForecastRaw"); - if ($raw === "") return []; + if ($raw === "") { + return []; + } $data = json_decode($raw, true); if (!is_array($data) || !isset($data["estimated_actuals"]) || !is_array($data["estimated_actuals"])) { @@ -157,7 +211,7 @@ class PV_Forecast extends IPSModule $ts = strtotime($row["period_end"]); // UTC korrekt if ($ts === false) continue; - // nur HEUTE (lokal) anzeigen + // nur HEUTE (lokal) if ($ts < $startTs || $ts >= $endTs) continue; // Solcast: kW @@ -169,13 +223,19 @@ class PV_Forecast extends IPSModule return $series; } + // ----------------- Series: Actual (Archive) ----------------- + private function GetActualSeriesFromArchive(int $startTs, int $endTs, int $bucketSeconds, int $nowTs): array { $varId = (int)$this->ReadPropertyInteger("ActualVariableID"); - if ($varId <= 0 || !IPS_VariableExists($varId)) return []; + if ($varId <= 0 || !IPS_VariableExists($varId)) { + return []; + } $archiveId = $this->GetArchiveInstanceID(); - if ($archiveId === 0) return []; + if ($archiveId === 0) { + return []; + } if (!AC_GetLoggingStatus($archiveId, $varId)) { $this->SendDebug("Actual", "Variable wird nicht geloggt im Archiv", 0); @@ -183,9 +243,12 @@ class PV_Forecast extends IPSModule } $logged = AC_GetLoggedValues($archiveId, $varId, $startTs, $endTs, 0); - if (!is_array($logged) || count($logged) === 0) return []; + if (!is_array($logged) || count($logged) === 0) { + return []; + } - $buckets = []; // bucketEnd => [sum, count] + // Bucket End -> [sum, count] + $buckets = []; foreach ($logged as $row) { if (!isset($row["TimeStamp"], $row["Value"])) continue; @@ -200,12 +263,12 @@ class PV_Forecast extends IPSModule $buckets[$bucketEnd][1] += 1; } - // Wir wollen ein sauberes 30-Minuten Raster über den ganzen Tag, - // aber nach "jetzt" soll die Ist-Linie abbrechen (null). $series = []; $isWatt = (bool)$this->ReadPropertyBoolean("ActualIsWatt"); + // sauberes Raster über den Tag, Zukunft => null for ($t = $startTs + $bucketSeconds; $t <= $endTs; $t += $bucketSeconds) { + // Zukunft: null (Linie bricht) if ($t > $nowTs + $bucketSeconds) { $series[] = [$t * 1000, null]; @@ -219,8 +282,10 @@ class PV_Forecast extends IPSModule $avg = $buckets[$t][0] / $buckets[$t][1]; - // W -> kW, damit es zu Solcast passt - if ($isWatt) $avg = $avg / 1000.0; + // W -> kW + if ($isWatt) { + $avg = $avg / 1000.0; + } $series[] = [$t * 1000, $avg]; } @@ -233,37 +298,8 @@ class PV_Forecast extends IPSModule $list = IPS_GetInstanceListByModuleID(self::ARCHIVE_GUID); return (is_array($list) && count($list) > 0) ? (int)$list[0] : 0; } - - private function HandleScheduledUpdate() - { - $mode = $this->ReadPropertyString("RefreshMode"); - - if ($mode === "interval") { - // 🔁 immer aktualisieren - $this->UpdateForecast(); - return; - } - - // ⏰ Tageszeit-Modus - $targetSec = (int)$this->ReadPropertyInteger("RefreshTime"); - $now = time(); - - // Sekunden seit Mitternacht - $nowSec = - (int)date("H", $now) * 3600 + - (int)date("i", $now) * 60; - - // Schon heute gelaufen? - $lastRun = (int)$this->GetBuffer("LastDailyRun"); - $today = strtotime("today"); - - if ($nowSec >= $targetSec && $lastRun < $today) { - $this->SendDebug("Scheduler", "Tägliches Update ausgelöst", 0); - $this->UpdateForecast(); - $this->SetBuffer("LastDailyRun", (string)$now); - } - } + // ----------------- optional: auto register hook ----------------- private function RegisterHook(string $Hook) {