diff --git a/Bat_EV_SDL_V3_Beta/README.md b/Bat_EV_SDL_V3_Beta/README.md new file mode 100644 index 0000000..b059e3a --- /dev/null +++ b/Bat_EV_SDL_V3_Beta/README.md @@ -0,0 +1,67 @@ +# Manager_1 +Beschreibung des Moduls. + +### Inhaltsverzeichnis + +1. [Funktionsumfang](#1-funktionsumfang) +2. [Voraussetzungen](#2-voraussetzungen) +3. [Software-Installation](#3-software-installation) +4. [Einrichten der Instanzen in IP-Symcon](#4-einrichten-der-instanzen-in-ip-symcon) +5. [Statusvariablen und Profile](#5-statusvariablen-und-profile) +6. [WebFront](#6-webfront) +7. [PHP-Befehlsreferenz](#7-php-befehlsreferenz) + +### 1. Funktionsumfang + +* + +### 2. Voraussetzungen + +- IP-Symcon ab Version 7.1 + +### 3. Software-Installation + +* Über den Module Store das 'Manager_1'-Modul installieren. +* Alternativ über das Module Control folgende URL hinzufügen + +### 4. Einrichten der Instanzen in IP-Symcon + + Unter 'Instanz hinzufügen' kann das 'Manager_1'-Modul mithilfe des Schnellfilters gefunden werden. + - Weitere Informationen zum Hinzufügen von Instanzen in der [Dokumentation der Instanzen](https://www.symcon.de/service/dokumentation/konzepte/instanzen/#Instanz_hinzufügen) + +__Konfigurationsseite__: + +Name | Beschreibung +-------- | ------------------ + | + | + +### 5. Statusvariablen und Profile + +Die Statusvariablen/Kategorien werden automatisch angelegt. Das Löschen einzelner kann zu Fehlfunktionen führen. + +#### Statusvariablen + +Name | Typ | Beschreibung +------ | ------- | ------------ + | | + | | + +#### Profile + +Name | Typ +------ | ------- + | + | + +### 6. WebFront + +Die Funktionalität, die das Modul im WebFront bietet. + +### 7. PHP-Befehlsreferenz + +`boolean GEF_BeispielFunktion(integer $InstanzID);` +Erklärung der Funktion. + +Beispiel: +`GEF_BeispielFunktion(12345);` \ No newline at end of file diff --git a/Bat_EV_SDL_V3_Beta/form.json b/Bat_EV_SDL_V3_Beta/form.json new file mode 100644 index 0000000..9a50854 --- /dev/null +++ b/Bat_EV_SDL_V3_Beta/form.json @@ -0,0 +1,112 @@ +{ + "elements": [ + { + "type": "Label", + "caption": "Aufgepasst: Bei Goodwe nur Ladenvariabel auswählen und entladen Dummy Variabel.\nGoodwe braucht nur eine Leistungssoll Variabel. Entlade NICHT auf gleiche Variabel setzen wie Laden\nGoodwe: Laden=11, Entladen=12,\nSolaredge: Laden=3, Entladen=4\nDefault: Laden=1, Entladen=2" + }, + { + "type":"List", + "name":"Batteries", + "caption":"Batterien mit Typ, Leistung und kapazität", + "add":true, + "delete":true, + "columns":[ + { + "caption":"Typ", + "name":"typ", + "width":"100px", + "add":"Stufe", + "edit":{ + "type":"ValidationTextBox" + } + }, + { + "caption":"Leistung in W", + "name":"powerbat", + "width":"150px", + "add":5000, + "edit":{ + "type":"NumberSpinner" + } + }, + { + "caption":"Kapazität in kWh", + "name":"capazity", + "width":"200px", + "add":60, + "edit":{ + "type":"NumberSpinner" + } + }, + { + "caption":"SoC", + "name":"soc", + "width":"200px", + "add":0, + "edit":{ + "type":"SelectVariable" + } + }, + { + "caption":"Batterieleistung Laden", + "name":"powerbat_laden", + "width":"200px", + "add":0, + "edit":{ + "type":"SelectVariable" + } + }, + { + "caption":"Batterieleistung Entladen", + "name":"powerbat_entladen", + "width":"200px", + "add":0, + "edit":{ + "type":"SelectVariable" + } + } + , + { + "caption": "Register Laden/Entladen Modus", + "name": "register_ladenentladen_modus", + "width": "260px", + "add": 0, + "edit": { + "type": "SelectVariable" + } + } + , + { + "caption": "Aktuelle Batterieleistung", + "name": "register_bat_power", + "width": "260px", + "add": 0, + "edit": { + "type": "SelectVariable" + } + } + + ] + }, + { + "type": "NumberSpinner", + "name": "SDL_Leistung_Laden", + "caption": "SDL_Leistung_Laden", + "suffix": "W" + }, + { + "type": "NumberSpinner", + "name": "SDL_Leistung_Entladen", + "caption": "SDL_Leistung_Entladen", + "suffix": "W" + }, + { + "type": "NumberSpinner", + "name": "UpdateInterval", + "caption": "Neuberechnung alle", + "suffix": "Sekunden", + "minimum": 0, + "maximum": 86400 + } + ] +} diff --git a/Bat_EV_SDL_V3_Beta/module.json b/Bat_EV_SDL_V3_Beta/module.json new file mode 100644 index 0000000..eb54ec3 --- /dev/null +++ b/Bat_EV_SDL_V3_Beta/module.json @@ -0,0 +1,12 @@ +{ + "id": "{9C3DCC91-FA9E-72E5-7FA6-7B483D1D114D}", + "name": "Bat_EV_SDL_V3_Beta", + "type": 3, + "vendor": "Belevo AG", + "aliases": [], + "parentRequirements": [], + "childRequirements": [], + "implemented": [], + "prefix": "GEF", + "url": "" +} \ No newline at end of file diff --git a/Bat_EV_SDL_V3_Beta/module.php b/Bat_EV_SDL_V3_Beta/module.php new file mode 100644 index 0000000..d00609a --- /dev/null +++ b/Bat_EV_SDL_V3_Beta/module.php @@ -0,0 +1,983 @@ +RegisterPropertyString("Batteries", "[]"); + //$this->RegisterPropertyInteger("$upKWh + $underKWh)", 0); + $this->RegisterPropertyInteger("SDL_Leistung_Laden", 0); + $this->RegisterPropertyInteger("SDL_Leistung_Entladen", 0); // W + $this->RegisterPropertyInteger("UpdateInterval", 5); // Minuten + + // Status + $this->RegisterVariableBoolean("State", "Aktiv", "~Switch", 1); + $this->EnableAction("State"); + + // Prozentwerte + $this->RegisterVariableFloat("SDL_Pos", "SDL Energie verfügbar (%)", "", 10); + $this->RegisterVariableFloat("SoC_EV", "EV Energie verfügbar (%)", "", 11); + + // Variablen + $this->RegisterVariableFloat("Nennleistung_Soll_EV", "Nennleistung Soll EV", "", 2); + $this->RegisterVariableFloat("Nennleistung_Soll_SDL", "Nennleistung Soll SDL", "", 3); + $this->RegisterVariableFloat("Aktuelle_Leistung_EV", "Aktuelle Leistung EV", "", 5); + $this->RegisterVariableFloat("Aktuelle_Leistung_SDL", "Aktuelle Leistung SDL", "", 4); + $this->RegisterVariableFloat("P_SDL_laden", "P SDL laden max (W)", "", 21); + $this->RegisterVariableFloat("P_SDL_entladen", "P SDL entladen max (W)", "", 22); + $this->RegisterVariableFloat("P_EV_laden", "P EV laden max (W)", "", 31); + $this->RegisterVariableFloat("P_EV_entladen", "P EV entladen max (W)", "", 32); + + // Debug + $this->RegisterVariableString("CalcJSON", "Berechnung (JSON)", "", 99); + + // Timer: wichtig -> Prefix muss passen + $this->RegisterTimer("UpdateTimer", 0, 'GEF_Update($_IPS["TARGET"]);'); + } + + public function ApplyChanges() + { + parent::ApplyChanges(); + + $intervalSec = (int)$this->ReadPropertyInteger("UpdateInterval"); + $this->SetTimerInterval("UpdateTimer", ($intervalSec > 0) ? $intervalSec * 1000 : 0); + + // Cache neu bauen (force) + $this->BuildBatteryCache(true); + + $this->Update(); + } + + public function RequestAction($Ident, $Value) + { + if ($Ident === "State") { + SetValue($this->GetIDForIdent("State"), (bool)$Value); + if ((bool)$Value) { + $this->Update(); + } + return; + } + + throw new Exception("Invalid Ident: " . $Ident); + } + + public function Update() + { + + + + // Reentranzschutz – verhindert parallele Update()-Läufe + $semKey = 'BatEVSDL_Update_' . $this->InstanceID; + + if (!IPS_SemaphoreEnter($semKey, 5000)) { + // Überspringen, wenn bereits ein Lauf aktiv ist + $this->SendDebug("Update", "SKIP (Semaphore locked)", 0); + return; + } + + + + try { + + if (!GetValue($this->GetIDForIdent("State"))) { + return; + } + + // Cache nur neu bauen, wenn nötig + $this->BuildBatteryCache(false); + + $cache = json_decode($this->GetBufferSafe("BatCacheJSON"), true); + if (!is_array($cache) || empty($cache["bats"])) { + $this->WriteAllZero("cache empty/invalid"); + return; + } + + + // ===================== + // Integrator: State laden + // ===================== + $now = time(); + + $lastTs = (int)$this->GetBufferSafe("Int_LastTs"); + $lastPEV = (float)$this->GetBufferSafe("Int_LastP_EV_W"); // W (+ laden, - entladen) + $lastPSDL = (float)$this->GetBufferSafe("Int_LastP_SDL_W"); // W (+ laden, - entladen) + + $Eev = (float)$this->GetBufferSafe("Int_E_EV_kWh"); // kWh-Konto EV + $Esdl = (float)$this->GetBufferSafe("Int_E_SDL_kWh"); // kWh-Konto SDL + + if ($lastTs <= 0) { + $lastTs = $now; + $lastPEV = 0.0; + $lastPSDL = 0.0; + $Eev = 0.0; + $Esdl = 0.0; + } + + $dtH = ($now - $lastTs) / 3600.0; // Stunden + if ($dtH < 0) $dtH = 0.0; + // wenn IPS mal hängt, keine riesen Sprünge integrieren: + if ($dtH > 1.0) $dtH = 1.0; // max 1h, kannst du anpassen + + + + $calc = [ + "inputs" => $cache["inputs"] ?? [], + "batteries" => [], + "total" => [] + ]; + + // Summen + $sdlDisKW_ges = 0.0; + $evDisKW_ges = 0.0; + $sdlChKW_ges = 0.0; + $evChKW_ges = 0.0; + + $real_kWh_ev_ges = 0.0; + $real_kWh_sdl_ges = 0.0; + + $SDL_kWh_ges = 0.0; + $EV_kWh_ges = 0.0; + $totalCapKWh = 0.0; + + foreach ($cache["bats"] as $i => $c) { + + $capKWh = (float)($c["capKWh"] ?? 0.0); + if ($capKWh <= 0.0) { + continue; + } + + // dynamisch: SoC lesen + $socVarId = (int)($c["socVarId"] ?? 0); + $socPct = $this->ReadSocPercent($socVarId); + + $real_kWh = $capKWh / 100.0 * $socPct; + + // vorkalkuliert: + $typ = (string)($c["typ"] ?? ("Bat " . ($i + 1))); + $underKWh = (float)($c["underKWh"] ?? 0.0); + $upKWh = (float)($c["upKWh"] ?? 0.0); + $SDL_kWh = (float)($c["SDL_kWh_total"] ?? 0.0); + $EV_kWh = (float)($c["EV_kWh_total"] ?? 0.0); + + $sdlShareKW_laden = (float)($c["sdlShareKW_laden"] ?? 0.0); + $sdlShareKW_entladen = (float)($c["sdlShareKW_entladen"] ?? 0.0); + $evShareKW_laden = (float)($c["evShareKW_laden"] ?? 0.0); + $evShareKW_entladen = (float)($c["evShareKW_entladen"] ?? 0.0); + + + + // Defaults + $EV_SOC = 0.0; + $SDL_SOC = 0.0; + + $sdlDisKW = 0.0; $evDisKW = 0.0; + $sdlChKW = 0.0; $evChKW = 0.0; + + $real_kWh_ev = 0.0; + $real_kWh_sdl = 0.0; + + // --- Deine 3 Fälle --- + if ($underKWh <= $real_kWh && $upKWh >= $real_kWh) { + + + + $win = ($upKWh - $underKWh); + if ($win > 0.0) { + $SDL_SOC = 100.0 * ($real_kWh - $underKWh) / $win; // 0..100 innerhalb Fenster + } else { + $SDL_SOC = 0.0; + } + $SDL_SOC = is_finite($SDL_SOC) ? max(0.0, min(100.0, $SDL_SOC)) : 0.0; + + + + if ($EV_kWh > 0.0) { + $EV_SOC = 100.0 * ($real_kWh - $underKWh) / $EV_kWh; + } else { + $EV_SOC = 0.0; // definierter Fallback + } + $EV_SOC = is_finite($EV_SOC) ? max(0.0, min(100.0, $EV_SOC)) : 0.0; + + + + + $sdlDisKW = $sdlShareKW_entladen; + $evDisKW = $evShareKW_entladen; + $sdlChKW = $sdlShareKW_laden; + $evChKW = $evShareKW_laden; + + $real_kWh_ev = $real_kWh - $underKWh; + $real_kWh_sdl = $underKWh; + + } elseif ($upKWh < $real_kWh) { + + $EV_SOC = 100.0; + + $den1 = ($capKWh - $real_kWh + $underKWh); + $den2 = (2.0 * $underKWh); + + if ($den1 > 0.0 && $den2 > 0.0) { + $SDL_SOC = min(100.0, ($den1 / $den2) * 100.0); + } else { + $SDL_SOC = 0.0; + } + $SDL_SOC = is_finite($SDL_SOC) ? max(0.0, min(100.0, $SDL_SOC)) : 0.0; + + $sdlDisKW = $sdlShareKW_entladen; + $evDisKW = $evShareKW_entladen; + $sdlChKW = $sdlShareKW_laden; + $evChKW = 0.0; + + $real_kWh_ev = $capKWh - ($capKWh - $upKWh + $underKWh); + $real_kWh_sdl = ($capKWh - $upKWh + $underKWh) - ($capKWh - $real_kWh); + + } elseif ($underKWh > $real_kWh) { + + $EV_SOC = 0.0; + + $den = $upKWh + $underKWh; + $SDL_SOC = ($den > 0.0) ? ($real_kWh * 100.0 / $den) : 0.0; + $SDL_SOC = is_finite($SDL_SOC) ? max(0.0, min(100.0, $SDL_SOC)) : 0.0; + + $sdlDisKW = $sdlShareKW_entladen; + $evDisKW = 0.0; + $sdlChKW = $sdlShareKW_laden; + $evChKW = $evShareKW_laden; + + $real_kWh_ev = 0.0; + $real_kWh_sdl = $real_kWh; + } + + // Null/Full + if ($real_kWh <= 0.0) { + $sdlDisKW = 0.0; + $real_kWh_ev = 0.0; + $real_kWh_sdl = 0.0; + } elseif ($real_kWh >= $capKWh) { + $sdlChKW = 0.0; + //$real_kWh_ev = $capKWh - ($upKWh + $underKWh); + //$real_kWh_sdl = $upKWh + $underKWh; + + $real_kWh_ev = $EV_kWh; + $real_kWh_sdl = $SDL_kWh; + } + + $real_kWh_ev = max(0.0, $real_kWh_ev); + $real_kWh_sdl = max(0.0, $real_kWh_sdl); + + // Summen + $totalCapKWh += $capKWh; + + $sdlDisKW_ges += $sdlDisKW; + $evDisKW_ges += $evDisKW; + $sdlChKW_ges += $sdlChKW; + $evChKW_ges += $evChKW; + + $real_kWh_ev_ges += $real_kWh_ev; + $real_kWh_sdl_ges += $real_kWh_sdl; + + $SDL_kWh_ges += $SDL_kWh; + $EV_kWh_ges += $EV_kWh; + + $calc["batteries"][] = [ + "idx" => $c["idx"] ?? $i, + "typ" => $typ, + "SoC_varId" => $socVarId, + "SoC_pct" => round($socPct, 3), + "Effektive kWh" => round($real_kWh, 3), + + "EV_SOC" => round($EV_SOC, 3), + "SDL_SOC" => round($SDL_SOC, 3), + + "EV_kWh" => round($real_kWh_ev, 3), + "SDL_kWh" => round($real_kWh_sdl, 3), + + "under_grenze_kWh" => round($underKWh, 3), + "up_grenze_kWh" => round($upKWh, 3), + + "SDL_Charge_kW" => round($sdlChKW, 3), + "SDL_Discharge_kW" => round($sdlDisKW, 3), + "EV_Charge_kW" => round($evChKW, 3), + "EV_Discharge_kW" => round($evDisKW, 3), + ]; + } + +// =================================================== +// Integrator: nur laufen lassen wenn Soll != 0W +// =================================================== +$pEvSoll = (float)GetValue($this->GetIDForIdent("Nennleistung_Soll_EV")); +$pSdlSoll = (float)GetValue($this->GetIDForIdent("Nennleistung_Soll_SDL")); + +$eps = 1.0; // 1W-Schwellwert +$evActive = (abs($pEvSoll) > $eps); +$sdlActive = (abs($pSdlSoll) > $eps); + +// 1) Initialisierung: Konto beim ersten Mal auf Referenz setzen +if ($this->GetBufferSafe("Int_Init") !== "1") { + $Eev = $real_kWh_ev_ges; + $Esdl = $real_kWh_sdl_ges; + $this->SetBuffer("Int_Init", "1"); +} + +// 2) Integration (kW*h = kWh) mit LETZTER Leistung aus dem vorherigen Intervall +// (+) laden -> steigt, (-) entladen -> fällt +if ($evActive) { + $Eev += (($lastPEV / 1000.0) * $dtH); +} else { + // Wenn EV aus: sofort auf physikalische Referenz + $Eev = $real_kWh_ev_ges; +} + +if ($sdlActive) { + $Esdl += (($lastPSDL / 1000.0) * $dtH); +} else { + // Wenn SDL aus: sofort auf physikalische Referenz + $Esdl = $real_kWh_sdl_ges; +} + + // 3) Grenzen einhalten (Clamp) + $Eev = ($EV_kWh_ges > 0.0) ? max(0.0, min($EV_kWh_ges, $Eev)) : 0.0; + $Esdl = ($SDL_kWh_ges > 0.0) ? max(0.0, min($SDL_kWh_ges, $Esdl)) : 0.0; + + // 4) Sanftes Wieder-Angleichen (nur wenn aktiv), damit es nicht wegdriftet + // Idee: E := E + alpha*(E_ref - E) + $tauH = 30.0 / 3600.0; // 30 Sekunden Zeitkonstante (weil dein Update in Sekunden ist) + + if ($tauH > 0.0) { + $alpha = 1.0 - exp(-$dtH / $tauH); + $alpha = max(0.0, min(0.2, $alpha)); // max 20% pro Update + + if ($evActive) { + $Eev += $alpha * ($real_kWh_ev_ges - $Eev); + } + if ($sdlActive) { + $Esdl += $alpha * ($real_kWh_sdl_ges - $Esdl); + } + + // nach dem angleichen nochmals clamp + $Eev = ($EV_kWh_ges > 0.0) ? max(0.0, min($EV_kWh_ges, $Eev)) : 0.0; + $Esdl = ($SDL_kWh_ges > 0.0) ? max(0.0, min($SDL_kWh_ges, $Esdl)) : 0.0; + } + + // 5) Prozentwerte aus den integrierten Konten + $evPosPct = ($EV_kWh_ges > 0.0) ? ($Eev / $EV_kWh_ges * 100.0) : 0.0; + $sdlPosPct = ($SDL_kWh_ges > 0.0) ? ($Esdl / $SDL_kWh_ges * 100.0) : 0.0; + + // Output setzen + $this->SetIdentValue("SDL_Pos", round($sdlPosPct, 3)); + $this->SetIdentValue("SoC_EV", round($evPosPct, 3)); + + $this->SetIdentValue("P_SDL_laden", round($sdlChKW_ges * 1000.0, 0)); + $this->SetIdentValue("P_SDL_entladen", round($sdlDisKW_ges * 1000.0, 0)); + $this->SetIdentValue("P_EV_laden", round($evChKW_ges * 1000.0, 0)); + $this->SetIdentValue("P_EV_entladen", round($evDisKW_ges * 1000.0, 0)); + + $calc["total"] = [ + "SDL_SoC_pct" => round($sdlPosPct, 3), + "EV_SoC_pct" => round($evPosPct, 3), + + "SDL_kWh_total" => round($SDL_kWh_ges, 3), + "EV_kWh_total" => round($EV_kWh_ges, 3), + + "SDL_Charge_kW" => round($sdlChKW_ges, 3), + "SDL_Discharge_kW" => round($sdlDisKW_ges, 3), + "EV_Charge_kW" => round($evChKW_ges, 3), + "EV_Discharge_kW" => round($evDisKW_ges, 3), + + "totalCap_kWh" => round($totalCapKWh, 3) + ]; + + $this->SetIdentValue("CalcJSON", json_encode($calc, JSON_PRETTY_PRINT)); + $this->ApplySetpoints(); + // ================================== + // Integrator: neue LastP für nächstes Intervall speichern + // ================================== + $aktEV = (float)GetValue($this->GetIDForIdent("Aktuelle_Leistung_EV")); + $aktSDL = (float)GetValue($this->GetIDForIdent("Aktuelle_Leistung_SDL")); + + $this->SetBuffer("Int_LastP_EV_W", (string)$aktEV); + $this->SetBuffer("Int_LastP_SDL_W", (string)$aktSDL); + $this->SetBuffer("Int_LastTs", (string)$now); + + $this->SetBuffer("Int_E_EV_kWh", (string)$Eev); + $this->SetBuffer("Int_E_SDL_kWh", (string)$Esdl); + + + } catch (Throwable $e) { + $this->SendDebug("Update ERROR", $e->getMessage() . " @ " . $e->getFile() . ":" . $e->getLine(), 0); + $this->WriteAllZero("Exception: " . $e->getMessage()); + } + finally { + IPS_SemaphoreLeave($semKey); // ✅ Tür immer wieder aufschließen + } + } + + private function BuildBatteryCache(bool $force): void + { + $batteriesRaw = $this->ReadPropertyString("Batteries"); + //$sdlTotalW = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung")); + $sdlTotalW_laden = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Laden")); + $sdlTotalW_entladen = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Entladen")); + $hours = self::HOURS; + + $hash = md5(json_encode([ + "Batteries" => $batteriesRaw, + "SDL_W_Laden" => $sdlTotalW_laden, + "SDL_W_Entladen" => $sdlTotalW_entladen, + "hours" => $hours + ])); + + $oldHash = $this->GetBufferSafe("BatCacheHash"); + if (!$force && $oldHash === $hash) { + return; + } + + $batteries = json_decode($batteriesRaw, true); + if (!is_array($batteries)) $batteries = []; + + $sumBatPowerW = 0.0; + foreach ($batteries as $b) { + $p = (float)($b["powerbat"] ?? 0); + if ($p > 0) $sumBatPowerW += $p; + } + + $cache = [ + "inputs" => [ + "SDL_Leistung_W_laden" => $sdlTotalW_laden, + "SDL_Leistung_W_entladen" => $sdlTotalW_entladen, + "SumBatPower_W" => round($sumBatPowerW, 0), + "hours" => $hours + ], + "bats" => [] + ]; + + if ($sumBatPowerW <= 0.0) { + $this->SetBuffer("BatCacheHash", $hash); + $this->SetBuffer("BatCacheJSON", json_encode($cache)); + $this->SendDebug("Cache", "sumBatPowerW=0 -> empty cache", 0); + return; + } + + $sumBatPowerkW = $sumBatPowerW / 1000.0; + //$sdlTotalkW = $sdlTotalW / 1000.0; + + $sdlTotalkW_laden = $sdlTotalW_laden / 1000.0; + $sdlTotalkW_entladen = $sdlTotalW_entladen / 1000.0; + + foreach ($batteries as $idx => $b) { + + $pBatW = max(0.0, (float)($b["powerbat"] ?? 0)); + $pBatkW = $pBatW / 1000.0; + + $capKWh = max(0.0, (float)($b["capazity"] ?? 0)); + if ($capKWh <= 0.0) continue; + + $socVarId = (int)($b["soc"] ?? 0); + $typ = (string)($b["typ"] ?? ("Bat " . ($idx + 1))); + + + // Mit laden und entladen unterschiedlich + //---------------------------------------------------- + $sdlShareKW_laden = ($sumBatPowerkW > 0.0) ? ($sdlTotalkW_laden / $sumBatPowerkW * $pBatkW) : 0.0; + $evShareKW_laden = $pBatkW - $sdlShareKW_laden; + $upKWh = $capKWh - $sdlShareKW_laden * 0.5; + + $sdlShareKW_entladen = ($sumBatPowerkW > 0.0) ? ($sdlTotalkW_entladen / $sumBatPowerkW * $pBatkW) : 0.0; + $evShareKW_entladen = $pBatkW - $sdlShareKW_entladen; + $underKWh = $sdlShareKW_entladen * 0.5; + + $SDL_kWh = $underKWh + ($capKWh -$upKWh); + $EV_kWh = $capKWh - $SDL_kWh; + //---------------------------------------------------- + + $cache["bats"][] = [ + "idx" => $idx, + "typ" => $typ, + + "socVarId" => $socVarId, + "capKWh" => $capKWh, + + "pBatW" => $pBatW, + "sdlShareKW_laden" => $sdlShareKW_laden, + "sdlShareKW_entladen" => $sdlShareKW_entladen, + "evShareKW_laden" => $evShareKW_laden, + "evShareKW_entladen" => $evShareKW_entladen, + + "underKWh" => $underKWh, + "upKWh" => $upKWh, + + "SDL_kWh_total" => $SDL_kWh, + "EV_kWh_total" => $EV_kWh + ]; + } + + $this->SetBuffer("BatCacheHash", $hash); + $this->SetBuffer("BatCacheJSON", json_encode($cache)); + + $this->SendDebug("Cache", "Battery cache rebuilt (" . count($cache["bats"]) . " bats)", 0); + } + + private function GetBufferSafe(string $name): string + { + $v = $this->GetBuffer($name); + return is_string($v) ? $v : ""; + } + + private function WriteAllZero(string $reason): void + { + $this->SetIdentValue("SDL_Pos", 0.0); + $this->SetIdentValue("SoC_EV", 0.0); + + $this->SetIdentValue("P_SDL_laden", 0.0); + $this->SetIdentValue("P_SDL_entladen", 0.0); + + $this->SetIdentValue("P_EV_laden", 0.0); + $this->SetIdentValue("P_EV_entladen", 0.0); + + $this->SetIdentValue("CalcJSON", json_encode(["error" => $reason], JSON_PRETTY_PRINT)); + } + + private function SetIdentValue(string $ident, $value): void + { + $id = @$this->GetIDForIdent($ident); + if ($id <= 0) { + $this->SendDebug(__FUNCTION__, "Ident nicht gefunden: $ident", 0); + return; + } + SetValue($id, $value); + } + + private function ReadSocPercent(int $varId): float + { + // Falls jemand statt Variable direkt 0..100 einträgt + if ($varId >= 0 && $varId <= 100 && !IPS_VariableExists($varId)) { + return (float)$varId; + } + + if ($varId <= 0 || !IPS_VariableExists($varId)) { + return 0.0; + } + + $v = GetValue($varId); + if (!is_numeric($v)) { + return 0.0; + } + + $soc = (float)$v; + if ($soc < 0.0) $soc = 0.0; + if ($soc > 100.0) $soc = 100.0; + + return $soc; + } + + public function ApplySetpoints(): void + { + $pEvW = (float) GetValue($this->GetIDForIdent("Nennleistung_Soll_EV")); + $pSdlW = (float) GetValue($this->GetIDForIdent("Nennleistung_Soll_SDL")); + + //Diverenz zwischen EV und SDl berechnen + + + // Methode für priorisierung + $distribution = $this->CalculateBatteryDistribution($pEvW,$pSdlW); + + + $this->WriteBatteryPowerSetpoints($distribution); + } + + + + +private function CalculateBatteryDistribution(float $pEvW, float $pSdlW): array +{ + $calcJsonId = $this->GetIDForIdent("CalcJSON"); + // Fallback, falls Variable leer ist oder nicht existiert + if (!IPS_VariableExists($calcJsonId)) { + return []; + } + $rawJson = (string)GetValue($calcJsonId); + if (empty($rawJson)) { + return []; + } + $calc = json_decode($rawJson, true); + $batteries = $calc['batteries'] ?? []; + + // --------------------------------------------------------- + // Hilfsfunktion: Verteilungslogik (Closure) + // --------------------------------------------------------- + $distributePower = function(float $targetPower, string $mode) use ($batteries): array { + // Initialisierung des Ergebnis-Arrays (Key = idx, Value = zugewiesene Watt) + $result = []; + foreach ($batteries as $bat) { + $result[$bat['idx']] = 0.0; + } + + if (abs($targetPower) < 0.01) { + return $result; // Nichts zu tun + } + + $isCharge = ($targetPower > 0); + $absPower = abs($targetPower); + // Relevante Keys basierend auf Modus (EV oder SDL) und Richtung (Laden/Entladen) + $socKey = ($mode === 'EV') ? 'EV_SOC' : 'SDL_SOC'; + // Achtung: JSON Limits sind in kW, wir rechnen in Watt -> * 1000 + $limitKey = $isCharge ? $mode . '_Charge_kW' : $mode . '_Discharge_kW'; + + // 1. Batterien vorbereiten und gruppieren nach gerundetem SoC + $groups = []; + foreach ($batteries as $bat) { + $soc = (int)round($bat[$socKey]); // Auf ganze Zahl runden + $maxW = ((float)$bat[$limitKey]) * 1000.0; // kW in Watt umrechnen + $groups[$soc][] = [ + 'idx' => $bat['idx'], + 'maxW' => $maxW + ]; + } + + // 2. Sortieren der Gruppen + // Laden: Wenig SoC zuerst (ASC) -> leere füllen + // Entladen: Viel SoC zuerst (DESC) -> volle leeren + if ($isCharge) { + ksort($groups); + } else { + krsort($groups); + } + + // 3. Verteilung + $remainingNeeded = $absPower; + + foreach ($groups as $soc => $groupBatteries) { + if ($remainingNeeded <= 0.01) break; + + // Gesamte verfügbare Leistung in dieser SoC-Gruppe ermitteln + $groupTotalCapacity = 0.0; + foreach ($groupBatteries as $gb) { + $groupTotalCapacity += $gb['maxW']; + } + + // Wie viel können wir dieser Gruppe zuteilen? + // Entweder alles was die Gruppe kann, oder den Restbedarf + $powerForThisGroup = min($remainingNeeded, $groupTotalCapacity); + + // Proportionale Aufteilung innerhalb der Gruppe + // Falls Gruppe Kapazität 0 hat (Defekt/Voll), verhindern wir DivByZero + if ($groupTotalCapacity > 0) { + $ratio = $powerForThisGroup / $groupTotalCapacity; + foreach ($groupBatteries as $gb) { + $assigned = $gb['maxW'] * $ratio; + $result[$gb['idx']] = $assigned; + } + } + + $remainingNeeded -= $powerForThisGroup; + } + + // Wenn wir entladen, müssen die Werte negativ sein + if (!$isCharge) { + foreach ($result as $idx => $val) { + $result[$idx] = -$val; + } + } + + return $result; + }; + + // --------------------------------------------------------- + // Hauptablauf + // --------------------------------------------------------- + + // 1. Berechnung für EV und SDL getrennt durchführen + $evDistribution = $distributePower($pEvW, 'EV'); + $sdlDistribution = $distributePower($pSdlW, 'SDL'); + + // 2. Ergebnisse zusammenführen und Output formatieren + $finalOutput = []; + + foreach ($batteries as $bat) { + $idx = $bat['idx']; + // Summe der beiden Anforderungen (kann sich gegenseitig aufheben) + $valEv = $evDistribution[$idx] ?? 0.0; + $valSdl = $sdlDistribution[$idx] ?? 0.0; + $totalW = $valEv + $valSdl; + + // Aufteilen in Charge / Discharge für das Return-Format + $chargeW = 0.0; + $dischargeW = 0.0; + + if ($totalW > 0) { + $chargeW = abs($totalW); + } else { + $dischargeW = abs($totalW); + } + + // JSON Objekt erstellen + $finalOutput[] = [ + "idx" => $idx, + "typ" => (string)$bat['typ'], // Typ als String beibehalten + "chargeW" => round($chargeW, 0), // Optional: runden für sauberes JSON + "dischargeW" => round($dischargeW, 0) + ]; + } + + $batteriesRaw = json_decode($this->ReadPropertyString("Batteries")); + + $totalPower_ist = 0; + + foreach ($batteriesRaw as $bat) { + $totalPower_ist += (-1)*GetValue($bat->register_bat_power); + } + + // ---------------------------- + // ALT (nicht mehr gebraucht) -> AUSKOMMENTIERT + // ---------------------------- + /* + $sumReq = (abs($pEvW) + abs($pSdlW)); + $sumReqRel = ($pEvW + $pSdlW); + + if($sumReq==0){ + + $this->SetValue("Aktuelle_Leistung_EV", $totalPower_ist / 2); + $this->SetValue("Aktuelle_Leistung_SDL", $totalPower_ist / 2); + + }else{ + + if($pEvW>=0){ + $this->SetValue("Aktuelle_Leistung_EV",((1+($totalPower_ist-$sumReqRel) / $sumReq)) * $pEvW); + }else{ + $this->SetValue("Aktuelle_Leistung_EV",((1-($totalPower_ist-$sumReqRel) / $sumReq)) * $pEvW); + } + + if($pSdlW>=0){ + $this->SetValue("Aktuelle_Leistung_SDL",((1+($totalPower_ist-$sumReqRel) / $sumReq)) * $pSdlW); + }else{ + $this->SetValue("Aktuelle_Leistung_SDL",((1-($totalPower_ist-$sumReqRel) / $sumReq)) * $pSdlW); + } + } + */ + + // ---------------------------- + // NEU (genau deine gewünschte Logik) + // aktuelle - EV_SOLL = aktuelle SDL + // aktuelle - SDL_SOLL = aktuelle EV + // ---------------------------- + + + + + /* + $eps = 0.01; + + $aktEV = 0.0; + $aktSDL = 0.0; + + // Optional: Wenn einer 0 ist -> aktiver Kanal bekommt totalPower_ist + if (abs($pEvW) < $eps && abs($pSdlW) < $eps) { + + $aktEV = 0.0; + $aktSDL = 0.0; + + } elseif (abs($pEvW) < $eps) { + + $aktEV = 0.0; + $aktSDL = $totalPower_ist; + + } elseif (abs($pSdlW) < $eps) { + + $aktEV = $totalPower_ist; + $aktSDL = 0.0; + + } else { + + // Beide aktiv + $sameDirection = (($pEvW > 0 && $pSdlW > 0) || ($pEvW < 0 && $pSdlW < 0)); + + if ($sameDirection) { + // Regel A: gleiche Richtung -> Ist proportional nach |Soll| aufteilen + $sumAbs = abs($pEvW) + abs($pSdlW); + + if ($sumAbs < $eps) { + // Fallback + $aktEV = $totalPower_ist / 2.0; + $aktSDL = $totalPower_ist / 2.0; + } else { + $aktEV = $totalPower_ist * (abs($pEvW) / $sumAbs); + $aktSDL = $totalPower_ist - $aktEV; // Summe bleibt exakt totalPower_ist + } + + } else { + // Regel B: gegensätzliche Richtung -> Sollwerte anzeigen + $aktEV = $pEvW; + $aktSDL = $pSdlW; + } + } + + $this->SetValue("Aktuelle_Leistung_EV", (float)$aktEV); + $this->SetValue("Aktuelle_Leistung_SDL", (float)$aktSDL); + */ + + $eps = 0.01; + + $sumSoll = (float)($pEvW + $pSdlW); + + if (abs($sumSoll) > $eps) { + // Normalfall: wir skalieren so, dass EV+SDL genau totalPower_ist ergibt + $factor = (float)($totalPower_ist / $sumSoll); + + $aktEV = (float)($pEvW * $factor); + $aktSDL = (float)($pSdlW * $factor); + + } else { + // Sonderfall: Soll hebt sich nahezu auf (sumSoll ~ 0) + // -> wenn Ist ebenfalls ~0: Soll anzeigen + if (abs($totalPower_ist) < 50.0) { // 50W Toleranz kannst du anpassen + $aktEV = (float)$pEvW; + $aktSDL = (float)$pSdlW; + } else { + // Ist ist nicht 0 obwohl Sollsumme 0 -> Abweichung zuweisen (z.B. SDL) + $aktEV = (float)$pEvW; + $aktSDL = (float)($totalPower_ist - $aktEV); + // Alternative: Abweichung EV geben: + // $aktSDL = (float)$pSdlW; + // $aktEV = (float)($totalPower_ist - $aktSDL); + } + } + + $this->SetValue("Aktuelle_Leistung_EV", $aktEV); + $this->SetValue("Aktuelle_Leistung_SDL", $aktSDL); + + + + return $finalOutput; +} + + + private function WriteBatteryPowerSetpoints(array $distribution): void + { + IPS_LogMessage( + __FUNCTION__, + "distribution=" . json_encode($distribution, JSON_PRETTY_PRINT) + ); + + $batteriesCfg = json_decode($this->ReadPropertyString("Batteries"), true); + if (!is_array($batteriesCfg) || empty($batteriesCfg)) { + return; + } + + foreach ($distribution as $d) { + + $idx = (int)($d["idx"] ?? -1); + if ($idx < 0 || !isset($batteriesCfg[$idx])) { + continue; + } + + $cfg = $batteriesCfg[$idx]; + + $typ = (string)($d["typ"] ?? ($cfg["typ"] ?? ("Bat " . ($idx + 1)))); + + // ✅ immer positiv + $chargeW = max(0.0, (float)($d["chargeW"] ?? 0.0)); + $dischargeW = max(0.0, (float)($d["dischargeW"] ?? 0.0)); + + // nie gleichzeitig + if ($chargeW > 0.0 && $dischargeW > 0.0) { + $this->SendDebug("WriteBatteryPowerSetpoints", "WARN: both >0 for $typ (idx=$idx) -> set 0", 0); + $chargeW = 0.0; + $dischargeW = 0.0; + } + + $this->WriteByVendorRegistersSingleMode($typ, $cfg, $chargeW, $dischargeW); + + $this->SendDebug("Setpoints", "$typ (idx=$idx) charge={$chargeW}W discharge={$dischargeW}W", 0); + } + } + +private function WriteByVendorRegistersSingleMode(string $typ, array $cfg, float $chargeW, float $dischargeW): void +{ + $t = mb_strtolower($typ); + + // Leistungs-Variablen + $varPowerCharge = (int)($cfg["powerbat_laden"] ?? 0); + $varPowerDisch = (int)($cfg["powerbat_entladen"] ?? 0); + + // ✅ EIN Modus-Register + $varMode = (int)($cfg["register_ladenentladen_modus"] ?? 0); + + // sichere Writer + $setInt = function(int $varId, int $value): void { + if ($varId > 0 && IPS_VariableExists($varId)) { + RequestAction($varId, $value); + } + }; + $setW = function(int $varId, float $w): void { + if ($varId > 0 && IPS_VariableExists($varId)) { + RequestAction($varId, (int)round(max(0.0, $w), 0)); // ✅ niemals negativ + } + }; + + // Moduscodes je Typ + $modeCharge = 1; $modeDisch = 2; // Default + if (strpos($t, "goodwe") !== false) { + $modeCharge = 11; $modeDisch = 12; + } elseif (strpos($t, "solaredge") !== false) { + $modeCharge = 3; $modeDisch = 4; + } + + // ========================= + // Laden + // ========================= + if ($chargeW > 0.0) { + + // Modus setzen (ein Register) + $setInt($varMode, $modeCharge); + + // GoodWe: nur powerbat_laden + if (strpos($t, "goodwe") !== false) { + $setW($varPowerCharge, (int)$chargeW); + $setW($varPowerDisch, (int)0); + return; + } + + // SolarEdge + Default: zwei Leistungsregister + $setW($varPowerCharge, $chargeW); + $setW($varPowerDisch, 0.0); + return; + } + + // ========================= + // Entladen + // ========================= + if ($dischargeW > 0.0) { + + // Modus setzen (ein Register) + $setInt($varMode, $modeDisch); + + // GoodWe: Entladen nutzt trotzdem powerbat_laden (immer positiv) + if (strpos($t, "goodwe") !== false) { + $setW($varPowerCharge, (int)$dischargeW); + $setW($varPowerDisch, (int)0); + return; + } + + // SolarEdge + Default: Entladen in powerbat_entladen + $setW($varPowerDisch, $dischargeW); + $setW($varPowerCharge, 0.0); + return; + } + + // ========================= + // Stop / Neutral + // ========================= + $setW($varPowerCharge, 0.0); + $setW($varPowerDisch, 0.0); + // optional: Modus nicht anfassen oder auf 0 setzen: + // $setInt($varMode, 0); +} + + +} +?>