/**
* [Wetter.com Forecast API v4.0 (TrueScript)]
* * CHANGELOG:
* - 2.6.1: 2026-02-25 - FIX: Konfigurations-Datenpunkte beschreibbar gemacht.
* - API-Key und Forecast_Days erhalten nun 'write: true', damit sie im ioBroker-Objektbaum editiert werden können.
* - Retroaktiver Fix (extendObjectAsync) integriert, um bereits gesperrte Datenpunkte automatisch zu entsperren.
* - 2.6.0: ULTRA-PERFORMANCE & EDGE-CASE FIXES (RAM-Cache, wcomWait entfernt).
* - 2.5.0: HIGH-END OPTIMIERUNGEN & DEADLOCK-SCHUTZ (Promise.race, Batching, isNaN-Handling).
* * KONTEXT:
* - Hardware: ioBroker Server | Schnittstellen: Meteonomiqs API v4.0 (HTTP)
* * BEKANNTE PROBLEME / TÜCKEN:
* - Strikte timeouts bei HTTP-Calls erforderlich, um die Single-Thread-Natur der JS-Engine nicht mit hängenden Sockets zu belasten.
* * 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',
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';
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;
// @KI_HINWEIS: RAM-Cache für angelegte States/Objekte. Verhindert hunderte CPU-Zyklen für existsObject() in Folgeläufen.
const ensuredPaths = new Set<string>();
// --- HILFSFUNKTIONEN ---
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);
}
}
/**
* @KI_HINWEIS: Typsichere Extraktion. Fängt null/undefined ab und loggt fehlerhaftes (NaN) API-Verhalten.
*/
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;
}
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()}`;
}
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<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;
}
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);
}
/**
* @KI_HINWEIS: Parameter `writeable` hinzugefügt, um Config-States (wie API-Key) von Read-Only-Wetterdaten zu differenzieren.
*/
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);
}
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);
}
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 ---
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}). Abruf gestoppt.`, 'warn');
return false;
}
if (source === 'start') {
const todayState = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_today`);
if (todayState && todayState.val !== null && Number(todayState.val) > 0) {
wcomLog(`Skript-Neustart erkannt. Abruf übersprungen, da heute bereits Daten geladen wurden.`, 'debug');
return false;
}
}
const now = new Date();
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
const daysLeft = daysInMonth - now.getDate();
if (source === 'afternoon' || (source === 'start' && now.getHours() >= 12)) {
const callsNeededFor2xDaily = (daysLeft * 2) + 1;
if (currentUsage + callsNeededFor2xDaily > CONFIG.MONTHLY_LIMIT) {
wcomLog(`SPARMODUS AKTIV: Nachmittags-Abruf übersprungen (Budget-Schutz). Verbrauch: ${currentUsage}`, 'warn');
return false;
}
}
if (source === 'morning' || (source === 'start' && now.getHours() < 12)) {
const callsNeededFor1xDaily = daysLeft + 1;
if (currentUsage + callsNeededFor1xDaily > CONFIG.MONTHLY_LIMIT) {
if (now.getDate() % 2 !== 0) {
wcomLog(`NOTLAUF AKTIV: Morgen-Abruf übersprungen (Budget extrem niedrig). Verbrauch: ${currentUsage}`, 'warn');
return false;
}
}
}
return true;
}
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);
}
}
}
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.requests_month`, 0, 'number', 'Anfragen Monat', 'value');
await wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_today`, 0, 'number', 'Anfragen heute', 'value');
// @KI_HINWEIS: Letzter Parameter (writeable) auf true gesetzt für Config-Objekte
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);
// @KI_HINWEIS: Retroaktiver Fix. Entsperrt Datenpunkte, die in v2.6.0 fälschlicherweise mit write: false angelegt wurden.
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;
}
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);
await wcomUpdateUsageInfo();
}
} catch (e: any) {
wcomLog(`Script-Fehler: ${e.message}`, 'error');
} finally {
isFetching = false;
}
}
async function wcomProcessForecastData(data: WetterComResponse, lang: string, forecastDays: number): Promise<void> {
await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
await wcomEnsureState(`${CONFIG.DP_PATH}.info.last_sync`, '', 'string', 'Letztes Update', 'text');
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 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 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 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}.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}.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');
}
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);
}
}
}
async function wcomUpdateUsageInfo(): Promise<void> {
const now: Date = new Date();
const timestamp: string = `${String(now.getDate()).padStart(2,'0')}.${String(now.getMonth()+1).padStart(2,'0')}.${now.getFullYear()} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
await setStateAsync(`${CONFIG.DP_PATH}.info.last_sync`, String(timestamp), true);
const countToday = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_today`);
await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, (countToday && countToday.val !== null ? Number(countToday.val) : 0) + 1, true);
const countMonth = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_month`);
await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, (countMonth && countMonth.val !== null ? Number(countMonth.val) : 0) + 1, true);
}
// --- ZEITSTEUERUNG & TRIGGER ---
function wcomGetRandomCron(startHour: number, endHour: number, minMinute: number = 0): string {
const hour: number = Math.floor(Math.random() * (endHour - startHour + 1)) + startHour;
let minute: number = (hour === startHour) ? Math.floor(Math.random() * (60 - minMinute)) + minMinute : (hour === endHour ? 0 : Math.floor(Math.random() * 60));
return `${minute} ${hour} * * *`;
}
schedule("0 0 1 * *", async () => {
const path = `${CONFIG.DP_PATH}.info.requests_month`;
if (existsState(path)) {
await setStateAsync(path, 0, true);
wcomLog('Monatszähler zurückgesetzt.', 'info');
}
});
schedule(wcomGetRandomCron(0, 5, 2), () => wcomFetchWeatherData('morning'));
schedule(wcomGetRandomCron(13, 17, 2), () => wcomFetchWeatherData('afternoon'));
on({ id: CONFIG.DP_API_KEY, change: 'ne' }, (obj) => {
if (obj.state && typeof obj.state.val === 'string' && obj.state.val.trim().length >= 10) {
wcomLog('Änderung des API-Keys erkannt. Starte sofortigen Test-Abruf...', 'info');
wcomFetchWeatherData('key_update');
}
});
on({ id: CONFIG.DP_FORECAST_DAYS, change: 'ne' }, (obj) => {
if (obj.state && obj.state.val !== null) {
wcomLog(`Änderung der Vorhersage-Tage auf ${obj.state.val} erkannt. Starte Aktualisierung & Bereinigung...`, 'info');
wcomFetchWeatherData('days_update');
}
});
// Initialer Aufruf beim Skriptstart
wcomFetchWeatherData('start');