diff --git a/Bat_EV_SDL_V4/README.md b/Bat_EV_SDL_V4/README.md new file mode 100644 index 0000000..b059e3a --- /dev/null +++ b/Bat_EV_SDL_V4/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_V4/form.json b/Bat_EV_SDL_V4/form.json new file mode 100644 index 0000000..5db89cf --- /dev/null +++ b/Bat_EV_SDL_V4/form.json @@ -0,0 +1,109 @@ +{ + "elements": [ + { + "type": "Label", + "caption": "Neues Modul: Grenzen werden nur bei ApplyChanges berechnet. Update läuft in Millisekunden." + }, + { + "type": "List", + "name": "Batteries", + "caption": "Batterien", + "add": true, + "delete": true, + "columns": [ + { + "caption": "Typ", + "name": "typ", + "width": "100px", + "add": "Goodwe", + "edit": { "type": "ValidationTextBox" } + }, + { + "caption": "Leistung in W", + "name": "powerbat", + "width": "150px", + "add": 5000, + "edit": { "type": "NumberSpinner" } + }, + { + "caption": "Kapazität in kWh", + "name": "capazity", + "width": "180px", + "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", + "minimum": 0 + }, + { + "type": "NumberSpinner", + "name": "SDL_Leistung_Entladen", + "caption": "SDL Leistung Entladen", + "suffix": "W", + "minimum": 0 + }, + { + "type": "NumberSpinner", + "name": "ReserveHours", + "caption": "SDL Reservezeit", + "suffix": "h", + "minimum": 0, + "maximum": 24, + "digits": 2 + }, + { + "type": "NumberSpinner", + "name": "UpdateIntervalMs", + "caption": "Neuberechnung alle", + "suffix": "ms", + "minimum": 100, + "maximum": 86400000 + }, + { + "type": "CheckBox", + "name": "FilterAktiv", + "caption": "Aktuelle Leistung filtern" + } + ] +} \ No newline at end of file diff --git a/Bat_EV_SDL_V4/module.json b/Bat_EV_SDL_V4/module.json new file mode 100644 index 0000000..ea496c8 --- /dev/null +++ b/Bat_EV_SDL_V4/module.json @@ -0,0 +1,12 @@ +{ + "id": "{770BB7E3-0F19-0469-D0D4-DD0A0E22BDEA}", + "name": "Bat_EV_SDL_V4", + "type": 3, + "vendor": "Belevo AG", + "aliases": [], + "parentRequirements": [], + "childRequirements": [], + "implemented": [], + "prefix": "GEF", + "url": "" +} \ No newline at end of file diff --git a/Bat_EV_SDL_V4/module.php b/Bat_EV_SDL_V4/module.php new file mode 100644 index 0000000..189a1c4 --- /dev/null +++ b/Bat_EV_SDL_V4/module.php @@ -0,0 +1,803 @@ +RegisterPropertyString("Batteries", "[]"); + $this->RegisterPropertyInteger("SDL_Leistung_Laden", 0); + $this->RegisterPropertyInteger("SDL_Leistung_Entladen", 0); + $this->RegisterPropertyInteger("UpdateIntervalMs", 1000); + $this->RegisterPropertyFloat("ReserveHours", 0.5); + $this->RegisterPropertyBoolean("FilterAktiv", false); + + $this->RegisterVariableBoolean("State", "Aktiv", "~Switch", 1); + $this->EnableAction("State"); + + $this->RegisterVariableFloat("Nennleistung_Soll_EV", "Nennleistung Soll EV", "", 2); + $this->RegisterVariableFloat("Nennleistung_Soll_SDL", "Nennleistung Soll SDL", "", 3); + $this->RegisterVariableFloat("Aktuelle_Leistung_SDL", "Aktuelle Leistung SDL", "", 4); + $this->RegisterVariableFloat("Aktuelle_Leistung_EV", "Aktuelle Leistung EV", "", 5); + + $this->RegisterVariableFloat("SDL_Pos", "SDL Energie verfügbar (%)", "", 10); + $this->RegisterVariableFloat("SoC_EV", "EV Energie verfügbar (%)", "", 11); + + $this->RegisterVariableFloat("P_SDL_laden", "P SDL laden max (W)", "", 21); + $this->RegisterVariableFloat("P_SDL_entladen", "P SDL entladen max (W)", "", 22); + $this->RegisterVariableInteger("P_EV_laden", "P EV laden max (W)", "", 31); + $this->RegisterVariableInteger("P_EV_entladen", "P EV entladen max (W)", "", 32); + + $this->RegisterVariableFloat("SDL_Start_Pos", "SDL Start SoC (%)", "", 35); + + $this->RegisterVariableBoolean("SDL_Reset", "SDL Konto Reset", "~Switch", 36); + $this->EnableAction("SDL_Reset"); + + $this->RegisterVariableBoolean("EV_Reset", "EV Konto Reset", "~Switch", 37); + $this->EnableAction("EV_Reset"); + + $this->RegisterVariableString("CalcJSON", "Berechnung JSON", "", 99); + + $this->RegisterTimer("UpdateTimer", 0, 'BEVSDL4_Update($_IPS["TARGET"]);'); + } + + public function ApplyChanges() + { + parent::ApplyChanges(); + + $intervalMs = (int)$this->ReadPropertyInteger("UpdateIntervalMs"); + $this->SetTimerInterval("UpdateTimer", ($intervalMs > 0) ? $intervalMs : 0); + + $this->BuildStaticBatteryConfig(); + + $this->Update(); + } + + public function RequestAction($Ident, $Value) + { + switch ($Ident) { + case "State": + SetValue($this->GetIDForIdent("State"), (bool)$Value); + if ((bool)$Value) { + $this->Update(); + } + return; + + case "SDL_Reset": + if ((bool)$Value) { + $this->ResetSDLToStartPoint(); + SetValue($this->GetIDForIdent("SDL_Reset"), false); + $this->Update(); + } + return; + + case "EV_Reset": + if ((bool)$Value) { + $this->ResetEVToRealPoint(); + SetValue($this->GetIDForIdent("EV_Reset"), false); + $this->Update(); + } + return; + } + + throw new Exception("Invalid Ident: " . $Ident); + } + + public function Update() + { + $semKey = "BEVSDL4_Update_" . $this->InstanceID; + + if (!IPS_SemaphoreEnter($semKey, 5000)) { + $this->SendDebug("Update", "SKIP Semaphore locked", 0); + return; + } + + try { + if (!GetValue($this->GetIDForIdent("State"))) { + return; + } + + $cfg = json_decode($this->GetBufferSafe("StaticBatteryConfig"), true); + if (!is_array($cfg) || empty($cfg["bats"])) { + $this->WriteAllZero("StaticBatteryConfig leer"); + return; + } + + $nowMs = (int)round(microtime(true) * 1000); + $lastMs = (int)$this->GetBufferSafe("Int_LastMs"); + + if ($lastMs <= 0) { + $lastMs = $nowMs; + } + + $dtMs = max(0, $nowMs - $lastMs); + $dtMs = min($dtMs, 60000); + $dtH = $dtMs / 3600000.0; + + $runtime = $this->CalculateRuntimeState($cfg); + + $sdlTotalKWh = (float)$runtime["total"]["SDL_kWh_total"]; + $evTotalKWh = (float)$runtime["total"]["EV_kWh_total"]; + + $EsdlKWh = (float)$this->GetBufferSafe("Int_E_SDL_kWh"); + $EevKWh = (float)$this->GetBufferSafe("Int_E_EV_kWh"); + + if ($this->GetBufferSafe("Int_Init_SDL") !== "1") { + $EsdlKWh = (float)$runtime["total"]["SDL_Start_kWh"]; + $this->SetBuffer("Int_Init_SDL", "1"); + } + + if ($this->GetBufferSafe("Int_Init_EV") !== "1") { + $EevKWh = (float)$runtime["total"]["Real_EV_kWh"]; + $this->SetBuffer("Int_Init_EV", "1"); + } + + $pSdlIstWRaw = (float)GetValue($this->GetIDForIdent("Aktuelle_Leistung_SDL")); + $pEvIstWRaw = (float)GetValue($this->GetIDForIdent("Aktuelle_Leistung_EV")); + + $maxSdlChargeW = (float)$runtime["total"]["SDL_Charge_kW"] * 1000.0; + $maxSdlDisW = (float)$runtime["total"]["SDL_Discharge_kW"] * 1000.0; + $maxEvChargeW = (float)$runtime["total"]["EV_Charge_kW"] * 1000.0; + $maxEvDisW = (float)$runtime["total"]["EV_Discharge_kW"] * 1000.0; + + $pSdlIstW = max(-$maxSdlDisW, min($maxSdlChargeW, $pSdlIstWRaw)); + $pEvIstW = max(-$maxEvDisW, min($maxEvChargeW, $pEvIstWRaw)); + + if (abs($pSdlIstW) > 1.0) { + $EsdlKWh += ($pSdlIstW / 1000.0) * $dtH; + } + + if (abs($pEvIstW) > 1.0) { + $EevKWh += ($pEvIstW / 1000.0) * $dtH; + } + + $EsdlKWh = ($sdlTotalKWh > 0) ? max(0.0, min($sdlTotalKWh, $EsdlKWh)) : 0.0; + $EevKWh = ($evTotalKWh > 0) ? max(0.0, min($evTotalKWh, $EevKWh)) : 0.0; + + $sdlPct = ($sdlTotalKWh > 0) ? ($EsdlKWh / $sdlTotalKWh * 100.0) : 0.0; + $evPct = ($evTotalKWh > 0) ? ($EevKWh / $evTotalKWh * 100.0) : 0.0; + + $sdlPct = max(0.0, min(100.0, $sdlPct)); + $evPct = max(0.0, min(100.0, $evPct)); + + $this->SetIdentValue("SDL_Pos", round($sdlPct, 3)); + $this->SetIdentValue("SoC_EV", round($evPct, 3)); + + $this->SetIdentValue("P_SDL_laden", round($maxSdlChargeW, 0)); + $this->SetIdentValue("P_SDL_entladen", round($maxSdlDisW, 0)); + $this->SetIdentValue("P_EV_laden", (int)round($maxEvChargeW, 0)); + $this->SetIdentValue("P_EV_entladen", (int)round($maxEvDisW, 0)); + $this->SetIdentValue("SDL_Start_Pos", round((float)$runtime["total"]["SDL_Start_pct"], 3)); + + $runtime["total"]["SDL_SoC_pct"] = round($sdlPct, 3); + $runtime["total"]["EV_SoC_pct"] = round($evPct, 3); + $runtime["total"]["SDL_E_kWh_virtual"] = round($EsdlKWh, 4); + $runtime["total"]["EV_E_kWh_virtual"] = round($EevKWh, 4); + $runtime["total"]["dtMs"] = $dtMs; + $runtime["total"]["SDL_Ist_W_raw"] = round($pSdlIstWRaw, 0); + $runtime["total"]["SDL_Ist_W_used"] = round($pSdlIstW, 0); + $runtime["total"]["EV_Ist_W_raw"] = round($pEvIstWRaw, 0); + $runtime["total"]["EV_Ist_W_used"] = round($pEvIstW, 0); + + $this->SetIdentValue("CalcJSON", json_encode($runtime, JSON_PRETTY_PRINT)); + + $this->ApplySetpoints($runtime); + + $this->SetBuffer("Int_E_SDL_kWh", (string)$EsdlKWh); + $this->SetBuffer("Int_E_EV_kWh", (string)$EevKWh); + $this->SetBuffer("Int_LastMs", (string)$nowMs); + + } catch (Throwable $e) { + $this->SendDebug("Update ERROR", $e->getMessage(), 0); + $this->WriteAllZero("Exception: " . $e->getMessage()); + } finally { + IPS_SemaphoreLeave($semKey); + } + } + + private function BuildStaticBatteryConfig(): void + { + $batteriesRaw = $this->ReadPropertyString("Batteries"); + $batteries = json_decode($batteriesRaw, true); + if (!is_array($batteries)) { + $batteries = []; + } + + $sdlWCharge = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Laden")); + $sdlWDis = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Entladen")); + $reserveH = max(0.0, (float)$this->ReadPropertyFloat("ReserveHours")); + + $sumPowerW = 0.0; + foreach ($batteries as $b) { + $sumPowerW += max(0.0, (float)($b["powerbat"] ?? 0)); + } + + $cfg = [ + "inputs" => [ + "SDL_Leistung_W_laden" => $sdlWCharge, + "SDL_Leistung_W_entladen" => $sdlWDis, + "ReserveHours" => $reserveH, + "SumBatPower_W" => round($sumPowerW, 0) + ], + "bats" => [] + ]; + + if ($sumPowerW <= 0.0) { + $this->SetBuffer("StaticBatteryConfig", json_encode($cfg)); + return; + } + + $sdlKWCharge = $sdlWCharge / 1000.0; + $sdlKWDis = $sdlWDis / 1000.0; + $sumPowerKW = $sumPowerW / 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 || $pBatKW <= 0.0) { + continue; + } + + $sdlShareKWCharge = ($sdlKWCharge / $sumPowerKW) * $pBatKW; + $sdlShareKWDis = ($sdlKWDis / $sumPowerKW) * $pBatKW; + + $evShareKWCharge = max(0.0, $pBatKW - $sdlShareKWCharge); + $evShareKWDis = max(0.0, $pBatKW - $sdlShareKWDis); + + $underKWh = $sdlShareKWDis * $reserveH; + $upperReserveKWh = $sdlShareKWCharge * $reserveH; + $upKWh = $capKWh - $upperReserveKWh; + + $underKWh = max(0.0, min($capKWh, $underKWh)); + $upKWh = max(0.0, min($capKWh, $upKWh)); + + if ($upKWh < $underKWh) { + $mid = $capKWh / 2.0; + $underKWh = $mid; + $upKWh = $mid; + } + + $sdlTotalKWh = $underKWh + ($capKWh - $upKWh); + $evTotalKWh = max(0.0, $capKWh - $sdlTotalKWh); + + $cfg["bats"][] = [ + "idx" => $idx, + "typ" => (string)($b["typ"] ?? ("Bat " . ($idx + 1))), + "socVarId" => (int)($b["soc"] ?? 0), + "capKWh" => $capKWh, + "pBatW" => $pBatW, + + "sdlShareKW_laden" => $sdlShareKWCharge, + "sdlShareKW_entladen" => $sdlShareKWDis, + "evShareKW_laden" => $evShareKWCharge, + "evShareKW_entladen" => $evShareKWDis, + + "underKWh" => $underKWh, + "upKWh" => $upKWh, + "upperReserveKWh" => $upperReserveKWh, + + "SDL_kWh_total" => $sdlTotalKWh, + "EV_kWh_total" => $evTotalKWh + ]; + } + + $this->SetBuffer("StaticBatteryConfig", json_encode($cfg)); + $this->SendDebug("BuildStaticBatteryConfig", "Neu berechnet: " . count($cfg["bats"]) . " Batterien", 0); + } + + private function CalculateRuntimeState(array $cfg): array + { + $calc = [ + "inputs" => $cfg["inputs"] ?? [], + "batteries" => [], + "total" => [] + ]; + + $sdlTotal = 0.0; + $evTotal = 0.0; + $sdlStart = 0.0; + $realEvTotal = 0.0; + $realSdlTotal = 0.0; + $sdlChargeKW = 0.0; + $sdlDisKW = 0.0; + $evChargeKW = 0.0; + $evDisKW = 0.0; + $totalCap = 0.0; + + foreach ($cfg["bats"] as $bat) { + $capKWh = (float)$bat["capKWh"]; + $socPct = $this->ReadSocPercent((int)$bat["socVarId"]); + $realKWh = $capKWh * $socPct / 100.0; + + $underKWh = (float)$bat["underKWh"]; + $upKWh = (float)$bat["upKWh"]; + + $sdlKWhTotal = (float)$bat["SDL_kWh_total"]; + $evKWhTotal = (float)$bat["EV_kWh_total"]; + + $realEvKWh = max(0.0, min($evKWhTotal, $realKWh - $underKWh)); + $realSdlKWh = max(0.0, $realKWh - $realEvKWh); + + if ($realKWh <= 0.0) { + $realEvKWh = 0.0; + $realSdlKWh = 0.0; + } + + if ($realKWh >= $capKWh) { + $realEvKWh = $evKWhTotal; + $realSdlKWh = $sdlKWhTotal; + } + + $evSocReal = ($evKWhTotal > 0.0) ? ($realEvKWh / $evKWhTotal * 100.0) : 0.0; + $sdlSocReal = ($sdlKWhTotal > 0.0) ? ($realSdlKWh / $sdlKWhTotal * 100.0) : 0.0; + + $sdlCh = (float)$bat["sdlShareKW_laden"]; + $sdlDis = (float)$bat["sdlShareKW_entladen"]; + $evCh = (float)$bat["evShareKW_laden"]; + $evDis = (float)$bat["evShareKW_entladen"]; + + // EV-Laden physikalisch sperren, wenn obere EV-Grenze erreicht ist + if ($realKWh >= $upKWh) { + $evCh = 0.0; + } + + // EV-Entladen physikalisch sperren, wenn untere EV-Grenze erreicht ist + if ($realKWh <= $underKWh) { + $evDis = 0.0; + } + + // Absolute Batteriegrenzen + if ($realKWh <= 0.0) { + $sdlDis = 0.0; + $evDis = 0.0; + } + + if ($realKWh >= $capKWh) { + $sdlCh = 0.0; + $evCh = 0.0; + } + + $sdlTotal += $sdlKWhTotal; + $evTotal += $evKWhTotal; + $sdlStart += $underKWh; + + $realEvTotal += $realEvKWh; + $realSdlTotal += $realSdlKWh; + + $sdlChargeKW += $sdlCh; + $sdlDisKW += $sdlDis; + $evChargeKW += $evCh; + $evDisKW += $evDis; + + $totalCap += $capKWh; + + $calc["batteries"][] = [ + "idx" => $bat["idx"], + "typ" => $bat["typ"], + "SoC_varId" => $bat["socVarId"], + "SoC_pct" => round($socPct, 3), + "Effektive_kWh" => round($realKWh, 3), + + "under_grenze_kWh" => round($underKWh, 3), + "up_grenze_kWh" => round($upKWh, 3), + + "EV_SOC_REAL" => round(max(0, min(100, $evSocReal)), 3), + "SDL_SOC_REAL" => round(max(0, min(100, $sdlSocReal)), 3), + + "EV_kWh_real" => round($realEvKWh, 3), + "SDL_kWh_real" => round($realSdlKWh, 3), + + "EV_kWh_total" => round($evKWhTotal, 3), + "SDL_kWh_total" => round($sdlKWhTotal, 3), + + "SDL_Charge_kW" => round($sdlCh, 3), + "SDL_Discharge_kW" => round($sdlDis, 3), + "EV_Charge_kW" => round($evCh, 3), + "EV_Discharge_kW" => round($evDis, 3) + ]; + } + + $startPct = ($sdlTotal > 0.0) ? ($sdlStart / $sdlTotal * 100.0) : 0.0; + + $calc["total"] = [ + "SDL_kWh_total" => round($sdlTotal, 4), + "EV_kWh_total" => round($evTotal, 4), + "SDL_Start_kWh" => round($sdlStart, 4), + "SDL_Start_pct" => round($startPct, 3), + + "Real_EV_kWh" => round($realEvTotal, 4), + "Real_SDL_kWh" => round($realSdlTotal, 4), + + "SDL_Charge_kW" => round($sdlChargeKW, 4), + "SDL_Discharge_kW" => round($sdlDisKW, 4), + "EV_Charge_kW" => round($evChargeKW, 4), + "EV_Discharge_kW" => round($evDisKW, 4), + + "totalCap_kWh" => round($totalCap, 4) + ]; + + return $calc; + } + + public function ApplySetpoints(array $runtime = null): void + { + if ($runtime === null) { + $raw = (string)GetValue($this->GetIDForIdent("CalcJSON")); + $runtime = json_decode($raw, true); + } + + if (!is_array($runtime)) { + return; + } + + $pEvW = (float)GetValue($this->GetIDForIdent("Nennleistung_Soll_EV")); + $pSdlW = (float)GetValue($this->GetIDForIdent("Nennleistung_Soll_SDL")); + + $distribution = $this->CalculateBatteryDistribution($runtime, $pEvW, $pSdlW); + $this->WriteBatteryPowerSetpoints($distribution); + } + + private function CalculateBatteryDistribution(array $runtime, float $pEvW, float $pSdlW): array + { + $batteries = $runtime["batteries"] ?? []; + if (empty($batteries)) { + return []; + } + + $distributePower = function (float $targetPower, string $mode) use ($batteries): array { + $result = []; + + foreach ($batteries as $bat) { + $result[(int)$bat["idx"]] = 0.0; + } + + if (abs($targetPower) < 0.01) { + return $result; + } + + $isCharge = $targetPower > 0.0; + $needW = abs($targetPower); + + $socKey = ($mode === "EV") ? "EV_SOC_REAL" : "SDL_SOC_REAL"; + $limitKey = $isCharge ? $mode . "_Charge_kW" : $mode . "_Discharge_kW"; + + $groups = []; + + foreach ($batteries as $bat) { + $soc = (int)round((float)$bat[$socKey]); + $maxW = max(0.0, (float)$bat[$limitKey] * 1000.0); + + if ($maxW <= 0.0) { + continue; + } + + $groups[$soc][] = [ + "idx" => (int)$bat["idx"], + "maxW" => $maxW + ]; + } + + if ($isCharge) { + ksort($groups); + } else { + krsort($groups); + } + + foreach ($groups as $group) { + if ($needW <= 0.01) { + break; + } + + $groupMax = 0.0; + foreach ($group as $g) { + $groupMax += $g["maxW"]; + } + + if ($groupMax <= 0.0) { + continue; + } + + $useW = min($needW, $groupMax); + $ratio = $useW / $groupMax; + + foreach ($group as $g) { + $result[$g["idx"]] += $g["maxW"] * $ratio; + } + + $needW -= $useW; + } + + if (!$isCharge) { + foreach ($result as $idx => $w) { + $result[$idx] = -$w; + } + } + + return $result; + }; + + $ev = $distributePower($pEvW, "EV"); + $sdl = $distributePower($pSdlW, "SDL"); + + $out = []; + + foreach ($batteries as $bat) { + $idx = (int)$bat["idx"]; + + $totalW = ($ev[$idx] ?? 0.0) + ($sdl[$idx] ?? 0.0); + + $out[] = [ + "idx" => $idx, + "typ" => (string)$bat["typ"], + "chargeW" => $totalW > 0 ? round($totalW, 0) : 0, + "dischargeW" => $totalW < 0 ? round(abs($totalW), 0) : 0 + ]; + } + + $this->UpdateActualPowerValues($pEvW, $pSdlW); + + return $out; + } + + private function UpdateActualPowerValues(float $pEvW, float $pSdlW): void + { + $totalPowerIst = $this->GetTotalBatteryPowerIstW(); + + $eps = 0.01; + $sumSoll = $pEvW + $pSdlW; + + if (abs($sumSoll) > $eps) { + $factor = $totalPowerIst / $sumSoll; + $rawEV = $pEvW * $factor; + $rawSDL = $pSdlW * $factor; + } else { + if (abs($totalPowerIst) < 50.0) { + $rawEV = $pEvW; + $rawSDL = $pSdlW; + } else { + $rawEV = $pEvW; + $rawSDL = $totalPowerIst - $rawEV; + } + } + + if ($this->ReadPropertyBoolean("FilterAktiv")) { + $rawEV = $this->FilterCurrent("EV", $rawEV, $pEvW); + $rawSDL = $this->FilterCurrent("SDL", $rawSDL, $pSdlW); + } else { + $this->ClearCurrentFilterBuffers(); + } + + $this->SetIdentValue("Aktuelle_Leistung_EV", $rawEV); + $this->SetIdentValue("Aktuelle_Leistung_SDL", $rawSDL); + } + + private function FilterCurrent(string $channel, float $raw, float $target): float + { + $tolW = max(300.0, abs($target) * 0.10); + $last = (float)$this->GetBufferSafe("CUR_{$channel}_VAL"); + + if ($this->GetBufferSafe("CUR_{$channel}_INIT") !== "1") { + $this->SetBuffer("CUR_{$channel}_INIT", "1"); + $this->SetBuffer("CUR_{$channel}_VAL", (string)$raw); + return $raw; + } + + if (abs($target) < 0.5) { + $this->SetBuffer("CUR_{$channel}_VAL", "0"); + return 0.0; + } + + $sameDirection = (($target > 0 && $raw > 0) || ($target < 0 && $raw < 0)); + $notCrazyHigh = abs($raw) <= abs($target) + $tolW; + + if ($sameDirection && $notCrazyHigh) { + $this->SetBuffer("CUR_{$channel}_VAL", (string)$raw); + return $raw; + } + + return $last; + } + + private function WriteBatteryPowerSetpoints(array $distribution): void + { + $batteriesCfg = json_decode($this->ReadPropertyString("Batteries"), true); + if (!is_array($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")); + + $chargeW = max(0.0, (float)($d["chargeW"] ?? 0)); + $dischargeW = max(0.0, (float)($d["dischargeW"] ?? 0)); + + if ($chargeW > 0.0 && $dischargeW > 0.0) { + $chargeW = 0.0; + $dischargeW = 0.0; + } + + $this->WriteByVendorRegistersSingleMode($typ, $cfg, $chargeW, $dischargeW); + } + } + + private function WriteByVendorRegistersSingleMode(string $typ, array $cfg, float $chargeW, float $dischargeW): void + { + $t = mb_strtolower($typ); + + $varPowerCharge = (int)($cfg["powerbat_laden"] ?? 0); + $varPowerDisch = (int)($cfg["powerbat_entladen"] ?? 0); + $varMode = (int)($cfg["register_ladenentladen_modus"] ?? 0); + + $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)); + } + }; + + $modeCharge = 1; + $modeDisch = 2; + + if (strpos($t, "goodwe") !== false) { + $modeCharge = 11; + $modeDisch = 12; + } elseif (strpos($t, "solaredge") !== false) { + $modeCharge = 3; + $modeDisch = 4; + } + + if ($chargeW > 0.0) { + $setInt($varMode, $modeCharge); + + if (strpos($t, "goodwe") !== false) { + $setW($varPowerCharge, $chargeW); + $setW($varPowerDisch, 0); + return; + } + + $setW($varPowerCharge, $chargeW); + $setW($varPowerDisch, 0); + return; + } + + if ($dischargeW > 0.0) { + $setInt($varMode, $modeDisch); + + if (strpos($t, "goodwe") !== false) { + $setW($varPowerCharge, $dischargeW); + $setW($varPowerDisch, 0); + return; + } + + $setW($varPowerDisch, $dischargeW); + $setW($varPowerCharge, 0); + return; + } + + $setW($varPowerCharge, 0); + $setW($varPowerDisch, 0); + } + + private function ResetSDLToStartPoint(): void + { + $cfg = json_decode($this->GetBufferSafe("StaticBatteryConfig"), true); + if (!is_array($cfg) || empty($cfg["bats"])) { + return; + } + + $total = 0.0; + $start = 0.0; + + foreach ($cfg["bats"] as $bat) { + $total += (float)$bat["SDL_kWh_total"]; + $start += (float)$bat["underKWh"]; + } + + $start = ($total > 0.0) ? max(0.0, min($total, $start)) : 0.0; + + $this->SetBuffer("Int_E_SDL_kWh", (string)$start); + $this->SetBuffer("Int_Init_SDL", "1"); + $this->SetBuffer("Int_LastMs", (string)round(microtime(true) * 1000)); + } + + private function ResetEVToRealPoint(): void + { + $cfg = json_decode($this->GetBufferSafe("StaticBatteryConfig"), true); + if (!is_array($cfg) || empty($cfg["bats"])) { + return; + } + + $runtime = $this->CalculateRuntimeState($cfg); + $realEv = (float)$runtime["total"]["Real_EV_kWh"]; + + $this->SetBuffer("Int_E_EV_kWh", (string)$realEv); + $this->SetBuffer("Int_Init_EV", "1"); + $this->SetBuffer("Int_LastMs", (string)round(microtime(true) * 1000)); + } + + private function GetTotalBatteryPowerIstW(): float + { + $cfg = json_decode($this->ReadPropertyString("Batteries"), true); + if (!is_array($cfg)) { + return 0.0; + } + + $sum = 0.0; + + foreach ($cfg as $b) { + $varId = (int)($b["register_bat_power"] ?? 0); + if ($varId > 0 && IPS_VariableExists($varId)) { + $sum += -1.0 * (float)GetValue($varId); + } + } + + return $sum; + } + + private function ReadSocPercent(int $varId): float + { + 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; + } + + return max(0.0, min(100.0, (float)$v)); + } + + private function SetIdentValue(string $ident, $value): void + { + $id = @$this->GetIDForIdent($ident); + if ($id > 0) { + SetValue($id, $value); + } + } + + private function GetBufferSafe(string $name): string + { + $v = $this->GetBuffer($name); + return is_string($v) ? $v : ""; + } + + private function ClearCurrentFilterBuffers(): void + { + foreach (["EV", "SDL"] as $ch) { + $this->SetBuffer("CUR_{$ch}_INIT", ""); + $this->SetBuffer("CUR_{$ch}_VAL", ""); + } + } + + 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); + $this->SetIdentValue("P_EV_entladen", 0); + $this->SetIdentValue("CalcJSON", json_encode(["error" => $reason], JSON_PRETTY_PRINT)); + } +} + +?> \ No newline at end of file