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

-
@schimi
Ich finde dein Script absolut perfekt! Vielen lieben Dank, wollte schon lange die Werte von wetter.com mal haben.
Da mir das so gut gefällt würde ich gerne komplett umsteigen.Hat irgendwer eine Lösung oder vielleicht ein fertiges Widget für VIS, wo man Symbole anzeigen kann?
Man müsste ja den "weather_text" in ein Symbol umwandlen und dann mit "String img src" oder "Image 8" Widget arbeiten, aber dazu müsste man erstmal alle Zustände von "weather_text" haben. Aber vielleicht hat sich ja einer schon die Mühe gemacht?
-
die stehen in der API...
ich muss jetzt erstmal (ein Monat) warten (habe da kein druck weil die grundfunktion geht) weil ich die 100 abfragen erreicht habe... wollte dann aber mal schauen, die Std vorhhersagen einzubauen.
kann dann auch nach einer brauchbareren lösung für die Icons gucken...
habs gemacht weio wetter.com bei mir die besten vorhersagen liefert, openweathermap und DasWetter waren meistens (extrem) daneben....
Ich bleibe aber dran 😉
-
Wi finde ich die Übersicht der einzelnen "weather_text" Passagen?
Hab hier was gefunden:
https://doc.meteonomiqs.com/doc/forecast_v4_0.html#section/References/Moonphase-and-MoonzodiacAber das ist englisch und der Text im Iobroker ist ja Deutsch.
Jetzt bräuchte ich die korrekte übersetzung, hätte da nämlich schon eine Idee wegen den Symbolen.
Einfach die Symbole nach dem Text bennnen und dann mit Bindings arbeiten.
Würde da mal was basteln wenn ich das komplett hätte.Und ja, ich finde wetter.com auch die beste Vorhersage 😀
-
Pedder
All @Proxmox/Trixie auf HP Elitedesk 800 G4; Zigbee: ZigStar (LAN), ~110Devices
Unifi, Motioneye/3Reolinks, PiHole, Bosch CS7800i via BBQKees/EMS-ESP, Fronius/BYD 11kWp via Modbus
Under construction: Smart-WoMo auf Raspi4 -
Habe eine neue Version (1.4.8) unter dem Spoiler ganz oben versteckt.
Diese gibt auch die Icon-URL im Datenpunkt aus ;-)@Schimi sagte in [Skript] Wetter.com Forecast/Vorhersage:
Habe eine neue Version (1.4.8) unter dem Spoiler ganz oben versteckt.
Diese gibt auch die Icon-URL im Datenpunkt aus ;-)Hammer! Vielen lieben Dank 😀
Probiere ich morgen gleich aus. -
@schimi
Vielen Dank für die Icons, funktionieren wunderbar.
Jetzt hätte ich noch eines: Kannst du irgendwie den Wochentag noch einblenden lassen, ich bekomme es einfach nicht hin, dass Datum in einen Wochentag im VIS zu konvertieren und auf Aliase oder ein extra Javascript hab ich eigentlich keine Lust :-) -
@schimi
Vielen Dank für die Icons, funktionieren wunderbar.
Jetzt hätte ich noch eines: Kannst du irgendwie den Wochentag noch einblenden lassen, ich bekomme es einfach nicht hin, dass Datum in einen Wochentag im VIS zu konvertieren und auf Aliase oder ein extra Javascript hab ich eigentlich keine Lust :-) -
Du bist echt der Beste!!!!
Vielen Dank :-) -
Ich setze die 1.4.9 ein. Seit der 1.4.8 habe ich den u.a. Fehler. Der Datenpunkt liegt nicht vor. Muss er von Hand eingetragen werden?
javascript.2 15:15:49.056 error [Wetter.com] Fehler: ICON_BASE_URL is not definedGruß
Martin
Intel NUCs mit Proxmox / Iobroker als VM unter Debian
Raspeberry mit USB Leseköpfen für Smartmeter
Homematic und Homematic IP -
versuche mal die hier im Spoiler... bitte um Feedback, dann tausche ich die oben aus
/** * ioBroker Script: Wetter.com Forecast API v4.0 * API: https://doc.meteonomiqs.com/doc/forecast_v4_0.html * * Changelog: * 1.4.11: FIX: Fallback-Werte für Konfigurations-Variablen (ICON_BASE_URL, DEFAULT_LANGUAGE) integriert. * Verhindert Abstürze ("ReferenceError"), wenn beim Update der Konfig-Bereich nicht aktualisiert wurde. * 1.4.10: Feature: Dynamische Spracheinstellung. * 1.4.8: Icon-URL Anpassung. * * free API-Key anfordern: https://www.meteonomiqs.com/de/wetter-api/#heading_PricePackages/ */ // --- KONFIGURATION --- const API_KEY = 'DEIN_API_KEY_HIER'; // <-- BITTE HIER DEINEN API-KEY EINTRAGEN const BASE_URL = 'https://forecast.meteonomiqs.com/v4_0'; const ICON_BASE_URL = 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather'; const DP_PATH = '0_userdata.0.wetter_com'; const DEFAULT_LANGUAGE = 'de'; // Fallback, falls System-Sprache nicht ermittelbar const FORECAST_DAYS = 7; // --- STANDORT KONFIGURATION --- const MANUAL_LATITUDE = ''; const MANUAL_LONGITUDE = ''; const FORCE_MANUAL_LOCATION = false; // --- RANDOMISIERUNG DER ZEITEN --- function getRandomCron(startHour, endHour, minMinute = 0) { const hour = Math.floor(Math.random() * (endHour - startHour + 1)) + startHour; let minute; if (hour === startHour) { minute = Math.floor(Math.random() * (60 - minMinute)) + minMinute; } else if (hour === endHour) { minute = 0; } else { minute = Math.floor(Math.random() * 60); } return `${minute} ${hour} * * *`; } const cron1 = getRandomCron(0, 5, 2); const cron2 = getRandomCron(13, 17, 2); console.log(`[Wetter.com] Schedules für heute gesetzt auf: "${cron1}" und "${cron2}"`); // --- HILFSFUNKTIONEN --- /** * Formatiert ein Datum manuell in das Format: "DD.MM.YYYY" */ function formatDate(dateStr) { if (!dateStr) return ''; const date = new Date(dateStr); if (isNaN(date.getTime())) return dateStr; const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear(); return `${day}.${month}.${year}`; } /** * Ermittelt den Wochentag basierend auf der übergebenen Sprache (Locale) */ function getDayName(dateStr, locale) { if (!dateStr) return ''; const date = new Date(dateStr); if (isNaN(date.getTime())) return ''; return date.toLocaleDateString(locale, { weekday: 'long' }); } /** * Ermittelt Einstellungen (Coords + Sprache) aus ioBroker System-Config */ async function getSystemSettings() { // Safety check für Variablen, falls User Config nicht aktualisiert hat const manualLat = (typeof MANUAL_LATITUDE !== 'undefined') ? MANUAL_LATITUDE : ''; const manualLon = (typeof MANUAL_LONGITUDE !== 'undefined') ? MANUAL_LONGITUDE : ''; const forceManual = (typeof FORCE_MANUAL_LOCATION !== 'undefined') ? FORCE_MANUAL_LOCATION : false; const defLang = (typeof DEFAULT_LANGUAGE !== 'undefined') ? DEFAULT_LANGUAGE : 'de'; // 1. Manuelle Location? let coords = null; if (forceManual && manualLat && manualLon) { console.log(`[Wetter.com] Nutze manuell konfigurierte Koordinaten (Override).`); coords = { lat: parseFloat(manualLat).toFixed(3), lon: parseFloat(manualLon).toFixed(3) }; } // 2. System Config lesen const systemConf = await new Promise((resolve) => { getObject('system.config', (err, obj) => { if (!err && obj && obj.common) { resolve({ lat: obj.common.latitude ? parseFloat(obj.common.latitude).toFixed(3) : null, lon: obj.common.longitude ? parseFloat(obj.common.longitude).toFixed(3) : null, lang: obj.common.language || defLang }); } else { resolve({ lat: null, lon: null, lang: defLang }); } }); }); // 3. Fallback Logik if (!coords) { if (systemConf.lat && systemConf.lon) { coords = { lat: systemConf.lat, lon: systemConf.lon }; } else if (manualLat && manualLon) { console.log('[Wetter.com] Warnung: Keine System-Koordinaten. Nutze Fallback.'); coords = { lat: parseFloat(manualLat).toFixed(3), lon: parseFloat(manualLon).toFixed(3) }; } } if (!coords) return null; return { ...coords, lang: systemConf.lang }; } async function ensureStructure(path, index, isInfo = false) { if (isInfo) { await createStateAsync(`${DP_PATH}.info.last_sync`, '', false, { name: 'Letztes erfolgreiches Update', type: 'string', role: 'text' }); await createStateAsync(`${DP_PATH}.info.requests_today`, 0, false, { name: 'Anfragen heute', type: 'number', role: 'value' }); await createStateAsync(`${DP_PATH}.info.requests_month`, 0, false, { name: 'Anfragen aktueller Monat', type: 'number', role: 'value' }); await createStateAsync(`${DP_PATH}.info.next_schedules`, '', false, { name: 'Geplante Abrufe', type: 'string', role: 'text' }); return; } const states = { 'date': { name: 'Datum', type: 'string', role: 'text', def: '' }, 'day_name': { name: 'Wochentag', type: 'string', role: 'text', def: '' }, 'temp_max': { name: 'Max Temperatur', type: 'number', unit: '°C', role: 'value.temperature.max', def: 0 }, 'temp_min': { name: 'Min Temperatur', type: 'number', unit: '°C', role: 'value.temperature.min', def: 0 }, 'weather_text': { name: 'Wetterzustand', type: 'string', role: 'weather.state', def: '' }, 'weather_code': { name: 'Wetter Code', type: 'number', role: 'value', def: 0 }, 'weather_icon': { name: 'Wetter Icon URL', type: 'string', role: 'weather.icon', def: '' }, 'prec_probability': { name: 'Regenrisiko', type: 'number', unit: '%', role: 'value.precipitation.probability', def: 0 }, 'prec_sum': { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation', def: 0 }, 'sun_hours': { name: 'Sonnenstunden', type: 'number', unit: 'h', role: 'value.sunshine', def: 0 }, 'wind_speed_max': { name: 'Windböen Max', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust', def: 0 }, 'wind_direction': { name: 'Windrichtung', type: 'string', role: 'weather.direction', def: '' } }; for (const [id, config] of Object.entries(states)) { await createStateAsync(`${path}.${id}`, config.def, false, { name: `Tag ${index}: ${config.name}`, type: config.type, role: config.role, unit: config.unit || '', read: true, write: false }); } } async function performRequest(url, options) { return new Promise((resolve, reject) => { httpGet(url, options, (error, response) => { if (error) return reject(new Error(error)); if (response.statusCode === 429) return reject(new Error('LIMIT_REACHED')); if (response.statusCode !== 200) return reject(new Error(`HTTP Status ${response.statusCode}`)); resolve(response); }); }); } async function cleanupObsoleteDays() { const channels = $(`${DP_PATH}.day_*`); channels.each(function(id) { const parts = id.split('.'); const lastPart = parts[parts.length - 1]; const dayIndex = parseInt(lastPart.replace('day_', '')); if (!isNaN(dayIndex) && dayIndex >= FORECAST_DAYS) { deleteObject(id, true); } }); } async function updateUsageInfo() { const now = new Date(); const day = String(now.getDate()).padStart(2, '0'); const month = String(now.getMonth() + 1).padStart(2, '0'); const year = now.getFullYear(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); const timestamp = `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`; await setStateAsync(`${DP_PATH}.info.last_sync`, String(timestamp), true); const currentCountState = await getStateAsync(`${DP_PATH}.info.requests_today`); const currentCount = currentCountState ? (currentCountState.val || 0) : 0; await setStateAsync(`${DP_PATH}.info.requests_today`, currentCount + 1, true); const currentMonthState = await getStateAsync(`${DP_PATH}.info.requests_month`); const currentMonthCount = currentMonthState ? (currentMonthState.val || 0) : 0; await setStateAsync(`${DP_PATH}.info.requests_month`, currentMonthCount + 1, true); const s1 = cron1.split(' '); const s2 = cron2.split(' '); await setStateAsync(`${DP_PATH}.info.next_schedules`, `${s1[1].padStart(2,'0')}:${s1[0].padStart(2,'0')} Uhr & ${s2[1].padStart(2,'0')}:${s2[0].padStart(2,'0')} Uhr`, true); } // --- LOGIK --- async function fetchWeatherData() { try { const settings = await getSystemSettings(); if (!settings) { console.error('[Wetter.com] Fehler: Keine Koordinaten gefunden!'); return; } console.log(`[Wetter.com] Starte Abruf für Lat ${settings.lat}, Lon ${settings.lon} (Sprache: ${settings.lang})`); const url = `${BASE_URL}/forecast/${settings.lat}/${settings.lon}/summary`; const options = { headers: { 'x-api-key': API_KEY, 'Accept-Language': settings.lang } }; try { const response = await performRequest(url, options); const data = JSON.parse(response.data); if (data && data.items) { await processForecastData(data.items, settings.lang); await cleanupObsoleteDays(); await updateUsageInfo(); } } catch (err) { if (err.message === 'LIMIT_REACHED') { console.error('[Wetter.com] Fehler 429: Monatslimit erreicht.'); } else { console.error(`[Wetter.com] Fehler: ${err.message}`); } } } catch (e) { console.error(`[Wetter.com] Script-Fehler: ${e.message}`); } } async function processForecastData(items, lang) { await ensureStructure('', 0, true); const daysToProcess = Math.min(items.length, FORECAST_DAYS); // Fallback URL, falls Variable durch Teil-Update fehlt const iconBaseUrl = (typeof ICON_BASE_URL !== 'undefined') ? ICON_BASE_URL : 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather'; for (let i = 0; i < daysToProcess; i++) { const day = items[i]; const dayPath = `${DP_PATH}.day_${i}`; await ensureStructure(dayPath, i); const germanDate = formatDate(day.date); const dayName = getDayName(day.date, lang); const weatherCode = day.weather?.state ?? 0; const iconUrl = `${iconBaseUrl}/d_${weatherCode}.svg`; await setStateAsync(`${dayPath}.date`, germanDate, true); await setStateAsync(`${dayPath}.day_name`, dayName, true); await setStateAsync(`${dayPath}.temp_max`, day.temperature?.max ?? 0, true); await setStateAsync(`${dayPath}.temp_min`, day.temperature?.min ?? 0, true); await setStateAsync(`${dayPath}.weather_text`, day.weather?.text || '', true); await setStateAsync(`${dayPath}.weather_code`, weatherCode, true); await setStateAsync(`${dayPath}.weather_icon`, iconUrl, true); await setStateAsync(`${dayPath}.prec_probability`, day.prec?.probability ?? 0, true); await setStateAsync(`${dayPath}.prec_sum`, day.prec?.sum ?? 0, true); await setStateAsync(`${dayPath}.sun_hours`, day.sunHours ?? 0, true); await setStateAsync(`${dayPath}.wind_speed_max`, day.wind?.max ?? 0, true); await setStateAsync(`${dayPath}.wind_direction`, day.wind?.direction || '', true); } console.log(`[Wetter.com] Update von ${daysToProcess} Tagen abgeschlossen.`); } schedule("0 0 * * *", () => { setState(`${DP_PATH}.info.requests_today`, 0, true); }); schedule("0 0 1 * *", () => { setState(`${DP_PATH}.info.requests_month`, 0, true); }); schedule(cron1, fetchWeatherData); schedule(cron2, fetchWeatherData); ensureStructure('', 0, true).then(() => { fetchWeatherData(); }); -
Sieht gut. Kein Fehler mehr.
Ich lasse diese Version nun laufen.Danke für die prompte Fehlerbehebung.
Gruß
Martin
Intel NUCs mit Proxmox / Iobroker als VM unter Debian
Raspeberry mit USB Leseköpfen für Smartmeter
Homematic und Homematic IP -
so, habe die 24 Std vorhersage eingebaut (und den 3ten API-Key verbraucht ;-))
es gab auch eine große Änderung!!!!!
Das Skript ist nun in TypeSkript geschrieben.
Ihr müsst es als neues Skript importieren und dort "TypeSkript" auswählen
Für mich wäre es dann jetzt "future-complete" und ich würde nur noch was bei Fehlern ändern und solange selber nutzen :-)
-
Leider wieder der Fehler mit ICON_BASE_URL:
javascript.2 2026-01-14 08:57:47.384 error script.js.Wetter.Forecast_Wetter_com_1-8-3: TypeScript compilation failed:';; ^ERROR: Unterminated string literal. lang: obj.common.language || DEFAULT_LANGUAGE ^ERROR: Cannot find name 'DEFAULT_LANGUAGE'. resolve({ lat: null, lon: null, lang: DEFAULT_LANGUAGE }); ^ERROR: Cannot find name 'DEFAULT_LANGUAGE'. await setStateAsync(`${dayPath}.weather_icon`, String(`${ICON_BASE_URL}/d_${day.weather?.state}.svg`), true); ^ERROR: Cannot find name 'ICON_BASE_URL'. await setStateChangedAsync(`${hPath}.weather_icon`, String(`${ICON_BASE_URL}/d_${h.weather?.state}.svg`), true); ^ERROR: Cannot find name 'ICON_BASE_URL'. javascript.2 2026-01-14 08:57:47.297 info script.js.Wetter.Forecast_Wetter_com_1-8-3: Compiling TypeScript source javascript.2 2026-01-14 08:57:43.337 info script.js.Wetter.Forecast_Wetter_com_1-8-3: Stopping script javascript.2 2026-01-14 08:57:31.016 error script.js.Wetter.Forecast_Wetter_com_1-8-3: TypeScript compilation failed:';; ^ERROR: Unterminated string literal. lang: obj.common.language || DEFAULT_LANGUAGE ^ERROR: Cannot find name 'DEFAULT_LANGUAGE'. resolve({ lat: null, lon: null, lang: DEFAULT_LANGUAGE }); ^ERROR: Cannot find name 'DEFAULT_LANGUAGE'. await setStateAsync(`${dayPath}.weather_icon`, String(`${ICON_BASE_URL}/d_${day.weather?.state}.svg`), true); ^ERROR: Cannot find name 'ICON_BASE_URL'. await setStateChangedAsync(`${hPath}.weather_icon`, String(`${ICON_BASE_URL}/d_${h.weather?.state}.svg`), true); ^ERROR: Cannot find name 'ICON_BASE_URL'. javascript.2 2026-01-14 08:57:30.119 info script.js.Wetter.Forecast_Wetter_com_1-8-3: Compiling TypeScript sourceGruß
Martin
Intel NUCs mit Proxmox / Iobroker als VM unter Debian
Raspeberry mit USB Leseköpfen für Smartmeter
Homematic und Homematic IP -
@martybr versuche mal das im Spoiler... vielleicht auch mal den Datenpunkt komplett löschen und neu erstellen lassen (in den Objekten)
Nicht das durch den Wechsel auf TypeSkript da auch was geändert wurde
/** * ioBroker Script: Wetter.com Forecast API v4.0 (TrueScript) * API: https://doc.meteonomiqs.com/doc/forecast_v4_0.html * * * Changelog: * 1.8.4: FIX: Compiler-Fehler & Syntax-Bereinigung. * - Konfiguration in 'CONFIG'-Objekt gekapselt, um Scope-Fehler ("Cannot find name") zu beheben. * - Bereinigung von Template-Strings (entfernte redundante String()-Wrapper). * - Typsichere Implementierung von getObject-Callbacks. * 1.8.3: FIX: Namenskonflikte behoben (wcom-Präfix). */ // --- KONFIGURATION --- const CONFIG = { API_KEY: 'DEIN_API_KEY_HIER', // <-- BITTE HIER DEINEN API-KEY EINTRAGEN BASE_URL: 'https://forecast.meteonomiqs.com/v4_0', ICON_BASE_URL: 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather', DP_PATH: '0_userdata.0.wetter_com', DEFAULT_LANGUAGE: 'de', FORECAST_DAYS: 7, // max. 16 Tage ENABLE_HOURLY: true, ENABLE_SPACES: true, LOCATION: { LAT: '', // Leer lassen für System-Einstellung LON: '', // Leer lassen für System-Einstellung FORCE_MANUAL: false } }; // --- INTERFACES --- interface WetterComValue { avg?: number; value?: number; sum?: number; max?: number; min?: number; } interface WetterComWeather { state: number; text: string; icon?: string; } interface WetterComWind { avg?: number | WetterComValue; gusts?: number | WetterComValue | { value: number | null }; direction?: string; unit?: string; } interface WetterComPrec { probability: number; sum: number | WetterComValue; } interface ForecastSummary { date: string; weather: WetterComWeather; temperature: { min: number | WetterComValue; max: number | WetterComValue; avg?: number | WetterComValue }; wind: WetterComWind; prec: WetterComPrec; clouds: number | WetterComValue; relativeHumidity: number | WetterComValue; } interface ForecastSpaceSegment { temperature: number | WetterComValue; weather: WetterComWeather; prec: WetterComPrec; wind: WetterComWind; clouds: number | WetterComValue; relativeHumidity: number | WetterComValue; } interface ForecastSpace { morning?: ForecastSpaceSegment; afternoon?: ForecastSpaceSegment; evening?: ForecastSpaceSegment; night?: ForecastSpaceSegment; } interface ForecastHourly { from: string; date: string; weather: WetterComWeather; temperature: number | WetterComValue; windchill: number | WetterComValue; wind: WetterComWind; prec: WetterComPrec; relativeHumidity: number | WetterComValue; } interface WetterComResponse { summary: ForecastSummary[]; spaces: ForecastSpace[]; hourly: ForecastHourly[]; } // --- HILFSFUNKTIONEN --- const wcomWait = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms)); function wcomExtractValue(val: any): number { if (val === null || val === undefined) return 0; if (typeof val === 'number') return val; if (typeof val === 'object') { if (val.avg !== undefined) return val.avg; if (val.value !== undefined) return val.value; if (val.sum !== undefined) return val.sum; if (val.max !== undefined) return val.max; if (val.min !== undefined) return val.min; if (val.gusts && typeof val.gusts === 'object' && val.gusts.value !== undefined) return val.gusts.value; } return parseFloat(String(val)) || 0; } function wcomFormatDate(dateStr: string): string { if (!dateStr) return ''; const date = new Date(dateStr); if (isNaN(date.getTime())) return dateStr; return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`; } function wcomGetDayName(dateStr: string, locale: string): string { if (!dateStr) return ''; const date = new Date(dateStr); if (isNaN(date.getTime())) return ''; return date.toLocaleDateString(locale, { weekday: 'long' }); } async function wcomGetSystemSettings(): Promise<{ lat: string; lon: string; lang: string } | null> { let coords: { lat: string; lon: string } | null = null; if (CONFIG.LOCATION.FORCE_MANUAL && CONFIG.LOCATION.LAT && CONFIG.LOCATION.LON) { coords = { lat: parseFloat(CONFIG.LOCATION.LAT).toFixed(3), lon: parseFloat(CONFIG.LOCATION.LON).toFixed(3) }; } const systemConf: any = await new Promise((resolve) => { getObject('system.config', (err, obj: any) => { if (!err && obj && obj.common) { resolve({ lat: obj.common.latitude ? parseFloat(String(obj.common.latitude)).toFixed(3) : null, lon: obj.common.longitude ? parseFloat(String(obj.common.longitude)).toFixed(3) : null, lang: obj.common.language || CONFIG.DEFAULT_LANGUAGE }); } else { resolve({ lat: null, lon: null, lang: CONFIG.DEFAULT_LANGUAGE }); } }); }); if (!coords && systemConf.lat && systemConf.lon) coords = { lat: systemConf.lat, lon: systemConf.lon }; return coords ? { ...coords, lang: systemConf.lang } : null; } async function wcomEnsureSubStructure(path: string, name: string, type: 'device' | 'channel' = 'channel'): Promise<void> { if (!path) return; if (!existsObject(path)) { await extendObjectAsync(path, { type: type, common: { name: name }, native: {} }); await wcomWait(50); } } async function wcomEnsureDayStates(path: string, index: number): Promise<void> { const states: Record<string, { name: string; type: iobJS.CommonType; role: string; unit?: string; init: any }> = { 'date': { name: 'Datum', type: 'string', role: 'text', init: '' }, 'day_name': { name: 'Wochentag', type: 'string', role: 'text', init: '' }, 'temp_max': { name: 'Max Temp', type: 'number', unit: '°C', role: 'value.temperature.max', init: 0 }, 'temp_min': { name: 'Min Temp', type: 'number', unit: '°C', role: 'value.temperature.min', init: 0 }, 'weather_text': { name: 'Wetter', type: 'string', role: 'weather.state', init: '' }, 'weather_icon': { name: 'Icon URL', type: 'string', role: 'weather.icon', init: '' }, 'prec_probability': { name: 'Regenrisiko', type: 'number', unit: '%', role: 'value.precipitation.probability', init: 0 }, 'prec_sum': { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation', init: 0 }, 'wind_gusts': { name: 'Windböen', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust', init: 0 }, 'clouds': { name: 'Bewölkung', type: 'number', unit: '%', role: 'value', init: 0 }, 'humidity': { name: 'Relative Feuchte', type: 'number', unit: '%', role: 'value.humidity', init: 0 } }; for (const [id, cfg] of Object.entries(states)) { await createStateAsync(`${path}.${id}`, cfg.init, false, { name: `Tag ${index}: ${cfg.name}`, type: cfg.type, role: cfg.role, unit: cfg.unit || '', read: true, write: false }); } } // --- LOGIK --- async function wcomFetchWeatherData(): Promise<void> { try { const settings = await wcomGetSystemSettings(); if (!settings) return; console.log(`[Wetter.com] Abruf für Lat ${settings.lat}, Lon ${settings.lon}`); const url: string = `${CONFIG.BASE_URL}/forecast/${settings.lat}/${settings.lon}`; const options = { headers: { 'x-api-key': CONFIG.API_KEY, 'Accept-Language': settings.lang } }; httpGet(url, options, async (err, response) => { if (response && response.statusCode === 429) { console.error('[Wetter.com] Das Limit von 100 API-Calls im Monat ist ausgeschöpft.'); return; } if (err || (response && response.statusCode !== 200)) { console.error(`[Wetter.com] API-Fehler: ${err || (response ? response.statusCode : 'Unbekannter Status')}`); return; } let data: WetterComResponse; try { data = JSON.parse(response.data); } catch (e) { return; } if (data && data.summary) { await wcomProcessForecastData(data, settings.lang); await wcomCleanupObsoleteDays(); await wcomUpdateUsageInfo(); } }); } catch (e: any) { console.error(`[Wetter.com] Script-Fehler: ${e.message}`); } } async function wcomProcessForecastData(data: WetterComResponse, lang: string): Promise<void> { await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device'); await wcomEnsureSubStructure(`${CONFIG.DP_PATH}.info`, 'Informationen'); await createStateAsync(`${CONFIG.DP_PATH}.info.last_sync`, '', false, { name: 'Letztes Update', type: 'string', role: 'text' } as any); await createStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, 0, false, { name: 'Anfragen heute', type: 'number', role: 'value' } as any); const maxDays: number = Math.min(data.summary.length, CONFIG.FORECAST_DAYS); for (let i = 0; i < maxDays; i++) { const day: ForecastSummary = data.summary[i]; const dayPath: string = `${CONFIG.DP_PATH}.day_${i}`; await wcomEnsureSubStructure(dayPath, `Tag ${i}`); await wcomEnsureDayStates(dayPath, i); await setStateAsync(`${dayPath}.date`, String(wcomFormatDate(day.date)), true); await setStateAsync(`${dayPath}.day_name`, String(wcomGetDayName(day.date, lang)), true); await setStateAsync(`${dayPath}.temp_max`, wcomExtractValue(day.temperature?.max), true); await setStateAsync(`${dayPath}.temp_min`, wcomExtractValue(day.temperature?.min), true); await setStateAsync(`${dayPath}.weather_text`, String(day.weather?.text || ''), true); await setStateAsync(`${dayPath}.weather_icon`, `${CONFIG.ICON_BASE_URL}/d_${day.weather?.state}.svg`, true); await setStateAsync(`${dayPath}.prec_probability`, wcomExtractValue(day.prec?.probability), true); await setStateAsync(`${dayPath}.prec_sum`, wcomExtractValue(day.prec?.sum), true); await setStateAsync(`${dayPath}.wind_gusts`, wcomExtractValue(day.wind?.gusts), true); await setStateAsync(`${dayPath}.clouds`, wcomExtractValue(day.clouds), true); await setStateAsync(`${dayPath}.humidity`, wcomExtractValue(day.relativeHumidity), true); if (CONFIG.ENABLE_SPACES && data.spaces && data.spaces[i]) { const spacesPath: string = `${dayPath}.spaces`; await wcomEnsureSubStructure(spacesPath, 'Tagesabschnitte'); const segments: (keyof ForecastSpace)[] = ['morning', 'afternoon', 'evening', 'night']; for (const seg of segments) { const sData = data.spaces[i][seg]; if (!sData) continue; const sPath: string = `${spacesPath}.${seg}`; await wcomEnsureSubStructure(sPath, seg); await createStateAsync(`${sPath}.temp`, 0, false, { name: 'Temperatur', type: 'number', unit: '°C', role: 'value.temperature' } as any); await setStateChangedAsync(`${sPath}.temp`, wcomExtractValue(sData.temperature), true); await createStateAsync(`${sPath}.text`, '', false, { name: 'Wetter', type: 'string', role: 'weather.state' } as any); await setStateChangedAsync(`${sPath}.text`, String(sData.weather?.text || ''), true); await createStateAsync(`${sPath}.prec_prob`, 0, false, { name: 'Regenrisiko', type: 'number', unit: '%', role: 'value.precipitation.probability' } as any); await setStateChangedAsync(`${sPath}.prec_prob`, wcomExtractValue(sData.prec?.probability), true); await createStateAsync(`${sPath}.prec_sum`, 0, false, { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation' } as any); await setStateChangedAsync(`${sPath}.prec_sum`, wcomExtractValue(sData.prec?.sum), true); await createStateAsync(`${sPath}.wind_speed`, 0, false, { name: 'Windgeschwindigkeit', type: 'number', unit: 'km/h', role: 'value.speed.wind' } as any); await setStateChangedAsync(`${sPath}.wind_speed`, wcomExtractValue(sData.wind?.avg), true); await createStateAsync(`${sPath}.wind_gusts`, 0, false, { name: 'Windböen', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust' } as any); await setStateChangedAsync(`${sPath}.wind_gusts`, wcomExtractValue(sData.wind?.gusts), true); await createStateAsync(`${sPath}.clouds`, 0, false, { name: 'Bewölkung', type: 'number', unit: '%', role: 'value' } as any); await setStateChangedAsync(`${sPath}.clouds`, wcomExtractValue(sData.clouds), true); await createStateAsync(`${sPath}.humidity`, 0, false, { name: 'Relative Feuchte', type: 'number', unit: '%', role: 'value.humidity' } as any); await setStateChangedAsync(`${sPath}.humidity`, wcomExtractValue(sData.relativeHumidity), true); } } if (CONFIG.ENABLE_HOURLY && i <= 1 && data.hourly) { const dateIso: string = day.date.split('T')[0]; const hourlyPath: string = `${dayPath}.hourly`; await wcomEnsureSubStructure(hourlyPath, 'Stündlich'); const dayHours = data.hourly.filter((h: ForecastHourly) => (h.from || h.date).startsWith(dateIso)); for (const h of dayHours) { const hourDate: Date = new Date(h.from || h.date); const hourLabel: string = String(hourDate.getHours()).padStart(2, '0'); const hPath: string = `${hourlyPath}.${hourLabel}`; await wcomEnsureSubStructure(hPath, `${hourLabel}:00 Uhr`); await createStateAsync(`${hPath}.time`, '', false, { name: 'Uhrzeit', type: 'string', role: 'text' } as any); await setStateChangedAsync(`${hPath}.time`, `${hourLabel}:00`, true); await createStateAsync(`${hPath}.temp`, 0, false, { name: 'Temperatur', type: 'number', unit: '°C', role: 'value.temperature' } as any); await setStateChangedAsync(`${hPath}.temp`, wcomExtractValue(h.temperature), true); await createStateAsync(`${hPath}.windchill`, 0, false, { name: 'Gefühlt', type: 'number', unit: '°C', role: 'value.temperature' } as any); await setStateChangedAsync(`${hPath}.windchill`, wcomExtractValue(h.windchill), true); await createStateAsync(`${hPath}.weather_text`, '', false, { name: 'Wetter', type: 'string', role: 'weather.state' } as any); await setStateChangedAsync(`${hPath}.weather_text`, String(h.weather?.text || ''), true); await createStateAsync(`${hPath}.weather_icon`, '', false, { name: 'Wetter Icon', type: 'string', role: 'weather.icon' } as any); await setStateChangedAsync(`${hPath}.weather_icon`, `${CONFIG.ICON_BASE_URL}/d_${h.weather?.state}.svg`, true); await createStateAsync(`${hPath}.prec_prob`, 0, false, { name: 'Regenwahrscheinlichkeit', type: 'number', unit: '%', role: 'value.precipitation.probability' } as any); await setStateChangedAsync(`${hPath}.prec_prob`, wcomExtractValue(h.prec?.probability), true); await createStateAsync(`${hPath}.prec_sum`, 0, false, { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation' } as any); await setStateChangedAsync(`${hPath}.prec_sum`, wcomExtractValue(h.prec?.sum), true); await createStateAsync(`${hPath}.wind_speed`, 0, false, { name: 'Windgeschwindigkeit', type: 'number', unit: 'km/h', role: 'value.speed.wind' } as any); await setStateChangedAsync(`${hPath}.wind_speed`, wcomExtractValue(h.wind?.avg), true); await createStateAsync(`${hPath}.wind_dir`, '', false, { name: 'Windrichtung', type: 'string', role: 'weather.direction' } as any); await setStateChangedAsync(`${hPath}.wind_dir`, String(h.wind?.direction || ''), true); await createStateAsync(`${hPath}.wind_gusts`, 0, false, { name: 'Windböen', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust' } as any); await setStateChangedAsync(`${hPath}.wind_gusts`, wcomExtractValue(h.wind?.gusts), true); await createStateAsync(`${hPath}.humidity`, 0, false, { name: 'Relative Feuchte', type: 'number', unit: '%', role: 'value.humidity' } as any); await setStateChangedAsync(`${hPath}.humidity`, wcomExtractValue(h.relativeHumidity), true); } } await wcomWait(20); } } async function wcomCleanupObsoleteDays(): Promise<void> { for (let i = CONFIG.FORECAST_DAYS; i <= 25; i++) { const path: string = `${CONFIG.DP_PATH}.day_${i}`; if (existsObject(path)) { await deleteObjectAsync(path, true); await wcomWait(150); } } } async function wcomUpdateUsageInfo(): Promise<void> { const now: Date = new Date(); const timestamp: string = `${String(now.getDate()).padStart(2,'0')}.${String(now.getMonth()+1).padStart(2,'0')}.${now.getFullYear()} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`; await setStateAsync(`${CONFIG.DP_PATH}.info.last_sync`, String(timestamp), true); const countState: iobJS.State | null = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_today`); await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, (countState ? (Number(countState.val) || 0) : 0) + 1, true); } // --- ZEITSTEUERUNG --- function wcomGetRandomCron(startHour: number, endHour: number, minMinute: number = 0): string { const hour: number = Math.floor(Math.random() * (endHour - startHour + 1)) + startHour; let minute: number = (hour === startHour) ? Math.floor(Math.random() * (60 - minMinute)) + minMinute : (hour === endHour ? 0 : Math.floor(Math.random() * 60)); return `${minute} ${hour} * * *`; } schedule("0 0 * * *", () => setState(`${CONFIG.DP_PATH}.info.requests_today`, 0, true)); schedule(wcomGetRandomCron(0, 5, 2), wcomFetchWeatherData); schedule(wcomGetRandomCron(13, 17, 2), wcomFetchWeatherData); wcomFetchWeatherData();
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