import { CONDITION_LABELS } from "../describe"; import { fetchJson, fromUnix, maybe, MAX_DAYS, MAX_HOURS, nextHours, round1 } from "../http"; import type { Condition, WeatherProvider } from "../types"; /** * Open-Meteo — free, no key, and the deepest data: hourly and daily out to 16 days. * Docs: https://open-meteo.com/en/docs * * We ask for `timeformat=unixtime` so every time comes back as a plain number. */ const HOURLY_VARS = [ "temperature_2m", "apparent_temperature", "precipitation_probability", "precipitation", "weather_code", "wind_speed_10m", "wind_gusts_10m", "wind_direction_10m", "relative_humidity_2m", "uv_index", "is_day", ] as const; const DAILY_VARS = [ "weather_code", "temperature_2m_max", "temperature_2m_min", "precipitation_probability_max", "precipitation_sum", "wind_speed_10m_max", "wind_gusts_10m_max", "wind_direction_10m_dominant", "uv_index_max", "sunrise", "sunset", ] as const; const CURRENT_VARS = [ "temperature_2m", "apparent_temperature", "relative_humidity_2m", "is_day", "precipitation", "weather_code", "cloud_cover", "pressure_msl", "wind_speed_10m", "wind_direction_10m", "wind_gusts_10m", "dew_point_2m", "visibility", "uv_index", ] as const; // Each section is "variable name → value" (current) or "variable name → array of values". type Row = Record; type Columns = Record & { time: number[] }; interface OpenMeteoResponse { timezone: string; current: Row & { time: number }; hourly: Columns; daily: Columns; } /** WMO weather codes, listed at the bottom of the Open-Meteo docs page. */ function toCondition(code: number | null): Condition { if (code === null || code <= 1) return "clear"; if (code <= 3) return "clouds"; if (code === 45 || code === 48) return "fog"; if ((code >= 71 && code <= 77) || code === 85 || code === 86) return "snow"; if (code >= 95) return "storm"; return "rain"; // drizzle (51–57), rain (61–67), showers (80–82) } const TWELVE_HOURS = 12 * 60 * 60; export const openMeteo: WeatherProvider = { id: "openmeteo", label: "Open-Meteo", envKey: null, limits: () => ({ forecastDays: 14, hourlyStepHours: 1 }), async getForecast(lat, lon) { const params = new URLSearchParams({ latitude: String(lat), longitude: String(lon), current: CURRENT_VARS.join(","), hourly: HOURLY_VARS.join(","), daily: DAILY_VARS.join(","), timezone: "auto", timeformat: "unixtime", forecast_days: String(MAX_DAYS), }); const { current: c, hourly: h, daily: d, timezone } = await fetchJson( `https://api.open-meteo.com/v1/forecast?${params}`, "Open-Meteo", ); const condition = toCondition(c.weather_code); const hourly = h.time.map((t, i) => ({ time: fromUnix(t), tempC: round1(h.temperature_2m[i] ?? 0), feelsLikeC: maybe(h.apparent_temperature[i]), condition: toCondition(h.weather_code[i]), isDay: h.is_day[i] === 1, precipChance: maybe(h.precipitation_probability[i]), precipMm: maybe(h.precipitation[i]), windKph: round1(h.wind_speed_10m[i] ?? 0), gustKph: maybe(h.wind_gusts_10m[i]), windDeg: maybe(h.wind_direction_10m[i]), humidity: maybe(h.relative_humidity_2m[i]), uvIndex: maybe(h.uv_index[i]), })); return { timeZone: timezone, hourlyStepHours: 1, alerts: [], // Open-Meteo has no alerts; the API route adds NWS alerts for US locations current: { time: fromUnix(c.time), tempC: round1(c.temperature_2m), feelsLikeC: round1(c.apparent_temperature), condition, description: CONDITION_LABELS[condition], isDay: c.is_day === 1, humidity: c.relative_humidity_2m, windKph: round1(c.wind_speed_10m), gustKph: maybe(c.wind_gusts_10m), windDeg: maybe(c.wind_direction_10m), pressureHpa: maybe(c.pressure_msl), dewPointC: maybe(c.dew_point_2m), visibilityKm: c.visibility == null ? null : round1(c.visibility / 1000), uvIndex: maybe(c.uv_index), cloudCover: maybe(c.cloud_cover), precipMm: maybe(c.precipitation), }, hourly: nextHours(hourly, MAX_HOURS), daily: d.time.map((t, i) => ({ time: fromUnix(t + TWELVE_HOURS), // daily times are local midnight; use midday highC: round1(d.temperature_2m_max[i] ?? 0), lowC: round1(d.temperature_2m_min[i] ?? 0), condition: toCondition(d.weather_code[i]), summary: null, precipChance: maybe(d.precipitation_probability_max[i]), precipMm: maybe(d.precipitation_sum[i]), windKph: maybe(d.wind_speed_10m_max[i]), gustKph: maybe(d.wind_gusts_10m_max[i]), windDeg: maybe(d.wind_direction_10m_dominant[i]), uvIndexMax: maybe(d.uv_index_max[i]), humidity: null, sunrise: d.sunrise[i] ? fromUnix(d.sunrise[i]!) : null, sunset: d.sunset[i] ? fromUnix(d.sunset[i]!) : null, })), }; }, };