From 96a86c4d12f5dbaac76ced0f1c921d5b5eeb7559 Mon Sep 17 00:00:00 2001 From: ztimson Date: Sat, 27 Jun 2026 17:52:32 -0400 Subject: [PATCH] OpenMeteo fixes --- server/src/openmeteo.mjs | 152 ++++++++++++++++++++++++++++----------- server/src/server.mjs | 53 ++++++++------ 2 files changed, 144 insertions(+), 61 deletions(-) diff --git a/server/src/openmeteo.mjs b/server/src/openmeteo.mjs index a3e53cb..5109d18 100644 --- a/server/src/openmeteo.mjs +++ b/server/src/openmeteo.mjs @@ -2,6 +2,8 @@ import { existsSync, mkdirSync } from 'fs' import { resolve, dirname } from 'path' import { fileURLToPath } from 'url' import { tzOffsetMinutes } from './config.mjs' +import { getWeatherCondition } from './forecast.mjs' +import { getCelestialForecast } from './celestial.mjs' const DIR = dirname(fileURLToPath(import.meta.url)) const ICON_DIR = resolve(DIR, 'public', 'icons') @@ -87,43 +89,78 @@ export async function dailyWeather(lat, lon, start, end) { const data = await cachedFetch(`daily_${lat}_${lon}_${startStr}_${endStr}`, url) if (!data?.daily?.time) return [] - return Promise.all(data.daily.time.map(async (time, i) => { + const rows = data.daily.time.map((time, i) => { const code = data.daily.weathercode[i] const icon = OWM_ICONS[code] || '01d' - const temp = data.daily.temperature_2m_max[i] + const tempMax = data.daily.temperature_2m_max[i] + const tempMin = data.daily.temperature_2m_min[i] + const temp = tempMax const humidity = data.daily.relative_humidity_2m_mean[i] - const uv = data.daily.uv_index_max[i] + const precip = data.daily.precipitation_sum[i] ?? 0 + const precipChance = data.daily.precipitation_probability_max[i] ?? 0 + const windSpeed = data.daily.windspeed_10m_max[i] ?? 0 + const windGusts = data.daily.windgusts_10m_max[i] ?? 0 - // "2026-03-01" → local midnight by shifting from UTC midnight by tz offset - const utcMidnight = new Date(`${time}T00:00:00Z`) - const offset = tzOffsetMinutes(utcMidnight) + const utcMidnight = new Date(`${time}T00:00:00Z`) + const offset = tzOffsetMinutes(utcMidnight) const localMidnight = new Date(utcMidnight.getTime() - offset * 60_000) - return { + const sunrise = data.daily.sunrise[i] ? new Date(data.daily.sunrise[i]) : null + const sunset = data.daily.sunset[i] ? new Date(data.daily.sunset[i]) : null + const daylight = sunrise && sunset + ? Math.round((sunset - sunrise) / 3600_000 * 100) / 100 + : null + + const clouds = humidity / 100 * 0.8 // rough estimate — not provided by daily endpoint + + const row = { + time: localMidnight, + daytime: true, label: WMO[code] || 'Unknown', icon, - code, - time: localMidnight, temperature: temp, - temperature_max: temp, - temperature_min: data.daily.temperature_2m_min[i], + temperature_max: tempMax, + temperature_min: tempMin, humidity, humidity_abs: absoluteHumidity(temp, humidity), dew_point: dewPoint(temp, humidity), heat_index: heatIndex(temp, humidity), vapor_pressure_deficit: vaporPressureDeficit(temp, humidity), - precipitation: data.daily.precipitation_sum[i], - precipitation_chance: data.daily.precipitation_probability_max[i], - wind_speed: data.daily.windspeed_10m_max[i], - wind_gusts: data.daily.windgusts_10m_max[i], - wind_direction: data.daily.winddirection_10m_dominant[i], - uv_index: uv, - solar_wm2: data.daily.shortwave_radiation_sum[i], - clouds: null, - sunrise: data.daily.sunrise[i] ? new Date(data.daily.sunrise[i]) : null, - sunset: data.daily.sunset[i] ? new Date(data.daily.sunset[i]) : null, + pressure_hpa: null, + pressure_slp: null, + pressure_rate: null, + storm_trend: 0, + wind_speed: windSpeed, + wind_gusts: windGusts, + wind_direction: data.daily.winddirection_10m_dominant[i] ?? 0, + clouds, + raining: precipChance > 60 ? 'True' : 'False', + precipitation: precip, + precipitation_chance: precipChance, + accumulation: precip, + uv_index: data.daily.uv_index_max[i] ?? 0, + uv_index_max: data.daily.uv_index_max[i] ?? 0, + uv_dose: 0, + solar_wm2: data.daily.shortwave_radiation_sum[i] ?? 0, + daily_light_integral: data.daily.shortwave_radiation_sum[i] + ? Math.round(data.daily.shortwave_radiation_sum[i] * 3600 / 1_000_000 * 4.57 * 100) / 100 + : 0, + lux: 0, + visibility: 50, + sunrise, + sunset, + daylight, + lightning_rate: 0, + lightning_distance: null, + seismic_magnitude: null, + air_quality: null, + ground_distance: null, } - })) + + return { ...row, ...getWeatherCondition(row) } + }) + + return rows } // ── Hourly ──────────────────────────────────────────────────────────────────── @@ -146,8 +183,6 @@ export async function hourlyWeather(lat, lon, start, end) { const data = await cachedFetch(`hourly_${lat}_${lon}_${startStr}_${endStr}`, url) if (!data?.hourly?.time) return [] - // Open-Meteo returns local time strings e.g. "2026-03-01T06:00" (no Z) - // sunMap keyed by date portion of those local strings const sunMap = {} if (data.daily?.time) { data.daily.time.forEach((date, i) => { @@ -158,44 +193,79 @@ export async function hourlyWeather(lat, lon, start, end) { }) } - return Promise.all(data.hourly.time.map(async (time, i) => { + const rows = data.hourly.time.map((time, i) => { const dateKey = time.slice(0, 10) const sun = sunMap[dateKey] ?? {} const t = new Date(time + 'Z').getTime() const daytime = sun.sunrise && sun.sunset - ? t >= new Date(sun.sunrise.toISOString().replace('Z', '') + 'Z').getTime() && t < new Date(sun.sunset.toISOString().replace('Z', '') + 'Z').getTime() + ? t >= sun.sunrise.getTime() && t < sun.sunset.getTime() : false const code = data.hourly.weathercode[i] - const icon = await ensureIcon(code).catch(() => null) const temp = data.hourly.temperature_2m[i] const humidity = data.hourly.relative_humidity_2m[i] - const uv = data.hourly.uv_index[i] const clouds = Math.round(data.hourly.cloudcover[i]) / 100 + const precip = data.hourly.precipitation[i] ?? 0 + const precipChance = data.hourly.precipitation_probability[i] ?? 0 + const windSpeed = data.hourly.windspeed_10m[i] ?? 0 + const uv = data.hourly.uv_index[i] ?? 0 - return { + const sunrise = sun.sunrise ?? null + const sunset = sun.sunset ?? null + const daylight = sunrise && sunset + ? Math.round((sunset - sunrise) / 3600_000 * 100) / 100 + : null + + const row = { time: new Date(time + 'Z'), daytime, label: WMO[code] || 'Unknown', - icon, - code, + icon: OWM_ICONS[code] || '01d', temperature: temp, + temperature_max: temp, + temperature_min: temp, humidity, humidity_abs: absoluteHumidity(temp, humidity), - dew_point: dewPoint(temp, humidity), + dew_point: data.hourly.dewpoint_2m[i] ?? dewPoint(temp, humidity), heat_index: heatIndex(temp, humidity), vapor_pressure_deficit: data.hourly.vapour_pressure_deficit[i] ?? vaporPressureDeficit(temp, humidity), - pressure_hpa: data.hourly.surface_pressure[i], - precipitation: data.hourly.precipitation[i], - precipitation_chance: data.hourly.precipitation_probability[i], - wind_speed: data.hourly.windspeed_10m[i], - wind_gusts: data.hourly.windgusts_10m[i], - wind_direction: data.hourly.winddirection_10m[i], + pressure_hpa: data.hourly.surface_pressure[i] ?? null, + pressure_slp: null, + pressure_rate: null, + storm_trend: 0, + wind_speed: windSpeed, + wind_gusts: data.hourly.windgusts_10m[i] ?? 0, + wind_direction: data.hourly.winddirection_10m[i] ?? 0, + clouds, + raining: precipChance > 60 ? 'True' : 'False', + precipitation: precip, + precipitation_chance: precipChance, + accumulation: precip, uv_index: uv, - solar_wm2: data.hourly.shortwave_radiation[i], + uv_index_max: uv, + uv_dose: 0, + solar_wm2: data.hourly.shortwave_radiation[i] ?? 0, + daily_light_integral: data.hourly.shortwave_radiation[i] + ? Math.round(data.hourly.shortwave_radiation[i] * 3600 / 1_000_000 * 4.57 * 100) / 100 + : 0, + lux: data.hourly.shortwave_radiation[i] + ? Math.round(data.hourly.shortwave_radiation[i] * 120) + : 0, visibility: data.hourly.visibility[i] != null ? Math.round(data.hourly.visibility[i] / 1000 * 10) / 10 - : null, + : 50, + sunrise, + sunset, + daylight, + lightning_rate: 0, + lightning_distance: null, + seismic_magnitude: null, + air_quality: null, + ground_distance: null, } - })) + + return { ...row, ...getWeatherCondition(row) } + }) + + return rows } diff --git a/server/src/server.mjs b/server/src/server.mjs index e1fec7d..b1895a2 100644 --- a/server/src/server.mjs +++ b/server/src/server.mjs @@ -78,41 +78,54 @@ app.get('/api/hourly', asyncHandler(async (req, res) => { const coords = await getCoords() const now = new Date() now.setMinutes(0, 0, 0) - const forecastLimit = new Date(now.getTime() + 24 * 60 * 60_000) + const forecast24Limit = new Date(now.getTime() + 24 * 60 * 60_000) const start = req.query.start ? new Date(req.query.start) : now - const end = req.query.end ? new Date(req.query.end) : forecastLimit + const end = req.query.end ? new Date(req.query.end) : forecast24Limit - const [history, forecast24, forecast3rdParty] = await Promise.all([ - start.getTime() < now.getTime() ? queryHourly(start, now) : Promise.resolve([]), - end.getTime() > now.getTime() ? getForecast().then(f => f.slots.filter(h => new Date(h.time) <= end)) : Promise.resolve([]), - end.getTime() > forecastLimit.getTime() ? hourlyWeather(coords.latitude, coords.longitude, forecastLimit, end) : Promise.resolve([]), + const [history, forecast24, meteo] = await Promise.all([ + start < now + ? queryHourly(start, now) + : Promise.resolve([]), + now < forecast24Limit && end > now + ? getForecast().then(f => f.slots.filter(h => new Date(h.time) >= now && new Date(h.time) <= Math.min(end, forecast24Limit))) + : Promise.resolve([]), + end > forecast24Limit + ? hourlyWeather(coords.latitude, coords.longitude, forecast24Limit, end) + : Promise.resolve([]), ]) - const hourly = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...forecast24, ...forecast3rdParty]) - res.json(filterArr(hourly, fields)) -})); + const combined = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...forecast24, ...meteo]) + res.json(filterArr(combined, fields)) +})) // ── Daily ───────────────────────────────────────────────────────────────────── app.get('/api/daily', asyncHandler(async (req, res) => { const { fields } = req.query - const coords = await getCoords(); - const now = new Date() + const coords = await getCoords() + const now = new Date() const todayStart = new Date(now) todayStart.setHours(0, 0, 0, 0) const todayEnd = new Date(todayStart) todayEnd.setDate(todayStart.getDate() + 1) - const start = req.query.start ? new Date(req.query.start) : todayEnd - const end = req.query.end ? new Date(req.query.end) : new Date(todayEnd.getTime() + 5 * 24 * 60 * 60_000) + const start = req.query.start ? new Date(req.query.start) : todayStart + const end = req.query.end ? new Date(req.query.end) : new Date(todayEnd.getTime() + 5 * 24 * 60 * 60_000) - const [history, current, future] = Promise.all([ - start.getTime() < todayStart.getTime() ? queryDaily(start, todayStart) : Promise.resolve([]), // Historic - start.getTime() <= todayStart.getTime() ? queryCurrent().then(resp => [resp]) : Promise.resolve([]), // Current - start.getTime() >= todayEnd.getTime() ? dailyWeather(coords.latitude, coords.longitude, todayStart, end) : Promise.resolve([]), // Future - ]); - const daily = [...history, ...current, ...getCelestialForecast(coords.latitude, coords.longitude, future)]; + const [history, current, future] = await Promise.all([ + start < todayStart + ? queryDaily(start, todayStart) + : Promise.resolve([]), + start <= todayStart && end >= todayStart + ? queryCurrent().then(resp => [resp]) + : Promise.resolve([]), + end > todayEnd + ? dailyWeather(coords.latitude, coords.longitude, todayEnd, end) + : Promise.resolve([]), + ]) + + const daily = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...current, ...future]) res.json(filterArr(daily, fields)) -})); +})) // ── Position ──────────────────────────────────────────────────────────────────