Using met.no forecasts with weather-tools¶
This notebook-style script shows how to fetch met.no forecasts and combine them
with SILO observations using the new weather_tools APIs. Open it directly in
JupyterLab or VS Code with Jupytext to run the cells interactively.
Prerequisites¶
Network access to
https://api.met.noand a valid contact e-mail for the User-Agent header.Set
SILO_API_KEY(your registered SILO e-mail) or passapi_key=directly toSiloAPI.Install
weather-toolsin your environment (either viapip install weather-toolsor the local checkout).Optional: enable logging (e.g.
logging.basicConfig(level=logging.INFO)) to inspect requests.
import pandas as pd
from weather_tools.merge_weather_data import get_merge_summary, merge_historical_and_forecast
from weather_tools.metno_api import MetNoAPI
from weather_tools.metno_models import MetNoFormat, MetNoQuery
from weather_tools.silo_api import SiloAPI
from weather_tools.silo_models import AustralianCoordinates
# silo_api = SiloAPI(log_level="INFO")
# station_meta = silo_api.search_stations("Northam").iloc[0]
station_meta = {
"station_code": 10111,
"name": "NORTHAM",
"latitude": -31.651,
"longitude": 116.659,
"state": "WA",
"elevation": 170.0,
}
1. Configure the met.no API client¶
met.no requires a descriptive User-Agent string that includes contact details.
Replace the placeholder below with your own application name and e-mail.
api = MetNoAPI(
# user_agent="weather-tools-example/0.1 (contact: you@example.com)",
enable_cache=True,
log_level="DEBUG",
)
2. Build a forecast query for your location¶
The AustralianCoordinates model validates that the latitude and longitude sit
within the range supported by SILO (GDA94 datum). Adjust the coordinates to
target your site of interest.
coordinates = AustralianCoordinates(
latitude=station_meta["latitude"], longitude=station_meta["longitude"]
) # Northam, WA
query = MetNoQuery(coordinates=coordinates, format=MetNoFormat.COMPACT)
response = api.query_forecast(query)
metadata = response.get_meta()
metadata
[11/21/25 13:17:05] DEBUG 🌐 Constructed URL: https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=-31.651&lon=116.659
DEBUG 📋 User-Agent: weather-tools/0.0.0 (Python 3.12)
DEBUG Making request (attempt 1/3): https://api.met.no/weatherapi/locationforecast/2.0/compact
[11/21/25 13:17:06] DEBUG Cached response for: e9450af0091badb1a028399707618845
DEBUG Request successful on attempt 1
{'updated_at': '2025-11-21T01:18:54Z',
'units': {'air_pressure_at_sea_level': 'hPa',
'air_temperature': 'celsius',
'cloud_area_fraction': '%',
'precipitation_amount': 'mm',
'relative_humidity': '%',
'wind_from_direction': 'degrees',
'wind_speed': 'm/s'}}
3. Convert forecasts to daily and hourly tables¶
MetNoAPI.to_dataframe can aggregate the hourly GeoJSON payload to daily summaries
or return hourly data. Use the frequency parameter to control aggregation:
frequency='daily'(default) - Daily aggregatesfrequency='hourly'- Raw hourly datafrequency='weekly'orfrequency='monthly'- Other time periods
daily_forecasts = api.to_dataframe(response, frequency="daily")
hourly_forecasts = api.to_dataframe(response, frequency="hourly")
daily_forecasts.head()
| date | min_temperature | max_temperature | total_precipitation | avg_wind_speed | max_wind_speed | avg_relative_humidity | avg_pressure | avg_cloud_fraction | dominant_weather_symbol | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2025-11-21 | 17.1 | 29.3 | 0.0 | 3.426316 | 5.1 | 48.757895 | 1008.336842 | 41.810526 | partlycloudy_night |
| 1 | 2025-11-22 | 15.0 | 29.4 | 0.3 | 5.058333 | 7.9 | 65.912500 | 1006.600000 | 33.129167 | rainshowers_day |
| 2 | 2025-11-23 | 12.1 | 22.6 | 0.1 | 6.073684 | 8.8 | 50.389474 | 1010.536842 | 19.694737 | partlycloudy_day |
| 3 | 2025-11-24 | 12.8 | 21.7 | 0.1 | 4.850000 | 6.4 | 58.775000 | 1011.625000 | 49.600000 | partlycloudy_day |
| 4 | 2025-11-25 | 12.6 | 23.8 | 0.0 | 4.500000 | 5.2 | 50.475000 | 1013.075000 | 23.850000 | partlycloudy_day |
hourly_forecasts.head()
| time | air_pressure_at_sea_level | air_temperature | cloud_area_fraction | relative_humidity | wind_from_direction | wind_speed | precipitation_amount | symbol_code | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 2025-11-21 05:00:00 | 1009.2 | 28.4 | 7.0 | 31.8 | 131.4 | 3.2 | 0.0 | clearsky_day |
| 1 | 2025-11-21 06:00:00 | 1008.4 | 29.0 | 5.5 | 29.8 | 136.2 | 2.4 | 0.0 | clearsky_day |
| 2 | 2025-11-21 07:00:00 | 1007.8 | 29.3 | 11.7 | 28.9 | 165.1 | 1.7 | 0.0 | clearsky_day |
| 3 | 2025-11-21 08:00:00 | 1007.5 | 29.2 | 26.6 | 28.6 | 200.4 | 2.1 | 0.0 | fair_day |
| 4 | 2025-11-21 09:00:00 | 1007.5 | 28.5 | 10.9 | 29.6 | 200.5 | 2.1 | 0.0 | clearsky_day |
Quick helper: get_daily_forecast¶
get_daily_forecast returns a pandas DataFrame directly with daily summaries,
daily_df = api.get_daily_forecast(
latitude=coordinates.latitude,
longitude=coordinates.longitude,
days=5,
)
daily_df # Already a DataFrame!
DEBUG 🌐 Constructed URL: https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=-31.651&lon=116.659
DEBUG 📋 User-Agent: weather-tools/0.0.0 (Python 3.12)
DEBUG Cache hit for request: e9450af0091badb1a028399707618845
| date | min_temperature | max_temperature | total_precipitation | avg_wind_speed | max_wind_speed | avg_relative_humidity | avg_pressure | avg_cloud_fraction | dominant_weather_symbol | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2025-11-21 | 17.1 | 29.3 | 0.0 | 3.426316 | 5.1 | 48.757895 | 1008.336842 | 41.810526 | partlycloudy_night |
| 1 | 2025-11-22 | 15.0 | 29.4 | 0.3 | 5.058333 | 7.9 | 65.912500 | 1006.600000 | 33.129167 | rainshowers_day |
| 2 | 2025-11-23 | 12.1 | 22.6 | 0.1 | 6.073684 | 8.8 | 50.389474 | 1010.536842 | 19.694737 | partlycloudy_day |
| 3 | 2025-11-24 | 12.8 | 21.7 | 0.1 | 4.850000 | 6.4 | 58.775000 | 1011.625000 | 49.600000 | partlycloudy_day |
| 4 | 2025-11-25 | 12.6 | 23.8 | 0.0 | 4.500000 | 5.2 | 50.475000 | 1013.075000 | 23.850000 | partlycloudy_day |
4. Merge met.no forecasts with SILO history¶
Pull the last five days from the SILO DataDrill API for the same coordinates,
then merge that history with the met.no forecast. The helper automatically
converts column names, fills optional variables (when enabled), and annotates
the data source for each record.
first_forecast_date = pd.to_datetime(daily_df["date"]).min()
# history_end = first_forecast_date - pd.Timedelta(days=1)
history_start = first_forecast_date - pd.Timedelta(days=4)
silo_api = SiloAPI(log_level="INFO")
silo_history, _ = silo_api.get_data_drill(
latitude=coordinates.latitude,
longitude=coordinates.longitude,
start_date=history_start.strftime("%Y%m%d"),
end_date=first_forecast_date.strftime("%Y%m%d"),
variables=["daily_rain", "max_temp", "min_temp", "et_short_crop"],
)
merged = merge_historical_and_forecast(
silo_data=silo_history,
metno_data=daily_df,
overlap_strategy="prefer_silo",
return_cols="silo_only",
)
merged
merge_summary = get_merge_summary(merged)
merge_summary
{'total_records': 9,
'silo_records': np.int64(4),
'metno_records': np.int64(5),
'date_range': {'start': Timestamp('2025-11-17 00:00:00'),
'end': Timestamp('2025-11-25 00:00:00'),
'days': 9},
'silo_period': {'start': Timestamp('2025-11-17 00:00:00'),
'end': Timestamp('2025-11-20 00:00:00')},
'metno_period': {'start': Timestamp('2025-11-21 00:00:00'),
'end': Timestamp('2025-11-25 00:00:00')},
'transition_date': Timestamp('2025-11-21 00:00:00')}
4b. Merge Forecast with SILO PatchedPoint¶
Pull the last four days from the SILO Patch Point API for the same coordinates, using the station code.
first_forecast_date = pd.to_datetime(daily_forecasts["date"]).min()
history_end = first_forecast_date # - pd.Timedelta(days=1) # Include day before forecast to check overlap handling
history_start = history_end - pd.Timedelta(days=4)
silo_station_data, _ = silo_api.get_patched_point(
station_code=str(station_meta["station_code"]),
start_date=history_start.strftime("%Y%m%d"),
end_date=history_end.strftime("%Y%m%d"),
variables=["daily_rain", "max_temp", "min_temp", "et_short_crop"],
)
merge_historical_and_forecast(
silo_data=silo_station_data,
metno_data=daily_df,
return_cols="silo_only",
)