RegisterPropertyString("URL", ""); $this->RegisterPropertyInteger("ActualVariableID", 0); $this->RegisterPropertyString("RefreshMode", "interval"); // interval | daily $this->RegisterPropertyInteger("RefreshMinutes", 5); $this->RegisterPropertyInteger("RefreshTime", 6 * 3600); // 06:00 in Sekunden $this->RegisterPropertyBoolean("ActualIsWatt", true); $this->RegisterTimer("UpdateForecastTimer", 0, 'IPS_RequestAction($_IPS["TARGET"], "UpdateForecast", 0);'); $this->RegisterHook("/hook/solcastcompare"); } public function ApplyChanges() { parent::ApplyChanges(); $this->SetVisualizationType(1); $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"); } public function RequestAction($Ident, $Value) { if ($Ident === "UpdateForecast") { $this->HandleScheduledUpdate(); return; } throw new Exception("Unknown Ident: " . $Ident); } private function UpdateForecast() { $url = trim($this->ReadPropertyString("URL")); if ($url === "") { $this->SendDebug("UpdateForecast", "URL ist leer", 0); return; } $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; } $this->SetBuffer("ForecastRaw", $json); $this->SetBuffer("ForecastTS", (string)time()); $this->SendDebug("UpdateForecast", "Forecast aktualisiert", 0); } public function GetVisualizationTile(): string { if ($this->GetBuffer("ForecastRaw") === "") { $this->UpdateForecast(); } $html = file_get_contents(__DIR__ . "/module.html"); $html = str_replace("{{INSTANCE_ID}}", (string)$this->InstanceID, $html); return $html; } protected function ProcessHookData() { $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: HEUTE (lokal) $tz = new DateTimeZone(date_default_timezone_get()); $startDT = new DateTime('today', $tz); $endDT = new DateTime('tomorrow', $tz); $start = $startDT->getTimestamp(); $end = $endDT->getTimestamp(); $now = time(); $forecast = $this->GetForecastSeriesFiltered($start, $end); $actual = $this->GetActualSeriesFromArchive($start, $end, 1800, $now); $out = [ "meta" => [ "forecast_cached_at" => (int)$this->GetBuffer("ForecastTS"), "bucket_seconds" => 1800, "actual_is_watt" => (bool)$this->ReadPropertyBoolean("ActualIsWatt"), "start" => $start * 1000, "end" => $end * 1000, "now" => $now * 1000 ], "series" => [ "forecast" => $forecast, "actual" => $actual ] ]; header("Content-Type: application/json; charset=utf-8"); echo json_encode($out); } private function GetForecastSeriesFiltered(int $startTs, int $endTs): array { $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 []; } $series = []; foreach ($data["estimated_actuals"] as $row) { if (!isset($row["period_end"], $row["pv_power_rooftop"])) continue; $ts = strtotime($row["period_end"]); // UTC korrekt if ($ts === false) continue; // nur HEUTE (lokal) anzeigen if ($ts < $startTs || $ts >= $endTs) continue; // Solcast: kW $val = (float)$row["pv_power_rooftop"]; $series[] = [$ts * 1000, $val]; } usort($series, fn($a, $b) => $a[0] <=> $b[0]); return $series; } private function GetActualSeriesFromArchive(int $startTs, int $endTs, int $bucketSeconds, int $nowTs): array { $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 []; } $logged = AC_GetLoggedValues($archiveId, $varId, $startTs, $endTs, 0); if (!is_array($logged) || count($logged) === 0) return []; $buckets = []; // bucketEnd => [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; 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; } // 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"); for ($t = $startTs + $bucketSeconds; $t <= $endTs; $t += $bucketSeconds) { // Zukunft: null (Linie bricht) if ($t > $nowTs + $bucketSeconds) { $series[] = [$t * 1000, null]; continue; } if (!isset($buckets[$t]) || $buckets[$t][1] <= 0) { $series[] = [$t * 1000, null]; continue; } $avg = $buckets[$t][0] / $buckets[$t][1]; // W -> kW, damit es zu Solcast passt if ($isWatt) $avg = $avg / 1000.0; $series[] = [$t * 1000, $avg]; } return $series; } private function GetArchiveInstanceID(): int { $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); } } private function RegisterHook(string $Hook) { $ids = IPS_GetInstanceListByModuleID("{015A6EB8-D6E5-4B93-B496-0D3FAD4D0E6E}"); if (count($ids) === 0) return; $hookID = (int)$ids[0]; $hooks = json_decode(IPS_GetProperty($hookID, "Hooks"), true); if (!is_array($hooks)) $hooks = []; foreach ($hooks as $h) { if (isset($h["Hook"]) && $h["Hook"] === $Hook) return; } $hooks[] = ["Hook" => $Hook, "TargetID" => $this->InstanceID]; IPS_SetProperty($hookID, "Hooks", json_encode($hooks)); IPS_ApplyChanges($hookID); } }