From 327f99228034ca13f426870094e9c8010b0e3f5e Mon Sep 17 00:00:00 2001 From: "belevo\\mh" Date: Tue, 12 May 2026 13:53:52 +0200 Subject: [PATCH] no message --- Bat_EV_SDL_V4/module.php | 221 ++++++++++++++++++++++++++++++++++----- 1 file changed, 192 insertions(+), 29 deletions(-) diff --git a/Bat_EV_SDL_V4/module.php b/Bat_EV_SDL_V4/module.php index 533e0ab..fef5702 100644 --- a/Bat_EV_SDL_V4/module.php +++ b/Bat_EV_SDL_V4/module.php @@ -1,5 +1,26 @@ 0) ? $intervalSec * 1000 : 0 ); - + // Bei Config-Änderung wird der statische Plan neu aufgebaut. $this->BuildBatteryPlan(true); // Kein Reset bei ApplyChanges. @@ -102,11 +123,24 @@ class Bat_EV_SDL_V4 extends IPSModule throw new Exception("Invalid Ident: " . $Ident); } - + /* + * Update() ist die zentrale zyklische Berechnung. + * + * Ablauf: + * 1. Semaphore sichern, damit keine parallelen Updates laufen. + * 2. Prüfen, ob Modul aktiv ist. + * 3. Batterieplan laden/aktualisieren. + * 4. Sollwerte auf Batterien verteilen. + * 5. Aktuelle EV-/SDL-Leistung rückrechnen. + * 6. Virtuelle Konten integrieren. + * 7. Diagnosewerte schreiben. + */ public function Update() { - $semKey = 'BatEVSDL_Update_' . $this->InstanceID; + $semKey = 'BatEVSDL_Update_' . $this->InstanceID; + + // Verhindert, dass ein Timerlauf den vorherigen überholt. if (!IPS_SemaphoreEnter($semKey, 5000)) { //$this->SendDebug("Update", "SKIP - Semaphore locked", 0); return; @@ -116,11 +150,12 @@ class Bat_EV_SDL_V4 extends IPSModule if (!GetValue($this->GetIDForIdent("State"))) { return; } - + // Baut den Plan nur neu, wenn sich relevante Config-Werte geändert haben. $this->BuildBatteryPlan(false); - $plan = json_decode($this->GetBufferSafe("BatPlanJSON"), true); + $plan = json_decode($this->GetBufferSafe("BatPlanJSON"), true); + // Reale SoC-Werte ändern sich laufend, daher werden Leistungsgrenzen aktualisiert. $plan = $this->RefreshDynamicPlanValues($plan); if (!is_array($plan) || empty($plan["bats"])) { @@ -133,10 +168,10 @@ class Bat_EV_SDL_V4 extends IPSModule // Danach EV und SDL virtuell integrieren. $virt = $this->IntegrateVirtualAccounts($plan); - + // Virtuelle Prozentwerte anzeigen. $this->SetIdentValue("SDL_Pos", round($virt["SDL_pct"], 3)); $this->SetIdentValue("SoC_EV", round($virt["EV_pct"], 3)); - + // Aktuelle dynamische Maximalleistungen anzeigen. $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)); @@ -152,7 +187,8 @@ class Bat_EV_SDL_V4 extends IPSModule $calcRounded = $this->RoundArray($calc, 3); $this->SetIdentValue("CalcJSON", json_encode($calcRounded, JSON_PRETTY_PRINT)); - + + // Reale Gesamtleistung aller Batterien anzeigen. $totalBat = $this->GetTotalBatteryPowerIstW(); $this->SetIdentValue("Aktuelle_Leistung_Batterien", round($totalBat, 0)); @@ -165,8 +201,18 @@ class Bat_EV_SDL_V4 extends IPSModule } /** - * Baut die statischen Batterie-Fenster und Leistungsanteile. - * Diese Methode enthält die komplette Grenzberechnung und läuft nur bei Config-Änderung neu. + * Baut den Batterieplan. + * + * Der Plan enthält pro Batterie: + * - Kapazität + * - maximale Leistung + * - reservierte SDL-Fenster unten/oben + * - nutzbares EV-Fenster + * - mögliche Lade-/Entladeleistungen + * + * Warum als Plan? + * Die statischen Werte müssen nicht in jedem Timerlauf komplett neu berechnet werden. + * Über einen Hash wird erkannt, ob die Konfiguration geändert wurde. */ private function BuildBatteryPlan(bool $force): void { @@ -175,8 +221,11 @@ class Bat_EV_SDL_V4 extends IPSModule $sdlTotalW_laden = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Laden")); $sdlTotalW_entladen = max(0, (int)$this->ReadPropertyInteger("SDL_Leistung_Entladen")); + //Reserve Zeit, zb 30 oder 45 min $reserveH = max(0.0, (float)$this->ReadPropertyFloat("ReserveHours")); + // Hash über alle statischen Eingaben. + // Wenn dieser gleich bleibt, muss der Plan nicht neu aufgebaut werden. $hash = md5(json_encode([ "Batteries" => $batteriesRaw, "SDL_W_Laden" => $sdlTotalW_laden, @@ -235,6 +284,9 @@ class Bat_EV_SDL_V4 extends IPSModule $pBatW = max(0.0, (float)($b["powerbat"] ?? 0)); $pBatKW = $pBatW / 1000.0; $capKWh = max(0.0, (float)($b["capazity"] ?? 0)); + + // Physischer Mindest-SoC, unter den EV nicht entladen soll. + // SDL darf bis zu diesem Mindestwert herunter. $minPhysicalSocPct = max(0.0, min(100.0, (float)($b["minPhysicalSocPct"] ?? 5.0))); $minPhysicalKWh = $capKWh * $minPhysicalSocPct / 100.0; @@ -244,19 +296,25 @@ class Bat_EV_SDL_V4 extends IPSModule $typ = (string)($b["typ"] ?? ("Bat " . ($idx + 1))); $socVarId = (int)($b["soc"] ?? 0); - + + // SDL-Leistung wird proportional zur Batterie-Nennleistung verteilt. $sdlShareKW_laden = ($sumBatPowerKW > 0.0) ? ($sdlTotalKW_laden / $sumBatPowerKW * $pBatKW) : 0.0; $sdlShareKW_entladen = ($sumBatPowerKW > 0.0) ? ($sdlTotalKW_entladen / $sumBatPowerKW * $pBatKW) : 0.0; + // EV bekommt den Rest der Batterie-Leistung. $evShareKW_laden = max(0.0, $pBatKW - $sdlShareKW_laden); $evShareKW_entladen = max(0.0, $pBatKW - $sdlShareKW_entladen); + // SDL-Reserve in kWh. $sdlLowerKWh = $sdlShareKW_entladen * $reserveH; $sdlUpperKWh = $sdlShareKW_laden * $reserveH; + // EV darf nur zwischen underKWh und upKWh arbeiten. + // Darunter liegt SDL-Entlade-Reserve, darüber SDL-Lade-Reserve. $underKWh = $minPhysicalKWh + $sdlLowerKWh; $upKWh = $capKWh - $sdlUpperKWh; + // Grenzen sauber in die Batteriekapazität klemmen. $underKWh = max($minPhysicalKWh, min($capKWh, $underKWh)); $upKWh = max($minPhysicalKWh, min($capKWh, $upKWh)); @@ -273,6 +331,7 @@ class Bat_EV_SDL_V4 extends IPSModule $realSocPct = $this->ReadSocPercent($socVarId); $realKWh = $capKWh * $realSocPct / 100.0; + // Dynamische Freigaben auf Basis des realen SoC. $canSDLDischarge = ($realKWh >= $minPhysicalKWh); @@ -340,6 +399,7 @@ class Bat_EV_SDL_V4 extends IPSModule $plan["total"]["EV_Discharge_kW"] += $effectiveEVDischargeKW; } + // Plan im Buffer speichern, damit Update() schnell darauf zugreifen kann. $this->SetBuffer("BatPlanHash", $hash); $this->SetBuffer("BatPlanJSON", json_encode($plan)); $this->SetBuffer("Int_CFG_HASH", $hash); @@ -350,14 +410,11 @@ class Bat_EV_SDL_V4 extends IPSModule //$this->SendDebug("Plan", "Battery plan rebuilt (" . count($plan["bats"]) . " bats)", 0); } - private function InitVirtualAccountsIfNeeded(): void - { - if ($this->GetBufferSafe("Int_Init") !== "1") { - $this->SetBuffer("Int_LastTs", (string)microtime(true)); - $this->SetBuffer("Int_Init", "1"); - } - } + /** + * Setzt die virtuellen Konten auf die konfigurierten Startwerte. + * Wird über den WebFront-Schalter Virtual_Reset ausgelöst. + */ public function ResetVirtualAccounts(): void { $this->BuildBatteryPlan(false); @@ -373,6 +430,7 @@ class Bat_EV_SDL_V4 extends IPSModule $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"))); + // Prozentwerte in kWh umrechnen und im Buffer speichern. $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)); @@ -384,6 +442,13 @@ class Bat_EV_SDL_V4 extends IPSModule $this->SendDebug("Virtual_Reset", "SDL=" . round($sdlStartPct, 3) . "%, EV=" . round($evStartPct, 3) . "%", 0); } + + /** + * Integriert die virtuellen SDL-/EV-Konten. + * + * Formel grob: + * Energieänderung kWh = Leistung kW * Zeit h + */ private function IntegrateVirtualAccounts(array $plan): array { $now = microtime(true); @@ -472,7 +537,9 @@ class Bat_EV_SDL_V4 extends IPSModule "dtSec" => round($dtSec, 3) ]; } - + /** + * Liest EV-/SDL-Sollwerte, verteilt sie auf die Batterien und schreibt die Register. + */ public function ApplySetpoints(?array $plan = null): void { if ($plan === null) { @@ -487,8 +554,22 @@ class Bat_EV_SDL_V4 extends IPSModule $distribution = $this->CalculateBatteryDistribution($pEvW, $pSdlW, $plan); $this->WriteBatteryPowerSetpoints($distribution); + + // Nach dem Schreiben wird aus der realen Batterie-Istleistung berechnet, + // welcher Anteil EV und welcher SDL zugeordnet wird. $this->UpdateActualPowerSplit($pEvW, $pSdlW); } + + /** + * Verteilt die gewünschte EV- und SDL-Leistung auf einzelne Batterien. + * + * Dabei wird geladen bevorzugt bei niedrigem virtuellem SoC, + * entladen bevorzugt bei hohem virtuellem SoC. + * + * Einschränkung: + * Da aktuell globaler virtueller SoC verwendet wird, haben alle Batterien denselben + * virtuellen SoC. Die Sortierung bringt deshalb nur begrenzt einen Effekt. + */ private function CalculateBatteryDistribution(float $pEvW, float $pSdlW, array $plan): array { @@ -560,6 +641,7 @@ class Bat_EV_SDL_V4 extends IPSModule continue; } + // Innerhalb einer Gruppe proportional zur möglichen Leistung verteilen. $powerForGroup = min($remaining, $groupTotal); $ratio = $powerForGroup / $groupTotal; @@ -598,6 +680,14 @@ class Bat_EV_SDL_V4 extends IPSModule return $finalOutput; } + /** + * Rechnet die reale Batterie-Gesamtleistung auf EV und SDL zurück. + * + * Beispiel: + * EV-Soll = 3000 W, SDL-Soll = 1000 W, Summe = 4000 W. + * Batterie liefert real nur 3600 W. + * Dann wird EV = 2700 W, SDL = 900 W angenommen. + */ private function UpdateActualPowerSplit(float $pEvW, float $pSdlW): void { $totalPowerIst = $this->GetTotalBatteryPowerIstW(); @@ -610,7 +700,8 @@ class Bat_EV_SDL_V4 extends IPSModule $rawEV = $pEvW * $factor; $rawSDL = $pSdlW * $factor; } else { - // KEIN Sprung mehr auf Soll! + // Wenn kein Sollwert vorhanden ist, nicht plötzlich auf Soll springen. + // Stattdessen letzten gefilterten Wert verwenden. $rawEV = (float)$this->GetBufferSafe("CUR_EV_VAL"); $rawSDL = (float)$this->GetBufferSafe("CUR_SDL_VAL"); } @@ -630,7 +721,15 @@ class Bat_EV_SDL_V4 extends IPSModule $this->SetIdentValue("Aktuelle_Leistung_EV", (float)$fEV); $this->SetIdentValue("Aktuelle_Leistung_SDL", (float)$fSDL); } - + /** + * Filtert die rückgerechnete Istleistung. + * + * Zweck: + * - Fehlmessungen abfangen + * - träge Batterieantwort glätten + * - harte Sprünge vermeiden + * - falsche Vorzeichen ignorieren + */ private function FilterCurrent(string $ch, float $raw, float $target, float $tolW, int $needHits): float { $lastVal = (float)$this->GetBufferSafe("CUR_{$ch}_VAL"); @@ -712,7 +811,9 @@ class Bat_EV_SDL_V4 extends IPSModule return $lastVal; } - + /** + * Löscht Filterzustände, wenn der Filter deaktiviert wird. + */ private function ClearCurrentFilterBuffers(): void { foreach (["EV", "SDL"] as $ch) { @@ -724,6 +825,9 @@ class Bat_EV_SDL_V4 extends IPSModule } } + /** + * Schreibt die berechneten Lade-/Entladeleistungen in die Batterie-Register. + */ private function WriteBatteryPowerSetpoints(array $distribution): void { //IPS_LogMessage(__FUNCTION__, "distribution=" . json_encode($distribution, JSON_PRETTY_PRINT)); @@ -756,6 +860,12 @@ class Bat_EV_SDL_V4 extends IPSModule } } + /** + * Schreibt herstellerspezifische Register. + * + * Je nach Batterie/Wechselrichter sind unterschiedliche Moduswerte nötig. + * GoodWe und SolarEdge werden hier speziell behandelt. + */ private function WriteByVendorRegistersSingleMode(string $typ, array $cfg, float $chargeW, float $dischargeW): void { $t = mb_strtolower($typ); @@ -837,7 +947,12 @@ class Bat_EV_SDL_V4 extends IPSModule $setW($varPowerCharge, 0.0); $setW($varPowerDisch, 0.0); } - + /** + * Summiert die reale Batterie-Istleistung. + * + * Die Multiplikation mit -1 dreht das Vorzeichen. + * Offenbar liefern die Batterie-Register ein umgekehrtes Vorzeichen. + */ private function GetTotalBatteryPowerIstW(): float { $cfg = json_decode($this->ReadPropertyString("Batteries"), true); @@ -855,6 +970,14 @@ class Bat_EV_SDL_V4 extends IPSModule return $sum; } + /** + * Liest einen SoC-Wert. + * + * Spezialfall: + * Wenn in der Config direkt eine Zahl von 0 bis 100 steht und keine IPS-Variable + * mit dieser ID existiert, wird diese Zahl als fester SoC interpretiert. + */ + private function ReadSocPercent(int $varId): float { if ($varId >= 0 && $varId <= 100 && !IPS_VariableExists($varId)) { @@ -872,13 +995,18 @@ class Bat_EV_SDL_V4 extends IPSModule return max(0.0, min(100.0, (float)$v)); } - + /** + * Sichere Buffer-Lesefunktion. + * Gibt immer einen String zurück, damit Casts kontrolliert funktionieren. + */ private function GetBufferSafe(string $name): string { $v = $this->GetBuffer($name); return is_string($v) ? $v : ""; } - + /** + * Schreibt bei Fehlern alle relevanten Ausgabewerte auf 0. + */ private function WriteAllZero(string $reason): void { $this->SetIdentValue("SDL_Pos", 0.0); @@ -890,7 +1018,10 @@ class Bat_EV_SDL_V4 extends IPSModule $this->SetIdentValue("CalcJSON", json_encode(["error" => $reason], JSON_PRETTY_PRINT)); } - + /** + * Rundet rekursiv alle Float-Werte in Arrays. + * Wird nur für die lesbare JSON-Diagnose verwendet. + */ private function RoundArray($data, int $decimals = 3) { if (is_array($data)) { @@ -906,7 +1037,10 @@ class Bat_EV_SDL_V4 extends IPSModule return $data; } - + /** + * Bewegt einen Wert begrenzt in Richtung Zielwert. + * Wird im Filter verwendet, damit Werte nicht sprunghaft wechseln. + */ private function MoveTowards(float $current, float $target, float $maxStep): float { $maxStep = max(0.0, $maxStep); @@ -918,6 +1052,25 @@ class Bat_EV_SDL_V4 extends IPSModule return $current + (($target > $current) ? $maxStep : -$maxStep); } +/** + * Synchronisiert das virtuelle EV-Konto gelegentlich mit dem realen Batterie-SoC. + * + * Hintergrund: + * Das virtuelle EV-Konto wird normalerweise nur über Leistung × Zeit integriert. + * Dadurch können mit der Zeit Abweichungen zur realen Batterie entstehen + * (z.B. durch Messfehler, Begrenzungen, Verluste oder Neustarts). + * + * Diese Methode berechnet deshalb den EV-Anteil neu aus den echten Batterie-SoC-Werten. + * + * Wichtig: + * Die Neuberechnung erfolgt nur: + * - alle X Stunden + * - wenn SDL genau bei 50% steht (Neutralpunkt) + * - und nur wenn die Abweichung gross genug ist + * + * Ziel: + * Das virtuelle EV-Konto langfristig wieder an die reale Batterie angleichen. + */ private function MaybeRecalculateEVFromPhysical(array $plan, float $eSDL, float &$eEV, float $sdlTotal, float $evTotal): void { @@ -1025,7 +1178,14 @@ class Bat_EV_SDL_V4 extends IPSModule ); } - + /** + * Aktualisiert die dynamischen Werte im Plan anhand des realen Batterie-SoC. + * + * Warum nötig? + * BuildBatteryPlan() baut hauptsächlich statische Grenzen. + * Der echte Batterie-SoC ändert sich aber laufend. + * Deshalb werden Lade-/Entladefreigaben hier bei jedem Update erneuert. + */ private function RefreshDynamicPlanValues(array $plan): array { foreach ($plan["bats"] as &$bat) { @@ -1076,7 +1236,10 @@ class Bat_EV_SDL_V4 extends IPSModule return $plan; } - + /** + * Hilfsfunktion zum sicheren Schreiben einer Instanzvariable per Ident. + * Wenn die Variable nicht existiert, wird still abgebrochen. + */ private function SetIdentValue(string $ident, $value): void { $id = @$this->GetIDForIdent($ident);