diff --git a/Bat_EV_SDL/module.php b/Bat_EV_SDL/module.php index 8ec0080..70b5371 100644 --- a/Bat_EV_SDL/module.php +++ b/Bat_EV_SDL/module.php @@ -2,71 +2,63 @@ class Bat_EV_SDL extends IPSModule { - /* =========================== - * 1) Setup - * =========================== */ public function Create() { parent::Create(); - // ---- Properties (kommen aus form.json) ---- - $this->RegisterPropertyString("Batteries", "[]"); // List JSON - $this->RegisterPropertyInteger("SDL_Leistung", 0); // W - $this->RegisterPropertyInteger("UpdateInterval", 5); + // Properties (aus form.json) + $this->RegisterPropertyString("Batteries", "[]"); + $this->RegisterPropertyInteger("SDL_Leistung", 0); // W + $this->RegisterPropertyInteger("UpdateInterval", 5); // Minuten, 0 = aus - // ---- Status / Steuerung ---- + // Status $this->RegisterVariableBoolean("State", "Aktiv", "~Switch", 1); $this->EnableAction("State"); - // ---- Ergebnisse (Gesamt) ---- - $this->RegisterVariableFloat("kWh_SDL", "Energie SDL (kWh)", "", 20); - $this->RegisterVariableFloat("kWh_EV", "Energie EV (kWh)", "", 21); + // Ergebnisse + $this->RegisterVariableFloat("kWh_SDL", "SDL Reservebedarf 30min (kWh)", "", 20); + $this->RegisterVariableFloat("kWh_EV", "EV Energie im Fenster (kWh)", "", 21); - $this->RegisterVariableFloat("SoC_SDL", "SoC SDL (%)", "", 12); - $this->RegisterVariableFloat("SoC_EV", "SoC EV (%)", "", 13); + // SDL_OK Quote (wie viele Batterien schaffen beide Richtungen) + $this->RegisterVariableFloat("SoC_SDL", "SDL OK Quote (%)", "", 12); + // EV-Füllstand relativ zum EV-Fenster + $this->RegisterVariableFloat("SoC_EV", "EV Fenster-Füllstand (%)", "", 13); + + // Fensterposition: 0% unten (E=Emin), 50% Mitte, 100% oben (E=Emax) + $this->RegisterVariableFloat("SDL_Pos", "SDL Fensterposition (%)", "", 14); + + // Leistungsgrenzen (statisch) $this->RegisterVariableFloat("P_SDL_max", "SDL max (W)", "", 10); - $this->RegisterVariableFloat("P_EV_max", "EV max (W)", "", 11); + $this->RegisterVariableFloat("P_EV_max", "EV max (W)", "", 11); + // Leistungsgrenzen (zustandsabhängig: Fenster / Headroom / Energy) $this->RegisterVariableFloat("P_EV_laden", "P EV laden max (W)", "", 22); $this->RegisterVariableFloat("P_EV_entladen", "P EV entladen max (W)", "", 23); $this->RegisterVariableFloat("P_SDL_laden", "P SDL laden max (W)", "", 24); $this->RegisterVariableFloat("P_SDL_entladen", "P SDL entladen max (W)", "", 25); - // Optional: Voller JSON-Dump zum Debuggen/Weiterverarbeiten + // Debug JSON $this->RegisterVariableString("CalcJSON", "Berechnung (JSON)", "", 99); - $this->RegisterTimer("UpdateTimer",0,'GEF_Update($_IPS["TARGET"]);'); - - + // Timer (Prefix aus module.json: "GEF") + $this->RegisterTimer("UpdateTimer", 0, 'GEF_Update($_IPS["TARGET"]);'); } - /* =========================== - * 2) ApplyChanges (Konsole speichern) - * =========================== */ public function ApplyChanges() { parent::ApplyChanges(); $intervalMin = (int)$this->ReadPropertyInteger("UpdateInterval"); - if ($intervalMin > 0) { - // Minuten → Millisekunden $this->SetTimerInterval("UpdateTimer", $intervalMin * 60 * 1000); - $this->SendDebug("ApplyChanges", "Timer aktiv: {$intervalMin} min", 0); } else { - // Timer aus $this->SetTimerInterval("UpdateTimer", 0); - $this->SendDebug("ApplyChanges", "Timer deaktiviert", 0); } - // Direkt einmal rechnen $this->Update(); } - /* =========================== - * 3) RequestAction (WebFront Klicks) - * =========================== */ public function RequestAction($Ident, $Value) { switch ($Ident) { @@ -82,218 +74,231 @@ class Bat_EV_SDL extends IPSModule } } - /* =========================== - * 4) Hauptlogik - * =========================== */ + /** + * Fenster-Modell: + * Pro Batterie reservieren wir Emin=Ereq und Emax=Cap-Ereq, damit SDL 30 min laden UND entladen kann. + * EV darf nur zwischen Emin..Emax arbeiten. + */ public function Update() - { - if (!GetValue($this->GetIDForIdent("State"))) { - return; - } - - $batteries = json_decode($this->ReadPropertyString("Batteries"), true); - if (!is_array($batteries) || count($batteries) === 0) { - $this->SendDebug("Update", "Keine Batterien konfiguriert.", 0); - $this->WriteEmptyResults(); - return; - } - - $sdlPowerW = (int)$this->ReadPropertyInteger("SDL_Leistung"); - if ($sdlPowerW < 0) $sdlPowerW = 0; - - $reserveH = 0.5; // FIX: 30 Minuten - $eps = 0.0001; - - // 1) Summe Max-Leistung aller Batterien - $sumBatPowerW = 0; - foreach ($batteries as $b) { - $p = (int)($b["powerbat"] ?? 0); - if ($p > 0) $sumBatPowerW += $p; - } - - if ($sumBatPowerW <= 0) { - $this->SendDebug("Update", "Summe powerbat ist 0 – bitte Leistungen setzen.", 0); - $this->WriteEmptyResults(); - return; - } - - // SDL nicht größer als physikalisch möglich - if ($sdlPowerW > $sumBatPowerW) { - $this->SendDebug("Update", "SDL_Leistung ($sdlPowerW W) > SummeBatPower ($sumBatPowerW W) -> begrenze.", 0); - $sdlPowerW = $sumBatPowerW; - } - - // Summen (Leistung) - $P_SDL_max_W = 0.0; - $P_EV_max_W = 0.0; - - $P_SDL_laden_W = 0.0; - $P_SDL_entladen_W = 0.0; - $P_EV_laden_W = 0.0; - $P_EV_entladen_W = 0.0; - - // Summen (Energie/Fenster) - $sumSDL_req_kWh = 0.0; // Summe Ereq (Reservebedarf 30 min) - $sumEVcap_kWh = 0.0; // Summe EV-Fensterkapazität = Sum(Cap - 2*Ereq) - $sumEV_kWh = 0.0; // Summe EV-Energie im Fenster - $sumSDL_ok_count = 0; // wie viele Batterien erfüllen beide Richtungen - $sumBat_count = 0; - - $calc = []; - - foreach ($batteries as $idx => $b) { - - $typ = (string)($b["typ"] ?? ("Bat#" . ($idx + 1))); - $pBatW = (int)($b["powerbat"] ?? 0); - $capKWh = (float)($b["capazity"] ?? 0); - $socVar = (int)($b["soc"] ?? 0); - - if ($pBatW <= 0 || $capKWh <= 0) { - $calc[] = ["typ" => $typ, "skip" => "powerbat<=0 oder capacity<=0"]; - continue; - } - - $sumBat_count++; - - // SDL Anteil Leistung proportional - $pSDL_W_raw = ($sdlPowerW * $pBatW) / $sumBatPowerW; - $pSDL_W = min($pSDL_W_raw, (float)$pBatW); - if ($pSDL_W < 0) $pSDL_W = 0.0; - - // EV Anteil Leistung ist Rest - $pEV_W = max(0.0, $pBatW - $pSDL_W); - - $P_SDL_max_W += $pSDL_W; - $P_EV_max_W += $pEV_W; - - // Reservebedarf für 30 Minuten (kWh) - $Ereq_kWh = ($pSDL_W * $reserveH) / 1000.0; - $sumSDL_req_kWh += $Ereq_kWh; - - // Aktuelle Energie aus SoC - $socPct = $this->ReadSocPercent($socVar); - $E_kWh = ($socPct / 100.0) * $capKWh; - - // Reserviertes Fenster für SDL - $Emin = $Ereq_kWh; - $Emax = $capKWh - $Ereq_kWh; - - // EV-Fensterkapazität (kann 0 werden wenn Cap < 2*Ereq) - $EVcap = max(0.0, $Emax - $Emin); - $sumEVcap_kWh += $EVcap; - - // EV-Energie innerhalb Fenster: clamp(E - Emin, 0..EVcap) - $EV_kWh = 0.0; - if ($EVcap > $eps) { - $EV_kWh = min(max($E_kWh - $Emin, 0.0), $EVcap); - } - $sumEV_kWh += $EV_kWh; - - // SDL kann 30min ENTladen? - $canSDLdischarge = ($E_kWh + $eps) >= $Emin; - - // SDL kann 30min LADEN? - $canSDLcharge = (($capKWh - $E_kWh) + $eps) >= $Ereq_kWh; // equivalent E_kWh <= Emax - - // EV kann ENTladen? (nur wenn über Emin) - $canEVdischarge = ($E_kWh - $Emin) > $eps; - - // EV kann LADEN? (nur wenn unter Emax) - $canEVcharge = ($Emax - $E_kWh) > $eps; - - // Leistungsmaxima (dein Wunsch) - if ($canSDLdischarge) $P_SDL_entladen_W += $pSDL_W; - if ($canSDLcharge) $P_SDL_laden_W += $pSDL_W; - - if ($canEVdischarge) $P_EV_entladen_W += $pEV_W; - if ($canEVcharge) $P_EV_laden_W += $pEV_W; - - // SDL "OK" wenn beide Richtungen 30min gehen - $sdlOK = ($canSDLdischarge && $canSDLcharge); - if ($sdlOK) $sumSDL_ok_count++; - - // Debug / Detail - $calc[] = [ - "typ" => $typ, - "pBat_W" => $pBatW, - "cap_kWh" => round($capKWh, 3), - "soc_pct" => round($socPct, 2), - "E_kWh" => round($E_kWh, 3), - - "pSDL_W" => round($pSDL_W, 0), - "Ereq_kWh" => round($Ereq_kWh, 3), - "Emin_kWh" => round($Emin, 3), - "Emax_kWh" => round($Emax, 3), - - "pEV_W" => round($pEV_W, 0), - "EVcap_kWh" => round($EVcap, 3), - "EV_kWh" => round($EV_kWh, 3), - - "canSDLcharge_30min" => $canSDLcharge, - "canSDLdischarge_30min" => $canSDLdischarge, - "canEVcharge" => $canEVcharge, - "canEVdischarge" => $canEVdischarge, - "SDL_OK_both_directions" => $sdlOK - ]; - } - - // SoC_EV = EV-Energie / EV-Fensterkapazität - $socEV = ($sumEVcap_kWh > $eps) ? ($sumEV_kWh / $sumEVcap_kWh) * 100.0 : 0.0; - - // SoC_SDL als "OK-Quote" (wie viele Batterien schaffen beide Richtungen) - // Wenn du stattdessen "Leistungs-gewichtete" Quote willst, sag Bescheid. - $socSDL = ($sumBat_count > 0) ? ($sumSDL_ok_count / $sumBat_count) * 100.0 : 0.0; - - // Setzen der Ergebnisvariablen - SetValue($this->GetIDForIdent("kWh_SDL"), round($sumSDL_req_kWh, 3)); - SetValue($this->GetIDForIdent("kWh_EV"), round($sumEV_kWh, 3)); - - SetValue($this->GetIDForIdent("SoC_SDL"), round($socSDL, 2)); - SetValue($this->GetIDForIdent("SoC_EV"), round($socEV, 2)); - - SetValue($this->GetIDForIdent("P_SDL_max"), round($P_SDL_max_W, 0)); - SetValue($this->GetIDForIdent("P_EV_max"), round($P_EV_max_W, 0)); - - SetValue($this->GetIDForIdent("P_SDL_laden"), round($P_SDL_laden_W, 0)); - SetValue($this->GetIDForIdent("P_SDL_entladen"), round($P_SDL_entladen_W, 0)); - SetValue($this->GetIDForIdent("P_EV_laden"), round($P_EV_laden_W, 0)); - SetValue($this->GetIDForIdent("P_EV_entladen"), round($P_EV_entladen_W, 0)); - - // JSON Debug / Buffer - $out = [ - "SDL_W" => $sdlPowerW, - "Reserve_h" => $reserveH, - "SumBatPower_W" => $sumBatPowerW, - - "SumSDL_req_kWh" => round($sumSDL_req_kWh, 3), - "SumEVcap_kWh" => round($sumEVcap_kWh, 3), - "SumEV_kWh" => round($sumEV_kWh, 3), - - "SoC_SDL_pct" => round($socSDL, 2), - "SoC_EV_pct" => round($socEV, 2), - - "P_SDL_max_W" => round($P_SDL_max_W, 0), - "P_EV_max_W" => round($P_EV_max_W, 0), - - "P_SDL_laden_W" => round($P_SDL_laden_W, 0), - "P_SDL_entladen_W" => round($P_SDL_entladen_W, 0), - "P_EV_laden_W" => round($P_EV_laden_W, 0), - "P_EV_entladen_W" => round($P_EV_entladen_W, 0), - - "batteries" => $calc - ]; - - $json = json_encode($out, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - SetValue($this->GetIDForIdent("CalcJSON"), $json); - $this->SetBuffer("BatteryCalc", $json); - $this->SendDebug("Update", $json, 0); + { + if (!GetValue($this->GetIDForIdent("State"))) { + return; } + $batteries = json_decode($this->ReadPropertyString("Batteries"), true); + if (!is_array($batteries) || count($batteries) === 0) { + $this->SendDebug("Update", "Keine Batterien konfiguriert.", 0); + $this->WriteEmptyResults(); + return; + } + $sdlPowerW = (int)$this->ReadPropertyInteger("SDL_Leistung"); + if ($sdlPowerW < 0) $sdlPowerW = 0; - /* =========================== - * 5) Helper - * =========================== */ + $reserveH = 0.5; // FIX: 30 Minuten + $eps = 0.0001; + + // Summe Batterie-Maxleistungen + $sumBatPowerW = 0; + foreach ($batteries as $b) { + $p = (int)($b["powerbat"] ?? 0); + if ($p > 0) $sumBatPowerW += $p; + } + + if ($sumBatPowerW <= 0) { + $this->SendDebug("Update", "Summe powerbat ist 0 – bitte Leistungen setzen.", 0); + $this->WriteEmptyResults(); + return; + } + + // SDL begrenzen (physikalisch) + if ($sdlPowerW > $sumBatPowerW) { + $this->SendDebug("Update", "SDL_Leistung ($sdlPowerW W) > SummeBatPower ($sumBatPowerW W) -> begrenze.", 0); + $sdlPowerW = $sumBatPowerW; + } + + // Summen Leistung + $P_SDL_max_W = 0.0; + $P_EV_max_W = 0.0; + + $P_SDL_laden_W = 0.0; + $P_SDL_entladen_W = 0.0; + $P_EV_laden_W = 0.0; + $P_EV_entladen_W = 0.0; + + // Summen Energie / Fenster + $sumSDL_req_kWh = 0.0; // Summe Ereq (Reservebedarf 30 min) + $sumEVcap_kWh = 0.0; // Summe EV-Fensterkapazität = Cap - 2*Ereq + $sumEV_kWh = 0.0; // Summe EV-Energie innerhalb Fenster + + // SDL OK Quote + $sumSDL_ok_count = 0; + $sumBat_count = 0; + + // SDL Fensterposition + $sumPos = 0.0; + $countPos = 0; + + $calc = []; + + foreach ($batteries as $idx => $b) { + + $typ = (string)($b["typ"] ?? ("Bat#" . ($idx + 1))); + $pBatW = (int)($b["powerbat"] ?? 0); + $capKWh = (float)($b["capazity"] ?? 0); + $socVar = (int)($b["soc"] ?? 0); + + if ($pBatW <= 0 || $capKWh <= 0) { + $calc[] = ["typ" => $typ, "skip" => "powerbat<=0 oder capacity<=0"]; + continue; + } + + $sumBat_count++; + + // SDL Anteil Leistung proportional + $pSDL_W_raw = ($sdlPowerW * $pBatW) / $sumBatPowerW; + $pSDL_W = min($pSDL_W_raw, (float)$pBatW); + if ($pSDL_W < 0) $pSDL_W = 0.0; + + // EV Anteil Leistung ist Rest + $pEV_W = max(0.0, $pBatW - $pSDL_W); + + $P_SDL_max_W += $pSDL_W; + $P_EV_max_W += $pEV_W; + + // Ereq für 30 Minuten (kWh) + $Ereq_kWh = ($pSDL_W * $reserveH) / 1000.0; + $sumSDL_req_kWh += $Ereq_kWh; + + // Aktuelle Energie aus SoC + $socPct = $this->ReadSocPercent($socVar); + $E_kWh = ($socPct / 100.0) * $capKWh; + + // Reserviertes SDL-Fenster + $Emin = $Ereq_kWh; + $Emax = $capKWh - $Ereq_kWh; + + // EV Fensterkapazität + $EVcap = max(0.0, $Emax - $Emin); // = cap - 2*Ereq + $sumEVcap_kWh += $EVcap; + + // EV Energie im Fenster: clamp(E - Emin, 0..EVcap) + $EV_kWh = 0.0; + if ($EVcap > $eps) { + $EV_kWh = min(max($E_kWh - $Emin, 0.0), $EVcap); + } + $sumEV_kWh += $EV_kWh; + + // SDL kann 30min entladen / laden? + $canSDLdischarge = ($E_kWh + $eps) >= $Emin; // E >= Ereq + $canSDLcharge = (($capKWh - $E_kWh) + $eps) >= $Ereq_kWh; // Cap-E >= Ereq <=> E <= Emax + + // EV kann laden / entladen im Fenster? + $canEVdischarge = ($E_kWh - $Emin) > $eps; // E > Emin + $canEVcharge = ($Emax - $E_kWh) > $eps; // E < Emax + + if ($canSDLdischarge) $P_SDL_entladen_W += $pSDL_W; + if ($canSDLcharge) $P_SDL_laden_W += $pSDL_W; + + if ($canEVdischarge) $P_EV_entladen_W += $pEV_W; + if ($canEVcharge) $P_EV_laden_W += $pEV_W; + + // SDL OK wenn beide Richtungen 30min möglich sind + $sdlOK = ($canSDLdischarge && $canSDLcharge); + if ($sdlOK) $sumSDL_ok_count++; + + // SDL Fensterposition (0..100) + $posPct = 0.0; + $range = ($Emax - $Emin); + if ($range > $eps) { + $pos = ($E_kWh - $Emin) / $range; // 0..1 + if ($pos < 0) $pos = 0; + if ($pos > 1) $pos = 1; + $posPct = $pos * 100.0; + $sumPos += $posPct; + $countPos++; + } + + $calc[] = [ + "typ" => $typ, + "pBat_W" => $pBatW, + "cap_kWh" => round($capKWh, 3), + "soc_pct" => round($socPct, 2), + "E_kWh" => round($E_kWh, 3), + + "pSDL_W" => round($pSDL_W, 0), + "Ereq_kWh" => round($Ereq_kWh, 3), + "Emin_kWh" => round($Emin, 3), + "Emax_kWh" => round($Emax, 3), + + "pEV_W" => round($pEV_W, 0), + "EVcap_kWh" => round($EVcap, 3), + "EV_kWh" => round($EV_kWh, 3), + + "canSDLcharge_30min" => $canSDLcharge, + "canSDLdischarge_30min" => $canSDLdischarge, + "canEVcharge" => $canEVcharge, + "canEVdischarge" => $canEVdischarge, + + "SDL_OK_both_directions" => $sdlOK, + "SDL_window_pos_pct" => round($posPct, 2) + ]; + } + + // EV-Füllstand bezogen auf EV-Fenster + $socEV = ($sumEVcap_kWh > $eps) ? ($sumEV_kWh / $sumEVcap_kWh) * 100.0 : 0.0; + + // SDL OK Quote (Batterie-Quote) + $socSDLok = ($sumBat_count > 0) ? ($sumSDL_ok_count / $sumBat_count) * 100.0 : 0.0; + + // SDL Fensterposition (Durchschnitt) + $sdlPos = ($countPos > 0) ? ($sumPos / $countPos) : 0.0; + + // Ausgabe + SetValue($this->GetIDForIdent("kWh_SDL"), round($sumSDL_req_kWh, 3)); + SetValue($this->GetIDForIdent("kWh_EV"), round($sumEV_kWh, 3)); + + SetValue($this->GetIDForIdent("SoC_SDL"), round($socSDLok, 2)); + SetValue($this->GetIDForIdent("SoC_EV"), round($socEV, 2)); + SetValue($this->GetIDForIdent("SDL_Pos"), round($sdlPos, 2)); + + SetValue($this->GetIDForIdent("P_SDL_max"), round($P_SDL_max_W, 0)); + SetValue($this->GetIDForIdent("P_EV_max"), round($P_EV_max_W, 0)); + + SetValue($this->GetIDForIdent("P_SDL_laden"), round($P_SDL_laden_W, 0)); + SetValue($this->GetIDForIdent("P_SDL_entladen"), round($P_SDL_entladen_W, 0)); + SetValue($this->GetIDForIdent("P_EV_laden"), round($P_EV_laden_W, 0)); + SetValue($this->GetIDForIdent("P_EV_entladen"), round($P_EV_entladen_W, 0)); + + $out = [ + "SDL_W" => $sdlPowerW, + "Reserve_h" => $reserveH, + "SumBatPower_W" => $sumBatPowerW, + + "SumSDL_req_kWh" => round($sumSDL_req_kWh, 3), + "SumEVcap_kWh" => round($sumEVcap_kWh, 3), + "SumEV_kWh" => round($sumEV_kWh, 3), + + "SDL_OK_quote_pct" => round($socSDLok, 2), + "SDL_window_pos_pct" => round($sdlPos, 2), + "EV_window_fill_pct" => round($socEV, 2), + + "P_SDL_max_W" => round($P_SDL_max_W, 0), + "P_EV_max_W" => round($P_EV_max_W, 0), + + "P_SDL_laden_W" => round($P_SDL_laden_W, 0), + "P_SDL_entladen_W" => round($P_SDL_entladen_W, 0), + "P_EV_laden_W" => round($P_EV_laden_W, 0), + "P_EV_entladen_W" => round($P_EV_entladen_W, 0), + + "batteries" => $calc + ]; + + $json = json_encode($out, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + SetValue($this->GetIDForIdent("CalcJSON"), $json); + $this->SetBuffer("BatteryCalc", $json); + $this->SendDebug("Update", $json, 0); + } /** * SoC in Prozent (0..100). @@ -328,10 +333,19 @@ class Bat_EV_SDL extends IPSModule { SetValue($this->GetIDForIdent("kWh_SDL"), 0.0); SetValue($this->GetIDForIdent("kWh_EV"), 0.0); + SetValue($this->GetIDForIdent("SoC_SDL"), 0.0); SetValue($this->GetIDForIdent("SoC_EV"), 0.0); + SetValue($this->GetIDForIdent("SDL_Pos"), 0.0); + SetValue($this->GetIDForIdent("P_SDL_max"), 0.0); SetValue($this->GetIDForIdent("P_EV_max"), 0.0); + + SetValue($this->GetIDForIdent("P_EV_laden"), 0.0); + SetValue($this->GetIDForIdent("P_EV_entladen"), 0.0); + SetValue($this->GetIDForIdent("P_SDL_laden"), 0.0); + SetValue($this->GetIDForIdent("P_SDL_entladen"), 0.0); + SetValue($this->GetIDForIdent("CalcJSON"), "{}"); $this->SetBuffer("BatteryCalc", "{}"); }