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
    nik82N
    Hallo, leider funktioniert das bei mir immer noch nicht, also der zweite Post von mir ist falsch. Falls jemand noch eine Idee hat wie man das per Blockly lösen kann, bitte gerne Bescheid geben :-)
  • 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
  • IAS Keypad: Funktionsgruppe korrekt? Datenpunkte anlegen?

    1
    1
    0 Stimmen
    1 Beiträge
    339 Aufrufe
    Niemand hat geantwortet
  • exec, sh script von debian ausführen

    9
    0 Stimmen
    9 Beiträge
    846 Aufrufe
    J
    @thomas-braun ja, arbeite jetzt mit systemd. funktioniert schon alles. echt super. ich ärgere mich über mich selber, da ich auch früher daran denken hätte können. in den programiiersprachen die ich kenne und nutze (php, jquery, mysql) gibt es die klammern auch nicht. nur bei html tags..... ach... ich dachte das linux ev. die klammern zum parsen bracuht, stattdessen nur platzhalter :-( im tutorial waren alle anderen platzhalter mit den typischen " (anführungszeichen deklariert)
  • Verschwinden von MQTT Geräten erkennen und melden

    16
    0 Stimmen
    16 Beiträge
    483 Aufrufe
    mickymM
    @martinp Nein das sagte ich doch, Du kannst Doch das ganze in eine Schleife einbinden und über ein Array durchiterieren.Oder Du triggerst direkt über den Thermostat oder abfragst. Wie gesagt welche Logik zum Abfragen du nimmst oder triggern ist Deine Sache. Aber so kannst Du einfach eine Liste von Thermostaten erstellen und alle topics erstellem_ [image: 1692114028255-af2fba55-1906-4ff1-bcb6-33c41440783a-image.png] Spoiler <xml xmlns="https://developers.google.com/blockly/xml"> <variables> <variable id="nu$N7|=f+vnO8RVm:cym">Liste_Thermostate</variable> <variable id="JW8gktT*}~bRUd6MfH#u">Thermostat</variable> </variables> <block type="variables_set" id="hK7EI~VB@!#$5jf,=)lr" x="-587" y="-112"> <field name="VAR" id="nu$N7|=f+vnO8RVm:cym">Liste_Thermostate</field> <value name="VALUE"> <block type="lists_create_with" id="apI:3%H0NG=t+k`}/0LA"> <mutation items="3"></mutation> <value name="ADD0"> <block type="text" id="p%1gC:]`|DHfy{KP7JQf"> <field name="TEXT">thermostat_1</field> </block> </value> <value name="ADD1"> <block type="text" id="pNnv)I]Eq3==EBw;hK}d"> <field name="TEXT">thermostat_2</field> </block> </value> <value name="ADD2"> <block type="text" id="H{[NPCBKq05V;rK-40,2"> <field name="TEXT">thermostat_3</field> </block> </value> </block> </value> <next> <block type="controls_forEach" id="g5T}i9pUy1C0p3vha+5z"> <field name="VAR" id="JW8gktT*}~bRUd6MfH#u">Thermostat</field> <value name="LIST"> <block type="variables_get" id="xbe)qBW[jl?dmaR.[:Ny"> <field name="VAR" id="nu$N7|=f+vnO8RVm:cym">Liste_Thermostate</field> </block> </value> <statement name="DO"> <block type="sendto_custom" id="ls8@a?b_FZd{/PwZ%,li"> <mutation xmlns="http://www.w3.org/1999/xhtml" items="topic,message" with_statement="false"></mutation> <field name="INSTANCE">mqtt.1</field> <field name="COMMAND">sendMessage2Client</field> <field name="LOG"></field> <field name="WITH_STATEMENT">FALSE</field> <value name="ARG0"> <shadow type="text" id="$,O^*f*}($?[H8_(d*+U"> <field name="TEXT">thermostat1/isAlive</field> </shadow> <block type="text_join" id="O6E{W{#l(2VcnvqOe|Rl"> <mutation items="2"></mutation> <value name="ADD0"> <block type="variables_get" id="AzpRo(og/GD94mdngxQF"> <field name="VAR" id="JW8gktT*}~bRUd6MfH#u">Thermostat</field> </block> </value> <value name="ADD1"> <block type="text" id="VB)a`Lf$skQC8CWbgCS_"> <field name="TEXT">/isAlive</field> </block> </value> </block> </value> <value name="ARG1"> <shadow type="text" id="kYzE4%b}Px*fTaC#_m53"> <field name="TEXT">Meine Nachricht</field> </shadow> <block type="logic_boolean" id="#^)Qo*Dc+?;*v.HB%8Q$"> <field name="BOOL">TRUE</field> </block> </value> </block> </statement> </block> </next> </block> </xml> das sind aber Basics beim Puzzeln (Geht aber auch mit steuere oder aktualisiere von Datenpunkten). Wie gesagt das sind Basics, dass man identische Codeteile über Schleifen durchläuft. Und wer mich kennt, weiss, dass ich ein anderes Tool bevorzuge - das sogar direkt mit mqtt kommunizieren kann. ;)
  • Skript für Schalter um Skript zu pausieren/deaktivieren

    13
    1
    0 Stimmen
    13 Beiträge
    1k Aufrufe
    M
    @cluni super Hinweis! Ist mir noch gar nicht aufgefallen! Danke
  • Mehrere Shelly Plus 1 PM | Urlaubsschaltung

    1
    0 Stimmen
    1 Beiträge
    420 Aufrufe
    Niemand hat geantwortet
  • Synology Adapter Snapshot erneuern

    3
    1
    0 Stimmen
    3 Beiträge
    177 Aufrufe
    Samson71S
    @quaxman Mehrfachposts machen den Sachverhalt nicht besser. Bitte vermeiden. https://forum.iobroker.net/topic/67078/synology-adapter-snapshot-per-telegram-versenden-gelöst/4?_=1692004867774
  • Neue Objekte regelmäßig in Datenbank schreiben

    5
    1
    0 Stimmen
    5 Beiträge
    386 Aufrufe
    I
    Erstmal danke. Ja, das würde ich gerne automatisieren. Gut, dann fange ich an zu coden.
  • Keine Änderungen in Scipts möglich

    13
    1
    0 Stimmen
    13 Beiträge
    1k Aufrufe
    crunchipC
    @berndroid sagte in Keine Änderungen in Scipts möglich: Beim Umzug der VM scheint ne Menge kaputt gegangen zu sein. was soll denn kaputt gehen, wenn du eine neue VM anlegst, iobroker installierst und dein backup zurück spielst, muss dieser natürlich aufgrund nodejs v18, neu gebaut werden. Es kann durchaus möglich sein, in speziellen Fällen, das der rebuild nicht funktioniert, diesbezüglich bekommt man im log Meldung und muss selbst Hand anlegen. Zu deinem Javascript Problem, wie eingangs erwähnt, du der Meinung bist, das dies erst aufgrund eines Umzugs entstanden ist, läuft möglicherweise ein script nicht korrekt(Ip Adresse irgendwo in Verwendung?)
  • Import vom Backup meiner Skripte im JS Adapter schlägt fehl

    10
    0 Stimmen
    10 Beiträge
    603 Aufrufe
    S
    @crunchip Damit könntest du recht haben
  • Astro-Trigger mit Versatz funktioniert nicht

    blockly
    10
    3
    0 Stimmen
    10 Beiträge
    2k Aufrufe
    A
    @basic80 said in Astro-Trigger mit Versatz funktioniert nicht: Mithilfe eines Testskripts habe ich inzwischen herausgefunden, dass bis 02:00 Uhr alles funktioniert. Wenn Golden hour-Ende minus x < 02:00 Uhr, wird der Trigger wohl ignoriert. [image: 1597213641959-goldenhour-test-log3.jpg] Um sicherzustellen, dass der Astrotrigger (mit Offset) wirklich nur bis 02:00 Uhr funktioniert und es keine andere Ursache gibt, wäre es schön, wenn jemand das mal testen könnte. Der Thread hier ist zwar schon uralt, aber ich bin gerade bei meinen eigenen Recherchen zum Astro-Trigger darüber gestolpert. Ich habe für das Verhalten eigentlich nur eine Erklärung: Bei der Berechnung der Datumsgrenze, also der Frage, ob der Triggerzeitpunkt auf dem heutigen oder dem gestrigen Tag liegt, wird intern die lokale Uhrzeit auf UTC umgerechnet. Da dein Beitrag aus dem Sommer stammt, dürften sich deine Zeitangaben also nach UTC+2 richten. Das bedeutet, dass "vor 2:00 Uhr" bei dir "vor 0:00 Uhr" nach UTC heißt. Ich teste jetzt mal selbst ein bisschen mit dem Astro-Trigger, um das zu verifizieren.
  • Wechselrichter Steuerung null Einspeisung Blockly Skript

    blockly javascript
    6
    2
    0 Stimmen
    6 Beiträge
    1k Aufrufe
    R
    @paul53 Zähler kommt alle 5 bis 10 sekunden, und das skript schaut ja alle 15 sekunden. ich vermute das das skript das negative vorzeichen irgendwie wegnimmt
  • Sun 1000 g2 WR im iobroker

    blockly javascript
    4
    0 Stimmen
    4 Beiträge
    527 Aufrufe
    G
    @mymeyer Das kann ich Dir nicht sagen, am Besten mal in den einschlägigen Foren gucken, oder den Entwickler selbst anschreiben. Ich nutze den Lunentree mit Trucki´s Platine, Läuft. Oder Du guckst mal bei Christian auf YT und stellst ihm einfach die Frage, wenn es einer weiss, dann er ;-) Hab grade mal geguckt, da muss dann noch eine Platine mit eingebaut werden, dann ist er kompatibel--> siehe github trucki-eu
  • VSCode Deklarationsfehler

    4
    0 Stimmen
    4 Beiträge
    406 Aufrufe
    T
    @oliverio sagte in VSCode Deklarationsfehler: @oberst_von_gatow https://codingbeautydev.com/blog/typescript-cannot-redeclare-block-scoped-variable/?utm_content=cmp-true Danke, das gefällt mir deutlich besser. gibts auch einen Weg um diesen "Fehler" zu entfernen: onStop(function (callback:any) { stop1 = true; callback(); }, 2000 /*ms*/); callback() wird als möglicherwiese nicht definiertes Objekt angezeigt. Ja gibts... einfach definieren, kaum hab ich die Frage gestellt, schon fällt mir die Antwort ein.
  • FEHLER: Cannot extract Blockly code

    12
    1
    0 Stimmen
    12 Beiträge
    875 Aufrufe
    crunchipC
    @shigi76 sagte in FEHLER: Cannot extract Blockly code: das kann ich gar nicht genau sagen.. du musst doch wissen ob diese Adapter installiert sind, falls etwas im blockly verwendet wird was aber nicht vorhanden ist, verursacht dies Probleme
  • json nach iobroker übertragen

    8
    0 Stimmen
    8 Beiträge
    591 Aufrufe
    haus-automatisierungH
    @arteck Das kann sein, der Adapter wird ja auch nicht mehr so richtig gepflegt soweit ich weiß. Ich nutze zumindest nur noch rest-api.
  • Unhandled promise rejection

    14
    2
    0 Stimmen
    14 Beiträge
    434 Aufrufe
    T
    Vielen Dank für die schnellen Rückmeldungen. Mit der 7.0.3 läuft wieder alles. Warum ich da nicht selbst drauf gekommen bin, weiß ich jetzt auch nicht. Wahrscheinlich verläuft man sich halt irgendwann. Hoffentlich muß ich jetzt nicht mein Zertifikat beim Matthias wieder abgeben :-) Danke!
  • [gelöst] Meteohub Daten, XML parsen, JSON durchsuchen

    javascript
    43
    0 Stimmen
    43 Beiträge
    5k Aufrufe
    T
    @steinche sagte in [gelöst] Meteohub Daten, XML parsen, JSON durchsuchen: ersetzt. "Problem" selbst gelöst und autodidaktisch den Zahlenbereich bis 20 erschlossen :) https://github.com/ioBroker/ioBroker.javascript/blob/master/docs/en/javascript.md#best-practice als referenze
  • Steckdose mit 2 Datenpunkten für on off

    31
    3
    0 Stimmen
    31 Beiträge
    3k Aufrufe
    MartinPM
    @vazi Ich bin selber blockly rookie, sorry - bevor ich Dir etwas falsches Mitgebe Man muss sehen, ob man den Zustand, den die Steckdose hat irgendwo braucht. Wenn das nicht nötig ist, dann kann man ja auch einfach die AN bzw AUS Pulse auch einfach so auslösen Hier habe ich einen Puls ausgelöst ( 35 Sekunden Tauch-Pumpe im Regenfass an, um die Tomaten zu wässern) Hängt aber an einem Zeitplan. Man könnte da vielleich einen Datenpunkt in Userdata anlegen, den man setzen und löschen kann. Der könnte dann statt des Zeitplans Auslöser der beiden Aktionen die der Zeitplan klammert sein.. Man braucht natürlich einen Puls zum Setzen und einen Puls zum Löschen ... [image: 1691164374282-90b05bc7-885d-43af-9074-63fa0cb8ff98-grafik.png]
  • Abfrage Verfügbarkeit Zigbee Schalter

    7
    0 Stimmen
    7 Beiträge
    738 Aufrufe
    T
    @ticaki ixh nutze den Zigbee Adapter, nicht Zigbee2Mqqt. Ujd bei meinem Adapter habe ich nichts gefunden, um den Wert anzupassen
  • [gelöst] Namen der Datenpunkte ändern

    5
    0 Stimmen
    5 Beiträge
    604 Aufrufe
    S
    @paul53 das wusste ich nicht. Nun geht es. Wunderbar, vielen Dank!

564

Online

32.8k

Benutzer

82.8k

Themen

1.3m

Beiträge