/**
* ioBroker Script: Wetter.com Forecast API v4.0 (TrueScript)
* API: https://doc.meteonomiqs.com/doc/forecast_v4_0.html
* * * Changelog:
* 1.8.4: FIX: Compiler-Fehler & Syntax-Bereinigung.
* - Konfiguration in 'CONFIG'-Objekt gekapselt, um Scope-Fehler ("Cannot find name") zu beheben.
* - Bereinigung von Template-Strings (entfernte redundante String()-Wrapper).
* - Typsichere Implementierung von getObject-Callbacks.
* 1.8.3: FIX: Namenskonflikte behoben (wcom-Präfix).
*/
// --- KONFIGURATION ---
const CONFIG = {
API_KEY: 'DEIN_API_KEY_HIER', // <-- BITTE HIER DEINEN API-KEY EINTRAGEN
BASE_URL: 'https://forecast.meteonomiqs.com/v4_0',
ICON_BASE_URL: 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather',
DP_PATH: '0_userdata.0.wetter_com',
DEFAULT_LANGUAGE: 'de',
FORECAST_DAYS: 7, // max. 16 Tage
ENABLE_HOURLY: true,
ENABLE_SPACES: true,
LOCATION: {
LAT: '', // Leer lassen für System-Einstellung
LON: '', // Leer lassen für System-Einstellung
FORCE_MANUAL: false
}
};
// --- INTERFACES ---
interface WetterComValue {
avg?: number;
value?: number;
sum?: number;
max?: number;
min?: number;
}
interface WetterComWeather {
state: number;
text: string;
icon?: string;
}
interface WetterComWind {
avg?: number | WetterComValue;
gusts?: number | WetterComValue | { value: number | null };
direction?: string;
unit?: string;
}
interface WetterComPrec {
probability: number;
sum: number | WetterComValue;
}
interface ForecastSummary {
date: string;
weather: WetterComWeather;
temperature: { min: number | WetterComValue; max: number | WetterComValue; avg?: number | WetterComValue };
wind: WetterComWind;
prec: WetterComPrec;
clouds: number | WetterComValue;
relativeHumidity: number | WetterComValue;
}
interface ForecastSpaceSegment {
temperature: number | WetterComValue;
weather: WetterComWeather;
prec: WetterComPrec;
wind: WetterComWind;
clouds: number | WetterComValue;
relativeHumidity: number | WetterComValue;
}
interface ForecastSpace {
morning?: ForecastSpaceSegment;
afternoon?: ForecastSpaceSegment;
evening?: ForecastSpaceSegment;
night?: ForecastSpaceSegment;
}
interface ForecastHourly {
from: string;
date: string;
weather: WetterComWeather;
temperature: number | WetterComValue;
windchill: number | WetterComValue;
wind: WetterComWind;
prec: WetterComPrec;
relativeHumidity: number | WetterComValue;
}
interface WetterComResponse {
summary: ForecastSummary[];
spaces: ForecastSpace[];
hourly: ForecastHourly[];
}
// --- HILFSFUNKTIONEN ---
const wcomWait = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
function wcomExtractValue(val: any): number {
if (val === null || val === undefined) return 0;
if (typeof val === 'number') return val;
if (typeof val === 'object') {
if (val.avg !== undefined) return val.avg;
if (val.value !== undefined) return val.value;
if (val.sum !== undefined) return val.sum;
if (val.max !== undefined) return val.max;
if (val.min !== undefined) return val.min;
if (val.gusts && typeof val.gusts === 'object' && val.gusts.value !== undefined) return val.gusts.value;
}
return parseFloat(String(val)) || 0;
}
function wcomFormatDate(dateStr: string): string {
if (!dateStr) return '';
const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr;
return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`;
}
function wcomGetDayName(dateStr: string, locale: string): string {
if (!dateStr) return '';
const date = new Date(dateStr);
if (isNaN(date.getTime())) return '';
return date.toLocaleDateString(locale, { weekday: 'long' });
}
async function wcomGetSystemSettings(): Promise<{ lat: string; lon: string; lang: string } | null> {
let coords: { lat: string; lon: string } | null = null;
if (CONFIG.LOCATION.FORCE_MANUAL && CONFIG.LOCATION.LAT && CONFIG.LOCATION.LON) {
coords = { lat: parseFloat(CONFIG.LOCATION.LAT).toFixed(3), lon: parseFloat(CONFIG.LOCATION.LON).toFixed(3) };
}
const systemConf: any = await new Promise((resolve) => {
getObject('system.config', (err, obj: any) => {
if (!err && obj && obj.common) {
resolve({
lat: obj.common.latitude ? parseFloat(String(obj.common.latitude)).toFixed(3) : null,
lon: obj.common.longitude ? parseFloat(String(obj.common.longitude)).toFixed(3) : null,
lang: obj.common.language || CONFIG.DEFAULT_LANGUAGE
});
} else { resolve({ lat: null, lon: null, lang: CONFIG.DEFAULT_LANGUAGE }); }
});
});
if (!coords && systemConf.lat && systemConf.lon) coords = { lat: systemConf.lat, lon: systemConf.lon };
return coords ? { ...coords, lang: systemConf.lang } : null;
}
async function wcomEnsureSubStructure(path: string, name: string, type: 'device' | 'channel' = 'channel'): Promise<void> {
if (!path) return;
if (!existsObject(path)) {
await extendObjectAsync(path, {
type: type,
common: { name: name },
native: {}
});
await wcomWait(50);
}
}
async function wcomEnsureDayStates(path: string, index: number): Promise<void> {
const states: Record<string, { name: string; type: iobJS.CommonType; role: string; unit?: string; init: any }> = {
'date': { name: 'Datum', type: 'string', role: 'text', init: '' },
'day_name': { name: 'Wochentag', type: 'string', role: 'text', init: '' },
'temp_max': { name: 'Max Temp', type: 'number', unit: '°C', role: 'value.temperature.max', init: 0 },
'temp_min': { name: 'Min Temp', type: 'number', unit: '°C', role: 'value.temperature.min', init: 0 },
'weather_text': { name: 'Wetter', type: 'string', role: 'weather.state', init: '' },
'weather_icon': { name: 'Icon URL', type: 'string', role: 'weather.icon', init: '' },
'prec_probability': { name: 'Regenrisiko', type: 'number', unit: '%', role: 'value.precipitation.probability', init: 0 },
'prec_sum': { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation', init: 0 },
'wind_gusts': { name: 'Windböen', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust', init: 0 },
'clouds': { name: 'Bewölkung', type: 'number', unit: '%', role: 'value', init: 0 },
'humidity': { name: 'Relative Feuchte', type: 'number', unit: '%', role: 'value.humidity', init: 0 }
};
for (const [id, cfg] of Object.entries(states)) {
await createStateAsync(`${path}.${id}`, cfg.init, false, {
name: `Tag ${index}: ${cfg.name}`,
type: cfg.type,
role: cfg.role,
unit: cfg.unit || '',
read: true,
write: false
});
}
}
// --- LOGIK ---
async function wcomFetchWeatherData(): Promise<void> {
try {
const settings = await wcomGetSystemSettings();
if (!settings) return;
console.log(`[Wetter.com] Abruf für Lat ${settings.lat}, Lon ${settings.lon}`);
const url: string = `${CONFIG.BASE_URL}/forecast/${settings.lat}/${settings.lon}`;
const options = { headers: { 'x-api-key': CONFIG.API_KEY, 'Accept-Language': settings.lang } };
httpGet(url, options, async (err, response) => {
if (response && response.statusCode === 429) {
console.error('[Wetter.com] Das Limit von 100 API-Calls im Monat ist ausgeschöpft.');
return;
}
if (err || (response && response.statusCode !== 200)) {
console.error(`[Wetter.com] API-Fehler: ${err || (response ? response.statusCode : 'Unbekannter Status')}`);
return;
}
let data: WetterComResponse;
try {
data = JSON.parse(response.data);
} catch (e) { return; }
if (data && data.summary) {
await wcomProcessForecastData(data, settings.lang);
await wcomCleanupObsoleteDays();
await wcomUpdateUsageInfo();
}
});
} catch (e: any) { console.error(`[Wetter.com] Script-Fehler: ${e.message}`); }
}
async function wcomProcessForecastData(data: WetterComResponse, lang: string): Promise<void> {
await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
await wcomEnsureSubStructure(`${CONFIG.DP_PATH}.info`, 'Informationen');
await createStateAsync(`${CONFIG.DP_PATH}.info.last_sync`, '', false, { name: 'Letztes Update', type: 'string', role: 'text' } as any);
await createStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, 0, false, { name: 'Anfragen heute', type: 'number', role: 'value' } as any);
const maxDays: number = Math.min(data.summary.length, CONFIG.FORECAST_DAYS);
for (let i = 0; i < maxDays; i++) {
const day: ForecastSummary = data.summary[i];
const dayPath: string = `${CONFIG.DP_PATH}.day_${i}`;
await wcomEnsureSubStructure(dayPath, `Tag ${i}`);
await wcomEnsureDayStates(dayPath, i);
await setStateAsync(`${dayPath}.date`, String(wcomFormatDate(day.date)), true);
await setStateAsync(`${dayPath}.day_name`, String(wcomGetDayName(day.date, lang)), true);
await setStateAsync(`${dayPath}.temp_max`, wcomExtractValue(day.temperature?.max), true);
await setStateAsync(`${dayPath}.temp_min`, wcomExtractValue(day.temperature?.min), true);
await setStateAsync(`${dayPath}.weather_text`, String(day.weather?.text || ''), true);
await setStateAsync(`${dayPath}.weather_icon`, `${CONFIG.ICON_BASE_URL}/d_${day.weather?.state}.svg`, true);
await setStateAsync(`${dayPath}.prec_probability`, wcomExtractValue(day.prec?.probability), true);
await setStateAsync(`${dayPath}.prec_sum`, wcomExtractValue(day.prec?.sum), true);
await setStateAsync(`${dayPath}.wind_gusts`, wcomExtractValue(day.wind?.gusts), true);
await setStateAsync(`${dayPath}.clouds`, wcomExtractValue(day.clouds), true);
await setStateAsync(`${dayPath}.humidity`, wcomExtractValue(day.relativeHumidity), true);
if (CONFIG.ENABLE_SPACES && data.spaces && data.spaces[i]) {
const spacesPath: string = `${dayPath}.spaces`;
await wcomEnsureSubStructure(spacesPath, 'Tagesabschnitte');
const segments: (keyof ForecastSpace)[] = ['morning', 'afternoon', 'evening', 'night'];
for (const seg of segments) {
const sData = data.spaces[i][seg];
if (!sData) continue;
const sPath: string = `${spacesPath}.${seg}`;
await wcomEnsureSubStructure(sPath, seg);
await createStateAsync(`${sPath}.temp`, 0, false, { name: 'Temperatur', type: 'number', unit: '°C', role: 'value.temperature' } as any);
await setStateChangedAsync(`${sPath}.temp`, wcomExtractValue(sData.temperature), true);
await createStateAsync(`${sPath}.text`, '', false, { name: 'Wetter', type: 'string', role: 'weather.state' } as any);
await setStateChangedAsync(`${sPath}.text`, String(sData.weather?.text || ''), true);
await createStateAsync(`${sPath}.prec_prob`, 0, false, { name: 'Regenrisiko', type: 'number', unit: '%', role: 'value.precipitation.probability' } as any);
await setStateChangedAsync(`${sPath}.prec_prob`, wcomExtractValue(sData.prec?.probability), true);
await createStateAsync(`${sPath}.prec_sum`, 0, false, { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation' } as any);
await setStateChangedAsync(`${sPath}.prec_sum`, wcomExtractValue(sData.prec?.sum), true);
await createStateAsync(`${sPath}.wind_speed`, 0, false, { name: 'Windgeschwindigkeit', type: 'number', unit: 'km/h', role: 'value.speed.wind' } as any);
await setStateChangedAsync(`${sPath}.wind_speed`, wcomExtractValue(sData.wind?.avg), true);
await createStateAsync(`${sPath}.wind_gusts`, 0, false, { name: 'Windböen', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust' } as any);
await setStateChangedAsync(`${sPath}.wind_gusts`, wcomExtractValue(sData.wind?.gusts), true);
await createStateAsync(`${sPath}.clouds`, 0, false, { name: 'Bewölkung', type: 'number', unit: '%', role: 'value' } as any);
await setStateChangedAsync(`${sPath}.clouds`, wcomExtractValue(sData.clouds), true);
await createStateAsync(`${sPath}.humidity`, 0, false, { name: 'Relative Feuchte', type: 'number', unit: '%', role: 'value.humidity' } as any);
await setStateChangedAsync(`${sPath}.humidity`, wcomExtractValue(sData.relativeHumidity), true);
}
}
if (CONFIG.ENABLE_HOURLY && i <= 1 && data.hourly) {
const dateIso: string = day.date.split('T')[0];
const hourlyPath: string = `${dayPath}.hourly`;
await wcomEnsureSubStructure(hourlyPath, 'Stündlich');
const dayHours = data.hourly.filter((h: ForecastHourly) => (h.from || h.date).startsWith(dateIso));
for (const h of dayHours) {
const hourDate: Date = new Date(h.from || h.date);
const hourLabel: string = String(hourDate.getHours()).padStart(2, '0');
const hPath: string = `${hourlyPath}.${hourLabel}`;
await wcomEnsureSubStructure(hPath, `${hourLabel}:00 Uhr`);
await createStateAsync(`${hPath}.time`, '', false, { name: 'Uhrzeit', type: 'string', role: 'text' } as any);
await setStateChangedAsync(`${hPath}.time`, `${hourLabel}:00`, true);
await createStateAsync(`${hPath}.temp`, 0, false, { name: 'Temperatur', type: 'number', unit: '°C', role: 'value.temperature' } as any);
await setStateChangedAsync(`${hPath}.temp`, wcomExtractValue(h.temperature), true);
await createStateAsync(`${hPath}.windchill`, 0, false, { name: 'Gefühlt', type: 'number', unit: '°C', role: 'value.temperature' } as any);
await setStateChangedAsync(`${hPath}.windchill`, wcomExtractValue(h.windchill), true);
await createStateAsync(`${hPath}.weather_text`, '', false, { name: 'Wetter', type: 'string', role: 'weather.state' } as any);
await setStateChangedAsync(`${hPath}.weather_text`, String(h.weather?.text || ''), true);
await createStateAsync(`${hPath}.weather_icon`, '', false, { name: 'Wetter Icon', type: 'string', role: 'weather.icon' } as any);
await setStateChangedAsync(`${hPath}.weather_icon`, `${CONFIG.ICON_BASE_URL}/d_${h.weather?.state}.svg`, true);
await createStateAsync(`${hPath}.prec_prob`, 0, false, { name: 'Regenwahrscheinlichkeit', type: 'number', unit: '%', role: 'value.precipitation.probability' } as any);
await setStateChangedAsync(`${hPath}.prec_prob`, wcomExtractValue(h.prec?.probability), true);
await createStateAsync(`${hPath}.prec_sum`, 0, false, { name: 'Regenmenge', type: 'number', unit: 'mm', role: 'value.precipitation' } as any);
await setStateChangedAsync(`${hPath}.prec_sum`, wcomExtractValue(h.prec?.sum), true);
await createStateAsync(`${hPath}.wind_speed`, 0, false, { name: 'Windgeschwindigkeit', type: 'number', unit: 'km/h', role: 'value.speed.wind' } as any);
await setStateChangedAsync(`${hPath}.wind_speed`, wcomExtractValue(h.wind?.avg), true);
await createStateAsync(`${hPath}.wind_dir`, '', false, { name: 'Windrichtung', type: 'string', role: 'weather.direction' } as any);
await setStateChangedAsync(`${hPath}.wind_dir`, String(h.wind?.direction || ''), true);
await createStateAsync(`${hPath}.wind_gusts`, 0, false, { name: 'Windböen', type: 'number', unit: 'km/h', role: 'value.speed.wind.gust' } as any);
await setStateChangedAsync(`${hPath}.wind_gusts`, wcomExtractValue(h.wind?.gusts), true);
await createStateAsync(`${hPath}.humidity`, 0, false, { name: 'Relative Feuchte', type: 'number', unit: '%', role: 'value.humidity' } as any);
await setStateChangedAsync(`${hPath}.humidity`, wcomExtractValue(h.relativeHumidity), true);
}
}
await wcomWait(20);
}
}
async function wcomCleanupObsoleteDays(): Promise<void> {
for (let i = CONFIG.FORECAST_DAYS; i <= 25; i++) {
const path: string = `${CONFIG.DP_PATH}.day_${i}`;
if (existsObject(path)) {
await deleteObjectAsync(path, true);
await wcomWait(150);
}
}
}
async function wcomUpdateUsageInfo(): Promise<void> {
const now: Date = new Date();
const timestamp: string = `${String(now.getDate()).padStart(2,'0')}.${String(now.getMonth()+1).padStart(2,'0')}.${now.getFullYear()} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
await setStateAsync(`${CONFIG.DP_PATH}.info.last_sync`, String(timestamp), true);
const countState: iobJS.State | null = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_today`);
await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, (countState ? (Number(countState.val) || 0) : 0) + 1, true);
}
// --- ZEITSTEUERUNG ---
function wcomGetRandomCron(startHour: number, endHour: number, minMinute: number = 0): string {
const hour: number = Math.floor(Math.random() * (endHour - startHour + 1)) + startHour;
let minute: number = (hour === startHour) ? Math.floor(Math.random() * (60 - minMinute)) + minMinute : (hour === endHour ? 0 : Math.floor(Math.random() * 60));
return `${minute} ${hour} * * *`;
}
schedule("0 0 * * *", () => setState(`${CONFIG.DP_PATH}.info.requests_today`, 0, true));
schedule(wcomGetRandomCron(0, 5, 2), wcomFetchWeatherData);
schedule(wcomGetRandomCron(13, 17, 2), wcomFetchWeatherData);
wcomFetchWeatherData();