Files
Symcon_Belevo_Energiemanage…/Bat_EV_SDL_V4/module.php
T
2026-05-04 19:31:47 +02:00

828 lines
29 KiB
PHP

<?php
class Bat_EV_SDL_V4 extends IPSModule
{
public function Create()
{
parent::Create();
// Properties
$this->RegisterPropertyString("Batteries", "[]");
$this->RegisterPropertyInteger("SDL_Leistung_Laden", 0); // W
$this->RegisterPropertyInteger("SDL_Leistung_Entladen", 0); // W
$this->RegisterPropertyFloat("ReserveHours", 0.5); // h
$this->RegisterPropertyFloat("SDL_Start_Pos_Config", 50.0); // %
$this->RegisterPropertyFloat("EV_Start_Pos_Config", 50.0); // %
$this->RegisterPropertyInteger("UpdateInterval", 2); // Sekunden
$this->RegisterPropertyBoolean("FilterAktiv", true);
// Status
$this->RegisterVariableBoolean("State", "Aktiv", "~Switch", 1);
$this->EnableAction("State");
// Sollwerte
$this->RegisterVariableFloat("Nennleistung_Soll_EV", "Nennleistung Soll EV", "", 2);
$this->RegisterVariableFloat("Nennleistung_Soll_SDL", "Nennleistung Soll SDL", "", 3);
// 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);
$this->RegisterVariableFloat("P_SDL_entladen", "P SDL entladen max (W)", "", 22);
$this->RegisterVariableInteger("P_EV_laden", "P EV laden max (W)", "", 31);
$this->RegisterVariableInteger("P_EV_entladen", "P EV entladen max (W)", "", 32);
// 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: Prefix/Funktionsname muss zu prefix in module.json 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
);
$this->BuildBatteryPlan(true);
$this->InitVirtualAccountsIfNeeded();
$this->Update();
}
public function RequestAction($Ident, $Value)
{
switch ($Ident) {
case "State":
SetValue($this->GetIDForIdent("State"), (bool)$Value);
if ((bool)$Value) {
$this->Update();
}
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);
}
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;
}
$this->BuildBatteryPlan(false);
$plan = json_decode($this->GetBufferSafe("BatPlanJSON"), true);
if (!is_array($plan) || empty($plan["bats"])) {
$this->WriteAllZero("plan empty/invalid");
return;
}
// Erst Sollwerte auf Batterien verteilen und aktuelle EV/SDL Leistung zurückrechnen.
$this->ApplySetpoints($plan);
// Danach EV und SDL virtuell integrieren.
$virt = $this->IntegrateVirtualAccounts($plan);
$this->SetIdentValue("SDL_Pos", round($virt["SDL_pct"], 3));
$this->SetIdentValue("SoC_EV", round($virt["EV_pct"], 3));
$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 = $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)
];
$calcRounded = $this->RoundArray($calc, 3);
$this->SetIdentValue("CalcJSON", json_encode($calcRounded, JSON_PRETTY_PRINT));
} catch (Throwable $e) {
$this->SendDebug("Update ERROR", $e->getMessage() . " @ " . $e->getFile() . ":" . $e->getLine(), 0);
$this->WriteAllZero("Exception: " . $e->getMessage());
} finally {
IPS_SemaphoreLeave($semKey);
}
}
/**
* 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_entladen = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Entladen"));
$reserveH = max(0.0, (float)$this->ReadPropertyFloat("ReserveHours"));
$hash = md5(json_encode([
"Batteries" => $batteriesRaw,
"SDL_W_Laden" => $sdlTotalW_laden,
"SDL_W_Entladen" => $sdlTotalW_entladen,
"ReserveHours" => $reserveH,
]));
if (!$force && $this->GetBufferSafe("BatPlanHash") === $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;
}
}
$plan = [
"inputs" => [
"SDL_Leistung_W_laden" => $sdlTotalW_laden,
"SDL_Leistung_W_entladen" => $sdlTotalW_entladen,
"ReserveHours" => $reserveH,
"SumBatPower_W" => round($sumBatPowerW, 0)
],
"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) {
$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;
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 || $pBatKW <= 0.0) {
continue;
}
$typ = (string)($b["typ"] ?? ("Bat " . ($idx + 1)));
$socVarId = (int)($b["soc"] ?? 0);
$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);
// Grenzen: individuell nach Reservezeit.
$underKWh = $sdlShareKW_entladen * $reserveH;
$upKWh = $capKWh - ($sdlShareKW_laden * $reserveH);
$underKWh = max(0.0, min($capKWh, $underKWh));
$upKWh = max(0.0, min($capKWh, $upKWh));
// Falls Reserve zu groß ist, EV-Fenster sauber auf 0 setzen.
if ($upKWh < $underKWh) {
$mid = $capKWh / 2.0;
$underKWh = $mid;
$upKWh = $mid;
}
$sdlLowerKWh = $underKWh;
$sdlUpperKWh = $capKWh - $upKWh;
$SDL_kWh = max(0.0, $sdlLowerKWh + $sdlUpperKWh);
$EV_kWh = max(0.0, $capKWh - $SDL_kWh);
$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,
"real_SOC_pct" => round($realSocPct, 3),
"real_kWh" => round($realKWh, 3),
"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;
}
$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);
}
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, $plan);
$this->WriteBatteryPowerSetpoints($distribution);
$this->UpdateActualPowerSplit($pEvW, $pSdlW);
}
private function CalculateBatteryDistribution(float $pEvW, float $pSdlW, array $plan): array
{
$batteries = $plan["bats"] ?? [];
if (empty($batteries)) {
return [];
}
$virtSDL = (float)$this->GetBufferSafe("Int_E_SDL_kWh");
$virtEV = (float)$this->GetBufferSafe("Int_E_EV_kWh");
$sdlTotal = (float)($plan["total"]["SDL_kWh_total"] ?? 0.0);
$evTotal = (float)($plan["total"]["EV_kWh_total"] ?? 0.0);
$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;
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;
}
if (abs($targetPower) < 0.01) {
return $result;
}
$isCharge = ($targetPower > 0.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 = max(0.0, (float)($bat[$limitKey] ?? 0.0) * 1000.0);
$groups[$soc][] = [
"idx" => $bat["idx"],
"maxW" => $maxW
];
}
if ($isCharge) {
ksort($groups);
} else {
krsort($groups);
}
$remaining = $absPower;
foreach ($groups as $groupBatteries) {
if ($remaining <= 0.01) {
break;
}
$groupTotal = 0.0;
foreach ($groupBatteries as $gb) {
$groupTotal += $gb["maxW"];
}
if ($groupTotal <= 0.0) {
continue;
}
$powerForGroup = min($remaining, $groupTotal);
$ratio = $powerForGroup / $groupTotal;
foreach ($groupBatteries as $gb) {
$result[$gb["idx"]] = $gb["maxW"] * $ratio;
}
$remaining -= $powerForGroup;
}
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"];
$totalW = ($evDistribution[$idx] ?? 0.0) + ($sdlDistribution[$idx] ?? 0.0);
$finalOutput[] = [
"idx" => $idx,
"typ" => (string)$bat["typ"],
"chargeW" => ($totalW > 0.0) ? round(abs($totalW), 0) : 0,
"dischargeW" => ($totalW < 0.0) ? round(abs($totalW), 0) : 0
];
}
return $finalOutput;
}
private function UpdateActualPowerSplit(float $pEvW, float $pSdlW): void
{
$totalPowerIst = $this->GetTotalBatteryPowerIstW();
$eps = 0.01;
$sumSoll = $pEvW + $pSdlW;
if (abs($sumSoll) > $eps) {
$factor = $totalPowerIst / $sumSoll;
$rawEV = $pEvW * $factor;
$rawSDL = $pSdlW * $factor;
} else {
if (abs($totalPowerIst) < 50.0) {
$rawEV = $pEvW;
$rawSDL = $pSdlW;
} else {
$rawEV = $pEvW;
$rawSDL = $totalPowerIst - $rawEV;
}
}
$filterAktiv = $this->ReadPropertyBoolean("FilterAktiv");
if ($filterAktiv) {
$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->ClearCurrentFilterBuffers();
}
$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)) {
$this->SetBuffer("CUR_{$ch}_HITS", "0");
return $lastVal;
}
$tolW = max(50.0, $tolW);
if (abs($raw - $pending) <= $tolW) {
$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) {
$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));
$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", 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 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 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;
}
return max(0.0, min(100.0, (float)$v));
}
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 ResetVirtualAccount(string $account): void
{
$plan = json_decode($this->GetBufferSafe("BatPlanJSON"), true);
if (!is_array($plan)) {
$this->BuildBatteryPlan(true);
$plan = json_decode($this->GetBufferSafe("BatPlanJSON"), true);
}
if (!is_array($plan) || empty($plan["total"])) {
return;
}
if ($account === "SDL") {
$total = (float)($plan["total"]["SDL_kWh_total"] ?? 0.0);
$startPct = 50.0;
$this->SetBuffer("Int_E_SDL_kWh", (string)($total * $startPct / 100.0));
$this->SetBuffer("Int_LastTs", (string)microtime(true));
return;
}
if ($account === "EV") {
$total = (float)($plan["total"]["EV_kWh_total"] ?? 0.0);
$startPct = 50.0;
$this->SetBuffer("Int_E_EV_kWh", (string)($total * $startPct / 100.0));
$this->SetBuffer("Int_LastTs", (string)microtime(true));
return;
}
}
private function RoundArray($data, int $decimals = 3)
{
if (is_array($data)) {
foreach ($data as $k => $v) {
$data[$k] = $this->RoundArray($v, $decimals);
}
return $data;
}
if (is_float($data)) {
return round($data, $decimals);
}
return $data;
}
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);
}
}
?>