getPeakTempC() previously only tried Open-Meteo when NO Met Office api_key
was set at all — if a key was set but the call itself failed (wrong
DataHub product subscription, outage, etc.) the whole fetch went dark
with no fallback, so weather compensation silently stopped surfacing
anywhere. Root cause hit live: fetchFromMetOffice() calls the "Global
Spot" deterministic product, which is a separate DataHub subscription
from "Site-Specific Blended Probabilistic Forecast" — subscribing to the
wrong one 403s. Now any Met Office failure degrades to Open-Meteo instead
of returning nothing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Warm/hot/heatwave tiers (ascending peak_above_c thresholds) override the
global ac_occupied template when today's forecast peak clears one, picked
once at the occupied-state transition so a guest's later manual adjustment
is never stomped. New lib/weather.js fetches the peak via Met Office
DataHub (if configured in settings) or Open-Meteo (no key needed) as a
fallback, TTL-cached with a stale-fallback window on fetch failure.
Zone-level per-field overrides still apply on top of a picked tier.
Thresholds/templates live in hvac_defaults.ac_occupied_tiers (existing
JSONB config blob, no migration) and are editable on the Settings page's
new Weather Compensation table. Hotel lat/lon and the optional Met Office
API key live centrally in the settings app (hotel_location/met_office
integrations), not hvac-local, for reuse by other apps later.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>