Files
Symcon_Belevo_Energiemanage…/Bat_EV_SDL/module.php
2026-01-22 16:31:34 +01:00

353 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
class Bat_EV_SDL extends IPSModule
{
public function Create()
{
parent::Create();
// Properties (aus form.json)
$this->RegisterPropertyString("Batteries", "[]");
$this->RegisterPropertyInteger("SDL_Leistung", 0); // W
$this->RegisterPropertyInteger("UpdateInterval", 5); // Minuten, 0 = aus
// Status
$this->RegisterVariableBoolean("State", "Aktiv", "~Switch", 1);
$this->EnableAction("State");
// Ergebnisse
$this->RegisterVariableFloat("kWh_SDL", "SDL Reservebedarf 30min (kWh)", "", 20);
$this->RegisterVariableFloat("kWh_EV", "EV Energie im Fenster (kWh)", "", 21);
// 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);
// 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);
// Debug JSON
$this->RegisterVariableString("CalcJSON", "Berechnung (JSON)", "", 99);
// Timer (Prefix aus module.json: "GEF")
$this->RegisterTimer("UpdateTimer", 0, 'GEF_Update($_IPS["TARGET"]);');
}
public function ApplyChanges()
{
parent::ApplyChanges();
$intervalMin = (int)$this->ReadPropertyInteger("UpdateInterval");
if ($intervalMin > 0) {
$this->SetTimerInterval("UpdateTimer", $intervalMin * 60 * 1000);
} else {
$this->SetTimerInterval("UpdateTimer", 0);
}
$this->Update();
}
public function RequestAction($Ident, $Value)
{
switch ($Ident) {
case "State":
SetValue($this->GetIDForIdent("State"), (bool)$Value);
if ((bool)$Value) {
$this->Update();
}
break;
default:
throw new Exception("Invalid Ident: " . $Ident);
}
}
/**
* 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;
// 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).
* Akzeptiert 0..1 (wird *100) oder 0..100.
*/
private function ReadSocPercent(int $varId): float
{
if ($varId <= 0 || !IPS_VariableExists($varId)) {
return 0.0;
}
$v = GetValue($varId);
if (!is_numeric($v)) {
return 0.0;
}
$f = (float)$v;
// 0..1 -> Prozent
if ($f >= 0.0 && $f <= 1.0) {
$f *= 100.0;
}
// Clamp
if ($f < 0.0) $f = 0.0;
if ($f > 100.0) $f = 100.0;
return $f;
}
private function WriteEmptyResults()
{
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", "{}");
}
}