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); } } }