OpenMeteo fixes
This commit is contained in:
@@ -2,6 +2,8 @@ import { existsSync, mkdirSync } from 'fs'
|
|||||||
import { resolve, dirname } from 'path'
|
import { resolve, dirname } from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { tzOffsetMinutes } from './config.mjs'
|
import { tzOffsetMinutes } from './config.mjs'
|
||||||
|
import { getWeatherCondition } from './forecast.mjs'
|
||||||
|
import { getCelestialForecast } from './celestial.mjs'
|
||||||
|
|
||||||
const DIR = dirname(fileURLToPath(import.meta.url))
|
const DIR = dirname(fileURLToPath(import.meta.url))
|
||||||
const ICON_DIR = resolve(DIR, 'public', 'icons')
|
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)
|
const data = await cachedFetch(`daily_${lat}_${lon}_${startStr}_${endStr}`, url)
|
||||||
if (!data?.daily?.time) return []
|
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 code = data.daily.weathercode[i]
|
||||||
const icon = OWM_ICONS[code] || '01d'
|
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 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 utcMidnight = new Date(`${time}T00:00:00Z`)
|
const offset = tzOffsetMinutes(utcMidnight)
|
||||||
const offset = tzOffsetMinutes(utcMidnight)
|
|
||||||
const localMidnight = new Date(utcMidnight.getTime() - offset * 60_000)
|
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',
|
label: WMO[code] || 'Unknown',
|
||||||
icon,
|
icon,
|
||||||
code,
|
|
||||||
time: localMidnight,
|
|
||||||
temperature: temp,
|
temperature: temp,
|
||||||
temperature_max: temp,
|
temperature_max: tempMax,
|
||||||
temperature_min: data.daily.temperature_2m_min[i],
|
temperature_min: tempMin,
|
||||||
humidity,
|
humidity,
|
||||||
humidity_abs: absoluteHumidity(temp, humidity),
|
humidity_abs: absoluteHumidity(temp, humidity),
|
||||||
dew_point: dewPoint(temp, humidity),
|
dew_point: dewPoint(temp, humidity),
|
||||||
heat_index: heatIndex(temp, humidity),
|
heat_index: heatIndex(temp, humidity),
|
||||||
vapor_pressure_deficit: vaporPressureDeficit(temp, humidity),
|
vapor_pressure_deficit: vaporPressureDeficit(temp, humidity),
|
||||||
precipitation: data.daily.precipitation_sum[i],
|
pressure_hpa: null,
|
||||||
precipitation_chance: data.daily.precipitation_probability_max[i],
|
pressure_slp: null,
|
||||||
wind_speed: data.daily.windspeed_10m_max[i],
|
pressure_rate: null,
|
||||||
wind_gusts: data.daily.windgusts_10m_max[i],
|
storm_trend: 0,
|
||||||
wind_direction: data.daily.winddirection_10m_dominant[i],
|
wind_speed: windSpeed,
|
||||||
uv_index: uv,
|
wind_gusts: windGusts,
|
||||||
solar_wm2: data.daily.shortwave_radiation_sum[i],
|
wind_direction: data.daily.winddirection_10m_dominant[i] ?? 0,
|
||||||
clouds: null,
|
clouds,
|
||||||
sunrise: data.daily.sunrise[i] ? new Date(data.daily.sunrise[i]) : null,
|
raining: precipChance > 60 ? 'True' : 'False',
|
||||||
sunset: data.daily.sunset[i] ? new Date(data.daily.sunset[i]) : null,
|
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 ────────────────────────────────────────────────────────────────────
|
// ── Hourly ────────────────────────────────────────────────────────────────────
|
||||||
@@ -146,8 +183,6 @@ export async function hourlyWeather(lat, lon, start, end) {
|
|||||||
const data = await cachedFetch(`hourly_${lat}_${lon}_${startStr}_${endStr}`, url)
|
const data = await cachedFetch(`hourly_${lat}_${lon}_${startStr}_${endStr}`, url)
|
||||||
if (!data?.hourly?.time) return []
|
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 = {}
|
const sunMap = {}
|
||||||
if (data.daily?.time) {
|
if (data.daily?.time) {
|
||||||
data.daily.time.forEach((date, i) => {
|
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 dateKey = time.slice(0, 10)
|
||||||
const sun = sunMap[dateKey] ?? {}
|
const sun = sunMap[dateKey] ?? {}
|
||||||
const t = new Date(time + 'Z').getTime()
|
const t = new Date(time + 'Z').getTime()
|
||||||
const daytime = sun.sunrise && sun.sunset
|
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
|
: false
|
||||||
|
|
||||||
const code = data.hourly.weathercode[i]
|
const code = data.hourly.weathercode[i]
|
||||||
const icon = await ensureIcon(code).catch(() => null)
|
|
||||||
const temp = data.hourly.temperature_2m[i]
|
const temp = data.hourly.temperature_2m[i]
|
||||||
const humidity = data.hourly.relative_humidity_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 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'),
|
time: new Date(time + 'Z'),
|
||||||
daytime,
|
daytime,
|
||||||
label: WMO[code] || 'Unknown',
|
label: WMO[code] || 'Unknown',
|
||||||
icon,
|
icon: OWM_ICONS[code] || '01d',
|
||||||
code,
|
|
||||||
temperature: temp,
|
temperature: temp,
|
||||||
|
temperature_max: temp,
|
||||||
|
temperature_min: temp,
|
||||||
humidity,
|
humidity,
|
||||||
humidity_abs: absoluteHumidity(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),
|
heat_index: heatIndex(temp, humidity),
|
||||||
vapor_pressure_deficit: data.hourly.vapour_pressure_deficit[i] ?? vaporPressureDeficit(temp, humidity),
|
vapor_pressure_deficit: data.hourly.vapour_pressure_deficit[i] ?? vaporPressureDeficit(temp, humidity),
|
||||||
pressure_hpa: data.hourly.surface_pressure[i],
|
pressure_hpa: data.hourly.surface_pressure[i] ?? null,
|
||||||
precipitation: data.hourly.precipitation[i],
|
pressure_slp: null,
|
||||||
precipitation_chance: data.hourly.precipitation_probability[i],
|
pressure_rate: null,
|
||||||
wind_speed: data.hourly.windspeed_10m[i],
|
storm_trend: 0,
|
||||||
wind_gusts: data.hourly.windgusts_10m[i],
|
wind_speed: windSpeed,
|
||||||
wind_direction: data.hourly.winddirection_10m[i],
|
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,
|
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
|
visibility: data.hourly.visibility[i] != null
|
||||||
? Math.round(data.hourly.visibility[i] / 1000 * 10) / 10
|
? 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 coords = await getCoords()
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
now.setMinutes(0, 0, 0)
|
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 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([
|
const [history, forecast24, meteo] = await Promise.all([
|
||||||
start.getTime() < now.getTime() ? queryHourly(start, now) : Promise.resolve([]),
|
start < now
|
||||||
end.getTime() > now.getTime() ? getForecast().then(f => f.slots.filter(h => new Date(h.time) <= end)) : Promise.resolve([]),
|
? queryHourly(start, now)
|
||||||
end.getTime() > forecastLimit.getTime() ? hourlyWeather(coords.latitude, coords.longitude, forecastLimit, end) : Promise.resolve([]),
|
: 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])
|
const combined = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...forecast24, ...meteo])
|
||||||
res.json(filterArr(hourly, fields))
|
res.json(filterArr(combined, fields))
|
||||||
}));
|
}))
|
||||||
|
|
||||||
// ── Daily ─────────────────────────────────────────────────────────────────────
|
// ── Daily ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
app.get('/api/daily', asyncHandler(async (req, res) => {
|
app.get('/api/daily', asyncHandler(async (req, res) => {
|
||||||
const { fields } = req.query
|
const { fields } = req.query
|
||||||
const coords = await getCoords();
|
const coords = await getCoords()
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const todayStart = new Date(now)
|
const todayStart = new Date(now)
|
||||||
todayStart.setHours(0, 0, 0, 0)
|
todayStart.setHours(0, 0, 0, 0)
|
||||||
const todayEnd = new Date(todayStart)
|
const todayEnd = new Date(todayStart)
|
||||||
todayEnd.setDate(todayStart.getDate() + 1)
|
todayEnd.setDate(todayStart.getDate() + 1)
|
||||||
const start = req.query.start ? new Date(req.query.start) : todayEnd
|
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 end = req.query.end ? new Date(req.query.end) : new Date(todayEnd.getTime() + 5 * 24 * 60 * 60_000)
|
||||||
|
|
||||||
const [history, current, future] = Promise.all([
|
const [history, current, future] = await Promise.all([
|
||||||
start.getTime() < todayStart.getTime() ? queryDaily(start, todayStart) : Promise.resolve([]), // Historic
|
start < todayStart
|
||||||
start.getTime() <= todayStart.getTime() ? queryCurrent().then(resp => [resp]) : Promise.resolve([]), // Current
|
? queryDaily(start, todayStart)
|
||||||
start.getTime() >= todayEnd.getTime() ? dailyWeather(coords.latitude, coords.longitude, todayStart, end) : Promise.resolve([]), // Future
|
: Promise.resolve([]),
|
||||||
]);
|
start <= todayStart && end >= todayStart
|
||||||
const daily = [...history, ...current, ...getCelestialForecast(coords.latitude, coords.longitude, future)];
|
? 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))
|
res.json(filterArr(daily, fields))
|
||||||
}));
|
}))
|
||||||
|
|
||||||
// ── Position ──────────────────────────────────────────────────────────────────
|
// ── Position ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user