/**
* ioBroker Script: Wetter.com Forecast API v4.0
* API: https://doc.meteonomiqs.com/doc/forecast_v4_0.html
* * Changelog:
* 1.4.11: FIX: Fallback-Werte für Konfigurations-Variablen (ICON_BASE_URL, DEFAULT_LANGUAGE) integriert.
* Verhindert Abstürze ("ReferenceError"), wenn beim Update der Konfig-Bereich nicht aktualisiert wurde.
* 1.4.10: Feature: Dynamische Spracheinstellung.
* 1.4.8: Icon-URL Anpassung.
* * free API-Key anfordern: https://www.meteonomiqs.com/de/wetter-api/#heading_PricePackages/
*/
// --- KONFIGURATION ---
const API_KEY = 'DEIN_API_KEY_HIER'; // <-- BITTE HIER DEINEN API-KEY EINTRAGEN
const BASE_URL = 'https://forecast.meteonomiqs.com/v4_0';
const ICON_BASE_URL = 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather';
const DP_PATH = '0_userdata.0.wetter_com';
const DEFAULT_LANGUAGE = 'de'; // Fallback, falls System-Sprache nicht ermittelbar
const FORECAST_DAYS = 7;
// --- STANDORT KONFIGURATION ---
const MANUAL_LATITUDE = '';
const MANUAL_LONGITUDE = '';
const FORCE_MANUAL_LOCATION = false;
// --- RANDOMISIERUNG DER ZEITEN ---
function getRandomCron(startHour, endHour, minMinute = 0) {
const hour = Math.floor(Math.random() * (endHour - startHour + 1)) + startHour;
let minute;
if (hour === startHour) {
minute = Math.floor(Math.random() * (60 - minMinute)) + minMinute;
} else if (hour === endHour) {
minute = 0;
} else {
minute = Math.floor(Math.random() * 60);
}
return `${minute} ${hour} * * *`;
}
const cron1 = getRandomCron(0, 5, 2);
const cron2 = getRandomCron(13, 17, 2);
console.log(`[Wetter.com] Schedules für heute gesetzt auf: "${cron1}" und "${cron2}"`);
// --- HILFSFUNKTIONEN ---
/**
* Formatiert ein Datum manuell in das Format: "DD.MM.YYYY"
*/
function formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr;
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
}
/**
* Ermittelt den Wochentag basierend auf der übergebenen Sprache (Locale)
*/
function getDayName(dateStr, locale) {
if (!dateStr) return '';
const date = new Date(dateStr);
if (isNaN(date.getTime())) return '';
return date.toLocaleDateString(locale, { weekday: 'long' });
}
/**
* Ermittelt Einstellungen (Coords + Sprache) aus ioBroker System-Config
*/
async function getSystemSettings() {
// Safety check für Variablen, falls User Config nicht aktualisiert hat
const manualLat = (typeof MANUAL_LATITUDE !== 'undefined') ? MANUAL_LATITUDE : '';
const manualLon = (typeof MANUAL_LONGITUDE !== 'undefined') ? MANUAL_LONGITUDE : '';
const forceManual = (typeof FORCE_MANUAL_LOCATION !== 'undefined') ? FORCE_MANUAL_LOCATION : false;
const defLang = (typeof DEFAULT_LANGUAGE !== 'undefined') ? DEFAULT_LANGUAGE : 'de';
// 1. Manuelle Location?
let coords = null;
if (forceManual && manualLat && manualLon) {
console.log(`[Wetter.com] Nutze manuell konfigurierte Koordinaten (Override).`);
coords = { lat: parseFloat(manualLat).toFixed(3), lon: parseFloat(manualLon).toFixed(3) };
}
// 2. System Config lesen
const systemConf = await new Promise((resolve) => {
getObject('system.config', (err, obj) => {
if (!err && obj && obj.common) {
resolve({
lat: obj.common.latitude ? parseFloat(obj.common.latitude).toFixed(3) : null,
lon: obj.common.longitude ? parseFloat(obj.common.longitude).toFixed(3) : null,
lang: obj.common.language || defLang
});
} else {
resolve({ lat: null, lon: null, lang: defLang });
}
});
});
// 3. Fallback Logik
if (!coords) {
if (systemConf.lat && systemConf.lon) {
coords = { lat: systemConf.lat, lon: systemConf.lon };
} else if (manualLat && manualLon) {
console.log('[Wetter.com] Warnung: Keine System-Koordinaten. Nutze Fallback.');
coords = { lat: parseFloat(manualLat).toFixed(3), lon: parseFloat(manualLon).toFixed(3) };
}
}
if (!coords) return null;
return { ...coords, lang: systemConf.lang };
}
async function ensureStructure(path, index, isInfo = false) {
if (isInfo) {
await createStateAsync(`${DP_PATH}.info.last_sync`, '', false, { name: 'Letztes erfolgreiches Update', type: 'string', role: 'text' });
await createStateAsync(`${DP_PATH}.info.requests_today`, 0, false, { name: 'Anfragen heute', type: 'number', role: 'value' });
await createStateAsync(`${DP_PATH}.info.requests_month`, 0, false, { name: 'Anfragen aktueller Monat', type: 'number', role: 'value' });
await createStateAsync(`${DP_PATH}.info.next_schedules`, '', false, { name: 'Geplante Abrufe', type: 'string', role: 'text' });
return;
}
const states = {
'date': { name: 'Datum', type: 'string', role: 'text', def: '' },
'day_name': { name: 'Wochentag', type: 'string', role: 'text', def: '' },
'temp_max': { name: 'Max Temperatur', type: 'number', unit: '°C', role: 'value.temperature.max', def: 0 },
'temp_min': { name: 'Min Temperatur', type: 'number', unit: '°C', role: 'value.temperature.min', def: 0 },
'weather_text': { name: 'Wetterzustand', type: 'string', role: 'weather.state', def: '' },
'weather_code': { name: 'Wetter Code', type: 'number', role: 'value', def: 0 },
'weather_icon': { name: 'Wetter Icon URL', type: 'string', role: 'weather.icon', def: '' },
'prec_probability': { name: 'Regenrisiko', type: 'number', unit: '%', role: 'value.precipitation.probability', def: 0 },
'prec_sum': { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation', def: 0 },
'sun_hours': { name: 'Sonnenstunden', type: 'number', unit: 'h', role: 'value.sunshine', def: 0 },
'wind_speed_max': { name: 'Windböen Max', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust', def: 0 },
'wind_direction': { name: 'Windrichtung', type: 'string', role: 'weather.direction', def: '' }
};
for (const [id, config] of Object.entries(states)) {
await createStateAsync(`${path}.${id}`, config.def, false, {
name: `Tag ${index}: ${config.name}`,
type: config.type,
role: config.role,
unit: config.unit || '',
read: true,
write: false
});
}
}
async function performRequest(url, options) {
return new Promise((resolve, reject) => {
httpGet(url, options, (error, response) => {
if (error) return reject(new Error(error));
if (response.statusCode === 429) return reject(new Error('LIMIT_REACHED'));
if (response.statusCode !== 200) return reject(new Error(`HTTP Status ${response.statusCode}`));
resolve(response);
});
});
}
async function cleanupObsoleteDays() {
const channels = $(`${DP_PATH}.day_*`);
channels.each(function(id) {
const parts = id.split('.');
const lastPart = parts[parts.length - 1];
const dayIndex = parseInt(lastPart.replace('day_', ''));
if (!isNaN(dayIndex) && dayIndex >= FORECAST_DAYS) {
deleteObject(id, true);
}
});
}
async function updateUsageInfo() {
const now = new Date();
const day = String(now.getDate()).padStart(2, '0');
const month = String(now.getMonth() + 1).padStart(2, '0');
const year = now.getFullYear();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const timestamp = `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`;
await setStateAsync(`${DP_PATH}.info.last_sync`, String(timestamp), true);
const currentCountState = await getStateAsync(`${DP_PATH}.info.requests_today`);
const currentCount = currentCountState ? (currentCountState.val || 0) : 0;
await setStateAsync(`${DP_PATH}.info.requests_today`, currentCount + 1, true);
const currentMonthState = await getStateAsync(`${DP_PATH}.info.requests_month`);
const currentMonthCount = currentMonthState ? (currentMonthState.val || 0) : 0;
await setStateAsync(`${DP_PATH}.info.requests_month`, currentMonthCount + 1, true);
const s1 = cron1.split(' ');
const s2 = cron2.split(' ');
await setStateAsync(`${DP_PATH}.info.next_schedules`, `${s1[1].padStart(2,'0')}:${s1[0].padStart(2,'0')} Uhr & ${s2[1].padStart(2,'0')}:${s2[0].padStart(2,'0')} Uhr`, true);
}
// --- LOGIK ---
async function fetchWeatherData() {
try {
const settings = await getSystemSettings();
if (!settings) {
console.error('[Wetter.com] Fehler: Keine Koordinaten gefunden!');
return;
}
console.log(`[Wetter.com] Starte Abruf für Lat ${settings.lat}, Lon ${settings.lon} (Sprache: ${settings.lang})`);
const url = `${BASE_URL}/forecast/${settings.lat}/${settings.lon}/summary`;
const options = { headers: { 'x-api-key': API_KEY, 'Accept-Language': settings.lang } };
try {
const response = await performRequest(url, options);
const data = JSON.parse(response.data);
if (data && data.items) {
await processForecastData(data.items, settings.lang);
await cleanupObsoleteDays();
await updateUsageInfo();
}
} catch (err) {
if (err.message === 'LIMIT_REACHED') {
console.error('[Wetter.com] Fehler 429: Monatslimit erreicht.');
} else {
console.error(`[Wetter.com] Fehler: ${err.message}`);
}
}
} catch (e) {
console.error(`[Wetter.com] Script-Fehler: ${e.message}`);
}
}
async function processForecastData(items, lang) {
await ensureStructure('', 0, true);
const daysToProcess = Math.min(items.length, FORECAST_DAYS);
// Fallback URL, falls Variable durch Teil-Update fehlt
const iconBaseUrl = (typeof ICON_BASE_URL !== 'undefined')
? ICON_BASE_URL
: 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather';
for (let i = 0; i < daysToProcess; i++) {
const day = items[i];
const dayPath = `${DP_PATH}.day_${i}`;
await ensureStructure(dayPath, i);
const germanDate = formatDate(day.date);
const dayName = getDayName(day.date, lang);
const weatherCode = day.weather?.state ?? 0;
const iconUrl = `${iconBaseUrl}/d_${weatherCode}.svg`;
await setStateAsync(`${dayPath}.date`, germanDate, true);
await setStateAsync(`${dayPath}.day_name`, dayName, true);
await setStateAsync(`${dayPath}.temp_max`, day.temperature?.max ?? 0, true);
await setStateAsync(`${dayPath}.temp_min`, day.temperature?.min ?? 0, true);
await setStateAsync(`${dayPath}.weather_text`, day.weather?.text || '', true);
await setStateAsync(`${dayPath}.weather_code`, weatherCode, true);
await setStateAsync(`${dayPath}.weather_icon`, iconUrl, true);
await setStateAsync(`${dayPath}.prec_probability`, day.prec?.probability ?? 0, true);
await setStateAsync(`${dayPath}.prec_sum`, day.prec?.sum ?? 0, true);
await setStateAsync(`${dayPath}.sun_hours`, day.sunHours ?? 0, true);
await setStateAsync(`${dayPath}.wind_speed_max`, day.wind?.max ?? 0, true);
await setStateAsync(`${dayPath}.wind_direction`, day.wind?.direction || '', true);
}
console.log(`[Wetter.com] Update von ${daysToProcess} Tagen abgeschlossen.`);
}
schedule("0 0 * * *", () => {
setState(`${DP_PATH}.info.requests_today`, 0, true);
});
schedule("0 0 1 * *", () => {
setState(`${DP_PATH}.info.requests_month`, 0, true);
});
schedule(cron1, fetchWeatherData);
schedule(cron2, fetchWeatherData);
ensureStructure('', 0, true).then(() => {
fetchWeatherData();
});