Files
Symcon_Belevo_Energiemanage…/Bat_EV_SDL_V3_Beta/module.php
T
2026-04-21 07:32:18 +02:00

989 lines
33 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_V3_Beta extends IPSModule
{
private const HOURS = 0.5; // 30 Minuten
public function Create()
{
parent::Create();
// Properties
$this->RegisterPropertyString("Batteries", "[]");
//$this->RegisterPropertyInteger("$upKWh + $underKWh)", 0);
$this->RegisterPropertyInteger("SDL_Leistung_Laden", 0);
$this->RegisterPropertyInteger("SDL_Leistung_Entladen", 0); // W
$this->RegisterPropertyInteger("UpdateInterval", 5); // Minuten
// Status
$this->RegisterVariableBoolean("State", "Aktiv", "~Switch", 1);
$this->EnableAction("State");
// Prozentwerte
$this->RegisterVariableFloat("SDL_Pos", "SDL Energie verfügbar (%)", "", 10);
$this->RegisterVariableFloat("SoC_EV", "EV Energie verfügbar (%)", "", 11);
// Variablen
$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);
$this->RegisterVariableFloat("Aktuelle_Leistung_SDL", "Aktuelle Leistung SDL", "", 4);
$this->RegisterVariableFloat("P_SDL_laden", "P SDL laden max (W)", "", 21);
$this->RegisterVariableFloat("P_SDL_entladen", "P SDL entladen max (W)", "", 22);
$this->RegisterVariableFloat("P_EV_laden", "P EV laden max (W)", "", 31);
$this->RegisterVariableFloat("P_EV_entladen", "P EV entladen max (W)", "", 32);
// Debug
$this->RegisterVariableString("CalcJSON", "Berechnung (JSON)", "", 99);
// Timer: wichtig -> Prefix muss passen
$this->RegisterTimer("UpdateTimer", 0, 'GEF_Update($_IPS["TARGET"]);');
}
public function ApplyChanges()
{
parent::ApplyChanges();
$intervalSec = (int)$this->ReadPropertyInteger("UpdateInterval");
$this->SetTimerInterval("UpdateTimer", ($intervalSec > 0) ? $intervalSec * 1000 : 0);
// Cache neu bauen (force)
$this->BuildBatteryCache(true);
$this->Update();
}
public function RequestAction($Ident, $Value)
{
if ($Ident === "State") {
SetValue($this->GetIDForIdent("State"), (bool)$Value);
if ((bool)$Value) {
$this->Update();
}
return;
}
throw new Exception("Invalid Ident: " . $Ident);
}
public function Update()
{
// Reentranzschutz verhindert parallele Update()-Läufe
$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;
}
// Cache nur neu bauen, wenn nötig
$this->BuildBatteryCache(false);
$cache = json_decode($this->GetBufferSafe("BatCacheJSON"), true);
if (!is_array($cache) || empty($cache["bats"])) {
$this->WriteAllZero("cache empty/invalid");
return;
}
// =====================
// Integrator: Zeit / Konten laden (microtime ist bei 2s besser)
// =====================
$now = microtime(true);
$lastTs = (float)$this->GetBufferSafe("Int_LastTs");
$Eev = (float)$this->GetBufferSafe("Int_E_EV_kWh");
$Esdl = (float)$this->GetBufferSafe("Int_E_SDL_kWh");
// Letzte gefilterte Leistungen (damit wir Slew-Rate begrenzen können)
$prevPEV = (float)$this->GetBufferSafe("Int_P_EV_W");
$prevPSDL = (float)$this->GetBufferSafe("Int_P_SDL_W");
if ($lastTs <= 0.0) {
$lastTs = $now;
$Eev = 0.0;
$Esdl = 0.0;
$prevPEV = 0.0;
$prevPSDL = 0.0;
}
$dtSec = $now - $lastTs;
if ($dtSec < 0.0) $dtSec = 0.0;
if ($dtSec > 3600.0) $dtSec = 3600.0; // Kappe 1h
$dtH = $dtSec / 3600.0;
$calc = [
"inputs" => $cache["inputs"] ?? [],
"batteries" => [],
"total" => []
];
// =====================
// Summen
// =====================
$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;
// Reset bei SoC=50% (mehrere Batterien): kapazitätsgewichteter Durchschnitt
$socCapSum = 0.0;
$socWeighted = 0.0;
foreach ($cache["bats"] as $i => $c) {
$capKWh = (float)($c["capKWh"] ?? 0.0);
if ($capKWh <= 0.0) continue;
// SoC lesen
$socVarId = (int)($c["socVarId"] ?? 0);
$socPct = $this->ReadSocPercent($socVarId);
$real_kWh = $capKWh / 100.0 * $socPct;
// SoC für Reset mitteln (gewichtet)
$socWeighted += $socPct * $capKWh;
$socCapSum += $capKWh;
// vorkalkuliert:
$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);
// Defaults
$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;
// --- 3 Fälle ---
if ($underKWh <= $real_kWh && $real_kWh <= $upKWh) {
// SDL_SOC im Fenster dynamisch (0..100)
$win = ($upKWh - $underKWh);
$SDL_SOC = ($win > 0.0) ? (100.0 * ($real_kWh - $underKWh) / $win) : 0.0;
$SDL_SOC = is_finite($SDL_SOC) ? max(0.0, min(100.0, $SDL_SOC)) : 0.0;
// EV_SOC im Fenster dynamisch
$EV_SOC = ($EV_kWh > 0.0) ? (100.0 * ($real_kWh - $underKWh) / $EV_kWh) : 0.0;
$EV_SOC = is_finite($EV_SOC) ? max(0.0, min(100.0, $EV_SOC)) : 0.0;
// Limits
$sdlDisKW = $sdlShareKW_entladen;
$evDisKW = $evShareKW_entladen;
$sdlChKW = $sdlShareKW_laden;
$evChKW = $evShareKW_laden;
// Referenz-kWh
$real_kWh_ev = $real_kWh - $underKWh;
$real_kWh_sdl = $underKWh;
} elseif ($real_kWh > $upKWh) {
// Oberhalb: EV voll, EV darf NICHT laden
$EV_SOC = 100.0;
$den1 = ($capKWh - $real_kWh + $underKWh);
$den2 = (2.0 * $underKWh);
$SDL_SOC = ($den1 > 0.0 && $den2 > 0.0) ? min(100.0, ($den1 / $den2) * 100.0) : 0.0;
$SDL_SOC = is_finite($SDL_SOC) ? max(0.0, min(100.0, $SDL_SOC)) : 0.0;
$sdlDisKW = $sdlShareKW_entladen;
$evDisKW = $evShareKW_entladen;
$sdlChKW = $sdlShareKW_laden;
$evChKW = 0.0; // ✅ EV darf oben nicht laden
$real_kWh_ev = $capKWh - ($capKWh - $upKWh + $underKWh);
$real_kWh_sdl = ($capKWh - $upKWh + $underKWh) - ($capKWh - $real_kWh);
} else { // $real_kWh < $underKWh
// Unterhalb: EV leer, EV darf NICHT entladen
$EV_SOC = 0.0;
$den = $upKWh + $underKWh;
$SDL_SOC = ($den > 0.0) ? ($real_kWh * 100.0 / $den) : 0.0;
$SDL_SOC = is_finite($SDL_SOC) ? max(0.0, min(100.0, $SDL_SOC)) : 0.0;
$sdlDisKW = $sdlShareKW_entladen;
$evDisKW = 0.0; // ✅ EV darf unten nicht entladen
$sdlChKW = $sdlShareKW_laden;
$evChKW = $evShareKW_laden;
$real_kWh_ev = 0.0;
$real_kWh_sdl = $real_kWh;
}
// Null/Full Schutz
if ($real_kWh <= 0.0) {
$sdlDisKW = 0.0;
$real_kWh_ev = 0.0;
$real_kWh_sdl = 0.0;
} elseif ($real_kWh >= $capKWh) {
$sdlChKW = 0.0;
$real_kWh_ev = $EV_kWh;
$real_kWh_sdl = $SDL_kWh;
}
$real_kWh_ev = max(0.0, $real_kWh_ev);
$real_kWh_sdl = max(0.0, $real_kWh_sdl);
// Summen bilden
$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),
];
}
// =====================
// Maximalleistungen setzen (Anzeige/Debug) jetzt sofort, weil wir sie gleich brauchen
// =====================
$this->SetIdentValue("P_SDL_laden", round($sdlChKW_ges * 1000.0, 0));
$this->SetIdentValue("P_SDL_entladen", round($sdlDisKW_ges * 1000.0, 0));
$this->SetIdentValue("P_EV_laden", round($evChKW_ges * 1000.0, 0));
$this->SetIdentValue("P_EV_entladen", round($evDisKW_ges * 1000.0, 0));
// =====================
// Reset bei SoC ~ 50% (einmalig)
// =====================
$socAvg = ($socCapSum > 0.0) ? ($socWeighted / $socCapSum) : 0.0;
$resetTarget = 50.0;
$resetTol = 0.5; // bei 2s Update: 0.2..0.5
$inWindow = (abs($socAvg - $resetTarget) <= $resetTol);
$armedStr = $this->GetBufferSafe("Int_ResetArmed");
$armed = ($armedStr === "") ? true : ($armedStr === "1");
$doReset50 = false;
if ($armed && $inWindow) {
$doReset50 = true;
$armed = false;
}
if (!$inWindow) {
$armed = true;
}
$this->SetBuffer("Int_ResetArmed", $armed ? "1" : "0");
// =====================
// Integrator Init (damit Zonenreset nicht überschrieben wird)
// =====================
if ($this->GetBufferSafe("Int_Init") !== "1") {
$Eev = $real_kWh_ev_ges;
$Esdl = $real_kWh_sdl_ges;
$this->SetBuffer("Int_Init", "1");
}
// =====================
// EV-Zone Edge Reset (oben/unten)
// =====================
$epsKWh = 0.001;
$zoneNow = 0;
if ($real_kWh_ev_ges <= $epsKWh) {
$zoneNow = -1; // EV leer
} elseif ($EV_kWh_ges > 0.0 && $real_kWh_ev_ges >= ($EV_kWh_ges - $epsKWh)) {
$zoneNow = 1; // EV voll
}
$zonePrevStr = $this->GetBufferSafe("EV_Zone");
$zonePrev = ($zonePrevStr === "") ? $zoneNow : (int)$zonePrevStr;
if ($zoneNow !== $zonePrev) {
if ($zoneNow === 1) $Eev = $EV_kWh_ges; // 100%
if ($zoneNow === -1) $Eev = 0.0; // 0%
}
$this->SetBuffer("EV_Zone", (string)$zoneNow);
// =====================
// 50%-Reset (du akzeptierst Sprung)
// =====================
if ($doReset50) {
$Eev = $real_kWh_ev_ges;
$Esdl = $real_kWh_sdl_ges;
}
// =====================
// Integrator soll mit "aktuellen" Werten laufen aber bereinigt!
// =====================
$pEvSoll = (float)GetValue($this->GetIDForIdent("Nennleistung_Soll_EV"));
$pSdlSoll = (float)GetValue($this->GetIDForIdent("Nennleistung_Soll_SDL"));
$epsW = 1.0;
$evActive = (abs($pEvSoll) > $epsW);
$sdlActive = (abs($pSdlSoll) > $epsW);
// 1) Rohwerte: "Aktuelle Leistung" aus Variablen lesen
$rawEV = (float)GetValue($this->GetIDForIdent("Aktuelle_Leistung_EV"));
$rawSDL = (float)GetValue($this->GetIDForIdent("Aktuelle_Leistung_SDL"));
// 2) Hard-Clamp an physikalische Maxima (lokale Summen, kW->W)
$maxEV_ch = $evChKW_ges * 1000.0;
$maxEV_dis = $evDisKW_ges * 1000.0;
$maxSDL_ch = $sdlChKW_ges * 1000.0;
$maxSDL_dis = $sdlDisKW_ges * 1000.0;
$rawEV = max(-$maxEV_dis, min($maxEV_ch, $rawEV));
$rawSDL = max(-$maxSDL_dis, min($maxSDL_ch, $rawSDL));
// 3) EV-Zonenregeln auch auf aktuelle Leistung anwenden
if ($zoneNow === 1 && $rawEV > 0.0) $rawEV = 0.0; // oben: EV darf nicht laden
if ($zoneNow === -1 && $rawEV < 0.0) $rawEV = 0.0; // unten: EV darf nicht entladen
// 4) Slew-Rate: Sprünge begrenzen (W pro Sekunde)
// Passe diesen Wert an dein System an:
$maxSlewWps = 10000.0; // 10 kW pro Sekunde
$maxDeltaW = $maxSlewWps * max(0.001, $dtSec);
$PEV = $rawEV;
$PSDL = $rawSDL;
if (is_finite($prevPEV) && abs($PEV - $prevPEV) > $maxDeltaW) {
$this->SendDebug("SpikeFilter", "EV spike ignored: new=$PEV prev=$prevPEV", 0);
$PEV = $prevPEV;
}
if (is_finite($prevPSDL) && abs($PSDL - $prevPSDL) > $maxDeltaW) {
$this->SendDebug("SpikeFilter", "SDL spike ignored: new=$PSDL prev=$prevPSDL", 0);
$PSDL = $prevPSDL;
}
// 5) Wenn Kanal inaktiv: Energie hart auf Referenz (wie du es wolltest)
if ($evActive) {
$Eev += (($PEV / 1000.0) * $dtH);
} else {
$Eev = $real_kWh_ev_ges;
$PEV = 0.0;
}
if ($sdlActive) {
$Esdl += (($PSDL / 1000.0) * $dtH);
} else {
$Esdl = $real_kWh_sdl_ges;
$PSDL = 0.0;
}
// Clamp Energiekonten
$Eev = ($EV_kWh_ges > 0.0) ? max(0.0, min($EV_kWh_ges, $Eev)) : 0.0;
$Esdl = ($SDL_kWh_ges > 0.0) ? max(0.0, min($SDL_kWh_ges, $Esdl)) : 0.0;
// Prozentwerte
$evPosPct = ($EV_kWh_ges > 0.0) ? ($Eev / $EV_kWh_ges * 100.0) : 0.0;
$sdlPosPct = ($SDL_kWh_ges > 0.0) ? ($Esdl / $SDL_kWh_ges * 100.0) : 0.0;
// Output setzen
$this->SetIdentValue("SoC_EV", round($evPosPct, 3));
$this->SetIdentValue("SDL_Pos", round($sdlPosPct, 3));
// JSON total
$calc["total"] = [
"SDL_SoC_pct" => round($sdlPosPct, 3),
"EV_SoC_pct" => round($evPosPct, 3),
"SDL_kWh_total" => round($SDL_kWh_ges, 3),
"EV_kWh_total" => round($EV_kWh_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),
"socAvg" => round($socAvg, 3),
"doReset50" => $doReset50 ? 1 : 0,
"evZone" => $zoneNow,
"PEV_used_W" => round($PEV, 0),
"PSDL_used_W" => round($PSDL, 0)
];
$this->SetIdentValue("CalcJSON", json_encode($calc, JSON_PRETTY_PRINT));
// Setpoints schreiben
$this->ApplySetpoints();
// =====================
// Integrator State speichern
// =====================
$this->SetBuffer("Int_LastTs", (string)$now);
$this->SetBuffer("Int_E_EV_kWh", (string)$Eev);
$this->SetBuffer("Int_E_SDL_kWh", (string)$Esdl);
// letzte gefilterte Leistung speichern
$this->SetBuffer("Int_P_EV_W", (string)$PEV);
$this->SetBuffer("Int_P_SDL_W", (string)$PSDL);
} 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 = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung"));
$sdlTotalW_laden = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Laden"));
$sdlTotalW_entladen = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Entladen"));
$hours = self::HOURS;
$hash = md5(json_encode([
"Batteries" => $batteriesRaw,
"SDL_W_Laden" => $sdlTotalW_laden,
"SDL_W_Entladen" => $sdlTotalW_entladen,
"hours" => $hours
]));
$oldHash = $this->GetBufferSafe("BatCacheHash");
if (!$force && $oldHash === $hash) {
return;
}
$batteries = json_decode($batteriesRaw, true);
if (!is_array($batteries)) $batteries = [];
$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),
"hours" => $hours
],
"bats" => []
];
if ($sumBatPowerW <= 0.0) {
$this->SetBuffer("BatCacheHash", $hash);
$this->SetBuffer("BatCacheJSON", json_encode($cache));
$this->SendDebug("Cache", "sumBatPowerW=0 -> empty cache", 0);
return;
}
$sumBatPowerkW = $sumBatPowerW / 1000.0;
//$sdlTotalkW = $sdlTotalW / 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)));
// Mit laden und entladen unterschiedlich
//----------------------------------------------------
$sdlShareKW_laden = ($sumBatPowerkW > 0.0) ? ($sdlTotalkW_laden / $sumBatPowerkW * $pBatkW) : 0.0;
$evShareKW_laden = $pBatkW - $sdlShareKW_laden;
$upKWh = $capKWh - $sdlShareKW_laden * 0.5;
$sdlShareKW_entladen = ($sumBatPowerkW > 0.0) ? ($sdlTotalkW_entladen / $sumBatPowerkW * $pBatkW) : 0.0;
$evShareKW_entladen = $pBatkW - $sdlShareKW_entladen;
$underKWh = $sdlShareKW_entladen * 0.5;
$SDL_kWh = $underKWh + ($capKWh -$upKWh);
$EV_kWh = $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,
"SDL_kWh_total" => $SDL_kWh,
"EV_kWh_total" => $EV_kWh
];
}
$this->SetBuffer("BatCacheHash", $hash);
$this->SetBuffer("BatCacheJSON", json_encode($cache));
$this->SendDebug("Cache", "Battery cache rebuilt (" . count($cache["bats"]) . " bats)", 0);
}
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.0);
$this->SetIdentValue("P_EV_entladen", 0.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
{
// Falls jemand statt Variable direkt 0..100 einträgt
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;
if ($soc < 0.0) $soc = 0.0;
if ($soc > 100.0) $soc = 100.0;
return $soc;
}
public function ApplySetpoints(): void
{
$pEvW = (float) GetValue($this->GetIDForIdent("Nennleistung_Soll_EV"));
$pSdlW = (float) GetValue($this->GetIDForIdent("Nennleistung_Soll_SDL"));
//Diverenz zwischen EV und SDl berechnen
// Methode für priorisierung
$distribution = $this->CalculateBatteryDistribution($pEvW,$pSdlW);
$this->WriteBatteryPowerSetpoints($distribution);
}
private function CalculateBatteryDistribution(float $pEvW, float $pSdlW): array
{
$calcJsonId = $this->GetIDForIdent("CalcJSON");
// Fallback, falls Variable leer ist oder nicht existiert
if (!IPS_VariableExists($calcJsonId)) {
return [];
}
$rawJson = (string)GetValue($calcJsonId);
if (empty($rawJson)) {
return [];
}
$calc = json_decode($rawJson, true);
$batteries = $calc['batteries'] ?? [];
// ---------------------------------------------------------
// Hilfsfunktion: Verteilungslogik (Closure)
// ---------------------------------------------------------
$distributePower = function(float $targetPower, string $mode) use ($batteries): array {
// Initialisierung des Ergebnis-Arrays (Key = idx, Value = zugewiesene Watt)
$result = [];
foreach ($batteries as $bat) {
$result[$bat['idx']] = 0.0;
}
if (abs($targetPower) < 0.01) {
return $result; // Nichts zu tun
}
$isCharge = ($targetPower > 0);
$absPower = abs($targetPower);
// Relevante Keys basierend auf Modus (EV oder SDL) und Richtung (Laden/Entladen)
$socKey = ($mode === 'EV') ? 'EV_SOC' : 'SDL_SOC';
// Achtung: JSON Limits sind in kW, wir rechnen in Watt -> * 1000
$limitKey = $isCharge ? $mode . '_Charge_kW' : $mode . '_Discharge_kW';
// 1. Batterien vorbereiten und gruppieren nach gerundetem SoC
$groups = [];
foreach ($batteries as $bat) {
$soc = (int)round($bat[$socKey]); // Auf ganze Zahl runden
$maxW = ((float)$bat[$limitKey]) * 1000.0; // kW in Watt umrechnen
$groups[$soc][] = [
'idx' => $bat['idx'],
'maxW' => $maxW
];
}
// 2. Sortieren der Gruppen
// Laden: Wenig SoC zuerst (ASC) -> leere füllen
// Entladen: Viel SoC zuerst (DESC) -> volle leeren
if ($isCharge) {
ksort($groups);
} else {
krsort($groups);
}
// 3. Verteilung
$remainingNeeded = $absPower;
foreach ($groups as $soc => $groupBatteries) {
if ($remainingNeeded <= 0.01) break;
// Gesamte verfügbare Leistung in dieser SoC-Gruppe ermitteln
$groupTotalCapacity = 0.0;
foreach ($groupBatteries as $gb) {
$groupTotalCapacity += $gb['maxW'];
}
// Wie viel können wir dieser Gruppe zuteilen?
// Entweder alles was die Gruppe kann, oder den Restbedarf
$powerForThisGroup = min($remainingNeeded, $groupTotalCapacity);
// Proportionale Aufteilung innerhalb der Gruppe
// Falls Gruppe Kapazität 0 hat (Defekt/Voll), verhindern wir DivByZero
if ($groupTotalCapacity > 0) {
$ratio = $powerForThisGroup / $groupTotalCapacity;
foreach ($groupBatteries as $gb) {
$assigned = $gb['maxW'] * $ratio;
$result[$gb['idx']] = $assigned;
}
}
$remainingNeeded -= $powerForThisGroup;
}
// Wenn wir entladen, müssen die Werte negativ sein
if (!$isCharge) {
foreach ($result as $idx => $val) {
$result[$idx] = -$val;
}
}
return $result;
};
// ---------------------------------------------------------
// Hauptablauf
// ---------------------------------------------------------
// 1. Berechnung für EV und SDL getrennt durchführen
$evDistribution = $distributePower($pEvW, 'EV');
$sdlDistribution = $distributePower($pSdlW, 'SDL');
// 2. Ergebnisse zusammenführen und Output formatieren
$finalOutput = [];
foreach ($batteries as $bat) {
$idx = $bat['idx'];
// Summe der beiden Anforderungen (kann sich gegenseitig aufheben)
$valEv = $evDistribution[$idx] ?? 0.0;
$valSdl = $sdlDistribution[$idx] ?? 0.0;
$totalW = $valEv + $valSdl;
// Aufteilen in Charge / Discharge für das Return-Format
$chargeW = 0.0;
$dischargeW = 0.0;
if ($totalW > 0) {
$chargeW = abs($totalW);
} else {
$dischargeW = abs($totalW);
}
// JSON Objekt erstellen
$finalOutput[] = [
"idx" => $idx,
"typ" => (string)$bat['typ'], // Typ als String beibehalten
"chargeW" => round($chargeW, 0), // Optional: runden für sauberes JSON
"dischargeW" => round($dischargeW, 0)
];
}
$batteriesRaw = json_decode($this->ReadPropertyString("Batteries"));
$totalPower_ist = 0;
foreach ($batteriesRaw as $bat) {
$totalPower_ist += (-1)*GetValue($bat->register_bat_power);
}
// =====================================================
// Stabilisierung "Aktuelle Leistung": Sample & Hold
// -> ändert sich nur wenn Sollwerte sich ändern
// =====================================================
$eps = 0.01;
// Toleranz: wann gilt Soll als "gleich geblieben" (W)
$tolW = 0.5;
// Letzte Sollwerte und Haltefaktor aus Buffer
$prevEvSoll = (float)$this->GetBufferSafe("Hold_EvSoll");
$prevSdlSoll = (float)$this->GetBufferSafe("Hold_SdlSoll");
$factorHold = (float)$this->GetBufferSafe("Hold_Factor");
// Initialwerte falls Buffer leer
if ($this->GetBufferSafe("Hold_Init") !== "1") {
$prevEvSoll = $pEvW;
$prevSdlSoll = $pSdlW;
$factorHold = 1.0;
$this->SetBuffer("Hold_Init", "1");
}
// Prüfen ob Sollwerte sich geändert haben
$changed =
(abs($pEvW - $prevEvSoll) > $tolW) ||
(abs($pSdlW - $prevSdlSoll) > $tolW);
// Beim Sollwechsel: neuen Faktor bestimmen und einfrieren
if ($changed) {
$sumSoll = (float)($pEvW + $pSdlW);
if (abs($sumSoll) > $eps) {
$factorHold = (float)($totalPower_ist / $sumSoll);
} else {
// Soll hebt sich auf / fast 0: keine Skalierung
$factorHold = 1.0;
}
// Faktor begrenzen (Schutz gegen Unsinn; ggf. anpassen)
$factorHold = max(-2.0, min(2.0, $factorHold));
// Sollwerte merken
$prevEvSoll = $pEvW;
$prevSdlSoll = $pSdlW;
$this->SetBuffer("Hold_EvSoll", (string)$prevEvSoll);
$this->SetBuffer("Hold_SdlSoll", (string)$prevSdlSoll);
$this->SetBuffer("Hold_Factor", (string)$factorHold);
}
// "Aktuelle" Leistungen aus gehaltenem Faktor berechnen
$aktEV = (float)($pEvW * $factorHold);
$aktSDL = (float)($pSdlW * $factorHold);
// Optional: wenn beide Soll = 0 -> beide aktuelle = 0
if (abs($pEvW) <= $eps && abs($pSdlW) <= $eps) {
$aktEV = 0.0;
$aktSDL = 0.0;
}
// Diese beiden Variablen stabil setzen (kein $this->SetValue!)
$this->SetIdentValue("Aktuelle_Leistung_EV", (float)$aktEV);
$this->SetIdentValue("Aktuelle_Leistung_SDL", (float)$aktSDL);
return $finalOutput;
}
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))));
// ✅ immer positiv
$chargeW = max(0.0, (float)($d["chargeW"] ?? 0.0));
$dischargeW = max(0.0, (float)($d["dischargeW"] ?? 0.0));
// nie gleichzeitig
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);
// Leistungs-Variablen
$varPowerCharge = (int)($cfg["powerbat_laden"] ?? 0);
$varPowerDisch = (int)($cfg["powerbat_entladen"] ?? 0);
// ✅ EIN Modus-Register
$varMode = (int)($cfg["register_ladenentladen_modus"] ?? 0);
// sichere Writer
$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)); // ✅ niemals negativ
}
};
// Moduscodes je Typ
$modeCharge = 1; $modeDisch = 2; // Default
if (strpos($t, "goodwe") !== false) {
$modeCharge = 11; $modeDisch = 12;
} elseif (strpos($t, "solaredge") !== false) {
$modeCharge = 3; $modeDisch = 4;
}
// =========================
// Laden
// =========================
if ($chargeW > 0.0) {
// Modus setzen (ein Register)
$setInt($varMode, $modeCharge);
// GoodWe: nur powerbat_laden
if (strpos($t, "goodwe") !== false) {
$setW($varPowerCharge, (int)$chargeW);
$setW($varPowerDisch, (int)0);
return;
}
// SolarEdge + Default: zwei Leistungsregister
$setW($varPowerCharge, $chargeW);
$setW($varPowerDisch, 0.0);
return;
}
// =========================
// Entladen
// =========================
if ($dischargeW > 0.0) {
// Modus setzen (ein Register)
$setInt($varMode, $modeDisch);
// GoodWe: Entladen nutzt trotzdem powerbat_laden (immer positiv)
if (strpos($t, "goodwe") !== false) {
$setW($varPowerCharge, (int)$dischargeW);
$setW($varPowerDisch, (int)0);
return;
}
// SolarEdge + Default: Entladen in powerbat_entladen
$setW($varPowerDisch, $dischargeW);
$setW($varPowerCharge, 0.0);
return;
}
// =========================
// Stop / Neutral
// =========================
$setW($varPowerCharge, 0.0);
$setW($varPowerDisch, 0.0);
// optional: Modus nicht anfassen oder auf 0 setzen:
// $setInt($varMode, 0);
}
}
?>