yr.no Weather Forecast¶
yr.no is a free weather service operated by the Norwegian Meteorological
Institute (MET Norway). The Locationforecast 2.0 API provides global
point-based weather forecasts in GeoJSON format. No API key is required --
only a User-Agent header identifying the application.
Key characteristics¶
| Property | Value |
|---|---|
| Provider | MET Norway (Norwegian Meteorological Institute) |
| Spatial resolution | Point-based (single lat/lon coordinate) |
| Temporal resolution | Hourly (0-48h), 6-hourly (2-10 days) |
| Coverage | Global |
| Forecast window | Up to ~9 days from current time |
| Historical data | None -- forecast only |
| Format | GeoJSON (RFC 7946) |
| CRS | EPSG:4326 (WGS 84) |
API endpoints¶
The Locationforecast 2.0 API offers three endpoints:
| Endpoint | Description |
|---|---|
/compact |
Subset of variables sufficient for most use cases |
/complete |
Full set of variables including percentiles and cloud layers |
/status |
API health check |
The pipeline uses the complete endpoint to access all available variables:
API requirements¶
User-Agent header¶
A custom User-Agent header is mandatory. It must contain the
application name and contact information (GitHub URL, email, or website).
Missing or generic User-Agent strings return 403 Forbidden. Falsifying
this information risks permanent blacklisting.
The pipeline sets:
Coordinate precision¶
Coordinates must use no more than 4 decimal places. Using 5 or more
decimals returns 403 Forbidden. This restriction enables effective
server-side caching. Four decimal places give ~11 m precision, which is
more than sufficient for weather data.
# Coordinates are truncated to 4 decimals before the API call
lat = round(lat, 4) # e.g. 8.4842
lon = round(lon, 4) # e.g. -13.2344
HTTPS¶
Always use HTTPS. Unencrypted HTTP traffic may be throttled or blocked.
Caching and conditional requests¶
MET Norway strongly encourages caching. Every response includes:
Expires-- when the forecast data will next be updated. Do not re-request before this time.Last-Modified-- when the data was last changed.
Use If-Modified-Since with the cached Last-Modified value on
subsequent requests. A 304 Not Modified response means the cached data
is still current.
Rate limiting¶
No hard published limit, but approximately 20 requests/second is the
practical ceiling. Monitor for 429 Too Many Requests responses. The
pipeline processes org units sequentially, which naturally stays well
within limits.
Status codes¶
| Code | Meaning |
|---|---|
| 200 | Success, new data returned |
| 304 | Not Modified -- use cached data |
| 403 | Forbidden -- bad User-Agent or coordinate precision |
| 429 | Throttled -- reduce request rate |
Response format¶
The API returns a GeoJSON FeatureCollection with a timeseries array.
Each entry has an ISO 8601 timestamp and nested data:
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-13.2344, 8.4842] },
"properties": {
"timeseries": [
{
"time": "2026-03-03T12:00:00Z",
"data": {
"instant": {
"details": {
"air_temperature": 28.5,
"air_pressure_at_sea_level": 1010.2,
"relative_humidity": 68.0,
"wind_speed": 2.3,
"cloud_area_fraction": 45.0,
"dew_point_temperature": 22.1,
"wind_from_direction": 210.5
}
},
"next_1_hours": {
"summary": { "symbol_code": "partlycloudy_day" },
"details": { "precipitation_amount": 0.0 }
},
"next_6_hours": {
"summary": { "symbol_code": "partlycloudy_day" },
"details": {
"precipitation_amount": 0.2,
"air_temperature_max": 30.1,
"air_temperature_min": 27.8
}
}
}
}
]
}
}
instant.details-- atmospheric variables at the exact timestamp.next_1_hours.details-- accumulated values for the next 1 hour (available for first ~48 hours).next_6_hours.details-- accumulated values for the next 6 hours (available for the full forecast window).
Available variables¶
Instant variables¶
Variables marked with compact are included in both /compact and
/complete; others are /complete only.
| Variable | Unit | Compact | Description |
|---|---|---|---|
air_temperature |
C | yes | Air temperature at 2 m height |
air_temperature_percentile_10 |
C | no | 10th percentile air temperature |
air_temperature_percentile_90 |
C | no | 90th percentile air temperature |
air_pressure_at_sea_level |
hPa | yes | Air pressure reduced to sea level |
cloud_area_fraction |
% | yes | Total cloud cover (all heights) |
cloud_area_fraction_high |
% | no | Cloud cover above 5000 m |
cloud_area_fraction_medium |
% | no | Cloud cover 2000-5000 m |
cloud_area_fraction_low |
% | no | Cloud cover below 2000 m |
dew_point_temperature |
C | no | Dewpoint temperature at 2 m |
fog_area_fraction |
% | no | Area covered in fog (visibility < 1 km) |
relative_humidity |
% | yes | Relative humidity at 2 m |
ultraviolet_index_clear_sky |
index | no | UV index for cloud-free conditions (0-11+) |
wind_from_direction |
degrees | yes | Direction wind is coming from (0 = N, 90 = E) |
wind_speed |
m/s | yes | Wind speed at 10 m (10-minute average) |
wind_speed_percentile_10 |
m/s | no | 10th percentile wind speed |
wind_speed_percentile_90 |
m/s | no | 90th percentile wind speed |
wind_speed_of_gust |
m/s | no | Maximum gust at 10 m |
Period variables¶
| Variable | Unit | Period | Description |
|---|---|---|---|
precipitation_amount |
mm | 1h, 6h | Expected precipitation for period |
precipitation_amount_max |
mm | 1h, 6h | Maximum likely precipitation |
precipitation_amount_min |
mm | 1h, 6h | Minimum likely precipitation |
probability_of_precipitation |
% | 1h, 6h, 12h | Chance of precipitation |
probability_of_thunder |
% | 1h, 6h | Chance of thunder |
air_temperature_max |
C | 6h | Maximum air temperature over period |
air_temperature_min |
C | 6h | Minimum air temperature over period |
symbol_code |
string | 1h, 6h, 12h | Weather icon code |
Variable names follow the international CF Standard Name vocabulary (required by the EU INSPIRE directive).
Variables used in the pipeline¶
Six variables are extracted from the /complete response and aggregated
from hourly/6-hourly to daily values:
| Variable | API field | Source | Daily aggregation | Unit |
|---|---|---|---|---|
| Temperature | air_temperature |
instant | Mean of all hourly values | C |
| Precipitation | precipitation_amount |
next_1_hours / next_6_hours | Sum of all periods | mm |
| Relative humidity | relative_humidity |
instant | Mean of all hourly values | % |
| Wind speed | wind_speed |
instant | Mean of all hourly values | m/s |
| Cloud cover | cloud_area_fraction |
instant | Mean of all hourly values | % |
| Air pressure | air_pressure_at_sea_level |
instant | Mean of all hourly values | hPa |
Daily aggregation logic¶
For each calendar day (UTC), the pipeline:
- Groups all timeseries entries by date.
- For instant variables (temperature, humidity, wind speed, cloud cover, pressure): computes the arithmetic mean of all hourly values within the day.
- For precipitation: sums the
precipitation_amountvalues. Prefersnext_1_hours(higher resolution) when available; falls back tonext_6_hoursfor the later part of the forecast window.
Days with no data for a given variable produce None (skipped during
DHIS2 import).
Approach: point-based forecasts via centroids¶
Unlike CHIRPS and ERA5 (which provide raster data processed with zonal statistics), yr.no returns point forecasts for specific coordinates.
The flow computes the centroid of each org unit polygon (mean of all coordinate vertices) and queries yr.no for that single point. This is a simple approximation that works well for org units where the weather is relatively uniform across the area.
from prefect_climate import centroid
lat, lon = centroid(org_unit.geometry)
# e.g. (8.4842, -13.2344) for Bo District, Sierra Leone
For large or irregularly shaped org units, the centroid may not be representative of conditions across the entire area. Consider this limitation when interpreting data for mountainous regions or coastal boundaries.
Pipeline overview¶
- Fetch org unit geometries from DHIS2 (Polygon/MultiPolygon only)
- Ensure 6 data elements and 1 daily data set exist in DHIS2
- For each org unit:
a. Compute polygon centroid (mean lat/lon)
b. Truncate coordinates to 4 decimal places
c. Fetch forecast from yr.no
/completeendpoint d. Aggregate hourly timeseries to daily values - Build DHIS2 data values (period format:
YYYYMMDD) - POST data values to DHIS2
/api/dataValueSets - Create markdown artifact with import summary
@flow(name="dhis2_yr_weather_import", log_prints=True)
def dhis2_yr_weather_import_flow(query: ClimateQuery | None = None) -> ImportResult:
client = get_dhis2_credentials().get_client()
org_units = ensure_dhis2_metadata(client, query.org_unit_level)
forecast_results = []
for ou in org_units:
result = fetch_org_unit_forecast(ou) # centroid + yr.no API
forecast_results.append(result)
data_values = build_data_values(forecast_results)
return import_to_dhis2(client, dhis2_url, org_units, data_values)
DHIS2 data model¶
Data Set: WF: yr.no: Weather Forecast (WfYrFrcSet1) [Daily, openFuturePeriods=10]
|- Data Element: WF: yr.no: Temperature (WfYrTmpEst1) -- mean C
|- Data Element: WF: yr.no: Precipitation (WfYrPrcEst1) -- sum mm
|- Data Element: WF: yr.no: Relative Humidity (WfYrHumEst1) -- mean %
|- Data Element: WF: yr.no: Wind Speed (WfYrWndEst1) -- mean m/s
|- Data Element: WF: yr.no: Cloud Cover (WfYrCldEst1) -- mean %
|- Data Element: WF: yr.no: Air Pressure (WfYrPrsEst1) -- mean hPa
| UID | Name | Aggregation | Unit |
|---|---|---|---|
WfYrTmpEst1 |
WF: yr.no: Temperature | Daily mean | degrees Celsius (C) |
WfYrPrcEst1 |
WF: yr.no: Precipitation | Daily sum | millimetres (mm) |
WfYrHumEst1 |
WF: yr.no: Relative Humidity | Daily mean | percent (%) |
WfYrWndEst1 |
WF: yr.no: Wind Speed | Daily mean | metres per second (m/s) |
WfYrCldEst1 |
WF: yr.no: Cloud Cover | Daily mean | percent (%) |
WfYrPrsEst1 |
WF: yr.no: Air Pressure | Daily mean | hectopascals (hPa) |
The data set uses openFuturePeriods=10 since forecasts extend ~9 days
into the future.
Building a time series¶
The yr.no API provides only future forecasts -- there is no historical archive. Each run of the flow imports the current ~9-day forecast window. Running the flow on a regular schedule (e.g. daily via cron) builds up a historical record of forecast data in DHIS2 over time. Overlapping forecast days from consecutive runs are updated in place.
Health relevance¶
| Variable | Health relevance |
|---|---|
| Temperature | Heat-related illness, malaria transmission windows (optimal 20-30 C) |
| Precipitation | Waterborne disease risk, flooding, mosquito breeding habitat |
| Relative humidity | Pathogen survival and airborne transmission, respiratory illness |
| Wind speed | Disease vector dispersal, air quality, extreme weather preparedness |
| Cloud cover | UV exposure estimation, solar energy availability |
| Air pressure | Migraine triggers, respiratory conditions, storm prediction |
Comparison with ERA5 and CHIRPS¶
| Property | yr.no | ERA5-Land | CHIRPS |
|---|---|---|---|
| Data type | Forecast (future) | Reanalysis (historical) | Observation (historical) |
| Spatial | Point-based (centroid) | ~9 km raster (zonal stats) | ~5 km raster (zonal stats) |
| Temporal | Daily (from hourly) | Monthly | Monthly (from daily) |
| Variables | 6 weather indicators | 7 climate indicators | Precipitation only |
| API key | None (User-Agent only) | CDS API key required | None (direct download) |
| Latency | Real-time forecast | ~5 days behind present | ~3 weeks behind present |
yr.no complements ERA5 and CHIRPS by providing near-term forecast data rather than historical observations. This enables early warning and preparedness workflows in DHIS2.
Running the flow¶
The flow is intended for manual or scheduled runs. Each run fetches the current forecast window (~9 days) and imports it into DHIS2.
# Direct run
uv run python flows/dhis2/dhis2_yr_weather_import.py
# Via Docker deployment
docker compose build dhis2-yr-weather-docker
docker compose up -d dhis2-yr-weather-docker
Direct API usage¶
from prefect_climate.yr import fetch_daily_forecasts
# Fetch forecast for Freetown, Sierra Leone
forecasts = fetch_daily_forecasts(lat=8.48, lon=-13.23)
for day in forecasts:
print(f"{day.date}: {day.temperature}C, {day.precipitation}mm rain")
from prefect_climate.yr import fetch_forecast, aggregate_forecast_daily
# Two-step: fetch raw response, then aggregate
raw = fetch_forecast(lat=8.48, lon=-13.23)
daily = aggregate_forecast_daily(raw)