diff --git a/backend/src/lib/weather-fetch.js b/backend/src/lib/weather-fetch.js index 156682e..f6ecd56 100644 --- a/backend/src/lib/weather-fetch.js +++ b/backend/src/lib/weather-fetch.js @@ -10,13 +10,15 @@ // other endpoints could actually serve. This matters because Met Office DataHub // sells "Global Spot" and "Forecast" as SEPARATE subscriptions with separate keys // (settings' met_office integration: api_key = Spot, forecast_api_key = Forecast, -// optional) — only Spot (used here for daily) has ever been confirmed live (hvac, -// 2026-08-25); hourly/three-hourly try forecast_api_key first (falling back to -// api_key if no separate Forecast key is configured), since Spot alone may not -// actually be subscribed to those endpoints. Field names for hourly/three-hourly -// are taken from the published Met Office DataHub docs, still not verified live — -// a mismatch there just throws and that tier falls back to Open-Meteo, same as an -// outright subscription failure. +// optional) — only Spot's daily endpoint has ever been confirmed live (hvac, +// 2026-08-25). Which key actually covers hourly/three-hourly is NOT documented +// anywhere we could find, and a first guess (Forecast key -> hourly) was tried and +// confirmed wrong via settings' met_office test route — so every tier just tries +// whichever keys are configured (Spot then Forecast, see fetchWithKeys) and keeps +// whichever works, discovered at runtime rather than assumed. Field names for +// hourly/three-hourly are taken from the published Met Office DataHub docs, still +// not verified live — a mismatch there just throws and that tier falls back to +// Open-Meteo, same as an outright subscription failure. import { pool, setStatus } from '../db.js' import { iconFromMetOffice, iconFromOpenMeteo } from './icons.js' @@ -217,6 +219,25 @@ function openMeteoAstroPoints(dailyRows, astroKeys) { .map(r => ({ date: r.dateKey, sunrise: r.sunrise, sunset: r.sunset })) } +// Tries each configured key in turn against the same tier, returning the first +// success. Which of Spot/Forecast actually covers which endpoint isn't documented +// anywhere we could find, and a first guess (Forecast -> hourly specifically) was +// tried and confirmed wrong live via settings' met_office test route — so rather +// than assuming again, every tier just tries whatever keys are configured and +// keeps whichever one actually works, discovered at runtime instead of guessed at. +async function fetchWithKeys(fetchFn, keys) { + const errors = [] + for (const [name, key] of keys) { + if (!key) continue + try { + return await fetchFn(key) + } catch (err) { + errors.push(`${name}: ${err.message}`) + } + } + throw new Error(errors.join('; ') || 'no Met Office key configured') +} + // ── Storage ────────────────────────────────────────────────────────────── async function replaceResolution(resolution, points) { const client = await pool.connect() @@ -293,13 +314,15 @@ export async function refreshForecast(log = console) { let hourlyPoints = [], threeHourlyPoints = [], dailyPoints = [], astroPoints = [] if (metOffice?.api_key) { - const spotKey = metOffice.api_key - const forecastKey = metOffice.forecast_api_key || metOffice.api_key + // Spot first (the one confirmed working, for daily) then Forecast, for + // every tier — see fetchWithKeys' comment for why this doesn't assume + // which key covers which endpoint. + const keys = [['Spot', metOffice.api_key], ['Forecast', metOffice.forecast_api_key]] const [hourlyR, threeHourlyR, dailyR] = await Promise.allSettled([ - fetchMetOfficeHourly(lat, lon, forecastKey), - fetchMetOfficeThreeHourly(lat, lon, forecastKey, dayKeys), - fetchMetOfficeDaily(lat, lon, spotKey, skipKeys, astroKeys), + fetchWithKeys(key => fetchMetOfficeHourly(lat, lon, key), keys), + fetchWithKeys(key => fetchMetOfficeThreeHourly(lat, lon, key, dayKeys), keys), + fetchWithKeys(key => fetchMetOfficeDaily(lat, lon, key, skipKeys, astroKeys), keys), ]) if (hourlyR.status === 'fulfilled') {