/** * Service for interacting with the Open-Meteo APIs * Documentation: * - Historical Weather: https://open-meteo.com/en/docs/historical-weather-api * - Forecast: https://open-meteo.com/en/docs * - Geocoding: https://open-meteo.com/en/docs/geocoding-api * - Air Quality: https://open-meteo.com/en/docs/air-quality-api * - Marine: https://open-meteo.com/en/docs/marine-weather-api */ import type { OpenMeteoHistoricalResponse, GeocodingResponse, OpenMeteoForecastResponse, OpenMeteoAirQualityResponse, OpenMeteoMarineResponse, OpenMeteoFloodResponse, OpenMeteoModelComparisonResponse, OpenMeteoEnsembleResponse, ClimateNormals } from '../types/openmeteo.js'; import { type ServiceProbeResult } from '../utils/serviceStatusProbe.js'; import { UnitPreferences } from '../config/units.js'; export interface OpenMeteoServiceConfig { baseURL?: string; geocodingURL?: string; forecastURL?: string; airQualityURL?: string; marineURL?: string; floodURL?: string; ensembleURL?: string; timeout?: number; maxRetries?: number; } export declare class OpenMeteoService { private client; private geocodingClient; private forecastClient; private airQualityClient; private marineClient; private floodClient; private ensembleClient; private maxRetries; private cache; /** * In-flight climate-normals table pulls, keyed by the table cache key (D3). * * `include_normals: true` is accepted by both `get_forecast` and * `get_current_conditions`, and a client may call the two **concurrently** * for the same coordinates — without this map both would miss the * (not-yet-populated) cache and each issue a full-year archive pull. * (`get_weather_summary` no longer reaches this path: it forwards only its * declared keys, and `include_normals` is not one.) Entries are removed once * settled, so a rejected pull is never cached and never left behind for the * next caller to join. */ private normalsTableInFlight; constructor(config?: OpenMeteoServiceConfig); /** * Handle API errors with helpful status information */ private handleError; /** * Make request with retry logic */ private makeRequest; /** * Get cache statistics */ getCacheStats(): import("../utils/cache.js").CacheStats; /** * Clear the cache */ clearCache(): void; /** * Check whether the Open-Meteo archive API answers, and how * Performs a lightweight health check by requesting a simple query. * Every HTTP status resolves (validateStatus) so the probe reads it itself; the * interceptor rewrites rejections, so the catch only records that no answer came. * @returns What the probe observed; never rejects */ checkServiceStatus(): Promise; /** * Get historical weather data for a location * * @param latitude - Latitude coordinate (-90 to 90) * @param longitude - Longitude coordinate (-180 to 180) * @param startDate - Start date in ISO format (YYYY-MM-DD) * @param endDate - End date in ISO format (YYYY-MM-DD) * @param useHourly - Whether to request hourly data (default: true) * @returns Historical weather data */ getHistoricalWeather(latitude: number, longitude: number, startDate: string, endDate: string, useHourly?: boolean, prefs?: UnitPreferences): Promise; /** * Build request parameters for historical weather data * @private */ private buildHistoricalParams; /** * Validate that the response contains the expected data * @private */ private validateResponse; /** * Get weather description from WMO weather code * WMO Weather interpretation codes (WW): https://open-meteo.com/en/docs */ getWeatherDescription(code: number): string; /** * Search for locations by name using the Open-Meteo Geocoding API * * @param query - Location name to search for (e.g., "Paris", "New York, NY", "Tokyo") * @param limit - Maximum number of results to return (default: 5, max: 100) * @param language - Language for results (default: 'en') * @returns Geocoding results with coordinates and metadata */ searchLocation(query: string, limit?: number, language?: string): Promise; /** * Get weather forecast from Open-Meteo Forecast API * * @param latitude - Latitude coordinate (-90 to 90) * @param longitude - Longitude coordinate (-180 to 180) * @param days - Number of forecast days (1-16, default: 7) * @param hourly - Whether to include hourly data (default: false) * @returns Weather forecast data */ getForecast(latitude: number, longitude: number, days?: number, hourly?: boolean, prefs?: UnitPreferences): Promise; /** * Get current weather conditions from Open-Meteo Forecast API * * @param latitude - Latitude coordinate (-90 to 90) * @param longitude - Longitude coordinate (-180 to 180) * @param prefs - Unit preferences (default: imperial) * @param includeFireWeather - When true, also request soil moisture and vapour-pressure * deficit (used to compute the Fosberg Fire Weather Index). Defaults to false so every * existing caller's request URL is unchanged. If the request 400s with these variables * attached, the fire variables are treated as best-effort garnish: the request is * retried once without them (see `fetchCurrentConditions`) rather than failing the * whole call. * @returns Forecast response populated with current conditions */ getCurrentConditions(latitude: number, longitude: number, prefs?: UnitPreferences, includeFireWeather?: boolean): Promise; /** * Issue the current-conditions request, retrying once without the fire-weather variables * if `includeFireWeather` is set and Open-Meteo rejects the request with a 400 * (`InvalidLocationError`). Fire weather is best-effort garnish (matching the ACIS/NIFC * precedent elsewhere in this codebase) — it must never take down the whole call. * * Only a 400 (`InvalidLocationError`) triggers the retry. Any other error class (rate * limit, 5xx, etc.) propagates immediately, since the fire variables were not the cause. * If the retry itself also fails, that error propagates — the original 400 was not the * garnish's fault, so no further fallback is attempted. * * @private */ private fetchCurrentConditions; /** * Build request parameters for current conditions data * * @param includeFireWeather - When true, appends soil moisture and vapour-pressure * deficit to the `current` variable list. These two variables are always returned * in fixed units (m³/m³ and kPa) — they are not affected by `openMeteoUnitParams`. * @private */ private buildCurrentParams; /** * Validate that the current conditions response contains the expected data * @private */ private validateCurrentResponse; /** * Build request parameters for forecast data * @private */ private buildForecastParams; /** * Make request to forecast API with retry logic * @private */ private makeRequestToForecast; /** * Validate that the forecast response contains the expected data * @private */ private validateForecastResponse; /** * Get a multi-model forecast comparison from the Open-Meteo Forecast API * (`get_forecast`'s `compare_models` flag, `docs/multi-model-comparison-plan.md` * D3). * * A distinct method from `getForecast` — not an option on it — because the * `models=` parameter changes the shape of every `daily` key (suffixed per * model) rather than adding new keys, and the comparison IS the requested * product (D7): unlike the fire-weather flag on `getCurrentConditions`, * there is no garnish-retry-without-the-extra-params fallback here. A * failed request propagates sanitized via `makeRequestToForecast`'s * existing error mapping. * * @param latitude - Latitude coordinate (-90 to 90) * @param longitude - Longitude coordinate (-180 to 180) * @param days - Number of forecast days (1-16, default: 7) * @param prefs - Unit preferences (default: imperial) * @returns Multi-model daily comparison data, suffixed per model */ getModelComparison(latitude: number, longitude: number, days?: number, prefs?: UnitPreferences): Promise; /** * Build request parameters for a multi-model comparison request. * * Deliberately NOT built on top of `buildForecastParams` — that method's * 19-variable daily list, multiplied across `COMPARISON_MODELS.length` * models, would sextuple response size for variables the comparison view * never renders (D3). Requests exactly the six verified daily variables in * a fixed order, plus `models=`. * @private */ private buildModelComparisonParams; /** * Validate that a multi-model comparison response contains usable data. * * Cannot reuse `validateForecastResponse`: with `models=` set to more than * one model, `daily.time` is present but the *unsuffixed* keys that * validator checks (e.g. plain `temperature_2m_max`) are absent — every key * is suffixed per model instead. This validator instead requires * non-empty `daily.time` AND at least one `temperature_2m_max_` key * present for a model in `COMPARISON_MODELS`. * * Defensive note (live-verified, design header fact (a)): a request naming * exactly ONE model returns UNSUFFIXED keys — our curated list always * requests six models, so that shape should never occur here, but this * validator fails loudly with `DataNotFoundError` rather than silently * mis-parsing if it ever does. * @private */ private validateModelComparisonResponse; /** * Make request to the ensemble API with retry logic. Follows * `makeRequestToFlood`'s shape exactly — retry/backoff/sanitization are * shared via `handleError`, wired identically for every host client. * @private */ private makeRequestToEnsemble; /** * Get a single-model ensemble spread from the Open-Meteo Ensemble API * (`get_forecast`'s `ensemble_spread` flag, `docs/ensemble-spread-plan.md` * D3). One fixed model (`ENSEMBLE_MODEL`, imported from the pure * `ensembleSpread` util — the service imports the constant from the util, * never the reverse, mirroring the corrected `compare_models` arrangement). * * This is **contract, not garnish** (D7): a failed request propagates * sanitized via `makeRequestToEnsemble`'s shared error mapping — there is * no retry-without-models and no degraded fallback to a plain forecast. * * @param latitude - Latitude coordinate (-90 to 90) * @param longitude - Longitude coordinate (-180 to 180) * @param days - Number of forecast days (1-16, default: 7) * @param prefs - Unit preferences (default: imperial) * @returns Single-model ensemble daily data: one unsuffixed control-run * series plus `_memberNN` perturbed-member series per variable */ getEnsembleSpread(latitude: number, longitude: number, days?: number, prefs?: UnitPreferences): Promise; /** * Build request parameters for a single-model ensemble request. * * Exactly five daily variables, in a fixed order — **never** * `precipitation_probability_max`: verified live on the ensemble endpoint * (design "Upstream verification" c) to return HTTP 200 with unit * `"undefined"` and all-null control *and* member arrays, because * probability is *derived from* ensembles rather than published by them — * the wet-member fraction the pure module computes from `precipitation_sum` * IS the probability product, so requesting the field would only add a * useless all-null series to every response. * @private */ private buildEnsembleParams; /** * Validate that a single-model ensemble response contains usable member * data. * * Requires non-empty `daily.time` AND a `temperature_2m_max_member01` key. * Fails loudly with `DataNotFoundError` rather than mis-parsing on two * shapes it must never silently accept (design "Upstream verification" h): * * 1. A **memberless plain-forecast shape** — only unsuffixed keys, no * `_memberNN` series (e.g. if the ensemble host ever served a plain * forecast response for some request). * 2. The **multi-model renamed-suffix shape** — with more than one model * requested, Open-Meteo suffixes member keys with *resolved internal * names* rather than the requested alias (e.g. * `temperature_2m_max_member01_ncep_gefs_seamless` instead of plain * `temperature_2m_max_member01`). This feature requests exactly one * model (`ENSEMBLE_MODEL`), so this shape is unreachable via our fixed * constant — guarded anyway, since a validator that mis-parses instead * of failing loudly is worse than one that's merely defensive. * @private */ private validateEnsembleResponse; /** * Get air quality data from Open-Meteo Air Quality API * * @param latitude - Latitude coordinate (-90 to 90) * @param longitude - Longitude coordinate (-180 to 180) * @param forecast - Whether to include hourly forecast (default: false, returns current only) * @param forecastDays - Number of forecast days (1-7, default: 5) * @returns Air quality data including AQI, pollutants, and UV index */ getAirQuality(latitude: number, longitude: number, forecast?: boolean, forecastDays?: number): Promise; /** * Build request parameters for air quality data * @private */ private buildAirQualityParams; /** * Make request to air quality API with retry logic * @private */ private makeRequestToAirQuality; /** * Validate that the air quality response contains the expected data * @private */ private validateAirQualityResponse; /** * Get marine conditions data from Open-Meteo Marine API * * @param latitude - Latitude coordinate (-90 to 90) * @param longitude - Longitude coordinate (-180 to 180) * @param forecast - Whether to include daily forecast aggregates (default: false, returns current only) * @param forecastDays - Number of forecast days (1-16, default: 5). The Marine API * accepts up to 16 days (verified live 2026-07-16), but the underlying model's * horizon is typically ~10 days — trailing days beyond that are null-padded. * @returns Marine conditions including waves, swell, and currents */ getMarine(latitude: number, longitude: number, forecast?: boolean, forecastDays?: number): Promise; /** * Build request parameters for marine data * @private */ private buildMarineParams; /** * Make request to marine API with retry logic * @private */ private makeRequestToMarine; /** * Validate that the marine response contains the expected data * @private */ private validateMarineResponse; /** * Get river discharge data from the Open-Meteo Flood API (GloFAS v4 model) * * Accepts one or more coordinate pairs in a single request — this is what * lets the channel-snapping probe (a 3x3 grid around a target point) fetch * its whole neighborhood as one HTTP call instead of nine. * * Does NOT validate that any returned series is non-null: a location with * no river running through its grid cell (ocean, desert) legitimately * returns HTTP 200 with null-filled arrays, and that must reach the * caller rather than being treated as an error. * * @param latitudes - Latitude coordinates (-90 to 90), one per point * @param longitudes - Longitude coordinates (-180 to 180), one per point, * same length and order as `latitudes` * @param forecastDays - Number of forecast days (1-210, default: 7). The * live API accepts up to 366, but 210 is the documented contract. * @returns One response per requested coordinate, always as an array — * Open-Meteo returns a bare object for a single-coordinate request and * an array for a multi-point request, and this normalizes both shapes. */ getRiverDischarge(latitudes: number[], longitudes: number[], forecastDays?: number): Promise; /** * Build request parameters for river discharge data * @private */ private buildFloodParams; /** * Make request to flood API with retry logic * @private */ private makeRequestToFlood; /** * Get climate normals (1991-2020 averages) for a specific date * * Backed by a per-location, per-instance **table** (D1): the first call for * a given `(lat2dp, lon2dp)` fetches one full year (1991-01-01…2020-12-31) * of daily archive data and computes all 366 `"MM-DD"` slots in one pass * (`computeNormalsTable`); the table itself — not a per-date result — is * what's cached, under `getNormalsTableCacheKey` with TTL * `CacheConfig.ttl.normals`. Every subsequent call for the same location, * for any date (including the fan-out `get_weather_summary` makes across * forecast + current), indexes into the cached table instead of refetching * — including when the table's slots are all unavailable (open ocean: the * table itself still caches, so a second date at that location makes no * second archive pull). * * @param latitude - Latitude (-90 to 90) * @param longitude - Longitude (-180 to 180) * @param month - Month (1-12) * @param day - Day of month (1-31) * @returns Climate normals (30-year averages) in Fahrenheit and inches * @throws {InvalidLocationError} If coordinates are invalid * @throws {DataNotFoundError} If the requested date's slot has too few * qualifying samples in the 30-year record (open ocean, sparse archive) * @throws {ServiceUnavailableError} If Open-Meteo API is unavailable */ getClimateNormals(latitude: number, longitude: number, month: number, day: number): Promise; /** * Get the cached full-year normals table for a location, fetching and * computing it on a cache miss. Private — `getClimateNormals` is the * public per-date entry point per D1's kept signature. * @private */ private getNormalsTable; /** * Fetch and compute one location's full-year normals table, with a single * bounded retry on a rate limit (D3). A second 429 — and every non-429 * error, immediately — propagates unchanged to the call site's existing * catch, so normals stay garnish and never fail the parent response. * @private */ private fetchNormalsTable; } //# sourceMappingURL=openmeteo.d.ts.map