Skip to content

flaticols/knmi-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

knmi-go

A small, stdlib-only Go client for the KNMI EDR API — Dutch in-situ weather observations and daily climatology, returned as typed Go domain values rather than raw CoverageJSON.

⚠️ Pre-1.0 — API is not stable until the v0.1.0 tag. Names, signatures, and behavior of exported identifiers may change without deprecation notice while we shake the surface out against the live KNMI API. Pin a commit if you depend on this in production.

Scope

KNMI's EDR endpoint serves:

  • In-situ observations — 10-min, hourly, daily (with _validated variants) from ~77 fixed weather stations across the Netherlands and surrounding waters.
  • Daily climatologyTg1, Tn1, Tx1, Rd1, EV24, wins50.

It does not serve precipitation nowcast or HARMONIE forecasts — those live behind KNMI's separate Open Data API as HDF5/GRIB files and are out of this module's scope.

The library is built around a small, opinionated surface for the "current conditions at a coordinate" use-case, with raw access for everything else.

Install

As a library

go get go.flaticols.dev/knmi-go

As a CLI

go install go.flaticols.dev/knmi-go/cmd/knmi@latest

Authentication

Get an API key from developer.dataplatform.knmi.nl. Either pass it explicitly or set the KNMI_API_KEY environment variable:

c := knmi.New("eyJv...")  // explicit
c := knmi.New("")          // falls back to $KNMI_API_KEY

The key is sent in the raw Authorization header (no Bearer prefix) — this is what KNMI expects.

Quick start

Library

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "go.flaticols.dev/knmi-go"
)

func main() {
    c := knmi.New("")  // picks up KNMI_API_KEY
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    cur, err := c.GetCurrent(ctx, 52.3676, 4.9041)  // Amsterdam
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Station: %s (%.4f, %.4f)\n",
        cur.Station.Name, cur.Station.Lat, cur.Station.Lon)
    if cur.TempC != nil {
        fmt.Printf("%.1f°C at %s\n",
            *cur.TempC, cur.Time.Format(time.RFC3339))
    }
}

The library asks for the nearest of KNMI's ~77 stations to the requested coordinates — Amsterdam center will resolve to Schiphol Airport, for example. The actual station picked is reported via cur.Station.

CLI

knmi -location amsterdam
KNMI current conditions — Amsterdam (52.3676, 4.9041)
Station:  Schiphol Airport (0-20000-0-06240) at 52.3172, 4.7897
Observed: 2026-04-30 18:10 UTC

  Temperature     18.0 °C
  Humidity        25 %
  Wind speed      6.0 m/s
  Wind direction  105°
  Precipitation   0.00 mm/h
  Pressure        1024.9 hPa

Other CLI inputs:

knmi -lat 52.37 -lon 4.90      # explicit coordinates
knmi -location 52.5,4.7         # literal lat,lon string
knmi -list-locations            # known NL place names
knmi -token <key> -location utrecht

Use cases

Time series — past 2 hours, 2 days, a week

SeriesAt returns the full time-history of one parameter at the nearest station to your coordinates. Pick the collection that matches the cadence you want:

Window Collection Cadence
≤ a few h Collection10MinObservations 10 minutes
hours–days CollectionHourlyObservations 1 hour
days–weeks CollectionDailyObservations 1 day

Note: KNMI EDR only serves observations — there is no forecast side. For "past 2 hours" you get the actual measured trend; there is no equivalent "next 2 hours".

Past 2 hours, 10-minute cadence

to := time.Now().UTC()
from := to.Add(-2 * time.Hour)

s, err := c.SeriesAt(ctx,
    knmi.Collection10MinObservations, "ta",
    52.3676, 4.9041,    // Amsterdam → resolves to Schiphol
    from, to,
)
if err != nil { log.Fatal(err) }

fmt.Printf("%s — %d readings of %s [%s]\n",
    s.Station.Name, len(s.Readings), s.Param, s.Unit)
for _, r := range s.Readings {
    if r.Value != nil {
        fmt.Printf("  %s  %.2f\n", r.Time.Format("15:04"), *r.Value)
    }
}

Sample output (live, Schiphol Airport):

Schiphol Airport — 12 readings of ta [°C]
  16:30  19.70
  16:40  19.60
  16:50  19.50
  17:00  19.30
  ...

Past 2 days, hourly cadence

s, _ := c.SeriesAt(ctx,
    knmi.CollectionHourlyObservations, "ta",
    52.3676, 4.9041,
    time.Now().Add(-48*time.Hour), time.Now(),
)
fmt.Printf("48h hourly: %d readings\n", len(s.Readings))

Past week, daily

s, _ := c.SeriesAt(ctx,
    knmi.CollectionDailyObservations, "ta",
    52.3676, 4.9041,
    time.Now().AddDate(0, 0, -7), time.Now(),
)
for _, r := range s.Readings {
    fmt.Printf("%s  %.1f°C\n", r.Time.Format("Mon"), *r.Value)
}

Other parameters

SeriesAt works for any KNMI shortname the chosen collection accepts — not just ta. Useful examples:

Param Meaning
ta air temperature (°C)
rh relative humidity (%)
ff 10-min mean wind speed (m/s)
gff wind gust (m/s)
dd wind direction (°)
pp station pressure (hPa)
rg rain-gauge intensity (mm/h)
R1H precipitation amount, last 1 h (mm)
R24H precipitation amount, last 24 h (mm)
vv visibility (m)
n total cloud cover (octas)

If you ask for a name the server doesn't recognize, you get ErrBadRequest wrapping a clear "supported values are…" hint.

Runnable versions of these snippets live as godoc examples — see Example_seriesLastTwoHours, Example_seriesLastTwoDays, and Example_seriesLastWeekDaily in example_series_test.go.

Custom parameter set

The default fetch returns six well-known shortnames (ta, rh, ff, dd, rg, pp). KNMI exposes ~85 valid shortnames per station; ask for whatever subset you want and read the rest from cur.Raw:

cur, _ := c.FetchObservations(ctx, 52.37, 4.90,
    knmi.WithObservationParams("ta", "gff", "vv", "R1H"),
)
fmt.Printf("gust:       %v\n", cur.Raw["gff"])  // wind gust m/s
fmt.Printf("visibility: %v\n", cur.Raw["vv"])   // metres
fmt.Printf("1h rain:    %v\n", cur.Raw["R1H"])  // mm in past hour

Parameter names are not validated client-side — KNMI's catalog evolves; the server returns a clear 400 with the supported list (wrapped in ErrBadRequest) if you request something it doesn't recognize.

Bypass cache for one call

cur, _ := c.FetchObservationsFresh(ctx, 52.37, 4.90)

Or disable cache globally:

c := knmi.New("", knmi.WithCache(false))

Manual station selection

If you'd rather pick the station yourself instead of letting the library nearest-match:

stations, _ := c.Locations(ctx, knmi.Collection10MinObservations)
for _, s := range stations {
    fmt.Printf("%s  %s  (%.4f, %.4f)\n", s.ID, s.Name, s.Lat, s.Lon)
}

raw, _ := c.LocationData(ctx,
    knmi.Collection10MinObservations,
    "0-20000-0-06240", // Schiphol
    time.Now().Add(-30*time.Minute), time.Now(),
    []string{"ta", "rh"},
)
cov, _ := coveragejson.DecodeCollection(raw)
series, unit, _ := cov.First().Series("ta")

Discovery

cols, _ := c.Collections(ctx)
for _, col := range cols {
    fmt.Printf("%s  %s\n", col.ID, col.Title)
}

col, _ := c.Collection(ctx, knmi.Collection10MinObservations)
for name := range col.ParameterNames {
    fmt.Println(name)
}

API surface

// Constructor
knmi.New(apiKey string, opts ...Option) *Client

// High-level: nearest-station observations
(*Client).GetCurrent(ctx, lat, lon)
(*Client).FetchObservations(ctx, lat, lon, opts ...ObservationOption)
(*Client).FetchObservationsFresh(ctx, lat, lon, opts ...ObservationOption)

// Time series for one parameter at the nearest station
(*Client).SeriesAt(ctx, collection, param, lat, lon, from, to)

// Stations
(*Client).Locations(ctx, collectionID)
(*Client).LocationData(ctx, collectionID, stationID, from, to, params)
(*Client).NearestStation(ctx, collectionID, lat, lon)

// Catalog discovery
(*Client).Collections(ctx)
(*Client).Collection(ctx, id)
(*Client).Instances(ctx, id)
(*Client).LatestInstance(ctx, id)

Options

Option Purpose
WithBaseURL(string) override EDR base URL (testing, staging)
WithUserAgent(string) set the User-Agent header
WithHTTPClient(*http.Client) inject your own http.Client (timeouts, proxy)
WithClock(func() time.Time) inject a clock for deterministic tests
WithLogger(LogFunc) structured per-request callback
WithCache(bool) enable/disable in-memory caching (default true)

Per-call observation options:

Option Purpose
WithObservationParams(params ...string) replace the default parameter set
WithObservationLookback(d time.Duration) widen the time window scanned for the latest reading

Data model

type CurrentConditions struct {
    Station      Station
    Time         time.Time
    TempC        *float64
    Humidity     *float64
    WindSpeedMS  *float64
    WindDirDeg   *float64
    PrecipMMHour *float64
    PressureHPa  *float64
    Raw          map[string]*float64  // every shortname returned, nullable
}

type Station struct {
    ID, Name string
    Lat, Lon float64
}

type Series struct {
    Station  Station
    Param    string  // KNMI shortname (e.g. "ta")
    Unit     string  // KNMI-reported unit (e.g. "°C", "m s-1")
    Readings []Reading
}

type Reading struct {
    Time  time.Time
    Value *float64  // nil for missing
}

Pointer fields are nil when the sensor dropped out — distinguish "no data" from "zero".

Errors

Network and HTTP errors are mapped to typed sentinels you can match with errors.Is:

Sentinel HTTP Meaning
ErrAuth 401, 403 API key issue
ErrNotFound 404 wrong collection / no stations
ErrBadRequest 400, 422 invalid param or coords
ErrRateLimit 429 throttled (auto-retried once)
ErrUpstream 5xx KNMI side

The full *APIError carries the response body (with KNMI's helpful "supported values are…" hint when you guess a parameter name).

Caching

Three TTLs:

Cache TTL
Stations list 6 hours
Observations data 10 min

Coordinates round to 3 decimals (~100 m) when forming cache keys, so small jitter in caller-supplied coords doesn't fragment the cache. Different parameter sets cache separately so they never alias each other.

Pinned collection IDs

Collection10MinObservations           = "10-minute-in-situ-meteorological-observations"
CollectionHourlyObservations          = "hourly-in-situ-meteorological-observations"
CollectionHourlyObservationsValidated = "hourly-in-situ-meteorological-observations-validated"
CollectionDailyObservations           = "daily-in-situ-meteorological-observations"
CollectionDailyObservationsValidated  = "daily-in-situ-meteorological-observations-validated"

CollectionDailyMeanTemperature = "Tg1"
CollectionDailyMinTemperature  = "Tn1"
CollectionDailyMaxTemperature  = "Tx1"
CollectionDailyPrecipAmount    = "Rd1"
CollectionDailyEvapotrans      = "EV24"
CollectionWindStatistics       = "wins50"

Build / test

go test ./...                                 # unit tests, no network
go test -race ./...
KNMI_API_KEY=... go test -tags=integration ./...   # live API
go run ./cmd/knmi -location amsterdam              # CLI demo

Limitations

  • Observations are sparse. KNMI's in-situ network is fixed sites (airports, offshore platforms, KNMI HQ in De Bilt). The library resolves your coordinates to the nearest station — Amsterdam center hits Schiphol, for example. If you need gridded data, this isn't the right API.
  • No nowcast / HARMONIE. Those are HDF5/GRIB on KNMI's separate Open Data API.
  • No write side. Read-only client.

License

MIT

About

Stdlib-only Go client for the KNMI EDR API — Dutch in-situ weather observations and climatology, returned as typed Go values.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages