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); $this->RegisterPropertyFloat("FilterTolerancePct", 15.0); // % $this->RegisterPropertyFloat("FilterRampWPerSec", 2000.0); // W/s $this->RegisterPropertyInteger("FilterHits", 1); // 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->RegisterVariableFloat("Aktuelle_Leistung_Batterien", "Aktuelle Leistung Batterien", "", 38); $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)); $totalBat = $this->GetTotalBatteryPowerIstW(); $this->SetIdentValue("Aktuelle_Leistung_Batterien", round($totalBat, 0)); } 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)); $minPhysicalSocPct = max(0.0, min(100.0, (float)($b["minPhysicalSocPct"] ?? 5.0))); $minPhysicalKWh = $capKWh * $minPhysicalSocPct / 100.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); $sdlLowerKWh = $sdlShareKW_entladen * $reserveH; $sdlUpperKWh = $sdlShareKW_laden * $reserveH; $underKWh = $minPhysicalKWh + $sdlLowerKWh; $upKWh = $capKWh - $sdlUpperKWh; $underKWh = max($minPhysicalKWh, min($capKWh, $underKWh)); $upKWh = max($minPhysicalKWh, 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; } $SDL_kWh = max(0.0, $sdlLowerKWh + $sdlUpperKWh); $EV_kWh = max(0.0, $upKWh - $underKWh); $realSocPct = $this->ReadSocPercent($socVarId); $realKWh = $capKWh * $realSocPct / 100.0; $canSDLDischarge = ($realKWh >= $minPhysicalKWh); $canEVDischarge = ($realKWh > $underKWh); $effectiveSDLDischargeKW = $canSDLDischarge ? $sdlShareKW_entladen : 0.0; $effectiveEVDischargeKW = $canEVDischarge ? $evShareKW_entladen : 0.0; $bat = [ "idx" => $idx, "typ" => $typ, "socVarId" => $socVarId, "capKWh" => $capKWh, "pBatW" => $pBatW, "minPhysicalSocPct" => $minPhysicalSocPct, "minPhysicalKWh" => $minPhysicalKWh, "sdlLowerKWh" => $sdlLowerKWh, "sdlUpperKWh" => $sdlUpperKWh, "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" => $effectiveSDLDischargeKW, "EV_Charge_kW" => $evShareKW_laden, "EV_Discharge_kW" => $effectiveEVDischargeKW, // 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"] += $effectiveSDLDischargeKW; $plan["total"]["EV_Charge_kW"] += $evShareKW_laden; $plan["total"]["EV_Discharge_kW"] += $effectiveEVDischargeKW; } $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 { // KEIN Sprung mehr auf Soll! $rawEV = (float)$this->GetBufferSafe("CUR_EV_VAL"); $rawSDL = (float)$this->GetBufferSafe("CUR_SDL_VAL"); } $filterAktiv = $this->ReadPropertyBoolean("FilterAktiv"); if ($filterAktiv) { $tolPct = max(0.0, (float)$this->ReadPropertyFloat("FilterTolerancePct")) / 100.0; $needHits = max(1, (int)$this->ReadPropertyInteger("FilterHits")); $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"); $lastTs = (float)$this->GetBufferSafe("CUR_{$ch}_TS"); $now = microtime(true); if ($this->GetBufferSafe("CUR_{$ch}_INIT") !== "1") { $lastVal = $raw; $pending = $target; $hits = 0; $lastTs = $now; $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); $this->SetBuffer("CUR_{$ch}_TS", (string)$lastTs); return $lastVal; } $dt = max(0.0, min(5.0, $now - $lastTs)); $this->SetBuffer("CUR_{$ch}_TS", (string)$now); $maxRampWPerSec = max(100.0, (float)$this->ReadPropertyFloat("FilterRampWPerSec")); if (abs($target) < 0.5) { $lastVal = $this->MoveTowards($lastVal, 0.0, $maxRampWPerSec * $dt); if (abs($lastVal) < 1.0) { $lastVal = 0.0; } $this->SetBuffer("CUR_{$ch}_VAL", (string)$lastVal); $this->SetBuffer("CUR_{$ch}_PEND", (string)$target); $this->SetBuffer("CUR_{$ch}_HITS", "0"); return $lastVal; } if (abs($target - $pending) > 0.5) { $pending = $target; $hits = 0; } if (($pending > 0.0 && $raw < -50.0) || ($pending < 0.0 && $raw > 50.0)) { $this->SetBuffer("CUR_{$ch}_VAL", (string)$lastVal); $this->SetBuffer("CUR_{$ch}_PEND", (string)$pending); $this->SetBuffer("CUR_{$ch}_HITS", "0"); return $lastVal; } $tolW = max(100.0, $tolW); if (abs($raw - $pending) <= $tolW) { $hits++; if ($hits >= $needHits) { $lastVal = $this->MoveTowards($lastVal, $raw, $maxRampWPerSec * $dt); $hits = 0; } } else { // Rohwert passt nicht zum Sollwert: // NICHT Richtung Soll laufen, sondern alten Wert halten. $hits = 0; } if (abs($lastVal) < 1.0) { $lastVal = 0.0; } $this->SetBuffer("CUR_{$ch}_VAL", (string)$lastVal); $this->SetBuffer("CUR_{$ch}_PEND", (string)$pending); $this->SetBuffer("CUR_{$ch}_HITS", (string)$hits); 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", ""); $this->SetBuffer("CUR_{$ch}_TS", ""); } } 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 MoveTowards(float $current, float $target, float $maxStep): float { $maxStep = max(0.0, $maxStep); if (abs($target - $current) <= $maxStep) { return $target; } return $current + (($target > $current) ? $maxStep : -$maxStep); } 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); } } ?>