Weiter zum Inhalt
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Hell
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dunkel
  • 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] Wetter.com Forecast/Vorhersage

NEWS

  • Der neue Monatsrückblick für Mai und Juni 2026 ist online!
    BluefoxB
    Bluefox
    8
    1
    908

  • wichtiges UPDATE für controller 7.2.2 im stable
    HomoranH
    Homoran
    10
    1
    3.5k

  • Neues YouTube-Video: Visualisierung im Devices-Adapter
    BluefoxB
    Bluefox
    16
    1
    5.9k

[TypeSkript] Wetter.com Forecast/Vorhersage

Geplant Angeheftet Gesperrt Verschoben JavaScript
73 Beiträge 11 Kommentatoren 7.4k Aufrufe 16 Beobachtet
  • Ä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
    #64

    @Jogis schaue mal ob du die neuste Version hast....

    Dann wäre es vielleicht mal nicht verkehrt, das Skript zu Stoppen > den "wetter_com" Ordner und userdata komplett löschen und das Skript neu starten damit es die Datenpunkte neu anlegt.

    Der API-Key wird nun im entsprechenden Datenpunkt gespeichert.....

    die aktuelle Version läuft bei mir nun schon "recht lange" fehlerfrei... diesen Monat habe ich auch den iobroker nicht so oft gestartet (dadurch war das 100 Limit kein Problem).
    Falls dir immernoch zuviele abfragen angezeigt werden... entweder bis zum 01.05 warten oder einen neuen API key beantragen...

    b91cb225-6089-465a-85f4-7ad043fbe411-image.jpeg

    1 Antwort Letzte Antwort
    0
    • J Offline
      J Offline
      Jogis
      schrieb am zuletzt editiert von
      #65

      Ich habe den Zähler "0_userdata.0.wetter_com.info.requests_month 100" auf 0 gesetzt jetzt kommen Daten wieder, benutze skript 2.6.1, iobroker läuft schon paar Monate und wird nicht neu gestartet. Frage nur warum im Monat April der Zähler nicht auf 0 gestellt war und die Daten nicht aktualisiert wurden? API key ist der gleiche.

      1 Antwort Letzte Antwort
      0
      • S Schimi

        [Skript] Wetter.com Forecast API v4.0 (Meteonomiqs) - Update v3.1.0

        Dieses performante TypeScript importiert Wetterdaten der neuen Wetter.com API (Meteonomiqs v4.0) ressourcenschonend in den ioBroker.

        🌟 Funktionen

        • Intelligente Prioritäts-Staffelung (Neu in v3.1.0): Standardmäßig erfolgen 3 Abrufe pro Tag (Nacht, Mittag, Abend). Wird das Monatsbudget gegen Monatsende knapp, greift ein Tier-System: Unwichtigere Abrufe (z.B. Mittag) fallen automatisch weg, um die Grundversorgung (Morgen/Abend) bis zum Monatsende durchgehend zu sichern.

        • Tages-Zusammenfassung (Summary): Max/Min Temperaturen, Regenrisiko & -menge, Windböen, Bewölkung und Luftfeuchtigkeit (bis 16 Tage).

        • Tagesabschnitte (Spaces): Detaillierte Daten für Vormittag, Nachmittag, Abend und Nacht.

        • Stündliche Vorhersage (Hourly): Präzise Stundenwerte für heute und morgen (48h).

        • Limit- & Cooldown-Schutz: Ein auf 5 Stunden reduzierter Cooldown verhindert, dass fehlerhafte Skript-Neustarts dein Limit aufbrauchen. Ein erweiterter Startup-Check prüft beim Systemstart direkt, ob deine geplante Update-Taktung überhaupt ins Monatslimit passt.

        • Ultra-Performance & Auto-Cleanup: Ein RAM-Cache minimiert die Schreiblast auf deinem System (Zero-Call Rerendering). Bei der Reduzierung der Vorhersage-Tage in den Einstellungen werden veraltete Datenpunkte sofort rekursiv gelöscht.

        🛠 Installation

        Erstelle im ioBroker ein neues Skript (z.B. im Ordner common).

        Wähle oben rechts als Typ zwingend TypeScript (NICHT JavaScript) aus.

        Kopiere den Code hinein, speichere und starte das Skript.

        ⚙️ Konfiguration

        1. Im Skript (CONFIG-Bereich):

        UPDATES: Beliebig anpassbare Liste für die Abrufzeiten (inkl. der Prioritäts-Staffelung).

        ENABLE_HOURLY / ENABLE_SPACES: Schaltet Detail-Ebenen ein/aus.

        LOCATION: Holt Koordinaten standardmäßig aus ioBroker-Settings, kann hier manuell überschrieben werden.

        2. Im ioBroker-Objektbaum (0_userdata.0.wetter.info):

        api_key: Hier zwingend deinen Meteonomiqs-Key eintragen!

        forecast_days: Anzahl der abzurufenden Tage (Standard: 7, max. 14).

        force_reset: Button für einen manuellen Reset des Monatszählers.

        📝 Hinweise zur VIS & Technik

        • Monitoring: Im Ordner info findest du Datenpunkte (Anfragen heute/Monat, letzter Sync), um deinen API-Verbrauch in der VIS zu überwachen.

        • Lokalisierung: Wochentage und Wetter-Texte (z.B. "Leicht bewölkt") passen sich dynamisch der ioBroker-Systemsprache an.

        • Icons: Zu jedem Wetterzustand wird direkt eine passende, von der API definierte Icon-URL als Datenpunkt ausgeliefert.

        • Free API-Key: Hier bei Meteonomiqs anfordern (Das 100 Calls/Monat Free-Tier reicht dank des neuen Tier-Systems völlig aus).

        letztes Update: 07.08.2026 - 19:38 Uhr

        /**
        * [Wetter.com / Meteonomiqs PWA v4.0 -> ioBroker]  (TrueScript / TypeScript)
        *
        * CHANGELOG:
        * - 3.1.0: 2026-08-06 - DREI ABRUFE PRO TAG MIT PRIORITAETS-STAFFELUNG
        *   FEATURE: Abrufzeiten liegen jetzt als Liste in CONFIG.UPDATES - beliebig viele Eintraege.
        *            01:10 (Tagesplanung) | 11:40 (Korrektur Nachmittag) | 18:40 (Nacht + Folgetag)
        *   FEATURE: Jeder Abruf hat eine 'tier'-Stufe. Wird das Budget knapp, fallen die Abrufe in
        *            umgekehrter Wichtigkeit weg (3 -> 2 -> 1/Tag), statt dass das Skript gegen Ende
        *            des Monats komplett stehen bleibt.
        *   FEATURE: Retry verbraucht keine Notreserve mehr (CONFIG.RESERVE_CALLS).
        *   FEATURE: Startup-Check prueft jetzt auch, ob die Taktung ueberhaupt ins Monatsbudget passt.
        *   CHANGE:  Cooldown 8 h -> 5 h (kuerzester Abstand der neuen Zeiten betraegt 6,5 h).
        *
        * - 3.0.0: 2026-08-06 - API-KONFORMITAET, BUGFIXES & DATENERWEITERUNG
        *   BUGFIX (kritisch): Cooldown blockierte dauerhaft einen der beiden Abrufe.
        *                      12 h Cooldown bei 11 h Cron-Abstand => Nachmittags-Abruf lief NIE.
        *                      Neu: 8 h Cooldown + automatische Plausibilitaetspruefung beim Start.
        *   BUGFIX (kritisch): Icon wurde selbst gebaut (`d_<state>.svg`). Die API liefert das
        *                      korrekte Icon inkl. Nacht- (n_), Unwetter- (d_e_) und Wind-Variante
        *                      (d_w_) im Feld `weather.icon`. Wird jetzt uebernommen.
        *   BUGFIX: Monats-/Tageszaehler wurden nur per Cron zurueckgesetzt. Lief ioBroker um
        *           00:00 am 1. nicht, blieb der Zaehler stehen -> Skript pausierte den ganzen Monat.
        *           Neu: Perioden-Marker (info.requests_month_period / _today_date), Reset lazy.
        *   BUGFIX: API-Calls wurden nur bei HTTP 200 gezaehlt. 4xx/5xx verbrauchen aber Kontingent.
        *           Neu: jede beantwortete Anfrage wird gezaehlt.
        *   BUGFIX: `spaces[i]` wurde per Index auf `summary[i]` gemappt. Neu: Zuordnung ueber `date`.
        *   BUGFIX: Datums-Strings "YYYY-MM-DD" wurden als UTC-Mitternacht geparst -> in Zeitzonen
        *           westlich von UTC um einen Tag verschoben. Neu: lokale Interpretation.
        *   BUGFIX: Stunden-Zuordnung basierte auf der Server-Zeitzone. Neu: Zeitzone des Ortes
        *           aus `location.timezone` der API (Intl), Fallback auf Serverzeit.
        *   BUGFIX: Vergangene Stunden blieben mit veralteten Werten stehen. Neu: `valid`-Flag.
        *   FEATURE: Aenderung von `forecast_days` rendert aus dem RAM-Cache - ohne API-Call.
        *   FEATURE: Neue Datenpunkte aus der v4.0-Spec (alle aus DERSELBEN Antwort, 0 Extra-Calls):
        *            Wetterwarnungen, Astronomie (Sonne/Mond), Luftdruck/Taupunkt/Windchill, Schnee.
        *   FEATURE: JSON-Aggregate `forecast_json` / `hourly_json` fuer VIS/jarvis/Material-Widgets.
        *   FEATURE: Ein Retry bei Netzwerkfehler/5xx, damit ein Abruffenster nicht verloren geht.
        *   REFACTOR: Tabellengetriebene Feld-Definitionen statt duplizierter setState-Bloecke.
        *
        * KONTEXT:
        * - Endpoint: GET https://forecast.meteonomiqs.com/v4_0/forecast/{lat}/{lon}
        * - Auth:     Header `x-api-key`, Sprache ueber `Accept-Language`
        * - Standort: automatisch aus system.config (Breiten-/Laengengrad des ioBroker-Systems)
        *
        * ZIELE:
        * - Zero-Churn (setStateChanged), Zero-I/O-Overhead (RAM-Cache fuer Objektpfade),
        *   typsichere Auswertung, harte Budget-Einhaltung.
        */
        
        // =====================================================================================
        // KONFIGURATION
        // =====================================================================================
        const CONFIG = {
           // --- Datenpunkte fuer Benutzereingaben ---
           DP_API_KEY: '0_userdata.0.wetter.info.api_key',
           DP_FORECAST_DAYS: '0_userdata.0.wetter.info.forecast_days',
           DP_FORCE_RESET: '0_userdata.0.wetter.info.force_reset',
           DP_FORCE_UPDATE: '0_userdata.0.wetter.info.force_update',
        
           /**
            * --- Zeitsteuerung ---
            * Feste Zeiten, keine Randomisierung. Jeder Eintrag hat eine Prioritaetsstufe:
            *
            *   tier = "wie viele Abrufe pro Tag muss das Restbudget noch tragen,
            *           damit dieser Abruf laufen darf?"
            *
            * Wird das Budget knapp, faellt zuerst tier 3 weg, dann tier 2. tier 1 laeuft
            * bis zuletzt (notfalls nur noch jeden zweiten Tag). Dadurch bricht die
            * Versorgung nie komplett ab, sie wird nur gröber.
            *
            *   01:10  tier 1  Tagesplanung        - die vollstaendige Vorhersage fuer den Tag
            *   11:40  tier 2  Nachmittags-Korrektur - Gewitter, Boeen, Hitzespitze vor der kritischen Phase
            *   18:40  tier 3  Abend + Folgetag    - Nachtfrost, Sturm, Vorschau auf morgen
            */
           UPDATES: [
               { cron: '10 1 * * *', id: 'nacht', tier: 1, name: 'Nacht (Tagesplanung)' },
               { cron: '40 11 * * *', id: 'mittag', tier: 2, name: 'Mittag (Korrektur Nachmittag)' },
               { cron: '40 18 * * *', id: 'abend', tier: 3, name: 'Abend (Nacht + Folgetag)' }
           ] as { cron: string; id: string; tier: number; name: string }[],
        
           // Cooldown MUSS kleiner sein als der kuerzeste Abstand zwischen zwei Abrufen.
           // 01:10 -> 11:40 = 10,5 h | 11:40 -> 18:40 = 7,0 h | 18:40 -> 01:10 = 6,5 h  => 5 h ist sicher.
           // Der Startup-Check rechnet das nach und meldet sich, falls es nicht passt.
           MIN_HOURS_BETWEEN_UPDATES: 5,
        
           // Notreserve: so viele Calls bleiben fuer manuelle Aktionen frei. Automatische
           // Wiederholungsversuche (Retry) greifen die Reserve nicht an.
           RESERVE_CALLS: 5,
        
           // --- API ---
           BASE_URL: 'https://forecast.meteonomiqs.com/v4_0',
           ICON_BASE_URL: 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather',
           HTTP_TIMEOUT_MS: 20000,
           MAX_RETRIES: 1,             // zusaetzliche Versuche bei Netzwerkfehler / HTTP 5xx
           RETRY_DELAY_MS: 30000,
        
           // --- Budget ---
           MONTHLY_LIMIT: 100,
        
           // --- Objektbaum ---
           DP_PATH: '0_userdata.0.wetter',
           DEFAULT_LANGUAGE: 'de',
           MAX_FORECAST_DAYS: 14,      // API liefert max. 14 Tage
           CLEANUP_UP_TO_DAY: 25,      // Aufraeumgrenze fuer alte Strukturen
        
           // --- Umfang der Datenpunkte (alles aus einem einzigen API-Call) ---
           ENABLE_SPACES: true,        // Tagesabschnitte morning/afternoon/evening/night
           ENABLE_HOURLY: true,        // Stundenwerte
           HOURLY_DAYS: 2,             // fuer wie viele Tage (0 = aus, 2 = heute + morgen)
           ENABLE_WARNINGS: true,      // highestWarning (Warngruppe, Schweregrad)
           ENABLE_ASTRO: true,         // Sonnenauf-/untergang, Daemmerung, Mond
           ENABLE_EXTENDED: true,      // Luftdruck, Taupunkt, Windchill
           ENABLE_SNOW: true,          // Schneefallgrenze, Frischschnee, Schneemenge
           ENABLE_JSON: true,          // forecast_json / hourly_json fuer VIS-Widgets
        
           LOG_LEVEL: 'info' as 'debug' | 'info' | 'warn' | 'error',
        
           LOCATION: {
               LAT: '',
               LON: '',
               FORCE_MANUAL: false
           }
        };
        
        // =====================================================================================
        // TYPEN
        // =====================================================================================
        type FetchSource = 'scheduled' | 'start' | 'key_update' | 'days_update' | 'force_reset' | 'manual';
        type FieldGroup = 'core' | 'warn' | 'astro' | 'extended' | 'snow';
        
        interface Ctx {
           lang: string;
           tz: string;
        }
        
        interface FieldDef {
           id: string;
           name: string;
           type: iobJS.CommonType;
           role: string;
           unit?: string;
           digits?: number;
           group?: FieldGroup;
           get: (src: any, ctx: Ctx) => any;
        }
        
        interface SystemConfig {
           lat: string;
           lon: string;
           lang: string;
        }
        
        // =====================================================================================
        // GLOBALER ZUSTAND
        // =====================================================================================
        let isFetching = false;
        let lastPayload: any = null;          // RAM-Cache der letzten API-Antwort
        let lastPayloadTs = 0;
        let daysDebounce: any = null;
        let authFailed = false;               // Sperre nach HTTP 401/403, bis der Key geaendert wird
        const ensuredPaths = new Set<string>();
        
        // =====================================================================================
        // HILFSFUNKTIONEN
        // =====================================================================================
        
        /** Filtert Log-Ausgaben anhand des konfigurierten Log-Levels. */
        function wcomLog(msg: string, level: 'debug' | 'info' | 'warn' | 'error' = 'info'): void {
           const levels = { debug: 0, info: 1, warn: 2, error: 3 };
           if (levels[level] >= levels[CONFIG.LOG_LEVEL]) log(`[Wetter.com] ${msg}`, level);
        }
        
        function wcomPad(n: number | string): string { return String(n).padStart(2, '0'); }
        
        function wcomSleep(ms: number): Promise<void> {
           return new Promise(resolve => setTimeout(resolve, ms));
        }
        
        /** Rundet und faengt NaN/Infinity ab (verhindert Float-Rauschen und damit unnoetige State-Writes). */
        function wcomRound(v: number, digits: number = 1): number {
           if (typeof v !== 'number' || !isFinite(v)) return 0;
           const p = Math.pow(10, digits);
           return Math.round(v * p) / p;
        }
        
        /**
        * Extrahiert typsicher numerische Werte aus den verschachtelten API-Objekten
        * ({value}|{avg}|{sum}|{max}|{min}) und faengt korrupte Rueckgaben ab.
        */
        function wcomExtractValue(val: any): number {
           if (val === null || val === undefined) return 0;
           if (typeof val === 'number') return isFinite(val) ? val : 0;
           if (typeof val === 'boolean') return val ? 1 : 0;
           if (typeof val === 'object') {
               for (const key of ['value', 'avg', 'sum', 'max', 'min']) {
                   if (val[key] !== undefined && val[key] !== null) return wcomExtractValue(val[key]);
               }
               return 0;
           }
           const parsed = parseFloat(String(val));
           if (isNaN(parsed)) {
               if (String(val).trim() !== '') wcomLog(`Unerwarteter Nicht-Zahlenwert von API: "${val}"`, 'debug');
               return 0;
           }
           return parsed;
        }
        
        /**
        * Parst API-Datumsangaben. "YYYY-MM-DD" wird bewusst LOKAL interpretiert -
        * `new Date('2026-08-06')` waere UTC-Mitternacht und in Zeitzonen westlich von
        * UTC damit der Vortag.
        */
        function wcomParseApiDate(input: string | Date): Date | null {
           if (!input) return null;
           if (input instanceof Date) return isNaN(input.getTime()) ? null : input;
           const s = String(input).trim();
           const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
           const d = m ? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : new Date(s);
           return isNaN(d.getTime()) ? null : d;
        }
        
        /** Formatiert nach DD.MM.YYYY. */
        function wcomFormatDate(input: string | Date): string {
           const d = wcomParseApiDate(input);
           if (!d) return '';
           return `${wcomPad(d.getDate())}.${wcomPad(d.getMonth() + 1)}.${d.getFullYear()}`;
        }
        
        /** Ausgeschriebener Wochentag in der Systemsprache. */
        function wcomGetDayName(dateStr: string, locale: string): string {
           const d = wcomParseApiDate(dateStr);
           if (!d) return '';
           try { return d.toLocaleDateString(locale, { weekday: 'long' }); }
           catch (e) { return d.toLocaleDateString('de', { weekday: 'long' }); }
        }
        
        /**
        * Rechnet einen UTC-Zeitstempel in die Ortszeit des Vorhersageortes um.
        * Nutzt die von der API gelieferte `location.timezone` - dadurch stimmen die
        * Stunden-Datenpunkte auch dann, wenn der ioBroker-Server in einer anderen
        * Zeitzone laeuft als der abgefragte Ort.
        */
        function wcomLocalParts(utcIso: string, tz: string): { date: string; hour: string } | null {
           const d = utcIso ? new Date(utcIso) : null;
           if (!d || isNaN(d.getTime())) return null;
           try {
               const parts: any = {};
               const fmt = new Intl.DateTimeFormat('en-GB', {
                   timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit',
                   hour: '2-digit', hourCycle: 'h23'
               });
               for (const p of fmt.formatToParts(d)) parts[p.type] = p.value;
               const hour = parts.hour === '24' ? '00' : parts.hour;
               return { date: `${parts.year}-${parts.month}-${parts.day}`, hour: hour };
           } catch (e) {
               return {
                   date: `${d.getFullYear()}-${wcomPad(d.getMonth() + 1)}-${wcomPad(d.getDate())}`,
                   hour: wcomPad(d.getHours())
               };
           }
        }
        
        /** Zieht "HH:MM" aus einem ISO-String mit Offset (z. B. 2026-08-06T06:17:49+02:00). */
        function wcomIsoTime(iso: any): string {
           const m = /T(\d{2}):(\d{2})/.exec(String(iso || ''));
           return m ? `${m[1]}:${m[2]}` : '';
        }
        
        /** Tageslaenge in Stunden aus Sonnenauf- und -untergang. */
        function wcomDayLength(sunrise: any, sunset: any): number {
           const a = new Date(String(sunrise || '')); const b = new Date(String(sunset || ''));
           if (isNaN(a.getTime()) || isNaN(b.getTime())) return 0;
           return wcomRound((b.getTime() - a.getTime()) / 3600000, 2);
        }
        
        /**
        * Liefert die vollstaendige Icon-URL. Bevorzugt das von der API gelieferte
        * Dateifeld - dieses beruecksichtigt Tag/Nacht (d_/n_), Unwetterwarnung (d_e_)
        * und Windwarnung (d_w_). Nur als Fallback wird aus dem State gebaut.
        */
        function wcomIconUrl(weather: any, isNight?: boolean): string {
           const raw = weather && typeof weather.icon === 'string' ? weather.icon.trim() : '';
           const file = raw !== '' ? raw : `${isNight ? 'n' : 'd'}_${weather && weather.state !== undefined && weather.state !== null ? weather.state : 999}.svg`;
           return `${CONFIG.ICON_BASE_URL}/${file}`;
        }
        
        /** Wolkenbedeckung in Prozent. Stuendlich liefert die API Oktas (`eights`). */
        function wcomCloudsPercent(clouds: any): number {
           if (clouds === null || clouds === undefined) return 0;
           if (typeof clouds === 'number') return clouds;
           if (typeof clouds.avg === 'number') return clouds.avg;
           if (typeof clouds.eights === 'number') return wcomRound((clouds.eights / 8) * 100, 1);
           if (typeof clouds.low === 'number' || typeof clouds.middle === 'number' || typeof clouds.high === 'number') {
               return Math.max(Number(clouds.low) || 0, Number(clouds.middle) || 0, Number(clouds.high) || 0);
           }
           return wcomExtractValue(clouds);
        }
        
        const MOON_PHASE: Record<string, string[]> = {
           de: ['', 'Neumond', 'Zunehmender Sichelmond', 'Zunehmender Halbmond', 'Zunehmender Dreiviertelmond',
               'Vollmond', 'Abnehmender Dreiviertelmond', 'Abnehmender Halbmond', 'Abnehmender Sichelmond'],
           en: ['', 'New Moon', 'Waxing Crescent', 'First Quarter', 'Waxing Gibbous',
               'Full Moon', 'Waning Gibbous', 'Last Quarter', 'Waning Crescent']
        };
        const MOON_ZODIAC: Record<string, string[]> = {
           de: ['', 'Widder', 'Stier', 'Zwillinge', 'Krebs', 'Löwe', 'Jungfrau', 'Waage', 'Skorpion', 'Schütze', 'Steinbock', 'Wassermann', 'Fische'],
           en: ['', 'Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
        };
        
        function wcomLookup(table: Record<string, string[]>, lang: string, idx: any): string {
           const list = table[String(lang).slice(0, 2).toLowerCase()] || table.de;
           const i = Number(idx);
           return (Number.isInteger(i) && i > 0 && i < list.length) ? list[i] : '';
        }
        
        /** Prueft, ob eine Feldgruppe laut CONFIG angelegt werden soll. */
        function wcomGroupEnabled(group?: FieldGroup): boolean {
           switch (group) {
               case 'warn': return CONFIG.ENABLE_WARNINGS;
               case 'astro': return CONFIG.ENABLE_ASTRO;
               case 'extended': return CONFIG.ENABLE_EXTENDED;
               case 'snow': return CONFIG.ENABLE_SNOW;
               default: return true;
           }
        }
        
        // =====================================================================================
        // FELD-DEFINITIONEN (tabellengetrieben statt duplizierter setState-Bloecke)
        // =====================================================================================
        
        const DAY_FIELDS: FieldDef[] = [
           { id: 'date', name: 'Datum', type: 'string', role: 'text', get: d => wcomFormatDate(d.date) },
           { id: 'date_iso', name: 'Datum (ISO)', type: 'string', role: 'date', get: d => d.date || '' },
           { id: 'day_name', name: 'Wochentag', type: 'string', role: 'text', get: (d, c) => wcomGetDayName(d.date, c.lang) },
           { id: 'temp_max', name: 'Max. Temperatur', type: 'number', role: 'value.temperature.max', unit: '°C', digits: 1, get: d => d.temperature?.max },
           { id: 'temp_min', name: 'Min. Temperatur', type: 'number', role: 'value.temperature.min', unit: '°C', digits: 1, get: d => d.temperature?.min },
           { id: 'temp_avg', name: 'Durchschnittstemperatur', type: 'number', role: 'value.temperature', unit: '°C', digits: 1, get: d => d.temperature?.avg },
           { id: 'weather_state', name: 'Wetter-Code', type: 'number', role: 'value', digits: 0, get: d => d.weather?.state },
           { id: 'weather_text', name: 'Wetter', type: 'string', role: 'weather.state', get: d => d.weather?.text },
           { id: 'weather_icon', name: 'Icon URL', type: 'string', role: 'weather.icon', get: d => wcomIconUrl(d.weather, false) },
           { id: 'prec_probability', name: 'Regenrisiko', type: 'number', role: 'value.precipitation.probability', unit: '%', digits: 0, get: d => d.prec?.probability },
           { id: 'prec_sum', name: 'Niederschlagsmenge', type: 'number', role: 'value.precipitation', unit: 'mm', digits: 2, get: d => d.prec?.sum },
           { id: 'prec_class', name: 'Niederschlagsklasse (0-3)', type: 'number', role: 'value', digits: 0, get: d => d.prec?.class },
           { id: 'rainy_hours', name: 'Regenstunden', type: 'number', role: 'value', unit: 'h', digits: 1, get: d => d.rainyHours },
           { id: 'sun_hours', name: 'Sonnenstunden', type: 'number', role: 'value.sun', unit: 'h', digits: 1, get: d => d.sunHours },
           { id: 'clouds', name: 'Bewölkung', type: 'number', role: 'value.clouds', unit: '%', digits: 1, get: d => wcomCloudsPercent(d.clouds) },
           { id: 'humidity', name: 'Relative Feuchte', type: 'number', role: 'value.humidity', unit: '%', digits: 0, get: d => d.relativeHumidity },
           { id: 'wind_speed', name: 'Windgeschwindigkeit (Ø)', type: 'number', role: 'value.speed.wind', unit: 'km/h', digits: 1, get: d => d.wind?.avg },
           { id: 'wind_speed_max', name: 'Max. Windgeschwindigkeit', type: 'number', role: 'value.speed.wind.max', unit: 'km/h', digits: 1, get: d => d.wind?.max ?? d.wind?.avg },
           { id: 'wind_gusts', name: 'Windböen', type: 'number', role: 'value.speed.wind.gust', unit: 'km/h', digits: 1, get: d => d.wind?.gusts },
           { id: 'wind_direction', name: 'Windrichtung', type: 'string', role: 'weather.direction', get: d => d.wind?.direction },
           { id: 'wind_direction_short', name: 'Windrichtung (kurz)', type: 'string', role: 'weather.direction', get: d => d.wind?.text },
           { id: 'wind_degree', name: 'Windrichtung (Grad)', type: 'number', role: 'value.direction.wind', unit: '°', digits: 0, get: d => d.wind?.degree },
        
           // --- Erweiterte meteorologische Werte ---
           { id: 'pressure', name: 'Luftdruck', type: 'number', role: 'value.pressure', unit: 'hPa', digits: 0, group: 'extended', get: d => d.pressure },
           { id: 'windchill_min', name: 'Gefühlt min.', type: 'number', role: 'value.temperature.min', unit: '°C', digits: 1, group: 'extended', get: d => d.windchill?.min },
           { id: 'windchill_max', name: 'Gefühlt max.', type: 'number', role: 'value.temperature.max', unit: '°C', digits: 1, group: 'extended', get: d => d.windchill?.max },
        
           // --- Schnee ---
           { id: 'snow_sum', name: 'Schnee (Wasseräquivalent)', type: 'number', role: 'value.precipitation', unit: 'mm', digits: 2, group: 'snow', get: d => d.prec?.snow },
           { id: 'fresh_snow_min', name: 'Neuschnee min.', type: 'number', role: 'value', unit: 'cm', digits: 1, group: 'snow', get: d => d.prec?.freshSnowRangeMin },
           { id: 'fresh_snow_max', name: 'Neuschnee max.', type: 'number', role: 'value', unit: 'cm', digits: 1, group: 'snow', get: d => d.prec?.freshSnowRangeMax },
           { id: 'snowline_min', name: 'Schneefallgrenze min.', type: 'number', role: 'value', unit: 'm', digits: 0, group: 'snow', get: d => d.snowLine?.min },
           { id: 'snowline_max', name: 'Schneefallgrenze max.', type: 'number', role: 'value', unit: 'm', digits: 0, group: 'snow', get: d => d.snowLine?.max },
        
           // --- Wetterwarnung ---
           { id: 'warn_active', name: 'Warnung aktiv', type: 'boolean', role: 'indicator.alarm', group: 'warn', get: d => !!d.highestWarning },
           { id: 'warn_group', name: 'Warnung Gruppe', type: 'string', role: 'text', group: 'warn', get: d => d.highestWarning?.group },
           { id: 'warn_text', name: 'Warnung Text', type: 'string', role: 'text', group: 'warn', get: d => d.highestWarning?.text },
           { id: 'warn_severity', name: 'Warnung Schweregrad', type: 'string', role: 'text', group: 'warn', get: d => d.highestWarning?.severity },
           { id: 'warn_severity_int', name: 'Warnung Schweregrad (0-4)', type: 'number', role: 'value.severity', digits: 0, group: 'warn', get: d => d.highestWarning?.severityInt }
        ];
        
        const ASTRO_FIELDS: FieldDef[] = [
           { id: 'sunrise', name: 'Sonnenaufgang', type: 'string', role: 'date.sunrise', get: a => wcomIsoTime(a.sunrise) },
           { id: 'sunset', name: 'Sonnenuntergang', type: 'string', role: 'date.sunset', get: a => wcomIsoTime(a.sunset) },
           { id: 'dawn', name: 'Morgendämmerung', type: 'string', role: 'text', get: a => wcomIsoTime(a.dawn) },
           { id: 'dusk', name: 'Abenddämmerung', type: 'string', role: 'text', get: a => wcomIsoTime(a.dusk) },
           { id: 'suntransit', name: 'Sonnenhöchststand', type: 'string', role: 'text', get: a => wcomIsoTime(a.suntransit) },
           { id: 'sunrise_iso', name: 'Sonnenaufgang (ISO)', type: 'string', role: 'date.sunrise', get: a => a.sunrise || '' },
           { id: 'sunset_iso', name: 'Sonnenuntergang (ISO)', type: 'string', role: 'date.sunset', get: a => a.sunset || '' },
           { id: 'day_length', name: 'Tageslänge', type: 'number', role: 'value', unit: 'h', digits: 2, get: a => wcomDayLength(a.sunrise, a.sunset) },
           { id: 'moonrise', name: 'Mondaufgang', type: 'string', role: 'text', get: a => wcomIsoTime(a.moonrise) },
           { id: 'moonset', name: 'Monduntergang', type: 'string', role: 'text', get: a => wcomIsoTime(a.moonset) },
           { id: 'moonphase', name: 'Mondphase (1-8)', type: 'number', role: 'value', digits: 0, get: a => a.moonphase },
           { id: 'moonphase_text', name: 'Mondphase', type: 'string', role: 'text', get: (a, c) => wcomLookup(MOON_PHASE, c.lang, a.moonphase) },
           { id: 'moonzodiac', name: 'Mond-Tierkreiszeichen (1-12)', type: 'number', role: 'value', digits: 0, get: a => a.moonzodiac },
           { id: 'moonzodiac_text', name: 'Mond-Tierkreiszeichen', type: 'string', role: 'text', get: (a, c) => wcomLookup(MOON_ZODIAC, c.lang, a.moonzodiac) }
        ];
        
        const SPACE_FIELDS: FieldDef[] = [
           { id: 'temp', name: 'Temperatur', type: 'number', role: 'value.temperature', unit: '°C', digits: 1, get: s => s.temperature?.avg ?? s.temperature },
           { id: 'temp_min', name: 'Temperatur min.', type: 'number', role: 'value.temperature.min', unit: '°C', digits: 1, get: s => s.temperature?.min },
           { id: 'temp_max', name: 'Temperatur max.', type: 'number', role: 'value.temperature.max', unit: '°C', digits: 1, get: s => s.temperature?.max },
           { id: 'weather_state', name: 'Wetter-Code', type: 'number', role: 'value', digits: 0, get: s => s.weather?.state },
           { id: 'text', name: 'Wetter', type: 'string', role: 'weather.state', get: s => s.weather?.text },
           { id: 'icon', name: 'Icon URL', type: 'string', role: 'weather.icon', get: s => wcomIconUrl(s.weather, !!s.isNight) },
           { id: 'is_night', name: 'Nacht', type: 'boolean', role: 'indicator', get: s => !!s.isNight },
           { id: 'prec_prob', name: 'Regenrisiko', type: 'number', role: 'value.precipitation.probability', unit: '%', digits: 0, get: s => s.prec?.probability },
           { id: 'prec_sum', name: 'Niederschlagsmenge', type: 'number', role: 'value.precipitation', unit: 'mm', digits: 2, get: s => s.prec?.sum },
           { id: 'wind_speed', name: 'Windgeschwindigkeit', type: 'number', role: 'value.speed.wind', unit: 'km/h', digits: 1, get: s => s.wind?.avg },
           { id: 'wind_gusts', name: 'Windböen', type: 'number', role: 'value.speed.wind.gust', unit: 'km/h', digits: 1, get: s => s.wind?.gusts },
           { id: 'wind_direction_short', name: 'Windrichtung (kurz)', type: 'string', role: 'weather.direction', get: s => s.wind?.text },
           { id: 'wind_degree', name: 'Windrichtung (Grad)', type: 'number', role: 'value.direction.wind', unit: '°', digits: 0, get: s => s.wind?.degree },
           { id: 'clouds', name: 'Bewölkung', type: 'number', role: 'value.clouds', unit: '%', digits: 1, get: s => wcomCloudsPercent(s.clouds) },
           { id: 'humidity', name: 'Relative Feuchte', type: 'number', role: 'value.humidity', unit: '%', digits: 0, get: s => s.relativeHumidity },
           { id: 'sun_hours', name: 'Sonnenstunden', type: 'number', role: 'value.sun', unit: 'h', digits: 1, get: s => s.sunHours },
        
           { id: 'pressure', name: 'Luftdruck', type: 'number', role: 'value.pressure', unit: 'hPa', digits: 0, group: 'extended', get: s => s.pressure },
           { id: 'windchill_min', name: 'Gefühlt min.', type: 'number', role: 'value.temperature.min', unit: '°C', digits: 1, group: 'extended', get: s => s.windchill?.min },
           { id: 'windchill_max', name: 'Gefühlt max.', type: 'number', role: 'value.temperature.max', unit: '°C', digits: 1, group: 'extended', get: s => s.windchill?.max },
        
           { id: 'snowline_min', name: 'Schneefallgrenze min.', type: 'number', role: 'value', unit: 'm', digits: 0, group: 'snow', get: s => s.snowLine?.min },
           { id: 'snowline_max', name: 'Schneefallgrenze max.', type: 'number', role: 'value', unit: 'm', digits: 0, group: 'snow', get: s => s.snowLine?.max },
           { id: 'fresh_snow_max', name: 'Neuschnee max.', type: 'number', role: 'value', unit: 'cm', digits: 1, group: 'snow', get: s => s.prec?.freshSnowRangeMax },
        
           { id: 'warn_active', name: 'Warnung aktiv', type: 'boolean', role: 'indicator.alarm', group: 'warn', get: s => !!s.highestWarning },
           { id: 'warn_text', name: 'Warnung Text', type: 'string', role: 'text', group: 'warn', get: s => s.highestWarning?.text },
           { id: 'warn_severity_int', name: 'Warnung Schweregrad (0-4)', type: 'number', role: 'value.severity', digits: 0, group: 'warn', get: s => s.highestWarning?.severityInt }
        ];
        
        const HOUR_FIELDS: FieldDef[] = [
           { id: 'time', name: 'Uhrzeit (Ortszeit)', type: 'string', role: 'text', get: h => h.__time },
           { id: 'from', name: 'Zeitstempel (UTC)', type: 'string', role: 'date', get: h => h.from || h.dateWithTimezone || '' },
           { id: 'valid', name: 'Wert aktuell', type: 'boolean', role: 'indicator', get: () => true },
           { id: 'temp', name: 'Temperatur', type: 'number', role: 'value.temperature', unit: '°C', digits: 1, get: h => h.temperature },
           { id: 'windchill', name: 'Gefühlt', type: 'number', role: 'value.temperature', unit: '°C', digits: 1, get: h => h.windchill },
           { id: 'weather_state', name: 'Wetter-Code', type: 'number', role: 'value', digits: 0, get: h => h.weather?.state },
           { id: 'weather_text', name: 'Wetter', type: 'string', role: 'weather.state', get: h => h.weather?.text },
           { id: 'weather_icon', name: 'Icon URL', type: 'string', role: 'weather.icon', get: h => wcomIconUrl(h.weather, !!h.isNight) },
           { id: 'is_night', name: 'Nacht', type: 'boolean', role: 'indicator', get: h => !!h.isNight },
           { id: 'prec_prob', name: 'Regenwahrscheinlichkeit', type: 'number', role: 'value.precipitation.probability', unit: '%', digits: 0, get: h => h.prec?.probability },
           { id: 'prec_sum', name: 'Niederschlagsmenge', type: 'number', role: 'value.precipitation', unit: 'mm', digits: 2, get: h => h.prec?.sum },
           { id: 'wind_speed', name: 'Windgeschwindigkeit', type: 'number', role: 'value.speed.wind', unit: 'km/h', digits: 1, get: h => h.wind?.avg },
           { id: 'wind_gusts', name: 'Windböen', type: 'number', role: 'value.speed.wind.gust', unit: 'km/h', digits: 1, get: h => h.wind?.gusts },
           // ACHTUNG: 'wind_dir' bleibt bewusst die Langform ("Westwind") wie in v2.7.0 -
           // bestehende VIS-Bindings brechen dadurch nicht. Die Kurzform kommt zusaetzlich dazu.
           { id: 'wind_dir', name: 'Windrichtung', type: 'string', role: 'weather.direction', get: h => h.wind?.direction },
           { id: 'wind_dir_short', name: 'Windrichtung (kurz)', type: 'string', role: 'weather.direction', get: h => h.wind?.text },
           { id: 'wind_degree', name: 'Windrichtung (Grad)', type: 'number', role: 'value.direction.wind', unit: '°', digits: 0, get: h => h.wind?.degree },
           { id: 'humidity', name: 'Relative Feuchte', type: 'number', role: 'value.humidity', unit: '%', digits: 0, get: h => h.relativeHumidity },
           { id: 'clouds', name: 'Bewölkung', type: 'number', role: 'value.clouds', unit: '%', digits: 1, get: h => wcomCloudsPercent(h.clouds) },
           { id: 'sun_hours', name: 'Sonnenstunden', type: 'number', role: 'value.sun', unit: 'h', digits: 2, get: h => h.sunHours },
        
           { id: 'pressure', name: 'Luftdruck', type: 'number', role: 'value.pressure', unit: 'hPa', digits: 0, group: 'extended', get: h => h.pressure },
           { id: 'dewpoint', name: 'Taupunkt', type: 'number', role: 'value.temperature.dewpoint', unit: '°C', digits: 1, group: 'extended', get: h => h.dewpoint },
        
           { id: 'fresh_snow', name: 'Neuschnee', type: 'number', role: 'value', unit: 'cm', digits: 1, group: 'snow', get: h => h.freshSnow },
           { id: 'snowline', name: 'Schneefallgrenze', type: 'number', role: 'value', unit: 'm', digits: 0, group: 'snow', get: h => h.prec?.snowLine },
        
           { id: 'warn_active', name: 'Warnung aktiv', type: 'boolean', role: 'indicator.alarm', group: 'warn', get: h => !!h.highestWarning },
           { id: 'warn_text', name: 'Warnung Text', type: 'string', role: 'text', group: 'warn', get: h => h.highestWarning?.text },
           { id: 'warn_severity_int', name: 'Warnung Schweregrad (0-4)', type: 'number', role: 'value.severity', digits: 0, group: 'warn', get: h => h.highestWarning?.severityInt }
        ];
        
        // =====================================================================================
        // OBJEKTBAUM
        // =====================================================================================
        
        /** Legt Ordner (device/channel) an. Nutzt den RAM-Cache, um I/O zu vermeiden. */
        async function wcomEnsureSubStructure(path: string, name: string, type: 'device' | 'channel' = 'channel'): Promise<void> {
           if (!path || ensuredPaths.has(path)) return;
           if (!existsObject(path)) {
               await extendObjectAsync(path, { type: type, common: { name: name }, native: {} });
           }
           ensuredPaths.add(path);
        }
        
        /** Legt einen Datenpunkt an. Nutzt den RAM-Cache, um I/O zu vermeiden. */
        async function wcomEnsureState(path: string, init: any, type: iobJS.CommonType, name: string, role: string, unit?: string, writeable: boolean = false): Promise<void> {
           if (ensuredPaths.has(path)) return;
           if (!existsObject(path)) {
               await createStateAsync(path, init, false, { name, type, role, unit: unit || '', read: true, write: writeable } as any);
           }
           ensuredPaths.add(path);
        }
        
        function wcomInitValue(type: iobJS.CommonType): any {
           return type === 'number' ? 0 : (type === 'boolean' ? false : '');
        }
        
        function wcomCoerce(raw: any, f: FieldDef): any {
           if (f.type === 'number') return wcomRound(wcomExtractValue(raw), f.digits === undefined ? 1 : f.digits);
           if (f.type === 'boolean') return !!raw;
           return raw === null || raw === undefined ? '' : String(raw);
        }
        
        /**
        * Legt alle Datenpunkte einer Feldtabelle an und puffert die Schreibvorgaenge.
        * Ein einziger Ort fuer Anlegen + Schreiben - dadurch koennen Definition und
        * Schreibvorgang nicht mehr auseinanderlaufen.
        */
        async function wcomRenderFields(basePath: string, labelPrefix: string, fields: FieldDef[], src: any, ctx: Ctx, buffer: Promise<any>[]): Promise<void> {
           const active = fields.filter(f => wcomGroupEnabled(f.group));
        
           await Promise.all(active.map(f =>
               wcomEnsureState(`${basePath}.${f.id}`, wcomInitValue(f.type), f.type, `${labelPrefix}${f.name}`, f.role, f.unit)
           ));
        
           for (const f of active) {
               let raw: any;
               try { raw = f.get(src, ctx); }
               catch (e) { raw = null; wcomLog(`Feld '${f.id}' konnte nicht gelesen werden: ${e}`, 'debug'); }
               buffer.push(setStateChangedAsync(`${basePath}.${f.id}`, wcomCoerce(raw, f), true));
           }
        }
        
        // =====================================================================================
        // SYSTEM / STANDORT
        // =====================================================================================
        
        /** Liest Koordinaten und Sprache aus den ioBroker-Systemeinstellungen. */
        async function wcomGetSystemSettings(): Promise<SystemConfig | null> {
           if (CONFIG.LOCATION.FORCE_MANUAL) {
               const lat = parseFloat(CONFIG.LOCATION.LAT);
               const lon = parseFloat(CONFIG.LOCATION.LON);
               if (isNaN(lat) || isNaN(lon)) {
                   wcomLog('FORCE_MANUAL aktiv, aber die manuellen Koordinaten sind ungültig.', 'error');
                   return null;
               }
               const sysLang = await wcomGetSystemLanguage();
               return { lat: lat.toFixed(3), lon: lon.toFixed(3), lang: sysLang };
           }
        
           const obj: any = await getObjectAsync('system.config');
           const common = obj && obj.common ? obj.common : null;
           if (!common || common.latitude === undefined || common.latitude === null || common.longitude === undefined || common.longitude === null) {
               wcomLog('Kein Standort in den ioBroker-Systemeinstellungen hinterlegt (Einstellungen -> System -> Standort).', 'error');
               return null;
           }
        
           const lat = parseFloat(String(common.latitude));
           const lon = parseFloat(String(common.longitude));
           if (isNaN(lat) || isNaN(lon)) {
               wcomLog('Standort in system.config ist nicht numerisch.', 'error');
               return null;
           }
        
           return { lat: lat.toFixed(3), lon: lon.toFixed(3), lang: common.language || CONFIG.DEFAULT_LANGUAGE };
        }
        
        async function wcomGetSystemLanguage(): Promise<string> {
           try {
               const obj: any = await getObjectAsync('system.config');
               return (obj && obj.common && obj.common.language) || CONFIG.DEFAULT_LANGUAGE;
           } catch (e) { return CONFIG.DEFAULT_LANGUAGE; }
        }
        
        // =====================================================================================
        // HTTP
        // =====================================================================================
        
        /** HTTP GET mit hartem Timeout - verhindert einen Deadlock im isFetching-Lock. */
        async function wcomHttpGetAsync(url: string, options: any): Promise<any> {
           let timeoutId: any = null;
        
           const fetchPromise = new Promise((resolve, reject) => {
               httpGet(url, options, (err: any, response: any) => {
                   if (err) reject(err); else resolve(response);
               });
           });
        
           const timeoutPromise = new Promise((_, reject) => {
               timeoutId = setTimeout(() => reject(new Error(`HTTP Timeout nach ${CONFIG.HTTP_TIMEOUT_MS / 1000} s`)), CONFIG.HTTP_TIMEOUT_MS + 2000);
           });
        
           try {
               return await Promise.race([fetchPromise, timeoutPromise]);
           } finally {
               if (timeoutId) clearTimeout(timeoutId);
           }
        }
        
        // =====================================================================================
        // BUDGET & ZAEHLER
        // =====================================================================================
        
        function wcomMonthKey(d: Date): string { return `${d.getFullYear()}-${wcomPad(d.getMonth() + 1)}`; }
        function wcomDayKey(d: Date): string { return `${d.getFullYear()}-${wcomPad(d.getMonth() + 1)}-${wcomPad(d.getDate())}`; }
        
        async function wcomGetNum(id: string, fallback = 0): Promise<number> {
           const s = await getStateAsync(id);
           return s && s.val !== null && s.val !== undefined ? Number(s.val) : fallback;
        }
        
        /**
        * Setzt Monats-/Tageszaehler anhand von Perioden-Markern zurueck.
        * Robuster als ein reiner Cron: laeuft ioBroker zum Umschaltzeitpunkt nicht,
        * wird beim naechsten Aufruf trotzdem korrekt zurueckgesetzt.
        */
        async function wcomRollPeriods(): Promise<void> {
           const now = new Date();
        
           const monthKey = wcomMonthKey(now);
           const storedMonth = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_month_period`);
           const storedMonthVal = storedMonth && storedMonth.val ? String(storedMonth.val) : '';
           if (storedMonthVal !== monthKey) {
               if (storedMonthVal) wcomLog(`Monatswechsel erkannt (${storedMonthVal} -> ${monthKey}). Monatszähler zurückgesetzt.`, 'info');
               await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, 0, true);
               await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month_period`, monthKey, true);
           }
        
           const dayKey = wcomDayKey(now);
           const storedDay = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_today_date`);
           const storedDayVal = storedDay && storedDay.val ? String(storedDay.val) : '';
           if (storedDayVal !== dayKey) {
               await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, 0, true);
               await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today_date`, dayKey, true);
           }
        }
        
        /** Hoechste konfigurierte Prioritaetsstufe = Abrufe pro Tag im Vollbetrieb. */
        function wcomMaxTier(): number {
           return CONFIG.UPDATES.reduce((m, u) => Math.max(m, u.tier), 1);
        }
        
        /**
        * Prueft Monatsbudget, Cooldown und die gestaffelte Sparlogik.
        *
        * @param source Ursprung des Abrufs.
        * @param tier   Prioritaetsstufe (nur bei automatischen Abrufen relevant).
        *               Ein Abruf der Stufe N laeuft nur, wenn das Restbudget noch
        *               N Abrufe pro Tag bis Monatsende traegt.
        * @param label  Klartextname fuer die Logausgabe.
        */
        async function wcomCheckBudget(source: FetchSource, tier: number = 0, label: string = ''): Promise<boolean> {
           await wcomRollPeriods();
        
           const currentUsage = await wcomGetNum(`${CONFIG.DP_PATH}.info.requests_month`, 0);
        
           if (currentUsage >= CONFIG.MONTHLY_LIMIT) {
               wcomLog(`Monatslimit erreicht (${currentUsage}/${CONFIG.MONTHLY_LIMIT}). Pausiert bis zum 01. des Folgemonats.`, 'warn');
               await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'Monatslimit erreicht', true);
               return false;
           }
        
           const automatic = (source === 'scheduled' || source === 'start');
           if (!automatic) return true;  // manuelle Aktionen: nur das harte Limit gilt
        
           // Cooldown schuetzt vor Skript-Neustart-Schleifen.
           const lastTs = await wcomGetNum(`${CONFIG.DP_PATH}.info.last_sync_ts`, 0);
           if (lastTs > 0) {
               const diffHours = (Date.now() - lastTs) / 3600000;
               if (diffHours < CONFIG.MIN_HOURS_BETWEEN_UPDATES) {
                   wcomLog(`Abruf übersprungen (Cooldown). Letzter Abruf vor ${diffHours.toFixed(1)} h, Minimum ${CONFIG.MIN_HOURS_BETWEEN_UPDATES} h.`, 'debug');
                   return false;
               }
           }
        
           // Ein Skriptstart wird wie der niedrigst priorisierte Abruf behandelt: er darf
           // nie die Reserve anknabbern, die die planmaessigen Abrufe am Leben haelt.
           const effTier = tier > 0 ? tier : wcomMaxTier();
        
           const now = new Date();
           const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
           const daysLeft = daysInMonth - now.getDate();      // volle Tage nach heute
           const needed = effTier * (daysLeft + 1);           // effTier Abrufe/Tag inkl. heute
        
           if (currentUsage + needed <= CONFIG.MONTHLY_LIMIT) return true;
        
           if (effTier > 1) {
               wcomLog(`SPARMODUS: "${label || source}" übersprungen. Das Restbudget trägt keine ${effTier} Abrufe/Tag mehr bis Monatsende (Verbrauch ${currentUsage}/${CONFIG.MONTHLY_LIMIT}, ${daysLeft + 1} Tage offen).`, 'warn');
               return false;
           }
        
           // Stufe 1 ist der letzte verbliebene Abruf: statt ganz auszusetzen nur noch
           // jeden zweiten Tag - lieber gröber als gar nicht.
           if (now.getDate() % 2 !== 0) {
               wcomLog(`NOTLAUF: "${label || source}" übersprungen (Budget kritisch, Abruf nur noch an geraden Tagen). Verbrauch ${currentUsage}/${CONFIG.MONTHLY_LIMIT}.`, 'warn');
               return false;
           }
        
           return true;
        }
        
        /**
        * Zaehlt eine beantwortete API-Anfrage gegen das Kontingent.
        * Bewusst auch bei Fehlerantworten (ausser 401/403) - diese verbrauchen beim
        * Anbieter ebenfalls Kontingent. In v2.7.0 wurde nur HTTP 200 gezaehlt, dadurch
        * konnte der lokale Zaehler dauerhaft vom echten Verbrauch abweichen.
        */
        async function wcomCountRequest(): Promise<void> {
           await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, (await wcomGetNum(`${CONFIG.DP_PATH}.info.requests_today`)) + 1, true);
           const month = (await wcomGetNum(`${CONFIG.DP_PATH}.info.requests_month`)) + 1;
           await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, month, true);
           await setStateAsync(`${CONFIG.DP_PATH}.info.requests_left`, Math.max(0, CONFIG.MONTHLY_LIMIT - month), true);
        }
        
        /** Schreibt die Sync-Zeitstempel nach einem erfolgreichen Abruf. */
        async function wcomUpdateSyncInfo(): Promise<void> {
           const now = new Date();
           const ts = `${wcomPad(now.getDate())}.${wcomPad(now.getMonth() + 1)}.${now.getFullYear()} ${wcomPad(now.getHours())}:${wcomPad(now.getMinutes())}`;
           await setStateAsync(`${CONFIG.DP_PATH}.info.last_sync`, ts, true);
           await setStateAsync(`${CONFIG.DP_PATH}.info.last_sync_ts`, now.getTime(), true);
        }
        
        // =====================================================================================
        // INITIALISIERUNG DER INFO-DATENPUNKTE
        // =====================================================================================
        async function wcomEnsureBaseStructure(): Promise<void> {
           await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
           await wcomEnsureSubStructure(`${CONFIG.DP_PATH}.info`, 'Informationen');
        
           await Promise.all([
               wcomEnsureState(`${CONFIG.DP_PATH}.info.last_sync`, '', 'string', 'Letztes Update', 'text'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.last_sync_ts`, 0, 'number', 'Letztes Update (Timestamp)', 'value.time'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_month`, 0, 'number', 'Anfragen Monat', 'value'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_month_period`, '', 'string', 'Abrechnungsmonat', 'text'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_today`, 0, 'number', 'Anfragen heute', 'value'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_today_date`, '', 'string', 'Zähltag', 'text'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_left`, CONFIG.MONTHLY_LIMIT, 'number', 'Verbleibende Anfragen', 'value'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.status`, '', 'string', 'Status', 'text'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.last_error`, '', 'string', 'Letzter Fehler', 'text'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.forecast_date`, '', 'string', 'Vorhersage erstellt (API)', 'date'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.next_update`, '', 'string', 'Nächstes API-Update', 'date'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.location`, '', 'string', 'Verwendeter Standort', 'text'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.timezone`, '', 'string', 'Zeitzone des Standorts', 'text'),
               wcomEnsureState(`${CONFIG.DP_PATH}.info.elevation`, 0, 'number', 'Höhe über NN', 'value', 'm'),
               // Benutzereingaben
               wcomEnsureState(CONFIG.DP_API_KEY, '', 'string', 'Wetter.com API Key', 'text', '', true),
               wcomEnsureState(CONFIG.DP_FORECAST_DAYS, 7, 'number', 'Vorhersage Tage', 'value', '', true),
               wcomEnsureState(CONFIG.DP_FORCE_RESET, false, 'boolean', 'Monatszähler zurücksetzen', 'button', '', true),
               wcomEnsureState(CONFIG.DP_FORCE_UPDATE, false, 'boolean', 'Jetzt aktualisieren', 'button', '', true)
           ]);
        
           if (CONFIG.ENABLE_JSON) {
               await Promise.all([
                   wcomEnsureState(`${CONFIG.DP_PATH}.forecast_json`, '[]', 'string', 'Vorhersage (JSON)', 'json'),
                   wcomEnsureState(`${CONFIG.DP_PATH}.hourly_json`, '[]', 'string', 'Stundenwerte (JSON)', 'json')
               ]);
           }
        }
        
        // =====================================================================================
        // HAUPTLOGIK
        // =====================================================================================
        
        async function wcomFetchWeatherData(source: FetchSource = 'start', tier: number = 0, label: string = ''): Promise<void> {
           if (isFetching) {
               wcomLog('Abruf läuft bereits (Lock aktiv). Abbruch.', 'debug');
               return;
           }
           isFetching = true;
        
           try {
               await wcomEnsureBaseStructure();
        
               // Schreibrechte auf den Eingabe-Datenpunkten sicherstellen (Migration alter Installationen)
               if (source === 'start') {
                   await extendObjectAsync(CONFIG.DP_API_KEY, { common: { write: true } });
                   await extendObjectAsync(CONFIG.DP_FORECAST_DAYS, { common: { write: true } });
               }
        
               const apiKeyState = await getStateAsync(CONFIG.DP_API_KEY);
               const apiKey = apiKeyState && apiKeyState.val ? String(apiKeyState.val).trim() : '';
               if (apiKey.length < 10) {
                   wcomLog(`Bitte einen gültigen API-Key in '${CONFIG.DP_API_KEY}' eintragen.`, 'error');
                   await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'Kein API-Key', true);
                   return;
               }
        
               if (authFailed && (source === 'start' || source === 'scheduled')) {
                   wcomLog('Abruf übersprungen: der API-Key wurde zuletzt abgelehnt. Bitte Key korrigieren.', 'warn');
                   return;
               }
        
               const forecastDays = await wcomGetForecastDays();
        
               if (!(await wcomCheckBudget(source, tier, label))) return;
        
               const settings = await wcomGetSystemSettings();
               if (!settings) {
                   await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'Kein Standort', true);
                   return;
               }
        
               const url = `${CONFIG.BASE_URL}/forecast/${settings.lat}/${settings.lon}`;
               const options = {
                   timeout: CONFIG.HTTP_TIMEOUT_MS,
                   headers: { 'x-api-key': apiKey, 'Accept-Language': settings.lang }
               };
        
               wcomLog(`Abruf gestartet (${label || source}) für ${settings.lat}/${settings.lon}, ${forecastDays} Tage.`, 'info');
        
               let data: any = null;
        
               // Wiederholungsversuche duerfen die Notreserve nicht aufbrauchen.
               const mayRetry = async () => (await wcomGetNum(`${CONFIG.DP_PATH}.info.requests_left`, CONFIG.MONTHLY_LIMIT)) > CONFIG.RESERVE_CALLS;
        
               for (let attempt = 0; attempt <= CONFIG.MAX_RETRIES; attempt++) {
                   let response: any = null;
                   let netError: any = null;
        
                   try { response = await wcomHttpGetAsync(url, options); }
                   catch (e) { netError = e; }
        
                   if (netError) {
                       const retry = attempt < CONFIG.MAX_RETRIES && await mayRetry();
                       wcomLog(`Netzwerkfehler: ${netError.message || netError}`, retry ? 'warn' : 'error');
                       await setStateAsync(`${CONFIG.DP_PATH}.info.last_error`, String(netError.message || netError), true);
                       if (retry) { await wcomSleep(CONFIG.RETRY_DELAY_MS); continue; }
                       await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'Netzwerkfehler', true);
                       return;
                   }
        
                   const code = response ? Number(response.statusCode) : 0;
        
                   // 401/403 werden vom Gateway vor der Verrechnung abgewiesen und daher nicht gezaehlt.
                   if (code !== 401 && code !== 403) await wcomCountRequest();
        
                   if (code === 429) {
                       wcomLog('API-Kontingent erschöpft (HTTP 429). Zähler wird auf das Limit gesetzt.', 'error');
                       await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, CONFIG.MONTHLY_LIMIT, true);
                       await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'HTTP 429 - Limit erschöpft', true);
                       return;
                   }
                   if (code === 401 || code === 403) {
                       // Sperre setzen, damit wiederholte Skript-Neustarts nicht dauerhaft
                       // gegen die API laufen. Wird beim Ändern des API-Keys aufgehoben.
                       authFailed = true;
                       wcomLog(`API-Key abgelehnt (HTTP ${code}). Automatische Abrufe pausiert, bis der Key geändert wird.`, 'error');
                       await setStateAsync(`${CONFIG.DP_PATH}.info.status`, `HTTP ${code} - Key ungültig`, true);
                       return;
                   }
                   if (code >= 500) {
                       const retry = attempt < CONFIG.MAX_RETRIES && await mayRetry();
                       wcomLog(`Serverfehler HTTP ${code}.`, retry ? 'warn' : 'error');
                       if (retry) { await wcomSleep(CONFIG.RETRY_DELAY_MS); continue; }
                       await setStateAsync(`${CONFIG.DP_PATH}.info.status`, `HTTP ${code}`, true);
                       return;
                   }
                   if (code !== 200) {
                       wcomLog(`Unerwarteter Statuscode HTTP ${code}.`, 'error');
                       await setStateAsync(`${CONFIG.DP_PATH}.info.status`, `HTTP ${code}`, true);
                       return;
                   }
        
                   try {
                       data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
                   } catch (e) {
                       wcomLog('API-Antwort konnte nicht als JSON gelesen werden.', 'error');
                       await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'JSON-Parsefehler', true);
                       return;
                   }
                   break;
               }
        
               if (!data || !Array.isArray(data.summary) || data.summary.length === 0) {
                   wcomLog('API-Antwort enthält keine Vorhersagedaten.', 'error');
                   await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'Leere Antwort', true);
                   return;
               }
        
               lastPayload = data;
               lastPayloadTs = Date.now();
               authFailed = false;
        
               await wcomUpdateSyncInfo();
               await wcomProcessForecastData(data, settings.lang, forecastDays);
               await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'OK', true);
               await setStateAsync(`${CONFIG.DP_PATH}.info.last_error`, '', true);
        
           } catch (e: any) {
               wcomLog(`Script-Fehler: ${e && e.message ? e.message : e}`, 'error');
               try { await setStateAsync(`${CONFIG.DP_PATH}.info.last_error`, String(e && e.message ? e.message : e), true); } catch (ignore) { /* noop */ }
           } finally {
               isFetching = false;
           }
        }
        
        async function wcomGetForecastDays(): Promise<number> {
           const raw = await wcomGetNum(CONFIG.DP_FORECAST_DAYS, 7);
           return Math.max(1, Math.min(Math.round(raw) || 7, CONFIG.MAX_FORECAST_DAYS));
        }
        
        // =====================================================================================
        // VERARBEITUNG
        // =====================================================================================
        
        async function wcomProcessForecastData(data: any, lang: string, forecastDays: number): Promise<void> {
           const tz = (data.location && data.location.timezone) ? String(data.location.timezone) : Intl.DateTimeFormat().resolvedOptions().timeZone;
           const ctx: Ctx = { lang: lang, tz: tz };
        
           await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
        
           // Metadaten aus der Antwort
           await Promise.all([
               setStateChangedAsync(`${CONFIG.DP_PATH}.info.forecast_date`, String(data.forecastDate || ''), true),
               setStateChangedAsync(`${CONFIG.DP_PATH}.info.next_update`, String(data.nextUpdate || ''), true),
               setStateChangedAsync(`${CONFIG.DP_PATH}.info.timezone`, tz, true),
               setStateChangedAsync(`${CONFIG.DP_PATH}.info.location`,
                   data.location && data.location.coordinates
                       ? `${data.location.coordinates.latitude}, ${data.location.coordinates.longitude}` : '', true),
               setStateChangedAsync(`${CONFIG.DP_PATH}.info.elevation`,
                   wcomExtractValue(data.location && data.location.coordinates ? data.location.coordinates.elevation : 0), true)
           ]);
        
           const summary: any[] = Array.isArray(data.summary) ? data.summary : [];
           const maxDays = Math.min(summary.length, forecastDays);
        
           // Tagesabschnitte ueber das Datum zuordnen statt ueber den Array-Index.
           const spacesByDate = new Map<string, any>();
           if (Array.isArray(data.spaces)) {
               for (const s of data.spaces) if (s && s.date) spacesByDate.set(String(s.date), s);
           }
        
           // Stundenwerte nach Ortsdatum gruppieren.
           const hourlyByDate = new Map<string, any[]>();
           if (Array.isArray(data.hourly)) {
               for (const h of data.hourly) {
                   const parts = wcomLocalParts(h.from || h.dateWithTimezone || h.date, tz);
                   const key = parts ? parts.date : String(h.date || '');
                   if (!key) continue;
                   h.__hour = parts ? parts.hour : '00';
                   h.__time = `${h.__hour}:00`;
                   if (!hourlyByDate.has(key)) hourlyByDate.set(key, []);
                   hourlyByDate.get(key)!.push(h);
               }
           }
        
           const jsonDays: any[] = [];
           const jsonHours: any[] = [];
           let totalWrites = 0;
        
           for (let i = 0; i < maxDays; i++) {
               const day = summary[i];
               if (!day) continue;
        
               const dayKey = String(day.date || '');
               const dayPath = `${CONFIG.DP_PATH}.day_${i}`;
               const buffer: Promise<any>[] = [];
        
               await wcomEnsureSubStructure(dayPath, `Tag ${i}`);
               await wcomRenderFields(dayPath, `Tag ${i}: `, DAY_FIELDS, day, ctx, buffer);
        
               // --- Astronomie ---
               if (CONFIG.ENABLE_ASTRO && day.astronomy) {
                   const astroPath = `${dayPath}.astro`;
                   await wcomEnsureSubStructure(astroPath, 'Sonne & Mond');
                   await wcomRenderFields(astroPath, `Tag ${i}: `, ASTRO_FIELDS, day.astronomy, ctx, buffer);
               }
        
               // --- Tagesabschnitte ---
               if (CONFIG.ENABLE_SPACES) {
                   const spaceDay = spacesByDate.get(dayKey);
                   if (spaceDay) {
                       const spacesPath = `${dayPath}.spaces`;
                       await wcomEnsureSubStructure(spacesPath, 'Tagesabschnitte');
                       const labels: Record<string, string> = { morning: 'Morgen', afternoon: 'Mittag', evening: 'Abend', night: 'Nacht' };
                       for (const seg of ['morning', 'afternoon', 'evening', 'night']) {
                           const sData = spaceDay[seg];
                           if (!sData) continue;
                           const sPath = `${spacesPath}.${seg}`;
                           await wcomEnsureSubStructure(sPath, labels[seg]);
                           await wcomRenderFields(sPath, `Tag ${i} ${labels[seg]}: `, SPACE_FIELDS, sData, ctx, buffer);
                       }
                   } else {
                       wcomLog(`Keine Tagesabschnitte für ${dayKey} in der API-Antwort.`, 'debug');
                   }
               }
        
               // --- Stundenwerte ---
               if (CONFIG.ENABLE_HOURLY && i < CONFIG.HOURLY_DAYS) {
                   const hourlyPath = `${dayPath}.hourly`;
                   await wcomEnsureSubStructure(hourlyPath, 'Stündlich');
                   const hours = hourlyByDate.get(dayKey) || [];
                   const delivered = new Set<string>();
        
                   for (const h of hours) {
                       const hPath = `${hourlyPath}.${h.__hour}`;
                       delivered.add(h.__hour);
                       await wcomEnsureSubStructure(hPath, `${h.__hour}:00 Uhr`);
                       await wcomRenderFields(hPath, `Tag ${i} ${h.__hour}:00: `, HOUR_FIELDS, h, ctx, buffer);
        
                       if (CONFIG.ENABLE_JSON && i === 0) {
                           jsonHours.push({
                               date: dayKey, time: h.__time, from: h.from || '',
                               temp: wcomRound(wcomExtractValue(h.temperature), 1),
                               icon: wcomIconUrl(h.weather, !!h.isNight),
                               text: h.weather?.text || '',
                               precProb: wcomRound(wcomExtractValue(h.prec?.probability), 0),
                               precSum: wcomRound(wcomExtractValue(h.prec?.sum), 2),
                               wind: wcomRound(wcomExtractValue(h.wind?.avg), 1),
                               gusts: wcomRound(wcomExtractValue(h.wind?.gusts), 1),
                               humidity: wcomRound(wcomExtractValue(h.relativeHumidity), 0)
                           });
                       }
                   }
        
                   // Bereits vergangene Stunden liefert die API nicht mehr mit:
                   // Werte stehen lassen, aber als nicht mehr aktuell markieren.
                   for (let hh = 0; hh < 24; hh++) {
                       const label = wcomPad(hh);
                       if (delivered.has(label)) continue;
                       const validPath = `${hourlyPath}.${label}.valid`;
                       if (existsObject(validPath)) buffer.push(setStateChangedAsync(validPath, false, true));
                   }
               }
        
               // --- JSON-Aggregat ---
               if (CONFIG.ENABLE_JSON) {
                   jsonDays.push({
                       day: i,
                       date: wcomFormatDate(day.date),
                       dateIso: dayKey,
                       dayName: wcomGetDayName(day.date, lang),
                       tempMin: wcomRound(wcomExtractValue(day.temperature?.min), 1),
                       tempMax: wcomRound(wcomExtractValue(day.temperature?.max), 1),
                       text: day.weather?.text || '',
                       icon: wcomIconUrl(day.weather, false),
                       state: wcomRound(wcomExtractValue(day.weather?.state), 0),
                       precProb: wcomRound(wcomExtractValue(day.prec?.probability), 0),
                       precSum: wcomRound(wcomExtractValue(day.prec?.sum), 2),
                       wind: wcomRound(wcomExtractValue(day.wind?.avg), 1),
                       gusts: wcomRound(wcomExtractValue(day.wind?.gusts), 1),
                       windDir: day.wind?.text || '',
                       sunHours: wcomRound(wcomExtractValue(day.sunHours), 1),
                       clouds: wcomRound(wcomCloudsPercent(day.clouds), 0),
                       humidity: wcomRound(wcomExtractValue(day.relativeHumidity), 0),
                       pressure: wcomRound(wcomExtractValue(day.pressure), 0),
                       sunrise: wcomIsoTime(day.astronomy?.sunrise),
                       sunset: wcomIsoTime(day.astronomy?.sunset),
                       warnActive: !!day.highestWarning,
                       warnText: day.highestWarning?.text || '',
                       warnSeverity: wcomRound(wcomExtractValue(day.highestWarning?.severityInt), 0)
                   });
               }
        
               totalWrites += buffer.length;
               await Promise.all(buffer);
           }
        
           if (CONFIG.ENABLE_JSON) {
               await Promise.all([
                   setStateChangedAsync(`${CONFIG.DP_PATH}.forecast_json`, JSON.stringify(jsonDays), true),
                   setStateChangedAsync(`${CONFIG.DP_PATH}.hourly_json`, JSON.stringify(jsonHours), true)
               ]);
           }
        
           await wcomCleanupObsoleteDays(maxDays);
           wcomLog(`Update abgeschlossen: ${maxDays} Tage, ${totalWrites} Werte verarbeitet.`, 'info');
        }
        
        /** Entfernt Tagesordner, die durch eine reduzierte Vorhersagedauer obsolet sind. */
        async function wcomCleanupObsoleteDays(activeDays: number): Promise<void> {
           for (let i = activeDays; i <= CONFIG.CLEANUP_UP_TO_DAY; i++) {
               const path = `${CONFIG.DP_PATH}.day_${i}`;
               if (existsObject(path)) {
                   await deleteObjectAsync(path, true);
                   // Cache-Eintraege des geloeschten Teilbaums verwerfen
                   for (const p of Array.from(ensuredPaths)) if (p === path || p.indexOf(`${path}.`) === 0) ensuredPaths.delete(p);
                   wcomLog(`Obsoleten Tagesordner entfernt: day_${i}`, 'debug');
               }
           }
        }
        
        /** Rendert die zuletzt geholte Antwort neu - ohne API-Call. */
        async function wcomRerenderFromCache(reason: string): Promise<boolean> {
           if (!lastPayload) return false;
           if (isFetching) return false;
           isFetching = true;
           try {
               const settings = await wcomGetSystemSettings();
               const lang = settings ? settings.lang : CONFIG.DEFAULT_LANGUAGE;
               const days = await wcomGetForecastDays();
               wcomLog(`${reason}: Neuaufbau aus dem Cache (Stand ${new Date(lastPayloadTs).toLocaleString()}) - kein API-Call nötig.`, 'info');
               await wcomProcessForecastData(lastPayload, lang, days);
               return true;
           } catch (e: any) {
               wcomLog(`Neuaufbau aus dem Cache fehlgeschlagen: ${e && e.message ? e.message : e}`, 'error');
               return false;
           } finally {
               isFetching = false;
           }
        }
        
        // =====================================================================================
        // SELBSTPRUEFUNG DER ZEITPLANUNG
        // =====================================================================================
        
        /** Liest "M H * * *" und liefert die Minuten seit Mitternacht, sonst null. */
        function wcomCronMinutes(cron: string): number | null {
           const p = String(cron).trim().split(/\s+/);
           if (p.length < 5) return null;
           const m = Number(p[0]); const h = Number(p[1]);
           if (!Number.isInteger(m) || !Number.isInteger(h)) return null;
           return h * 60 + m;
        }
        
        /**
        * Prueft beim Start zwei Dinge, die sich sonst nur als "es kommen keine Daten"
        * bemerkbar machen:
        *  1. Ist der Cooldown kleiner als der kuerzeste Abstand zwischen zwei Abrufen?
        *     (Genau das liess in v2.7.0 den Nachmittags-Abruf dauerhaft ausfallen.)
        *  2. Passt die gewaehlte Taktung ueberhaupt in das Monatsbudget?
        */
        function wcomValidateSchedule(): void {
           const hhmm = (min: number) => `${wcomPad(Math.floor(min / 60))}:${wcomPad(min % 60)}`;
           const times = CONFIG.UPDATES
               .map(u => ({ min: wcomCronMinutes(u.cron), u }))
               .filter(x => x.min !== null) as { min: number; u: typeof CONFIG.UPDATES[0] }[];
        
           if (times.length === 0) { wcomLog('Keine gültigen Abrufzeiten konfiguriert.', 'error'); return; }
        
           times.sort((x, y) => x.min - y.min);
           const plan = times.map(t => `${hhmm(t.min)} (${t.u.name}, Stufe ${t.u.tier})`).join(' | ');
        
           // Kleinster Abstand im 24-h-Kreis
           let minGap = 24 * 60;
           for (let i = 0; i < times.length; i++) {
               const next = times[(i + 1) % times.length];
               const raw = next.min - times[i].min;
               minGap = Math.min(minGap, raw > 0 ? raw : raw + 1440);
           }
           const gapHours = times.length > 1 ? minGap / 60 : 24;
        
           wcomLog(`Zeitplan: ${plan}`, 'info');
        
           if (CONFIG.MIN_HOURS_BETWEEN_UPDATES >= gapHours) {
               wcomLog(`KONFIGURATIONSFEHLER: Cooldown (${CONFIG.MIN_HOURS_BETWEEN_UPDATES} h) ist größer/gleich dem kürzesten Abstand der Abrufzeiten (${gapHours.toFixed(1)} h). Mindestens ein Abruf wird IMMER übersprungen. Bitte MIN_HOURS_BETWEEN_UPDATES verkleinern.`, 'error');
           } else {
               wcomLog(`Kürzester Abstand ${gapHours.toFixed(1)} h, Cooldown ${CONFIG.MIN_HOURS_BETWEEN_UPDATES} h - passt.`, 'debug');
           }
        
           // Budget-Plausibilitaet fuer den laengsten Monat
           const perDay = wcomMaxTier();
           const worstCase = perDay * 31;
           if (worstCase > CONFIG.MONTHLY_LIMIT) {
               wcomLog(`BUDGET-WARNUNG: ${perDay} Abrufe/Tag ergeben in einem 31-Tage-Monat ${worstCase} Calls, das Limit liegt bei ${CONFIG.MONTHLY_LIMIT}. Die Sparstaffelung greift dann gegen Monatsende automatisch.`, 'warn');
           } else {
               const spare = CONFIG.MONTHLY_LIMIT - worstCase;
               wcomLog(`Budget: ${perDay} Abrufe/Tag = max. ${worstCase} Calls/Monat von ${CONFIG.MONTHLY_LIMIT} (${spare} Reserve).`, 'info');
           }
        }
        
        // =====================================================================================
        // ZEITSTEUERUNG & TRIGGER
        // =====================================================================================
        
        for (const upd of CONFIG.UPDATES) {
           schedule(upd.cron, () => wcomFetchWeatherData('scheduled', upd.tier, upd.name));
        }
        
        // Perioden-Reset zusaetzlich taeglich anstossen, damit die Zaehler in der
        // Oberflaeche zeitnah umspringen. Der eigentliche Reset ist lazy abgesichert.
        schedule('1 0 * * *', () => wcomRollPeriods().catch(e => wcomLog(String(e), 'error')));
        
        on({ id: CONFIG.DP_API_KEY, change: 'ne' }, (obj) => {
           if (obj.state && typeof obj.state.val === 'string' && obj.state.val.trim().length >= 10) {
               authFailed = false; // neue Chance nach einem zuvor abgelehnten Key
               wcomLog('API-Key geändert. Starte Test-Abruf.', 'info');
               wcomFetchWeatherData('key_update');
           }
        });
        
        on({ id: CONFIG.DP_FORECAST_DAYS, change: 'ne' }, (obj) => {
           if (!obj.state || obj.state.val === null) return;
           // Entprellen: schnelle Aenderungen (Slider in VIS) zu einem Vorgang zusammenfassen.
           if (daysDebounce) clearTimeout(daysDebounce);
           daysDebounce = setTimeout(async () => {
               daysDebounce = null;
               wcomLog(`Vorhersage-Tage geändert auf ${obj.state.val}.`, 'info');
               const done = await wcomRerenderFromCache('Änderung der Vorhersagedauer');
               if (!done) wcomFetchWeatherData('days_update');
           }, 3000);
        });
        
        on({ id: CONFIG.DP_FORCE_RESET, change: 'any', val: true }, async () => {
           wcomLog('Manueller Reset: Monatszähler wird auf 0 gesetzt.', 'warn');
           await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, 0, true);
           await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month_period`, wcomMonthKey(new Date()), true);
           await setStateAsync(CONFIG.DP_FORCE_RESET, false, true);
        });
        
        on({ id: CONFIG.DP_FORCE_UPDATE, change: 'any', val: true }, async () => {
           await setStateAsync(CONFIG.DP_FORCE_UPDATE, false, true);
           authFailed = false;
           wcomLog('Manueller Abruf ausgelöst (umgeht den Cooldown, Monatslimit bleibt aktiv).', 'info');
           wcomFetchWeatherData('manual');
        });
        
        // --- Skriptstart ---
        wcomValidateSchedule();
        wcomFetchWeatherData('start');
        

        P Offline
        P Offline
        pk68
        schrieb am zuletzt editiert von
        #66

        Hi @Schimi , danke für das Skript.

        Ich glaube, es gibt einen kleinen Bug bei den stündlichen Daten. Die Werte für die ersten beiden Stunden des Tages werden falsch zugeordnet. Zum Debuggen habe ich das Skript angepasst und den Datenpunkt "from" mitschreiben lassen. Da ergibt sich folgendes Bild:

        0_userdata.0.wetter_com.day_0.hourly.23.from = 2026-04-25T21:00:00Z
        0_userdata.0.wetter_com.day_1.hourly.00.from = 2026-04-26T22:00:00Z
        0_userdata.0.wetter_com.day_1.hourly.01.from = 2026-04-26T23:00:00Z
        0_userdata.0.wetter_com.day_1.hourly.02.from = 2026-04-26T00:00:00Z
        

        Die TImestamps sind ja UTC, also 2h Versatz zu Deutschland. Die Uhrzeiten sind alle ok, aber bei 00:00 und 01:00 Uhr stimmt das Datum nicht. Da müsste als Tag der 25. drin stehen.

        S 1 Antwort Letzte Antwort
        0
        • P pk68

          Hi @Schimi , danke für das Skript.

          Ich glaube, es gibt einen kleinen Bug bei den stündlichen Daten. Die Werte für die ersten beiden Stunden des Tages werden falsch zugeordnet. Zum Debuggen habe ich das Skript angepasst und den Datenpunkt "from" mitschreiben lassen. Da ergibt sich folgendes Bild:

          0_userdata.0.wetter_com.day_0.hourly.23.from = 2026-04-25T21:00:00Z
          0_userdata.0.wetter_com.day_1.hourly.00.from = 2026-04-26T22:00:00Z
          0_userdata.0.wetter_com.day_1.hourly.01.from = 2026-04-26T23:00:00Z
          0_userdata.0.wetter_com.day_1.hourly.02.from = 2026-04-26T00:00:00Z
          

          Die TImestamps sind ja UTC, also 2h Versatz zu Deutschland. Die Uhrzeiten sind alle ok, aber bei 00:00 und 01:00 Uhr stimmt das Datum nicht. Da müsste als Tag der 25. drin stehen.

          S Online
          S Online
          Schimi
          schrieb am zuletzt editiert von
          #67

          @pk68 Hi danke für die Info....

          habe mal ne neue version erstellt, weil ich auch wieder ins Limit gelaufen bin... vielleicht hatte das sogar was miteinander zutun... falls nicht, bin ich langsam überfragt :-)

          /**
          * [Wetter.com Forecast API v4.0 (TrueScript)]
          * * CHANGELOG:
          * - 2.6.4: 2026-04-27 - HOTFIX (Initialization Order)
          * - FIX: State "info.last_sync" not found. Die Initialisierung des last_sync-Datenpunkts wurde an den Anfang der Fetch-Funktion vorgezogen, damit wcomUpdateUsageInfo (Ghost-Call Fix) nicht ins Leere schreibt.
          * - 2.6.3: CRITICAL BUGFIX UPDATE (Ghost-Calls & UTC-Offset)
          * - 2.6.2: FEATURE: Manueller Reset & Log-Präzision.
          * - 2.6.1: FIX: Konfigurations-Datenpunkte beschreibbar gemacht.
          * - 2.6.0: ULTRA-PERFORMANCE (RAM-Cache, wcomWait entfernt).
          * * KONTEXT:
          * - Hardware: ioBroker Server | Schnittstellen: Meteonomiqs API v4.0 (HTTP)
          * * ZIELE:
          * - Maximale Effizienz (Zero-Churn, Zero-I/O Overhead) und 100% typsichere Ausfallsicherheit unter Budget-Einhaltung.
          */
          
          // --- KONFIGURATION ---
          const CONFIG = {
             // SECURITY: Den API Key NIEMALS hier im Klartext speichern!
             DP_API_KEY: '0_userdata.0.wetter_com.info.api_key', 
             DP_FORECAST_DAYS: '0_userdata.0.wetter_com.info.forecast_days',
             DP_FORCE_RESET: '0_userdata.0.wetter_com.info.force_reset', 
             
             BASE_URL: 'https://forecast.meteonomiqs.com/v4_0',
             ICON_BASE_URL: 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather',
             DP_PATH: '0_userdata.0.wetter_com',
             DEFAULT_LANGUAGE: 'de',
             ENABLE_HOURLY: true,
             ENABLE_SPACES: true,
             MONTHLY_LIMIT: 100,
             LOG_LEVEL: 'info' as 'debug' | 'info' | 'warn' | 'error',
             LOCATION: {
                 LAT: '',
                 LON: '',
                 FORCE_MANUAL: false
             }
          };
          
          // --- STATISCHE DEFINITIONEN ---
          const STATE_DEFS: Record<string, { name: string; type: iobJS.CommonType; role: string; unit?: string; init: any }> = {
             'date': { name: 'Datum', type: 'string', role: 'text', init: '' },
             'day_name': { name: 'Wochentag', type: 'string', role: 'text', init: '' },
             'temp_max': { name: 'Max Temp', type: 'number', unit: '°C', role: 'value.temperature.max', init: 0 },
             'temp_min': { name: 'Min Temp', type: 'number', unit: '°C', role: 'value.temperature.min', init: 0 },
             'weather_text': { name: 'Wetter', type: 'string', role: 'weather.state', init: '' },
             'weather_icon': { name: 'Icon URL', type: 'string', role: 'weather.icon', init: '' },
             'prec_probability': { name: 'Regenrisiko', type: 'number', unit: '%', role: 'value.precipitation.probability', init: 0 },
             'prec_sum': { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation', init: 0 },
             'wind_gusts': { name: 'Windböen', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust', init: 0 },
             'wind_speed_max': { name: 'Max. Windgeschwindigkeit', type: 'number', unit: 'km/h', role: 'value.speed.wind.max', init: 0 },
             'sun_hours': { name: 'Sonnenstunden', type: 'number', unit: 'h', role: 'value.sun', init: 0 },
             'clouds': { name: 'Bewölkung', type: 'number', unit: '%', role: 'value', init: 0 },
             'humidity': { name: 'Relative Feuchte', type: 'number', unit: '%', role: 'value.humidity', init: 0 }
          };
          
          // --- INTERFACES ---
          
          type FetchSource = 'morning' | 'afternoon' | 'start' | 'key_update' | 'days_update' | 'force_reset';
          
          interface WetterComValue {
             avg?: number;
             value?: number;
             sum?: number;
             max?: number;
             min?: number;
          }
          
          interface WetterComWeather {
             state: number;
             text: string;
             icon?: string;
          }
          
          interface WetterComWind {
             avg?: number | WetterComValue;
             min?: number | WetterComValue;
             max?: number | WetterComValue;
             gusts?: number | WetterComValue | { value: number | null };
             direction?: string;
             unit?: string;
          }
          
          interface WetterComPrec {
             probability: number;
             sum: number | WetterComValue;
          }
          
          interface ForecastSummary {
             date: string;
             weather: WetterComWeather;
             temperature: { min: number | WetterComValue; max: number | WetterComValue; avg?: number | WetterComValue };
             wind: WetterComWind;
             prec: WetterComPrec;
             clouds: number | WetterComValue;
             relativeHumidity: number | WetterComValue;
             sunHours?: number;
          }
          
          interface ForecastSpaceSegment {
             temperature: number | WetterComValue;
             weather: WetterComWeather;
             prec: WetterComPrec;
             wind: WetterComWind;
             clouds: number | WetterComValue;
             relativeHumidity: number | WetterComValue;
          }
          
          interface ForecastSpace {
             morning?: ForecastSpaceSegment;
             afternoon?: ForecastSpaceSegment;
             evening?: ForecastSpaceSegment;
             night?: ForecastSpaceSegment;
          }
          
          interface ForecastHourly {
             from: string;
             date: string;
             weather: WetterComWeather;
             temperature: number | WetterComValue;
             windchill: number | WetterComValue;
             wind: WetterComWind;
             prec: WetterComPrec;
             relativeHumidity: number | WetterComValue;
          }
          
          interface WetterComResponse {
             summary: ForecastSummary[];
             spaces: ForecastSpace[];
             hourly: ForecastHourly[];
          }
          
          interface SystemConfig {
             lat: string | null;
             lon: string | null;
             lang: string;
          }
          
          // --- GLOBALE VARIABLEN ---
          let isFetching: boolean = false;
          const ensuredPaths = new Set<string>();
          
          // --- HILFSFUNKTIONEN ---
          
          /**
          * Filtert und gibt Log-Meldungen basierend auf dem konfigurierten Log-Level aus.
          * @param msg Die auszugebende Nachricht.
          * @param level Das Loglevel (debug, info, warn, error).
          */
          function wcomLog(msg: string, level: 'debug' | 'info' | 'warn' | 'error' = 'info'): void {
             const levels = { debug: 0, info: 1, warn: 2, error: 3 };
             if (levels[level] >= levels[CONFIG.LOG_LEVEL]) {
                 log(`[Wetter.com] ${msg}`, level);
             }
          }
          
          /**
          * Extrahiert typsicher numerische Werte aus API-Objekten und fängt korrupte Rückgaben ab.
          * @KI_HINWEIS: Fängt null/undefined ab und loggt fehlerhaftes (NaN) API-Verhalten ohne zu crashen.
          * @param val Der rohe Wert aus der JSON-Antwort.
          * @returns Bereinigter numerischer Wert oder 0 als Fallback.
          */
          function wcomExtractValue(val: any): number {
             if (val === null || val === undefined) return 0;
             if (typeof val === 'number') {
                 if (isNaN(val)) {
                     wcomLog('API lieferte explizites NaN als number-Typ', 'debug');
                     return 0;
                 }
                 return val;
             }
             if (typeof val === 'object') {
                 if (val.value !== undefined && val.value !== null) return val.value;
                 if (val.avg !== undefined && val.avg !== null) return val.avg;
                 if (val.sum !== undefined && val.sum !== null) return val.sum;
                 if (val.max !== undefined && val.max !== null) return val.max;
                 if (val.min !== undefined && val.min !== null) return val.min;
             }
             const parsed = parseFloat(String(val));
             if (isNaN(parsed)) {
                 if (String(val).trim() !== '') {
                     wcomLog(`Unerwarteter Nicht-Zahlenwert (NaN) von API empfangen: "${val}"`, 'debug');
                 }
                 return 0;
             }
             return parsed;
          }
          
          /**
          * Formatiert einen Datumsstring oder ein Date-Objekt ins Format DD.MM.YYYY basierend auf der lokalen Zeit.
          * @param dateInput UTC-String oder Date Objekt.
          * @returns Formatiertes lokales Datum.
          */
          function wcomFormatDate(dateInput: string | Date): string {
             if (!dateInput) return '';
             const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
             if (isNaN(date.getTime())) return String(dateInput);
             return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`;
          }
          
          /**
          * Ermittelt den ausgeschriebenen Wochentag basierend auf dem Datum und der Spracheinstellung.
          * @param dateStr Datumsstring.
          * @param locale Sprachcode (z.B. 'de').
          * @returns Wochentag als String.
          */
          function wcomGetDayName(dateStr: string, locale: string): string {
             if (!dateStr) return '';
             const date = new Date(dateStr);
             if (isNaN(date.getTime())) return '';
             return date.toLocaleDateString(locale, { weekday: 'long' });
          }
          
          /**
          * Holt die Geokoordinaten und Sprache aus den globalen ioBroker-Systemeinstellungen.
          * @returns SystemConfig Objekt mit lat, lon und lang, oder null bei Fehlern.
          */
          async function wcomGetSystemSettings(): Promise<SystemConfig | null> {
             let coords: { lat: string; lon: string } | null = null;
             if (CONFIG.LOCATION.FORCE_MANUAL && CONFIG.LOCATION.LAT && CONFIG.LOCATION.LON) {
                 const lat = parseFloat(CONFIG.LOCATION.LAT);
                 const lon = parseFloat(CONFIG.LOCATION.LON);
                 if (!isNaN(lat) && !isNaN(lon)) {
                     coords = { lat: lat.toFixed(3), lon: lon.toFixed(3) };
                 } else {
                     wcomLog('Manuelle Koordinaten sind ungültig (NaN).', 'error');
                     return null;
                 }
             }
          
             const systemConf: SystemConfig = await new Promise((resolve) => {
                 getObject('system.config', (err, obj: any) => {
                     if (!err && obj && obj.common) {
                         const sysLat = obj.common.latitude !== undefined && obj.common.latitude !== null ? parseFloat(String(obj.common.latitude)).toFixed(3) : null;
                         const sysLon = obj.common.longitude !== undefined && obj.common.longitude !== null ? parseFloat(String(obj.common.longitude)).toFixed(3) : null;
                         
                         resolve({
                             lat: sysLat,
                             lon: sysLon,
                             lang: obj.common.language || CONFIG.DEFAULT_LANGUAGE
                         });
                     } else { 
                         resolve({ lat: null, lon: null, lang: CONFIG.DEFAULT_LANGUAGE }); 
                     }
                 });
             });
          
             if (!coords && systemConf.lat && systemConf.lon) coords = { lat: systemConf.lat, lon: systemConf.lon };
             return coords ? { ...coords, lang: systemConf.lang } : null;
          }
          
          /**
          * Erstellt asynchron Ordner-Strukturen (Devices/Channels) im ioBroker Objektbaum unter Nutzung des RAM-Caches.
          * @param path Zielpfad im Objektbaum.
          * @param name Anzeigename.
          * @param type Objekttyp (device oder channel).
          */
          async function wcomEnsureSubStructure(path: string, name: string, type: 'device' | 'channel' = 'channel'): Promise<void> {
             if (!path || ensuredPaths.has(path)) return;
             if (!existsObject(path)) {
                 await extendObjectAsync(path, {
                     type: type,
                     common: { name: name },
                     native: {}
                 });
             }
             ensuredPaths.add(path);
          }
          
          /**
          * Erstellt asynchron Datenpunkte im ioBroker Objektbaum unter Nutzung des RAM-Caches.
          * @param path Zielpfad des Datenpunkts.
          * @param init Initialwert.
          * @param type Datentyp.
          * @param name Anzeigename.
          * @param role ioBroker-Rolle.
          * @param unit Physikalische Einheit (optional).
          * @param writeable Definiert, ob der Wert vom User beschrieben werden darf.
          */
          async function wcomEnsureState(path: string, init: any, type: iobJS.CommonType, name: string, role: string, unit?: string, writeable: boolean = false): Promise<void> {
             if (ensuredPaths.has(path)) return;
             if (!existsObject(path)) {
                 await createStateAsync(path, init, false, { name, type, role, unit: unit || '', read: true, write: writeable } as any);
             }
             ensuredPaths.add(path);
          }
          
          /**
          * Iteriert über STATE_DEFS und legt die Basis-Datenpunkte für einen spezifischen Forecast-Tag an.
          * @param path Zielpfad des Tages-Ordners.
          * @param index Index des Tages (0 = heute).
          */
          async function wcomEnsureDayStates(path: string, index: number): Promise<void> {
             const promises = Object.entries(STATE_DEFS).map(([id, cfg]) => {
                 return wcomEnsureState(`${path}.${id}`, cfg.init, cfg.type, `Tag ${index}: ${cfg.name}`, cfg.role, cfg.unit);
             });
             
             await Promise.all(promises);
          }
          
          /**
          * Führt einen asynchronen HTTP GET Request aus, abgesichert durch einen 10-Sekunden Timeout.
          * @KI_HINWEIS: Verhindert persistente Deadlocks im isFetching-Lock, falls die API oder das Netzwerk hängt.
          * @param url Die Ziel-URL.
          * @param options Header-Konfiguration.
          * @returns HTTP Response Objekt.
          */
          async function wcomHttpGetAsync(url: string, options: any): Promise<any> {
             let timeoutId: NodeJS.Timeout;
             
             const fetchPromise = new Promise((resolve, reject) => {
                 httpGet(url, options, (err, response) => {
                     if (err) reject(err);
                     else resolve(response);
                 });
             });
          
             const timeoutPromise = new Promise((_, reject) => {
                 timeoutId = setTimeout(() => reject(new Error('HTTP Timeout nach 10 Sekunden')), 10000);
             });
          
             try {
                 return await Promise.race([fetchPromise, timeoutPromise]);
             } finally {
                 if (timeoutId!) clearTimeout(timeoutId);
             }
          }
          
          // --- LOGIK ---
          
          /**
          * Prüft das verbleibende Monatsbudget und berechnet, ob ein Abruf zulässig ist.
          * @param source Ursprung des Triggers.
          * @returns True wenn Budget vorhanden, false wenn limitiert.
          */
          async function wcomCheckBudget(source: FetchSource): Promise<boolean> {
             const requestState = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_month`);
             const currentUsage = requestState && requestState.val !== null ? Number(requestState.val) : 0;
             
             if (currentUsage >= CONFIG.MONTHLY_LIMIT) {
                 wcomLog(`Monatslimit erreicht (${currentUsage}/${CONFIG.MONTHLY_LIMIT}). Skript pausiert automatisch bis zum 01. des Folgemonats.`, 'warn');
                 return false;
             }
          
             if (source === 'start') {
                 const todayState = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_today`);
                 if (todayState && todayState.val !== null && Number(todayState.val) > 0) {
                     wcomLog(`Skript-Neustart erkannt. Abruf übersprungen, da heute bereits Daten geladen wurden.`, 'debug');
                     return false;
                 }
             }
          
             const now = new Date();
             const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
             const daysLeft = daysInMonth - now.getDate(); 
          
             if (source === 'afternoon' || (source === 'start' && now.getHours() >= 12)) {
                 const callsNeededFor2xDaily = (daysLeft * 2) + 1; 
                 if (currentUsage + callsNeededFor2xDaily > CONFIG.MONTHLY_LIMIT) {
                     wcomLog(`SPARMODUS AKTIV: Nachmittags-Abruf übersprungen (Budget-Schutz). Verbrauch: ${currentUsage}`, 'warn');
                     return false;
                 }
             }
          
             if (source === 'morning' || (source === 'start' && now.getHours() < 12)) {
                 const callsNeededFor1xDaily = daysLeft + 1;
                 if (currentUsage + callsNeededFor1xDaily > CONFIG.MONTHLY_LIMIT) {
                     if (now.getDate() % 2 !== 0) {
                         wcomLog(`NOTLAUF AKTIV: Morgen-Abruf übersprungen (Budget extrem niedrig). Verbrauch: ${currentUsage}`, 'warn');
                         return false;
                     }
                 }
             }
          
             return true;
          }
          
          /**
          * Erkennt einen Tageswechsel lokal und setzt den Tageszähler asynchron zurück.
          */
          async function wcomCheckDailyReset(): Promise<void> {
             const nowStr = wcomFormatDate(new Date()); 
             const lastSyncState = await getStateAsync(`${CONFIG.DP_PATH}.info.last_sync`);
             
             if (lastSyncState && typeof lastSyncState.val === 'string') {
                 const lastSyncDate = lastSyncState.val.split(' ')[0]; 
                 if (lastSyncDate && lastSyncDate !== nowStr) {
                     wcomLog('Tageswechsel erkannt. Setze requests_today auf 0.', 'debug');
                     await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, 0, true);
                 }
             }
          }
          
          /**
          * Hauptfunktion für den Abruf und die Validierung der Wetterdaten.
          * @param source Ursprung des Triggers zur Budget-Berechnung.
          */
          async function wcomFetchWeatherData(source: FetchSource = 'start'): Promise<void> {
             if (isFetching) {
                 wcomLog('Abruf läuft bereits (Lock aktiv). Abbruch.', 'debug');
                 return;
             }
             isFetching = true;
          
             try {
                 await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
                 await wcomEnsureSubStructure(`${CONFIG.DP_PATH}.info`, 'Informationen');
                 
                 // @KI_HINWEIS: Initialization Order Fix (2.6.4). last_sync muss zwingend hier gesichert werden,
                 // bevor wcomUpdateUsageInfo aufgerufen wird.
                 await wcomEnsureState(`${CONFIG.DP_PATH}.info.last_sync`, '', 'string', 'Letztes Update', 'text');
                 await wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_month`, 0, 'number', 'Anfragen Monat', 'value');
                 await wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_today`, 0, 'number', 'Anfragen heute', 'value');
                 
                 await wcomEnsureState(CONFIG.DP_API_KEY, '', 'string', 'Wetter.com API Key', 'text', '', true);
                 await wcomEnsureState(CONFIG.DP_FORECAST_DAYS, 7, 'number', 'Vorhersage Tage', 'value', '', true);
                 await wcomEnsureState(CONFIG.DP_FORCE_RESET, false, 'boolean', 'Manueller Zähler-Reset', 'button', '', true);
          
                 if (source === 'start') {
                     await extendObjectAsync(CONFIG.DP_API_KEY, { common: { write: true } });
                     await extendObjectAsync(CONFIG.DP_FORECAST_DAYS, { common: { write: true } });
                 }
          
                 const apiKeyObj = await getStateAsync(CONFIG.DP_API_KEY);
                 const apiKeyValue = apiKeyObj ? String(apiKeyObj.val).trim() : '';
          
                 if (!apiKeyValue || apiKeyValue.length < 10) {
                     wcomLog(`Bitte gültigen API-Key im beschreibbaren Datenpunkt '${CONFIG.DP_API_KEY}' eintragen!`, 'error');
                     return;
                 }
          
                 const daysObj = await getStateAsync(CONFIG.DP_FORECAST_DAYS);
                 let forecastDays = daysObj && daysObj.val !== null ? Number(daysObj.val) : 7;
                 forecastDays = Math.max(1, Math.min(forecastDays, 16));
          
                 await wcomCheckDailyReset();
          
                 const allowFetch = await wcomCheckBudget(source);
                 if (!allowFetch) return;
          
                 const settings = await wcomGetSystemSettings();
                 if (!settings) return;
                 
                 wcomLog(`Abruf gestartet für Lat: ${settings.lat}, Lon: ${settings.lon} (Trigger: ${source}, Tage: ${forecastDays})`, 'info');
          
                 const url: string = `${CONFIG.BASE_URL}/forecast/${settings.lat}/${settings.lon}`;
                 const options = { headers: { 'x-api-key': apiKeyValue, 'Accept-Language': settings.lang } };
          
                 const response = await wcomHttpGetAsync(url, options);
          
                 if (response && response.statusCode === 429) {
                     wcomLog('Das Limit von 100 API-Calls im Monat ist ausgeschöpft (HTTP 429).', 'error');
                     await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, CONFIG.MONTHLY_LIMIT, true);
                     return;
                 }
          
                 if (response && response.statusCode !== 200) {
                     wcomLog(`API-Fehler: HTTP ${response.statusCode}`, 'error');
                     return;
                 }
          
                 // @KI_HINWEIS: SOFORTIGES Inkrement. Sichert die Limits ab, selbst wenn danach JSON-Fehler 
                 // oder Datenbank-Latenzen im System zu einem unvollständigen Skriptdurchlauf führen (Ghost-Call Fix).
                 await wcomUpdateUsageInfo();
          
                 let data: WetterComResponse;
                 try {
                     data = JSON.parse(response.data);
                 } catch (e) { 
                     wcomLog('Konnte API-Antwort nicht parsen.', 'error');
                     return; 
                 }
          
                 if (data && data.summary) {
                     await wcomProcessForecastData(data, settings.lang, forecastDays);
                     await wcomCleanupObsoleteDays(forecastDays);
                 }
          
             } catch (e: any) { 
                 wcomLog(`Script-Fehler: ${e.message}`, 'error'); 
             } finally {
                 isFetching = false;
             }
          }
          
          /**
          * Schreibt das validierte JSON in die ioBroker-Datenpunkte mittels Promise-Batching.
          * @param data Parsed JSON von Meteonomiqs.
          * @param lang Verwendete Sprache.
          * @param forecastDays Limitierung der Zukunfts-Tage aus den Einstellungen.
          */
          async function wcomProcessForecastData(data: WetterComResponse, lang: string, forecastDays: number): Promise<void> {
             await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
          
             const maxDays: number = Math.min((data.summary ?? []).length, forecastDays);
             let totalWrites = 0;
          
             for (let i = 0; i < maxDays; i++) {
                 const dayWriteBuffer: Promise<any>[] = [];
                 const day: ForecastSummary = data.summary[i];
                 const dayPath: string = `${CONFIG.DP_PATH}.day_${i}`;
                 
                 await wcomEnsureSubStructure(dayPath, `Tag ${i}`);
                 await wcomEnsureDayStates(dayPath, i);
          
                 // @KI_HINWEIS: Referenzdatum in strikt lokaler Zeit berechnen (Zeitzonen-Fix)
                 const dayDateStrLocal = wcomFormatDate(day.date);
                 const iconName = `d_${day.weather?.state ?? 0}.svg`;
          
                 dayWriteBuffer.push(
                     setStateChangedAsync(`${dayPath}.date`, String(wcomFormatDate(day.date)), true),
                     setStateChangedAsync(`${dayPath}.day_name`, String(wcomGetDayName(day.date, lang)), true),
                     setStateChangedAsync(`${dayPath}.temp_max`, wcomExtractValue(day.temperature?.max), true),
                     setStateChangedAsync(`${dayPath}.temp_min`, wcomExtractValue(day.temperature?.min), true),
                     setStateChangedAsync(`${dayPath}.weather_text`, String(day.weather?.text || ''), true),
                     setStateChangedAsync(`${dayPath}.weather_icon`, `${CONFIG.ICON_BASE_URL}/${iconName}`, true),
                     setStateChangedAsync(`${dayPath}.prec_probability`, wcomExtractValue(day.prec?.probability), true),
                     setStateChangedAsync(`${dayPath}.prec_sum`, wcomExtractValue(day.prec?.sum), true),
                     setStateChangedAsync(`${dayPath}.wind_gusts`, wcomExtractValue(day.wind?.gusts), true),
                     setStateChangedAsync(`${dayPath}.wind_speed_max`, wcomExtractValue(day.wind?.max ?? day.wind?.avg), true),
                     setStateChangedAsync(`${dayPath}.sun_hours`, wcomExtractValue(day.sunHours), true),
                     setStateChangedAsync(`${dayPath}.clouds`, wcomExtractValue(day.clouds), true),
                     setStateChangedAsync(`${dayPath}.humidity`, wcomExtractValue(day.relativeHumidity), true)
                 );
          
                 if (CONFIG.ENABLE_SPACES && data.spaces && data.spaces[i]) {
                     const spacesPath: string = `${dayPath}.spaces`;
                     await wcomEnsureSubStructure(spacesPath, 'Tagesabschnitte');
                     const segments: (keyof ForecastSpace)[] = ['morning', 'afternoon', 'evening', 'night'];
                     
                     for (const seg of segments) {
                         const sData = data.spaces[i][seg];
                         if (!sData) continue;
                         const sPath: string = `${spacesPath}.${seg}`;
                         await wcomEnsureSubStructure(sPath, seg);
                         
                         await Promise.all([
                             wcomEnsureState(`${sPath}.temp`, 0, 'number', 'Temperatur', 'value.temperature', '°C'),
                             wcomEnsureState(`${sPath}.text`, '', 'string', 'Wetter', 'weather.state'),
                             wcomEnsureState(`${sPath}.prec_prob`, 0, 'number', 'Regenrisiko', 'value.precipitation.probability', '%'),
                             wcomEnsureState(`${sPath}.prec_sum`, 0, 'number', 'Regenmenge', 'value.precipitation', 'mm'),
                             wcomEnsureState(`${sPath}.wind_speed`, 0, 'number', 'Windgeschwindigkeit', 'value.speed.wind', 'km/h'),
                             wcomEnsureState(`${sPath}.wind_gusts`, 0, 'number', 'Windböen', 'value.speed.wind.gust', 'km/h'),
                             wcomEnsureState(`${sPath}.clouds`, 0, 'number', 'Bewölkung', 'value', '%'),
                             wcomEnsureState(`${sPath}.humidity`, 0, 'number', 'Relative Feuchte', 'value.humidity', '%')
                         ]);
          
                         dayWriteBuffer.push(
                             setStateChangedAsync(`${sPath}.temp`, wcomExtractValue(sData.temperature), true),
                             setStateChangedAsync(`${sPath}.text`, String(sData.weather?.text || ''), true),
                             setStateChangedAsync(`${sPath}.prec_prob`, wcomExtractValue(sData.prec?.probability), true),
                             setStateChangedAsync(`${sPath}.prec_sum`, wcomExtractValue(sData.prec?.sum), true),
                             setStateChangedAsync(`${sPath}.wind_speed`, wcomExtractValue(sData.wind?.avg), true),
                             setStateChangedAsync(`${sPath}.wind_gusts`, wcomExtractValue(sData.wind?.gusts), true),
                             setStateChangedAsync(`${sPath}.clouds`, wcomExtractValue(sData.clouds), true),
                             setStateChangedAsync(`${sPath}.humidity`, wcomExtractValue(sData.relativeHumidity), true)
                         );
                     }
                 }
          
                 if (CONFIG.ENABLE_HOURLY && i <= 1 && data.hourly) {
                     const hourlyPath: string = `${dayPath}.hourly`;
                     await wcomEnsureSubStructure(hourlyPath, 'Stündlich');
                     
                     // @KI_HINWEIS: Filtern der Stunden über exaktes Matching des lokalen Datums-Strings zur Vermeidung von UTC-Versatz
                     const dayHours = (data.hourly ?? []).filter((h: ForecastHourly) => {
                         const hDateLocalStr = wcomFormatDate(h.from || h.date);
                         return hDateLocalStr === dayDateStrLocal;
                     });
                     
                     for (const h of dayHours) {
                         const hourDate: Date = new Date(h.from || h.date);
                         const hourNum: number = hourDate.getHours();
                         const hourLabel: string = String(hourNum).padStart(2, '0');
                         const hPath: string = `${hourlyPath}.${hourLabel}`;
                         
                         await wcomEnsureSubStructure(hPath, `${hourLabel}:00 Uhr`);
                         
                         const hourIcon = (hourNum >= 18 || hourNum < 6) ? `n_${h.weather?.state ?? 0}.svg` : `d_${h.weather?.state ?? 0}.svg`;
          
                         await Promise.all([
                             wcomEnsureState(`${hPath}.time`, '', 'string', 'Uhrzeit', 'text'),
                             wcomEnsureState(`${hPath}.from`, '', 'string', 'Zeitstempel (UTC)', 'text'),
                             wcomEnsureState(`${hPath}.temp`, 0, 'number', 'Temperatur', 'value.temperature', '°C'),
                             wcomEnsureState(`${hPath}.windchill`, 0, 'number', 'Gefühlt', 'value.temperature', '°C'),
                             wcomEnsureState(`${hPath}.weather_text`, '', 'string', 'Wetter', 'weather.state'),
                             wcomEnsureState(`${hPath}.weather_icon`, '', 'string', 'Wetter Icon', 'weather.icon'),
                             wcomEnsureState(`${hPath}.prec_prob`, 0, 'number', 'Regenwahrscheinlichkeit', 'value.precipitation.probability', '%'),
                             wcomEnsureState(`${hPath}.prec_sum`, 0, 'number', 'Regenmenge', 'value.precipitation', 'mm'),
                             wcomEnsureState(`${hPath}.wind_speed`, 0, 'number', 'Windgeschwindigkeit', 'value.speed.wind', 'km/h'),
                             wcomEnsureState(`${hPath}.wind_dir`, '', 'string', 'Windrichtung', 'weather.direction'),
                             wcomEnsureState(`${hPath}.wind_gusts`, 0, 'number', 'Windböen', 'value.speed.wind.gust', 'km/h'),
                             wcomEnsureState(`${hPath}.humidity`, 0, 'number', 'Relative Feuchte', 'value.humidity', '%')
                         ]);
          
                         dayWriteBuffer.push(
                             setStateChangedAsync(`${hPath}.time`, `${hourLabel}:00`, true),
                             setStateChangedAsync(`${hPath}.from`, String(h.from || h.date), true),
                             setStateChangedAsync(`${hPath}.temp`, wcomExtractValue(h.temperature), true),
                             setStateChangedAsync(`${hPath}.windchill`, wcomExtractValue(h.windchill), true),
                             setStateChangedAsync(`${hPath}.weather_text`, String(h.weather?.text || ''), true),
                             setStateChangedAsync(`${hPath}.weather_icon`, `${CONFIG.ICON_BASE_URL}/${hourIcon}`, true),
                             setStateChangedAsync(`${hPath}.prec_prob`, wcomExtractValue(h.prec?.probability), true),
                             setStateChangedAsync(`${hPath}.prec_sum`, wcomExtractValue(h.prec?.sum), true),
                             setStateChangedAsync(`${hPath}.wind_speed`, wcomExtractValue(h.wind?.avg), true),
                             setStateChangedAsync(`${hPath}.wind_dir`, String(h.wind?.direction || ''), true),
                             setStateChangedAsync(`${hPath}.wind_gusts`, wcomExtractValue(h.wind?.gusts), true),
                             setStateChangedAsync(`${hPath}.humidity`, wcomExtractValue(h.relativeHumidity), true)
                         );
                     }
                 }
                 
                 totalWrites += dayWriteBuffer.length;
                 await Promise.all(dayWriteBuffer);
             }
             wcomLog(`Update von ${maxDays} Tagen abgeschlossen (${totalWrites} Werte prozessiert).`, 'info');
          }
          
          /**
          * Löscht obsolete Tagesordner, falls die Vorhersage-Dauer reduziert wurde.
          * @param forecastDays Aktuell konfigurierte Maximaldauer.
          */
          async function wcomCleanupObsoleteDays(forecastDays: number): Promise<void> {
             for (let i = forecastDays; i <= 25; i++) {
                 const path: string = `${CONFIG.DP_PATH}.day_${i}`;
                 if (existsObject(path)) {
                     await deleteObjectAsync(path, true);
                 }
             }
          }
          
          /**
          * Aktualisiert den letzten Sync-Timestamp nach erfolgreichem HTTP 200.
          */
          async function wcomUpdateUsageInfo(): Promise<void> {
             const now: Date = new Date();
             const timestamp: string = `${String(now.getDate()).padStart(2,'0')}.${String(now.getMonth()+1).padStart(2,'0')}.${now.getFullYear()} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
             
             await setStateAsync(`${CONFIG.DP_PATH}.info.last_sync`, String(timestamp), true);
             
             const countToday = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_today`);
             await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, (countToday && countToday.val !== null ? Number(countToday.val) : 0) + 1, true);
          
             const countMonth = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_month`);
             await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, (countMonth && countMonth.val !== null ? Number(countMonth.val) : 0) + 1, true);
          }
          
          // --- ZEITSTEUERUNG & TRIGGER ---
          
          /**
          * Generiert einen Pseudo-Zufalls-Cronjob innerhalb eines definierten Stunden-Fensters zur Lastverteilung.
          * @param startHour Früheste Ausführungsstunde.
          * @param endHour Späteste Ausführungsstunde.
          * @param minMinute Minimale Minute (optional).
          * @returns Cron-String.
          */
          function wcomGetRandomCron(startHour: number, endHour: number, minMinute: number = 0): string {
             const hour: number = Math.floor(Math.random() * (endHour - startHour + 1)) + startHour;
             let minute: number = (hour === startHour) ? Math.floor(Math.random() * (60 - minMinute)) + minMinute : (hour === endHour ? 0 : Math.floor(Math.random() * 60));
             return `${minute} ${hour} * * *`;
          }
          
          schedule("0 0 1 * *", async () => {
             const path = `${CONFIG.DP_PATH}.info.requests_month`;
             if (existsState(path)) {
                 await setStateAsync(path, 0, true);
                 wcomLog('Monatszähler zurückgesetzt.', 'info');
             }
          });
          
          schedule(wcomGetRandomCron(0, 5, 2), () => wcomFetchWeatherData('morning'));
          schedule(wcomGetRandomCron(13, 17, 2), () => wcomFetchWeatherData('afternoon'));
          
          on({ id: CONFIG.DP_API_KEY, change: 'ne' }, (obj) => {
             if (obj.state && typeof obj.state.val === 'string' && obj.state.val.trim().length >= 10) {
                 wcomLog('Änderung des API-Keys erkannt. Starte sofortigen Test-Abruf...', 'info');
                 wcomFetchWeatherData('key_update');
             }
          });
          
          on({ id: CONFIG.DP_FORECAST_DAYS, change: 'ne' }, (obj) => {
             if (obj.state && obj.state.val !== null) {
                 wcomLog(`Änderung der Vorhersage-Tage auf ${obj.state.val} erkannt. Starte Aktualisierung & Bereinigung...`, 'info');
                 wcomFetchWeatherData('days_update');
             }
          });
          
          on({ id: CONFIG.DP_FORCE_RESET, change: 'any', val: true }, async () => {
             wcomLog('Manueller Reset ausgelöst. Setze Monatszähler auf 0.', 'warn');
             await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, 0, true);
             await setStateAsync(CONFIG.DP_FORCE_RESET, false, true);
             wcomFetchWeatherData('force_reset');
          });
          
          // Initialer Aufruf beim Skriptstart
          wcomFetchWeatherData('start');
          

          P 1 Antwort Letzte Antwort
          1
          • J Offline
            J Offline
            Jogis
            schrieb am zuletzt editiert von
            #68

            Dankeschön für deine Bemühung , werde nächste Woche den Skript testen 👍

            1 Antwort Letzte Antwort
            0
            • S Schimi

              @pk68 Hi danke für die Info....

              habe mal ne neue version erstellt, weil ich auch wieder ins Limit gelaufen bin... vielleicht hatte das sogar was miteinander zutun... falls nicht, bin ich langsam überfragt :-)

              /**
              * [Wetter.com Forecast API v4.0 (TrueScript)]
              * * CHANGELOG:
              * - 2.6.4: 2026-04-27 - HOTFIX (Initialization Order)
              * - FIX: State "info.last_sync" not found. Die Initialisierung des last_sync-Datenpunkts wurde an den Anfang der Fetch-Funktion vorgezogen, damit wcomUpdateUsageInfo (Ghost-Call Fix) nicht ins Leere schreibt.
              * - 2.6.3: CRITICAL BUGFIX UPDATE (Ghost-Calls & UTC-Offset)
              * - 2.6.2: FEATURE: Manueller Reset & Log-Präzision.
              * - 2.6.1: FIX: Konfigurations-Datenpunkte beschreibbar gemacht.
              * - 2.6.0: ULTRA-PERFORMANCE (RAM-Cache, wcomWait entfernt).
              * * KONTEXT:
              * - Hardware: ioBroker Server | Schnittstellen: Meteonomiqs API v4.0 (HTTP)
              * * ZIELE:
              * - Maximale Effizienz (Zero-Churn, Zero-I/O Overhead) und 100% typsichere Ausfallsicherheit unter Budget-Einhaltung.
              */
              
              // --- KONFIGURATION ---
              const CONFIG = {
                 // SECURITY: Den API Key NIEMALS hier im Klartext speichern!
                 DP_API_KEY: '0_userdata.0.wetter_com.info.api_key', 
                 DP_FORECAST_DAYS: '0_userdata.0.wetter_com.info.forecast_days',
                 DP_FORCE_RESET: '0_userdata.0.wetter_com.info.force_reset', 
                 
                 BASE_URL: 'https://forecast.meteonomiqs.com/v4_0',
                 ICON_BASE_URL: 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather',
                 DP_PATH: '0_userdata.0.wetter_com',
                 DEFAULT_LANGUAGE: 'de',
                 ENABLE_HOURLY: true,
                 ENABLE_SPACES: true,
                 MONTHLY_LIMIT: 100,
                 LOG_LEVEL: 'info' as 'debug' | 'info' | 'warn' | 'error',
                 LOCATION: {
                     LAT: '',
                     LON: '',
                     FORCE_MANUAL: false
                 }
              };
              
              // --- STATISCHE DEFINITIONEN ---
              const STATE_DEFS: Record<string, { name: string; type: iobJS.CommonType; role: string; unit?: string; init: any }> = {
                 'date': { name: 'Datum', type: 'string', role: 'text', init: '' },
                 'day_name': { name: 'Wochentag', type: 'string', role: 'text', init: '' },
                 'temp_max': { name: 'Max Temp', type: 'number', unit: '°C', role: 'value.temperature.max', init: 0 },
                 'temp_min': { name: 'Min Temp', type: 'number', unit: '°C', role: 'value.temperature.min', init: 0 },
                 'weather_text': { name: 'Wetter', type: 'string', role: 'weather.state', init: '' },
                 'weather_icon': { name: 'Icon URL', type: 'string', role: 'weather.icon', init: '' },
                 'prec_probability': { name: 'Regenrisiko', type: 'number', unit: '%', role: 'value.precipitation.probability', init: 0 },
                 'prec_sum': { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation', init: 0 },
                 'wind_gusts': { name: 'Windböen', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust', init: 0 },
                 'wind_speed_max': { name: 'Max. Windgeschwindigkeit', type: 'number', unit: 'km/h', role: 'value.speed.wind.max', init: 0 },
                 'sun_hours': { name: 'Sonnenstunden', type: 'number', unit: 'h', role: 'value.sun', init: 0 },
                 'clouds': { name: 'Bewölkung', type: 'number', unit: '%', role: 'value', init: 0 },
                 'humidity': { name: 'Relative Feuchte', type: 'number', unit: '%', role: 'value.humidity', init: 0 }
              };
              
              // --- INTERFACES ---
              
              type FetchSource = 'morning' | 'afternoon' | 'start' | 'key_update' | 'days_update' | 'force_reset';
              
              interface WetterComValue {
                 avg?: number;
                 value?: number;
                 sum?: number;
                 max?: number;
                 min?: number;
              }
              
              interface WetterComWeather {
                 state: number;
                 text: string;
                 icon?: string;
              }
              
              interface WetterComWind {
                 avg?: number | WetterComValue;
                 min?: number | WetterComValue;
                 max?: number | WetterComValue;
                 gusts?: number | WetterComValue | { value: number | null };
                 direction?: string;
                 unit?: string;
              }
              
              interface WetterComPrec {
                 probability: number;
                 sum: number | WetterComValue;
              }
              
              interface ForecastSummary {
                 date: string;
                 weather: WetterComWeather;
                 temperature: { min: number | WetterComValue; max: number | WetterComValue; avg?: number | WetterComValue };
                 wind: WetterComWind;
                 prec: WetterComPrec;
                 clouds: number | WetterComValue;
                 relativeHumidity: number | WetterComValue;
                 sunHours?: number;
              }
              
              interface ForecastSpaceSegment {
                 temperature: number | WetterComValue;
                 weather: WetterComWeather;
                 prec: WetterComPrec;
                 wind: WetterComWind;
                 clouds: number | WetterComValue;
                 relativeHumidity: number | WetterComValue;
              }
              
              interface ForecastSpace {
                 morning?: ForecastSpaceSegment;
                 afternoon?: ForecastSpaceSegment;
                 evening?: ForecastSpaceSegment;
                 night?: ForecastSpaceSegment;
              }
              
              interface ForecastHourly {
                 from: string;
                 date: string;
                 weather: WetterComWeather;
                 temperature: number | WetterComValue;
                 windchill: number | WetterComValue;
                 wind: WetterComWind;
                 prec: WetterComPrec;
                 relativeHumidity: number | WetterComValue;
              }
              
              interface WetterComResponse {
                 summary: ForecastSummary[];
                 spaces: ForecastSpace[];
                 hourly: ForecastHourly[];
              }
              
              interface SystemConfig {
                 lat: string | null;
                 lon: string | null;
                 lang: string;
              }
              
              // --- GLOBALE VARIABLEN ---
              let isFetching: boolean = false;
              const ensuredPaths = new Set<string>();
              
              // --- HILFSFUNKTIONEN ---
              
              /**
              * Filtert und gibt Log-Meldungen basierend auf dem konfigurierten Log-Level aus.
              * @param msg Die auszugebende Nachricht.
              * @param level Das Loglevel (debug, info, warn, error).
              */
              function wcomLog(msg: string, level: 'debug' | 'info' | 'warn' | 'error' = 'info'): void {
                 const levels = { debug: 0, info: 1, warn: 2, error: 3 };
                 if (levels[level] >= levels[CONFIG.LOG_LEVEL]) {
                     log(`[Wetter.com] ${msg}`, level);
                 }
              }
              
              /**
              * Extrahiert typsicher numerische Werte aus API-Objekten und fängt korrupte Rückgaben ab.
              * @KI_HINWEIS: Fängt null/undefined ab und loggt fehlerhaftes (NaN) API-Verhalten ohne zu crashen.
              * @param val Der rohe Wert aus der JSON-Antwort.
              * @returns Bereinigter numerischer Wert oder 0 als Fallback.
              */
              function wcomExtractValue(val: any): number {
                 if (val === null || val === undefined) return 0;
                 if (typeof val === 'number') {
                     if (isNaN(val)) {
                         wcomLog('API lieferte explizites NaN als number-Typ', 'debug');
                         return 0;
                     }
                     return val;
                 }
                 if (typeof val === 'object') {
                     if (val.value !== undefined && val.value !== null) return val.value;
                     if (val.avg !== undefined && val.avg !== null) return val.avg;
                     if (val.sum !== undefined && val.sum !== null) return val.sum;
                     if (val.max !== undefined && val.max !== null) return val.max;
                     if (val.min !== undefined && val.min !== null) return val.min;
                 }
                 const parsed = parseFloat(String(val));
                 if (isNaN(parsed)) {
                     if (String(val).trim() !== '') {
                         wcomLog(`Unerwarteter Nicht-Zahlenwert (NaN) von API empfangen: "${val}"`, 'debug');
                     }
                     return 0;
                 }
                 return parsed;
              }
              
              /**
              * Formatiert einen Datumsstring oder ein Date-Objekt ins Format DD.MM.YYYY basierend auf der lokalen Zeit.
              * @param dateInput UTC-String oder Date Objekt.
              * @returns Formatiertes lokales Datum.
              */
              function wcomFormatDate(dateInput: string | Date): string {
                 if (!dateInput) return '';
                 const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
                 if (isNaN(date.getTime())) return String(dateInput);
                 return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`;
              }
              
              /**
              * Ermittelt den ausgeschriebenen Wochentag basierend auf dem Datum und der Spracheinstellung.
              * @param dateStr Datumsstring.
              * @param locale Sprachcode (z.B. 'de').
              * @returns Wochentag als String.
              */
              function wcomGetDayName(dateStr: string, locale: string): string {
                 if (!dateStr) return '';
                 const date = new Date(dateStr);
                 if (isNaN(date.getTime())) return '';
                 return date.toLocaleDateString(locale, { weekday: 'long' });
              }
              
              /**
              * Holt die Geokoordinaten und Sprache aus den globalen ioBroker-Systemeinstellungen.
              * @returns SystemConfig Objekt mit lat, lon und lang, oder null bei Fehlern.
              */
              async function wcomGetSystemSettings(): Promise<SystemConfig | null> {
                 let coords: { lat: string; lon: string } | null = null;
                 if (CONFIG.LOCATION.FORCE_MANUAL && CONFIG.LOCATION.LAT && CONFIG.LOCATION.LON) {
                     const lat = parseFloat(CONFIG.LOCATION.LAT);
                     const lon = parseFloat(CONFIG.LOCATION.LON);
                     if (!isNaN(lat) && !isNaN(lon)) {
                         coords = { lat: lat.toFixed(3), lon: lon.toFixed(3) };
                     } else {
                         wcomLog('Manuelle Koordinaten sind ungültig (NaN).', 'error');
                         return null;
                     }
                 }
              
                 const systemConf: SystemConfig = await new Promise((resolve) => {
                     getObject('system.config', (err, obj: any) => {
                         if (!err && obj && obj.common) {
                             const sysLat = obj.common.latitude !== undefined && obj.common.latitude !== null ? parseFloat(String(obj.common.latitude)).toFixed(3) : null;
                             const sysLon = obj.common.longitude !== undefined && obj.common.longitude !== null ? parseFloat(String(obj.common.longitude)).toFixed(3) : null;
                             
                             resolve({
                                 lat: sysLat,
                                 lon: sysLon,
                                 lang: obj.common.language || CONFIG.DEFAULT_LANGUAGE
                             });
                         } else { 
                             resolve({ lat: null, lon: null, lang: CONFIG.DEFAULT_LANGUAGE }); 
                         }
                     });
                 });
              
                 if (!coords && systemConf.lat && systemConf.lon) coords = { lat: systemConf.lat, lon: systemConf.lon };
                 return coords ? { ...coords, lang: systemConf.lang } : null;
              }
              
              /**
              * Erstellt asynchron Ordner-Strukturen (Devices/Channels) im ioBroker Objektbaum unter Nutzung des RAM-Caches.
              * @param path Zielpfad im Objektbaum.
              * @param name Anzeigename.
              * @param type Objekttyp (device oder channel).
              */
              async function wcomEnsureSubStructure(path: string, name: string, type: 'device' | 'channel' = 'channel'): Promise<void> {
                 if (!path || ensuredPaths.has(path)) return;
                 if (!existsObject(path)) {
                     await extendObjectAsync(path, {
                         type: type,
                         common: { name: name },
                         native: {}
                     });
                 }
                 ensuredPaths.add(path);
              }
              
              /**
              * Erstellt asynchron Datenpunkte im ioBroker Objektbaum unter Nutzung des RAM-Caches.
              * @param path Zielpfad des Datenpunkts.
              * @param init Initialwert.
              * @param type Datentyp.
              * @param name Anzeigename.
              * @param role ioBroker-Rolle.
              * @param unit Physikalische Einheit (optional).
              * @param writeable Definiert, ob der Wert vom User beschrieben werden darf.
              */
              async function wcomEnsureState(path: string, init: any, type: iobJS.CommonType, name: string, role: string, unit?: string, writeable: boolean = false): Promise<void> {
                 if (ensuredPaths.has(path)) return;
                 if (!existsObject(path)) {
                     await createStateAsync(path, init, false, { name, type, role, unit: unit || '', read: true, write: writeable } as any);
                 }
                 ensuredPaths.add(path);
              }
              
              /**
              * Iteriert über STATE_DEFS und legt die Basis-Datenpunkte für einen spezifischen Forecast-Tag an.
              * @param path Zielpfad des Tages-Ordners.
              * @param index Index des Tages (0 = heute).
              */
              async function wcomEnsureDayStates(path: string, index: number): Promise<void> {
                 const promises = Object.entries(STATE_DEFS).map(([id, cfg]) => {
                     return wcomEnsureState(`${path}.${id}`, cfg.init, cfg.type, `Tag ${index}: ${cfg.name}`, cfg.role, cfg.unit);
                 });
                 
                 await Promise.all(promises);
              }
              
              /**
              * Führt einen asynchronen HTTP GET Request aus, abgesichert durch einen 10-Sekunden Timeout.
              * @KI_HINWEIS: Verhindert persistente Deadlocks im isFetching-Lock, falls die API oder das Netzwerk hängt.
              * @param url Die Ziel-URL.
              * @param options Header-Konfiguration.
              * @returns HTTP Response Objekt.
              */
              async function wcomHttpGetAsync(url: string, options: any): Promise<any> {
                 let timeoutId: NodeJS.Timeout;
                 
                 const fetchPromise = new Promise((resolve, reject) => {
                     httpGet(url, options, (err, response) => {
                         if (err) reject(err);
                         else resolve(response);
                     });
                 });
              
                 const timeoutPromise = new Promise((_, reject) => {
                     timeoutId = setTimeout(() => reject(new Error('HTTP Timeout nach 10 Sekunden')), 10000);
                 });
              
                 try {
                     return await Promise.race([fetchPromise, timeoutPromise]);
                 } finally {
                     if (timeoutId!) clearTimeout(timeoutId);
                 }
              }
              
              // --- LOGIK ---
              
              /**
              * Prüft das verbleibende Monatsbudget und berechnet, ob ein Abruf zulässig ist.
              * @param source Ursprung des Triggers.
              * @returns True wenn Budget vorhanden, false wenn limitiert.
              */
              async function wcomCheckBudget(source: FetchSource): Promise<boolean> {
                 const requestState = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_month`);
                 const currentUsage = requestState && requestState.val !== null ? Number(requestState.val) : 0;
                 
                 if (currentUsage >= CONFIG.MONTHLY_LIMIT) {
                     wcomLog(`Monatslimit erreicht (${currentUsage}/${CONFIG.MONTHLY_LIMIT}). Skript pausiert automatisch bis zum 01. des Folgemonats.`, 'warn');
                     return false;
                 }
              
                 if (source === 'start') {
                     const todayState = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_today`);
                     if (todayState && todayState.val !== null && Number(todayState.val) > 0) {
                         wcomLog(`Skript-Neustart erkannt. Abruf übersprungen, da heute bereits Daten geladen wurden.`, 'debug');
                         return false;
                     }
                 }
              
                 const now = new Date();
                 const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
                 const daysLeft = daysInMonth - now.getDate(); 
              
                 if (source === 'afternoon' || (source === 'start' && now.getHours() >= 12)) {
                     const callsNeededFor2xDaily = (daysLeft * 2) + 1; 
                     if (currentUsage + callsNeededFor2xDaily > CONFIG.MONTHLY_LIMIT) {
                         wcomLog(`SPARMODUS AKTIV: Nachmittags-Abruf übersprungen (Budget-Schutz). Verbrauch: ${currentUsage}`, 'warn');
                         return false;
                     }
                 }
              
                 if (source === 'morning' || (source === 'start' && now.getHours() < 12)) {
                     const callsNeededFor1xDaily = daysLeft + 1;
                     if (currentUsage + callsNeededFor1xDaily > CONFIG.MONTHLY_LIMIT) {
                         if (now.getDate() % 2 !== 0) {
                             wcomLog(`NOTLAUF AKTIV: Morgen-Abruf übersprungen (Budget extrem niedrig). Verbrauch: ${currentUsage}`, 'warn');
                             return false;
                         }
                     }
                 }
              
                 return true;
              }
              
              /**
              * Erkennt einen Tageswechsel lokal und setzt den Tageszähler asynchron zurück.
              */
              async function wcomCheckDailyReset(): Promise<void> {
                 const nowStr = wcomFormatDate(new Date()); 
                 const lastSyncState = await getStateAsync(`${CONFIG.DP_PATH}.info.last_sync`);
                 
                 if (lastSyncState && typeof lastSyncState.val === 'string') {
                     const lastSyncDate = lastSyncState.val.split(' ')[0]; 
                     if (lastSyncDate && lastSyncDate !== nowStr) {
                         wcomLog('Tageswechsel erkannt. Setze requests_today auf 0.', 'debug');
                         await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, 0, true);
                     }
                 }
              }
              
              /**
              * Hauptfunktion für den Abruf und die Validierung der Wetterdaten.
              * @param source Ursprung des Triggers zur Budget-Berechnung.
              */
              async function wcomFetchWeatherData(source: FetchSource = 'start'): Promise<void> {
                 if (isFetching) {
                     wcomLog('Abruf läuft bereits (Lock aktiv). Abbruch.', 'debug');
                     return;
                 }
                 isFetching = true;
              
                 try {
                     await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
                     await wcomEnsureSubStructure(`${CONFIG.DP_PATH}.info`, 'Informationen');
                     
                     // @KI_HINWEIS: Initialization Order Fix (2.6.4). last_sync muss zwingend hier gesichert werden,
                     // bevor wcomUpdateUsageInfo aufgerufen wird.
                     await wcomEnsureState(`${CONFIG.DP_PATH}.info.last_sync`, '', 'string', 'Letztes Update', 'text');
                     await wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_month`, 0, 'number', 'Anfragen Monat', 'value');
                     await wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_today`, 0, 'number', 'Anfragen heute', 'value');
                     
                     await wcomEnsureState(CONFIG.DP_API_KEY, '', 'string', 'Wetter.com API Key', 'text', '', true);
                     await wcomEnsureState(CONFIG.DP_FORECAST_DAYS, 7, 'number', 'Vorhersage Tage', 'value', '', true);
                     await wcomEnsureState(CONFIG.DP_FORCE_RESET, false, 'boolean', 'Manueller Zähler-Reset', 'button', '', true);
              
                     if (source === 'start') {
                         await extendObjectAsync(CONFIG.DP_API_KEY, { common: { write: true } });
                         await extendObjectAsync(CONFIG.DP_FORECAST_DAYS, { common: { write: true } });
                     }
              
                     const apiKeyObj = await getStateAsync(CONFIG.DP_API_KEY);
                     const apiKeyValue = apiKeyObj ? String(apiKeyObj.val).trim() : '';
              
                     if (!apiKeyValue || apiKeyValue.length < 10) {
                         wcomLog(`Bitte gültigen API-Key im beschreibbaren Datenpunkt '${CONFIG.DP_API_KEY}' eintragen!`, 'error');
                         return;
                     }
              
                     const daysObj = await getStateAsync(CONFIG.DP_FORECAST_DAYS);
                     let forecastDays = daysObj && daysObj.val !== null ? Number(daysObj.val) : 7;
                     forecastDays = Math.max(1, Math.min(forecastDays, 16));
              
                     await wcomCheckDailyReset();
              
                     const allowFetch = await wcomCheckBudget(source);
                     if (!allowFetch) return;
              
                     const settings = await wcomGetSystemSettings();
                     if (!settings) return;
                     
                     wcomLog(`Abruf gestartet für Lat: ${settings.lat}, Lon: ${settings.lon} (Trigger: ${source}, Tage: ${forecastDays})`, 'info');
              
                     const url: string = `${CONFIG.BASE_URL}/forecast/${settings.lat}/${settings.lon}`;
                     const options = { headers: { 'x-api-key': apiKeyValue, 'Accept-Language': settings.lang } };
              
                     const response = await wcomHttpGetAsync(url, options);
              
                     if (response && response.statusCode === 429) {
                         wcomLog('Das Limit von 100 API-Calls im Monat ist ausgeschöpft (HTTP 429).', 'error');
                         await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, CONFIG.MONTHLY_LIMIT, true);
                         return;
                     }
              
                     if (response && response.statusCode !== 200) {
                         wcomLog(`API-Fehler: HTTP ${response.statusCode}`, 'error');
                         return;
                     }
              
                     // @KI_HINWEIS: SOFORTIGES Inkrement. Sichert die Limits ab, selbst wenn danach JSON-Fehler 
                     // oder Datenbank-Latenzen im System zu einem unvollständigen Skriptdurchlauf führen (Ghost-Call Fix).
                     await wcomUpdateUsageInfo();
              
                     let data: WetterComResponse;
                     try {
                         data = JSON.parse(response.data);
                     } catch (e) { 
                         wcomLog('Konnte API-Antwort nicht parsen.', 'error');
                         return; 
                     }
              
                     if (data && data.summary) {
                         await wcomProcessForecastData(data, settings.lang, forecastDays);
                         await wcomCleanupObsoleteDays(forecastDays);
                     }
              
                 } catch (e: any) { 
                     wcomLog(`Script-Fehler: ${e.message}`, 'error'); 
                 } finally {
                     isFetching = false;
                 }
              }
              
              /**
              * Schreibt das validierte JSON in die ioBroker-Datenpunkte mittels Promise-Batching.
              * @param data Parsed JSON von Meteonomiqs.
              * @param lang Verwendete Sprache.
              * @param forecastDays Limitierung der Zukunfts-Tage aus den Einstellungen.
              */
              async function wcomProcessForecastData(data: WetterComResponse, lang: string, forecastDays: number): Promise<void> {
                 await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
              
                 const maxDays: number = Math.min((data.summary ?? []).length, forecastDays);
                 let totalWrites = 0;
              
                 for (let i = 0; i < maxDays; i++) {
                     const dayWriteBuffer: Promise<any>[] = [];
                     const day: ForecastSummary = data.summary[i];
                     const dayPath: string = `${CONFIG.DP_PATH}.day_${i}`;
                     
                     await wcomEnsureSubStructure(dayPath, `Tag ${i}`);
                     await wcomEnsureDayStates(dayPath, i);
              
                     // @KI_HINWEIS: Referenzdatum in strikt lokaler Zeit berechnen (Zeitzonen-Fix)
                     const dayDateStrLocal = wcomFormatDate(day.date);
                     const iconName = `d_${day.weather?.state ?? 0}.svg`;
              
                     dayWriteBuffer.push(
                         setStateChangedAsync(`${dayPath}.date`, String(wcomFormatDate(day.date)), true),
                         setStateChangedAsync(`${dayPath}.day_name`, String(wcomGetDayName(day.date, lang)), true),
                         setStateChangedAsync(`${dayPath}.temp_max`, wcomExtractValue(day.temperature?.max), true),
                         setStateChangedAsync(`${dayPath}.temp_min`, wcomExtractValue(day.temperature?.min), true),
                         setStateChangedAsync(`${dayPath}.weather_text`, String(day.weather?.text || ''), true),
                         setStateChangedAsync(`${dayPath}.weather_icon`, `${CONFIG.ICON_BASE_URL}/${iconName}`, true),
                         setStateChangedAsync(`${dayPath}.prec_probability`, wcomExtractValue(day.prec?.probability), true),
                         setStateChangedAsync(`${dayPath}.prec_sum`, wcomExtractValue(day.prec?.sum), true),
                         setStateChangedAsync(`${dayPath}.wind_gusts`, wcomExtractValue(day.wind?.gusts), true),
                         setStateChangedAsync(`${dayPath}.wind_speed_max`, wcomExtractValue(day.wind?.max ?? day.wind?.avg), true),
                         setStateChangedAsync(`${dayPath}.sun_hours`, wcomExtractValue(day.sunHours), true),
                         setStateChangedAsync(`${dayPath}.clouds`, wcomExtractValue(day.clouds), true),
                         setStateChangedAsync(`${dayPath}.humidity`, wcomExtractValue(day.relativeHumidity), true)
                     );
              
                     if (CONFIG.ENABLE_SPACES && data.spaces && data.spaces[i]) {
                         const spacesPath: string = `${dayPath}.spaces`;
                         await wcomEnsureSubStructure(spacesPath, 'Tagesabschnitte');
                         const segments: (keyof ForecastSpace)[] = ['morning', 'afternoon', 'evening', 'night'];
                         
                         for (const seg of segments) {
                             const sData = data.spaces[i][seg];
                             if (!sData) continue;
                             const sPath: string = `${spacesPath}.${seg}`;
                             await wcomEnsureSubStructure(sPath, seg);
                             
                             await Promise.all([
                                 wcomEnsureState(`${sPath}.temp`, 0, 'number', 'Temperatur', 'value.temperature', '°C'),
                                 wcomEnsureState(`${sPath}.text`, '', 'string', 'Wetter', 'weather.state'),
                                 wcomEnsureState(`${sPath}.prec_prob`, 0, 'number', 'Regenrisiko', 'value.precipitation.probability', '%'),
                                 wcomEnsureState(`${sPath}.prec_sum`, 0, 'number', 'Regenmenge', 'value.precipitation', 'mm'),
                                 wcomEnsureState(`${sPath}.wind_speed`, 0, 'number', 'Windgeschwindigkeit', 'value.speed.wind', 'km/h'),
                                 wcomEnsureState(`${sPath}.wind_gusts`, 0, 'number', 'Windböen', 'value.speed.wind.gust', 'km/h'),
                                 wcomEnsureState(`${sPath}.clouds`, 0, 'number', 'Bewölkung', 'value', '%'),
                                 wcomEnsureState(`${sPath}.humidity`, 0, 'number', 'Relative Feuchte', 'value.humidity', '%')
                             ]);
              
                             dayWriteBuffer.push(
                                 setStateChangedAsync(`${sPath}.temp`, wcomExtractValue(sData.temperature), true),
                                 setStateChangedAsync(`${sPath}.text`, String(sData.weather?.text || ''), true),
                                 setStateChangedAsync(`${sPath}.prec_prob`, wcomExtractValue(sData.prec?.probability), true),
                                 setStateChangedAsync(`${sPath}.prec_sum`, wcomExtractValue(sData.prec?.sum), true),
                                 setStateChangedAsync(`${sPath}.wind_speed`, wcomExtractValue(sData.wind?.avg), true),
                                 setStateChangedAsync(`${sPath}.wind_gusts`, wcomExtractValue(sData.wind?.gusts), true),
                                 setStateChangedAsync(`${sPath}.clouds`, wcomExtractValue(sData.clouds), true),
                                 setStateChangedAsync(`${sPath}.humidity`, wcomExtractValue(sData.relativeHumidity), true)
                             );
                         }
                     }
              
                     if (CONFIG.ENABLE_HOURLY && i <= 1 && data.hourly) {
                         const hourlyPath: string = `${dayPath}.hourly`;
                         await wcomEnsureSubStructure(hourlyPath, 'Stündlich');
                         
                         // @KI_HINWEIS: Filtern der Stunden über exaktes Matching des lokalen Datums-Strings zur Vermeidung von UTC-Versatz
                         const dayHours = (data.hourly ?? []).filter((h: ForecastHourly) => {
                             const hDateLocalStr = wcomFormatDate(h.from || h.date);
                             return hDateLocalStr === dayDateStrLocal;
                         });
                         
                         for (const h of dayHours) {
                             const hourDate: Date = new Date(h.from || h.date);
                             const hourNum: number = hourDate.getHours();
                             const hourLabel: string = String(hourNum).padStart(2, '0');
                             const hPath: string = `${hourlyPath}.${hourLabel}`;
                             
                             await wcomEnsureSubStructure(hPath, `${hourLabel}:00 Uhr`);
                             
                             const hourIcon = (hourNum >= 18 || hourNum < 6) ? `n_${h.weather?.state ?? 0}.svg` : `d_${h.weather?.state ?? 0}.svg`;
              
                             await Promise.all([
                                 wcomEnsureState(`${hPath}.time`, '', 'string', 'Uhrzeit', 'text'),
                                 wcomEnsureState(`${hPath}.from`, '', 'string', 'Zeitstempel (UTC)', 'text'),
                                 wcomEnsureState(`${hPath}.temp`, 0, 'number', 'Temperatur', 'value.temperature', '°C'),
                                 wcomEnsureState(`${hPath}.windchill`, 0, 'number', 'Gefühlt', 'value.temperature', '°C'),
                                 wcomEnsureState(`${hPath}.weather_text`, '', 'string', 'Wetter', 'weather.state'),
                                 wcomEnsureState(`${hPath}.weather_icon`, '', 'string', 'Wetter Icon', 'weather.icon'),
                                 wcomEnsureState(`${hPath}.prec_prob`, 0, 'number', 'Regenwahrscheinlichkeit', 'value.precipitation.probability', '%'),
                                 wcomEnsureState(`${hPath}.prec_sum`, 0, 'number', 'Regenmenge', 'value.precipitation', 'mm'),
                                 wcomEnsureState(`${hPath}.wind_speed`, 0, 'number', 'Windgeschwindigkeit', 'value.speed.wind', 'km/h'),
                                 wcomEnsureState(`${hPath}.wind_dir`, '', 'string', 'Windrichtung', 'weather.direction'),
                                 wcomEnsureState(`${hPath}.wind_gusts`, 0, 'number', 'Windböen', 'value.speed.wind.gust', 'km/h'),
                                 wcomEnsureState(`${hPath}.humidity`, 0, 'number', 'Relative Feuchte', 'value.humidity', '%')
                             ]);
              
                             dayWriteBuffer.push(
                                 setStateChangedAsync(`${hPath}.time`, `${hourLabel}:00`, true),
                                 setStateChangedAsync(`${hPath}.from`, String(h.from || h.date), true),
                                 setStateChangedAsync(`${hPath}.temp`, wcomExtractValue(h.temperature), true),
                                 setStateChangedAsync(`${hPath}.windchill`, wcomExtractValue(h.windchill), true),
                                 setStateChangedAsync(`${hPath}.weather_text`, String(h.weather?.text || ''), true),
                                 setStateChangedAsync(`${hPath}.weather_icon`, `${CONFIG.ICON_BASE_URL}/${hourIcon}`, true),
                                 setStateChangedAsync(`${hPath}.prec_prob`, wcomExtractValue(h.prec?.probability), true),
                                 setStateChangedAsync(`${hPath}.prec_sum`, wcomExtractValue(h.prec?.sum), true),
                                 setStateChangedAsync(`${hPath}.wind_speed`, wcomExtractValue(h.wind?.avg), true),
                                 setStateChangedAsync(`${hPath}.wind_dir`, String(h.wind?.direction || ''), true),
                                 setStateChangedAsync(`${hPath}.wind_gusts`, wcomExtractValue(h.wind?.gusts), true),
                                 setStateChangedAsync(`${hPath}.humidity`, wcomExtractValue(h.relativeHumidity), true)
                             );
                         }
                     }
                     
                     totalWrites += dayWriteBuffer.length;
                     await Promise.all(dayWriteBuffer);
                 }
                 wcomLog(`Update von ${maxDays} Tagen abgeschlossen (${totalWrites} Werte prozessiert).`, 'info');
              }
              
              /**
              * Löscht obsolete Tagesordner, falls die Vorhersage-Dauer reduziert wurde.
              * @param forecastDays Aktuell konfigurierte Maximaldauer.
              */
              async function wcomCleanupObsoleteDays(forecastDays: number): Promise<void> {
                 for (let i = forecastDays; i <= 25; i++) {
                     const path: string = `${CONFIG.DP_PATH}.day_${i}`;
                     if (existsObject(path)) {
                         await deleteObjectAsync(path, true);
                     }
                 }
              }
              
              /**
              * Aktualisiert den letzten Sync-Timestamp nach erfolgreichem HTTP 200.
              */
              async function wcomUpdateUsageInfo(): Promise<void> {
                 const now: Date = new Date();
                 const timestamp: string = `${String(now.getDate()).padStart(2,'0')}.${String(now.getMonth()+1).padStart(2,'0')}.${now.getFullYear()} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
                 
                 await setStateAsync(`${CONFIG.DP_PATH}.info.last_sync`, String(timestamp), true);
                 
                 const countToday = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_today`);
                 await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, (countToday && countToday.val !== null ? Number(countToday.val) : 0) + 1, true);
              
                 const countMonth = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_month`);
                 await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, (countMonth && countMonth.val !== null ? Number(countMonth.val) : 0) + 1, true);
              }
              
              // --- ZEITSTEUERUNG & TRIGGER ---
              
              /**
              * Generiert einen Pseudo-Zufalls-Cronjob innerhalb eines definierten Stunden-Fensters zur Lastverteilung.
              * @param startHour Früheste Ausführungsstunde.
              * @param endHour Späteste Ausführungsstunde.
              * @param minMinute Minimale Minute (optional).
              * @returns Cron-String.
              */
              function wcomGetRandomCron(startHour: number, endHour: number, minMinute: number = 0): string {
                 const hour: number = Math.floor(Math.random() * (endHour - startHour + 1)) + startHour;
                 let minute: number = (hour === startHour) ? Math.floor(Math.random() * (60 - minMinute)) + minMinute : (hour === endHour ? 0 : Math.floor(Math.random() * 60));
                 return `${minute} ${hour} * * *`;
              }
              
              schedule("0 0 1 * *", async () => {
                 const path = `${CONFIG.DP_PATH}.info.requests_month`;
                 if (existsState(path)) {
                     await setStateAsync(path, 0, true);
                     wcomLog('Monatszähler zurückgesetzt.', 'info');
                 }
              });
              
              schedule(wcomGetRandomCron(0, 5, 2), () => wcomFetchWeatherData('morning'));
              schedule(wcomGetRandomCron(13, 17, 2), () => wcomFetchWeatherData('afternoon'));
              
              on({ id: CONFIG.DP_API_KEY, change: 'ne' }, (obj) => {
                 if (obj.state && typeof obj.state.val === 'string' && obj.state.val.trim().length >= 10) {
                     wcomLog('Änderung des API-Keys erkannt. Starte sofortigen Test-Abruf...', 'info');
                     wcomFetchWeatherData('key_update');
                 }
              });
              
              on({ id: CONFIG.DP_FORECAST_DAYS, change: 'ne' }, (obj) => {
                 if (obj.state && obj.state.val !== null) {
                     wcomLog(`Änderung der Vorhersage-Tage auf ${obj.state.val} erkannt. Starte Aktualisierung & Bereinigung...`, 'info');
                     wcomFetchWeatherData('days_update');
                 }
              });
              
              on({ id: CONFIG.DP_FORCE_RESET, change: 'any', val: true }, async () => {
                 wcomLog('Manueller Reset ausgelöst. Setze Monatszähler auf 0.', 'warn');
                 await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, 0, true);
                 await setStateAsync(CONFIG.DP_FORCE_RESET, false, true);
                 wcomFetchWeatherData('force_reset');
              });
              
              // Initialer Aufruf beim Skriptstart
              wcomFetchWeatherData('start');
              

              P Offline
              P Offline
              pk68
              schrieb am zuletzt editiert von
              #69

              @Schimi sagte:

              habe mal ne neue version erstellt, weil ich auch wieder ins Limit gelaufen bin... >

              Danke fürs fixen. Vom ersten Eindruck passt das so.

              Ich bin vor 2 Tagen auch ans Limit gelaufen, obwohl der Monatszähler bestimmt unter 50 war. Den Zähler zu reseten hat nichts gebracht. Ging dann nur mit einem neuen API-Key.

              1 Antwort Letzte Antwort
              0
              • P Offline
                P Offline
                peterfido
                schrieb am zuletzt editiert von peterfido
                #70

                Hallo,

                bei mir läuft das Skript auch regelmäßig ins Limit. Ich schätze, dass der Scheduler nicht rund läuft oder die Schedules nicht gelöscht werden, wenn man das Skript neu startet.

                Vielleicht so ändern, dass man manuell die Uhrzeiten vorgibt. Oder, besser, der letzte Durchlauf 8 Stunden her sein muss.

                Gruß

                Peterfido

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

                  ich hatte es auch wieder... habe aber auch (wiedermal) sehr oft neu gestartet (Raspi, Javascript Adapter, etc)

                  Hier mal die gewünschte änderung... wahrscheinlich ist es so am ende am besten :-)

                  Ich ändere es auch im ersten Post

                  /**
                  * [Wetter.com Forecast API v4.0 (TrueScript)]
                  * * CHANGELOG:
                  * - 2.7.0: 2026-05-29 - SCHEDULING & RATE-LIMITING REWORK
                  * - FEATURE: Feste Cron-Zeiten in der Konfiguration definiert, statt schwer nachvollziehbarer Randomisierung.
                  * - FEATURE: Hard-Cooldown (8 Stunden) implementiert. Verhindert, dass durch mehrfache Skript-Neustarts am selben Tag das API-Limit aufgebraucht wird.
                  * - FIX: Zusätzlicher Datenpunkt 'info.last_sync_ts' für präzise Millisekunden-Cooldown-Berechnung eingeführt.
                  * - 2.6.4: HOTFIX (Initialization Order) - Ghost-Call Fix abgesichert.
                  * - 2.6.3: CRITICAL BUGFIX UPDATE (Ghost-Calls & UTC-Offset)
                  * - 2.6.0: ULTRA-PERFORMANCE (RAM-Cache, wcomWait entfernt).
                  * * KONTEXT:
                  * - Hardware: ioBroker Server | Schnittstellen: Meteonomiqs API v4.0 (HTTP)
                  * * ZIELE:
                  * - Maximale Effizienz (Zero-Churn, Zero-I/O Overhead) und 100% typsichere Ausfallsicherheit unter Budget-Einhaltung.
                  */
                  
                  // --- KONFIGURATION ---
                  const CONFIG = {
                     // SECURITY: Den API Key NIEMALS hier im Klartext speichern!
                     DP_API_KEY: '0_userdata.0.wetter_com.info.api_key', 
                     DP_FORECAST_DAYS: '0_userdata.0.wetter_com.info.forecast_days',
                     DP_FORCE_RESET: '0_userdata.0.wetter_com.info.force_reset', 
                     
                     // @KI_HINWEIS: Feste Update-Zeiten für maximale Transparenz (User-Feedback)
                     UPDATE_CRON_MORNING: '15 4 * * *',   // Täglich um 04:15 Uhr
                     UPDATE_CRON_AFTERNOON: '15 15 * * *',// Täglich um 15:15 Uhr
                     MIN_HOURS_BETWEEN_UPDATES: 12,        // Cooldown-Schutz vor Skript-Neustart-Loops
                     
                     BASE_URL: 'https://forecast.meteonomiqs.com/v4_0',
                     ICON_BASE_URL: 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather',
                     DP_PATH: '0_userdata.0.wetter_com',
                     DEFAULT_LANGUAGE: 'de',
                     ENABLE_HOURLY: true,
                     ENABLE_SPACES: true,
                     MONTHLY_LIMIT: 100,
                     LOG_LEVEL: 'info' as 'debug' | 'info' | 'warn' | 'error',
                     LOCATION: {
                         LAT: '',
                         LON: '',
                         FORCE_MANUAL: false
                     }
                  };
                  
                  // --- STATISCHE DEFINITIONEN ---
                  const STATE_DEFS: Record<string, { name: string; type: iobJS.CommonType; role: string; unit?: string; init: any }> = {
                     'date': { name: 'Datum', type: 'string', role: 'text', init: '' },
                     'day_name': { name: 'Wochentag', type: 'string', role: 'text', init: '' },
                     'temp_max': { name: 'Max Temp', type: 'number', unit: '°C', role: 'value.temperature.max', init: 0 },
                     'temp_min': { name: 'Min Temp', type: 'number', unit: '°C', role: 'value.temperature.min', init: 0 },
                     'weather_text': { name: 'Wetter', type: 'string', role: 'weather.state', init: '' },
                     'weather_icon': { name: 'Icon URL', type: 'string', role: 'weather.icon', init: '' },
                     'prec_probability': { name: 'Regenrisiko', type: 'number', unit: '%', role: 'value.precipitation.probability', init: 0 },
                     'prec_sum': { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation', init: 0 },
                     'wind_gusts': { name: 'Windböen', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust', init: 0 },
                     'wind_speed_max': { name: 'Max. Windgeschwindigkeit', type: 'number', unit: 'km/h', role: 'value.speed.wind.max', init: 0 },
                     'sun_hours': { name: 'Sonnenstunden', type: 'number', unit: 'h', role: 'value.sun', init: 0 },
                     'clouds': { name: 'Bewölkung', type: 'number', unit: '%', role: 'value', init: 0 },
                     'humidity': { name: 'Relative Feuchte', type: 'number', unit: '%', role: 'value.humidity', init: 0 }
                  };
                  
                  // --- INTERFACES ---
                  
                  type FetchSource = 'morning' | 'afternoon' | 'start' | 'key_update' | 'days_update' | 'force_reset';
                  
                  interface WetterComValue {
                     avg?: number;
                     value?: number;
                     sum?: number;
                     max?: number;
                     min?: number;
                  }
                  
                  interface WetterComWeather {
                     state: number;
                     text: string;
                     icon?: string;
                  }
                  
                  interface WetterComWind {
                     avg?: number | WetterComValue;
                     min?: number | WetterComValue;
                     max?: number | WetterComValue;
                     gusts?: number | WetterComValue | { value: number | null };
                     direction?: string;
                     unit?: string;
                  }
                  
                  interface WetterComPrec {
                     probability: number;
                     sum: number | WetterComValue;
                  }
                  
                  interface ForecastSummary {
                     date: string;
                     weather: WetterComWeather;
                     temperature: { min: number | WetterComValue; max: number | WetterComValue; avg?: number | WetterComValue };
                     wind: WetterComWind;
                     prec: WetterComPrec;
                     clouds: number | WetterComValue;
                     relativeHumidity: number | WetterComValue;
                     sunHours?: number;
                  }
                  
                  interface ForecastSpaceSegment {
                     temperature: number | WetterComValue;
                     weather: WetterComWeather;
                     prec: WetterComPrec;
                     wind: WetterComWind;
                     clouds: number | WetterComValue;
                     relativeHumidity: number | WetterComValue;
                  }
                  
                  interface ForecastSpace {
                     morning?: ForecastSpaceSegment;
                     afternoon?: ForecastSpaceSegment;
                     evening?: ForecastSpaceSegment;
                     night?: ForecastSpaceSegment;
                  }
                  
                  interface ForecastHourly {
                     from: string;
                     date: string;
                     weather: WetterComWeather;
                     temperature: number | WetterComValue;
                     windchill: number | WetterComValue;
                     wind: WetterComWind;
                     prec: WetterComPrec;
                     relativeHumidity: number | WetterComValue;
                  }
                  
                  interface WetterComResponse {
                     summary: ForecastSummary[];
                     spaces: ForecastSpace[];
                     hourly: ForecastHourly[];
                  }
                  
                  interface SystemConfig {
                     lat: string | null;
                     lon: string | null;
                     lang: string;
                  }
                  
                  // --- GLOBALE VARIABLEN ---
                  let isFetching: boolean = false;
                  const ensuredPaths = new Set<string>();
                  
                  // --- HILFSFUNKTIONEN ---
                  
                  /**
                  * Filtert und gibt Log-Meldungen basierend auf dem konfigurierten Log-Level aus.
                  * @param msg Die auszugebende Nachricht.
                  * @param level Das Loglevel (debug, info, warn, error).
                  */
                  function wcomLog(msg: string, level: 'debug' | 'info' | 'warn' | 'error' = 'info'): void {
                     const levels = { debug: 0, info: 1, warn: 2, error: 3 };
                     if (levels[level] >= levels[CONFIG.LOG_LEVEL]) {
                         log(`[Wetter.com] ${msg}`, level);
                     }
                  }
                  
                  /**
                  * Extrahiert typsicher numerische Werte aus API-Objekten und fängt korrupte Rückgaben ab.
                  * @KI_HINWEIS: Fängt null/undefined ab und loggt fehlerhaftes (NaN) API-Verhalten ohne zu crashen.
                  * @param val Der rohe Wert aus der JSON-Antwort.
                  * @returns Bereinigter numerischer Wert oder 0 als Fallback.
                  */
                  function wcomExtractValue(val: any): number {
                     if (val === null || val === undefined) return 0;
                     if (typeof val === 'number') {
                         if (isNaN(val)) {
                             wcomLog('API lieferte explizites NaN als number-Typ', 'debug');
                             return 0;
                         }
                         return val;
                     }
                     if (typeof val === 'object') {
                         if (val.value !== undefined && val.value !== null) return val.value;
                         if (val.avg !== undefined && val.avg !== null) return val.avg;
                         if (val.sum !== undefined && val.sum !== null) return val.sum;
                         if (val.max !== undefined && val.max !== null) return val.max;
                         if (val.min !== undefined && val.min !== null) return val.min;
                     }
                     const parsed = parseFloat(String(val));
                     if (isNaN(parsed)) {
                         if (String(val).trim() !== '') {
                             wcomLog(`Unerwarteter Nicht-Zahlenwert (NaN) von API empfangen: "${val}"`, 'debug');
                         }
                         return 0;
                     }
                     return parsed;
                  }
                  
                  /**
                  * Formatiert einen Datumsstring oder ein Date-Objekt ins Format DD.MM.YYYY basierend auf der lokalen Zeit.
                  * @param dateInput UTC-String oder Date Objekt.
                  * @returns Formatiertes lokales Datum.
                  */
                  function wcomFormatDate(dateInput: string | Date): string {
                     if (!dateInput) return '';
                     const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
                     if (isNaN(date.getTime())) return String(dateInput);
                     return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`;
                  }
                  
                  /**
                  * Ermittelt den ausgeschriebenen Wochentag basierend auf dem Datum und der Spracheinstellung.
                  * @param dateStr Datumsstring.
                  * @param locale Sprachcode (z.B. 'de').
                  * @returns Wochentag als String.
                  */
                  function wcomGetDayName(dateStr: string, locale: string): string {
                     if (!dateStr) return '';
                     const date = new Date(dateStr);
                     if (isNaN(date.getTime())) return '';
                     return date.toLocaleDateString(locale, { weekday: 'long' });
                  }
                  
                  /**
                  * Holt die Geokoordinaten und Sprache aus den globalen ioBroker-Systemeinstellungen.
                  * @returns SystemConfig Objekt mit lat, lon und lang, oder null bei Fehlern.
                  */
                  async function wcomGetSystemSettings(): Promise<SystemConfig | null> {
                     let coords: { lat: string; lon: string } | null = null;
                     if (CONFIG.LOCATION.FORCE_MANUAL && CONFIG.LOCATION.LAT && CONFIG.LOCATION.LON) {
                         const lat = parseFloat(CONFIG.LOCATION.LAT);
                         const lon = parseFloat(CONFIG.LOCATION.LON);
                         if (!isNaN(lat) && !isNaN(lon)) {
                             coords = { lat: lat.toFixed(3), lon: lon.toFixed(3) };
                         } else {
                             wcomLog('Manuelle Koordinaten sind ungültig (NaN).', 'error');
                             return null;
                         }
                     }
                  
                     const systemConf: SystemConfig = await new Promise((resolve) => {
                         getObject('system.config', (err, obj: any) => {
                             if (!err && obj && obj.common) {
                                 const sysLat = obj.common.latitude !== undefined && obj.common.latitude !== null ? parseFloat(String(obj.common.latitude)).toFixed(3) : null;
                                 const sysLon = obj.common.longitude !== undefined && obj.common.longitude !== null ? parseFloat(String(obj.common.longitude)).toFixed(3) : null;
                                 
                                 resolve({
                                     lat: sysLat,
                                     lon: sysLon,
                                     lang: obj.common.language || CONFIG.DEFAULT_LANGUAGE
                                 });
                             } else { 
                                 resolve({ lat: null, lon: null, lang: CONFIG.DEFAULT_LANGUAGE }); 
                             }
                         });
                     });
                  
                     if (!coords && systemConf.lat && systemConf.lon) coords = { lat: systemConf.lat, lon: systemConf.lon };
                     return coords ? { ...coords, lang: systemConf.lang } : null;
                  }
                  
                  /**
                  * Erstellt asynchron Ordner-Strukturen (Devices/Channels) im ioBroker Objektbaum unter Nutzung des RAM-Caches.
                  * @param path Zielpfad im Objektbaum.
                  * @param name Anzeigename.
                  * @param type Objekttyp (device oder channel).
                  */
                  async function wcomEnsureSubStructure(path: string, name: string, type: 'device' | 'channel' = 'channel'): Promise<void> {
                     if (!path || ensuredPaths.has(path)) return;
                     if (!existsObject(path)) {
                         await extendObjectAsync(path, {
                             type: type,
                             common: { name: name },
                             native: {}
                         });
                     }
                     ensuredPaths.add(path);
                  }
                  
                  /**
                  * Erstellt asynchron Datenpunkte im ioBroker Objektbaum unter Nutzung des RAM-Caches.
                  * @param path Zielpfad des Datenpunkts.
                  * @param init Initialwert.
                  * @param type Datentyp.
                  * @param name Anzeigename.
                  * @param role ioBroker-Rolle.
                  * @param unit Physikalische Einheit (optional).
                  * @param writeable Definiert, ob der Wert vom User beschrieben werden darf.
                  */
                  async function wcomEnsureState(path: string, init: any, type: iobJS.CommonType, name: string, role: string, unit?: string, writeable: boolean = false): Promise<void> {
                     if (ensuredPaths.has(path)) return;
                     if (!existsObject(path)) {
                         await createStateAsync(path, init, false, { name, type, role, unit: unit || '', read: true, write: writeable } as any);
                     }
                     ensuredPaths.add(path);
                  }
                  
                  /**
                  * Iteriert über STATE_DEFS und legt die Basis-Datenpunkte für einen spezifischen Forecast-Tag an.
                  * @param path Zielpfad des Tages-Ordners.
                  * @param index Index des Tages (0 = heute).
                  */
                  async function wcomEnsureDayStates(path: string, index: number): Promise<void> {
                     const promises = Object.entries(STATE_DEFS).map(([id, cfg]) => {
                         return wcomEnsureState(`${path}.${id}`, cfg.init, cfg.type, `Tag ${index}: ${cfg.name}`, cfg.role, cfg.unit);
                     });
                     
                     await Promise.all(promises);
                  }
                  
                  /**
                  * Führt einen asynchronen HTTP GET Request aus, abgesichert durch einen 10-Sekunden Timeout.
                  * @KI_HINWEIS: Verhindert persistente Deadlocks im isFetching-Lock, falls die API oder das Netzwerk hängt.
                  * @param url Die Ziel-URL.
                  * @param options Header-Konfiguration.
                  * @returns HTTP Response Objekt.
                  */
                  async function wcomHttpGetAsync(url: string, options: any): Promise<any> {
                     let timeoutId: NodeJS.Timeout;
                     
                     const fetchPromise = new Promise((resolve, reject) => {
                         httpGet(url, options, (err, response) => {
                             if (err) reject(err);
                             else resolve(response);
                         });
                     });
                  
                     const timeoutPromise = new Promise((_, reject) => {
                         timeoutId = setTimeout(() => reject(new Error('HTTP Timeout nach 10 Sekunden')), 10000);
                     });
                  
                     try {
                         return await Promise.race([fetchPromise, timeoutPromise]);
                     } finally {
                         if (timeoutId!) clearTimeout(timeoutId);
                     }
                  }
                  
                  // --- LOGIK ---
                  
                  /**
                  * Prüft das verbleibende Monatsbudget, wendet Spar-Modi an und validiert den Cooldown.
                  * @KI_HINWEIS: Cooldown-Implementierung (8h) blockiert Limit-Verstöße durch Skript-Neustarts.
                  * @param source Ursprung des Triggers.
                  * @returns True wenn Budget/Zeit vorhanden, false wenn limitiert oder Cooldown aktiv.
                  */
                  async function wcomCheckBudget(source: FetchSource): Promise<boolean> {
                     const requestState = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_month`);
                     const currentUsage = requestState && requestState.val !== null ? Number(requestState.val) : 0;
                     
                     if (currentUsage >= CONFIG.MONTHLY_LIMIT) {
                         wcomLog(`Monatslimit erreicht (${currentUsage}/${CONFIG.MONTHLY_LIMIT}). Skript pausiert automatisch bis zum 01. des Folgemonats.`, 'warn');
                         return false;
                     }
                  
                     // Harter Cooldown-Check bei automatisierten Abrufen
                     if (['morning', 'afternoon', 'start'].includes(source)) {
                         const lastTsState = await getStateAsync(`${CONFIG.DP_PATH}.info.last_sync_ts`);
                         if (lastTsState && lastTsState.val) {
                             const diffHours = (Date.now() - Number(lastTsState.val)) / 3600000;
                             if (diffHours < CONFIG.MIN_HOURS_BETWEEN_UPDATES) {
                                 wcomLog(`Abruf übersprungen (Cooldown aktiv). Letzter Abruf vor ${diffHours.toFixed(1)} Stunden. Minimum: ${CONFIG.MIN_HOURS_BETWEEN_UPDATES}h.`, 'debug');
                                 return false;
                             }
                         }
                     }
                  
                     const now = new Date();
                     const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
                     const daysLeft = daysInMonth - now.getDate(); 
                  
                     if (source === 'afternoon' || (source === 'start' && now.getHours() >= 12)) {
                         const callsNeededFor2xDaily = (daysLeft * 2) + 1; 
                         if (currentUsage + callsNeededFor2xDaily > CONFIG.MONTHLY_LIMIT) {
                             wcomLog(`SPARMODUS AKTIV: Nachmittags-Abruf übersprungen (Budget-Schutz). Verbrauch: ${currentUsage}`, 'warn');
                             return false;
                         }
                     }
                  
                     if (source === 'morning' || (source === 'start' && now.getHours() < 12)) {
                         const callsNeededFor1xDaily = daysLeft + 1;
                         if (currentUsage + callsNeededFor1xDaily > CONFIG.MONTHLY_LIMIT) {
                             if (now.getDate() % 2 !== 0) {
                                 wcomLog(`NOTLAUF AKTIV: Morgen-Abruf übersprungen (Budget extrem niedrig). Verbrauch: ${currentUsage}`, 'warn');
                                 return false;
                             }
                         }
                     }
                  
                     return true;
                  }
                  
                  /**
                  * Erkennt einen Tageswechsel lokal und setzt den Tageszähler asynchron zurück.
                  */
                  async function wcomCheckDailyReset(): Promise<void> {
                     const nowStr = wcomFormatDate(new Date()); 
                     const lastSyncState = await getStateAsync(`${CONFIG.DP_PATH}.info.last_sync`);
                     
                     if (lastSyncState && typeof lastSyncState.val === 'string') {
                         const lastSyncDate = lastSyncState.val.split(' ')[0]; 
                         if (lastSyncDate && lastSyncDate !== nowStr) {
                             wcomLog('Tageswechsel erkannt. Setze requests_today auf 0.', 'debug');
                             await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, 0, true);
                         }
                     }
                  }
                  
                  /**
                  * Hauptfunktion für den Abruf und die Validierung der Wetterdaten.
                  * @param source Ursprung des Triggers zur Budget-Berechnung.
                  */
                  async function wcomFetchWeatherData(source: FetchSource = 'start'): Promise<void> {
                     if (isFetching) {
                         wcomLog('Abruf läuft bereits (Lock aktiv). Abbruch.', 'debug');
                         return;
                     }
                     isFetching = true;
                  
                     try {
                         await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
                         await wcomEnsureSubStructure(`${CONFIG.DP_PATH}.info`, 'Informationen');
                         
                         await wcomEnsureState(`${CONFIG.DP_PATH}.info.last_sync`, '', 'string', 'Letztes Update', 'text');
                         await wcomEnsureState(`${CONFIG.DP_PATH}.info.last_sync_ts`, 0, 'number', 'Letztes Update (Timestamp)', 'value.time');
                         await wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_month`, 0, 'number', 'Anfragen Monat', 'value');
                         await wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_today`, 0, 'number', 'Anfragen heute', 'value');
                         
                         await wcomEnsureState(CONFIG.DP_API_KEY, '', 'string', 'Wetter.com API Key', 'text', '', true);
                         await wcomEnsureState(CONFIG.DP_FORECAST_DAYS, 7, 'number', 'Vorhersage Tage', 'value', '', true);
                         await wcomEnsureState(CONFIG.DP_FORCE_RESET, false, 'boolean', 'Manueller Zähler-Reset', 'button', '', true);
                  
                         if (source === 'start') {
                             await extendObjectAsync(CONFIG.DP_API_KEY, { common: { write: true } });
                             await extendObjectAsync(CONFIG.DP_FORECAST_DAYS, { common: { write: true } });
                         }
                  
                         const apiKeyObj = await getStateAsync(CONFIG.DP_API_KEY);
                         const apiKeyValue = apiKeyObj ? String(apiKeyObj.val).trim() : '';
                  
                         if (!apiKeyValue || apiKeyValue.length < 10) {
                             wcomLog(`Bitte gültigen API-Key im beschreibbaren Datenpunkt '${CONFIG.DP_API_KEY}' eintragen!`, 'error');
                             return;
                         }
                  
                         const daysObj = await getStateAsync(CONFIG.DP_FORECAST_DAYS);
                         let forecastDays = daysObj && daysObj.val !== null ? Number(daysObj.val) : 7;
                         forecastDays = Math.max(1, Math.min(forecastDays, 16));
                  
                         await wcomCheckDailyReset();
                  
                         const allowFetch = await wcomCheckBudget(source);
                         if (!allowFetch) return;
                  
                         const settings = await wcomGetSystemSettings();
                         if (!settings) return;
                         
                         wcomLog(`Abruf gestartet für Lat: ${settings.lat}, Lon: ${settings.lon} (Trigger: ${source}, Tage: ${forecastDays})`, 'info');
                  
                         const url: string = `${CONFIG.BASE_URL}/forecast/${settings.lat}/${settings.lon}`;
                         const options = { headers: { 'x-api-key': apiKeyValue, 'Accept-Language': settings.lang } };
                  
                         const response = await wcomHttpGetAsync(url, options);
                  
                         if (response && response.statusCode === 429) {
                             wcomLog('Das Limit von 100 API-Calls im Monat ist ausgeschöpft (HTTP 429).', 'error');
                             await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, CONFIG.MONTHLY_LIMIT, true);
                             return;
                         }
                  
                         if (response && response.statusCode !== 200) {
                             wcomLog(`API-Fehler: HTTP ${response.statusCode}`, 'error');
                             return;
                         }
                  
                         await wcomUpdateUsageInfo();
                  
                         let data: WetterComResponse;
                         try {
                             data = JSON.parse(response.data);
                         } catch (e) { 
                             wcomLog('Konnte API-Antwort nicht parsen.', 'error');
                             return; 
                         }
                  
                         if (data && data.summary) {
                             await wcomProcessForecastData(data, settings.lang, forecastDays);
                             await wcomCleanupObsoleteDays(forecastDays);
                         }
                  
                     } catch (e: any) { 
                         wcomLog(`Script-Fehler: ${e.message}`, 'error'); 
                     } finally {
                         isFetching = false;
                     }
                  }
                  
                  /**
                  * Schreibt das validierte JSON in die ioBroker-Datenpunkte mittels Promise-Batching.
                  * @param data Parsed JSON von Meteonomiqs.
                  * @param lang Verwendete Sprache.
                  * @param forecastDays Limitierung der Zukunfts-Tage aus den Einstellungen.
                  */
                  async function wcomProcessForecastData(data: WetterComResponse, lang: string, forecastDays: number): Promise<void> {
                     await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
                  
                     const maxDays: number = Math.min((data.summary ?? []).length, forecastDays);
                     let totalWrites = 0;
                  
                     for (let i = 0; i < maxDays; i++) {
                         const dayWriteBuffer: Promise<any>[] = [];
                         const day: ForecastSummary = data.summary[i];
                         const dayPath: string = `${CONFIG.DP_PATH}.day_${i}`;
                         
                         await wcomEnsureSubStructure(dayPath, `Tag ${i}`);
                         await wcomEnsureDayStates(dayPath, i);
                  
                         const dayDateStrLocal = wcomFormatDate(day.date);
                         const iconName = `d_${day.weather?.state ?? 0}.svg`;
                  
                         dayWriteBuffer.push(
                             setStateChangedAsync(`${dayPath}.date`, String(wcomFormatDate(day.date)), true),
                             setStateChangedAsync(`${dayPath}.day_name`, String(wcomGetDayName(day.date, lang)), true),
                             setStateChangedAsync(`${dayPath}.temp_max`, wcomExtractValue(day.temperature?.max), true),
                             setStateChangedAsync(`${dayPath}.temp_min`, wcomExtractValue(day.temperature?.min), true),
                             setStateChangedAsync(`${dayPath}.weather_text`, String(day.weather?.text || ''), true),
                             setStateChangedAsync(`${dayPath}.weather_icon`, `${CONFIG.ICON_BASE_URL}/${iconName}`, true),
                             setStateChangedAsync(`${dayPath}.prec_probability`, wcomExtractValue(day.prec?.probability), true),
                             setStateChangedAsync(`${dayPath}.prec_sum`, wcomExtractValue(day.prec?.sum), true),
                             setStateChangedAsync(`${dayPath}.wind_gusts`, wcomExtractValue(day.wind?.gusts), true),
                             setStateChangedAsync(`${dayPath}.wind_speed_max`, wcomExtractValue(day.wind?.max ?? day.wind?.avg), true),
                             setStateChangedAsync(`${dayPath}.sun_hours`, wcomExtractValue(day.sunHours), true),
                             setStateChangedAsync(`${dayPath}.clouds`, wcomExtractValue(day.clouds), true),
                             setStateChangedAsync(`${dayPath}.humidity`, wcomExtractValue(day.relativeHumidity), true)
                         );
                  
                         if (CONFIG.ENABLE_SPACES && data.spaces && data.spaces[i]) {
                             const spacesPath: string = `${dayPath}.spaces`;
                             await wcomEnsureSubStructure(spacesPath, 'Tagesabschnitte');
                             const segments: (keyof ForecastSpace)[] = ['morning', 'afternoon', 'evening', 'night'];
                             
                             for (const seg of segments) {
                                 const sData = data.spaces[i][seg];
                                 if (!sData) continue;
                                 const sPath: string = `${spacesPath}.${seg}`;
                                 await wcomEnsureSubStructure(sPath, seg);
                                 
                                 await Promise.all([
                                     wcomEnsureState(`${sPath}.temp`, 0, 'number', 'Temperatur', 'value.temperature', '°C'),
                                     wcomEnsureState(`${sPath}.text`, '', 'string', 'Wetter', 'weather.state'),
                                     wcomEnsureState(`${sPath}.prec_prob`, 0, 'number', 'Regenrisiko', 'value.precipitation.probability', '%'),
                                     wcomEnsureState(`${sPath}.prec_sum`, 0, 'number', 'Regenmenge', 'value.precipitation', 'mm'),
                                     wcomEnsureState(`${sPath}.wind_speed`, 0, 'number', 'Windgeschwindigkeit', 'value.speed.wind', 'km/h'),
                                     wcomEnsureState(`${sPath}.wind_gusts`, 0, 'number', 'Windböen', 'value.speed.wind.gust', 'km/h'),
                                     wcomEnsureState(`${sPath}.clouds`, 0, 'number', 'Bewölkung', 'value', '%'),
                                     wcomEnsureState(`${sPath}.humidity`, 0, 'number', 'Relative Feuchte', 'value.humidity', '%')
                                 ]);
                  
                                 dayWriteBuffer.push(
                                     setStateChangedAsync(`${sPath}.temp`, wcomExtractValue(sData.temperature), true),
                                     setStateChangedAsync(`${sPath}.text`, String(sData.weather?.text || ''), true),
                                     setStateChangedAsync(`${sPath}.prec_prob`, wcomExtractValue(sData.prec?.probability), true),
                                     setStateChangedAsync(`${sPath}.prec_sum`, wcomExtractValue(sData.prec?.sum), true),
                                     setStateChangedAsync(`${sPath}.wind_speed`, wcomExtractValue(sData.wind?.avg), true),
                                     setStateChangedAsync(`${sPath}.wind_gusts`, wcomExtractValue(sData.wind?.gusts), true),
                                     setStateChangedAsync(`${sPath}.clouds`, wcomExtractValue(sData.clouds), true),
                                     setStateChangedAsync(`${sPath}.humidity`, wcomExtractValue(sData.relativeHumidity), true)
                                 );
                             }
                         }
                  
                         if (CONFIG.ENABLE_HOURLY && i <= 1 && data.hourly) {
                             const hourlyPath: string = `${dayPath}.hourly`;
                             await wcomEnsureSubStructure(hourlyPath, 'Stündlich');
                             
                             const dayHours = (data.hourly ?? []).filter((h: ForecastHourly) => {
                                 const hDateLocalStr = wcomFormatDate(h.from || h.date);
                                 return hDateLocalStr === dayDateStrLocal;
                             });
                             
                             for (const h of dayHours) {
                                 const hourDate: Date = new Date(h.from || h.date);
                                 const hourNum: number = hourDate.getHours();
                                 const hourLabel: string = String(hourNum).padStart(2, '0');
                                 const hPath: string = `${hourlyPath}.${hourLabel}`;
                                 
                                 await wcomEnsureSubStructure(hPath, `${hourLabel}:00 Uhr`);
                                 
                                 const hourIcon = (hourNum >= 18 || hourNum < 6) ? `n_${h.weather?.state ?? 0}.svg` : `d_${h.weather?.state ?? 0}.svg`;
                  
                                 await Promise.all([
                                     wcomEnsureState(`${hPath}.time`, '', 'string', 'Uhrzeit', 'text'),
                                     wcomEnsureState(`${hPath}.from`, '', 'string', 'Zeitstempel (UTC)', 'text'),
                                     wcomEnsureState(`${hPath}.temp`, 0, 'number', 'Temperatur', 'value.temperature', '°C'),
                                     wcomEnsureState(`${hPath}.windchill`, 0, 'number', 'Gefühlt', 'value.temperature', '°C'),
                                     wcomEnsureState(`${hPath}.weather_text`, '', 'string', 'Wetter', 'weather.state'),
                                     wcomEnsureState(`${hPath}.weather_icon`, '', 'string', 'Wetter Icon', 'weather.icon'),
                                     wcomEnsureState(`${hPath}.prec_prob`, 0, 'number', 'Regenwahrscheinlichkeit', 'value.precipitation.probability', '%'),
                                     wcomEnsureState(`${hPath}.prec_sum`, 0, 'number', 'Regenmenge', 'value.precipitation', 'mm'),
                                     wcomEnsureState(`${hPath}.wind_speed`, 0, 'number', 'Windgeschwindigkeit', 'value.speed.wind', 'km/h'),
                                     wcomEnsureState(`${hPath}.wind_dir`, '', 'string', 'Windrichtung', 'weather.direction'),
                                     wcomEnsureState(`${hPath}.wind_gusts`, 0, 'number', 'Windböen', 'value.speed.wind.gust', 'km/h'),
                                     wcomEnsureState(`${hPath}.humidity`, 0, 'number', 'Relative Feuchte', 'value.humidity', '%')
                                 ]);
                  
                                 dayWriteBuffer.push(
                                     setStateChangedAsync(`${hPath}.time`, `${hourLabel}:00`, true),
                                     setStateChangedAsync(`${hPath}.from`, String(h.from || h.date), true),
                                     setStateChangedAsync(`${hPath}.temp`, wcomExtractValue(h.temperature), true),
                                     setStateChangedAsync(`${hPath}.windchill`, wcomExtractValue(h.windchill), true),
                                     setStateChangedAsync(`${hPath}.weather_text`, String(h.weather?.text || ''), true),
                                     setStateChangedAsync(`${hPath}.weather_icon`, `${CONFIG.ICON_BASE_URL}/${hourIcon}`, true),
                                     setStateChangedAsync(`${hPath}.prec_prob`, wcomExtractValue(h.prec?.probability), true),
                                     setStateChangedAsync(`${hPath}.prec_sum`, wcomExtractValue(h.prec?.sum), true),
                                     setStateChangedAsync(`${hPath}.wind_speed`, wcomExtractValue(h.wind?.avg), true),
                                     setStateChangedAsync(`${hPath}.wind_dir`, String(h.wind?.direction || ''), true),
                                     setStateChangedAsync(`${hPath}.wind_gusts`, wcomExtractValue(h.wind?.gusts), true),
                                     setStateChangedAsync(`${hPath}.humidity`, wcomExtractValue(h.relativeHumidity), true)
                                 );
                             }
                         }
                         
                         totalWrites += dayWriteBuffer.length;
                         await Promise.all(dayWriteBuffer);
                     }
                     wcomLog(`Update von ${maxDays} Tagen abgeschlossen (${totalWrites} Werte prozessiert).`, 'info');
                  }
                  
                  /**
                  * Löscht obsolete Tagesordner, falls die Vorhersage-Dauer reduziert wurde.
                  * @param forecastDays Aktuell konfigurierte Maximaldauer.
                  */
                  async function wcomCleanupObsoleteDays(forecastDays: number): Promise<void> {
                     for (let i = forecastDays; i <= 25; i++) {
                         const path: string = `${CONFIG.DP_PATH}.day_${i}`;
                         if (existsObject(path)) {
                             await deleteObjectAsync(path, true);
                         }
                     }
                  }
                  
                  /**
                  * Aktualisiert die Sync-Timestamps nach erfolgreichem HTTP 200.
                  */
                  async function wcomUpdateUsageInfo(): Promise<void> {
                     const now: Date = new Date();
                     const timestamp: string = `${String(now.getDate()).padStart(2,'0')}.${String(now.getMonth()+1).padStart(2,'0')}.${now.getFullYear()} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
                     
                     await setStateAsync(`${CONFIG.DP_PATH}.info.last_sync`, String(timestamp), true);
                     await setStateAsync(`${CONFIG.DP_PATH}.info.last_sync_ts`, now.getTime(), true); // Neu für Cooldown
                     
                     const countToday = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_today`);
                     await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, (countToday && countToday.val !== null ? Number(countToday.val) : 0) + 1, true);
                  
                     const countMonth = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_month`);
                     await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, (countMonth && countMonth.val !== null ? Number(countMonth.val) : 0) + 1, true);
                  }
                  
                  // --- ZEITSTEUERUNG & TRIGGER ---
                  
                  schedule("0 0 1 * *", async () => {
                     const path = `${CONFIG.DP_PATH}.info.requests_month`;
                     if (existsState(path)) {
                         await setStateAsync(path, 0, true);
                         wcomLog('Monatszähler zurückgesetzt.', 'info');
                     }
                  });
                  
                  // @KI_HINWEIS: Ersetzt die randomisierten Crons durch klar definierte, feste Zeiten im CONFIG.
                  schedule(CONFIG.UPDATE_CRON_MORNING, () => wcomFetchWeatherData('morning'));
                  schedule(CONFIG.UPDATE_CRON_AFTERNOON, () => wcomFetchWeatherData('afternoon'));
                  
                  on({ id: CONFIG.DP_API_KEY, change: 'ne' }, (obj) => {
                     if (obj.state && typeof obj.state.val === 'string' && obj.state.val.trim().length >= 10) {
                         wcomLog('Änderung des API-Keys erkannt. Starte Test-Abruf...', 'info');
                         wcomFetchWeatherData('key_update');
                     }
                  });
                  
                  on({ id: CONFIG.DP_FORECAST_DAYS, change: 'ne' }, (obj) => {
                     if (obj.state && obj.state.val !== null) {
                         wcomLog(`Änderung der Vorhersage-Tage auf ${obj.state.val} erkannt. Starte Aktualisierung & Bereinigung...`, 'info');
                         wcomFetchWeatherData('days_update');
                     }
                  });
                  
                  on({ id: CONFIG.DP_FORCE_RESET, change: 'any', val: true }, async () => {
                     wcomLog('Manueller Reset ausgelöst. Setze Monatszähler auf 0.', 'warn');
                     await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, 0, true);
                     await setStateAsync(CONFIG.DP_FORCE_RESET, false, true);
                     wcomFetchWeatherData('force_reset');
                  });
                  
                  // Initialer Aufruf beim Skriptstart
                  wcomFetchWeatherData('start');
                  
                  

                  1 Antwort Letzte Antwort
                  1
                  • P Offline
                    P Offline
                    peterfido
                    schrieb am zuletzt editiert von peterfido
                    #72

                    Danke,
                    witzig, in meinem Entwurf war es auch last_sync_ts.

                    Allerdings legt Dein Skript den Datenpunkt schreibgeschützt an. Wohl, weil der Wert nur vom Skript überschrieben werden soll.

                    Gruß

                    Peterfido

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

                      Habe mal "Claude" ausprobiert und das Skript überarbeiten lassen. ;-)

                      1 Antwort Letzte Antwort
                      0

                      Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.

                      Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.

                      Mit deinem Input könnte dieser Beitrag noch besser werden 💗

                      Registrieren Anmelden
                      Antworten
                      • In einem neuen Thema antworten
                      Anmelden zum Antworten
                      • Älteste zuerst
                      • Neuste zuerst
                      • Meiste Stimmen


                      Support us

                      ioBroker
                      Community Adapters
                      Donate

                      402

                      Online

                      33.0k

                      Benutzer

                      83.5k

                      Themen

                      1.3m

                      Beiträge
                      Community
                      Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                      ioBroker Community 2014-2026
                      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