Skip to content
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Skripten / Logik
  4. JavaScript
  5. [TypeSkript] Zendure SolarFlow Steuerung: KI

NEWS

  • Jahresrückblick 2025 – unser neuer Blogbeitrag ist online! ✨
    BluefoxB
    Bluefox
    16
    1
    1.7k

  • Neuer Blogbeitrag: Monatsrückblick - Dezember 2025 🎄
    BluefoxB
    Bluefox
    13
    1
    876

  • Weihnachtsangebot 2025! 🎄
    BluefoxB
    Bluefox
    25
    1
    2.1k

[TypeSkript] Zendure SolarFlow Steuerung: KI

Geplant Angeheftet Gesperrt Verschoben JavaScript
4 Beiträge 2 Kommentatoren 36 Aufrufe 2 Watching
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • S Online
    S Online
    Schimi
    schrieb am zuletzt editiert von Schimi
    #1

    [TypeSkript] Zendure SolarFlow Steuerung: PI-Regelung, Tibber & KI-Prognose

    Ich stelle hier mein TypeScript zur Steuerung des Zendure SolarFlow (2400 AC, etc.) zur Verfügung. Das Skript zielt auf maximale Wirtschaftlichkeit durch Nulleinspeisung, dynamische Strompreise (Tibber) und eine intelligente Ladeplanung ab.

    Es handelt sich um eine typsichere "Full-Control"-Lösung, die weit über einfache Blocklys hinausgeht und Hardware-Eigenheiten (z.B. AC-Mode Latenzen) abfängt.


    🚀 Features

    • PI-Regelung: Schnelle und präzise Nulleinspeisung (Target 0W) mit konfigurierbarer Hysterese und Deadband.
    • Tibber-Integration:
      • Eco-Stop: Entladesperre, wenn der Strompreis unter einer Schwelle (z.B. 23ct) liegt.
      • Strategische Ladung: Lädt den Akku nachts aus dem Netz, wenn der Preis günstig ist und die PV-Prognose für den nächsten Tag nicht ausreicht.
    • KI / Predictive Charging:
      • Lernt das Haus-Verbrauchsprofil der letzten 21 Tage (SQL-basiert).
      • Berechnet den Energiebedarf für die Nacht.
      • Bezieht PV-Forecast (Adapter pvforecast) mit ein.
      • Berücksichtigt Heizbedarf basierend auf Außentemperatur und Wettervorhersage.
    • EVCC-Integration:
      • Erkennt EVCC-Modi (Normal, Entladesperre, Zwangsladung).
      • Verhindert, dass der Haus-Akku das Auto lädt (Entladesperre bei Auto-Ladung).
    • Hardware-Schutz:
      • Beachtet Cooldowns beim Wechsel zwischen Input Mode (Laden) und Output Mode (Entladen).
      • Normalisiert inkonsistente MQTT-Statusmeldungen von Zendure.

    📋 Voraussetzungen

    1. Hardware: Zendure SolarFlow System & ein SmartMeter (z.B. Shelly 3EM/Pro3EM) am Hausanschluss.
    2. Adapter:
      • mqtt (Client-Modus).
      • javascript (TypeScript muss aktiviert sein).
      • sql (oder history) für die KI-Berechnungen.
      • (Optional) tibberlink für Preisdaten.
      • (Optional) pvforecast für Ertragsprognosen.
      • (Optional) wetter.com (oder ähnlich) für Temperaturdaten.

    ⚙️ Konfigurations-Anleitung

    Das Skript ist in zwei Bereiche unterteilt, die angepasst werden müssen: CONFIG und IDs.

    1. Systemeinstellungen (const CONFIG)

    Hier werden die physikalischen Limits und Strategien definiert. Suche im Skript nach const CONFIG.

    const CONFIG: SystemConfig = {
        // Hardware
        BATTERY_CAPACITY_KWH: 8.64,       // Kapazität eures Speichers
        MAX_CHARGE_W: 2400,               // Max Ladeleistung
        MAX_DISCHARGE_W: 2400,            // Max Entladeleistung
    
        // Wirtschaftlichkeit
        MIN_PRICE_FLOOR_CT_KWH: 23,       // Tibber: Unter diesem Preis nicht entladen
        HISTORY_DAYS: 21,                 // Analyse-Zeitraum für die KI
    
        // Regelung
        TARGET_W: 0,                      // Zielwert (0 = Nulleinspeisung)
        REGEL_INTERVALL_MS: 2000,         // Regelgeschwindigkeit (2s empfohlen)
        
        // ... weitere Parameter für Heizung, Tuning etc. sind im Skript kommentiert
    };
    
    

    2. Datenpunkte verknüpfen (const IDs)

    Das Skript benötigt eure spezifischen ioBroker-IDs (Objekt-Pfade). Ihr müsst die Pfade im Bereich const IDs anpassen.

    Wichtig: Damit die KI funktioniert, müssen bestimmte Datenpunkte per SQL-Adapter geloggt werden.

    Variable im Skript Beschreibung Benötigtes Format Logging (SQL)
    netz Aktuelle Leistung am Hausanschluss Watt (Number) Nein (Live)
    soc Akku-Stand Zendure % (0-100) Nein (Live)
    acMode, inputSet... Zendure MQTT Steuerpunkte - Nein
    KI-Datenpunkte:
    batTotalEnergy Gesamtenergie Akku (Lifetime) Wh JA (Nur Änderungen)
    batDischargeTotal Entladene Energie (Lifetime) Wh JA (Nur Änderungen)
    netImportTotalKWh Netzbezug Zählerstand kWh JA (Nur Änderungen)
    netExportTotalKWh Einspeisung Zählerstand kWh JA (Nur Änderungen)
    pvTodayKWh PV-Ertrag Heute (oder Total) kWh JA (Nur Änderungen)
    temperature Außentemperatur °C JA

    Hinweis zum SQL-Logging:
    Stellt im SQL-Adapter für die KI-Datenpunkte (Import, Export, PV, Akku-Total) folgendes ein:

    • Loggen von Änderungen: Aktiviert
    • Minimale Differenz: 0 (oder sehr klein, z.B. 0.001)
    • Vorhaltezeit: Mindestens 1 Monat (Skript nutzt standardmäßig 21 Tage Analyse)

    3. Installation

    1. Neues Skript im "Common"-Ordner des JavaScript-Adapters erstellen.
    2. Typ oben rechts auf TypeScript (TS) stellen.
    3. Code einfügen.
    4. Konfiguration (CONFIG und IDs) im oberen Teil des Skripts anpassen.
    5. Speichern und starten.

    ⚠️ Wichtige Hinweise

    • Zendure MQTT: Zendure erwartet die Limits (inputLimit.set / outputLimit.set) oft zwingend als String, obwohl die Datenpunkte im ioBroker oft als "Number" angelegt sind. Das Skript fängt dies ab Version 7.3.1 automatisch ab, um Warnungen im Log zu vermeiden.
    • Verbrauchsprofil: Nach dem ersten Start dauert es einige Sekunden, bis die KI die SQL-Daten analysiert hat. Wenn ihr noch keine 21 Tage Historie habt, nutzt das Skript Fallback-Werte, lernt aber mit der Zeit dazu.
    • AC-Mode: Das Skript normalisiert die Status-Meldungen von Zendure ("Input", "Input mode", "input"), da diese je nach Firmware variieren können.

    letztes Update: 26.01.2026 - 12:46

    /**
    * Batteriesteuerung Zendure CLUSTER (TypeScript)
    * * CHANGELOG:
    * - v8.0.12-TS: [26.01.2026] - DOCS: Detaillierte Kommentierung aller Config-Bereiche wiederhergestellt (SQL-Settings, Optional-Flags).
    * - v8.0.11-TS: [26.01.2026] - OPTIMIZATION: Log-Spam reduziert (Status-Log vs. Live-Visu getrennt).
    * - v8.0.10-TS: [26.01.2026] - FEATURE: Status-Logs bei Änderung + "Bis wann?"-Anzeige.
    * - v8.0.0-TS: [26.01.2026] - MAJOR: Umbau auf Multi-Unit-Cluster.
    * * KONTEXT:
    * - Hardware: 1-3x Zendure SolarFlow 2400 AC + Shelly 3EM/Pro3EM
    * - Architektur: Master-Controller regelt Netzbezug (PI), Dispatcher verteilt Last auf Units.
    * * ZIELE:
    * - Skalierbarkeit: 1 bis N Batterien ohne Code-Änderung (nur Config).
    * - Effizienz: Bei <100W nur eine Unit aktiv ("Sticky Master").
    * - Balancing: Bei >100W Lastverteilung nach nutzbarem SoC.
    */
    
    // =====================================================================================
    // === 1. USER KONFIGURATION ===
    // =====================================================================================
    
    // Globale Systemeinstellungen
    const SYSTEM_CONFIG = {
       // --- MQTT ---
       // Der Basis-Pfad im ioBroker Objects-Baum, unter dem der Zendure-Adapter/MQTT liegt.
       // WICHTIG: Ohne abschließenden Punkt angeben!
       MQTT_BASE_PATH: "mqtt.2.Zendure",
    
       // --- LOGGING & SYSTEM ---
       INFO_LOGS: true,                  // Zeigt verständliche Statusmeldungen im Log (Empfohlen: true)
       DEBUG: false,                     // Zeigt technische Details zur Fehleranalyse (Nur bei Problemen: true)
       
       // Instanz-ID des SQL-Adapters für historische Abfragen.
       // WICHTIG: Muss installiert und aktiv sein für KI-Funktionen!
       SQL_INSTANCE_ID: 'sql.0',
    
       // --- FEATURE FLAGS ---
       ENABLE_TIBBER: true,              // Aktiviert Preis-Optimierung (braucht Tibber-Datenpunkte)
       ENABLE_EVCC: true,                // Aktiviert externe Steuerung via EVCC-Datenpunkt
       ENABLE_PREDICTIVE_CHARGE: true,   // Aktiviert vorausschauendes Laden basierend auf History & Prognose
       ENABLE_WEATHER_FORECAST: true,    // Nutzt Wetterdaten zur Korrektur des Heizbedarfs
    
       // --- PI-REGLER (Global für Netzbezug) ---
       REGEL_INTERVALL_MS: 2000,         // Wie oft wird geregelt? (2000ms = 2 Sekunden)
       TARGET_W: 0,                      // Zielwert am Netzanschlusspunkt (0 = Nulleinspeisung)
       DEADBAND_W: 30,                   // Totzone: Schwankungen +/- 30W werden ignoriert (schont Hardware)
       HYSTERESIS_SWITCH_W: 200,         // Mindest-Leistungsänderung, bevor der Regler reagiert (verhindert Zittern)
       
       // Tuning (PID-Parameter) - Nur ändern, wenn man weiß, was man tut!
       KP_N: 0.70,                       // Proportional-Anteil (Schnelligkeit)
       KI_N: 0.08,                       // Integral-Anteil (Genauigkeit über Zeit)
       KAW: 0.9,                         // Anti-Windup (Verhindert Übersteuern bei Limits)
       LEAK: 0.998,                      // Integrator-Leak (Verhindert Wegdriften bei Inaktivität)
    
       // --- WIRTSCHAFTLICHKEIT ---
       MIN_PRICE_FLOOR_CT_KWH: 23,       // [ct/kWh] Unter diesem Preis wird NICHT entladen (Akku sparen)
       HISTORY_DAYS: 21,                 // [Tage] Zeitraum für die Analyse des Verbrauchsverhaltens (SQL-Datenbasis)
       
       // --- HEIZUNG / KI ---
       MAX_HEATING_RESERVE_KWH: 6.0,     // [kWh] Wieviel Akku wird bei Kälte maximal für die Nacht reserviert?
       HEATING_FACTOR: 0.5               // Wie stark beeinflusst Kälte den Reserve-Bedarf?
    };
    
    // Cluster-Konfiguration
    const CLUSTER_CONFIG = {
       // [Watt] Ab welcher Gesamtlast sollen mehrere Akkus parallel laufen?
       // Unter diesem Wert läuft nur EIN Akku ("Sticky Master") für bessere Effizienz.
       MIN_LOAD_PARALLEL_W: 100, 
       
       // [%] Wieviel Prozent muss ein anderer Akku "besser" gefüllt sein, 
       // damit er im Low-Load-Bereich neuer Master wird? (Verhindert ständiges Wechseln)
       SOC_SWITCH_HYSTERESIS: 5,
       
       // [ms] Wie lange muss ein Master mindestens laufen, bevor gewechselt werden darf?
       MIN_MASTER_RUNTIME_MS: 300000, // 5 Minuten
    
       // Definition der Einheiten (Batterien)
       UNITS: [
           { 
               id: "BAT_1",                  // Interne ID (frei wählbar, z.B. "Links", "Garage")
               deviceId: "HOA1NPN3N210791",  // PFLICHT: Die ID aus dem MQTT-Topic (z.B. zendure/HOA...)
               enabled: true,                // Schalter zum temporären Deaktivieren einer Unit
               capacityKWh: 8.64,            // Gesamtkapazität dieser Unit (AB1000/2000 Akkus summieren)
               maxChargeW: 2400,             // Hardware-Limit Laden (meist 1200W pro Hub, hier AC gekoppelt?)
               maxDischargeW: 2400,          // Hardware-Limit Entladen
               acModeCooldownMs: 60000       // Zwangspause nach Moduswechsel (Input<->Output)
           },
           // BEISPIEL FÜR ZWEITEN AKKU (einfach auskommentieren/anpassen):
           /*
           { 
               id: "BAT_2", 
               deviceId: "HOA2XXXXXXXXXXX", 
               enabled: true, 
               capacityKWh: 3.84,
               maxChargeW: 2400, 
               maxDischargeW: 2400,
               acModeCooldownMs: 60000
           }
           */// BEISPIEL FÜR DRITTEN AKKU (einfach auskommentieren/anpassen):
           /*
           { 
               id: "BAT_2", 
               deviceId: "HOA2XXXXXXXXXXX", 
               enabled: true, 
               capacityKWh: 3.84,
               maxChargeW: 2400, 
               maxDischargeW: 2400,
               acModeCooldownMs: 60000
           }
           */
       ] as UnitConfig[]
    };
    
    // Zentrale Datenpunkte
    // LEGENDE:
    // [PFLICHT]  = Muss existieren, sonst funktioniert das Skript nicht.
    // [OPTIONAL] = Kann leer ("") gelassen werden, das entsprechende Feature wird dann deaktiviert.
    // [SQL]      = SQL-Logging im ioBroker aktivieren! (Zahnrad beim Datenpunkt -> SQL -> Aktivieren)
    //              Empfohlene SQL-Settings: 
    //              - "Nur Änderungen aufzeichnen": JA
    //              - "Entprellzeit": 1000ms (optional)
    //              - "Aufbewahrungszeit": Mindestens SYSTEM_CONFIG.HISTORY_DAYS (z.B. 1 Monat)
    const GLOBAL_IDS = {
       // [PFLICHT] Aktuelle Leistung am Netzpunkt (Negativ = Einspeisung, Positiv = Bezug)
       netz: "shelly.0.SHEM-3#8CAAB5619A05#1.Total.InstantPower",
       
       // --- Wetter & KI ---
       // [OPTIONAL] [SQL WICHTIG] Außentemperatur. Leer = 10°C Fallback.
       // SQL: Zwingend erforderlich für Heizbedarf-Prognose.
       temperature: "0_userdata.0.zendure.KI.Temperatur_Mittelwert",
       
       // [OPTIONAL] [SQL WICHTIG] Zählerstände für Verbraucher, die NICHT aus dem Akku bedient werden sollen (rausrechnen).
       // SQL: Zwingend erforderlich für Profilbildung.
       heatLoadTotal: "0_userdata.0.zendure.KI.Heizung_Leistung_Tag", // Z.B. Wärmepumpe
       wwLoadTotal: "0_userdata.0.zendure.KI.WW_Leistung_Tag",        // Z.B. Heizstab
       wbTotalKWh: "0_userdata.0.zendure.KI.Wallboxen_Verbrauch",     // Z.B. Auto-Ladung
       
       // [OPTIONAL] Wettervorhersage (Adapter: wetter.com oder daswetter). Leer = Deaktiviert.
       forecastMinTempToday: "0_userdata.0.wetter_com.day_0.temp_min",
       forecastMinTempTomorrow: "0_userdata.0.wetter_com.day_1.temp_min",
       
       // [OPTIONAL] [SQL WICHTIG] PV-Ertrag Heute (kWh). Leer = Deaktiviert PV-Korrektur.
       // SQL: Zwingend erforderlich für Genauigkeits-Berechnung.
       pvTodayKWh: "0_userdata.0.PV-Anlage.Erzeugt_heute",
       
       // --- Tibber (Dynamische Strompreise) ---
       // [OPTIONAL] [SQL WICHTIG] Tibber Link / Pulse Daten (Zählerstände).
       // Wenn keine Tibber-Daten da sind, alle 4 auf "" setzen -> Tibber-Logik deaktiviert.
       // SQL: Zwingend erforderlich für Verbrauchsprofil (Import/Export Deltas).
       netImportTotalKWh: "tibberlink.0.LocalPulse.0.Import_total",
       netExportTotalKWh: "tibberlink.0.LocalPulse.0.Export_total",
       tibberPricesToday: "tibberlink.0.Homes.f760a90a-56ba-45f2-b694-a7a565e72066.PricesToday.json",
       tibberPricesTomorrow: "tibberlink.0.Homes.f760a90a-56ba-45f2-b694-a7a565e72066.PricesTomorrow.json",
       
       // --- Steuerung ---
       // [OPTIONAL] EVCC Modus (1=Normal, 2=Sperre, 3=Zwang). Leer = Immer Normal.
       evccModus: "0_userdata.0.zendure.EVCC_Modus",
       // [OPTIONAL] Urlaubsmodus (true/false). Leer = Inaktiv. Reduziert Grundlast-Annahme.
       vacationMode: "0_userdata.0.Sonstiges.UrlaubsModus_An",
       
       // --- Outputs (Werden vom Skript erstellt) ---
       // Keine manuelle Konfiguration nötig.
       lastPVAccuracy: "0_userdata.0.zendure.KI.lastPVAccuracy",
       consumptionProfile: "0_userdata.0.zendure.KI.ConsumptionProfile",
       clusterTotalSoC: "0_userdata.0.zendure.Cluster.TotalSoC",
       clusterTotalPower: "0_userdata.0.zendure.Cluster.TotalPower",
       clusterStatus: "0_userdata.0.zendure.Cluster.Status"
    };
    
    // --- PV FORECAST ADAPTER ---
    // [OPTIONAL] Datenpunkte des "pvforecast" Adapters.
    // Wenn nicht vorhanden, alle Felder auf "" setzen.
    const PV_FC_IDS = {
       now: "pvforecast.0.summary.energy.now",                   
       rest: "pvforecast.0.summary.energy.nowUntilEndOfDay",     
       total: "pvforecast.0.summary.energy.today",               
       tomorrow: "pvforecast.0.summary.energy.tomorrow"   
    };
    
    
    // =====================================================================================
    // === 2. INTERFACES & TYPES ===
    // =====================================================================================
    
    interface UnitConfig {
       id: string;
       deviceId: string;
       enabled: boolean;
       capacityKWh: number;
       maxChargeW: number;
       maxDischargeW: number;
       acModeCooldownMs: number;
    }
    
    interface UnitRuntimeState {
       soc: number;
       minSoc: number;
       acMode: string;
       usableSoC: number;      
       isAvailable: boolean;   
       canCharge: boolean;     
       lastSwitchTime: number;
       currentTargetW: number; 
       ids: {
           acMode: string; acModeSet: string; inputSet: string; outputSet: string;
           soc: string; minSoc: string;
       }
    }
    
    interface ClusterState {
       totalSoC: number;           
       totalCapacityKWh: number;
       totalUsableKWh: number;     
       virtualChargeLimit: number;    
       virtualDischargeLimit: number; 
       activeMasterId: string | null;
       lastMasterSwitch: number;
    }
    
    interface ControlCache { 
       isTibberChargeNow: boolean; 
       isTibberDischargeNow: boolean; 
       isPriceBelowFloor: boolean; 
       currentPrice: number; 
       predictedChargeKWh: number;
       globalReserveKWh: number;
    }
    
    interface TibberPriceRaw { startsAt: string; total: number; }
    interface TibberPriceProcessed { start: Date; price: number; durationMs: number; }
    interface TimeInterval { start: number; end: number; }
    
    interface SqlEntry { val: number | null; ts: number; }
    interface SqlResult { result: SqlEntry[]; error?: string; }
    interface DeltaData { time: number; deltaKWh: number; }
    interface DailyBucket { import: number; export: number; pv: number; wb: number; batOut: number; wd: number; }
    interface ConsumptionProfile { [weekday: number]: { [time: string]: number[]; }; }
    
    const AC_MODES = { INPUT: "Input mode", OUTPUT: "Output mode" };
    
    // =====================================================================================
    // === 3. GLOBALE STATES ===
    // =====================================================================================
    
    const units = new Map<string, UnitRuntimeState>();
    
    const cluster: ClusterState = {
       totalSoC: 0,
       totalCapacityKWh: 0,
       totalUsableKWh: 0,
       virtualChargeLimit: 0,
       virtualDischargeLimit: 0,
       activeMasterId: null,
       lastMasterSwitch: 0
    };
    
    let integral = 0.0;
    const controlCache: ControlCache = {
       isTibberChargeNow: false, isTibberDischargeNow: false, isPriceBelowFloor: false, currentPrice: 0,
       predictedChargeKWh: 0, globalReserveKWh: 0
    };
    
    let tibberPriceData: TibberPriceProcessed[] = [];
    let cheapestIntervals: TimeInterval[] = [];
    let expensiveIntervals: TimeInterval[] = [];
    
    let historicalConsumptionProfile: ConsumptionProfile = {};
    let lastValidAccuracy: number | null = null;
    let dynamicPriceReference = 0;
    // @AUDIT-FIX: Status-Log Variable
    let lastStatusLog = "";
    
    // =====================================================================================
    // === 4. HELPER FUNCTIONS ===
    // =====================================================================================
    
    function logInfo(msg: string) { if(SYSTEM_CONFIG.INFO_LOGS) log(`[Cluster] ${msg}`, 'info'); }
    function logWarn(msg: string) { log(`[Cluster WARN] ${msg}`, 'warn'); }
    function logDebug(msg: string) { if(SYSTEM_CONFIG.DEBUG) log(`[Cluster DEBUG] ${msg}`, 'debug'); }
    
    function zendurePath(deviceId: string, type: string, endpoint: string): string {
       return `${SYSTEM_CONFIG.MQTT_BASE_PATH}.${type}.${deviceId}.${endpoint}`;
    }
    
    function safeGetNumber(id: string, defaultVal: number): number {
       try {
           if (!id) return defaultVal;
           const state = getState(id);
           if (!state || state.val == null) return defaultVal;
           const n = Number(state.val);
           return isNaN(n) ? defaultVal : n;
       } catch { return defaultVal; }
    }
    
    function safeGetString(id: string, defaultVal: string): string {
       try {
           if (!id) return defaultVal;
           const state = getState(id);
           return (state && state.val != null) ? String(state.val) : defaultVal;
       } catch { return defaultVal; }
    }
    
    function normalizeAcMode(val: unknown): string {
       if (!val) return AC_MODES.OUTPUT;
       const s = String(val).toLowerCase();
       if (s.includes('input')) return AC_MODES.INPUT;
       return AC_MODES.OUTPUT;
    }
    
    function delay(ms: number): Promise<void> {
       return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    function sqlRequest(instance: string, command: string, message: any): Promise<SqlResult> {
       return new Promise((resolve, reject) => {
           const timeout = setTimeout(() => {
               reject(new Error("SQL Request Timeout (5s)"));
           }, 5000);
           sendTo(instance, command, message, function (result: SqlResult) {
               clearTimeout(timeout);
               if (result.error) reject(new Error(result.error)); else resolve(result);
           });
       });
    }
    
    function logStrategyLists(limitCharge: number, limitDischarge: number) {
       if (!SYSTEM_CONFIG.INFO_LOGS) return;
    
       const format = (intervals: TimeInterval[], type: string, limit: number) => {
           if (!intervals.length) return;
           const lines = intervals.slice(0, 10).map(iv => {
               const match = tibberPriceData.find(p => p.start.getTime() === iv.start);
               if (!match) return null;
               const d = match.start;
               const dateStr = `${d.getDate()}.${d.getMonth()+1}.${d.getFullYear()}`;
               const timeStr = `${d.getHours()}:${(d.getMinutes()<10?'0':'')+d.getMinutes()}:00`;
               return `- ${dateStr}, ${timeStr} (${match.price.toFixed(2)} ct/kWh)`;
           }).filter(l => l !== null);
           
           if (lines.length) {
               logInfo(`[Strategie] Geplante **${type}**-Zeiten (${intervals.length}x) [Limit: ${limit.toFixed(2)}ct]:\n${lines.join("\n")}`);
           }
       };
    
       format(cheapestIntervals, "Lade", limitCharge);
       format(expensiveIntervals, "Entlade", limitDischarge);
    }
    
    // =====================================================================================
    // === 5. KI & DATA ANALYSIS ===
    // =====================================================================================
    
    async function getHistoryDeltas(id: string, scaleFactor: number = 1.0): Promise<DeltaData[]> {
       if (!id || !existsState(id)) return []; 
       const end = Date.now();
       const start = end - (SYSTEM_CONFIG.HISTORY_DAYS * 24 * 3600 * 1000);
       const RECORD_LIMIT = Math.max(100000, SYSTEM_CONFIG.HISTORY_DAYS * 24 * 60 * 2);
    
       try {
           const result = await sqlRequest(SYSTEM_CONFIG.SQL_INSTANCE_ID, 'getHistory', {
               id: id, options: { start, end, aggregate: 'none', limit: RECORD_LIMIT, returnNewestEntries: true, removeBorderValues: true, ignoreNull: true, ack: true }
           });
           const data = result.result;
           
           if (!data || data.length < 2) return [];
    
           data.sort((a, b) => a.ts - b.ts);
           const res: DeltaData[] = [];
           let lastVal: number | null = null;
           const CHUNK_SIZE = 2000;
           const MAX_REASONABLE_DELTA = 100; 
    
           for(let i=0; i<data.length; i++) {
                if (i % CHUNK_SIZE === 0 && i > 0) await delay(1);
               if (data[i].val === null) continue;
               const curVal = data[i].val as number;
               if (lastVal === null) { lastVal = curVal; continue; }
               
               let delta = (curVal - lastVal) * scaleFactor;
               
               if (delta < 0 || delta > MAX_REASONABLE_DELTA) { 
                   lastVal = curVal; 
                   continue; 
               } 
               
               lastVal = curVal;
               res.push({ time: data[i].ts, deltaKWh: delta });
           }
           return res;
       } catch(e) { logDebug(`Fehler SQL ${id}: ${e}`); return []; }
    }
    
    function calculatePVAccuracy(actualSoFar: number, forecastSoFar: number, minTemp: number, lastKnown: number | null): number {
       if (actualSoFar < 0.2 && forecastSoFar < 0.5) return lastKnown ?? 0.8;
       if (forecastSoFar < 0.5) {
           if (minTemp < 0.0) return lastKnown ?? 0.5;
           if (minTemp < 2.0) return lastKnown ?? 0.6;
           return lastKnown ?? 1.0; 
       }
       const deviation = Math.abs(actualSoFar - forecastSoFar);
       const acc = 1.0 - (deviation / Math.max(forecastSoFar * 1.5, 1.0)); 
       return Math.max(0.3, Math.min(1.0, acc));
    }
    
    async function loadTibberPriceData() {
       if(!GLOBAL_IDS.tibberPricesToday) return; 
       let today: TibberPriceRaw[] = [], tomorrow: TibberPriceRaw[] = [];
       try {
           const tState = getState(GLOBAL_IDS.tibberPricesToday); 
           const mState = getState(GLOBAL_IDS.tibberPricesTomorrow);
           if(tState && tState.val) today = JSON.parse(tState.val as string);
           if(mState && mState.val) tomorrow = JSON.parse(mState.val as string);
       } catch(e) {}
       tibberPriceData = [];
       [...today, ...tomorrow].forEach(i => {
           tibberPriceData.push({ start: new Date(i.startsAt), price: i.total * 100, durationMs: 15 * 60 * 1000 });
       });
    }
    
    function calculateDynamicPriceReference() {
       if (tibberPriceData.length === 0) { dynamicPriceReference = SYSTEM_CONFIG.MIN_PRICE_FLOOR_CT_KWH; return; }
       const now = Date.now();
       const futurePrices = tibberPriceData.filter(i => (i.start.getTime() + i.durationMs) > now);
       const listToUse = (futurePrices.length > 4) ? futurePrices : tibberPriceData;
       
       if (listToUse.length < 4) { dynamicPriceReference = SYSTEM_CONFIG.MIN_PRICE_FLOOR_CT_KWH; return; }
    
       const sortedPrices = [...listToUse].sort((a, b) => b.price - a.price);
       const topCount = Math.max(1, Math.ceil(sortedPrices.length * 0.35));
       const topSlice = sortedPrices.slice(0, topCount);
       
       const avgTop = topSlice.reduce((sum, i) => sum + i.price, 0) / topSlice.length;
       dynamicPriceReference = Math.max(avgTop, SYSTEM_CONFIG.MIN_PRICE_FLOOR_CT_KWH);
    }
    
    // Profil Bildung
    async function updateConsumptionProfileFromSQL() {
       logInfo("[KI] Starte SQL-Analyse für Verbrauchsprofil...");
       historicalConsumptionProfile = {}; 
       
       const [importD, exportD, pvDeltas, wbD] = await Promise.all([
           getHistoryDeltas(GLOBAL_IDS.netImportTotalKWh, 1.0),
           getHistoryDeltas(GLOBAL_IDS.netExportTotalKWh, 1.0),
           getHistoryDeltas(GLOBAL_IDS.pvTodayKWh, 1.0),
           getHistoryDeltas(GLOBAL_IDS.wbTotalKWh, 1.0)
       ]);
       
       const dailyBuckets: { [date: string]: { [hm: string]: DailyBucket } } = {}; 
       
       const addDeltas = async (deltas: DeltaData[], type: keyof DailyBucket) => {
           if(!deltas) return;
           for(let i=0; i < deltas.length; i++) {
                if (i % 2000 === 0 && i > 0) await delay(1); 
               const d = deltas[i];
               const dateObj = new Date(d.time);
               const dateKey = dateObj.toISOString().split('T')[0];
               const minutes = Math.floor(dateObj.getMinutes() / 15) * 15;
               const hm = `${('0'+dateObj.getHours()).slice(-2)}:${('0'+minutes).slice(-2)}`;
               
               if (!dailyBuckets[dateKey]) dailyBuckets[dateKey] = {};
               if (!dailyBuckets[dateKey][hm]) dailyBuckets[dateKey][hm] = { import: 0, export: 0, pv: 0, wb: 0, batOut: 0, wd: dateObj.getDay() };
               dailyBuckets[dateKey][hm][type] += d.deltaKWh;
           }
       };
    
       await addDeltas(importD, 'import'); await addDeltas(exportD, 'export'); await addDeltas(pvDeltas, 'pv'); await addDeltas(wbD, 'wb');
    
       for (const dateKey in dailyBuckets) {
           const dayData = dailyBuckets[dateKey];
           for (const hm in dayData) {
               const b = dayData[hm];
               const consumption = Math.max(0, (b.import + b.pv - b.export) - (b.wb || 0));
               
               if (!historicalConsumptionProfile[b.wd]) historicalConsumptionProfile[b.wd] = {};
               if (!historicalConsumptionProfile[b.wd][hm]) historicalConsumptionProfile[b.wd][hm] = [];
               historicalConsumptionProfile[b.wd][hm].push(consumption);
           }
       }
       
       try {
           setState(GLOBAL_IDS.consumptionProfile, JSON.stringify(historicalConsumptionProfile), true);
       } catch(e) {}
       logInfo("[KI] Verbrauchsprofil aktualisiert.");
    }
    
    async function runPredictiveAnalysis() {
       if (!SYSTEM_CONFIG.ENABLE_TIBBER) return;
       await loadTibberPriceData();
       calculateDynamicPriceReference();
    
       const actualPV = safeGetNumber(GLOBAL_IDS.pvTodayKWh, 0);
       const forecastSoFar = safeGetNumber(PV_FC_IDS.now, 0);
       const minTempToday = parseFloat(safeGetString(GLOBAL_IDS.forecastMinTempToday, "99")) || 99;
       
       logInfo(`[Diagnose PV] Ist (bis jetzt): ${actualPV.toFixed(2)} kWh | Soll (bis jetzt): ${forecastSoFar.toFixed(2)} kWh`);
       
       const acc = calculatePVAccuracy(actualPV, forecastSoFar, minTempToday, lastValidAccuracy);
       setState(GLOBAL_IDS.lastPVAccuracy, Math.round(acc * 100), true);
       lastValidAccuracy = acc;
       
       logInfo(`[Diagnose PV] Berechnete Genauigkeit: ${(acc*100).toFixed(0)}% (Delta: ${Math.abs(actualPV - forecastSoFar).toFixed(2)} kWh)`);
    
       const soc = cluster.totalSoC;
       const capacity = cluster.totalCapacityKWh;
       const freeSpace = capacity * (1 - (soc/100));
       controlCache.predictedChargeKWh = freeSpace; 
       
       const now = Date.now();
       const bep = dynamicPriceReference * 0.9; 
       
       cheapestIntervals = tibberPriceData
           .filter(i => (i.start.getTime() + i.durationMs) > now && i.price < bep)
           .map(i => ({ start: i.start.getTime(), end: i.start.getTime() + i.durationMs }));
           
       expensiveIntervals = tibberPriceData
           .filter(i => (i.start.getTime() + i.durationMs) > now && i.price > dynamicPriceReference)
           .map(i => ({ start: i.start.getTime(), end: i.start.getTime() + i.durationMs }));
    
       const currentTemp = safeGetNumber(GLOBAL_IDS.temperature, 10);
       const avgTemp = 10; 
       const tempDelta = avgTemp - currentTemp;
       let heatingReserve = 0;
       if (tempDelta > 0) heatingReserve = Math.min(SYSTEM_CONFIG.MAX_HEATING_RESERVE_KWH, tempDelta * SYSTEM_CONFIG.HEATING_FACTOR);
       
       const pvTom = safeGetNumber(PV_FC_IDS.tomorrow, 0) * acc;
       controlCache.globalReserveKWh = Math.max(0.5, heatingReserve - (pvTom * 0.3));
    
       logInfo(`[KI-Analyse] Wärme-Reserve: ${heatingReserve.toFixed(2)}kWh (Kalkulations-Basis: ${currentTemp.toFixed(1)}°C)`);
       logStrategyLists(bep, dynamicPriceReference);
       logInfo(`[KI] PV-Genauigkeit: ${(acc*100).toFixed(0)}% | Wärme-Res: ${heatingReserve.toFixed(2)}kWh | Global-Res: ${controlCache.globalReserveKWh.toFixed(2)}kWh`);
       
       updateControlFlags();
    }
    
    function updateControlFlags() {
       if (!tibberPriceData.length) return;
    
       const nowMs = Date.now();
       controlCache.isTibberChargeNow = cheapestIntervals.some(i => nowMs >= i.start && nowMs < i.end);
       controlCache.isTibberDischargeNow = expensiveIntervals.some(i => nowMs >= i.start && nowMs < i.end);
       
       const currentPriceObj = tibberPriceData.find(i => nowMs >= i.start.getTime() && nowMs < (i.start.getTime() + i.durationMs));
       if (currentPriceObj) {
           controlCache.currentPrice = currentPriceObj.price;
           controlCache.isPriceBelowFloor = currentPriceObj.price < SYSTEM_CONFIG.MIN_PRICE_FLOOR_CT_KWH;
       }
    }
    
    // =====================================================================================
    // === 6. DISPATCHER LOGIC (CORE) ===
    // =====================================================================================
    
    function updateClusterState() {
       let sumStoredKWh = 0; 
       let sumCap = 0;
       let sumUsableKWh = 0;
       let sumChargeW = 0;
       let sumDischargeW = 0;
    
       CLUSTER_CONFIG.UNITS.forEach(cfg => {
           if (!cfg.enabled) return;
           
           let u = units.get(cfg.id);
           if (!u) return;
    
           u.soc = safeGetNumber(u.ids.soc, 0);
           u.minSoc = safeGetNumber(u.ids.minSoc, 10);
           u.acMode = normalizeAcMode(safeGetString(u.ids.acMode, AC_MODES.OUTPUT));
           u.usableSoC = Math.max(0, u.soc - u.minSoc);
           
           u.isAvailable = u.soc > (u.minSoc + 1);
           u.canCharge = u.soc < 100;
    
           sumStoredKWh += (u.soc / 100) * cfg.capacityKWh;
           sumUsableKWh += (u.usableSoC / 100) * cfg.capacityKWh;
           sumCap += cfg.capacityKWh;
    
           if (u.canCharge) sumChargeW += cfg.maxChargeW;
           if (u.isAvailable) sumDischargeW += cfg.maxDischargeW;
           
           units.set(cfg.id, u);
       });
    
       cluster.totalSoC = sumCap > 0 ? ((sumStoredKWh / sumCap) * 100) : 0;
       cluster.totalCapacityKWh = sumCap;
       cluster.totalUsableKWh = sumUsableKWh;
       cluster.virtualChargeLimit = sumChargeW;
       cluster.virtualDischargeLimit = sumDischargeW;
       
       setState(GLOBAL_IDS.clusterTotalSoC, parseFloat(cluster.totalSoC.toFixed(1)), true);
    }
    
    function distributePower(totalTargetW: number): Map<string, number> {
       const distribution = new Map<string, number>();
       const absTarget = Math.abs(totalTargetW);
       const isCharging = totalTargetW > 0;
       
       const candidates = CLUSTER_CONFIG.UNITS.filter(cfg => {
           const u = units.get(cfg.id);
           if (!u || !cfg.enabled) return false;
           return isCharging ? u.canCharge : u.isAvailable;
       });
    
       if (candidates.length === 0) return distribution; 
    
       // STRATEGIE A: LOW LOAD -> STICKY MASTER
       if (absTarget < CLUSTER_CONFIG.MIN_LOAD_PARALLEL_W) {
           let masterId = cluster.activeMasterId;
           const currentMasterCfg = candidates.find(c => c.id === masterId);
           
           let masterNeedsSwitch = !currentMasterCfg; 
           
           if (!masterNeedsSwitch && currentMasterCfg) {
               const currentMasterState = units.get(masterId!)!;
               const runtime = Date.now() - cluster.lastMasterSwitch;
               
               if (runtime > CLUSTER_CONFIG.MIN_MASTER_RUNTIME_MS) {
                   const betterCandidate = candidates.find(c => {
                       if (c.id === masterId) return false;
                       const u = units.get(c.id)!;
                       
                       if (isCharging) {
                           return u.soc < (currentMasterState.soc - CLUSTER_CONFIG.SOC_SWITCH_HYSTERESIS);
                       } else {
                           return u.soc > (currentMasterState.soc + CLUSTER_CONFIG.SOC_SWITCH_HYSTERESIS);
                       }
                   });
                   
                   if (betterCandidate) {
                       logInfo(`[Dispatcher] Master-Wechsel: ${masterId} -> ${betterCandidate.id}`);
                       masterId = betterCandidate.id;
                       cluster.lastMasterSwitch = Date.now();
                   }
               }
           }
           
           if (!masterId || masterNeedsSwitch) {
               candidates.sort((a, b) => {
                   const uA = units.get(a.id)!;
                   const uB = units.get(b.id)!;
                   if (isCharging) return uA.soc - uB.soc; 
                   return uB.soc - uA.soc; 
               });
               masterId = candidates[0].id;
               cluster.lastMasterSwitch = Date.now();
           }
           
           cluster.activeMasterId = masterId;
           distribution.set(masterId!, totalTargetW);
       
       } 
       // STRATEGIE B: HIGH LOAD -> PARALLEL
       else {
           let totalWeight = 0;
           const weightedUnits: { id: string, weight: number, cfg: UnitConfig }[] = [];
           
           candidates.forEach(cfg => {
               const u = units.get(cfg.id)!;
               let weight = isCharging ? ((100 - u.soc) + 1) : (u.usableSoC + 1);
               totalWeight += weight;
               weightedUnits.push({ id: cfg.id, weight, cfg });
           });
           
           let remainingW = totalTargetW;
           weightedUnits.forEach((item, index) => {
               let targetW = 0;
               if (index === weightedUnits.length - 1) {
                   targetW = remainingW;
               } else {
                   const share = item.weight / totalWeight;
                   targetW = Math.round(totalTargetW * share);
               }
               
               if (isCharging) targetW = Math.min(targetW, item.cfg.maxChargeW);
               else targetW = Math.max(targetW, -item.cfg.maxDischargeW);
               
               distribution.set(item.id, targetW);
               remainingW -= targetW;
           });
       }
       return distribution;
    }
    
    function applyClusterPower(distribution: Map<string, number>) {
       let totalP = 0;
       
       for (const cfg of CLUSTER_CONFIG.UNITS) {
           if (!cfg.enabled) continue;
           const u = units.get(cfg.id);
           if (!u) continue;
           
           const targetW = distribution.get(cfg.id) || 0;
           const absW = Math.abs(targetW);
           const targetMode = targetW > 0 ? AC_MODES.INPUT : AC_MODES.OUTPUT;
           
           const isSwitchingTimeout = (Date.now() - u.lastSwitchTime) > cfg.acModeCooldownMs;
           const needsSwitch = normalizeAcMode(u.acMode) !== normalizeAcMode(targetMode);
           
           if (needsSwitch && absW > 0) {
               if (isSwitchingTimeout) {
                   setState(u.ids.acModeSet, targetMode);
                   u.lastSwitchTime = Date.now();
                   setState(u.ids.inputSet, "0");
                   setState(u.ids.outputSet, "0");
                   
                   u.currentTargetW = targetW;
                   units.set(cfg.id, u);
                   
                   logInfo(`[${cfg.id}] Switch zu ${targetMode}`);
                   continue; 
               }
           } else {
               const setID = (normalizeAcMode(u.acMode) === AC_MODES.INPUT) ? u.ids.inputSet : u.ids.outputSet;
               const currentSet = safeGetNumber(setID, 0);
               if (Math.abs(currentSet - absW) > 30 || (absW === 0 && currentSet !== 0)) {
                   setState(setID, String(absW));
               }
           }
           
           u.currentTargetW = targetW;
           units.set(cfg.id, u);
           totalP += targetW;
       }
       setState(GLOBAL_IDS.clusterTotalPower, totalP, true);
    }
    
    
    // =====================================================================================
    // === 7. PI CONTROLLER (GLOBAL) ===
    // =====================================================================================
    
    function mainControlLoop() {
       updateClusterState();
       
       const netzW = safeGetNumber(GLOBAL_IDS.netz, 0);
       const evccMode = safeGetNumber(GLOBAL_IDS.evccModus, 1);
       
       let maxCharge = cluster.virtualChargeLimit;
       let maxDischarge = cluster.virtualDischargeLimit;
       let overrideStatus = "";
       
       const forceCharge = (controlCache.isTibberChargeNow || evccMode === 3);
       
       if (forceCharge) {
           maxDischarge = 0;
           overrideStatus = "Force Charge";
       } else {
           if (evccMode === 2) {
               maxDischarge = 0;
               overrideStatus = "EVCC Sperre";
               if (netzW > -SYSTEM_CONFIG.DEADBAND_W) maxCharge = 0; 
           }
           if (SYSTEM_CONFIG.ENABLE_TIBBER && controlCache.isPriceBelowFloor) {
               maxDischarge = 0;
               overrideStatus = "Eco Price Stop";
               
               // @FEATURE: "Bis wann?" für Eco Mode
               const nextPriceRise = tibberPriceData.find(p => p.start.getTime() > Date.now() && p.price > SYSTEM_CONFIG.MIN_PRICE_FLOOR_CT_KWH);
               if (nextPriceRise) {
                   const t = nextPriceRise.start;
                   const timeStr = `${t.getHours()}:${(t.getMinutes()<10?'0':'')+t.getMinutes()}`;
                   overrideStatus += ` (bis ${timeStr})`;
               }
           }
           
           const totalUsableKWh = cluster.totalUsableKWh;
           
           if (!controlCache.isTibberDischargeNow && totalUsableKWh < controlCache.globalReserveKWh) {
                maxDischarge = 0; 
                overrideStatus = "Reserve Schutz";
                
                // @FEATURE: "Bis wann?" für Reserve Schutz (Warten auf Hochpreis)
                const nextHigh = expensiveIntervals.find(iv => iv.start > Date.now());
                if (nextHigh) {
                    const t = new Date(nextHigh.start);
                    const timeStr = `${t.getHours()}:${(t.getMinutes()<10?'0':'')+t.getMinutes()}`;
                    overrideStatus += ` (bis ${timeStr})`;
                }
           }
       }
       
       let error = SYSTEM_CONFIG.TARGET_W - netzW;
       if (Math.abs(netzW) < SYSTEM_CONFIG.DEADBAND_W) error = 0;
       
       const canControl = (maxCharge > 0 || maxDischarge > 0);
       if (!canControl) {
           integral = 0;
           error = 0;
           overrideStatus = "Cluster nicht bereit";
       }
       
       if (forceCharge) {
           integral = 0;
           var output = maxCharge;
       } else {
           const NORM_P = 2400; 
           const eNorm = error / NORM_P;
           const dt = SYSTEM_CONFIG.REGEL_INTERVALL_MS / 1000;
           
           if (maxCharge === 0 && maxDischarge === 0) integral = 0;
           else {
               integral *= SYSTEM_CONFIG.LEAK;
               integral += eNorm * dt;
           }
           
           const maxInt = 1.0 / SYSTEM_CONFIG.KI_N; 
           integral = Math.max(-maxInt, Math.min(maxInt, integral));
    
           const p = SYSTEM_CONFIG.KP_N * eNorm;
           const i = SYSTEM_CONFIG.KI_N * integral;
           var output = (p + i) * NORM_P;
       }
       
       output = Math.max(-maxDischarge, Math.min(maxCharge, output));
       
       if (Math.abs(output) < SYSTEM_CONFIG.HYSTERESIS_SWITCH_W && !forceCharge) {
           if (Math.abs(output) < 50) output = 0;
       }
    
       const distribution = distributePower(output);
       applyClusterPower(distribution);
       
       // @AUDIT-FIX: Status-Logik getrennt (Logbuch vs. Anzeige)
       let logState = "Standby"; // Stabil für Log
       let textState = "Standby"; // Detailliert für Visu
    
       if (overrideStatus) {
           logState = overrideStatus;
           textState = overrideStatus;
       } else if (output > 0) {
           logState = "Laden";
           textState = `Laden (${Math.round(output)}W)`;
       } else if (output < 0) {
           logState = "Entladen";
           textState = `Entladen (${Math.round(output)}W)`;
       } else {
           logState = "Standby";
           textState = "Standby / Leerlauf";
       }
    
       // @FEATURE: Status-Änderung loggen (Nur bei Änderung des Basis-Zustands)
       if (logState !== lastStatusLog) {
           logInfo(`[Status] Wechsel zu: ${logState} (Aktuell: ${textState})`);
           lastStatusLog = logState;
       }
    
       setState(GLOBAL_IDS.clusterStatus, textState, true);
    }
    
    
    // =====================================================================================
    // === 8. INITIALISIERUNG ===
    // =====================================================================================
    
    async function createDataPoints() {
       await createStateAsync(GLOBAL_IDS.clusterTotalSoC, { name: "Cluster SoC", type: "number", unit: "%", role: "value.battery" });
       await createStateAsync(GLOBAL_IDS.clusterTotalPower, { name: "Cluster Power", type: "number", unit: "W", role: "value.power" });
       await createStateAsync(GLOBAL_IDS.lastPVAccuracy, { name: "PV Genauigkeit", type: "number", unit: "%", role: "value" });
       await createStateAsync(GLOBAL_IDS.consumptionProfile, { name: "Verbrauchsprofil", type: "string", role: "json" });
       await createStateAsync(GLOBAL_IDS.clusterStatus, { name: "Cluster Status", type: "string", role: "text" });
    }
    
    async function initialize() {
       logInfo("--- Zendure CLUSTER Controller v8.0.12 (DOCS & LOG-FIX) Start ---");
       
       await createDataPoints();
       
       // Load persisted data
       try {
           const accState = getState(GLOBAL_IDS.lastPVAccuracy);
           if(accState && accState.val != null) lastValidAccuracy = Number(accState.val) / 100;
           
           const profState = getState(GLOBAL_IDS.consumptionProfile);
           if(profState && profState.val) historicalConsumptionProfile = JSON.parse(String(profState.val));
       } catch(e) {}
    
       // Init Units
       for (const cfg of CLUSTER_CONFIG.UNITS) {
           if (!cfg.enabled) continue;
           
           const uState: UnitRuntimeState = {
               soc: 0, minSoc: 10, acMode: AC_MODES.OUTPUT,
               usableSoC: 0, isAvailable: false, canCharge: false,
               lastSwitchTime: Date.now() - cfg.acModeCooldownMs, 
               currentTargetW: 0,
               ids: {
                   acMode: zendurePath(cfg.deviceId, "select", "acMode"),
                   acModeSet: zendurePath(cfg.deviceId, "select", "acMode.set"),
                   inputSet: zendurePath(cfg.deviceId, "number", "inputLimit.set"),
                   outputSet: zendurePath(cfg.deviceId, "number", "outputLimit.set"),
                   soc: zendurePath(cfg.deviceId, "sensor", "electricLevel"),
                   minSoc: zendurePath(cfg.deviceId, "number", "minSoc")
               }
           };
           units.set(cfg.id, uState);
           logInfo(`Unit ${cfg.id} registriert.`);
       }
    
       // Start
       await runPredictiveAnalysis();
       updateControlFlags();
    
       setInterval(mainControlLoop, SYSTEM_CONFIG.REGEL_INTERVALL_MS);
       
       schedule('5 * * * *', runPredictiveAnalysis); 
       schedule('0 3 * * *', updateConsumptionProfileFromSQL); 
       schedule('*/3 * * * *', updateControlFlags); 
    }
    
    initialize();
    

    1 Antwort Letzte Antwort
    0
    • S Online
      S Online
      Schimi
      schrieb am zuletzt editiert von
      #2

      so, lange dran rumgewerkelt und bei mir funktioniert es recht gut.... habe aber oft das Gefühl das der Akku viel größer sein müsste um es wirklich effektiv zu nutzen.

      Wollte es aber trotzdem mal veröffentlichen um den ganzen HomeAssistent Jungs nicht das Feld alleine zu überlassen ;-)

      Falls kein Interesse besteht (oder es zu kompliziert ist, ist halt sehr speziell) können wir es löschen oder in der Versenkung verschwinden lassen...

      Deshalb auch erstmal nur für ein Gerät, hatte auch kurz eine Version für max. 3, aber mangels Möglichkeit des testens, habe ich es erstmal gelassen

      1 Antwort Letzte Antwort
      0
      • L Online
        L Online
        lesiflo
        Most Active
        schrieb am zuletzt editiert von
        #3

        Hi,
        so was ähnliches habe ich mir auch schon mit javascript gebaut. Allerdings mit mehrere Scripten. Ein einziges wurde mir irgendwann zu unübersichtlich. Bei mir läuft ein Hauptscript für die beiden Hyper, zusätzlich habe ich auch noch eine große PV-Anlage. Weitere Scripts für PV-Forecast, Logging in InfluxDB, Balkendiagrammanzeige ...

        1 Antwort Letzte Antwort
        1
        • S Online
          S Online
          Schimi
          schrieb zuletzt editiert von
          #4

          habe es mal auf mehrere Geräte erweitert.. die "Single" Regelung läuft bei mir, aber wie gut es mit mehreren läuft kann ich leider nicht testen

          1 Antwort Letzte Antwort
          0
          Antworten
          • In einem neuen Thema antworten
          Anmelden zum Antworten
          • Älteste zuerst
          • Neuste zuerst
          • Meiste Stimmen


          Support us

          ioBroker
          Community Adapters
          Donate

          749

          Online

          32.6k

          Benutzer

          82.1k

          Themen

          1.3m

          Beiträge
          Community
          Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
          ioBroker Community 2014-2025
          logo
          • Anmelden

          • Du hast noch kein Konto? Registrieren

          • Anmelden oder registrieren, um zu suchen
          • Erster Beitrag
            Letzter Beitrag
          0
          • Home
          • Aktuell
          • Tags
          • Ungelesen 0
          • Kategorien
          • Unreplied
          • Beliebt
          • GitHub
          • Docu
          • Hilfe