<?php

/**
 * Bat_EV_SDL_V4
 *
 * Zweck des Moduls:
 * Dieses IPS-Modul verteilt Lade- und Entlade-Sollwerte auf mehrere Batteriesysteme.
 * Dabei werden zwei virtuelle Energiekonten geführt:
 *
 * - SDL: reservierter Bereich für SDL-Leistung
 * - EV: restlicher nutzbarer Bereich für EV-Leistung
 *
 * Zusätzlich werden reale Batterie-SoC-Werte gelesen, um Lade- und Entladegrenzen
 * dynamisch zu berücksichtigen.
 *
 * Vorzeichenlogik in diesem Modul:
 * - Positive Leistung = Laden
 * - Negative Leistung = Entladen
 *
 * Achtung:
 * Das Modul führt aktuell nur globale virtuelle SDL-/EV-Konten.
 * Die virtuelle Energie wird also nicht separat pro Batterie geführt.
 */
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);
        $this->RegisterPropertyFloat("FilterTolerancePct", 15.0); // %
        $this->RegisterPropertyFloat("FilterRampWPerSec", 2000.0); // W/s
        $this->RegisterPropertyInteger("FilterHits", 1);
        $this->RegisterPropertyFloat("EV_Recalc_IntervalHours", 6.0);
        $this->RegisterPropertyFloat("EV_Recalc_TolerancePct", 2.0);

        // 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
    );
    // Bei Config-Änderung wird der statische Plan neu aufgebaut.
    $this->BuildBatteryPlan(true);

    // Kein Reset bei ApplyChanges.
    // Nur Zeitstempel setzen, falls noch keiner vorhanden ist.
    if ($this->GetBufferSafe("Int_LastTs") === "") {
        $this->SetBuffer("Int_LastTs", (string)microtime(true));
    }

    //$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);
    }
    /*
     * 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;
        
        // Verhindert, dass ein Timerlauf den vorherigen überholt.
        if (!IPS_SemaphoreEnter($semKey, 5000)) {
            //$this->SendDebug("Update", "SKIP - Semaphore locked", 0);
            return;
        }

        try {
            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);
             // Reale SoC-Werte ändern sich laufend, daher werden Leistungsgrenzen aktualisiert.
            $plan = $this->RefreshDynamicPlanValues($plan);

            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);
            // 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));
            $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));
            
            // Reale Gesamtleistung aller Batterien anzeigen.
            $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 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
    {
        $batteriesRaw = $this->ReadPropertyString("Batteries");

        $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,
            "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));

            // 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;

            if ($capKWh <= 0.0 || $pBatKW <= 0.0) {
                continue;
            }

            $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));

            // 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;

             // Dynamische Freigaben auf Basis des realen SoC.
            $canSDLDischarge =
            ($realKWh >= $minPhysicalKWh);

            $canEVDischarge =
                ($realKWh > $underKWh);

            $canEVCharge =
            ($realKWh < $upKWh);

            $effectiveSDLChargeKW =
                $sdlShareKW_laden;

            $effectiveEVChargeKW =
                $canEVCharge ? $evShareKW_laden : 0.0;

            $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" => $effectiveSDLChargeKW,
                "SDL_Discharge_kW" => $effectiveSDLDischargeKW,
                "EV_Charge_kW" => $effectiveEVChargeKW,
                "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"] += $effectiveSDLChargeKW;
            $plan["total"]["SDL_Discharge_kW"] += $effectiveSDLDischargeKW;
            $plan["total"]["EV_Charge_kW"] += $effectiveEVChargeKW;
            $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);

        $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);
    }


    /**
     * Setzt die virtuellen Konten auf die konfigurierten Startwerte.
     * Wird über den WebFront-Schalter Virtual_Reset ausgelöst.
     */
    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")));

        // 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));
        $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);
    }


    /**
     * Integriert die virtuellen SDL-/EV-Konten.
     *
     * Formel grob:
     * Energieänderung kWh = Leistung kW * Zeit h
     */
    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);

        if (
        $this->GetBufferSafe("Int_Init") !== "1" ||
        $this->GetBufferSafe("Int_E_SDL_kWh") === "" ||
        $this->GetBufferSafe("Int_E_EV_kWh") === ""
    ) {
        $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")));

        $lastSdlPct = (float)GetValue($this->GetIDForIdent("SDL_Pos"));
        $lastEvPct  = (float)GetValue($this->GetIDForIdent("SoC_EV"));

        $lastSdlPct = max(0.0, min(100.0, $lastSdlPct));
        $lastEvPct  = max(0.0, min(100.0, $lastEvPct));

        $eSDL = ($sdlTotal > 0.0) ? $sdlTotal * $lastSdlPct / 100.0 : 0.0;
        $eEV  = ($evTotal > 0.0)  ? $evTotal  * $lastEvPct  / 100.0 : 0.0;
        
        
        $this->SetBuffer("Int_E_SDL_kWh", (string)$eSDL);
        $this->SetBuffer("Int_E_EV_kWh",  (string)$eEV);
        $this->SetBuffer("Int_LastTs", (string)$now);
        $this->SetBuffer("Int_Init", "1");

        $lastTs = $now;
        $dtSec = 0.0;
        $dtH = 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;

        // EV alle X Stunden anhand realem SoC nachkalibrieren
        $this->MaybeRecalculateEVFromPhysical($plan, $eSDL, $eEV, $sdlTotal, $evTotal);

        $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)
        ];
    }
    /**
     * Liest EV-/SDL-Sollwerte, verteilt sie auf die Batterien und schreibt die Register.
     */
    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);

        // 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
    {
        $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;
                }

                // Innerhalb einer Gruppe proportional zur möglichen Leistung verteilen.
                $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;
    }

    /**
     * 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();

        $eps = 0.01;
        $sumSoll = $pEvW + $pSdlW;

        if (abs($sumSoll) > $eps) {
            $factor = $totalPowerIst / $sumSoll;
            $rawEV = $pEvW * $factor;
            $rawSDL = $pSdlW * $factor;
        } else {
            // 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");
        }

        $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);
    }
   /**
     * 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");
        $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;
    }
    /**
     * Löscht Filterzustände, wenn der Filter deaktiviert wird.
     */
    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", "");
        }
    }

    /**
     * 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));

        $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);
        }
    }

    /**
     * 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);

        $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));
            }
        };
        */

        $setInt = function (int $varId, int $value): void {
        if ($varId > 0 && IPS_VariableExists($varId)) {
            $old = (int)GetValue($varId);

            if ($old !== $value) {
                RequestAction($varId, $value);
            }
            }
        };

        $setW = function (int $varId, float $w): void {
            if ($varId > 0 && IPS_VariableExists($varId)) {
                $new = (int)round(max(0.0, $w), 0);
                $old = (int)GetValue($varId);

                if (abs($old - $new) > 50) {
                    RequestAction($varId, $new);
                }
            }
        };

        $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);
    }
    /**
     * 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);
        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;
    }

    /**
     * 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)) {
            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));
    }
   /**
     * 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);
        $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));
    }

   /**
     * 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)) {
            foreach ($data as $k => $v) {
                $data[$k] = $this->RoundArray($v, $decimals);
            }
            return $data;
        }

        if (is_float($data)) {
            return round($data, $decimals);
        }

        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);

        if (abs($target - $current) <= $maxStep) {
            return $target;
        }

        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
    {
        if ($evTotal <= 0.0 || $sdlTotal <= 0.0) {
            return;
        }

        $intervalHours = max(0.0, (float)$this->ReadPropertyFloat("EV_Recalc_IntervalHours"));

        if ($intervalHours <= 0.0) {
            return;
        }

        $now = microtime(true);

        $lastCheckTs = (float)$this->GetBufferSafe("EV_Recalc_LastCheckTs");

        // Nur alle X Stunden prüfen
        if (
            $lastCheckTs > 0.0 &&
            ($now - $lastCheckTs) < ($intervalHours * 3600.0)
        ) {
            return;
        }

        $this->SetBuffer("EV_Recalc_LastCheckTs", (string)$now);

        // SDL muss bei 50% sein
        $sdlPct = ($eSDL / $sdlTotal) * 100.0;

        // SDL muss im neutralen Bereich sein.
        // Zwischen 50% und 60% ist die EV-Rückrechnung erlaubt.
        if ($sdlPct < 50.0 || $sdlPct > 51.0) {
            $this->SendDebug(
                "EV_Recalc",
                "Skip: SDL nicht im Bereich 50-60%, aktuell=" . round($sdlPct, 3) . "%",
                0
            );
            return;
        }

        $newEVkWh = 0.0;

        foreach (($plan["bats"] ?? []) as $bat) {

            $capKWh = (float)($bat["capKWh"] ?? 0.0);
            $socVarId = (int)($bat["socVarId"] ?? 0);

            $realSocPct = $this->ReadSocPercent($socVarId);
            $realKWh = $capKWh * $realSocPct / 100.0;

            $underKWh = (float)($bat["underKWh"] ?? 0.0);

            // Maximales EV Fenster dieser Batterie
            $evBatTotal = (float)($bat["EV_kWh_total"] ?? 0.0);

            // EV Anteil relativ zur unteren Grenze
            $evPart = $realKWh - $underKWh;

            // Begrenzung:
            // unter underKWh => 0
            // über upKWh => max EV Fenster
            $evPart = max(0.0, min($evBatTotal, $evPart));

            $newEVkWh += $evPart;
        }

        // Global begrenzen
        $newEVkWh = max(0.0, min($evTotal, $newEVkWh));

        $newEVpct = ($evTotal > 0.0)
            ? ($newEVkWh / $evTotal * 100.0)
            : 0.0;

        $oldEVpct = ($evTotal > 0.0)
            ? ($eEV / $evTotal * 100.0)
            : 0.0;

        $tolPct = max(0.0, (float)$this->ReadPropertyFloat("EV_Recalc_TolerancePct"));

        // Nur übernehmen wenn Differenz gross genug
        if (abs($newEVpct - $oldEVpct) <= $tolPct) {

            $this->SendDebug(
                "EV_Recalc",
                "Skip: Differenz innerhalb Toleranz. Alt=" .
                round($oldEVpct, 3) .
                "% Neu=" .
                round($newEVpct, 3) .
                "%",
                0
            );

            return;
        }

        $eEV = $newEVkWh;

        $this->SendDebug(
            "EV_Recalc",
            "EV neu berechnet. Alt=" .
            round($oldEVpct, 3) .
            "% Neu=" .
            round($newEVpct, 3) .
            "%",
            0
        );
    }

    /**
     * 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) {
            $socVarId = (int)($bat["socVarId"] ?? 0);
            $capKWh = (float)($bat["capKWh"] ?? 0.0);

            $realSocPct = $this->ReadSocPercent($socVarId);
            $realKWh = $capKWh * $realSocPct / 100.0;

            $minPhysicalKWh = (float)($bat["minPhysicalKWh"] ?? 0.0);
            $underKWh = (float)($bat["underKWh"] ?? 0.0);
            $upKWh = (float)($bat["upKWh"] ?? 0.0);

            $bat["real_SOC_pct"] = round($realSocPct, 3);
            $bat["real_kWh"] = round($realKWh, 3);

            $bat["SDL_Discharge_kW"] =
                ($realKWh >= $minPhysicalKWh)
                    ? (float)$bat["sdlShareKW_entladen"]
                    : 0.0;

            $bat["EV_Discharge_kW"] =
                ($realKWh > $underKWh)
                    ? (float)$bat["evShareKW_entladen"]
                    : 0.0;

            $bat["EV_Charge_kW"] =
                ($realKWh < $upKWh)
                    ? (float)$bat["evShareKW_laden"]
                    : 0.0;

            $bat["SDL_Charge_kW"] =
                (float)$bat["sdlShareKW_laden"];
        }
        unset($bat);

        $plan["total"]["SDL_Charge_kW"] = 0.0;
        $plan["total"]["SDL_Discharge_kW"] = 0.0;
        $plan["total"]["EV_Charge_kW"] = 0.0;
        $plan["total"]["EV_Discharge_kW"] = 0.0;

        foreach ($plan["bats"] as $bat) {
            $plan["total"]["SDL_Charge_kW"] += (float)$bat["SDL_Charge_kW"];
            $plan["total"]["SDL_Discharge_kW"] += (float)$bat["SDL_Discharge_kW"];
            $plan["total"]["EV_Charge_kW"] += (float)$bat["EV_Charge_kW"];
            $plan["total"]["EV_Discharge_kW"] += (float)$bat["EV_Discharge_kW"];
        }

        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);
        if ($id <= 0) {
            //$this->SendDebug(__FUNCTION__, "Ident nicht gefunden: $ident", 0);
            return;
        }
        SetValue($id, $value);
    }
}

?>
