Try every configured Met Office key per tier, don't assume which covers what

Forecast key -> hourly was tried and confirmed wrong (settings test route
reported 'not subscribed'). New fetchWithKeys() tries Spot then Forecast
for EVERY tier (hourly, three-hourly, daily) and keeps whichever
succeeds, rather than assigning a specific key to a specific endpoint
based on an unverified guess about Met Office's product catalog.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-25 13:43:36 +00:00
parent 4b535a34f4
commit 15a801b1f4

View file

@ -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') {