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
    1.1k

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

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

[TypeSkript] Wetter.com Forecast/Vorhersage

Geplant Angeheftet Gesperrt Verschoben JavaScript
76 Beiträge 11 Kommentatoren 7.8k Aufrufe 17 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.
  • P pk68

    Hi @Schimi , danke für das Skript.

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

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

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

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

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

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

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

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

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

      1 Antwort Letzte Antwort
      0
      • S Schimi

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

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

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

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

        @Schimi sagte:

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

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

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

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

          Hallo,

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

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

          Gruß

          Peterfido

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

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

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

            Ich ändere es auch im ersten Post

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

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

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

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

              Gruß

              Peterfido

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

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

                P 1 Antwort Letzte Antwort
                0
                • S Schimi

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

                  P Offline
                  P Offline
                  pk68
                  schrieb zuletzt editiert von
                  #74

                  @Schimi
                  Danke für die Überarbeitung.
                  Bei Adaptern wie "daswetter" oder "open-meteo-weather" gibt es einen current-Ordner, wo immer die Wetterdaten der aktuellen Stunde abgelgt sind. Könntest du dir vorstellen, dies in dein Skript einzubauen?

                  S 1 Antwort Letzte Antwort
                  0
                  • P pk68

                    @Schimi
                    Danke für die Überarbeitung.
                    Bei Adaptern wie "daswetter" oder "open-meteo-weather" gibt es einen current-Ordner, wo immer die Wetterdaten der aktuellen Stunde abgelgt sind. Könntest du dir vorstellen, dies in dein Skript einzubauen?

                    S Online
                    S Online
                    Schimi
                    schrieb zuletzt editiert von
                    #75

                    @pk68 versuche mal die aktuelle Version (3.2.1) ;-)

                    Ich schaue mal demnächst ob und wie ich einen Adapter erstellen kann, ich glaube das wäre von der config einfacher und würde mehr Leute erreichen....
                    Aber da muss ich mich erst einlesen.... Falls es da was neues geben wird, werdeihc hier berichten :-)

                    P 1 Antwort Letzte Antwort
                    2
                    • S Schimi

                      @pk68 versuche mal die aktuelle Version (3.2.1) ;-)

                      Ich schaue mal demnächst ob und wie ich einen Adapter erstellen kann, ich glaube das wäre von der config einfacher und würde mehr Leute erreichen....
                      Aber da muss ich mich erst einlesen.... Falls es da was neues geben wird, werdeihc hier berichten :-)

                      P Offline
                      P Offline
                      pk68
                      schrieb zuletzt editiert von
                      #76

                      @Schimi

                      Danke fürs schnelle Anpassen.
                      Bis jetzt keine Probleme bemerkt. Ich habe allerdings bei mir die API Abrufzeiten geändert. Der erste Aufruf des Tages findet bei mir erst kurz vor 6 Uhr statt. Wenn ich das recht überblicke, sollte die Funktion wcomUpdateCurrent damit klarkommen.

                      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

                      438

                      Online

                      33.0k

                      Benutzer

                      83.6k

                      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