Weiter zum Inhalt

Skripten / Logik

16.6k Themen 214.5k Beiträge

Hilfe zu JavaScript, Blockly, TypeScript, Node-RED, Scenes und text2command

NEWS

Unterkategorien


  • Hilfe für Skripterstellung mit JavaScript

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

    7k 80k
    7k Themen
    80k Beiträge
    M
    Gestern war bei VolMax 3,54V Ladeende 100%, das Entladen stoppt halt dann doch relativ bald wenn ich bei Volmin 3,16V stoppe, aber wird wohl Sinn machen. Damit es ein bisschen hinausgezögert wird habe ich aber aktuell schon von 40% (500W) bis 10% verlaufend das Entladelimmit bis zu nur noch maximal 200W runtergesetzt.
  • Hilfe für Skripterstellung mit Node-RED

    956 13k
    956 Themen
    13k Beiträge
    S
    Hallo Zusammen, ich bin gerade am überlegen, wie ich folgende Aufgabe umsetzen könnte: Ein true an einer (Trigger)Node lässt einen Zähler laufen und setzt einen eigenen Ausgang auf true. Ein false der Trigger Node stoppt den Zähler setzt diesen aber nicht auf false. Ein true lässt den Zähler einfach weiterlaufen... Nach Ablauf der Zeit im Zähler wird der Ausgang des Zählers auf false gesetzt. Mehr soll nicht passieren. Ein separater Eingang am Zähler setzt diesen auf 0 zurück. Was ich damit anfangen möchte: Die Laufzeit der Poolpumpe wird auf eine Mindestlaufzeit von bspw. 4h überwacht -kann aber manuell bedient bei Bedarf länger laufen. Ein Trigger um 00:01Uhr setzt die Laufzeit zurück. Die Zählernode gilt hier als "Anforderung Pumpe wegen Mindestlaufzeit unterschritten". Ich habe noch keine brauchbaren Ideen, wie ich dass sauber und mit wenig Aufwand umsetzen könnte... Vielen Dank und VG Torsten
  • Scriptsammlung Vol. 2

    Angeheftet Gesperrt
    3
    3 Stimmen
    3 Beiträge
    5k Aufrufe
    NegaleinN
    Achtung: Diese Scripts sind teils auch ungetestet bzw. nur vom Ersteller getestet worden. Blockly diverse Scripte Schimpfwortgenerator (BananaJoe, Nikolai Radke) Ein Schimpfwortgenerator ioBroker-Forum-Thread: Schimpfwortgenerator Witze aus API (mading) Ein Witzegenerator ioBroker-Forum-Thread: Witzegenerator Bilder mittels LLM ChatGPT Vision ananalysieren (David G.) Bilder mit ChatGPT ananalysieren ioBroker-Forum-Thread: Bilder mittels LLM ChatGPT Vision ananalysieren Visualisierung Agentdvr-Aufnahmen in der Visualisierung darstellen (David G.) Agentdvr-Aufnahmen anzeigen ioBroker-Forum-Thread: Agentdvr-Aufnahmen in der Visualisierung darstellen Trash HTML Widget VIS2 (skvarel) Trash HTML Widget VIS2 ioBroker-Forum-Thread: Trash HTML Widget VIS2 GitHub GitHub
  • Scriptsammlung Vol. 2 -- Diskussion

    Angeheftet
    67
    1 Stimmen
    67 Beiträge
    12k Aufrufe
    NegaleinN
    @Schimi sagte in Scriptsammlung Vol. 2 -- Diskussion: Wetter.com Forecast/Vorhersage erledigt :)
  • [Vorlage] Anwesenheitssimulation mit dauerhaftem Lernen

    javascript template security
    21
    1 Stimmen
    21 Beiträge
    751 Aufrufe
    NashraN
    @mrMuppet Danke für die gute Erklärung
  • verschiedene Skripte

    javascript
    1
    1
    0 Stimmen
    1 Beiträge
    52 Aufrufe
    Niemand hat geantwortet
  • [Vorlage] Luftqualitätswerte abrufen

    55
    2
    4 Stimmen
    55 Beiträge
    8k Aufrufe
    Siggi0904S
    @Boronsbruder sagte: Wenn ich mal Zeit habe, schau ich mir APIv4 an ;) Wenn man in die News zur V4 schaut, sind "nur" Statistiken dazu gekommen. Ich habe bei mir auf die V4 mit den aktuellen Einstellungen umgestellt und es funktioniert bisher. Vielleicht kann man das Script noch ausbauen und weitere Funktionen der API implementieren. Danke und frohe Ostern.
  • Bambu Lab A1 - Status "fertig"

    6
    0 Stimmen
    6 Beiträge
    119 Aufrufe
    skvarelS
    Es gibt einen Datenpunkt im Bambu Adapter auf den ich reagiere ... zumindest für den P1S Ich prüfe alle 15 Minuten ob der Druck fertig ist und ob die Temperaturen niedrig genug sind. In der VIS habe ich einen Switch um das automatische Abschalten zu steuern. Man will ja nicht nach jedem Druck abschalten ;) Hier mal mein Script: [image: 1775111135695-0cb2eba1-8d7c-46d7-b489-669dc835de7e-image.jpeg] Die View dazu: [image: 1775111001000-fb8b3d6e-705a-4553-8ab5-8660c2ab8d28-image.jpeg]
  • Blockly: Astro-Block - (Zeit)-Versatz wird nicht ausgeführt

    4
    0 Stimmen
    4 Beiträge
    117 Aufrufe
    HomoranH
    @w00dy sagte: demnach passt es. Was immer du da vorhast, -45 Minuten ist vor Sonnenuntergang!
  • [gelöst]Lautstärke verändern mit Aquara Cube und Onkyo Amp

    5
    2
    0 Stimmen
    5 Beiträge
    116 Aufrufe
    J
    Ich habe den Fehler g[image: 1774804343972-bildschirmfoto-2026-03-29-um-19.10.27-resized.png] efunden, es war im Adapter limitiert, danke trotzdem
  • Auf Fehlermeldung im Log reagieren (gelöst)

    9
    0 Stimmen
    9 Beiträge
    207 Aufrufe
    G
    @paul53 Vielen DANK, hat geklappt :)
  • Bastellösung: Polestar Ladezustand via Tibber App API

    35
    10
    0 Stimmen
    35 Beiträge
    43k Aufrufe
    G
    Hallo, die Adapter-Lösung von @tombox läuft einwandfrei. Danke dafür!!! Allerdings ist das etwas am Thema dieses thread vorbei. Warum? Wie wir wahrscheinlich schon Alle erfahren mussten, ändert Polestar hin und wieder die API womit alle diese Bastellösungen wieder angepasst werden müssen. Das geht, je nach Lust, Laune und Zeit des jeweiligen Entwicklers 'mal schneller und auch 'mal langsamer, ganz selten auch gar nicht. Nicht falsch verstehen, das ist kein Vorwurf - es ist vollkommen klar das die Anpassung ein Hobby ist und ich bin jedem Profi dankbar der mir als DAU weiterhilft. Polestar macht ja leider keine Anstalten eine offizielle API anzubieten. Meine Erfahrung ist hier aber, dass die Tibber-API grundsätzlich seltener geändert wird. Deshalb hatte ich auf meinem System immer zwei Lösungen parallel am laufen - Polestar & Tibber. Sollte bei einer API keine sinnvollen Daten mehr kommen, wird automatisch auf die andere gewechselt. So habe ich eine, für mich ausreichende, Datensicherheit erreicht. Mittlerweile gibt es viele Lösungen die sich auf die Polestar-API stützen, aber seit Anfang 03/26 keine funktionierende Lösung für Tibber mehr. Ändert sich nun die Polestar-API versagen alle diese Lösungen gemeinsam. Ich habe leider bis dato keine Alternative gefunden und zum Analysieren der API und ein Script selbst schreiben fehlen mir die Kenntnisse. Lange Rede, wenig Sinn: hat/kennt einer eine Lösung für Tibber (oder was anderes, nicht Polestar) und könnte die bitte hier publizieren/verlinken?
  • [Vorlage] Automatisches Git-Backup für Skripte/Blockly

    javascript blockly
    5
    0 Stimmen
    5 Beiträge
    171 Aufrufe
    Meister MopperM
    @mrMuppet Ich empfehle, das Skript selbst in .gitignore zu setzen. Wenn es nämlich sich selbst aktualisiert - so ist es bei mir passiert - hat der js-controller es einfach gelöscht, weil er dachte, das gehört so. Seitdem spiegele ich meine Skripte in VS Code (geniale Erweiterung: ioBroker-javascript) und nutze für den Ordner ein lokales git. Mit der Erweiterung GitLens ist dann die Versionsverwaltung der Skripte ein Kinderspiel. Bei Bedarf kann man den Ordner mit einem privaten GitHub-Repo synchronsisieren.
  • Fußballergebnisse immer Live, ohne Konferenz. ;)

    Verschoben
    27
    1 Stimmen
    27 Beiträge
    4k Aufrufe
    icebearI
    @robson sagte in Fußballergebnisse immer Live, ohne Konferenz. ;): Genau, HA = Home Assistant Ich habe bei Github den Adapter direkt gefunden. Vllt genügt das schon als erste Doku: Ich hab das bei mir umgesetzt, zuerst mit der kompletten 1.BL und 2.BL und mit dem HA Adapter die ganzen Daten abgegriffen. Das Problem war allerdings und das beschreiben auch immer wieder die Football Nerds in den USA die die komplette NFL Season abbilden das das ganze sehr Ressourcen hungrig ist. Das kann ich auch bestätigen. An einem Spieltag der BL hat sich mein iobroker immer wieder aufgehangen (Synology NAS mit 16GB RAM). Deshalb hab ich das ganze Projekt mit der kompletten 1.BL und 2.BL und den Daten von HA nach iobroker wieder gecanceled. Ich hab jetzt nur noch meine Manschaft als TeamTracker Card mit Toralarm. Hier mal die DP's die von ESPN (HA) TeamTracker bereitgestellt werden: [image: 1773133226277-haf95-resized.png] Ich hab jetzt nur noch die TeamTracker Card die bei Spielbeginn auf meiner VIS Startseite angezeigt wird: [image: 1773133395206-team_tracker.png] und über ALEXA wird ein Sound abgespielt wenn ein Tor fällt. Der Tor-Alarm ist (bei mir) ca 1-2 min Zeitverzögert, das stört mich aber nicht so sehr, da das ja eh nur dafür ist wenn ich nicht live schauen kann und so trotzdem mitbekomme wenn ein Tor fällt.
  • [Vorlage] todoist.com To-Do-Listen Script für VIS

    Verschoben
    133
    2
    0 Stimmen
    133 Beiträge
    28k Aufrufe
    F
    danke für den Hinweis. Habe das Skript aber wieder zum laufen bekommen ;) Lösung waren einfach neue URLs: https://api.todoist.com/api/v2/projects https://api.todoist.com/api/v2/tasks
  • Alexa Shopping List mit Bring synchronisieren

    182
    0 Stimmen
    182 Beiträge
    40k Aufrufe
    mcBirneM
    @grrfield sagte in Alexa Shopping List mit Bring synchronisieren: @mcBirne Es ist zwar schon einige Zeit her, aber hast Du das Skript als TypeScript eingefügt? Die Fehlermeldungen sehen nach JavaScript aus. nein, das wars, danke für den Tipp!
  • Analogwerte an loxone übertragen

    Verschoben
    19
    0 Stimmen
    19 Beiträge
    6k Aufrufe
    A
    Hallo, Habs nun hin bekommen. Man muss in der Loxone Config beim Virtuellen Eingang den hacken bei nur Status Anzeige" entfernen. Was für mich erst mal unlogisch ist, da ich ja mit dem Eingang nur was Anzeigen will. Egal nun funktioniert es perfekt.
  • Anwesenheitscontrol basierend auf TR64 Adapter - Script

    Verschoben
    118
    1
    2 Stimmen
    118 Beiträge
    30k Aufrufe
    D
    @cephalopod Ja .. aktuell auf 8.21 .. ist aber schon eine Weile so
  • Verbesserung erfahren Blockly/Javascript/KI/AI

    48
    1 Stimmen
    48 Beiträge
    2k Aufrufe
    crunchipC
    @Meister-Mopper und weils interessant ist, was die verschiedenen KI´s so ausspucken Fehleranalyse & Ressourcen-Check Dein Script hat 1 kritischen Fehler und 3 Performance-Probleme, ist aber grundsätzlich solide aufgebaut. Kritische Fehler ❌ createState() fehlt komplett States wie Tagesverbrauch, Netzbezug etc. existieren beim ersten Start nicht → Script crasht mit "State not found". ioBroker erstellt States NICHT automatisch bei setState(). Ressourcenverschwendung Problem Impact Fix getState() in Schleife Blockiert 10-50ms bei jedem Trigger (4x/Sekunde = 200ms CPU-Last!) Cache in Variable speicherMax statisch Änderungen der Speichergröße erst nach Script-Neustart Trigger hinzufügen Tagesverbrauch ohne Cache Liest State bei jedem Update (360x/Stunde) Global cachen 7 setState() pro Trigger ~420 DB-Writes/Stunde bei 10s-Updates Akzeptabel, aber debounce möglich Formel-Validierung javascript hausverbrauch = pvPower + netPower - batPower Korrekt NUR WENN Victron-Konvention: batPower = +Laden (Energie geht rein), -Entladen (Energie kommt raus). Teste mit log("Bat: " + batPower) beim Laden/Entladen! ​
  • Bestandsliste für Filament

    2
    1 Stimmen
    2 Beiträge
    113 Aufrufe
    NegaleinN
    @Mirtl sagte in Bestandsliste für Filament: Vielen Dank schon mal. versuchs mit Hilfe von ChatGPT https://chatgpt.com/c/6989d273-c148-8333-83ce-d8e5a22ee001
  • Anwesenheitssimulation - Standalone-Version?

    3
    0 Stimmen
    3 Beiträge
    162 Aufrufe
    B
    Ich stelle mir das event. so vor. Ist mein erstes Projekt auf github. Das ist noch alles "Beta". https://github.com/Kenaschon/aws-anwesenheitssimulation Ist noch nicht getestet. Komme ich erst jetzt am WE dazu.
  • Timeout bei Event wieder stoppen, dynamische Instanz

    7
    0 Stimmen
    7 Beiträge
    210 Aufrufe
    S
    Und den Bug mit den counts habe ich noch gar nicht realisiert. In Java wäre das gegangenen :) ein zweites mal vielen Dank!

526

Online

32.8k

Benutzer

82.8k

Themen

1.3m

Beiträge