met_office: add forecast_api_key, a second Met Office DataHub key

Met Office DataHub sells 'Global Spot' and 'Forecast' as separate
subscriptions with separate keys. api_key (existing) is Spot, used for the
daily endpoint (confirmed live via hvac). forecast_api_key is new and
optional -- weather's hourly/three-hourly endpoints try it first, falling
back to api_key, then Open-Meteo, per tier.

Test route now checks each key against the endpoint it's actually used for
(Spot -> daily, Forecast -> hourly) rather than only ever testing api_key
against daily.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-25 13:35:17 +00:00
parent 5f84a44d1f
commit 210649d7ad
2 changed files with 24 additions and 4 deletions

View file

@ -60,7 +60,13 @@ export const INTEGRATIONS = {
met_office: {
name: 'Met Office DataHub',
configFields: [],
secretFields: ['api_key'],
// Met Office DataHub sells the "Global Spot" and "Forecast" site-specific
// products as separate subscriptions, each with its own API key — api_key is
// Spot (used for the daily endpoint, confirmed live via hvac); forecast_api_key
// is optional and, if set, is what weather's hourly/three-hourly endpoints try
// first (falling back to api_key, then to Open-Meteo, per tier — see weather's
// lib/weather-fetch.js). Leave it blank if you only have a Spot subscription.
secretFields: ['api_key', 'forecast_api_key'],
},
}

View file

@ -171,12 +171,26 @@ export async function integrationRoutes(app) {
const { rows: [loc] } = await pool.query(`SELECT config FROM integrations WHERE slug = 'hotel_location'`)
const lat = loc?.config?.latitude || '51.5072'
const lon = loc?.config?.longitude || '-0.1276'
const res = await fetch(
// Two separate subscriptions, two separate keys — test each against the
// endpoint weather actually uses it for (Spot -> daily, Forecast ->
// hourly). forecast_api_key is optional, only tested if set.
const errors = []
const dailyRes = await fetch(
`https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily?latitude=${lat}&longitude=${lon}`,
{ headers: { apikey: creds.api_key }, signal: AbortSignal.timeout(8000) }
)
if (res.status === 401 || res.status === 403) throw new Error('Invalid Met Office API key')
if (!res.ok) throw new Error(`Met Office DataHub returned ${res.status}`)
if (dailyRes.status === 401 || dailyRes.status === 403) errors.push('Spot key: invalid or not subscribed to the daily endpoint')
else if (!dailyRes.ok) errors.push(`Spot key: daily endpoint returned ${dailyRes.status}`)
if (creds.forecast_api_key) {
const hourlyRes = await fetch(
`https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly?latitude=${lat}&longitude=${lon}`,
{ headers: { apikey: creds.forecast_api_key }, signal: AbortSignal.timeout(8000) }
)
if (hourlyRes.status === 401 || hourlyRes.status === 403) errors.push('Forecast key: invalid or not subscribed to the hourly endpoint')
else if (!hourlyRes.ok) errors.push(`Forecast key: hourly endpoint returned ${hourlyRes.status}`)
}
if (errors.length) throw new Error(errors.join(' | '))
}
else if (req.params.slug === 'sambapos') {
const errors = []