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
    923

  • 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.5k 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 Offline
    S Offline
    Schimi
    schrieb am zuletzt editiert von Schimi
    #1

    [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 1 Antwort Letzte Antwort
    4
    • Pedder007P Offline
      Pedder007P Offline
      Pedder007
      schrieb am zuletzt editiert von
      #2

      Hi @schimi, evtl. bin ich ja gerade blind, aber wo finde ich Dein Script? :-)

      Pedder
      All @Proxmox/Trixie auf HP Elitedesk 800 G4; Zigbee: ZigStar (LAN), ~110Devices
      Unifi, Motioneye/3Reolinks, PiHole, Bosch CS7800i via BBQKees/EMS-ESP, Fronius/BYD 11kWp via Modbus
      Under construction: Smart-WoMo auf Raspi4

      sigi234S S 2 Antworten Letzte Antwort
      0
      • Pedder007P Pedder007

        Hi @schimi, evtl. bin ich ja gerade blind, aber wo finde ich Dein Script? :-)

        sigi234S Online
        sigi234S Online
        sigi234
        Forum Testing Most Active
        schrieb am zuletzt editiert von
        #3

        @Pedder007 sagte in [Skript] Wetter.com Forecast/Vorhersage:

        Hi @schimi, evtl. bin ich ja gerade blind, aber wo finde ich Dein Script? :-)

        Klick mal auf Spoiler

        Bitte benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.
        Immer Daten sichern!

        1 Antwort Letzte Antwort
        1
        • Pedder007P Pedder007

          Hi @schimi, evtl. bin ich ja gerade blind, aber wo finde ich Dein Script? :-)

          S Offline
          S Offline
          Schimi
          schrieb am zuletzt editiert von
          #4

          @Pedder007 wie @sigi234 schon schrieb.... :-)

          0e527a28-1eb5-456d-bcd0-897f1a089dbe-image.png

          1 Antwort Letzte Antwort
          1
          • nik82N Offline
            nik82N Offline
            nik82
            Most Active
            schrieb am zuletzt editiert von
            #5

            @schimi
            Ich finde dein Script absolut perfekt! Vielen lieben Dank, wollte schon lange die Werte von wetter.com mal haben.
            Da mir das so gut gefällt würde ich gerne komplett umsteigen.

            Hat irgendwer eine Lösung oder vielleicht ein fertiges Widget für VIS, wo man Symbole anzeigen kann?

            Man müsste ja den "weather_text" in ein Symbol umwandlen und dann mit "String img src" oder "Image 8" Widget arbeiten, aber dazu müsste man erstmal alle Zustände von "weather_text" haben. Aber vielleicht hat sich ja einer schon die Mühe gemacht?

            1 Antwort Letzte Antwort
            1
            • S Offline
              S Offline
              Schimi
              schrieb am zuletzt editiert von
              #6

              die stehen in der API...

              ich muss jetzt erstmal (ein Monat) warten (habe da kein druck weil die grundfunktion geht) weil ich die 100 abfragen erreicht habe... wollte dann aber mal schauen, die Std vorhhersagen einzubauen.

              kann dann auch nach einer brauchbareren lösung für die Icons gucken...

              habs gemacht weio wetter.com bei mir die besten vorhersagen liefert, openweathermap und DasWetter waren meistens (extrem) daneben....

              Ich bleibe aber dran 😉

              1 Antwort Letzte Antwort
              1
              • nik82N Offline
                nik82N Offline
                nik82
                Most Active
                schrieb am zuletzt editiert von
                #7

                Wi finde ich die Übersicht der einzelnen "weather_text" Passagen?

                Hab hier was gefunden:
                https://doc.meteonomiqs.com/doc/forecast_v4_0.html#section/References/Moonphase-and-Moonzodiac

                Aber das ist englisch und der Text im Iobroker ist ja Deutsch.
                Jetzt bräuchte ich die korrekte übersetzung, hätte da nämlich schon eine Idee wegen den Symbolen.
                Einfach die Symbole nach dem Text bennnen und dann mit Bindings arbeiten.
                Würde da mal was basteln wenn ich das komplett hätte.

                Und ja, ich finde wetter.com auch die beste Vorhersage 😀

                1 Antwort Letzte Antwort
                0
                • Pedder007P Offline
                  Pedder007P Offline
                  Pedder007
                  schrieb am zuletzt editiert von Pedder007
                  #8

                  @schimi und @sigi234 danke, super!
                  Ich war echt blind :-) ... und hat auf Anhieb funktioniert :-)

                  Pedder
                  All @Proxmox/Trixie auf HP Elitedesk 800 G4; Zigbee: ZigStar (LAN), ~110Devices
                  Unifi, Motioneye/3Reolinks, PiHole, Bosch CS7800i via BBQKees/EMS-ESP, Fronius/BYD 11kWp via Modbus
                  Under construction: Smart-WoMo auf Raspi4

                  1 Antwort Letzte Antwort
                  0
                  • S Offline
                    S Offline
                    Schimi
                    schrieb am zuletzt editiert von
                    #9

                    @nik82

                    Habe eine neue Version (1.4.8) unter dem Spoiler ganz oben versteckt.
                    Diese gibt auch die Icon-URL im Datenpunkt aus ;-)

                    nik82N 1 Antwort Letzte Antwort
                    2
                    • S Schimi

                      @nik82

                      Habe eine neue Version (1.4.8) unter dem Spoiler ganz oben versteckt.
                      Diese gibt auch die Icon-URL im Datenpunkt aus ;-)

                      nik82N Offline
                      nik82N Offline
                      nik82
                      Most Active
                      schrieb am zuletzt editiert von
                      #10

                      @Schimi sagte in [Skript] Wetter.com Forecast/Vorhersage:

                      @nik82

                      Habe eine neue Version (1.4.8) unter dem Spoiler ganz oben versteckt.
                      Diese gibt auch die Icon-URL im Datenpunkt aus ;-)

                      Hammer! Vielen lieben Dank 😀
                      Probiere ich morgen gleich aus.

                      1 Antwort Letzte Antwort
                      0
                      • nik82N Offline
                        nik82N Offline
                        nik82
                        Most Active
                        schrieb am zuletzt editiert von
                        #11

                        @schimi
                        Vielen Dank für die Icons, funktionieren wunderbar.
                        Jetzt hätte ich noch eines: Kannst du irgendwie den Wochentag noch einblenden lassen, ich bekomme es einfach nicht hin, dass Datum in einen Wochentag im VIS zu konvertieren und auf Aliase oder ein extra Javascript hab ich eigentlich keine Lust :-)

                        S 1 Antwort Letzte Antwort
                        0
                        • nik82N nik82

                          @schimi
                          Vielen Dank für die Icons, funktionieren wunderbar.
                          Jetzt hätte ich noch eines: Kannst du irgendwie den Wochentag noch einblenden lassen, ich bekomme es einfach nicht hin, dass Datum in einen Wochentag im VIS zu konvertieren und auf Aliase oder ein extra Javascript hab ich eigentlich keine Lust :-)

                          S Offline
                          S Offline
                          Schimi
                          schrieb am zuletzt editiert von
                          #12

                          @nik82 siehe Spoiler ;-)

                          1 Antwort Letzte Antwort
                          1
                          • nik82N Offline
                            nik82N Offline
                            nik82
                            Most Active
                            schrieb am zuletzt editiert von
                            #13

                            Du bist echt der Beste!!!!
                            Vielen Dank :-)

                            1 Antwort Letzte Antwort
                            0
                            • S Offline
                              S Offline
                              Schimi
                              schrieb am zuletzt editiert von
                              #14

                              Danke, aber ich mache nicht viel...

                              Das meiste macht "Gemini"

                              1 Antwort Letzte Antwort
                              0
                              • M Offline
                                M Offline
                                MartyBr
                                schrieb am zuletzt editiert von
                                #15

                                @schimi

                                Ich setze die 1.4.9 ein. Seit der 1.4.8 habe ich den u.a. Fehler. Der Datenpunkt liegt nicht vor. Muss er von Hand eingetragen werden?

                                
                                javascript.2	15:15:49.056	error	
                                [Wetter.com] Fehler: ICON_BASE_URL is not defined
                                
                                

                                Gruß
                                Martin


                                Intel NUCs mit Proxmox / Iobroker als VM unter Debian
                                Raspeberry mit USB Leseköpfen für Smartmeter
                                Homematic und Homematic IP

                                1 Antwort Letzte Antwort
                                0
                                • S Offline
                                  S Offline
                                  Schimi
                                  schrieb am zuletzt editiert von
                                  #16

                                  versuche mal die hier im Spoiler... bitte um Feedback, dann tausche ich die oben aus

                                  /**
                                  * ioBroker Script: Wetter.com Forecast API v4.0
                                  * API: https://doc.meteonomiqs.com/doc/forecast_v4_0.html
                                  * * Changelog:
                                  * 1.4.11: FIX: Fallback-Werte für Konfigurations-Variablen (ICON_BASE_URL, DEFAULT_LANGUAGE) integriert. 
                                  * Verhindert Abstürze ("ReferenceError"), wenn beim Update der Konfig-Bereich nicht aktualisiert wurde.
                                  * 1.4.10: Feature: Dynamische Spracheinstellung.
                                  * 1.4.8: Icon-URL Anpassung.
                                  * * free API-Key anfordern: https://www.meteonomiqs.com/de/wetter-api/#heading_PricePackages/
                                  */
                                  
                                  // --- KONFIGURATION ---
                                  const API_KEY = 'DEIN_API_KEY_HIER'; // <-- BITTE HIER DEINEN API-KEY EINTRAGEN
                                  const BASE_URL = 'https://forecast.meteonomiqs.com/v4_0';
                                  const ICON_BASE_URL = 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather'; 
                                  const DP_PATH = '0_userdata.0.wetter_com';
                                  const DEFAULT_LANGUAGE = 'de'; // Fallback, falls System-Sprache nicht ermittelbar
                                  const FORECAST_DAYS = 7; 
                                  
                                  // --- STANDORT KONFIGURATION ---
                                  const MANUAL_LATITUDE = ''; 
                                  const MANUAL_LONGITUDE = '';
                                  const FORCE_MANUAL_LOCATION = false; 
                                  
                                  // --- RANDOMISIERUNG DER ZEITEN ---
                                  
                                  function getRandomCron(startHour, endHour, minMinute = 0) {
                                     const hour = Math.floor(Math.random() * (endHour - startHour + 1)) + startHour;
                                     let minute;
                                     if (hour === startHour) {
                                         minute = Math.floor(Math.random() * (60 - minMinute)) + minMinute;
                                     } else if (hour === endHour) {
                                         minute = 0; 
                                     } else {
                                         minute = Math.floor(Math.random() * 60);
                                     }
                                     return `${minute} ${hour} * * *`;
                                  }
                                  
                                  const cron1 = getRandomCron(0, 5, 2);
                                  const cron2 = getRandomCron(13, 17, 2);
                                  
                                  console.log(`[Wetter.com] Schedules für heute gesetzt auf: "${cron1}" und "${cron2}"`);
                                  
                                  // --- HILFSFUNKTIONEN ---
                                  
                                  /**
                                  * Formatiert ein Datum manuell in das Format: "DD.MM.YYYY"
                                  */
                                  function formatDate(dateStr) {
                                     if (!dateStr) return '';
                                     const date = new Date(dateStr);
                                     if (isNaN(date.getTime())) return dateStr;
                                     const day = String(date.getDate()).padStart(2, '0');
                                     const month = String(date.getMonth() + 1).padStart(2, '0');
                                     const year = date.getFullYear();
                                     return `${day}.${month}.${year}`;
                                  }
                                  
                                  /**
                                  * Ermittelt den Wochentag basierend auf der übergebenen Sprache (Locale)
                                  */
                                  function getDayName(dateStr, locale) {
                                     if (!dateStr) return '';
                                     const date = new Date(dateStr);
                                     if (isNaN(date.getTime())) return '';
                                     return date.toLocaleDateString(locale, { weekday: 'long' });
                                  }
                                  
                                  /**
                                  * Ermittelt Einstellungen (Coords + Sprache) aus ioBroker System-Config
                                  */
                                  async function getSystemSettings() {
                                     // Safety check für Variablen, falls User Config nicht aktualisiert hat
                                     const manualLat = (typeof MANUAL_LATITUDE !== 'undefined') ? MANUAL_LATITUDE : '';
                                     const manualLon = (typeof MANUAL_LONGITUDE !== 'undefined') ? MANUAL_LONGITUDE : '';
                                     const forceManual = (typeof FORCE_MANUAL_LOCATION !== 'undefined') ? FORCE_MANUAL_LOCATION : false;
                                     const defLang = (typeof DEFAULT_LANGUAGE !== 'undefined') ? DEFAULT_LANGUAGE : 'de';
                                  
                                     // 1. Manuelle Location?
                                     let coords = null;
                                     if (forceManual && manualLat && manualLon) {
                                          console.log(`[Wetter.com] Nutze manuell konfigurierte Koordinaten (Override).`);
                                          coords = { lat: parseFloat(manualLat).toFixed(3), lon: parseFloat(manualLon).toFixed(3) };
                                     }
                                  
                                     // 2. System Config lesen
                                     const systemConf = await new Promise((resolve) => {
                                         getObject('system.config', (err, obj) => {
                                             if (!err && obj && obj.common) {
                                                 resolve({
                                                     lat: obj.common.latitude ? parseFloat(obj.common.latitude).toFixed(3) : null,
                                                     lon: obj.common.longitude ? parseFloat(obj.common.longitude).toFixed(3) : null,
                                                     lang: obj.common.language || defLang
                                                 });
                                             } else {
                                                 resolve({ lat: null, lon: null, lang: defLang });
                                             }
                                         });
                                     });
                                  
                                     // 3. Fallback Logik
                                     if (!coords) {
                                         if (systemConf.lat && systemConf.lon) {
                                             coords = { lat: systemConf.lat, lon: systemConf.lon };
                                         } else if (manualLat && manualLon) {
                                             console.log('[Wetter.com] Warnung: Keine System-Koordinaten. Nutze Fallback.');
                                             coords = { lat: parseFloat(manualLat).toFixed(3), lon: parseFloat(manualLon).toFixed(3) };
                                         }
                                     }
                                  
                                     if (!coords) return null;
                                  
                                     return { ...coords, lang: systemConf.lang };
                                  }
                                  
                                  async function ensureStructure(path, index, isInfo = false) {
                                     if (isInfo) {
                                         await createStateAsync(`${DP_PATH}.info.last_sync`, '', false, { name: 'Letztes erfolgreiches Update', type: 'string', role: 'text' });
                                         await createStateAsync(`${DP_PATH}.info.requests_today`, 0, false, { name: 'Anfragen heute', type: 'number', role: 'value' });
                                         await createStateAsync(`${DP_PATH}.info.requests_month`, 0, false, { name: 'Anfragen aktueller Monat', type: 'number', role: 'value' });
                                         await createStateAsync(`${DP_PATH}.info.next_schedules`, '', false, { name: 'Geplante Abrufe', type: 'string', role: 'text' });
                                         return;
                                     }
                                  
                                     const states = {
                                         'date': { name: 'Datum', type: 'string', role: 'text', def: '' },
                                         'day_name': { name: 'Wochentag', type: 'string', role: 'text', def: '' },
                                         'temp_max': { name: 'Max Temperatur', type: 'number', unit: '°C', role: 'value.temperature.max', def: 0 },
                                         'temp_min': { name: 'Min Temperatur', type: 'number', unit: '°C', role: 'value.temperature.min', def: 0 },
                                         'weather_text': { name: 'Wetterzustand', type: 'string', role: 'weather.state', def: '' },
                                         'weather_code': { name: 'Wetter Code', type: 'number', role: 'value', def: 0 },
                                         'weather_icon': { name: 'Wetter Icon URL', type: 'string', role: 'weather.icon', def: '' },
                                         'prec_probability': { name: 'Regenrisiko', type: 'number', unit: '%', role: 'value.precipitation.probability', def: 0 },
                                         'prec_sum': { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation', def: 0 },
                                         'sun_hours': { name: 'Sonnenstunden', type: 'number', unit: 'h', role: 'value.sunshine', def: 0 },
                                         'wind_speed_max': { name: 'Windböen Max', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust', def: 0 },
                                         'wind_direction': { name: 'Windrichtung', type: 'string', role: 'weather.direction', def: '' }
                                     };
                                  
                                     for (const [id, config] of Object.entries(states)) {
                                         await createStateAsync(`${path}.${id}`, config.def, false, {
                                             name: `Tag ${index}: ${config.name}`,
                                             type: config.type,
                                             role: config.role,
                                             unit: config.unit || '',
                                             read: true,
                                             write: false
                                         });
                                     }
                                  }
                                  
                                  async function performRequest(url, options) {
                                     return new Promise((resolve, reject) => {
                                         httpGet(url, options, (error, response) => {
                                             if (error) return reject(new Error(error));
                                             if (response.statusCode === 429) return reject(new Error('LIMIT_REACHED'));
                                             if (response.statusCode !== 200) return reject(new Error(`HTTP Status ${response.statusCode}`));
                                             resolve(response);
                                         });
                                     });
                                  }
                                  
                                  async function cleanupObsoleteDays() {
                                     const channels = $(`${DP_PATH}.day_*`);
                                     channels.each(function(id) {
                                         const parts = id.split('.');
                                         const lastPart = parts[parts.length - 1]; 
                                         const dayIndex = parseInt(lastPart.replace('day_', ''));
                                         if (!isNaN(dayIndex) && dayIndex >= FORECAST_DAYS) {
                                             deleteObject(id, true);
                                         }
                                     });
                                  }
                                  
                                  async function updateUsageInfo() {
                                     const now = new Date();
                                     const day = String(now.getDate()).padStart(2, '0');
                                     const month = String(now.getMonth() + 1).padStart(2, '0');
                                     const year = now.getFullYear();
                                     const hours = String(now.getHours()).padStart(2, '0');
                                     const minutes = String(now.getMinutes()).padStart(2, '0');
                                     const seconds = String(now.getSeconds()).padStart(2, '0');
                                     
                                     const timestamp = `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`;
                                     await setStateAsync(`${DP_PATH}.info.last_sync`, String(timestamp), true);
                                     
                                     const currentCountState = await getStateAsync(`${DP_PATH}.info.requests_today`);
                                     const currentCount = currentCountState ? (currentCountState.val || 0) : 0;
                                     await setStateAsync(`${DP_PATH}.info.requests_today`, currentCount + 1, true);
                                  
                                     const currentMonthState = await getStateAsync(`${DP_PATH}.info.requests_month`);
                                     const currentMonthCount = currentMonthState ? (currentMonthState.val || 0) : 0;
                                     await setStateAsync(`${DP_PATH}.info.requests_month`, currentMonthCount + 1, true);
                                     
                                     const s1 = cron1.split(' ');
                                     const s2 = cron2.split(' ');
                                     await setStateAsync(`${DP_PATH}.info.next_schedules`, `${s1[1].padStart(2,'0')}:${s1[0].padStart(2,'0')} Uhr & ${s2[1].padStart(2,'0')}:${s2[0].padStart(2,'0')} Uhr`, true);
                                  }
                                  
                                  // --- LOGIK ---
                                  
                                  async function fetchWeatherData() {
                                     try {
                                         const settings = await getSystemSettings();
                                         if (!settings) {
                                             console.error('[Wetter.com] Fehler: Keine Koordinaten gefunden!');
                                             return;
                                         }
                                         
                                         console.log(`[Wetter.com] Starte Abruf für Lat ${settings.lat}, Lon ${settings.lon} (Sprache: ${settings.lang})`);
                                  
                                         const url = `${BASE_URL}/forecast/${settings.lat}/${settings.lon}/summary`;
                                         const options = { headers: { 'x-api-key': API_KEY, 'Accept-Language': settings.lang } };
                                  
                                         try {
                                             const response = await performRequest(url, options);
                                             const data = JSON.parse(response.data);
                                             if (data && data.items) {
                                                 await processForecastData(data.items, settings.lang);
                                                 await cleanupObsoleteDays();
                                                 await updateUsageInfo();
                                             }
                                         } catch (err) {
                                             if (err.message === 'LIMIT_REACHED') {
                                                 console.error('[Wetter.com] Fehler 429: Monatslimit erreicht.');
                                             } else {
                                                 console.error(`[Wetter.com] Fehler: ${err.message}`);
                                             }
                                         }
                                     } catch (e) {
                                         console.error(`[Wetter.com] Script-Fehler: ${e.message}`);
                                     }
                                  }
                                  
                                  async function processForecastData(items, lang) {
                                     await ensureStructure('', 0, true); 
                                     const daysToProcess = Math.min(items.length, FORECAST_DAYS);
                                  
                                     // Fallback URL, falls Variable durch Teil-Update fehlt
                                     const iconBaseUrl = (typeof ICON_BASE_URL !== 'undefined') 
                                         ? ICON_BASE_URL 
                                         : 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather';
                                  
                                     for (let i = 0; i < daysToProcess; i++) {
                                         const day = items[i];
                                         const dayPath = `${DP_PATH}.day_${i}`;
                                         await ensureStructure(dayPath, i);
                                  
                                         const germanDate = formatDate(day.date);
                                         const dayName = getDayName(day.date, lang);
                                         
                                         const weatherCode = day.weather?.state ?? 0;
                                         const iconUrl = `${iconBaseUrl}/d_${weatherCode}.svg`;
                                  
                                         await setStateAsync(`${dayPath}.date`, germanDate, true);
                                         await setStateAsync(`${dayPath}.day_name`, dayName, true);
                                         await setStateAsync(`${dayPath}.temp_max`, day.temperature?.max ?? 0, true);
                                         await setStateAsync(`${dayPath}.temp_min`, day.temperature?.min ?? 0, true);
                                         await setStateAsync(`${dayPath}.weather_text`, day.weather?.text || '', true);
                                         await setStateAsync(`${dayPath}.weather_code`, weatherCode, true);
                                         await setStateAsync(`${dayPath}.weather_icon`, iconUrl, true);
                                         await setStateAsync(`${dayPath}.prec_probability`, day.prec?.probability ?? 0, true);
                                         await setStateAsync(`${dayPath}.prec_sum`, day.prec?.sum ?? 0, true);
                                         await setStateAsync(`${dayPath}.sun_hours`, day.sunHours ?? 0, true);
                                         await setStateAsync(`${dayPath}.wind_speed_max`, day.wind?.max ?? 0, true);
                                         await setStateAsync(`${dayPath}.wind_direction`, day.wind?.direction || '', true);
                                     }
                                     console.log(`[Wetter.com] Update von ${daysToProcess} Tagen abgeschlossen.`);
                                  }
                                  
                                  schedule("0 0 * * *", () => {
                                     setState(`${DP_PATH}.info.requests_today`, 0, true);
                                  });
                                  
                                  schedule("0 0 1 * *", () => {
                                     setState(`${DP_PATH}.info.requests_month`, 0, true);
                                  });
                                  
                                  schedule(cron1, fetchWeatherData);
                                  schedule(cron2, fetchWeatherData);
                                  
                                  ensureStructure('', 0, true).then(() => {
                                     fetchWeatherData();
                                  });
                                  

                                  1 Antwort Letzte Antwort
                                  1
                                  • M Offline
                                    M Offline
                                    MartyBr
                                    schrieb am zuletzt editiert von
                                    #17

                                    Sieht gut. Kein Fehler mehr.
                                    Ich lasse diese Version nun laufen.

                                    Danke für die prompte Fehlerbehebung.

                                    Gruß
                                    Martin


                                    Intel NUCs mit Proxmox / Iobroker als VM unter Debian
                                    Raspeberry mit USB Leseköpfen für Smartmeter
                                    Homematic und Homematic IP

                                    1 Antwort Letzte Antwort
                                    1
                                    • S Offline
                                      S Offline
                                      Schimi
                                      schrieb am zuletzt editiert von
                                      #18

                                      so, habe die 24 Std vorhersage eingebaut (und den 3ten API-Key verbraucht ;-))

                                      es gab auch eine große Änderung!!!!!

                                      Das Skript ist nun in TypeSkript geschrieben.

                                      Ihr müsst es als neues Skript importieren und dort "TypeSkript" auswählen

                                      Für mich wäre es dann jetzt "future-complete" und ich würde nur noch was bei Fehlern ändern und solange selber nutzen :-)

                                      1 Antwort Letzte Antwort
                                      0
                                      • M Offline
                                        M Offline
                                        MartyBr
                                        schrieb am zuletzt editiert von
                                        #19

                                        Leider wieder der Fehler mit ICON_BASE_URL:

                                        javascript.2
                                        2026-01-14 08:57:47.384	error	script.js.Wetter.Forecast_Wetter_com_1-8-3: TypeScript compilation failed:';; ^ERROR: Unterminated string literal. lang: obj.common.language || DEFAULT_LANGUAGE ^ERROR: Cannot find name 'DEFAULT_LANGUAGE'. resolve({ lat: null, lon: null, lang: DEFAULT_LANGUAGE }); ^ERROR: Cannot find name 'DEFAULT_LANGUAGE'. await setStateAsync(`${dayPath}.weather_icon`, String(`${ICON_BASE_URL}/d_${day.weather?.state}.svg`), true); ^ERROR: Cannot find name 'ICON_BASE_URL'. await setStateChangedAsync(`${hPath}.weather_icon`, String(`${ICON_BASE_URL}/d_${h.weather?.state}.svg`), true); ^ERROR: Cannot find name 'ICON_BASE_URL'.
                                        
                                        javascript.2
                                        2026-01-14 08:57:47.297	info	script.js.Wetter.Forecast_Wetter_com_1-8-3: Compiling TypeScript source
                                        
                                        javascript.2
                                        2026-01-14 08:57:43.337	info	script.js.Wetter.Forecast_Wetter_com_1-8-3: Stopping script
                                        
                                        javascript.2
                                        2026-01-14 08:57:31.016	error	script.js.Wetter.Forecast_Wetter_com_1-8-3: TypeScript compilation failed:';; ^ERROR: Unterminated string literal. lang: obj.common.language || DEFAULT_LANGUAGE ^ERROR: Cannot find name 'DEFAULT_LANGUAGE'. resolve({ lat: null, lon: null, lang: DEFAULT_LANGUAGE }); ^ERROR: Cannot find name 'DEFAULT_LANGUAGE'. await setStateAsync(`${dayPath}.weather_icon`, String(`${ICON_BASE_URL}/d_${day.weather?.state}.svg`), true); ^ERROR: Cannot find name 'ICON_BASE_URL'. await setStateChangedAsync(`${hPath}.weather_icon`, String(`${ICON_BASE_URL}/d_${h.weather?.state}.svg`), true); ^ERROR: Cannot find name 'ICON_BASE_URL'.
                                        
                                        javascript.2
                                        2026-01-14 08:57:30.119	info	script.js.Wetter.Forecast_Wetter_com_1-8-3: Compiling TypeScript source
                                        

                                        Gruß
                                        Martin


                                        Intel NUCs mit Proxmox / Iobroker als VM unter Debian
                                        Raspeberry mit USB Leseköpfen für Smartmeter
                                        Homematic und Homematic IP

                                        1 Antwort Letzte Antwort
                                        0
                                        • S Offline
                                          S Offline
                                          Schimi
                                          schrieb am zuletzt editiert von Schimi
                                          #20

                                          @martybr versuche mal das im Spoiler... vielleicht auch mal den Datenpunkt komplett löschen und neu erstellen lassen (in den Objekten)

                                          Nicht das durch den Wechsel auf TypeSkript da auch was geändert wurde

                                          /**
                                          * ioBroker Script: Wetter.com Forecast API v4.0 (TrueScript)
                                          * API: https://doc.meteonomiqs.com/doc/forecast_v4_0.html
                                          * * * Changelog:
                                          * 1.8.4: FIX: Compiler-Fehler & Syntax-Bereinigung.
                                          * - Konfiguration in 'CONFIG'-Objekt gekapselt, um Scope-Fehler ("Cannot find name") zu beheben.
                                          * - Bereinigung von Template-Strings (entfernte redundante String()-Wrapper).
                                          * - Typsichere Implementierung von getObject-Callbacks.
                                          * 1.8.3: FIX: Namenskonflikte behoben (wcom-Präfix).
                                          */
                                          
                                          // --- KONFIGURATION ---
                                          const CONFIG = {
                                             API_KEY: 'DEIN_API_KEY_HIER', // <-- BITTE HIER DEINEN API-KEY EINTRAGEN
                                             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',
                                             FORECAST_DAYS: 7, // max. 16 Tage
                                             ENABLE_HOURLY: true,
                                             ENABLE_SPACES: true,
                                             LOCATION: {
                                                 LAT: '', // Leer lassen für System-Einstellung
                                                 LON: '', // Leer lassen für System-Einstellung
                                                 FORCE_MANUAL: false
                                             }
                                          };
                                          
                                          // --- INTERFACES ---
                                          
                                          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;
                                             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;
                                          }
                                          
                                          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[];
                                          }
                                          
                                          // --- HILFSFUNKTIONEN ---
                                          
                                          const wcomWait = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
                                          
                                          function wcomExtractValue(val: any): number {
                                             if (val === null || val === undefined) return 0;
                                             if (typeof val === 'number') return val;
                                             if (typeof val === 'object') {
                                                 if (val.avg !== undefined) return val.avg;
                                                 if (val.value !== undefined) return val.value;
                                                 if (val.sum !== undefined) return val.sum;
                                                 if (val.max !== undefined) return val.max;
                                                 if (val.min !== undefined) return val.min;
                                                 if (val.gusts && typeof val.gusts === 'object' && val.gusts.value !== undefined) return val.gusts.value;
                                             }
                                             return parseFloat(String(val)) || 0;
                                          }
                                          
                                          function wcomFormatDate(dateStr: string): string {
                                             if (!dateStr) return '';
                                             const date = new Date(dateStr);
                                             if (isNaN(date.getTime())) return dateStr;
                                             return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`;
                                          }
                                          
                                          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' });
                                          }
                                          
                                          async function wcomGetSystemSettings(): Promise<{ lat: string; lon: string; lang: string } | null> {
                                             let coords: { lat: string; lon: string } | null = null;
                                             if (CONFIG.LOCATION.FORCE_MANUAL && CONFIG.LOCATION.LAT && CONFIG.LOCATION.LON) {
                                                  coords = { lat: parseFloat(CONFIG.LOCATION.LAT).toFixed(3), lon: parseFloat(CONFIG.LOCATION.LON).toFixed(3) };
                                             }
                                          
                                             const systemConf: any = await new Promise((resolve) => {
                                                 getObject('system.config', (err, obj: any) => {
                                                     if (!err && obj && obj.common) {
                                                         resolve({
                                                             lat: obj.common.latitude ? parseFloat(String(obj.common.latitude)).toFixed(3) : null,
                                                             lon: obj.common.longitude ? parseFloat(String(obj.common.longitude)).toFixed(3) : null,
                                                             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;
                                          }
                                          
                                          async function wcomEnsureSubStructure(path: string, name: string, type: 'device' | 'channel' = 'channel'): Promise<void> {
                                             if (!path) return;
                                             if (!existsObject(path)) {
                                                 await extendObjectAsync(path, {
                                                     type: type,
                                                     common: { name: name },
                                                     native: {}
                                                 });
                                                 await wcomWait(50);
                                             }
                                          }
                                          
                                          async function wcomEnsureDayStates(path: string, index: number): Promise<void> {
                                             const states: 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 },
                                                 'clouds': { name: 'Bewölkung', type: 'number', unit: '%', role: 'value', init: 0 },
                                                 'humidity': { name: 'Relative Feuchte', type: 'number', unit: '%', role: 'value.humidity', init: 0 }
                                             };
                                          
                                             for (const [id, cfg] of Object.entries(states)) {
                                                 await createStateAsync(`${path}.${id}`, cfg.init, false, { 
                                                     name: `Tag ${index}: ${cfg.name}`, 
                                                     type: cfg.type,
                                                     role: cfg.role,
                                                     unit: cfg.unit || '',
                                                     read: true, 
                                                     write: false 
                                                 });
                                             }
                                          }
                                          
                                          // --- LOGIK ---
                                          
                                          async function wcomFetchWeatherData(): Promise<void> {
                                             try {
                                                 const settings = await wcomGetSystemSettings();
                                                 if (!settings) return;
                                                 
                                                 console.log(`[Wetter.com] Abruf für Lat ${settings.lat}, Lon ${settings.lon}`);
                                          
                                                 const url: string = `${CONFIG.BASE_URL}/forecast/${settings.lat}/${settings.lon}`;
                                                 const options = { headers: { 'x-api-key': CONFIG.API_KEY, 'Accept-Language': settings.lang } };
                                          
                                                 httpGet(url, options, async (err, response) => {
                                                     if (response && response.statusCode === 429) {
                                                         console.error('[Wetter.com] Das Limit von 100 API-Calls im Monat ist ausgeschöpft.');
                                                         return;
                                                     }
                                          
                                                     if (err || (response && response.statusCode !== 200)) {
                                                         console.error(`[Wetter.com] API-Fehler: ${err || (response ? response.statusCode : 'Unbekannter Status')}`);
                                                         return;
                                                     }
                                          
                                                     let data: WetterComResponse;
                                                     try {
                                                         data = JSON.parse(response.data);
                                                     } catch (e) { return; }
                                          
                                                     if (data && data.summary) {
                                                         await wcomProcessForecastData(data, settings.lang);
                                                         await wcomCleanupObsoleteDays();
                                                         await wcomUpdateUsageInfo();
                                                     }
                                                 });
                                             } catch (e: any) { console.error(`[Wetter.com] Script-Fehler: ${e.message}`); }
                                          }
                                          
                                          async function wcomProcessForecastData(data: WetterComResponse, lang: string): Promise<void> {
                                             await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
                                             await wcomEnsureSubStructure(`${CONFIG.DP_PATH}.info`, 'Informationen');
                                             await createStateAsync(`${CONFIG.DP_PATH}.info.last_sync`, '', false, { name: 'Letztes Update', type: 'string', role: 'text' } as any);
                                             await createStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, 0, false, { name: 'Anfragen heute', type: 'number', role: 'value' } as any);
                                          
                                             const maxDays: number = Math.min(data.summary.length, CONFIG.FORECAST_DAYS);
                                          
                                             for (let i = 0; i < maxDays; i++) {
                                                 const day: ForecastSummary = data.summary[i];
                                                 const dayPath: string = `${CONFIG.DP_PATH}.day_${i}`;
                                                 
                                                 await wcomEnsureSubStructure(dayPath, `Tag ${i}`);
                                                 await wcomEnsureDayStates(dayPath, i);
                                          
                                                 await setStateAsync(`${dayPath}.date`, String(wcomFormatDate(day.date)), true);
                                                 await setStateAsync(`${dayPath}.day_name`, String(wcomGetDayName(day.date, lang)), true);
                                                 await setStateAsync(`${dayPath}.temp_max`, wcomExtractValue(day.temperature?.max), true);
                                                 await setStateAsync(`${dayPath}.temp_min`, wcomExtractValue(day.temperature?.min), true);
                                                 await setStateAsync(`${dayPath}.weather_text`, String(day.weather?.text || ''), true);
                                                 await setStateAsync(`${dayPath}.weather_icon`, `${CONFIG.ICON_BASE_URL}/d_${day.weather?.state}.svg`, true);
                                                 await setStateAsync(`${dayPath}.prec_probability`, wcomExtractValue(day.prec?.probability), true);
                                                 await setStateAsync(`${dayPath}.prec_sum`, wcomExtractValue(day.prec?.sum), true);
                                                 await setStateAsync(`${dayPath}.wind_gusts`, wcomExtractValue(day.wind?.gusts), true);
                                                 await setStateAsync(`${dayPath}.clouds`, wcomExtractValue(day.clouds), true);
                                                 await setStateAsync(`${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 createStateAsync(`${sPath}.temp`, 0, false, { name: 'Temperatur', type: 'number', unit: '°C', role: 'value.temperature' } as any);
                                                         await setStateChangedAsync(`${sPath}.temp`, wcomExtractValue(sData.temperature), true);
                                                         await createStateAsync(`${sPath}.text`, '', false, { name: 'Wetter', type: 'string', role: 'weather.state' } as any);
                                                         await setStateChangedAsync(`${sPath}.text`, String(sData.weather?.text || ''), true);
                                                         await createStateAsync(`${sPath}.prec_prob`, 0, false, { name: 'Regenrisiko', type: 'number', unit: '%', role: 'value.precipitation.probability' } as any);
                                                         await setStateChangedAsync(`${sPath}.prec_prob`, wcomExtractValue(sData.prec?.probability), true);
                                                         await createStateAsync(`${sPath}.prec_sum`, 0, false, { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation' } as any);
                                                         await setStateChangedAsync(`${sPath}.prec_sum`, wcomExtractValue(sData.prec?.sum), true);
                                                         await createStateAsync(`${sPath}.wind_speed`, 0, false, { name: 'Windgeschwindigkeit', type: 'number', unit: 'km/h', role: 'value.speed.wind' } as any);
                                                         await setStateChangedAsync(`${sPath}.wind_speed`, wcomExtractValue(sData.wind?.avg), true);
                                                         await createStateAsync(`${sPath}.wind_gusts`, 0, false, { name: 'Windböen', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust' } as any);
                                                         await setStateChangedAsync(`${sPath}.wind_gusts`, wcomExtractValue(sData.wind?.gusts), true);
                                                         await createStateAsync(`${sPath}.clouds`, 0, false, { name: 'Bewölkung', type: 'number', unit: '%', role: 'value' } as any);
                                                         await setStateChangedAsync(`${sPath}.clouds`, wcomExtractValue(sData.clouds), true);
                                                         await createStateAsync(`${sPath}.humidity`, 0, false, { name: 'Relative Feuchte', type: 'number', unit: '%', role: 'value.humidity' } as any);
                                                         await setStateChangedAsync(`${sPath}.humidity`, wcomExtractValue(sData.relativeHumidity), true);
                                                     }
                                                 }
                                          
                                                 if (CONFIG.ENABLE_HOURLY && i <= 1 && data.hourly) {
                                                     const dateIso: string = day.date.split('T')[0];
                                                     const hourlyPath: string = `${dayPath}.hourly`;
                                                     await wcomEnsureSubStructure(hourlyPath, 'Stündlich');
                                                     const dayHours = data.hourly.filter((h: ForecastHourly) => (h.from || h.date).startsWith(dateIso));
                                                     
                                                     for (const h of dayHours) {
                                                         const hourDate: Date = new Date(h.from || h.date);
                                                         const hourLabel: string = String(hourDate.getHours()).padStart(2, '0');
                                                         const hPath: string = `${hourlyPath}.${hourLabel}`;
                                                         
                                                         await wcomEnsureSubStructure(hPath, `${hourLabel}:00 Uhr`);
                                                         
                                                         await createStateAsync(`${hPath}.time`, '', false, { name: 'Uhrzeit', type: 'string', role: 'text' } as any);
                                                         await setStateChangedAsync(`${hPath}.time`, `${hourLabel}:00`, true);
                                                         await createStateAsync(`${hPath}.temp`, 0, false, { name: 'Temperatur', type: 'number', unit: '°C', role: 'value.temperature' } as any);
                                                         await setStateChangedAsync(`${hPath}.temp`, wcomExtractValue(h.temperature), true);
                                                         await createStateAsync(`${hPath}.windchill`, 0, false, { name: 'Gefühlt', type: 'number', unit: '°C', role: 'value.temperature' } as any);
                                                         await setStateChangedAsync(`${hPath}.windchill`, wcomExtractValue(h.windchill), true);
                                                         await createStateAsync(`${hPath}.weather_text`, '', false, { name: 'Wetter', type: 'string', role: 'weather.state' } as any);
                                                         await setStateChangedAsync(`${hPath}.weather_text`, String(h.weather?.text || ''), true);
                                                         await createStateAsync(`${hPath}.weather_icon`, '', false, { name: 'Wetter Icon', type: 'string', role: 'weather.icon' } as any);
                                                         await setStateChangedAsync(`${hPath}.weather_icon`, `${CONFIG.ICON_BASE_URL}/d_${h.weather?.state}.svg`, true);
                                                         await createStateAsync(`${hPath}.prec_prob`, 0, false, { name: 'Regenwahrscheinlichkeit', type: 'number', unit: '%', role: 'value.precipitation.probability' } as any);
                                                         await setStateChangedAsync(`${hPath}.prec_prob`, wcomExtractValue(h.prec?.probability), true);
                                                         await createStateAsync(`${hPath}.prec_sum`, 0, false, { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation' } as any);
                                                         await setStateChangedAsync(`${hPath}.prec_sum`, wcomExtractValue(h.prec?.sum), true);
                                                         await createStateAsync(`${hPath}.wind_speed`, 0, false, { name: 'Windgeschwindigkeit', type: 'number', unit: 'km/h', role: 'value.speed.wind' } as any);
                                                         await setStateChangedAsync(`${hPath}.wind_speed`, wcomExtractValue(h.wind?.avg), true);
                                                         await createStateAsync(`${hPath}.wind_dir`, '', false, { name: 'Windrichtung', type: 'string', role: 'weather.direction' } as any);
                                                         await setStateChangedAsync(`${hPath}.wind_dir`, String(h.wind?.direction || ''), true);
                                                         await createStateAsync(`${hPath}.wind_gusts`, 0, false, { name: 'Windböen', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust' } as any);
                                                         await setStateChangedAsync(`${hPath}.wind_gusts`, wcomExtractValue(h.wind?.gusts), true);
                                                         await createStateAsync(`${hPath}.humidity`, 0, false, { name: 'Relative Feuchte', type: 'number', unit: '%', role: 'value.humidity' } as any);
                                                         await setStateChangedAsync(`${hPath}.humidity`, wcomExtractValue(h.relativeHumidity), true);
                                                     }
                                                 }
                                                 await wcomWait(20); 
                                             }
                                          }
                                          
                                          async function wcomCleanupObsoleteDays(): Promise<void> {
                                             for (let i = CONFIG.FORECAST_DAYS; i <= 25; i++) {
                                                 const path: string = `${CONFIG.DP_PATH}.day_${i}`;
                                                 if (existsObject(path)) {
                                                     await deleteObjectAsync(path, true);
                                                     await wcomWait(150);
                                                 }
                                             }
                                          }
                                          
                                          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 countState: iobJS.State | null = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_today`);
                                             await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, (countState ? (Number(countState.val) || 0) : 0) + 1, true);
                                          }
                                          
                                          // --- ZEITSTEUERUNG ---
                                          
                                          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 * * *", () => setState(`${CONFIG.DP_PATH}.info.requests_today`, 0, true));
                                          schedule(wcomGetRandomCron(0, 5, 2), wcomFetchWeatherData);
                                          schedule(wcomGetRandomCron(13, 17, 2), wcomFetchWeatherData);
                                          
                                          wcomFetchWeatherData();
                                          

                                          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

                                          433

                                          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