diff --git a/Batterie/module.php b/Batterie/module.php
index 3a061f2..a4f27e5 100644
--- a/Batterie/module.php
+++ b/Batterie/module.php
@@ -250,35 +250,6 @@ public function RequestAction($Ident, $Value)
}elseif ($batterieManagement == 2 && $batterietyp == 2) {
$this->SetValue("Batteriemanagement_Variabel", 4);
}
- /*
- if($this->GetValue("Is_Peak_Shaving")==true){
- if ($power >= 0) {
- $this->SetValue("Ladeleistung", $power);
- $this->SetValue("Entladeleistung", 0);
- $this->SetValue("Laden3_Entladen4", 3);
- } else {
- $this->SetValue("Entladeleistung", abs($power));
- $this->SetValue("Ladeleistung", 0);
- $this->SetValue("Laden3_Entladen4", 4);
- }
-
- }else{
- if ($power >= 0) {
- $this->SetValue("Ladeleistung", $power);
- $this->SetValue("Entladeleistung", 0);
- $this->SetValue("Laden3_Entladen4", 3);
- } else {
- $this->SetValue("Entladeleistung", abs($power));
- $this->SetValue("Ladeleistung", 0);
- $this->SetValue("Laden3_Entladen4", 4);
- }
-
- }
- */
-
-
-
-
$batterietyp = $this->ReadPropertyInteger("Batterietyp");
diff --git a/PV_Forecast/form.json b/PV_Forecast/form.json
deleted file mode 100644
index c746a10..0000000
--- a/PV_Forecast/form.json
+++ /dev/null
@@ -1,51 +0,0 @@
-{
- "elements": [
- {
- "type": "ValidationTextBox",
- "name": "URL",
- "caption": "Solcast URL"
- },
- {
- "type": "SelectVariable",
- "name": "ActualVariableID",
- "caption": "Ist-Produktion Variable (Leistung)"
- },
- {
- "type": "Select",
- "name": "RefreshMode",
- "caption": "Aktualisierungsmodus",
- "options": [
- { "caption": "Alle X Minuten", "value": "interval" },
- { "caption": "Einmal täglich (Uhrzeit)", "value": "daily" }
- ]
- },
- {
- "type": "NumberSpinner",
- "name": "RefreshMinutes",
- "caption": "Intervall (Minuten)",
- "minimum": 1,
- "maximum": 240
- },
- {
- "type": "ValidationTextBox",
- "name": "RefreshTime",
- "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.html b/PV_Forecast/module.html
deleted file mode 100644
index 4ddd974..0000000
--- a/PV_Forecast/module.html
+++ /dev/null
@@ -1,186 +0,0 @@
-
-
-
-
- PV_Forecast
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/PV_Forecast/module.json b/PV_Forecast/module.json
deleted file mode 100644
index 4057eb7..0000000
--- a/PV_Forecast/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "id": "{D32FEB32-3F6E-AA8C-CEE6-A09C2789EDC7}",
- "name": "PV_Forecast ",
- "type": 3,
- "vendor": "Belevo AG",
- "aliases": [],
- "parentRequirements": [],
- "childRequirements": [],
- "implemented": [],
- "prefix": "PVF",
- "url": ""
-}
diff --git a/PV_Forecast/module.php b/PV_Forecast/module.php
deleted file mode 100644
index 4000592..0000000
--- a/PV_Forecast/module.php
+++ /dev/null
@@ -1,348 +0,0 @@
-RegisterPropertyString("URL", "");
- $this->RegisterPropertyInteger("ActualVariableID", 0);
-
- // Scheduler
- $this->RegisterPropertyString("RefreshMode", "interval"); // interval | daily
- $this->RegisterPropertyInteger("RefreshMinutes", 5);
- $this->RegisterPropertyString("RefreshTime", "06:00"); // HH:MM
- $this->RegisterPropertyBoolean("ActualIsWatt", true);
-
- // Timer -> ruft RequestAction(UpdateForecast) auf
- $this->RegisterTimer("UpdateForecastTimer", 0, 'IPS_RequestAction($_IPS["TARGET"], "UpdateForecast", 0);');
-
- // WebHook Endpoint
- $this->RegisterHook("/hook/solcastcompare");
- }
-
- public function ApplyChanges()
- {
- parent::ApplyChanges();
-
- // Tile Visualization aktivieren
- $this->SetVisualizationType(1);
-
- $mode = $this->ReadPropertyString("RefreshMode");
-
- 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);
- }
-
- // optional: auto-register hook (wenn GUID passt)
- $this->RegisterHook("/hook/solcastcompare");
- }
-
- public function RequestAction($Ident, $Value)
- {
- if ($Ident === "UpdateForecast") {
- $this->HandleScheduledUpdate();
- return;
- }
-
- throw new Exception("Unknown Ident: " . $Ident);
- }
-
- // ----------------- 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 === "") {
- $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);
- }
-
- // ----------------- Tile Visualization -----------------
-
- 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;
- }
-
- // ----------------- WebHook: data for chart -----------------
-
- 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;
- }
-
- $tzLocal = new DateTimeZone(date_default_timezone_get());
- $tzUtc = new DateTimeZone('UTC');
-
- // Lokaler Tag (für Archiv / Anzeige)
- $startLocal = new DateTime('today', $tzLocal);
- $endLocal = new DateTime('tomorrow', $tzLocal);
-
- $startLocalTs = $startLocal->getTimestamp();
- $endLocalTs = $endLocal->getTimestamp();
-
- // Lokaler Tagesbereich als UTC-Grenzen (für Solcast "Z")
- $startUtcTs = (clone $startLocal)->setTimezone($tzUtc)->getTimestamp();
- $endUtcTs = (clone $endLocal)->setTimezone($tzUtc)->getTimestamp();
-
- $nowTs = time();
-
- // Forecast: komplett für HEUTE (lokaler Tag), korrekt über UTC gefiltert
- $forecast = $this->GetForecastSeriesFilteredUtc($startUtcTs, $endUtcTs);
-
- // Actual: komplett HEUTE lokal, aber nach "jetzt" abbrechen (null)
- $actual = $this->GetActualSeriesFromArchive($startLocalTs, $endLocalTs, 1800, $nowTs);
-
- $out = [
- "meta" => [
- "forecast_cached_at" => (int)$this->GetBuffer("ForecastTS"),
- "bucket_seconds" => 1800,
- "actual_is_watt" => (bool)$this->ReadPropertyBoolean("ActualIsWatt"),
- "start_local" => $startLocalTs * 1000,
- "end_local" => $endLocalTs * 1000,
- "start_utc" => $startUtcTs * 1000,
- "end_utc" => $endUtcTs * 1000,
- "now" => $nowTs * 1000
- ],
- "series" => [
- "forecast" => $forecast,
- "actual" => $actual
- ]
- ];
-
- header("Content-Type: application/json; charset=utf-8");
- echo json_encode($out);
- }
-
- // ----------------- Series: Forecast (Solcast) -----------------
-
- private function GetForecastSeriesFilteredUtc(int $startUtcTs, int $endUtcTs): array
- {
- $raw = $this->GetBuffer("ForecastRaw");
- if ($raw === "") return [];
-
- $data = json_decode($raw, true);
- if (!is_array($data)) return [];
-
- // forecast endpoint: "forecasts"
- if (isset($data["forecasts"]) && is_array($data["forecasts"])) {
- $rows = $data["forecasts"];
- } elseif (isset($data["estimated_actuals"]) && is_array($data["estimated_actuals"])) {
- // fallback live endpoint
- $rows = $data["estimated_actuals"];
- } else {
- return [];
- }
-
- $series = [];
- foreach ($rows as $row) {
- if (!isset($row["period_end"], $row["pv_power_rooftop"])) continue;
-
- $ts = strtotime($row["period_end"]); // UTC wegen Z
- if ($ts === false) continue;
-
- if ($ts < $startUtcTs || $ts >= $endUtcTs) continue;
-
- $series[] = [$ts * 1000, (float)$row["pv_power_rooftop"]];
- }
-
- usort($series, fn($a, $b) => $a[0] <=> $b[0]);
- 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 [];
- }
-
- $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) {
- // trotzdem Raster mit null zurückgeben, damit Linie sauber endet
- return $this->BuildNullRaster($startTs, $endTs, $bucketSeconds);
- }
-
- // bucketEnd -> [sum, count]
- $buckets = [];
- 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;
- }
-
- $series = [];
- $isWatt = (bool)$this->ReadPropertyBoolean("ActualIsWatt");
-
- // Sauberes Raster über den Tag, nach "jetzt" -> null
- for ($t = $startTs + $bucketSeconds; $t <= $endTs; $t += $bucketSeconds) {
-
- // Zukunft: null, damit Istlinie nicht parallel bis 24:00 weiterläuft
- 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
- if ($isWatt) {
- $avg = $avg / 1000.0;
- }
-
- $series[] = [$t * 1000, $avg];
- }
-
- return $series;
- }
-
- private function BuildNullRaster(int $startTs, int $endTs, int $bucketSeconds): array
- {
- $series = [];
- for ($t = $startTs + $bucketSeconds; $t <= $endTs; $t += $bucketSeconds) {
- $series[] = [$t * 1000, null];
- }
- return $series;
- }
-
- private function GetArchiveInstanceID(): int
- {
- $list = IPS_GetInstanceListByModuleID(self::ARCHIVE_GUID);
- return (is_array($list) && count($list) > 0) ? (int)$list[0] : 0;
- }
-
- // ----------------- optional: auto register hook -----------------
-
- 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);
- }
-}
diff --git a/PV_Forecast/readme.md b/PV_Forecast/readme.md
deleted file mode 100644
index bc8dd0b..0000000
--- a/PV_Forecast/readme.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# 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);
-```
diff --git a/PV_Forecast_plotmemory/form.json b/PV_Forecast_plotmemory/form.json
deleted file mode 100644
index 9a451ff..0000000
--- a/PV_Forecast_plotmemory/form.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "elements": [
- { "type": "ValidationTextBox", "name": "kunde", "caption": "Kundenname" },
- { "type": "ValidationTextBox", "name": "URL", "caption": "Solcast URL" },
- { "type": "SelectVariable", "name": "ActualVariableID", "caption": "Ist-Produktion Variable (Leistung)" },
- { "type": "CheckBox", "name": "ActualIsWatt", "caption": "Istwerte sind in Watt (in kW umrechnen)" },
-
- {
- "type": "Select",
- "name": "RefreshMode",
- "caption": "Forecast-Aktualisierung",
- "options": [
- { "caption": "Alle X Minuten", "value": "interval" },
- { "caption": "Einmal täglich (Uhrzeit)", "value": "daily" }
- ]
- },
- { "type": "NumberSpinner", "name": "RefreshMinutes", "caption": "Intervall (Minuten)", "minimum": 1, "maximum": 240 },
- { "type": "ValidationTextBox", "name": "RefreshTime", "caption": "Tägliche Uhrzeit (HH:MM, z.B. 00:01)" },
-
- { "type": "CheckBox", "name": "EnableDailySnapshots", "caption": "Tägliche Tages-Plots speichern" },
- { "type": "ValidationTextBox", "name": "SnapshotTime", "caption": "Snapshot-Uhrzeit (HH:MM, z.B. 23:59)" },
- { "type": "NumberSpinner", "name": "KeepDays", "caption": "Aufbewahrung (Tage)", "minimum": 1, "maximum": 365 }
- ],
- "actions": [
- {
- "type": "Button",
- "caption": "Forecast jetzt aktualisieren",
- "onClick": "IPS_RequestAction($id, \"UpdateForecast\", 0);"
- }
- ]
-}
diff --git a/PV_Forecast_plotmemory/module.html b/PV_Forecast_plotmemory/module.html
deleted file mode 100644
index be31056..0000000
--- a/PV_Forecast_plotmemory/module.html
+++ /dev/null
@@ -1,186 +0,0 @@
-
-
-
-
- PV_Forecast_plotmemory
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/PV_Forecast_plotmemory/module.json b/PV_Forecast_plotmemory/module.json
deleted file mode 100644
index 1b034b7..0000000
--- a/PV_Forecast_plotmemory/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "id": "{DF3B17A3-C2F7-0B57-F632-34668D9206E6}",
- "name": "PV_Forecast_plotmemory ",
- "type": 3,
- "vendor": "Belevo AG",
- "aliases": [],
- "parentRequirements": [],
- "childRequirements": [],
- "implemented": [],
- "prefix": "PVF",
- "url": ""
-}
diff --git a/PV_Forecast_plotmemory/module.php b/PV_Forecast_plotmemory/module.php
deleted file mode 100644
index 05159bd..0000000
--- a/PV_Forecast_plotmemory/module.php
+++ /dev/null
@@ -1,604 +0,0 @@
-RegisterPropertyString("URL", "");
- $this->RegisterPropertyString("kunde", "");
- $this->RegisterPropertyInteger("ActualVariableID", 0);
- $this->RegisterPropertyBoolean("ActualIsWatt", true);
-
- // Forecast fetch scheduler
- $this->RegisterPropertyString("RefreshMode", "interval"); // interval | daily
- $this->RegisterPropertyInteger("RefreshMinutes", 5);
- $this->RegisterPropertyString("RefreshTime", "06:00"); // HH:MM
-
- // Daily snapshot
- $this->RegisterPropertyBoolean("EnableDailySnapshots", true);
- $this->RegisterPropertyString("SnapshotTime", "23:59"); // HH:MM
- $this->RegisterPropertyInteger("KeepDays", 30);
-
- // Important: Prevent tile from triggering URL fetch (strict daily behavior)
- $this->RegisterPropertyBoolean("AllowTileFetchIfEmpty", false);
-
- // Timer
- $this->RegisterTimer("UpdateForecastTimer", 0, 'IPS_RequestAction($_IPS["TARGET"], "UpdateForecast", 0);');
- $this->RegisterTimer("SnapshotTimer", 0, 'IPS_RequestAction($_IPS["TARGET"], "SnapshotTick", 0);');
-
- // WebHook endpoint
- $this->RegisterHook("/hook/solcastcompare_plotmemory");
-
- // Persisted run markers (survive restarts)
- $this->RegisterAttributeInteger("LastDailyRunForecast", 0);
- $this->RegisterAttributeInteger("LastDailySnapshotRun", 0);
- }
-
- public function ApplyChanges()
- {
- parent::ApplyChanges();
-
- // Tile Visualization aktivieren
- $this->SetVisualizationType(1);
-
- // Forecast timer
- $mode = $this->ReadPropertyString("RefreshMode");
- if ($mode === "interval") {
- $mins = max(1, (int)$this->ReadPropertyInteger("RefreshMinutes"));
- $this->SetTimerInterval("UpdateForecastTimer", $mins * 60 * 1000);
- } else {
- // daily: check each minute
- $this->SetTimerInterval("UpdateForecastTimer", 60 * 1000);
- }
-
- // Snapshot timer
- if ($this->ReadPropertyBoolean("EnableDailySnapshots")) {
- $this->SetTimerInterval("SnapshotTimer", 60 * 1000); // every minute
- } else {
- $this->SetTimerInterval("SnapshotTimer", 0);
- }
- }
-
- public function RequestAction($Ident, $Value)
- {
- switch ($Ident) {
- case "UpdateForecast":
- $this->HandleForecastSchedule();
- return;
-
- case "SnapshotTick":
- $this->HandleSnapshotSchedule();
- return;
-
- default:
- throw new Exception("Unknown Ident: " . $Ident);
- }
- }
-
- // ----------------- Forecast scheduling -----------------
-
- private function HandleForecastSchedule(): void
- {
- $mode = $this->ReadPropertyString("RefreshMode");
-
- if ($mode === "interval") {
- $this->UpdateForecast();
- return;
- }
-
- // daily
- $timeStr = trim($this->ReadPropertyString("RefreshTime")); // HH:MM
- $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);
-
- $lastRun = (int)$this->ReadAttributeInteger("LastDailyRunForecast");
- $today0 = strtotime("today");
-
- if ($nowSec >= $targetSec && $lastRun < $today0) {
- $this->SendDebug("Scheduler", "Taegliches Forecast-Update (" . $timeStr . ")", 0);
- $this->UpdateForecast();
- $this->WriteAttributeInteger("LastDailyRunForecast", $now);
- }
- }
-
- // ----------------- Snapshot scheduling -----------------
-
- private function HandleSnapshotSchedule(): void
- {
- if (!$this->ReadPropertyBoolean("EnableDailySnapshots")) {
- return;
- }
-
- $timeStr = trim($this->ReadPropertyString("SnapshotTime"));
- $targetSec = $this->ParseHHMMToSeconds($timeStr);
- if ($targetSec === null) {
- $this->SendDebug("Snapshot", "Ungueltige SnapshotTime: " . $timeStr, 0);
- return;
- }
-
- $now = time();
- $nowSec = ((int)date("H", $now) * 3600) + ((int)date("i", $now) * 60);
-
- // IMPORTANT: use Snapshot marker, not Forecast marker
- $lastRun = (int)$this->ReadAttributeInteger("LastDailySnapshotRun");
- $today0 = strtotime("today");
-
- if ($nowSec >= $targetSec && $lastRun < $today0) {
- $this->SendDebug("Snapshot", "Tages-Snapshot wird erstellt (" . $timeStr . ")", 0);
-
- // Safety: ensure we have a forecast cached (if not, fetch once)
- if ($this->GetBuffer("ForecastRaw") === "") {
- $this->UpdateForecast();
- }
-
- $this->CreateDailySnapshotForDate(new DateTime('today', new DateTimeZone(date_default_timezone_get())));
- $this->CleanupOldSnapshots();
-
- $this->WriteAttributeInteger("LastDailySnapshotRun", $now);
- }
- }
-
- private function ParseHHMMToSeconds(string $hhmm): ?int
- {
- 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 === "") {
- $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);
- }
-
- // ----------------- Tile Visualization -----------------
-
- public function GetVisualizationTile(): string
- {
- $html = file_get_contents(__DIR__ . "/module.html");
- $html = str_replace("{{INSTANCE_ID}}", (string)$this->InstanceID, $html);
- return $html;
- }
-
- // ----------------- WebHook: data + download -----------------
-
- protected function ProcessHookData()
- {
- $instance = isset($_GET["instance"]) ? (int)$_GET["instance"] : 0;
- $action = isset($_GET["action"]) ? (string)$_GET["action"] : "data";
-
- if ($instance !== $this->InstanceID) {
- http_response_code(404);
- echo "Wrong instance";
- return;
- }
-
- if ($action === "data") {
- $this->HandleHookData();
- return;
- }
-
- if ($action === "download") {
- $days = isset($_GET["days"]) ? max(1, min(365, (int)$_GET["days"])) : 7;
- $this->HandleHookDownloadZip($days);
- return;
- }
-
- http_response_code(400);
- echo "Unknown action";
- }
-
- private function HandleHookData(): void
- {
- $tzLocal = new DateTimeZone(date_default_timezone_get());
- $tzUtc = new DateTimeZone('UTC');
-
- // Lokaler Tag
- $startLocal = new DateTime('today', $tzLocal);
- $endLocal = new DateTime('tomorrow', $tzLocal);
-
- $startLocalTs = $startLocal->getTimestamp();
- $endLocalTs = $endLocal->getTimestamp();
-
- // Lokaler Tagesbereich als UTC-Grenzen (Solcast period_end ist UTC "Z")
- $startUtcTs = (clone $startLocal)->setTimezone($tzUtc)->getTimestamp();
- $endUtcTs = (clone $endLocal)->setTimezone($tzUtc)->getTimestamp();
-
- $nowTs = time();
-
- $forecast = $this->GetForecastSeriesFilteredUtc($startUtcTs, $endUtcTs);
- $actual = $this->GetActualSeriesFromArchive($startLocalTs, $endLocalTs, 1800, $nowTs, true);
-
- $out = [
- "meta" => [
- "forecast_cached_at" => (int)$this->GetBuffer("ForecastTS"),
- "bucket_seconds" => 1800,
- "actual_is_watt" => (bool)$this->ReadPropertyBoolean("ActualIsWatt"),
- "start_local" => $startLocalTs * 1000,
- "end_local" => $endLocalTs * 1000,
- "now" => $nowTs * 1000
- ],
- "series" => [
- "forecast" => $forecast,
- "actual" => $actual
- ]
- ];
-
- header("Content-Type: application/json; charset=utf-8");
- echo json_encode($out);
- }
-
- // ----------------- Forecast series (Solcast) -----------------
-
- private function GetForecastSeriesFilteredUtc(int $startUtcTs, int $endUtcTs): array
- {
- $raw = $this->GetBuffer("ForecastRaw");
- if ($raw === "") return [];
-
- $data = json_decode($raw, true);
- if (!is_array($data)) return [];
-
- // forecast endpoint: forecasts; live endpoint: estimated_actuals
- if (isset($data["forecasts"]) && is_array($data["forecasts"])) {
- $rows = $data["forecasts"];
- } elseif (isset($data["estimated_actuals"]) && is_array($data["estimated_actuals"])) {
- $rows = $data["estimated_actuals"];
- } else {
- return [];
- }
-
- $series = [];
- foreach ($rows as $row) {
- if (!isset($row["period_end"], $row["pv_power_rooftop"])) continue;
-
- $ts = strtotime($row["period_end"]); // UTC wegen Z
- if ($ts === false) continue;
-
- if ($ts < $startUtcTs || $ts >= $endUtcTs) continue;
-
- // Solcast: kW
- $series[] = [$ts * 1000, (float)$row["pv_power_rooftop"]];
- }
-
- usort($series, fn($a, $b) => $a[0] <=> $b[0]);
- return $series;
- }
-
- // ----------------- Actual series (Archive) -----------------
- // $cutAfterNow=true -> future buckets = null (live view)
- // $cutAfterNow=false -> keep as much as possible (snapshot)
- private function GetActualSeriesFromArchive(int $startTs, int $endTs, int $bucketSeconds, int $nowTs, bool $cutAfterNow): 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 $this->BuildNullRaster($startTs, $endTs, $bucketSeconds);
- }
-
- // bucketEnd -> [sum, count]
- $buckets = [];
- 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;
- }
-
- $series = [];
- $isWatt = (bool)$this->ReadPropertyBoolean("ActualIsWatt");
-
- for ($t = $startTs + $bucketSeconds; $t <= $endTs; $t += $bucketSeconds) {
-
- if ($cutAfterNow && $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
- if ($isWatt) $avg = $avg / 1000.0;
-
- $series[] = [$t * 1000, $avg];
- }
-
- return $series;
- }
-
- private function BuildNullRaster(int $startTs, int $endTs, int $bucketSeconds): array
- {
- $series = [];
- for ($t = $startTs + $bucketSeconds; $t <= $endTs; $t += $bucketSeconds) {
- $series[] = [$t * 1000, null];
- }
- return $series;
- }
-
- private function GetArchiveInstanceID(): int
- {
- $list = IPS_GetInstanceListByModuleID(self::ARCHIVE_GUID);
- return (is_array($list) && count($list) > 0) ? (int)$list[0] : 0;
- }
-
- // ----------------- Daily snapshot creation -----------------
-
- private function GetStorageDir(): string
- {
- $dir = __DIR__ . "/storage";
- if (!is_dir($dir)) {
- @mkdir($dir, 0775, true);
- }
- return $dir;
- }
-
- private function CreateDailySnapshotForDate(DateTime $dayLocal): void
- {
- $tzLocal = new DateTimeZone(date_default_timezone_get());
- $tzUtc = new DateTimeZone('UTC');
-
- // dayLocal should be "today" in local TZ
- $dayLocal->setTimezone($tzLocal);
-
- $startLocal = (clone $dayLocal);
- $startLocal->setTime(0, 0, 0);
-
- $endLocal = (clone $startLocal);
- $endLocal->modify('+1 day');
-
- $startLocalTs = $startLocal->getTimestamp();
- $endLocalTs = $endLocal->getTimestamp();
-
- $startUtcTs = (clone $startLocal)->setTimezone($tzUtc)->getTimestamp();
- $endUtcTs = (clone $endLocal)->setTimezone($tzUtc)->getTimestamp();
-
- $nowTs = time(); // snapshot time (typically 23:59)
-
- $forecast = $this->GetForecastSeriesFilteredUtc($startUtcTs, $endUtcTs);
- $actual = $this->GetActualSeriesFromArchive($startLocalTs, $endLocalTs, 1800, $nowTs, false);
-
- $dateStr = $startLocal->format('Y-m-d');
-
- $payload = [
- "meta" => [
- "date" => $dateStr,
- "created_at" => time(),
- "forecast_cached_at" => (int)$this->GetBuffer("ForecastTS"),
- "bucket_seconds" => 1800,
- "actual_is_watt" => (bool)$this->ReadPropertyBoolean("ActualIsWatt"),
- "timezone" => $tzLocal->getName()
- ],
- "series" => [
- "forecast" => $forecast,
- "actual" => $actual
- ]
- ];
-
- $dir = $this->GetStorageDir();
-
- // Save JSON
- $jsonPath = $dir . "/" . $dateStr . ".json";
- @file_put_contents($jsonPath, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
-
- // Save HTML plot
- $htmlPath = $dir . "/" . $dateStr . ".html";
- @file_put_contents($htmlPath, $this->BuildSnapshotHtml($payload));
-
- $this->SendDebug("Snapshot", "Gespeichert: " . basename($jsonPath) . " / " . basename($htmlPath), 0);
- }
-
- private function BuildSnapshotHtml(array $payload): string
- {
- $date = $payload["meta"]["date"] ?? "unbekannt";
- $forecastJson = json_encode($payload["series"]["forecast"] ?? []);
- $actualJson = json_encode($payload["series"]["actual"] ?? []);
-
- return '
-
-
-
- PV Snapshot ' . htmlspecialchars($date) . '
-
-
-
-
- PV: Erwartung vs. Tatsächlich (' . htmlspecialchars($date) . ')
- Gespeichert: ' . date('d.m.Y H:i:s', (int)($payload["meta"]["created_at"] ?? time())) . '
-
-
-
-
-';
- }
-
- private function CleanupOldSnapshots(): void
- {
- $keep = max(1, (int)$this->ReadPropertyInteger("KeepDays"));
- $dir = $this->GetStorageDir();
-
- $cutoff = strtotime("today") - ($keep * 86400);
-
- foreach (glob($dir . "/*.json") as $file) {
- $base = basename($file, ".json"); // YYYY-MM-DD
- $ts = strtotime($base);
- if ($ts !== false && $ts < $cutoff) {
- @unlink($file);
- $html = $dir . "/" . $base . ".html";
- if (is_file($html)) @unlink($html);
- }
- }
- }
-
- // ----------------- ZIP download -----------------
-
- private function HandleHookDownloadZip(int $days): void
- {
- $dir = $this->GetStorageDir();
-
- $dates = [];
- for ($i = 0; $i < $days; $i++) {
- $d = new DateTime('today', new DateTimeZone(date_default_timezone_get()));
- $d->modify("-{$i} day");
- $dates[] = $d->format("Y-m-d");
- }
-
- $zipPath = sys_get_temp_dir() . "/pv_forecast_" . $this->InstanceID . "_" . time() . ".zip";
- $zip = new ZipArchive();
- if ($zip->open($zipPath, ZipArchive::CREATE) !== true) {
- http_response_code(500);
- echo "Could not create zip";
- return;
- }
-
- $added = 0;
- foreach (array_reverse($dates) as $dateStr) {
- $jsonFile = $dir . "/" . $dateStr . ".json";
- $htmlFile = $dir . "/" . $dateStr . ".html";
-
- if (is_file($htmlFile)) {
- $zip->addFile($htmlFile, $dateStr . ".html");
- $added++;
- }
- if (is_file($jsonFile)) {
- $zip->addFile($jsonFile, $dateStr . ".json");
- $added++;
- }
- }
-
- $zip->close();
-
- if ($added === 0) {
- @unlink($zipPath);
- http_response_code(404);
- echo "No snapshots found";
- return;
- }
-
- $kunde = trim($this->ReadPropertyString("kunde"));
- if ($kunde === "") {
- $kunde = "unbekannt";
- }
-
- // Sonderzeichen entfernen (für Dateinamen!)
- $kundeSafe = preg_replace('/[^a-zA-Z0-9_-]/', '_', $kunde);
-
- header("Content-Type: application/zip");
- header(
- 'Content-Disposition: attachment; filename="pv_plots_' .
- $kundeSafe . '_' .
- $this->InstanceID . '_' .
- $days . 'days.zip"'
- );
- header("Content-Length: " . filesize($zipPath));
-
- readfile($zipPath);
- @unlink($zipPath);
- }
-
- // ----------------- Hook registration -----------------
-
- private function RegisterHook(string $Hook)
- {
- $ids = IPS_GetInstanceListByModuleID(self::WEBHOOK_GUID);
- 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"] === ltrim($Hook, "/")) {
- return;
- }
- if (isset($h["Hook"]) && $h["Hook"] === $Hook) {
- return;
- }
- }
-
- $hooks[] = ["Hook" => $Hook, "TargetID" => $this->InstanceID];
- IPS_SetProperty($hookID, "Hooks", json_encode($hooks));
- IPS_ApplyChanges($hookID);
- }
-}
diff --git a/PV_Forecast_plotmemory/readme.md b/PV_Forecast_plotmemory/readme.md
deleted file mode 100644
index bc8dd0b..0000000
--- a/PV_Forecast_plotmemory/readme.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# 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);
-```
diff --git a/PV_Visu/form.json b/PV_Visu/form.json
deleted file mode 100644
index 1acde8f..0000000
--- a/PV_Visu/form.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "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)" }
- ],
- "actions": []
-}
diff --git a/PV_Visu/module.html b/PV_Visu/module.html
deleted file mode 100644
index 4d2975c..0000000
--- a/PV_Visu/module.html
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
-
-
-
Produktion (Eigenverbrauch / Einspeisung)
-
-
-
-
-
Verbrauch (PV / Netz)
-
-
-
-
-
-
-
diff --git a/PV_Visu/module.json b/PV_Visu/module.json
deleted file mode 100644
index dfbdcba..0000000
--- a/PV_Visu/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "id": "{DDE89CBE-4411-5FF4-4931-14204E05CAD0}",
- "name": "PV_Visu",
- "type": 3,
- "vendor": "Belevo AG",
- "aliases": [],
- "parentRequirements": [],
- "childRequirements": [],
- "implemented": [],
- "prefix": "",
- "url": ""
-}
diff --git a/PV_Visu/module.php b/PV_Visu/module.php
deleted file mode 100644
index 475b3be..0000000
--- a/PV_Visu/module.php
+++ /dev/null
@@ -1,99 +0,0 @@
-RegisterPropertyInteger('VarProduction', 0);
- $this->RegisterPropertyInteger('VarConsumption', 0);
- $this->RegisterPropertyInteger('VarFeedIn', 0);
- $this->RegisterPropertyInteger('VarGrid', 0);
-
- $this->RegisterVariableString('JSONData', 'Visualisierungsdaten', '', 0);
- IPS_SetHidden($this->GetIDForIdent('JSONData'), true);
-
- $this->SetVisualizationType(1); // HTML SDK Tile
- }
-
- public function ApplyChanges()
- {
- parent::ApplyChanges();
-
- foreach (['VarProduction', 'VarConsumption', 'VarFeedIn', 'VarGrid'] as $prop) {
- $vid = $this->ReadPropertyInteger($prop);
- if ($vid > 0) {
- $this->RegisterMessage($vid, VM_UPDATE);
- }
- }
-
- $this->UpdateData(); // Initial
- }
-
- public function MessageSink($TimeStamp, $SenderID, $Message, $Data)
- {
- if ($Message === VM_UPDATE) {
- $this->UpdateData();
- }
- }
-
- public function GetVisualizationTile()
- {
- $initialData = '';
- $html = file_get_contents(__DIR__ . '/module.html');
- return $html . $initialData;
- }
-
- public function RequestAction($Ident, $Value)
- {
- if ($Ident === 'update') {
- return $this->UpdateData(); // Rückgabe für Visualisierung
- }
- throw new \Exception("Unknown Ident: $Ident");
- }
-
- public function UpdateData()
- {
- $start = strtotime('today 00:00');
- $end = time();
-
- $prod = $this->GetDailyTotal($this->ReadPropertyInteger('VarProduction'), $start, $end);
- $cons = $this->GetDailyTotal($this->ReadPropertyInteger('VarConsumption'), $start, $end);
- $feed = $this->GetDailyTotal($this->ReadPropertyInteger('VarFeedIn'), $start, $end);
- $grid = $this->GetDailyTotal($this->ReadPropertyInteger('VarGrid'), $start, $end);
-
- $prodCons = $prod > 0 ? (($cons - $grid) / $prod) * 100 : 0;
- $prodFeed = $prod > 0 ? 100 - $prodCons : 0;
- $consPV = $cons > 0 ? min($prod, ($cons - $grid)) / $cons * 100 : 0;
- $consGrid = $cons > 0 ? 100 - $consPV : 0;
-
- $data = [
- 'prodCons' => round($prodCons, 1),
- 'prodFeed' => round($prodFeed, 1),
- 'consPV' => round($consPV, 1),
- 'consGrid' => round($consGrid, 1),
- 'value' => [
- 'prod' => round($prod, 2),
- 'cons' => round($cons, 2),
- 'feed' => round($feed, 2),
- 'grid' => round($grid, 2),
- ],
- ];
-
- $json = json_encode($data);
- SetValueString($this->GetIDForIdent('JSONData'), $json);
- return $data;
- }
-
- private function GetDailyTotal(int $varID, int $start, int $end)
- {
- if ($varID <= 0) return 0.0;
-
- $archiveID = @IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
- if (!$archiveID) return 0.0;
-
- $values = @AC_GetAggregatedValues($archiveID, $varID, 1, $start, $end, 1);
- return isset($values[0]['Avg']) ? (float)$values[0]['Avg'] : 0.0;
- }
-}
\ No newline at end of file
diff --git a/PV_Visu/readme.md b/PV_Visu/readme.md
deleted file mode 100644
index bc8dd0b..0000000
--- a/PV_Visu/readme.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# 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);
-```
diff --git a/SofarWechselrichter/README.md b/SofarWechselrichter/README.md
deleted file mode 100644
index 4fa144c..0000000
--- a/SofarWechselrichter/README.md
+++ /dev/null
@@ -1,105 +0,0 @@
-2. Starte IP-Symcon neu bzw. klicke in der Konsole auf „Module aktualisieren“.
-
-Anschließend erscheint in der Instanzliste der neue Gerätetyp **Sofar Wechselrichter**.
-
----
-
-## Konfiguration
-
-1. **Instanz anlegen**
-- Gehe in der IPS-Konsole auf „Instanzen hinzufügen“ → Hersteller: (falls sichtbar) → Modul: **Sofar Wechselrichter**.
-- Vergib einen sinnvollen Namen und eine Beschreibung.
-
-2. **Einstellungen in den Eigenschaften**
-- **Logger-Seriennummer**:
- Gib hier die „Logger-Nummer“ deines Sofar-Wechselrichters ein (Dezimal).
-- **Abfragezyklus (Sekunden)**:
- Intervall in Sekunden, in dem zyklisch alle eingetragenen Register abgefragt werden.
- Ein Wert von `0` deaktiviert den Timer (keine zyklischen Abfragen).
-- **Register-Tabelle**:
- Hier definierst du beliebig viele Zeilen, jeweils mit:
- 1. **Register-Nummer** (dezimal)
- 2. **Bezeichnung** (z. B. „Gesamtproduktion“ oder „Spannung Phase L1“)
- 3. **Skalierungs-faktor** (z. B. `0.1`, `1`, `10` usw.)
-
- Wird beispielsweise für Register `1476` als Bezeichnung „Gesamtproduktion“ mit Skalierungsfaktor `1` eingetragen,
- so liest das Modul alle 60 Sekunden (oder den von dir gewählten Zyklus) Register 1476,
- multipliziert das rohe UINT16-Ergebnis mit `1` und legt den Wert in einer IPS-Variable an.
-
-3. **Speichern/Übernehmen**
-Klicke auf „Übernehmen“, um die Änderungen zu übernehmen.
-Das Modul legt automatisch untergeordnete IPS-Variablen an:
-
-- **Vorheriger Wert (Register 1160)**
- → INT16BE, unverändert (analog zur alten Node-RED-Logik).
- Diese Variable heißt intern `Vorheriger Wert` und wird automatisch gepflegt.
-- **Alle weiteren Einträge aus der Register-Tabelle**
- → Für jede Zeile wird eine Float-Variable mit der von dir angegebenen „Bezeichnung“ angelegt.
- Der Variablen-Identifier lautet automatisch `Reg` (z. B. `Reg1476`).
-
----
-
-## Funktionsweise
-
-1. **Initialisierung (ApplyChanges)**
-- Liest den Abfragezyklus aus den Moduleigenschaften und initialisiert den Timer.
-- Legt eine Integer-Variable `Vorheriger Wert` (Reg 1160) an.
-- Legt für jede Zeile in der Register-Tabelle eine Float-Variable an (Ident `Reg`).
-
-2. **Zyklische Abfrage (Timer-Callback)**
-- **Register 1160 (INT16BE)**
- → Wird als „Vorheriger Wert“ in die Variable geschrieben (signed interpretiert).
-- **Alle weiteren Register aus der Tabelle**
- → Jedes Register wird per Modbus-ähnlichem TCP-Paketaustausch abgefragt, als UINT16 ausgelesen,
- mit dem angegebenen Skalierungsfaktor multipliziert und in der zugehörigen Float-Variable gespeichert.
-
-3. **Kommunikation**
-- TCP-Verbindung zu `192.168.0.0:8899` (feste IP im Code).
-- Der Aufruf von `readRegister()` baut ein „Out_Frame“ wie in Node-RED,
- rechnet CRC16-Modbus über die letzten 6 Bytes, hängt eine Summen-Checksum + 0x15 an,
- sendet das Paket, liest die Antwort, schneidet exakt 2 Daten-Bytes heraus und liefert sie zurück.
-
----
-
-## Beispiel: Register 1476 („Gesamtproduktion“)
-
-- **Register-Tabelle**
-| Register‐Nummer | Bezeichnung | Skalierungsfaktor |
-| --------------: | :------------------ | ----------------: |
-| 1476 | Gesamtproduktion | 1 |
-
-- **Ergebnis**
-- Eine Float-Variable mit der Bezeichnung „Gesamtproduktion“ wird angelegt.
-- Wenn der PollInterval auf `60` Sekunden steht, liest das Modul alle 60 Sekunden das Register 1476,
- skaliert mit 1 und schreibt den numerischen Wert in `Reg1476`.
-
----
-
-## Fehlersuche
-
-- Falls die Variable „Vorheriger Wert“ immer denselben Wert liefert oder ein Lesefehler auftritt, prüfe bitte:
-1. **Logger-Nummer**: Ist sie korrekt (Dezimal)?
-2. **Netzwerk/Firewall**: Kann Symcon die Adresse `192.168.0.100:8899` erreichen?
-3. **Debug-Ausgaben**:
- – Öffne in der Konsole „Kernel-Log“ → Filter „SofarWechselrichter“.
- – Dort werden WARNs und ERRs protokolliert, falls z. B. keine Antwort kommt oder das Datenpaket inkorrekt ist.
-
-- Falls du andere Datentypen brauchst (z. B. INT16 für Register außerhalb 1160), definiere sie analog als separate Zeile:
-– Trage die `Register‐Nummer` ein, gib als Skalierungsfaktor `1` (oder `0.1` etc.) an.
-– Der absolute Rohwert wird stets als UINT16 interpretiert (0–65535).
-– Solltest du negative INT16 benötigen, kannst du nachträglich einfach die Variable „Vorheriger Wert“ (Reg 1160)
- als Beispiel nehmen und in einem Script umrechnen (Werte über 32767 → –32768 + Rest).
-
----
-
-## Versionshistorie
-
-- **1.0**
-- Erstveröffentlichung:
- • Zyklische Abfrage beliebiger Register in einer Matrix konfigurieren
- • Automatische Anlage von Variablen für jeden Eintrag
- • Spezieller „Vorheriger Wert“ (Register 1160 als INT16)
-
----
-
-*Ende der Dokumentation.*
diff --git a/SofarWechselrichter/form.json b/SofarWechselrichter/form.json
deleted file mode 100644
index c7c602b..0000000
--- a/SofarWechselrichter/form.json
+++ /dev/null
@@ -1,106 +0,0 @@
-{
- "elements": [
- {
- "type": "Label",
- "caption": "Sofar Wechselrichter Konfiguration"
- },
- {
- "type": "ValidationTextBox",
- "name": "IPAddress",
- "caption": "Inverter IP-Adresse"
- },
- {
- "type": "ValidationTextBox",
- "name": "LoggerNumber",
- "caption": "Logger-Seriennummer"
- },
- {
- "type": "NumberSpinner",
- "name": "PollInterval",
- "caption": "Abfragezyklus (Sekunden)",
- "minimum": 1,
- "suffix": "s"
- },
- {
- "type": "List",
- "name": "Registers",
- "caption": "Register-Tabelle",
- "add": "Neues Register hinzufügen",
- "delete": "Lösche Eintrag",
- "columns": [
- {
- "caption": "Register-Nummer",
- "name": "RegisterNumber",
- "width": "200px",
- "add": 0,
- "edit": {
- "type": "NumberSpinner",
- "minimum": 0
- }
- },
- {
- "caption": "Bezeichnung",
- "name": "Label",
- "width": "300px",
- "add": "",
- "edit": {
- "type": "ValidationTextBox"
- }
- },
- {
- "caption": "Skalierungs-faktor",
- "name": "ScalingFactor",
- "width": "200px",
- "add": 1,
- "edit": {
- "type": "NumberSpinner",
- "digits": 4,
- "minimum": -999999,
- "maximum": 999999
- }
- },
- {
- "caption": "Endian",
- "name": "Endian",
- "width": "80px",
- "add": "BE",
- "edit": {
- "type": "Select",
- "options": [
- { "caption": "BE", "value": "BE" },
- { "caption": "LE", "value": "LE" }
- ]
- }
- },
- {
- "caption": "Bit-Länge",
- "name": "BitLength",
- "width": "80px",
- "add": "16",
- "edit": {
- "type": "Select",
- "options": [
- { "caption": "16 Bit", "value": "16" },
- { "caption": "32 Bit", "value": "32" },
- { "caption": "64 Bit", "value": "64" }
- ]
- }
- },
- {
- "caption": "Signed/Unsigned",
- "name": "Signedness",
- "width": "100px",
- "add": "Unsigned",
- "edit": {
- "type": "Select",
- "options": [
- { "caption": "Unsigned", "value": "Unsigned" },
- { "caption": "Signed", "value": "Signed" }
- ]
- }
- }
- ]
- }
- ],
- "actions": []
-}
diff --git a/SofarWechselrichter/module.json b/SofarWechselrichter/module.json
deleted file mode 100644
index 1d594c2..0000000
--- a/SofarWechselrichter/module.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "id": "{C26E97C8-BA00-0563-6B14-B807C5ACE17F}",
- "name": "SofarWechselrichter",
- "type": 3,
- "vendor": "Belevo AG",
- "aliases": [],
- "parentRequirements": [],
- "childRequirements": [],
- "implemented": [],
- "prefix": "GEF",
- "url": ""
-}
diff --git a/SofarWechselrichter/module.php b/SofarWechselrichter/module.php
deleted file mode 100644
index e289af9..0000000
--- a/SofarWechselrichter/module.php
+++ /dev/null
@@ -1,379 +0,0 @@
-RegisterPropertyString('IPAddress', '');
- $this->RegisterPropertyString('LoggerNumber', '0'); // als String
- $this->RegisterPropertyInteger('PollInterval', 60);
- $this->RegisterPropertyString('Registers', '[]'); // JSON-String
-
- // Timer für zyklische Abfragen (per RequestAction("Query"))
- $script = 'IPS_RequestAction(' . $this->InstanceID . ', "Query", "");';
- $this->RegisterTimer('QueryTimer', 0, $script);
- }
-
- public function ApplyChanges()
- {
- parent::ApplyChanges();
- // Timer-Intervall (ms)
- $intervalSec = $this->ReadPropertyInteger('PollInterval');
- $intervalMs = ($intervalSec > 0) ? $intervalSec * 1000 : 0;
- $this->SetTimerInterval('QueryTimer', $intervalMs);
-
- // Aktuelle Registerliste
- $registers = json_decode($this->ReadPropertyString('Registers'), true);
- if (!is_array($registers)) {
- $registers = [];
- }
-
- // 1) Variablen anlegen (neu hinzugekommene Register)
- $position = 10;
- foreach ($registers as $entry) {
- $regNo = (int) $entry['RegisterNumber'];
- $label = trim($entry['Label']);
- if ($regNo < 0 || $label === '') {
- continue;
- }
- $ident = 'Reg' . $regNo;
- if (!$this->VariableExists($ident)) {
- $this->RegisterVariableFloat($ident, $label, '', $position);
- }
- $position += 10;
- }
-
- // 2) Variablen löschen (falls Register entfernt wurden)
- $validIdents = [];
- foreach ($registers as $entry) {
- $validIdents[] = 'Reg' . ((int)$entry['RegisterNumber']);
- }
- $children = IPS_GetChildrenIDs($this->InstanceID);
- foreach ($children as $childID) {
- $obj = IPS_GetObject($childID);
- if ($obj['ObjectType'] !== 2) {
- continue;
- }
- $ident = $obj['ObjectIdent'];
- if (substr($ident, 0, 3) === 'Reg') {
- if (!in_array($ident, $validIdents)) {
- IPS_DeleteVariable($childID);
- }
- }
- }
- }
-
- public function Destroy()
- {
- parent::Destroy();
- }
-
- /**
- * Wird aufgerufen durch IPS_RequestAction über den Timer
- */
- public function RequestAction($Ident, $Value)
- {
- switch ($Ident) {
- case 'Query':
- $this->Query();
- break;
- default:
- throw new Exception('Ungültiger Ident: ' . $Ident);
- }
- }
-
- /**
- * Zyklische Abfrage aller definierten Register
- */
- private function Query(): void
- {
- $this->LogMessage('Query invoked', KL_MESSAGE);
-
- $ip = trim($this->ReadPropertyString('IPAddress'));
- $loggerNumberStr = trim($this->ReadPropertyString('LoggerNumber'));
-
- // 1) Validierung IP
- if (!filter_var($ip, FILTER_VALIDATE_IP)) {
- $this->LogMessage('Abbruch: Ungültige IP = "' . $ip . '"', KL_WARNING);
- return;
- }
- // 2) Validierung LoggerNumber (Dezimal-String, > 0)
- if ($loggerNumberStr === '' || !ctype_digit($loggerNumberStr) || bccomp($loggerNumberStr, '1') < 0) {
- $this->LogMessage('Abbruch: Ungültige LoggerNumber = "' . $loggerNumberStr . '"', KL_WARNING);
- return;
- }
-
- // 3) Register-Liste einlesen
- $registers = json_decode($this->ReadPropertyString('Registers'), true);
- if (!is_array($registers) || count($registers) === 0) {
- // Keine Register definiert
- return;
- }
-
- // 4) Für jedes Register: einzeln auslesen, zusammensetzen, skalieren, speichern
- foreach ($registers as $entry) {
- $regNo = (int) $entry['RegisterNumber'];
- $label = trim((string)$entry['Label']);
- $scale = (string) $entry['ScalingFactor']; // kann negativ sein
- $endian = strtoupper(trim((string)$entry['Endian']));
- $bitLength = (int) $entry['BitLength']; // 16, 32 oder 64
- $signedness = trim((string)$entry['Signedness']); // "Signed" oder "Unsigned"
- $ident = 'Reg' . $regNo;
-
- if ($regNo < 0 || $label === '' || !in_array($bitLength, [16, 32, 64])) {
- continue;
- }
-
- try {
- $numRegs = $bitLength / 16; // 1, 2 oder 4
- // Bytes registerweise einzeln abfragen und zusammenfügen:
- $dataBytes = '';
- for ($i = 0; $i < $numRegs; $i++) {
- $chunk = $this->readSingleRegister($ip, $loggerNumberStr, $regNo + $i);
- $dataBytes .= $chunk;
- }
- // Debug: raw combined response hex
- $combinedHex = strtoupper(bin2hex($dataBytes));
- $this->LogMessage("Raw data for Reg {$regNo} ({$bitLength}bit): {$combinedHex}", KL_MESSAGE);
-
- // Endian-Handling: falls LE, kehre gesamte Byte-Reihenfolge um
- if ($endian === 'LE') {
- $combinedHex = $this->reverseByteOrder($combinedHex);
- $this->LogMessage("After LE reverse: {$combinedHex}", KL_MESSAGE);
- }
-
- // Konvertiere Hex in Dezimal-String
- $rawDec = $this->hexToDecimal($combinedHex);
-
- // Bei "Signed" → Zwei-Komplement-Umrechnung
- if ($signedness === 'Signed') {
- $half = bcpow('2', (string)($bitLength - 1), 0); // 2^(bitLength-1)
- $fullRange = bcpow('2', (string)$bitLength, 0); // 2^bitLength
- if (bccomp($rawDec, $half) >= 0) {
- $rawDec = bcsub($rawDec, $fullRange, 0);
- }
- $this->LogMessage("Signed rawDec for Reg {$regNo}: {$rawDec}", KL_MESSAGE);
- }
-
- // Skaliere (bc*-Multiplikation, 4 Nachkommastellen)
- $valueStr = bcmul($rawDec, $scale, 4);
- SetValueFloat($this->GetIDForIdent($ident), (float)$valueStr);
- $this->LogMessage("Final value for Reg {$regNo}: {$valueStr}", KL_MESSAGE);
- } catch (Exception $e) {
- $this->LogMessage(
- "Fehler Lesen Reg {$regNo} ({$bitLength}bit, {$signedness}): " . $e->getMessage(),
- KL_WARNING
- );
- }
- }
- }
-
- /**
- * Liest genau ein Register (16 Bit) per Modbus-ähnlichem TCP (2 Bytes zurück).
- *
- * @param string $ip Inverter-IP
- * @param string $serial_nr_str Logger-Seriennummer als Dezimal-String
- * @param int $reg Register-Adresse
- * @return string 2-Byte-Binär-String
- * @throws Exception Bei Kommunikationsfehlern
- */
- private function readSingleRegister(string $ip, string $serial_nr_str, int $reg): string
- {
- // 1) Out_Frame ohne CRC aufbauen
- $oFrame = 'a5170010450000';
-
- // Dezimal-String → 8-stellige Hex
- $hexSN8 = $this->decStringToHex8($serial_nr_str);
- $hexSNbytes = [
- substr($hexSN8, 6, 2),
- substr($hexSN8, 4, 2),
- substr($hexSN8, 2, 2),
- substr($hexSN8, 0, 2),
- ];
- $oFrame .= implode('', $hexSNbytes);
-
- // Data-Field (16 Hex-Zeichen konstant)
- $oFrame .= '020000000000000000000000000000';
-
- // Business-Field: 01 03 + Start-Register + Anzahl Register (1)
- $startHex = str_pad(dechex($reg), 4, '0', STR_PAD_LEFT);
- $numHex = str_pad(dechex(1), 4, '0', STR_PAD_LEFT);
- $oFrame .= '0103' . $startHex . $numHex;
-
- // 2) CRC16-Modbus (letzte 6 Bytes)
- $crcInputHex = substr($oFrame, -12);
- $crcInputBin = hex2bin($crcInputHex);
- if ($crcInputBin === false) {
- throw new Exception("Ungültiges Hex in CRC-Input: {$crcInputHex}");
- }
- $crcValue = $this->calculateCRC16Modbus($crcInputBin);
- $crcHex = strtoupper(str_pad(dechex($crcValue), 4, '0', STR_PAD_LEFT));
- $crcSwapped = substr($crcHex, 2, 2) . substr($crcHex, 0, 2);
- $oFrameWithCRC = $oFrame . strtolower($crcSwapped);
-
- // 3) Summen-Checksum (alle Bytes ab Index 1) + 0x15
- $l = strlen($oFrameWithCRC) / 2;
- $bArr = [];
- for ($i = 0; $i < $l; $i++) {
- $byteHex = substr($oFrameWithCRC, 2 * $i, 2);
- $bArr[$i] = hexdec($byteHex);
- }
- $crcSum = 0;
- for ($i = 1; $i < $l; $i++) {
- $crcSum += $bArr[$i];
- $crcSum &= 0xFF;
- }
- $bArr[$l] = $crcSum;
- $bArr[$l+1] = 0x15;
-
- $frameBin = '';
- foreach ($bArr as $b) {
- $frameBin .= chr($b);
- }
-
- // 4) TCP-Verbindung öffnen & Paket senden (Port fest 8899)
- $port = 8899;
- $fp = @stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr, 5);
- if (!$fp) {
- throw new Exception("Verbindung zu {$ip}:{$port} fehlgeschlagen ({$errno}: {$errstr})");
- }
- fwrite($fp, $frameBin);
- stream_set_timeout($fp, 2);
-
- // 5) Antwort einlesen
- $response = '';
- while (!feof($fp)) {
- $chunk = fread($fp, 1024);
- if ($chunk === false || $chunk === '') {
- break;
- }
- $response .= $chunk;
- }
- fclose($fp);
- if ($response === '') {
- throw new Exception("Keine Antwort vom Inverter erhalten.");
- }
-
- // Debug: log raw response hex
- $respHex = strtoupper(bin2hex($response));
- $this->LogMessage("Raw response for single reg {$reg}: {$respHex}", KL_MESSAGE);
-
- // 6) Slice-Logik: l = 2*1 + 4 = 6, slice(-6, 2) → 2 Bytes
- $lModbus = 2 * 1 + 4; // = 6
- $numBytes = 2;
- if (strlen($response) < $lModbus) {
- throw new Exception("Unerwartet kurze Antwort (< {$lModbus} Bytes).");
- }
- $dataBytes = substr($response, -$lModbus, $numBytes);
- if (strlen($dataBytes) < $numBytes) {
- throw new Exception("Data-Segment enthält weniger als {$numBytes} Bytes.");
- }
- $dataHex = strtoupper(bin2hex($dataBytes));
- $this->LogMessage("Sliced data for single reg {$reg}: {$dataHex}", KL_MESSAGE);
-
- return $dataBytes;
- }
-
- /**
- * Wandelt einen Dezimal-String in einen 8-stelligen Hex-String um.
- *
- * @param string $decString
- * @return string 8-stellige Hex (uppercase)
- */
- private function decStringToHex8(string $decString): string
- {
- $num = ltrim($decString, '0');
- if ($num === '') {
- return '00000000';
- }
- $hex = '';
- while (bccomp($num, '0') > 0) {
- $mod = bcmod($num, '16');
- $digit = dechex((int)$mod);
- $hex = strtoupper($digit) . $hex;
- $num = bcdiv($num, '16', 0);
- }
- return str_pad($hex, 8, '0', STR_PAD_LEFT);
- }
-
- /**
- * Kehrt die Byte-Reihenfolge eines Hex-Strings um (2 Hex-Zeichen = 1 Byte).
- *
- * @param string $hex Hex-Repr. (z.B. "A1B2C3D4")
- * @return string Umgekehrte Byte-Reihenfolge (z.B. "D4C3B2A1")
- */
- private function reverseByteOrder(string $hex): string
- {
- $bytes = str_split($hex, 2);
- $bytes = array_reverse($bytes);
- return implode('', $bytes);
- }
-
- /**
- * Konvertiert einen Hex-String in einen Dezimal-String (BCMath).
- *
- * @param string $hex Uppercase-Hex ohne Präfix (z.B. "00FF10A3")
- * @return string Dezimal-String (z.B. "16737763")
- */
- private function hexToDecimal(string $hex): string
- {
- $hex = ltrim($hex, '0');
- if ($hex === '') {
- return '0';
- }
- $len = strlen($hex);
- $dec = '0';
- $power16 = '1';
- for ($i = $len - 1; $i >= 0; $i--) {
- $digit = hexdec($hex[$i]);
- $term = bcmul((string)$digit, $power16, 0);
- $dec = bcadd($dec, $term, 0);
- $power16 = bcmul($power16, '16', 0);
- }
- return $dec;
- }
-
- /**
- * Berechnet CRC16-Modbus (Init=0xFFFF, Poly=0xA001) über Binärdaten.
- */
- private function calculateCRC16Modbus(string $binaryData): int
- {
- $crc = 0xFFFF;
- $len = strlen($binaryData);
- for ($pos = 0; $pos < $len; $pos++) {
- $crc ^= ord($binaryData[$pos]);
- for ($i = 0; $i < 8; $i++) {
- if (($crc & 0x0001) !== 0) {
- $crc >>= 1;
- $crc ^= 0xA001;
- } else {
- $crc >>= 1;
- }
- }
- }
- return $crc;
- }
-
- /**
- * Prüft, ob eine Variable mit Ident existiert.
- */
- private function VariableExists(string $ident): bool
- {
- $vid = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
- return ($vid !== false && IPS_VariableExists($vid));
- }
-}