From 2e69afa9f4ceaa6263777077aae3346009d86a75 Mon Sep 17 00:00:00 2001 From: "belevo\\mh" Date: Mon, 4 May 2026 18:48:09 +0200 Subject: [PATCH] no message --- Bat_EV_SDL_V4/form.json | 193 +++++---- Bat_EV_SDL_V4/module.php | 867 +++++++++++++++------------------------ 2 files changed, 450 insertions(+), 610 deletions(-) diff --git a/Bat_EV_SDL_V4/form.json b/Bat_EV_SDL_V4/form.json index cec16fd..68b156b 100644 --- a/Bat_EV_SDL_V4/form.json +++ b/Bat_EV_SDL_V4/form.json @@ -1,66 +1,75 @@ { "elements": [ - { - "type": "Label", - "caption": "Aufgepasst: Bei Goodwe nur Ladenvariable auswählen und Entladen Dummy-Variable. Goodwe braucht nur eine Leistungssoll-Variable. Entlade NICHT auf gleiche Variable 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", + "caption": "Batterien", + "rowCount": 5, "add": true, "delete": true, + "sort": { + "column": "typ", + "direction": "ascending" + }, "columns": [ { "caption": "Typ", "name": "typ", - "width": "120px", - "add": "Batterie", + "width": "160px", "edit": { "type": "ValidationTextBox" } }, { - "caption": "Leistung (W)", + "caption": "Kapazität", + "name": "capazity", + "width": "100px", + "suffix": " kWh", + "edit": { + "type": "NumberSpinner", + "minimum": 0, + "digits": 2 + } + }, + { + "caption": "Batterieleistung", "name": "powerbat", "width": "120px", - "add": 5000, + "suffix": " W", "edit": { - "type": "NumberSpinner" + "type": "NumberSpinner", + "minimum": 0, + "digits": 0 } }, { - "caption": "Kapazität (kWh)", - "name": "capazity", - "width": "140px", - "add": 60, - "edit": { - "type": "NumberSpinner" - } - }, - { - "caption": "SoC", + "caption": "SoC Variable", "name": "soc", - "width": "160px", - "add": 0, + "width": "120px", "edit": { "type": "SelectVariable" } }, { - "caption": "Power Laden", + "caption": "Ist-Leistung Register", + "name": "register_bat_power", + "width": "150px", + "edit": { + "type": "SelectVariable" + } + }, + { + "caption": "Ladeleistung Register", "name": "powerbat_laden", - "width": "160px", - "add": 0, + "width": "150px", "edit": { "type": "SelectVariable" } }, { - "caption": "Power Entladen", + "caption": "Entladeleistung Register", "name": "powerbat_entladen", "width": "160px", - "add": 0, "edit": { "type": "SelectVariable" } @@ -68,56 +77,102 @@ { "caption": "Modus Register", "name": "register_ladenentladen_modus", - "width": "180px", - "add": 0, - "edit": { - "type": "SelectVariable" - } - }, - { - "caption": "Ist-Leistung", - "name": "register_bat_power", - "width": "180px", - "add": 0, + "width": "140px", "edit": { "type": "SelectVariable" } } + ], + "values": [] + }, + { + "type": "ExpansionPanel", + "caption": "SDL / EV Fenster", + "items": [ + { + "type": "NumberSpinner", + "name": "SDL_Leistung_Laden", + "caption": "SDL Leistung Laden", + "suffix": " W", + "minimum": 0, + "digits": 0 + }, + { + "type": "NumberSpinner", + "name": "SDL_Leistung_Entladen", + "caption": "SDL Leistung Entladen", + "suffix": " W", + "minimum": 0, + "digits": 0 + }, + { + "type": "NumberSpinner", + "name": "ReserveHoursLaden", + "caption": "SDL Reservezeit Laden", + "suffix": " h", + "minimum": 0, + "maximum": 24, + "digits": 2 + }, + { + "type": "NumberSpinner", + "name": "ReserveHoursEntladen", + "caption": "SDL Reservezeit Entladen", + "suffix": " h", + "minimum": 0, + "maximum": 24, + "digits": 2 + }, + { + "type": "NumberSpinner", + "name": "SDL_Start_Pos_Config", + "caption": "Virtueller SDL Startwert", + "suffix": " %", + "minimum": 0, + "maximum": 100, + "digits": 2 + }, + { + "type": "NumberSpinner", + "name": "EV_Start_Pos_Config", + "caption": "Virtueller EV Startwert", + "suffix": " %", + "minimum": 0, + "maximum": 100, + "digits": 2 + } ] }, { - "type": "NumberSpinner", - "name": "SDL_Leistung_Laden", - "caption": "SDL Leistung Laden", - "suffix": "W" + "type": "ExpansionPanel", + "caption": "Regelung", + "items": [ + { + "type": "NumberSpinner", + "name": "UpdateInterval", + "caption": "Update Intervall", + "suffix": " min", + "minimum": 0, + "digits": 0 + }, + { + "type": "CheckBox", + "name": "FilterAktiv", + "caption": "Filter für aktuelle EV/SDL Leistung aktiv" + } + ] + } + ], + "actions": [ + { + "type": "Button", + "caption": "Virtuelle Konten auf Startwerte zurücksetzen", + "onClick": "GEF_ResetVirtualAccounts($id);" }, { - "type": "NumberSpinner", - "name": "SDL_Leistung_Entladen", - "caption": "SDL Leistung Entladen", - "suffix": "W" - }, - { - "type": "NumberSpinner", - "name": "ReserveHours", - "caption": "SDL Reservezeit", - "suffix": "h", - "minimum": 0, - "maximum": 24, - "digits": 2 - }, - { - "type": "NumberSpinner", - "name": "UpdateInterval", - "caption": "Update Intervall", - "suffix": "Sekunden", - "minimum": 0, - "maximum": 86400 - }, - { - "type": "CheckBox", - "name": "FilterAktiv", - "caption": "Aktuelle Leistung filtern" + "type": "Button", + "caption": "Jetzt aktualisieren", + "onClick": "GEF_Update($id);" } ] -} \ No newline at end of file +} diff --git a/Bat_EV_SDL_V4/module.php b/Bat_EV_SDL_V4/module.php index 9d45ef1..afbb2b7 100644 --- a/Bat_EV_SDL_V4/module.php +++ b/Bat_EV_SDL_V4/module.php @@ -10,25 +10,28 @@ class Bat_EV_SDL_V4 extends IPSModule $this->RegisterPropertyString("Batteries", "[]"); $this->RegisterPropertyInteger("SDL_Leistung_Laden", 0); // W $this->RegisterPropertyInteger("SDL_Leistung_Entladen", 0); // W - $this->RegisterPropertyFloat("ReserveHours", 0.5); // h, vorher fix 0.5h - $this->RegisterPropertyInteger("UpdateInterval", 5); // Sekunden + $this->RegisterPropertyFloat("ReserveHoursLaden", 0.5); // h + $this->RegisterPropertyFloat("ReserveHoursEntladen", 0.5); // h + $this->RegisterPropertyFloat("SDL_Start_Pos_Config", 50.0); // % + $this->RegisterPropertyFloat("EV_Start_Pos_Config", 50.0); // % + $this->RegisterPropertyInteger("UpdateInterval", 5); // Minuten $this->RegisterPropertyBoolean("FilterAktiv", true); // Status $this->RegisterVariableBoolean("State", "Aktiv", "~Switch", 1); $this->EnableAction("State"); - // Prozentwerte / virtuelle Konten - $this->RegisterVariableFloat("SDL_Pos", "SDL Energie verfügbar (%)", "", 10); - $this->RegisterVariableFloat("SoC_EV", "EV Energie verfügbar (%)", "", 11); - $this->RegisterVariableFloat("SDL_Start_Pos", "SDL Start SoC (%)", "", 35); - $this->RegisterVariableFloat("EV_Start_Pos", "EV Start SoC (%)", "", 36); - - // Soll-/Istwerte + // Sollwerte $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); + + // Aktuelle Leistung / Rückrechnung $this->RegisterVariableFloat("Aktuelle_Leistung_SDL", "Aktuelle Leistung SDL", "", 4); + $this->RegisterVariableFloat("Aktuelle_Leistung_EV", "Aktuelle Leistung EV", "", 5); + + // Virtuelle SoC Werte + $this->RegisterVariableFloat("SDL_Pos", "SDL Energie verfügbar virtuell (%)", "", 10); + $this->RegisterVariableFloat("SoC_EV", "EV Energie verfügbar virtuell (%)", "", 11); // Maximalleistungen $this->RegisterVariableFloat("P_SDL_laden", "P SDL laden max (W)", "", 21); @@ -36,14 +39,16 @@ class Bat_EV_SDL_V4 extends IPSModule $this->RegisterVariableInteger("P_EV_laden", "P EV laden max (W)", "", 31); $this->RegisterVariableInteger("P_EV_entladen", "P EV entladen max (W)", "", 32); - // Reset - $this->RegisterVariableBoolean("SDL_Reset", "SDL/EV Konto Reset (Start)", "~Switch", 37); - $this->EnableAction("SDL_Reset"); + // Startwerte / Reset + $this->RegisterVariableFloat("SDL_Start_Pos", "SDL Start SoC (%)", "", 35); + $this->RegisterVariableFloat("EV_Start_Pos", "EV Start SoC (%)", "", 36); + $this->RegisterVariableBoolean("Virtual_Reset", "Virtuelle Konten Reset", "~Switch", 37); + $this->EnableAction("Virtual_Reset"); // Debug $this->RegisterVariableString("CalcJSON", "Berechnung (JSON)", "", 99); - // Timer + // Timer: Prefix/Funktionsname muss zu prefix in module.json passen $this->RegisterTimer("UpdateTimer", 0, 'GEF_Update($_IPS["TARGET"]);'); } @@ -51,37 +56,34 @@ class Bat_EV_SDL_V4 extends IPSModule { parent::ApplyChanges(); - $intervalSec = (int)$this->ReadPropertyInteger("UpdateInterval"); - $this->SetTimerInterval("UpdateTimer", ($intervalSec > 0) ? $intervalSec * 1000 : 0); + $intervalMin = (int)$this->ReadPropertyInteger("UpdateInterval"); + $this->SetTimerInterval("UpdateTimer", ($intervalMin > 0) ? $intervalMin * 60 * 1000 : 0); - // Grenz-/Fensterberechnung nur hier hart neu aufbauen - $this->BuildBatteryCache(true); - - // Wenn Konfig neu ist: Integratoren sauber auf Startpunkte setzen - $this->ResetVirtualAccountsToStartPoint(false); + $this->BuildBatteryPlan(true); + $this->InitVirtualAccountsIfNeeded(); $this->Update(); } public function RequestAction($Ident, $Value) { - if ($Ident === "State") { - SetValue($this->GetIDForIdent("State"), (bool)$Value); - if ((bool)$Value) { - $this->Update(); - } - return; - } + switch ($Ident) { + case "State": + SetValue($this->GetIDForIdent("State"), (bool)$Value); + if ((bool)$Value) { + $this->Update(); + } + return; - if ($Ident === "SDL_Reset") { - if ((bool)$Value) { - $this->ResetVirtualAccountsToStartPoint(true); - SetValue($this->GetIDForIdent("SDL_Reset"), false); - $this->Update(); - } else { - SetValue($this->GetIDForIdent("SDL_Reset"), false); - } - return; + case "Virtual_Reset": + if ((bool)$Value) { + $this->ResetVirtualAccounts(); + SetValue($this->GetIDForIdent("Virtual_Reset"), false); + $this->Update(); + } else { + SetValue($this->GetIDForIdent("Virtual_Reset"), false); + } + return; } throw new Exception("Invalid Ident: " . $Ident); @@ -92,7 +94,7 @@ class Bat_EV_SDL_V4 extends IPSModule $semKey = 'BatEVSDL_Update_' . $this->InstanceID; if (!IPS_SemaphoreEnter($semKey, 5000)) { - $this->SendDebug("Update", "SKIP (Semaphore locked)", 0); + $this->SendDebug("Update", "SKIP - Semaphore locked", 0); return; } @@ -101,279 +103,38 @@ class Bat_EV_SDL_V4 extends IPSModule return; } - // Kein ständiges Neuberechnen der Grenzen mehr. - // Cache muss durch ApplyChanges() oder Reset gebaut sein. - $cache = json_decode($this->GetBufferSafe("BatCacheJSON"), true); - if (!is_array($cache) || empty($cache["bats"])) { - $this->WriteAllZero("cache empty/invalid"); + $this->BuildBatteryPlan(false); + + $plan = json_decode($this->GetBufferSafe("BatPlanJSON"), true); + if (!is_array($plan) || empty($plan["bats"])) { + $this->WriteAllZero("plan empty/invalid"); return; } - $now = microtime(true); - $lastTs = (float)$this->GetBufferSafe("Int_LastTs"); - if ($lastTs <= 0.0) { - $lastTs = $now; - } + // Erst Sollwerte auf Batterien verteilen und aktuelle EV/SDL Leistung zurückrechnen. + $this->ApplySetpoints($plan); - $dtSec = $now - $lastTs; - if ($dtSec < 0.0) { - $dtSec = 0.0; - } - if ($dtSec > 10.0) { - $dtSec = 10.0; - } - $dtH = $dtSec / 3600.0; + // Danach EV und SDL virtuell integrieren. + $virt = $this->IntegrateVirtualAccounts($plan); - $Esdl_kWh = (float)$this->GetBufferSafe("Int_E_SDL_kWh"); - $Eev_kWh = (float)$this->GetBufferSafe("Int_E_EV_kWh"); + $this->SetIdentValue("SDL_Pos", round($virt["SDL_pct"], 3)); + $this->SetIdentValue("SoC_EV", round($virt["EV_pct"], 3)); - // Startwerte nur einmal initialisieren, falls noch leer - if ($this->GetBufferSafe("Int_Init") !== "1") { - $this->ResetVirtualAccountsToStartPoint(false); - $Esdl_kWh = (float)$this->GetBufferSafe("Int_E_SDL_kWh"); - $Eev_kWh = (float)$this->GetBufferSafe("Int_E_EV_kWh"); - } + $tot = $plan["total"] ?? []; + $this->SetIdentValue("P_SDL_laden", round((float)($tot["SDL_Charge_kW"] ?? 0.0) * 1000.0, 0)); + $this->SetIdentValue("P_SDL_entladen", round((float)($tot["SDL_Discharge_kW"] ?? 0.0) * 1000.0, 0)); + $this->SetIdentValue("P_EV_laden", (int)round((float)($tot["EV_Charge_kW"] ?? 0.0) * 1000.0, 0)); + $this->SetIdentValue("P_EV_entladen", (int)round((float)($tot["EV_Discharge_kW"] ?? 0.0) * 1000.0, 0)); - $calc = [ - "inputs" => $cache["inputs"] ?? [], - "batteries" => [], - "total" => [] - ]; - - $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; - } - - $socVarId = (int)($c["socVarId"] ?? 0); - $socPct = $this->ReadSocPercent($socVarId); - $real_kWh = $capKWh / 100.0 * $socPct; - - $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); - - $sdlVirtPct = ($SDL_kWh_ges > 0.0) - ? max(0.0, min(100.0, ($Esdl_kWh / max(0.0001, $this->GetCacheTotalKWh($cache, "SDL_kWh_total"))) * 100.0)) - : 0.0; - - $evVirtPct = ($EV_kWh_ges > 0.0) - ? max(0.0, min(100.0, ($Eev_kWh / max(0.0001, $this->GetCacheTotalKWh($cache, "EV_kWh_total"))) * 100.0)) - : 0.0; - - // Standard EV-Fenster - $evUnderKWh = $underKWh; - $evUpKWh = $upKWh; - - // Wenn SDL virtuell leer/voll ist, EV-Fenster dynamisch erweitern - if ($sdlVirtPct <= 0.1) { - $evUnderKWh = 0.0; - } - if ($sdlVirtPct >= 99.9) { - $evUpKWh = $capKWh; - } - - $evWindowKWh = max(0.0, $evUpKWh - $evUnderKWh); - - $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; - - if ($real_kWh >= $evUnderKWh && $real_kWh <= $evUpKWh) { - $EV_SOC = ($evWindowKWh > 0.0) - ? 100.0 * ($real_kWh - $evUnderKWh) / $evWindowKWh - : 0.0; - - $sdlDisKW = $sdlShareKW_entladen; - $evDisKW = $evShareKW_entladen; - $sdlChKW = $sdlShareKW_laden; - $evChKW = $evShareKW_laden; - - $real_kWh_ev = max(0.0, min($evWindowKWh, $real_kWh - $evUnderKWh)); - $real_kWh_sdl = max(0.0, $real_kWh - $real_kWh_ev); - } elseif ($real_kWh > $evUpKWh) { - $EV_SOC = 100.0; - - $sdlDisKW = $sdlShareKW_entladen; - $evDisKW = $evShareKW_entladen; - $sdlChKW = $sdlShareKW_laden; - $evChKW = 0.0; - - $real_kWh_ev = $evWindowKWh; - $real_kWh_sdl = max(0.0, $real_kWh - $real_kWh_ev); - } else { - $EV_SOC = 0.0; - - $sdlDisKW = $sdlShareKW_entladen; - $evDisKW = 0.0; - $sdlChKW = $sdlShareKW_laden; - $evChKW = $evShareKW_laden; - - $real_kWh_ev = 0.0; - $real_kWh_sdl = max(0.0, $real_kWh); - } - - if ($real_kWh <= 0.0) { - $sdlDisKW = 0.0; - $evDisKW = 0.0; - $real_kWh_ev = 0.0; - $real_kWh_sdl = 0.0; - } - - if ($real_kWh >= $capKWh) { - $sdlChKW = 0.0; - $evChKW = 0.0; - $real_kWh_ev = $EV_kWh; - $real_kWh_sdl = $SDL_kWh; - } - - $EV_SOC = is_finite($EV_SOC) ? max(0.0, min(100.0, $EV_SOC)) : 0.0; - $SDL_SOC = ($SDL_kWh > 0.0) ? 100.0 * $real_kWh_sdl / $SDL_kWh : 0.0; - $SDL_SOC = is_finite($SDL_SOC) ? max(0.0, min(100.0, $SDL_SOC)) : 0.0; - - $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), - ]; - } - - $maxSDL_ch = $sdlChKW_ges * 1000.0; - $maxSDL_dis = $sdlDisKW_ges * 1000.0; - $maxEV_ch = $evChKW_ges * 1000.0; - $maxEV_dis = $evDisKW_ges * 1000.0; - - // Aktuelle Istleistungen lesen - $pSdlIstW = (float)GetValue($this->GetIDForIdent("Aktuelle_Leistung_SDL")); - $pEvIstW = (float)GetValue($this->GetIDForIdent("Aktuelle_Leistung_EV")); - - // Clamp robust - $pSdlIstW_used = max(-$maxSDL_dis, min($maxSDL_ch, $pSdlIstW)); - $pEvIstW_used = max(-$maxEV_dis, min($maxEV_ch, $pEvIstW)); - - $epsW = 1.0; - - // SDL und EV jetzt beide fließend integrieren - if (abs($pSdlIstW_used) > $epsW) { - $Esdl_kWh += ($pSdlIstW_used / 1000.0) * $dtH; - } - - if (abs($pEvIstW_used) > $epsW) { - $Eev_kWh += ($pEvIstW_used / 1000.0) * $dtH; - } - - $Esdl_kWh = ($SDL_kWh_ges > 0.0) ? max(0.0, min($SDL_kWh_ges, $Esdl_kWh)) : 0.0; - $Eev_kWh = ($EV_kWh_ges > 0.0) ? max(0.0, min($EV_kWh_ges, $Eev_kWh)) : 0.0; - - $sdlPosPct = ($SDL_kWh_ges > 0.0) ? $Esdl_kWh / $SDL_kWh_ges * 100.0 : 0.0; - $evPosPct = ($EV_kWh_ges > 0.0) ? $Eev_kWh / $EV_kWh_ges * 100.0 : 0.0; - - $sdlPosPct = max(0.0, min(100.0, $sdlPosPct)); - $evPosPct = max(0.0, min(100.0, $evPosPct)); - - $this->SetIdentValue("SDL_Pos", round($sdlPosPct, 3)); - $this->SetIdentValue("SoC_EV", round($evPosPct, 3)); - - $this->SetIdentValue("P_SDL_laden", round($maxSDL_ch, 0)); - $this->SetIdentValue("P_SDL_entladen", round($maxSDL_dis, 0)); - $this->SetIdentValue("P_EV_laden", (int)round($maxEV_ch, 0)); - $this->SetIdentValue("P_EV_entladen", (int)round($maxEV_dis, 0)); - - $calc["total"] = [ - "SDL_SoC_pct" => round($sdlPosPct, 3), - "EV_SoC_pct" => round($evPosPct, 3), - - "SDL_kWh_account" => round($Esdl_kWh, 3), - "EV_kWh_account" => round($Eev_kWh, 3), - - "SDL_kWh_total" => round($SDL_kWh_ges, 3), - "EV_kWh_total" => round($EV_kWh_ges, 3), - - "Real_EV_kWh_from_SOC" => round($real_kWh_ev_ges, 3), - "Real_SDL_kWh_from_SOC" => round($real_kWh_sdl_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), - - "SDL_Ist_W_raw" => round($pSdlIstW, 0), - "SDL_Ist_W_used" => round($pSdlIstW_used, 0), - "EV_Ist_W_raw" => round($pEvIstW, 0), - "EV_Ist_W_used" => round($pEvIstW_used, 0), - - "dtSec" => round($dtSec, 3) + $calc = $plan; + $calc["virtual"] = $virt; + $calc["actual"] = [ + "EV_W" => round((float)GetValue($this->GetIDForIdent("Aktuelle_Leistung_EV")), 0), + "SDL_W" => round((float)GetValue($this->GetIDForIdent("Aktuelle_Leistung_SDL")), 0) ]; $this->SetIdentValue("CalcJSON", json_encode($calc, JSON_PRETTY_PRINT)); - // Setpoints schreiben / aktuelle Leistungen berechnen - $this->ApplySetpoints(); - - $this->SetBuffer("Int_LastTs", (string)$now); - $this->SetBuffer("Int_E_SDL_kWh", (string)$Esdl_kWh); - $this->SetBuffer("Int_E_EV_kWh", (string)$Eev_kWh); - $this->SetBuffer("Int_Init", "1"); } catch (Throwable $e) { $this->SendDebug("Update ERROR", $e->getMessage() . " @ " . $e->getFile() . ":" . $e->getLine(), 0); $this->WriteAllZero("Exception: " . $e->getMessage()); @@ -382,22 +143,29 @@ class Bat_EV_SDL_V4 extends IPSModule } } - private function BuildBatteryCache(bool $force): void + /** + * Baut die statischen Batterie-Fenster und Leistungsanteile. + * Diese Methode enthält die komplette Grenzberechnung und läuft nur bei Config-Änderung neu. + */ + private function BuildBatteryPlan(bool $force): void { $batteriesRaw = $this->ReadPropertyString("Batteries"); - $sdlTotalW_laden = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Laden")); + + $sdlTotalW_laden = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Laden")); $sdlTotalW_entladen = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Entladen")); - $hours = max(0.0, (float)$this->ReadPropertyFloat("ReserveHours")); + + $reserveH_laden = max(0.0, (float)$this->ReadPropertyFloat("ReserveHoursLaden")); + $reserveH_entladen = max(0.0, (float)$this->ReadPropertyFloat("ReserveHoursEntladen")); $hash = md5(json_encode([ "Batteries" => $batteriesRaw, "SDL_W_Laden" => $sdlTotalW_laden, "SDL_W_Entladen" => $sdlTotalW_entladen, - "ReserveHours" => $hours + "ReserveHoursLaden" => $reserveH_laden, + "ReserveHoursEntladen" => $reserveH_entladen ])); - $oldHash = $this->GetBufferSafe("BatCacheHash"); - if (!$force && $oldHash === $hash) { + if (!$force && $this->GetBufferSafe("BatPlanHash") === $hash) { return; } @@ -406,17 +174,7 @@ class Bat_EV_SDL_V4 extends IPSModule $batteries = []; } - $cache = $this->CalculateBatteryWindows($batteries, $sdlTotalW_laden, $sdlTotalW_entladen, $hours); - - $this->SetBuffer("BatCacheHash", $hash); - $this->SetBuffer("BatCacheJSON", json_encode($cache)); - $this->SendDebug("Cache", "Battery cache rebuilt (" . count($cache["bats"]) . " bats)", 0); - } - - private function CalculateBatteryWindows(array $batteries, float $sdlTotalW_laden, float $sdlTotalW_entladen, float $hours): array - { $sumBatPowerW = 0.0; - foreach ($batteries as $b) { $p = (float)($b["powerbat"] ?? 0); if ($p > 0) { @@ -424,124 +182,263 @@ class Bat_EV_SDL_V4 extends IPSModule } } - $cache = [ + $plan = [ "inputs" => [ - "SDL_Leistung_W_laden" => $sdlTotalW_laden, + "SDL_Leistung_W_laden" => $sdlTotalW_laden, "SDL_Leistung_W_entladen" => $sdlTotalW_entladen, - "SumBatPower_W" => round($sumBatPowerW, 0), - "ReserveHours" => $hours + "ReserveHoursLaden" => $reserveH_laden, + "ReserveHoursEntladen" => $reserveH_entladen, + "SumBatPower_W" => round($sumBatPowerW, 0) ], - "bats" => [] + "bats" => [], + "total" => [ + "SDL_kWh_total" => 0.0, + "EV_kWh_total" => 0.0, + "totalCap_kWh" => 0.0, + "SDL_Charge_kW" => 0.0, + "SDL_Discharge_kW" => 0.0, + "EV_Charge_kW" => 0.0, + "EV_Discharge_kW" => 0.0 + ] ]; if ($sumBatPowerW <= 0.0) { - return $cache; + $this->SetBuffer("BatPlanHash", $hash); + $this->SetBuffer("BatPlanJSON", json_encode($plan)); + $this->SendDebug("Plan", "sumBatPowerW=0 -> empty plan", 0); + return; } - $sumBatPowerkW = $sumBatPowerW / 1000.0; - $sdlTotalkW_laden = $sdlTotalW_laden / 1000.0; - $sdlTotalkW_entladen = $sdlTotalW_entladen / 1000.0; + $sumBatPowerKW = $sumBatPowerW / 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; + $pBatKW = $pBatW / 1000.0; $capKWh = max(0.0, (float)($b["capazity"] ?? 0)); - if ($capKWh <= 0.0) { + if ($capKWh <= 0.0 || $pBatKW <= 0.0) { continue; } - $socVarId = (int)($b["soc"] ?? 0); $typ = (string)($b["typ"] ?? ("Bat " . ($idx + 1))); + $socVarId = (int)($b["soc"] ?? 0); - $sdlShareKW_laden = ($sumBatPowerkW > 0.0) - ? ($sdlTotalkW_laden / $sumBatPowerkW * $pBatkW) - : 0.0; + $sdlShareKW_laden = ($sumBatPowerKW > 0.0) ? ($sdlTotalKW_laden / $sumBatPowerKW * $pBatKW) : 0.0; + $sdlShareKW_entladen = ($sumBatPowerKW > 0.0) ? ($sdlTotalKW_entladen / $sumBatPowerKW * $pBatKW) : 0.0; - $sdlShareKW_entladen = ($sumBatPowerkW > 0.0) - ? ($sdlTotalkW_entladen / $sumBatPowerkW * $pBatkW) - : 0.0; + $evShareKW_laden = max(0.0, $pBatKW - $sdlShareKW_laden); + $evShareKW_entladen = max(0.0, $pBatKW - $sdlShareKW_entladen); - $evShareKW_laden = max(0.0, $pBatkW - $sdlShareKW_laden); - $evShareKW_entladen = max(0.0, $pBatkW - $sdlShareKW_entladen); + // Grenzen: individuell nach Reservezeit. + $underKWh = $sdlShareKW_entladen * $reserveH_entladen; + $upKWh = $capKWh - ($sdlShareKW_laden * $reserveH_laden); - // Neue Reservezeit statt fixer 0.5h - $underKWh = $sdlShareKW_entladen * $hours; - $upperReserveKWh = $sdlShareKW_laden * $hours; - $upKWh = $capKWh - $upperReserveKWh; - - // Sicherheitsgrenzen $underKWh = max(0.0, min($capKWh, $underKWh)); $upKWh = max(0.0, min($capKWh, $upKWh)); - // Falls Reserve zu groß ist, EV-Fenster nicht negativ werden lassen - if ($underKWh > $upKWh) { + // Falls Reserve zu groß ist, EV-Fenster sauber auf 0 setzen. + if ($upKWh < $underKWh) { $mid = $capKWh / 2.0; $underKWh = $mid; $upKWh = $mid; } - $SDL_kWh = $underKWh + ($capKWh - $upKWh); - $EV_kWh = max(0.0, $capKWh - $SDL_kWh); + $sdlLowerKWh = $underKWh; + $sdlUpperKWh = $capKWh - $upKWh; + $SDL_kWh = max(0.0, $sdlLowerKWh + $sdlUpperKWh); + $EV_kWh = max(0.0, $capKWh - $SDL_kWh); - $cache["bats"][] = [ + $realSocPct = $this->ReadSocPercent($socVarId); + $realKWh = $capKWh * $realSocPct / 100.0; + + $bat = [ "idx" => $idx, "typ" => $typ, "socVarId" => $socVarId, "capKWh" => $capKWh, "pBatW" => $pBatW, + "underKWh" => $underKWh, + "upKWh" => $upKWh, + "SDL_kWh_total" => $SDL_kWh, + "EV_kWh_total" => $EV_kWh, + "sdlShareKW_laden" => $sdlShareKW_laden, "sdlShareKW_entladen" => $sdlShareKW_entladen, "evShareKW_laden" => $evShareKW_laden, "evShareKW_entladen" => $evShareKW_entladen, - "underKWh" => $underKWh, - "upKWh" => $upKWh, - "upperReserveKWh" => $upperReserveKWh, + "real_SOC_pct" => round($realSocPct, 3), + "real_kWh" => round($realKWh, 3), - "SDL_kWh_total" => $SDL_kWh, - "EV_kWh_total" => $EV_kWh + "SDL_Charge_kW" => $sdlShareKW_laden, + "SDL_Discharge_kW" => $sdlShareKW_entladen, + "EV_Charge_kW" => $evShareKW_laden, + "EV_Discharge_kW" => $evShareKW_entladen, + + // Start-SoC für Sortier-/Verteillogik. Laufend wird später virtuell überschrieben. + "SDL_SOC" => 0.0, + "EV_SOC" => 0.0 ]; + + $plan["bats"][] = $bat; + + $plan["total"]["SDL_kWh_total"] += $SDL_kWh; + $plan["total"]["EV_kWh_total"] += $EV_kWh; + $plan["total"]["totalCap_kWh"] += $capKWh; + $plan["total"]["SDL_Charge_kW"] += $sdlShareKW_laden; + $plan["total"]["SDL_Discharge_kW"] += $sdlShareKW_entladen; + $plan["total"]["EV_Charge_kW"] += $evShareKW_laden; + $plan["total"]["EV_Discharge_kW"] += $evShareKW_entladen; } - return $cache; + $this->SetBuffer("BatPlanHash", $hash); + $this->SetBuffer("BatPlanJSON", json_encode($plan)); + $this->SetBuffer("Int_CFG_HASH", $hash); + + $this->SetIdentValue("SDL_Start_Pos", round((float)$this->ReadPropertyFloat("SDL_Start_Pos_Config"), 3)); + $this->SetIdentValue("EV_Start_Pos", round((float)$this->ReadPropertyFloat("EV_Start_Pos_Config"), 3)); + + $this->SendDebug("Plan", "Battery plan rebuilt (" . count($plan["bats"]) . " bats)", 0); } - public function ApplySetpoints(): void + private function InitVirtualAccountsIfNeeded(): void { + if ($this->GetBufferSafe("Int_Init") === "1") { + return; + } + $this->ResetVirtualAccounts(); + } + + public function ResetVirtualAccounts(): void + { + $this->BuildBatteryPlan(false); + + $plan = json_decode($this->GetBufferSafe("BatPlanJSON"), true); + if (!is_array($plan)) { + return; + } + + $sdlTotal = (float)($plan["total"]["SDL_kWh_total"] ?? 0.0); + $evTotal = (float)($plan["total"]["EV_kWh_total"] ?? 0.0); + + $sdlStartPct = max(0.0, min(100.0, (float)$this->ReadPropertyFloat("SDL_Start_Pos_Config"))); + $evStartPct = max(0.0, min(100.0, (float)$this->ReadPropertyFloat("EV_Start_Pos_Config"))); + + $this->SetBuffer("Int_E_SDL_kWh", (string)($sdlTotal * $sdlStartPct / 100.0)); + $this->SetBuffer("Int_E_EV_kWh", (string)($evTotal * $evStartPct / 100.0)); + $this->SetBuffer("Int_LastTs", (string)microtime(true)); + $this->SetBuffer("Int_Init", "1"); + + $this->SetIdentValue("SDL_Start_Pos", round($sdlStartPct, 3)); + $this->SetIdentValue("EV_Start_Pos", round($evStartPct, 3)); + + $this->SendDebug("Virtual_Reset", "SDL=" . round($sdlStartPct, 3) . "%, EV=" . round($evStartPct, 3) . "%", 0); + } + + private function IntegrateVirtualAccounts(array $plan): array + { + $now = microtime(true); + $lastTs = (float)$this->GetBufferSafe("Int_LastTs"); + if ($lastTs <= 0.0) { + $lastTs = $now; + } + + $dtSec = $now - $lastTs; + if ($dtSec < 0.0) { + $dtSec = 0.0; + } + if ($dtSec > 10.0) { + $dtSec = 10.0; + } + $dtH = $dtSec / 3600.0; + + $eSDL = (float)$this->GetBufferSafe("Int_E_SDL_kWh"); + $eEV = (float)$this->GetBufferSafe("Int_E_EV_kWh"); + + $pSDL = (float)GetValue($this->GetIDForIdent("Aktuelle_Leistung_SDL")); + $pEV = (float)GetValue($this->GetIDForIdent("Aktuelle_Leistung_EV")); + + $sdlTotal = (float)($plan["total"]["SDL_kWh_total"] ?? 0.0); + $evTotal = (float)($plan["total"]["EV_kWh_total"] ?? 0.0); + + $maxSDLChargeW = (float)($plan["total"]["SDL_Charge_kW"] ?? 0.0) * 1000.0; + $maxSDLDisW = (float)($plan["total"]["SDL_Discharge_kW"] ?? 0.0) * 1000.0; + $maxEVChargeW = (float)($plan["total"]["EV_Charge_kW"] ?? 0.0) * 1000.0; + $maxEVDisW = (float)($plan["total"]["EV_Discharge_kW"] ?? 0.0) * 1000.0; + + $pSDL = max(-$maxSDLDisW, min($maxSDLChargeW, $pSDL)); + $pEV = max(-$maxEVDisW, min($maxEVChargeW, $pEV)); + + if (abs($pSDL) > 1.0) { + $eSDL += ($pSDL / 1000.0) * $dtH; + } + if (abs($pEV) > 1.0) { + $eEV += ($pEV / 1000.0) * $dtH; + } + + $eSDL = ($sdlTotal > 0.0) ? max(0.0, min($sdlTotal, $eSDL)) : 0.0; + $eEV = ($evTotal > 0.0) ? max(0.0, min($evTotal, $eEV)) : 0.0; + + $this->SetBuffer("Int_E_SDL_kWh", (string)$eSDL); + $this->SetBuffer("Int_E_EV_kWh", (string)$eEV); + $this->SetBuffer("Int_LastTs", (string)$now); + + return [ + "SDL_kWh" => round($eSDL, 6), + "EV_kWh" => round($eEV, 6), + "SDL_pct" => ($sdlTotal > 0.0) ? max(0.0, min(100.0, $eSDL / $sdlTotal * 100.0)) : 0.0, + "EV_pct" => ($evTotal > 0.0) ? max(0.0, min(100.0, $eEV / $evTotal * 100.0)) : 0.0, + "SDL_Ist_W_used" => round($pSDL, 0), + "EV_Ist_W_used" => round($pEV, 0), + "dtSec" => round($dtSec, 3) + ]; + } + + public function ApplySetpoints(?array $plan = null): void + { + if ($plan === null) { + $plan = json_decode($this->GetBufferSafe("BatPlanJSON"), true); + } + if (!is_array($plan)) { + return; + } + $pEvW = (float)GetValue($this->GetIDForIdent("Nennleistung_Soll_EV")); $pSdlW = (float)GetValue($this->GetIDForIdent("Nennleistung_Soll_SDL")); - $distribution = $this->CalculateBatteryDistribution($pEvW, $pSdlW); + $distribution = $this->CalculateBatteryDistribution($pEvW, $pSdlW, $plan); $this->WriteBatteryPowerSetpoints($distribution); + $this->UpdateActualPowerSplit($pEvW, $pSdlW); } - private function CalculateBatteryDistribution(float $pEvW, float $pSdlW): array + private function CalculateBatteryDistribution(float $pEvW, float $pSdlW, array $plan): array { - $calcJsonId = $this->GetIDForIdent("CalcJSON"); - if (!IPS_VariableExists($calcJsonId)) { + $batteries = $plan["bats"] ?? []; + if (empty($batteries)) { return []; } - $rawJson = (string)GetValue($calcJsonId); - if ($rawJson === "") { - return []; - } + $virtSDL = (float)$this->GetBufferSafe("Int_E_SDL_kWh"); + $virtEV = (float)$this->GetBufferSafe("Int_E_EV_kWh"); - $calc = json_decode($rawJson, true); - if (!is_array($calc)) { - return []; - } + $sdlTotal = (float)($plan["total"]["SDL_kWh_total"] ?? 0.0); + $evTotal = (float)($plan["total"]["EV_kWh_total"] ?? 0.0); - $batteries = $calc["batteries"] ?? []; - if (!is_array($batteries) || empty($batteries)) { - return []; - } + $globalSDLpct = ($sdlTotal > 0.0) ? max(0.0, min(100.0, $virtSDL / $sdlTotal * 100.0)) : 0.0; + $globalEVpct = ($evTotal > 0.0) ? max(0.0, min(100.0, $virtEV / $evTotal * 100.0)) : 0.0; - $distributePower = function(float $targetPower, string $mode) use ($batteries): array { + foreach ($batteries as &$bat) { + // Mit globalem virtuellem SoC sortieren. Bei Bedarf später pro Batterie virtualisieren. + $bat["SDL_SOC"] = $globalSDLpct; + $bat["EV_SOC"] = $globalEVpct; + } + unset($bat); + + $distributePower = function (float $targetPower, string $mode) use ($batteries): array { $result = []; - foreach ($batteries as $bat) { $result[$bat["idx"]] = 0.0; } @@ -550,7 +447,7 @@ class Bat_EV_SDL_V4 extends IPSModule return $result; } - $isCharge = ($targetPower > 0); + $isCharge = ($targetPower > 0.0); $absPower = abs($targetPower); $socKey = ($mode === "EV") ? "EV_SOC" : "SDL_SOC"; @@ -559,12 +456,7 @@ class Bat_EV_SDL_V4 extends IPSModule $groups = []; foreach ($batteries as $bat) { $soc = (int)round((float)($bat[$socKey] ?? 0.0)); - $maxW = ((float)($bat[$limitKey] ?? 0.0)) * 1000.0; - - if ($maxW <= 0.0) { - continue; - } - + $maxW = max(0.0, (float)($bat[$limitKey] ?? 0.0) * 1000.0); $groups[$soc][] = [ "idx" => $bat["idx"], "maxW" => $maxW @@ -577,30 +469,29 @@ class Bat_EV_SDL_V4 extends IPSModule krsort($groups); } - $remainingNeeded = $absPower; - + $remaining = $absPower; foreach ($groups as $groupBatteries) { - if ($remainingNeeded <= 0.01) { + if ($remaining <= 0.01) { break; } - $groupTotalCapacity = 0.0; + $groupTotal = 0.0; foreach ($groupBatteries as $gb) { - $groupTotalCapacity += $gb["maxW"]; + $groupTotal += $gb["maxW"]; } - if ($groupTotalCapacity <= 0.0) { + if ($groupTotal <= 0.0) { continue; } - $powerForThisGroup = min($remainingNeeded, $groupTotalCapacity); - $ratio = $powerForThisGroup / $groupTotalCapacity; + $powerForGroup = min($remaining, $groupTotal); + $ratio = $powerForGroup / $groupTotal; foreach ($groupBatteries as $gb) { $result[$gb["idx"]] = $gb["maxW"] * $ratio; } - $remainingNeeded -= $powerForThisGroup; + $remaining -= $powerForGroup; } if (!$isCharge) { @@ -616,37 +507,22 @@ class Bat_EV_SDL_V4 extends IPSModule $sdlDistribution = $distributePower($pSdlW, "SDL"); $finalOutput = []; - foreach ($batteries as $bat) { $idx = $bat["idx"]; - - $valEv = $evDistribution[$idx] ?? 0.0; - $valSdl = $sdlDistribution[$idx] ?? 0.0; - $totalW = $valEv + $valSdl; - - $chargeW = 0.0; - $dischargeW = 0.0; - - if ($totalW > 0.0) { - $chargeW = abs($totalW); - } elseif ($totalW < 0.0) { - $dischargeW = abs($totalW); - } + $totalW = ($evDistribution[$idx] ?? 0.0) + ($sdlDistribution[$idx] ?? 0.0); $finalOutput[] = [ "idx" => $idx, - "typ" => (string)($bat["typ"] ?? ""), - "chargeW" => round($chargeW, 0), - "dischargeW" => round($dischargeW, 0) + "typ" => (string)$bat["typ"], + "chargeW" => ($totalW > 0.0) ? round(abs($totalW), 0) : 0, + "dischargeW" => ($totalW < 0.0) ? round(abs($totalW), 0) : 0 ]; } - $this->UpdateCurrentPowerChannels($pEvW, $pSdlW); - return $finalOutput; } - private function UpdateCurrentPowerChannels(float $pEvW, float $pSdlW): void + private function UpdateActualPowerSplit(float $pEvW, float $pSdlW): void { $totalPowerIst = $this->GetTotalBatteryPowerIstW(); @@ -655,39 +531,28 @@ class Bat_EV_SDL_V4 extends IPSModule if (abs($sumSoll) > $eps) { $factor = $totalPowerIst / $sumSoll; - $aktEV = $pEvW * $factor; - $aktSDL = $pSdlW * $factor; + $rawEV = $pEvW * $factor; + $rawSDL = $pSdlW * $factor; } else { if (abs($totalPowerIst) < 50.0) { - $aktEV = $pEvW; - $aktSDL = $pSdlW; + $rawEV = $pEvW; + $rawSDL = $pSdlW; } else { - $aktEV = $pEvW; - $aktSDL = $totalPowerIst - $aktEV; + $rawEV = $pEvW; + $rawSDL = $totalPowerIst - $rawEV; } } - $rawEV = (float)$aktEV; - $rawSDL = (float)$aktSDL; - - $targetEV = (float)$pEvW; - $targetSDL = (float)$pSdlW; - - $tolPct = 0.10; - $needHits = 1; - - $tolW_EV = max(50.0, abs($targetEV) * $tolPct); - $tolW_SDL = max(50.0, abs($targetSDL) * $tolPct); - $filterAktiv = $this->ReadPropertyBoolean("FilterAktiv"); - if ($filterAktiv) { - $fEV = $this->FilterCurrent("EV", $rawEV, $targetEV, $tolW_EV, $needHits); - $fSDL = $this->FilterCurrent("SDL", $rawSDL, $targetSDL, $tolW_SDL, $needHits); + $tolPct = 0.10; + $needHits = 1; + $fEV = $this->FilterCurrent("EV", $rawEV, $pEvW, abs($pEvW) * $tolPct, $needHits); + $fSDL = $this->FilterCurrent("SDL", $rawSDL, $pSdlW, abs($pSdlW) * $tolPct, $needHits); } else { $fEV = $rawEV; $fSDL = $rawSDL; - $this->ResetCurrentFilterBuffers(); + $this->ClearCurrentFilterBuffers(); } $this->SetIdentValue("Aktuelle_Leistung_EV", (float)$fEV); @@ -726,31 +591,38 @@ class Bat_EV_SDL_V4 extends IPSModule } if (($pending > 0.0 && $raw < 0.0) || ($pending < 0.0 && $raw > 0.0)) { - $hits = 0; - $this->SetBuffer("CUR_{$ch}_HITS", (string)$hits); + $this->SetBuffer("CUR_{$ch}_HITS", "0"); return $lastVal; } + $tolW = max(50.0, $tolW); if (abs($raw - $pending) <= $tolW) { $hits++; - $this->SetBuffer("CUR_{$ch}_HITS", (string)$hits); - if ($hits >= $needHits) { $lastVal = $raw; $hits = 0; $this->SetBuffer("CUR_{$ch}_VAL", (string)$lastVal); - $this->SetBuffer("CUR_{$ch}_HITS", (string)$hits); } + $this->SetBuffer("CUR_{$ch}_HITS", (string)$hits); } else { if ($hits !== 0) { - $hits = 0; - $this->SetBuffer("CUR_{$ch}_HITS", (string)$hits); + $this->SetBuffer("CUR_{$ch}_HITS", "0"); } } return $lastVal; } + private function ClearCurrentFilterBuffers(): void + { + foreach (["EV", "SDL"] as $ch) { + $this->SetBuffer("CUR_{$ch}_INIT", ""); + $this->SetBuffer("CUR_{$ch}_VAL", ""); + $this->SetBuffer("CUR_{$ch}_PEND", ""); + $this->SetBuffer("CUR_{$ch}_HITS", ""); + } + } + private function WriteBatteryPowerSetpoints(array $distribution): void { IPS_LogMessage(__FUNCTION__, "distribution=" . json_encode($distribution, JSON_PRETTY_PRINT)); @@ -773,13 +645,13 @@ class Bat_EV_SDL_V4 extends IPSModule $dischargeW = max(0.0, (float)($d["dischargeW"] ?? 0.0)); if ($chargeW > 0.0 && $dischargeW > 0.0) { - $this->SendDebug("WriteBatteryPowerSetpoints", "WARN: both >0 for $typ (idx=$idx) -> set 0", 0); + $this->SendDebug("WriteBatteryPowerSetpoints", "WARN both >0 for $typ idx=$idx", 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); + $this->SendDebug("Setpoints", "$typ idx=$idx charge={$chargeW}W discharge={$dischargeW}W", 0); } } @@ -788,16 +660,16 @@ class Bat_EV_SDL_V4 extends IPSModule $t = mb_strtolower($typ); $varPowerCharge = (int)($cfg["powerbat_laden"] ?? 0); - $varPowerDisch = (int)($cfg["powerbat_entladen"] ?? 0); + $varPowerDisch = (int)($cfg["powerbat_entladen"] ?? 0); $varMode = (int)($cfg["register_ladenentladen_modus"] ?? 0); - $setInt = function(int $varId, int $value): void { + $setInt = function (int $varId, int $value): void { if ($varId > 0 && IPS_VariableExists($varId)) { RequestAction($varId, $value); } }; - $setW = function(int $varId, float $w): void { + $setW = function (int $varId, float $w): void { if ($varId > 0 && IPS_VariableExists($varId)) { RequestAction($varId, (int)round(max(0.0, $w), 0)); } @@ -816,13 +688,11 @@ class Bat_EV_SDL_V4 extends IPSModule if ($chargeW > 0.0) { $setInt($varMode, $modeCharge); - if (strpos($t, "goodwe") !== false) { $setW($varPowerCharge, $chargeW); $setW($varPowerDisch, 0.0); return; } - $setW($varPowerCharge, $chargeW); $setW($varPowerDisch, 0.0); return; @@ -830,13 +700,11 @@ class Bat_EV_SDL_V4 extends IPSModule if ($dischargeW > 0.0) { $setInt($varMode, $modeDisch); - if (strpos($t, "goodwe") !== false) { $setW($varPowerCharge, $dischargeW); $setW($varPowerDisch, 0.0); return; } - $setW($varPowerDisch, $dischargeW); $setW($varPowerCharge, 0.0); return; @@ -846,66 +714,6 @@ class Bat_EV_SDL_V4 extends IPSModule $setW($varPowerDisch, 0.0); } - private function ResetVirtualAccountsToStartPoint(bool $forceCacheRebuild): void - { - if ($forceCacheRebuild) { - $this->BuildBatteryCache(true); - } - - $cache = json_decode($this->GetBufferSafe("BatCacheJSON"), true); - if (!is_array($cache) || empty($cache["bats"])) { - $this->SendDebug("Reset", "Cache leer/invalid", 0); - return; - } - - $sdlKWhTotal = 0.0; - $evKWhTotal = 0.0; - $sdlStartKWh = 0.0; - $evStartKWh = 0.0; - - foreach ($cache["bats"] as $bat) { - $sdlTotal = (float)($bat["SDL_kWh_total"] ?? 0.0); - $evTotal = (float)($bat["EV_kWh_total"] ?? 0.0); - $under = (float)($bat["underKWh"] ?? 0.0); - - $sdlKWhTotal += $sdlTotal; - $evKWhTotal += $evTotal; - - // SDL Start = untere Reserve, wie vorher - $sdlStartKWh += $under; - - // EV Start = tatsächlicher SoC innerhalb EV-Fenster beim Reset - $socPct = $this->ReadSocPercent((int)($bat["socVarId"] ?? 0)); - $capKWh = (float)($bat["capKWh"] ?? 0.0); - $realKWh = $capKWh * $socPct / 100.0; - $up = (float)($bat["upKWh"] ?? 0.0); - $evWindow = max(0.0, $up - $under); - $evPart = max(0.0, min($evWindow, $realKWh - $under)); - $evStartKWh += $evPart; - } - - $sdlStartKWh = ($sdlKWhTotal > 0.0) ? max(0.0, min($sdlKWhTotal, $sdlStartKWh)) : 0.0; - $evStartKWh = ($evKWhTotal > 0.0) ? max(0.0, min($evKWhTotal, $evStartKWh)) : 0.0; - - $this->SetBuffer("Int_E_SDL_kWh", (string)$sdlStartKWh); - $this->SetBuffer("Int_E_EV_kWh", (string)$evStartKWh); - $this->SetBuffer("Int_Init", "1"); - $this->SetBuffer("Int_LastTs", (string)microtime(true)); - - $sdlStartPct = ($sdlKWhTotal > 0.0) ? $sdlStartKWh / $sdlKWhTotal * 100.0 : 0.0; - $evStartPct = ($evKWhTotal > 0.0) ? $evStartKWh / $evKWhTotal * 100.0 : 0.0; - - $this->SetIdentValue("SDL_Start_Pos", round(max(0.0, min(100.0, $sdlStartPct)), 3)); - $this->SetIdentValue("EV_Start_Pos", round(max(0.0, min(100.0, $evStartPct)), 3)); - - $cacheHash = (string)$this->GetBufferSafe("BatCacheHash"); - if ($cacheHash !== "") { - $this->SetBuffer("Int_CFG_HASH", $cacheHash); - } - - $this->SendDebug("Reset", "SDL_StartKWh=" . round($sdlStartKWh, 3) . ", EV_StartKWh=" . round($evStartKWh, 3), 0); - } - private function GetTotalBatteryPowerIstW(): float { $cfg = json_decode($this->ReadPropertyString("Batteries"), true); @@ -920,27 +728,25 @@ class Bat_EV_SDL_V4 extends IPSModule $sum += (-1.0) * (float)GetValue($varId); } } - return $sum; } - private function GetCacheTotalKWh(array $cache, string $key): float + private function ReadSocPercent(int $varId): float { - $sum = 0.0; - foreach (($cache["bats"] ?? []) as $bat) { - $sum += (float)($bat[$key] ?? 0.0); + if ($varId >= 0 && $varId <= 100 && !IPS_VariableExists($varId)) { + return (float)$varId; } - return $sum; - } - private function ResetCurrentFilterBuffers(): void - { - foreach (["EV", "SDL"] as $ch) { - $this->SetBuffer("CUR_{$ch}_INIT", ""); - $this->SetBuffer("CUR_{$ch}_VAL", ""); - $this->SetBuffer("CUR_{$ch}_PEND", ""); - $this->SetBuffer("CUR_{$ch}_HITS", ""); + 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 GetBufferSafe(string $name): string @@ -969,27 +775,6 @@ class Bat_EV_SDL_V4 extends IPSModule } SetValue($id, $value); } - - 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; - } - - $soc = (float)$v; - return max(0.0, min(100.0, $soc)); - } } ?> - -