Hallo zusammen,
da wetter.com die alten API-Versionen abschaltet, habe ich ein neues Skript für die API v4.0 (Meteonomiqs) erstellt. (und weil es keinen Adapter dafür gibt)
Das Skript ist darauf ausgelegt, "Plug & Play" zu funktionieren, die Daten sauber in 0_userdata.0 zu strukturieren und dabei die Limits des Free-Accounts (100 Abrufe/Monat) im Blick zu behalten.
Features
API v4.0 Support: Nutzt die aktuellen Endpunkte von Meteonomiqs/wetter.com.
Automatische Struktur: Legt alle benötigten Ordner und Datenpunkte unter 0_userdata.0.wetter_com selbständig an.
Standort-Automatik: Liest Breiten- und Längengrad direkt aus den ioBroker-Systemeinstellungen.
Fallback/Override: Manuelle Koordinaten können im Skript hinterlegt werden (z.B. für Ferienhäuser).
Limit-Wächter: Überwacht die Anzahl der API-Abrufe (täglich und monatlich).
Bei Fehler 429 (Limit erreicht) wird eine Warnung ausgegeben und der Abruf gestoppt, statt das Log vollzuschreiben.
Server-Schutz (Random Cron): Um die API nicht zu überlasten, wenn viele User das Skript nutzen, werden die Abrufzeiten bei jedem Skriptstart zufällig innerhalb sinnvoller Zeitfenster (morgens 00:02–05:00 Uhr und nachmittags 13:02–17:00 Uhr) generiert.
Cleanup: Wenn man die Anzahl der Vorhersage-Tage reduziert, werden überflüssige Datenpunkte automatisch gelöscht.
Datenpunkte:
Datum
Min/Max Temperatur
Wetterzustand (Text & Code für Icons)
Regenwahrscheinlichkeit & Menge
Sonnenstunden
Wind (Geschwindigkeit & Richtung)
Voraussetzungen
API Key: Ihr benötigt einen kostenlosen API-Key von Meteonomiqs/wetter.com. (Free-Paket wählen).
https://www.meteonomiqs.com/de/wetter-api/#heading_PricePackages/
Koordinaten: Sollten in den ioBroker Haupteinstellungen (System -> Einstellungen) hinterlegt sein.
(Kann im Skript, im Bereich "STANDORT KONFIGURATION", angepasst/geändert werden)
Installation & Konfiguration
Neues JS-Skript im ioBroker anlegen.
Code hineinkopieren.
Im Bereich --- KONFIGURATION --- euren API_KEY eintragen.
Optional: FORECAST_DAYS anpassen (Standard: 7 Tage).
info-Datenpunkt
Unter 0_userdata.0.wetter_com.info findet ihr Datenpunkte wie requests_month oder next_schedules, die ihr in der VIS anzeigen könnt, um euren Verbrauch zu überwachen.
Spoiler
/**
* ioBroker Script: Wetter.com Forecast API v4.0
* API: https://doc.meteonomiqs.com/doc/forecast_v4_0.html
* * Changelog:
* 1.4.6: Feature: Manuelle Standort-Konfiguration (Fallback & Override) hinzugefügt.
* 1.4.5: FIX: Syntax-Fehler im Header behoben.
* 1.4.4: Anpassung an Monatslimit (100 Requests).
* 1.4.3: Korrektur "last_sync" Formatierung.
* 1.4.1: Randomisierung der Abrufzeiten.
* 1.4.0: Info-Datenpunkte hinzugefügt.
* * 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 DP_PATH = '0_userdata.0.wetter_com';
const LANGUAGE = 'de-de';
const FORECAST_DAYS = 7;
// --- STANDORT KONFIGURATION ---
// Hier Koordinaten eintragen für Fallback oder spezifischen Standort (z.B. '52.520')
const MANUAL_LATITUDE = '';
const MANUAL_LONGITUDE = '';
// true = Nutze IMMER die manuellen Koordinaten (Ignoriert ioBroker-Einstellungen)
// false = Nutze manuelle Koordinaten nur als Fallback, falls im System keine hinterlegt sind
const FORCE_MANUAL_LOCATION = false;
// --- RANDOMISIERUNG DER ZEITEN ---
/**
* Erzeugt eine zufällige Cron-Zeit innerhalb eines Fensters
* @param {number} startHour
* @param {number} endHour
* @param {number} minMinute (optional, z.B. 2 für 00:02)
*/
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} * * *`;
}
// Zeitfenster 1: 00:02 bis 05:00
const cron1 = getRandomCron(0, 5, 2);
// Zeitfenster 2: 13:02 bis 17:00
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 deutsches Format: "DD.MM.YYYY"
*/
function formatToGermanDate(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 die Koordinaten basierend auf Konfiguration und System-Einstellungen
*/
async function getCoordinates() {
// 1. Override: Manuelle Koordinaten erzwingen
if (FORCE_MANUAL_LOCATION && MANUAL_LATITUDE && MANUAL_LONGITUDE) {
console.log(`[Wetter.com] Nutze manuell konfigurierte Koordinaten (Override).`);
return { lat: parseFloat(MANUAL_LATITUDE).toFixed(3), lon: parseFloat(MANUAL_LONGITUDE).toFixed(3) };
}
// 2. System: Versuche ioBroker Einstellungen zu lesen
const systemCoords = await new Promise((resolve) => {
getObject('system.config', (err, obj) => {
if (!err && obj && obj.common && obj.common.latitude && obj.common.longitude) {
resolve({
lat: parseFloat(obj.common.latitude).toFixed(3),
lon: parseFloat(obj.common.longitude).toFixed(3)
});
} else {
resolve(null);
}
});
});
if (systemCoords) return systemCoords;
// 3. Fallback: Nutze manuelle Koordinaten, falls System fehlschlug
if (MANUAL_LATITUDE && MANUAL_LONGITUDE) {
console.log('[Wetter.com] Warnung: Keine System-Koordinaten gefunden. Nutze Fallback-Koordinaten aus Skript.');
return { lat: parseFloat(MANUAL_LATITUDE).toFixed(3), lon: parseFloat(MANUAL_LONGITUDE).toFixed(3) };
}
return null;
}
/**
* Erstellt die Struktur inkl. Info-Datenpunkten
*/
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: '' },
'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 },
'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)) {
const fullId = `${path}.${id}`;
await createStateAsync(fullId, 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);
}
});
}
/**
* Aktualisiert die Info-Datenpunkte (Zähler und Zeitstempel)
*/
async function updateUsageInfo() {
const now = new Date();
// Manuelle Formatierung
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);
// Tageszähler erhöhen
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);
// Monatszähler erhöhen
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 coords = await getCoordinates();
if (!coords) {
console.error('[Wetter.com] Fehler: Keine Koordinaten gefunden (Weder System noch Fallback)!');
return;
}
const url = `${BASE_URL}/forecast/${coords.lat}/${coords.lon}/summary`;
const options = { headers: { 'x-api-key': API_KEY, 'Accept-Language': LANGUAGE } };
try {
const response = await performRequest(url, options);
const data = JSON.parse(response.data);
if (data && data.items) {
await processForecastData(data.items);
await cleanupObsoleteDays();
await updateUsageInfo();
}
} catch (err) {
if (err.message === 'LIMIT_REACHED') {
console.error('[Wetter.com] Fehler 429: Das monatliche Abruflimit (100 Anfragen) wurde erreicht.');
} else {
console.error(`[Wetter.com] Fehler: ${err.message}`);
}
}
} catch (e) {
console.error(`[Wetter.com] Script-Fehler: ${e.message}`);
}
}
async function processForecastData(items) {
await ensureStructure('', 0, true);
const daysToProcess = Math.min(items.length, FORECAST_DAYS);
for (let i = 0; i < daysToProcess; i++) {
const day = items[i];
const dayPath = `${DP_PATH}.day_${i}`;
await ensureStructure(dayPath, i);
await setStateAsync(`${dayPath}.date`, formatToGermanDate(day.date), 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`, day.weather?.state ?? 0, 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.`);
}
// Zähler jeden Tag um Mitternacht zurücksetzen
schedule("0 0 * * *", () => {
setState(`${DP_PATH}.info.requests_today`, 0, true);
});
// Zähler jeden Monat (am 1. um 00:00) zurücksetzen
schedule("0 0 1 * *", () => {
setState(`${DP_PATH}.info.requests_month`, 0, true);
});
schedule(cron1, fetchWeatherData);
schedule(cron2, fetchWeatherData);
ensureStructure('', 0, true).then(() => {
fetchWeatherData();
});