OpenMeteo fixes
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user