From 210649d7ad415bc98c6a1929815fd716ec149e29 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Tue, 25 Aug 2026 13:35:17 +0000 Subject: [PATCH] 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 --- src/integrations/schema.js | 8 +++++++- src/routes/integrations.js | 20 +++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/integrations/schema.js b/src/integrations/schema.js index 4b9079a..2c5958e 100644 --- a/src/integrations/schema.js +++ b/src/integrations/schema.js @@ -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'], }, } diff --git a/src/routes/integrations.js b/src/routes/integrations.js index 76ac858..94251d9 100644 --- a/src/routes/integrations.js +++ b/src/routes/integrations.js @@ -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 = []