Weiter zum Inhalt

Skripten / Logik

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

    7k 80k
    7k Themen
    80k Beiträge
    paul53P
    @Qlink [sagte]: innerhalb des Timeouts nicht neu gestartet werden Version, die auch einen Skriptstart innerhalb der 60 Minuten übersteht: [image: 1780056247637-blockly_temp.jpg]
  • Hilfe für Skripterstellung mit Node-RED

    959 13k
    959 Themen
    13k Beiträge
    G
    Hallo, die Temperatur wird im dashboard ausgegeben, aber nicht in der CerboGX, Danke erstmal. Mir wurde dieses Forum empfohlen !
  • 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
    13k Aufrufe
    NegaleinN
    @Schimi sagte in Scriptsammlung Vol. 2 -- Diskussion: Wetter.com Forecast/Vorhersage erledigt :)
  • Alexa Shopping List mit Bring synchronisieren

    183
    0 Stimmen
    183 Beiträge
    42k Aufrufe
    S
    Hallo zusammen, nach dem ich hier viel über IO Broker in Verbindung mit Bring und der Alexa Shopping Liste gelernt habe und immer mal wieder "tweaks" an meinem abgewandelten Script vorgenommen habe und es nun super geschmeidigt läuft, möchte ich gerne das aktuelle ALEXA BRING SCRIPT mit euch teilen und der hervorragenden Community auf diesem Weg etwas zurück geben und einfach nur DANKE sagen. Zum Script: Bei der brindBaseID müsst ihr XXX wie gewohnt durch eure Bring Shopping List ID ersetzen, damit die richtige Liste befüllt wird. Ansonste alles wie immer: Alexa2.0 Adapter und bring.0 Adapter sind Voraussetzung, dass es funktioniert. Steht aber auch auskommentiert im Script. // ===================================================================== // Alexa Shopping List <-> Bring! Zwei-Wege-Sync // Engine: 3-Wege-Merge gegen persistierten Schatten + Zeitstempel-Konfliktlösung // Behandelt korrekt: Hinzufügen, Abhaken, Reaktivieren, Löschen (beidseitig) // ===================================================================== // --- KONFIGURATION: an die eigene Installation anpassen --- // Bring-Listen-ID findest du in den Objekten unter "bring.0" -> Channel deiner Liste (UUID): const bringBaseId = 'bring.0.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; // Alexa-Einkaufsliste, bei den meisten Setups: const alexa2BaseId = 'alexa2.0.Lists.SHOPPING_LIST'; // Voraussetzungen: Adapter "alexa2" + "bring" eingerichtet, 0_userdata.0 vorhanden. // Persistenter Speicher für den Schatten (0_userdata.0 existiert in modernen ioBroker-Installs) const SHADOW_STATE = '0_userdata.0.alexaBringSync.shadow'; // Timing const DEBOUNCE_MS = 2000; // warte nach der letzten Änderung, bevor synchronisiert wird const LOCK_MS = 5000; // Beruhigungsfenster, bis async Schreibvorgänge der Adapter durch sind // Bei einem echten Konflikt (beide Seiten gleichzeitig geändert, Zeitstempel uneindeutig): // true = im Zweifel "active" gewinnen lassen (nichts verlieren, was man evtl. noch braucht) const CONFLICT_PREFER_ACTIVE = true; // Optionaler Watchdog: warnt im Log, wenn die alexa2-Cloud-Verbindung wegbricht const WATCH_ALEXA_CONNECTION = true; // --- ABGELEITETE IDs --- const bringListId = bringBaseId + '.content'; const bringListCompletedId = bringBaseId + '.recentContent'; const bringAddToList = bringBaseId + '.saveItem'; // hinzufügen / aus recent reaktivieren const bringCompleteItem = bringBaseId + '.moveToRecentContent'; // abhaken (-> recent) const bringRemoveItem = bringBaseId + '.removeItem'; // komplett entfernen (content + recent) const alexaAddToList = alexa2BaseId + '.#New'; const alexaListId = alexa2BaseId + '.json'; // --- LAUFZEIT-ZUSTAND --- let isSyncing = false; let pendingSync = false; let syncTimeout = null; let debounceTimer = null; // --- HILFSFUNKTIONEN --- function cleanName(str) { return str ? String(str).trim().toLowerCase() : ''; } function formatName(str) { if (!str) return ''; str = String(str).trim(); return str.charAt(0).toUpperCase() + str.slice(1); } function loadShadow() { try { const s = getState(SHADOW_STATE); return (s && s.val) ? JSON.parse(s.val) : {}; } catch (e) { log('Schatten nicht lesbar, starte leer: ' + e, 'warn'); return {}; } } function saveShadow(sh) { setState(SHADOW_STATE, JSON.stringify(sh), true); } // Konfliktlösung: jüngerer Zeitstempel gewinnt, sonst konfigurierte Vorzugsregel function resolveConflict(aStatus, bStatus, aTs, bTs, displayName) { let winner; if (aTs > bTs) winner = aStatus; else if (bTs > aTs) winner = bStatus; else winner = CONFLICT_PREFER_ACTIVE ? ((aStatus === 'active' || bStatus === 'active') ? 'active' : 'done') : ((aStatus === 'done' || bStatus === 'done') ? 'done' : 'active'); log(`[KONFLIKT] "${displayName}": Alexa=${aStatus}(${aTs}) vs Bring=${bStatus}(${bTs}) -> ${winner}`, 'warn'); return winner; } function finishLock() { if (syncTimeout) clearTimeout(syncTimeout); syncTimeout = setTimeout(() => { isSyncing = false; if (pendingSync) { pendingSync = false; triggerSync(); } // Nachlauf für während der Sperre Eingetroffenes }, LOCK_MS); } // --- HAUPTLOGIK --- function doSync() { if (isSyncing) { pendingSync = true; return; } isSyncing = true; try { const aState = getState(alexaListId); const bState = getState(bringListId); const rState = getState(bringListCompletedId); if (!aState || aState.val == null || !bState || bState.val == null) { finishLock(); return; } const alexaList = JSON.parse(aState.val); const bringActive = JSON.parse(bState.val); const bringRecent = (rState && rState.val) ? JSON.parse(rState.val) : []; // grobe Bring-Zeitstempel (Listenebene) nur für die Konfliktlösung const bringContentLc = bState.lc || bState.ts || 0; const bringRecentLc = (rState && (rState.lc || rState.ts)) || 0; // --- Maps der aktuellen Zustände --- const alexaByName = {}; // active gewinnt bei Namens-Duplikaten alexaList.forEach(it => { const k = cleanName(it.value); if (!k) return; const st = it.completed ? 'done' : 'active'; if (!alexaByName[k] || st === 'active') alexaByName[k] = it; }); const bringActiveMap = {}; bringActive.forEach(it => { const k = cleanName(it.name); if (k) bringActiveMap[k] = it; }); const bringRecentMap = {}; bringRecent.forEach(it => { const k = cleanName(it.name); if (k) bringRecentMap[k] = it; }); const shadow = loadShadow(); const newShadow = {}; const names = new Set([ ...Object.keys(alexaByName), ...Object.keys(bringActiveMap), ...Object.keys(bringRecentMap), ...Object.keys(shadow) ]); names.forEach(name => { const aItem = alexaByName[name]; const aStatus = aItem ? (aItem.completed ? 'done' : 'active') : 'absent'; const bStatus = bringActiveMap[name] ? 'active' : (bringRecentMap[name] ? 'done' : 'absent'); const prev = shadow[name] || { a: 'absent', b: 'absent', c: 'absent' }; const aChanged = aStatus !== prev.a; const bChanged = bStatus !== prev.b; const displayName = (bringActiveMap[name] || bringRecentMap[name] || {}).name || (aItem ? aItem.value : null) || prev.name || formatName(name); const aTs = aItem ? (aItem.updatedDateTime || 0) : 0; const bTs = (bStatus === 'active') ? bringContentLc : (bStatus === 'done' ? bringRecentLc : 0); // --- Zielzustand T bestimmen --- let T; if (aChanged && !bChanged) T = aStatus; else if (bChanged && !aChanged) T = bStatus; else if (aChanged && bChanged) T = (aStatus === bStatus) ? aStatus : resolveConflict(aStatus, bStatus, aTs, bTs, displayName); else T = prev.c; // nichts geändert -> Kanon halten if (!T) T = (aStatus !== 'absent') ? aStatus : bStatus; // --- Alexa angleichen --- let aResult = aStatus; if (T === 'active') { if (aStatus === 'absent') { log(`[ALEXA +] ${displayName}`); setState(alexaAddToList, displayName); aResult = 'active'; } else if (aStatus === 'done') { log(`[ALEXA reaktiv.] ${displayName}`); setState(`${alexa2BaseId}.items.${aItem.id}.completed`, false); aResult = 'active'; } } else if (T === 'done') { if (aStatus === 'active') { log(`[ALEXA abgehakt] ${displayName}`); setState(`${alexa2BaseId}.items.${aItem.id}.completed`, true); aResult = 'done'; } // aStatus 'absent' -> erledigtes Item lässt sich in Alexa nicht erzeugen, bleibt absent } else if (T === 'absent') { if (aStatus !== 'absent') { log(`[ALEXA gelöscht] ${displayName}`); setState(`${alexa2BaseId}.items.${aItem.id}.#delete`, true); aResult = 'absent'; } } // --- Bring angleichen --- let bResult = bStatus; if (T === 'active') { if (bStatus !== 'active') { log(`[BRING +] ${displayName}`); setState(bringAddToList, displayName); bResult = 'active'; } } else if (T === 'done') { if (bStatus === 'active') { log(`[BRING abgehakt] ${displayName}`); setState(bringCompleteItem, displayName); bResult = 'done'; } // bStatus 'absent' -> kann nicht als erledigt erzeugt werden, bleibt absent } else if (T === 'absent') { if (bStatus !== 'absent') { log(`[BRING gelöscht] ${displayName}`); setState(bringRemoveItem, displayName); bResult = 'absent'; } } // --- neuen Schatten schreiben (komplett verschwundene Items fallen raus) --- if (!(aResult === 'absent' && bResult === 'absent')) { newShadow[name] = { a: aResult, b: bResult, c: T, name: displayName, ts: Date.now() }; } }); saveShadow(newShadow); } catch (e) { log('Fehler im Sync-Skript: ' + e, 'error'); } finishLock(); } // --- TRIGGER mit Debounce --- function triggerSync() { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(doSync, DEBOUNCE_MS); } on({ id: bringListId, change: 'ne' }, triggerSync); on({ id: bringListCompletedId, change: 'ne' }, triggerSync); on({ id: alexaListId, change: 'ne' }, triggerSync); if (WATCH_ALEXA_CONNECTION) { on({ id: 'alexa2.0.info.connection', change: 'ne' }, obj => { if (obj.state.val === false) log('alexa2-Cloud-Verbindung verloren – Sync evtl. inaktiv bis zum Reconnect.', 'warn'); }); } // --- START: Schatten-State sicherstellen, dann initial abgleichen --- createState(SHADOW_STATE, '{}', false, { name: 'Alexa-Bring Sync Shadow', type: 'string', read: true, write: true }, () => { log('Alexa <-> Bring Sync (Shadow-Merge) gestartet. Initialer Abgleich …'); triggerSync(); }); Das Script müsste ihr einfach als JS Script in IO Broker kopieren und starten. Der Rest funktioniert dann automatisch. WICHTIG NOCHMAL: Die Bring Base ID bzw. der Platzhalter XXX mit eurer echten Listen ID ersetzen. DANKE nochmal an die gesamte Community, mit deren Hilfe dieses angepasste und für meine Zwecke hervorragend funktionierende Script entstanden ist. KUDOS an alle die ihre Scripts geteilt haben.
  • VCF Datei auslesen

    2
    0 Stimmen
    2 Beiträge
    69 Aufrufe
    M
    Thema hat sich erledigt. War am Ende total easy. http://"user":"passwort"@xxx.xxx.xxx.xxx:5000/carddav/"user"/"adressbuch-id"/"kontakt-id".vcf war die Lösung. Jetzt sind auch die Anrufbilder verfügbar.
  • Habe ein Problem in Typescript ....Fehler im Script

    6
    0 Stimmen
    6 Beiträge
    127 Aufrufe
    T
    @OliverIO sagte: @ticaki Allerdings ist es meiner Meinung nach aktuell immer noch so ist, das man Grundlagen der Programmierung kennen muss. Wenn du absolut blank bist, verstehst du nichtmal die Ausgabe der KI und gibst beim 3 Versuch auf. Die Eisntiegshürden sind zwar wirklich gering geworden, aber man muss das auch lernen wollen und auch die prompts richtig formulieren. sonst kommt nur schrott raus. Das stimmt, war mir zuviel arbeit den Text rüber zu kopieren und passend zu formatieren. Umso mehr ahnung man hat umso schneller kommt man als Ziel außer man macht sowas: ❯ lass den quark und gehe es richtig an - setzte agenten darauf an im internet zu suchen wie man das macht und obs überhaupt geht - prüfe die infos und komme dann mit neuen vorschlägen :D
  • Einschaltverzögerung mit schwankenten Werten

    17
    0 Stimmen
    17 Beiträge
    285 Aufrufe
    J
    ich wollte hier keine Verwirrung auslösen, ich bin froh das hier Leute wie ihr bereit sind zu helfen und dann werden die Themen besprochen...Dank geht an Euch!
  • [gelöst] Wie Timer finden?

    10
    0 Stimmen
    10 Beiträge
    269 Aufrufe
    I
    DANKE! Ja, der Bekannte hat das Script (tatsächlich waren es noch 2 weitere verschollene) im Objektbaum javascript gesehen und dort zunächst disabled und gelöscht, und der Timer wird nicht mehr angezeigt. Jetzt warten wir noch 12:12 Uhr ab ;-) (Update: hat geklappt) Ich denke, das war's! Vielen Dank an alle für die freundliche Hilfe, eine kleine Spende für's Projekt geht gleich raus.
  • Mr Pure Salzelektrolyse

    6
    0 Stimmen
    6 Beiträge
    504 Aufrufe
    H
    also, ich habs also geschafft. in die Tuya App hinzugefügt und dann mittels Adapter. Ist allerdings ein bissl eine Spielerei mit dem Developer Account und dem verlinken...aber ich hab alle Daten DANKE
  • Proxmox-Updater (Host/LXC/VM) auch ioBroker,piHole,etc

    58
    3 Stimmen
    58 Beiträge
    11k Aufrufe
    da_WoodyD
    @Bass-T GRANULATION! hab ich schon lange geinstet, echt goil!
  • Daten Seriell von Paradigma Solaranlage lesen

    javascript communication heating
    55
    0 Stimmen
    55 Beiträge
    8k Aufrufe
    Samson71S
    @MatthiasROW Unabhängig davon, dass dieser Beitrag mit über 5 Jahren mehr als steinalt ist........ Schonmal auf der genannten Webseite selber nachgesehen? Nur weil ein (uralter) Link nicht mehr passt, existiert die Webseite und auch deren Inhalte grundsätzlich noch. Ich finde da jedenfalls ne Menge Infos zum Systa Aqua II - Logger incl. der Schaltungsbeschreibung.
  • Ecconreset bei mqtt Teilnehmer

    1
    2
    0 Stimmen
    1 Beiträge
    41 Aufrufe
    Niemand hat geantwortet
  • mqtt-Abruf WiCAN-OBD-Dongle mit mqtt-Adapter und Blockly

    12
    1
    0 Stimmen
    12 Beiträge
    2k Aufrufe
    K
    Hi, Läuft es bei dir noch? Ich bin am überlegen mir auch ein WiCAN-OBD für unseren eUP zu holen. Habe davon aber ehrlich gesagt keine Ahnung. Könntest du mich dann unterstützen? Das wichtigste für mich ist, den aktuellen Akkustand in iobroker zu bekommen. Vielej Dank vorab! :)
  • Ostrom Api auslesen

    4
    0 Stimmen
    4 Beiträge
    675 Aufrufe
    NicolomaN
    ich habe weiter ein anderes: const axios = require('axios'); // ======= OSTROM PROD ======= const CLIENT_ID = 'DEINE_ID'; const CLIENT_SECRET = 'DEIN_SECRET'; const ZIP = '59759'; // ======= INFLUXDB 2.x ======= const INFLUX_URL = 'http://192.168.178.103:8086'; const INFLUX_ORG = 'my_org'; const INFLUX_BUCKET = 'iobroker'; const INFLUX_TOKEN = 'DEIN_TOKEN'; // ======= IO BROKER ======= const BASE_DP = '0_userdata.0.ostrom'; const AUTH_URL = 'https://auth.production.ostrom-api.io/oauth2/token'; const API_URL = 'https://production.ostrom-api.io'; let isRunning = false; createStates(); // API nur selten abrufen schedule('30 14 * * *', fetchPrices); // Status stündlich aktualisieren schedule('0 * * * *', updateCurrentFromCache); // beim Start nur Status aus Cache aktualisieren setTimeout(updateCurrentFromCache, 10000); // manueller API-Abruf on({ id: `${BASE_DP}.control.runNow`, val: true }, async () => { await setStateAsync(`${BASE_DP}.control.runNow`, false, true); await fetchPrices(); }); async function fetchPrices() { if (isRunning) return; const lastFetch = getState(`${BASE_DP}.meta.lastFetch`)?.val || 0; const now = Date.now(); if (now - lastFetch < 120000) { log('Ostrom: Letzte API-Abfrage < 2 Minuten – überspringe', 'warn'); return; } isRunning = true; try { const token = await getAccessToken(); const prices = await getPrices(token); const sorted = prices .slice() .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); await setStateAsync(`${BASE_DP}.todayTomorrow.json`, JSON.stringify(sorted), true); await updateCurrentFromPrices(sorted); await updateForecast(sorted); await writeToInflux(sorted); await setStateAsync(`${BASE_DP}.meta.lastFetch`, now, true); await setStateAsync(`${BASE_DP}.meta.lastFetchReadable`, new Date(now).toLocaleString('de-DE'), true); log(`Ostrom OK: ${sorted.length} Preise`, 'info'); } catch (err) { if (err.response?.status === 429) { log('Ostrom 429: Rate Limit – später erneut versuchen', 'warn'); } else { log(`Ostrom Fehler: ${err.message}`, 'error'); } if (err.response) { log(`Status: ${err.response.status}`, 'error'); log(JSON.stringify(err.response.data), 'error'); } } finally { isRunning = false; } } async function updateCurrentFromCache() { const json = getState(`${BASE_DP}.todayTomorrow.json`)?.val; if (!json) { log('Ostrom: Kein Preis-Cache vorhanden', 'warn'); return; } try { const prices = JSON.parse(json); await updateCurrentFromPrices(prices); log('Ostrom current.isFree stündlich aktualisiert', 'info'); } catch (err) { log(`Ostrom Cache Fehler: ${err.message}`, 'error'); } } async function updateCurrentFromPrices(prices) { const current = findCurrentPrice(prices); if (!current) return; const grossKwhPrice = Number(current.grossKwhPrice ?? 0); const grossKwhTaxAndLevies = Number(current.grossKwhTaxAndLevies ?? 0); // echter variabler Bruttopreis ohne monatliche Kosten const effectiveGrossKwhPrice = grossKwhPrice + grossKwhTaxAndLevies; await setStateAsync(`${BASE_DP}.current.grossKwhPrice`, round(grossKwhPrice), true); await setStateAsync(`${BASE_DP}.current.grossKwhTaxAndLevies`, round(grossKwhTaxAndLevies), true); await setStateAsync(`${BASE_DP}.current.effectiveGrossKwhPrice`, round(effectiveGrossKwhPrice), true); await setStateAsync(`${BASE_DP}.current.date`, current.date, true); await setStateAsync(`${BASE_DP}.current.dateReadable`, new Date(current.date).toLocaleString('de-DE'), true); await setStateAsync(`${BASE_DP}.current.hour`, new Date(current.date).getHours(), true); await setStateAsync(`${BASE_DP}.current.isFree`, effectiveGrossKwhPrice <= 0, true); } async function updateForecast(prices) { const effectivePrices = prices.map(p => { const gross = Number(p.grossKwhPrice ?? 0); const tax = Number(p.grossKwhTaxAndLevies ?? 0); return { date: p.date, value: gross + tax }; }); const values = effectivePrices.map(p => p.value); const min = Math.min(...values); const max = Math.max(...values); const avg = values.reduce((a, b) => a + b, 0) / values.length; const cheapest = effectivePrices.find(p => p.value === min); const expensive = effectivePrices.find(p => p.value === max); const nextFree = effectivePrices.find(p => new Date(p.date).getTime() >= Date.now() && p.value <= 0); await setStateAsync(`${BASE_DP}.forecast.minEffectiveGrossKwhPrice`, round(min), true); await setStateAsync(`${BASE_DP}.forecast.maxEffectiveGrossKwhPrice`, round(max), true); await setStateAsync(`${BASE_DP}.forecast.avgEffectiveGrossKwhPrice`, round(avg), true); await setStateAsync(`${BASE_DP}.forecast.cheapestDate`, cheapest?.date || '', true); await setStateAsync(`${BASE_DP}.forecast.cheapestDateReadable`, cheapest ? new Date(cheapest.date).toLocaleString('de-DE') : '', true); await setStateAsync(`${BASE_DP}.forecast.mostExpensiveDate`, expensive?.date || '', true); await setStateAsync(`${BASE_DP}.forecast.mostExpensiveDateReadable`, expensive ? new Date(expensive.date).toLocaleString('de-DE') : '', true); await setStateAsync(`${BASE_DP}.forecast.nextFreeDate`, nextFree?.date || '', true); await setStateAsync(`${BASE_DP}.forecast.nextFreeDateReadable`, nextFree ? new Date(nextFree.date).toLocaleString('de-DE') : '', true); await setStateAsync(`${BASE_DP}.forecast.hasFreeHour`, !!nextFree, true); } async function getAccessToken() { const auth = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64'); const res = await axios.post( AUTH_URL, 'grant_type=client_credentials', { headers: { Authorization: `Basic ${auth}`, 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 15000 } ); return res.data.access_token; } async function getPrices(token) { const now = new Date(); const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0); const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 2, 0, 0, 0); const res = await axios.get(`${API_URL}/spot-prices`, { params: { startDate: start.toISOString(), endDate: end.toISOString(), resolution: 'HOUR', zip: ZIP }, headers: { Authorization: `Bearer ${token}` }, timeout: 15000 }); return res.data.data || []; } async function writeToInflux(prices) { if (!prices.length) return; const lines = []; for (const p of prices) { const ts = BigInt(new Date(p.date).getTime()) * 1000000n; const grossKwhPrice = Number(p.grossKwhPrice ?? 0); const grossKwhTaxAndLevies = Number(p.grossKwhTaxAndLevies ?? 0); const effectiveGrossKwhPrice = grossKwhPrice + grossKwhTaxAndLevies; lines.push( `ostrom_prices,source=production,zip=${escapeTag(ZIP)} ` + `grossKwhPrice=${grossKwhPrice},` + `grossKwhTaxAndLevies=${grossKwhTaxAndLevies},` + `effectiveGrossKwhPrice=${effectiveGrossKwhPrice} ` + `${ts.toString()}` ); } const url = `${INFLUX_URL}/api/v2/write` + `?org=${encodeURIComponent(INFLUX_ORG)}` + `&bucket=${encodeURIComponent(INFLUX_BUCKET)}` + `&precision=ns`; await axios.post(url, lines.join('\n'), { headers: { Authorization: `Token ${INFLUX_TOKEN}`, 'Content-Type': 'text/plain' }, timeout: 15000 }); } function findCurrentPrice(prices) { const now = new Date(); return prices.find(p => { const start = new Date(p.date); const end = new Date(start.getTime() + 3600000); return now >= start && now < end; }); } async function createStates() { await ensure(`${BASE_DP}.control.runNow`, 'boolean', true, true, 'button'); await ensure(`${BASE_DP}.meta.lastFetch`, 'number', 0, true, 'value'); await ensure(`${BASE_DP}.meta.lastFetchReadable`, 'string', '', true, 'text'); await ensure(`${BASE_DP}.todayTomorrow.json`, 'string', '', true, 'json'); await ensure(`${BASE_DP}.current.grossKwhPrice`, 'number', 0, true, 'value', 'ct/kWh'); await ensure(`${BASE_DP}.current.grossKwhTaxAndLevies`, 'number', 0, true, 'value', 'ct/kWh'); await ensure(`${BASE_DP}.current.effectiveGrossKwhPrice`, 'number', 0, true, 'value', 'ct/kWh'); await ensure(`${BASE_DP}.current.date`, 'string', '', true, 'text'); await ensure(`${BASE_DP}.current.dateReadable`, 'string', '', true, 'text'); await ensure(`${BASE_DP}.current.hour`, 'number', 0, true, 'value', 'h'); await ensure(`${BASE_DP}.current.isFree`, 'boolean', false, true, 'indicator'); await ensure(`${BASE_DP}.forecast.minEffectiveGrossKwhPrice`, 'number', 0, true, 'value', 'ct/kWh'); await ensure(`${BASE_DP}.forecast.maxEffectiveGrossKwhPrice`, 'number', 0, true, 'value', 'ct/kWh'); await ensure(`${BASE_DP}.forecast.avgEffectiveGrossKwhPrice`, 'number', 0, true, 'value', 'ct/kWh'); await ensure(`${BASE_DP}.forecast.cheapestDate`, 'string', '', true, 'text'); await ensure(`${BASE_DP}.forecast.cheapestDateReadable`, 'string', '', true, 'text'); await ensure(`${BASE_DP}.forecast.mostExpensiveDate`, 'string', '', true, 'text'); await ensure(`${BASE_DP}.forecast.mostExpensiveDateReadable`, 'string', '', true, 'text'); await ensure(`${BASE_DP}.forecast.nextFreeDate`, 'string', '', true, 'text'); await ensure(`${BASE_DP}.forecast.nextFreeDateReadable`, 'string', '', true, 'text'); await ensure(`${BASE_DP}.forecast.hasFreeHour`, 'boolean', false, true, 'indicator'); } async function ensure(id, type, def, write, role, unit = '') { const exists = await existsStateAsync(id); if (!exists) { await createStateAsync(id, def, { type, role, read: true, write, unit }); } } function round(value) { return Math.round(value * 1000) / 1000; } function escapeTag(value) { return String(value) .replace(/ /g, '\\ ') .replace(/,/g, '\\,') .replace(/=/g, '\\='); }
  • [Vorlage] Anwesenheitssimulation mit dauerhaftem Lernen

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

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

    55
    2
    4 Stimmen
    55 Beiträge
    9k 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
    240 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
    192 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
    196 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
    364 Aufrufe
    G
    @paul53 Vielen DANK, hat geklappt :)

564

Online

32.9k

Benutzer

83.1k

Themen

1.3m

Beiträge