From 8772f14e8cf068c4f689812f0c074cc7afe115bd Mon Sep 17 00:00:00 2001 From: "belevo\\mh" Date: Mon, 4 May 2026 18:36:01 +0200 Subject: [PATCH] no message --- Bat_EV_SDL_V4/module.php | 931 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 930 insertions(+), 1 deletion(-) diff --git a/Bat_EV_SDL_V4/module.php b/Bat_EV_SDL_V4/module.php index ad3550e..9d45ef1 100644 --- a/Bat_EV_SDL_V4/module.php +++ b/Bat_EV_SDL_V4/module.php @@ -63,4 +63,933 @@ class Bat_EV_SDL_V4 extends IPSModule $this->Update(); } -?> \ No newline at end of file + public function RequestAction($Ident, $Value) + { + if ($Ident === "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; + } + + throw new Exception("Invalid Ident: " . $Ident); + } + + public function Update() + { + $semKey = 'BatEVSDL_Update_' . $this->InstanceID; + + if (!IPS_SemaphoreEnter($semKey, 5000)) { + $this->SendDebug("Update", "SKIP (Semaphore locked)", 0); + return; + } + + try { + if (!GetValue($this->GetIDForIdent("State"))) { + 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"); + return; + } + + $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_kWh = (float)$this->GetBufferSafe("Int_E_SDL_kWh"); + $Eev_kWh = (float)$this->GetBufferSafe("Int_E_EV_kWh"); + + // 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"); + } + + $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) + ]; + + $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()); + } finally { + IPS_SemaphoreLeave($semKey); + } + } + + private function BuildBatteryCache(bool $force): void + { + $batteriesRaw = $this->ReadPropertyString("Batteries"); + $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")); + + $hash = md5(json_encode([ + "Batteries" => $batteriesRaw, + "SDL_W_Laden" => $sdlTotalW_laden, + "SDL_W_Entladen" => $sdlTotalW_entladen, + "ReserveHours" => $hours + ])); + + $oldHash = $this->GetBufferSafe("BatCacheHash"); + if (!$force && $oldHash === $hash) { + return; + } + + $batteries = json_decode($batteriesRaw, true); + if (!is_array($batteries)) { + $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) { + $sumBatPowerW += $p; + } + } + + $cache = [ + "inputs" => [ + "SDL_Leistung_W_laden" => $sdlTotalW_laden, + "SDL_Leistung_W_entladen" => $sdlTotalW_entladen, + "SumBatPower_W" => round($sumBatPowerW, 0), + "ReserveHours" => $hours + ], + "bats" => [] + ]; + + if ($sumBatPowerW <= 0.0) { + return $cache; + } + + $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; + $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))); + + $sdlShareKW_laden = ($sumBatPowerkW > 0.0) + ? ($sdlTotalkW_laden / $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); + + // 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) { + $mid = $capKWh / 2.0; + $underKWh = $mid; + $upKWh = $mid; + } + + $SDL_kWh = $underKWh + ($capKWh - $upKWh); + $EV_kWh = max(0.0, $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, + "upperReserveKWh" => $upperReserveKWh, + + "SDL_kWh_total" => $SDL_kWh, + "EV_kWh_total" => $EV_kWh + ]; + } + + return $cache; + } + + public function ApplySetpoints(): void + { + $pEvW = (float)GetValue($this->GetIDForIdent("Nennleistung_Soll_EV")); + $pSdlW = (float)GetValue($this->GetIDForIdent("Nennleistung_Soll_SDL")); + + $distribution = $this->CalculateBatteryDistribution($pEvW, $pSdlW); + $this->WriteBatteryPowerSetpoints($distribution); + } + + private function CalculateBatteryDistribution(float $pEvW, float $pSdlW): array + { + $calcJsonId = $this->GetIDForIdent("CalcJSON"); + if (!IPS_VariableExists($calcJsonId)) { + return []; + } + + $rawJson = (string)GetValue($calcJsonId); + if ($rawJson === "") { + return []; + } + + $calc = json_decode($rawJson, true); + if (!is_array($calc)) { + return []; + } + + $batteries = $calc["batteries"] ?? []; + if (!is_array($batteries) || empty($batteries)) { + return []; + } + + $distributePower = function(float $targetPower, string $mode) use ($batteries): array { + $result = []; + + foreach ($batteries as $bat) { + $result[$bat["idx"]] = 0.0; + } + + if (abs($targetPower) < 0.01) { + return $result; + } + + $isCharge = ($targetPower > 0); + $absPower = abs($targetPower); + + $socKey = ($mode === "EV") ? "EV_SOC" : "SDL_SOC"; + $limitKey = $isCharge ? $mode . "_Charge_kW" : $mode . "_Discharge_kW"; + + $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; + } + + $groups[$soc][] = [ + "idx" => $bat["idx"], + "maxW" => $maxW + ]; + } + + if ($isCharge) { + ksort($groups); + } else { + krsort($groups); + } + + $remainingNeeded = $absPower; + + foreach ($groups as $groupBatteries) { + if ($remainingNeeded <= 0.01) { + break; + } + + $groupTotalCapacity = 0.0; + foreach ($groupBatteries as $gb) { + $groupTotalCapacity += $gb["maxW"]; + } + + if ($groupTotalCapacity <= 0.0) { + continue; + } + + $powerForThisGroup = min($remainingNeeded, $groupTotalCapacity); + $ratio = $powerForThisGroup / $groupTotalCapacity; + + foreach ($groupBatteries as $gb) { + $result[$gb["idx"]] = $gb["maxW"] * $ratio; + } + + $remainingNeeded -= $powerForThisGroup; + } + + if (!$isCharge) { + foreach ($result as $idx => $val) { + $result[$idx] = -$val; + } + } + + return $result; + }; + + $evDistribution = $distributePower($pEvW, "EV"); + $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); + } + + $finalOutput[] = [ + "idx" => $idx, + "typ" => (string)($bat["typ"] ?? ""), + "chargeW" => round($chargeW, 0), + "dischargeW" => round($dischargeW, 0) + ]; + } + + $this->UpdateCurrentPowerChannels($pEvW, $pSdlW); + + return $finalOutput; + } + + private function UpdateCurrentPowerChannels(float $pEvW, float $pSdlW): void + { + $totalPowerIst = $this->GetTotalBatteryPowerIstW(); + + $eps = 0.01; + $sumSoll = $pEvW + $pSdlW; + + if (abs($sumSoll) > $eps) { + $factor = $totalPowerIst / $sumSoll; + $aktEV = $pEvW * $factor; + $aktSDL = $pSdlW * $factor; + } else { + if (abs($totalPowerIst) < 50.0) { + $aktEV = $pEvW; + $aktSDL = $pSdlW; + } else { + $aktEV = $pEvW; + $aktSDL = $totalPowerIst - $aktEV; + } + } + + $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); + } else { + $fEV = $rawEV; + $fSDL = $rawSDL; + $this->ResetCurrentFilterBuffers(); + } + + $this->SetIdentValue("Aktuelle_Leistung_EV", (float)$fEV); + $this->SetIdentValue("Aktuelle_Leistung_SDL", (float)$fSDL); + } + + private function FilterCurrent(string $ch, float $raw, float $target, float $tolW, int $needHits): float + { + $lastVal = (float)$this->GetBufferSafe("CUR_{$ch}_VAL"); + $pending = (float)$this->GetBufferSafe("CUR_{$ch}_PEND"); + $hits = (int)$this->GetBufferSafe("CUR_{$ch}_HITS"); + + if ($this->GetBufferSafe("CUR_{$ch}_INIT") !== "1") { + $lastVal = $raw; + $pending = $target; + $hits = 0; + $this->SetBuffer("CUR_{$ch}_INIT", "1"); + $this->SetBuffer("CUR_{$ch}_VAL", (string)$lastVal); + $this->SetBuffer("CUR_{$ch}_PEND", (string)$pending); + $this->SetBuffer("CUR_{$ch}_HITS", (string)$hits); + } + + if (abs($target - $pending) > 0.5) { + $pending = $target; + $hits = 0; + $this->SetBuffer("CUR_{$ch}_PEND", (string)$pending); + $this->SetBuffer("CUR_{$ch}_HITS", (string)$hits); + } + + if (abs($pending) < 0.5) { + $lastVal = 0.0; + $hits = 0; + $this->SetBuffer("CUR_{$ch}_VAL", (string)$lastVal); + $this->SetBuffer("CUR_{$ch}_HITS", (string)$hits); + return $lastVal; + } + + if (($pending > 0.0 && $raw < 0.0) || ($pending < 0.0 && $raw > 0.0)) { + $hits = 0; + $this->SetBuffer("CUR_{$ch}_HITS", (string)$hits); + return $lastVal; + } + + 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); + } + } else { + if ($hits !== 0) { + $hits = 0; + $this->SetBuffer("CUR_{$ch}_HITS", (string)$hits); + } + } + + return $lastVal; + } + + 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)))); + + $chargeW = max(0.0, (float)($d["chargeW"] ?? 0.0)); + $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); + $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); + + $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.0); + return; + } + + $setW($varPowerCharge, $chargeW); + $setW($varPowerDisch, 0.0); + return; + } + + 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; + } + + $setW($varPowerCharge, 0.0); + $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); + 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 GetCacheTotalKWh(array $cache, string $key): float + { + $sum = 0.0; + foreach (($cache["bats"] ?? []) as $bat) { + $sum += (float)($bat[$key] ?? 0.0); + } + 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", ""); + } + } + + 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); + $this->SetIdentValue("P_EV_entladen", 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 + { + 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)); + } +} + +?> + +