Luchtkwaliteit-alert voor je eigen straat
Een melding op je telefoon als de lucht bij jou thuis slecht is of slecht wordt, op basis van de verwachting van Luchtmeetnet, de meetstations in je stad, sensoren van buren en de RIVM-jaarkaart.
geef_project("luchtkwaliteit-alert")geef_bron("luchtmeetnet")geef_bron("rivm-samenmeten")geef_bron("rivm-gcn")
Ik heb het project, de geteste aanroepen en de valkuilen. Ik bouw een alert met Luchtmeetnet, Samen Meten (RIVM), Grootschalige concentratiekaarten (GCN) en KNMI daggegevens van het weer. Zo kan het eruitzien:
Dit bouw je zelf. Kopieer de prompt naar Claude, ChatGPT of Cursor, met de MCP-server aan, of volg de stappen hieronder.
Dit is een mockup, geen werkende versie, met de cijfers uit onze proefdraai bij de Domtoren in Utrecht op 16 oktober 2026, 23:47 (Luchtmeetnet, RIVM Samen Meten en de RIVM-jaarkaart 2025). De MCP-server zet je aan met het adres op Voor AI.
Voor wie
Mensen met astma of COPD en hun omgeving, ouders die willen weten of buiten spelen of hardlopen een goed idee is, buurtinitiatieven met eigen sensoren, en makers die een eerste echte alert-app willen bouwen.
Wat je krijgt
Een script (en daarna een app of bot) dat elk uur voor één plek vier dingen bekijkt en "ALERT" of "OK" zegt: de verwachte luchtkwaliteitsindex (LKI) op je punt voor het komende etmaal, de laatste uurwaarden op de officiële meetstations in je stad, de gekalibreerde fijnstofwaarde van sensoren van burgers binnen 500 meter, en het berekende jaargemiddelde op je plek naast het WHO-advies.
Proefdraai op 16 oktober 2026, 21:47 UTC, bij de Domtoren in Utrecht: verwachte LKI nu 4,1 en hoogstens 4,7 (17 oktober 12:00 UTC), ozon hoogstens 75 µg/m³ (informatiedrempel 180). Op Utrecht-Griftpark (1,3 km) was de LKI 4, stikstofdioxide (NO2) 4,1 en fijnstof (PM2,5) 1,6 µg/m³ in het uur tot 21:00 UTC; over 24 uur gemiddeld 17,0 µg/m³ NO2 (WHO-advies 25) en 5,2 µg/m³ PM2,5 (WHO-advies 15). Van de 10 Samen Meten-meetpunten binnen 500 meter gaven er maar 2 een recente waarde (2,2 en 1,4 µg/m³ PM2,5). Het jaargemiddelde 2025 op die plek volgens de RIVM-kaart: NO2 18,5, PM2,5 10,8 en PM10 18,5 µg/m³, dus 1,8, 2,2 en 1,2 keer het WHO-jaaradvies. Uitkomst: "OK". Een rustige avond met wind, maar op jaarbasis zit ook deze plek boven het WHO-advies.
Stikstofdioxide in 2025: het hoogst in de Randstad en langs de snelwegen, het laagst in het noorden
Het berekende jaargemiddelde NO2 over 2025 in vakjes van een kilometer, uit de kaartdienst van het RIVM. Volgens de klassen van de dienst is het lichtste blauw minder dan 10 microgram per kubieke meter; zo kleurt het grootste deel van Friesland, Groningen en Drenthe. De Randstad is donkerder, de snelwegen tekenen zich af als lijnen en de donkerste vlekken liggen rond Rotterdam, Den Haag en Amsterdam. Het is een rekenkaart op basis van metingen en modellen; een waarde op een punt vraag je op met GetFeatureInfo.
Zo zou je kunnen starten8 stappen
- 1
Vraag de verwachting op je eigen punt op
Luchtmeetnet heeft een weinig bekend eindpunt dat voor één coördinaat de huidige en verwachte waarden geeft (volgens de API-documentatie "current and predicted concentrations"): LKI, NO2, O3 of PM10. Je krijgt per uur een waarde vanaf het laatste hele uur tot ongeveer 19 uur vooruit; de modelwaarden veranderen in blokken van drie uur. Dit is de kern van een alert: je kunt waarschuwen vóórdat het slecht wordt. Geen paginering, geen sleutel. Breedte en lengte in WGS84 (gewone GPS-graden). Tijden zijn UTC; in de zomertijd is 21:00 UTC 23:00 in Nederland.
https://api.luchtmeetnet.nl/open_api/concentrations?formula=LKI&latitude=52.0907&longitude=5.1213
{"data": [ {"formula":"LKI","value":4.070000171661377,"timestamp_measured":"2026-10-16T21:00:00+00:00"}, {"formula":"LKI","value":4.070000171661377,"timestamp_measured":"2026-10-16T22:00:00+00:00"}, {"formula":"LKI","value":4.070000171661377,"timestamp_measured":"2026-10-16T23:00:00+00:00"}, {"formula":"LKI","value":3.549999952316284,"timestamp_measured":"2026-10-17T00:00:00+00:00"}, ...]} (20 uren, tot 2026-10-17T16:00) - 2
Leg de drempels vast, met herkomst
De luchtkwaliteitsindex (LKI) van het RIVM loopt van 1 tot 11 en heeft vijf klassen: goed (1 tot 3), matig (4 tot 6), onvoldoende (7 en 8), slecht (9 en 10) en zeer slecht (11). Teletekst 711 (bron RIVM) zette de index op 16 oktober 23 uur op 5, in de kolom "matig". Welke concentratie bij welk indexgetal hoort, geeft de API zelf: /components/<stof> heeft een lijst `limits` met per indexgetal een onder- en bovengrens in µg/m³ (NO2: index 4 is 30 tot 45; O3: index 9 begint bij 180, de Europese informatiedrempel, en 11 bij 240, de alarmdrempel). Kies daarnaast het WHO-advies van 2021 voor het lange termijn: 24 uur NO2 25, PM2,5 15, PM10 45; jaar NO2 10, PM2,5 5, PM10 15 µg/m³. De wettelijke EU-jaarnorm staat in de beschrijving van de stof (NO2 40, PM2,5 25). Negeer de kleurcodes in `limits`: die zijn niet consistent (index 5 heeft dezelfde kleur als index 2).
https://api.luchtmeetnet.nl/open_api/components/O3
{"data": {"formula": "O3", "description": {"NL": "... In heel Europa wordt de bevolking gewaarschuwd bij ozonconcentraties boven 180 (μg/m3). Een concentratie van 240 (μg/m3) is de Europese alarmdrempel. ..."}, "limits": [{"rating": 1, "lowerband": null, "upperband": 15, ...}, ... {"rating": 8, "lowerband": 140, "upperband": 180, ...}, {"rating": 9, "lowerband": 180, "upperband": 200, ...}, {"rating": 10, "lowerband": 200, "upperband": 240, ...}, {"rating": 11, "lowerband": 240, "upperband": 10000, ...}]}} - 3
Zoek de meetstations in je stad
De stationslijst (/stations, 5 pagina's) geeft alleen nummer en locatienaam. Ligging, soort (Traffic = verkeer, Municipal = stadsachtergrond, Regional = platteland) en welke stoffen het station meet staan alleen in /stations/<nummer>. Haal namen en ligging altijd daar vandaan, niet uit het hoofd (wij verwisselden eerder Stadhouderskade en Vondelpark). Reken zelf de afstand tot je punt uit. Een verkeersstation zegt iets over een drukke straat, een stadsachtergrondstation over de buurt eromheen.
https://api.luchtmeetnet.nl/open_api/stations/NL10643
{"data": {"type": "Municipal", "components": ["PM25", "NO", "O3", "NO2"], "geometry": {"type": "point", "coordinates": [5.128183, 52.101308]}, "municipality": "Utrecht", "organisation": "RIVM", "location": "Utrecht-Griftpark", ...}} - 4
Haal de index van alle stations in één keer op
Zet start en end op hetzelfde hele uur (UTC) en je krijgt de LKI van alle ongeveer 97 stations in één antwoord. Hetzelfde werkt voor /measurements met een formula (NO2: 86 stations). `timestamp_measured` is het eind van het uur: 21:00 is het gemiddelde van 20:00 tot 21:00. Let op: de LKI van een station is het hoogste indexgetal van de stoffen die het meet. Utrecht-Kardinaal de Jongweg meet geen ozon en kwam op 1, het Griftpark met ozon op 4. Vergelijk stations dus niet blind op hun LKI.
https://api.luchtmeetnet.nl/open_api/lki?start=2026-10-16T21:00:00Z&end=2026-10-16T21:00:00Z
{"pagination": {"last_page": 1, "current_page": 1, ...}, "data": [{"station_number":"NL49565","formula":"LKI","value":2,"timestamp_measured":"2026-10-16T21:00:00+00:00"}, {"station_number":"NL53020","formula":"LKI","value":1,"timestamp_measured":"2026-10-16T21:00:00+00:00"}, ... {"station_number":"NL10636","formula":"LKI","value":1,"timestamp_measured":"2026-10-16T21:00:00+00:00"}, {"station_number":"NL10643","formula":"LKI","value":4,"timestamp_measured":"2026-10-16T21:00:00+00:00"}, ...]} (97 stations) - 5
Bereken een 24-uursgemiddelde voor het WHO-advies
Het WHO-advies geldt voor een daggemiddelde, niet voor één uur. Vraag de laatste 24 uurwaarden van het dichtstbijzijnde station dat de stof meet. Maximaal een week per verzoek: vraag je meer, dan antwoordt de API met een 302 naar een adres met een stilletjes ingekorte start (wij vroegen 1 tot 16 oktober en kregen 9 tot 16 oktober). Waarden zijn voorlopig en ongevalideerd. Volgens de documentatie mag je 100 verzoeken per 5 minuten doen.
https://api.luchtmeetnet.nl/open_api/measurements?station_number=NL10643&formula=PM25&start=2026-10-15T22:00:00Z&end=2026-10-16T21:00:00Z&order_by=timestamp_measured&order_direction=desc
{"pagination": {"last_page": 1, ...}, "data": [{"station_number":"NL10643","value":1.61,"timestamp_measured":"2026-10-16T21:00:00+00:00","formula":"PM25", "timestamp_measured_start":"2026-10-16T20:00:00+00:00","timestamp_measured_end":"2026-10-16T21:00:00+00:00"}, {"station_number":"NL10643","value":0.95,"timestamp_measured":"2026-10-16T20:00:00+00:00", ...}, {"station_number":"NL10643","value":0.66,"timestamp_measured":"2026-10-16T19:00:00+00:00", ...}, ...]} (24 uren) - 6
Kijk naar sensoren van burgers in je straat
Samen Meten (RIVM) is een SensorThings-API met meer dan 12.000 meetpunten. Zoek meetpunten rond je plek met geo.distance; de afstand is in graden (0,005 graad is ongeveer 550 meter noord-zuid en 340 meter oost-west) en de geografie moet `SRID=4326` bevatten. Locations bevat ook oude standplaatsen van verplaatste of mobiele kastjes: bij de Dam in Amsterdam gaven 33 locaties binnen 0,005 graad geen enkele recente meting (vooral oude posities van Hollandse Luchten-kastjes). Bij de Dom in Utrecht: 10 meetpunten, waarvan 2 (Meetjestad) een waarde van dat uur hadden.
https://api-samenmeten.rivm.nl/v1.0/Locations?$filter=geo.distance(location,geography'SRID=4326;POINT(5.1213 52.0907)') le 0.005&$select=location&$expand=Things($select=id,name)&$top=100
{"value": [ {"location": {"coordinates": [5.121, 52.09], ...}, "Things": [{"@iot.id": 11446, "name": "SMU_350976650159780"}]}, {"location": {"coordinates": [5.123, 52.087], ...}, "Things": [{"@iot.id": 9524, "name": "MJS_1000"}]}, {"location": {"coordinates": [5.122, 52.086], ...}, "Things": [{"@iot.id": 7473, "name": "LTD_67490"}]}, ...]} (10 meetpunten) - 7
Neem alleen de gekalibreerde, recente waarde
Per meetpunt vraag je de reeks `pm25_kal` (door het RIVM gekalibreerd) met de laatste meting. Gebruik nooit de ruwe `pm25`: goedkope sensoren lopen sterk uit bij hoge luchtvochtigheid. Controleer de tijd: van de 10 meetpunten bij de Dom was de laatste waarde van 8 tussen juni 2019 en augustus 2026. Brede vragen (een $expand van Observations over veel meetpunten tegelijk, of een filter op Thing/Locations vanuit Datastreams) gaven een 504 of een leeg antwoord: werk per meetpunt, reken op ongeveer 2 seconden per aanroep. Elk meetpunt noemt het dichtstbijzijnde Luchtmeetnet-station (`pm25closecode`) en KNMI-station (`knmicode`).
https://api-samenmeten.rivm.nl/v1.0/Things(9524)/Datastreams?$filter=endswith(name,'pm25_kal')&$select=name&$expand=Observations($top=3;$orderby=phenomenonTime desc;$select=phenomenonTime,result)
{"value": [{"name": "MJS_1000-12-pm25_kal", "Observations": [{"phenomenonTime": "2026-10-16T20:00:00.000Z", "result": 2.1511495}, {"phenomenonTime": "2026-10-16T19:00:00.000Z", "result": 1.3685033}, {"phenomenonTime": "2026-10-16T18:00:00.000Z", "result": 0.4082183}]}]} Things(9524): "properties": {"project": "Meetjestad", "pm25closecode": "NL10643", "knmicode": "knmi_06260", ...} - 8
Zet er het jaargemiddelde op je plek naast
Een alert gaat over vandaag, maar de gezondheidsschade zit vooral in het jaargemiddelde. De Grootschalige Concentratiekaarten (GCN) van het RIVM geven het berekende jaargemiddelde per vakje van 1 km. Vraag de waarde op je punt met WMS GetFeatureInfo. Laagnamen zijn hoofdlettergevoelig: `conc_NO2_2025` werkt, `conc_no2_2025` geeft "LayerNotDefined". In WMS 1.3.0 met EPSG:4326 is de volgorde in bbox breedte,lengte. Buiten Nederland (op zee) komt er een lege lijst `features` terug.
https://data.rivm.nl/geo/gcn/wms?service=WMS&version=1.3.0&request=GetFeatureInfo&layers=conc_PM25_2025&query_layers=conc_PM25_2025&styles=&crs=EPSG:4326&bbox=52.0897,5.1203,52.0917,5.1223&width=3&height=3&i=1&j=1&info_format=application/json
{"type":"FeatureCollection","features":[{"type":"Feature","id":"","geometry":null, "properties":{"conc_PM25_2025":10.85}}],"totalFeatures":"unknown","numberReturned":1, "timeStamp":"2026-10-16T21:46:32.483Z","crs":null}
Hoe de bronnen samenkomen
- Samen Meten (RIVM)Luchtmeetnetvia
pm25closecode / no2closecode (stationsnummer, bv. NL10643)Elk Samen Meten-meetpunt noemt zijn dichtstbijzijnde officiële station. MJS_1000 bij de Dom wijst naar NL10643 (Utrecht-Griftpark). Haal daar hetzelfde uur op om te zien of de sensor ongeveer klopt voordat je er een melding op baseert.
- LuchtmeetnetGrootschalige concentratiekaarten (GCN)via
coördinaten (WGS84)Dezelfde breedte en lengte die je aan /concentrations geeft, gebruik je voor GetFeatureInfo op de GCN-kaart. Zo staan de verwachting voor vandaag en het jaargemiddelde op dezelfde plek naast elkaar.
- Samen Meten (RIVM)KNMI daggegevens van het weervia
knmicode (knmi_06260 = station 260, De Bilt)Leg achteraf de windsnelheid (FH per uur, FG per dag) naast je meldingen: windstille avonden geven vaker hoge waarden. Let op: op 16 oktober 2026 liepen de KNMI-dag- en uurgegevens tot en met 14 oktober, dus voor de alert zelf is dit te laat, voor de analyse niet.
Laat AI het bouwen
Plak deze prompt in Claude, ChatGPT, Cursor of Lovable. Hij noemt de echte endpoints, de koppelsleutels en de valkuilen, zodat de assistent meteen werkende code schrijft. Controleer de uitkomst altijd met de bron zelf.
Bouw een luchtkwaliteit-alert voor één adres in Nederland: een klein Python-script (of Node) dat elk uur draait en een
pushmelding stuurt (bijvoorbeeld via ntfy.sh of Telegram) als de lucht slecht is of in de komende uren slecht wordt.
Daarna een eenvoudige webpagina met de stand van nu. Alles zonder API-sleutel. Stuur een eerlijke User-Agent mee
("mijn-project/1.0 (+jij@voorbeeld.nl)").
Bronnen en endpoints (allemaal getest op 16 oktober 2026):
1. Verwachting op je punt (Luchtmeetnet): GET https://api.luchtmeetnet.nl/open_api/concentrations?formula=LKI&latitude=52.0907&longitude=5.1213
formula kan LKI, NO2, O3 of PM10 zijn. Antwoord {"data":[{"formula","value","timestamp_measured"}]}, ongeveer 20 uren
vanaf het laatste hele uur, waarden met decimalen, in blokken van 3 uur gelijk. Geen paginering.
2. Index van alle stations voor één uur: GET https://api.luchtmeetnet.nl/open_api/lki?start=2026-10-16T21:00:00Z&end=2026-10-16T21:00:00Z
(97 stations in één antwoord). Uurwaarden: /open_api/measurements?formula=NO2&start=…&end=… (zelfde uur = alle stations),
of met station_number voor één station.
3. Stations: /open_api/stations?page=1..5 (alleen number en location), details met ligging, soort en gemeten stoffen in
/open_api/stations/<nummer>, bv. NL10643 = Utrecht-Griftpark. Haal namen altijd hier vandaan.
4. Drempels per stof: /open_api/components/<NO2|O3|PM25|PM10|LKI> geeft `limits`: per indexgetal 1 tot 11 een lowerband en
upperband in µg/m³. Negeer de kleuren daarin (inconsistent).
5. Sensoren van burgers (RIVM Samen Meten, SensorThings):
GET https://api-samenmeten.rivm.nl/v1.0/Locations?$filter=geo.distance(location,geography'SRID=4326;POINT(<lon> <lat>)') le 0.005&$expand=Things($select=id,name)&$top=100
en per Thing: /v1.0/Things(<id>)/Datastreams?$filter=endswith(name,'pm25_kal')&$expand=Observations($top=1;$orderby=phenomenonTime desc)
6. Jaargemiddelde (RIVM GCN, 1 km): WMS GetFeatureInfo op https://data.rivm.nl/geo/gcn/wms met layers=conc_NO2_2025
(ook conc_PM25_2025, conc_PM10_2025), version=1.3.0, crs=EPSG:4326, bbox=<lat-0.001>,<lon-0.001>,<lat+0.001>,<lon+0.001>,
width=3, height=3, i=1, j=1, info_format=application/json. Waarde in features[0].properties[<laagnaam>].
Drempels voor de melding:
- LKI van het RIVM: 1 tot 3 goed, 4 tot 6 matig, 7 en 8 onvoldoende, 9 en 10 slecht, 11 zeer slecht. Melding vanaf 7
(instelbaar; mensen met longziekten willen misschien al bij 6).
- Ozon: EU-informatiedrempel 180 µg/m³ (uurgemiddelde), alarmdrempel 240.
- WHO-advies 2021: 24 uur NO2 25, PM2,5 15, PM10 45; jaar NO2 10, PM2,5 5, PM10 15 µg/m³. EU-jaarnorm NO2 40, PM2,5 25.
Valkuilen die je moet afvangen:
- Alle tijden zijn UTC. timestamp_measured is het eind van het uur (21:00 = 20:00 tot 21:00). Toon Nederlandse tijd.
- Luchtmeetnet: maximaal 7 dagen per verzoek; vraag je meer, dan volgt een 302 met een stil ingekorte start. Volg
redirects en controleer het bereik. Maximaal 100 verzoeken per 5 minuten. Uurwaarden zijn voorlopig.
- Het laatste uur is soms nog niet binnen: probeer het uur ervoor als de lijst leeg is.
- De LKI van een station is het maximum over de stoffen die het meet; een station zonder ozon scoort vaak lager.
- Niet elk station meet elke stof: kijk in components van /stations/<nummer>.
- Samen Meten: gebruik alleen pm25_kal en pm10_kal, nooit de ruwe pm25/pm10. Neem alleen waarden van de laatste 3 uur
(veel meetpunten zijn al jaren stil). Locations bevat oude posities van verplaatste kastjes. geo.distance rekent in graden.
$top maximaal 200. Brede $expand-vragen geven 504 of een leeg antwoord: per meetpunt opvragen, en cachen.
- Geef een sensor nooit alleen de doorslag: meld "volgens sensoren in de buurt" en vergelijk met pm25closecode.
- GCN: laagnamen hoofdlettergevoelig; buiten de kaart een lege features-lijst.
- Stuur niet elk uur dezelfde melding: alleen bij een overgang van OK naar ALERT, en één keer "weer goed".
Uitkomst: een script met instelbare plek en drempels, een melding met een korte zin ("Morgen 14:00 verwacht: LKI 7,
onvoldoende. Vooral ozon."), en een pagina met de verwachting als lijn, de stations op afstand en het jaargemiddelde
naast het WHO-advies. Vermeld onder alles: bron Luchtmeetnet en RIVM, voorlopige gegevens.
Probeer het zelf
De kern van het project in een klein script. Nodig: Python 3.10+ en requests. Getest op 2026-10-16; hieronder de echte uitvoer van die proefdraai.
# Luchtkwaliteit-alert voor je eigen plek. Geen sleutel nodig.
# Bronnen: Luchtmeetnet (verwachting op je punt + metingen van stations), RIVM Samen Meten (sensoren van burgers),
# RIVM GCN (berekend jaargemiddelde 2025). Luchtmeetnet staat 100 verzoeken per 5 minuten toe: dit script doet er ~16.
import math, requests
from datetime import datetime, timedelta, timezone
UA = {"User-Agent": "mijn-project/1.0 (+jij@voorbeeld.nl)"}
LON, LAT, STAD = 5.1213, 52.0907, "Utrecht" # de Domtoren; zet hier je eigen plek
LM = "https://api.luchtmeetnet.nl/open_api"
SM = "https://api-samenmeten.rivm.nl/v1.0"
# Drempels met herkomst. LKI-banden en ozon-informatiedrempel staan in de API zelf (/components/<stof>).
LKI_ALERT = 7 # LKI 7 of hoger: 'onvoldoende' of slechter (5 klassen, schaal 1 tot 11, RIVM)
O3_INFO = 180 # EU-informatiedrempel ozon, uurgemiddelde (ook genoemd in /components/O3)
WHO_24U = {"NO2": 25, "PM25": 15, "PM10": 45} # WHO-advies 2021, 24-uursgemiddelde in µg/m³
WHO_JAAR = {"NO2": 10, "PM25": 5, "PM10": 15} # WHO-advies 2021, jaargemiddelde in µg/m³
def get(url, **params):
r = requests.get(url, params=params, headers=UA, timeout=60)
r.raise_for_status()
return r.json()
def km(lon1, lat1, lon2, lat2):
return 6371 * math.hypot(math.radians(lon2 - lon1) * math.cos(math.radians(lat1)), math.radians(lat2 - lat1))
def iso(t):
return t.strftime("%Y-%m-%dT%H:%M:%SZ")
meldingen = []
# 1. Verwachting op je punt (huidig uur + ~19 uur vooruit), formula LKI, NO2, O3 of PM10.
verw = {f: get(f"{LM}/concentrations", formula=f, latitude=LAT, longitude=LON)["data"] for f in ["LKI", "O3"]}
lki = verw["LKI"]
piek = max(lki, key=lambda x: x["value"])
print(f"1. Verwachting op {LAT}, {LON} (Luchtmeetnet, model)")
print(f" LKI nu {lki[0]['value']:.1f} ({lki[0]['timestamp_measured'][:16]} UTC), hoogste {piek['value']:.1f} om {piek['timestamp_measured'][:16]} UTC")
o3max = max(x["value"] for x in verw["O3"])
print(f" ozon hoogste verwachte uurwaarde {o3max:.0f} µg/m³ (informatiedrempel {O3_INFO})")
if piek["value"] >= LKI_ALERT: meldingen.append(f"verwachte LKI {piek['value']:.1f} op je plek")
if o3max >= O3_INFO: meldingen.append(f"verwachte ozon {o3max:.0f} µg/m³")
# 2. Stations in je stad: namen en ligging uit /stations/<nummer> (de lijst heeft alleen nummer en locatie-tekst).
lijst, pagina = [], 1
while True:
j = get(f"{LM}/stations", page=pagina)
lijst += j["data"]
if pagina >= j["pagination"]["last_page"]: break
pagina += 1
stations = {}
for s in lijst:
if s["location"].replace(" ", "").startswith(STAD + "-"):
d = get(f"{LM}/stations/{s['number']}")["data"]
lon, lat = d["geometry"]["coordinates"]
stations[s["number"]] = {"naam": d["location"], "soort": d["type"], "km": km(LON, LAT, lon, lat), "stoffen": d["components"]}
# Laatste volle uur waarvoor de LKI er is (één verzoek geeft alle ~97 stations).
uur = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
for _ in range(6):
alle_lki = get(f"{LM}/lki", start=iso(uur), end=iso(uur))["data"]
if alle_lki: break
uur -= timedelta(hours=1)
lki_st = {x["station_number"]: x["value"] for x in alle_lki}
uurwaarde = {}
for f in ["NO2", "PM25", "O3"]:
for x in get(f"{LM}/measurements", formula=f, start=iso(uur), end=iso(uur))["data"]:
uurwaarde[(x["station_number"], f)] = x["value"]
print(f"\n2. Stations in {STAD}, uur tot {uur:%d-%m %H:%M} UTC (voorlopige uurwaarden, µg/m³)")
print(f" {'afstand':>7} {'station':30} {'soort':10} {'LKI':>3} {'NO2':>5} {'PM2,5':>5} {'O3':>5}")
dichtbij = sorted(stations.items(), key=lambda kv: kv[1]["km"])
for nr, s in dichtbij:
w = [uurwaarde.get((nr, f)) for f in ["NO2", "PM25", "O3"]]
tekst = [" -" if v is None else f"{v:5.1f}" for v in w]
print(f" {s['km']:5.1f} km {s['naam']:30} {s['soort']:10} {lki_st.get(nr, '-'):>3} {' '.join(tekst)}")
for nr, s in dichtbij[:3]:
if lki_st.get(nr, 0) >= LKI_ALERT: meldingen.append(f"LKI {lki_st[nr]} op {s['naam']}")
# 24-uursgemiddelde op het dichtstbijzijnde station dat de stof meet, tegen het WHO-advies.
print("\n 24-uursgemiddelde, dichtstbijzijnde station dat de stof meet:")
for f in ["NO2", "PM25"]:
nr, s = next((nr, s) for nr, s in dichtbij if f in s["stoffen"])
m = get(f"{LM}/measurements", station_number=nr, formula=f, start=iso(uur - timedelta(hours=23)), end=iso(uur))["data"]
w = [x["value"] for x in m if x["value"] is not None]
gem = sum(w) / len(w)
print(f" {f:4} {s['naam']:30} {gem:5.1f} µg/m³ over {len(w)} uur (WHO-advies 24 uur: {WHO_24U[f]})")
if gem > WHO_24U[f]: meldingen.append(f"{f} 24-uursgemiddelde {gem:.1f} boven WHO-advies op {s['naam']}")
# 3. Sensoren van burgers binnen ~500 m (geo.distance in graden). Locations bevat ook oude plekken van verplaatste
# kastjes: houd alleen reeksen met een gekalibreerde PM2,5-waarde van de laatste 3 uur over. ~2 s per meetpunt.
locs = get(f"{SM}/Locations", **{"$filter": f"geo.distance(location,geography'SRID=4326;POINT({LON} {LAT})') le 0.005",
"$expand": "Things($select=id,name)", "$top": 100})["value"]
things = {t["@iot.id"]: t["name"] for loc in locs for t in loc["Things"]}
grens = datetime.now(timezone.utc) - timedelta(hours=3)
pm25 = []
for tid, naam in things.items():
ds = get(f"{SM}/Things({tid})/Datastreams", **{"$filter": "endswith(name,'pm25_kal')",
"$expand": "Observations($top=1;$orderby=phenomenonTime desc;$select=phenomenonTime,result)"})["value"]
for d in ds:
o = (d.get("Observations") or [None])[0]
if o and o["result"] is not None and datetime.fromisoformat(o["phenomenonTime"].replace("Z", "+00:00")) >= grens:
pm25.append((naam, o["result"], o["phenomenonTime"][11:16]))
print(f"\n3. Samen Meten binnen ~500 m: {len(things)} meetpunten, {len(pm25)} met gekalibreerd PM2,5 van de laatste 3 uur")
for naam, w, t in pm25:
print(f" {naam:22} {w:5.1f} µg/m³ om {t} UTC (indicatief, geen officiële meting)")
# 4. Berekend jaargemiddelde 2025 op je plek (RIVM GCN, 1 x 1 km). Laagnamen zijn hoofdlettergevoelig.
print("\n4. Jaargemiddelde 2025 op je plek (RIVM GCN):")
for f in ["NO2", "PM25", "PM10"]:
laag = f"conc_{f}_2025"
p = get("https://data.rivm.nl/geo/gcn/wms", service="WMS", version="1.3.0", request="GetFeatureInfo", layers=laag,
query_layers=laag, styles="", crs="EPSG:4326", bbox=f"{LAT-.001},{LON-.001},{LAT+.001},{LON+.001}",
width=3, height=3, i=1, j=1, info_format="application/json")["features"]
if not p:
print(f" {f:4} geen waarde (punt buiten de kaart)"); continue
p = p[0]["properties"][laag]
print(f" {f:4} {p:5.1f} µg/m³ (WHO-advies jaar: {WHO_JAAR[f]}, {p / WHO_JAAR[f]:.1f} keer zo hoog)")
print("\n" + ("ALERT: " + "; ".join(meldingen) if meldingen else f"OK: geen drempel overschreden (LKI-alert vanaf {LKI_ALERT})"))
1. Verwachting op 52.0907, 5.1213 (Luchtmeetnet, model)
LKI nu 4.1 (2026-10-16T21:00 UTC), hoogste 4.7 om 2026-10-17T12:00 UTC
ozon hoogste verwachte uurwaarde 75 µg/m³ (informatiedrempel 180)
2. Stations in Utrecht, uur tot 16-10 21:00 UTC (voorlopige uurwaarden, µg/m³)
afstand station soort LKI NO2 PM2,5 O3
1.3 km Utrecht-Griftpark Municipal 4 4.1 1.6 54.5
1.6 km Utrecht-Kardinaal de Jongweg Traffic 1 5.0 2.0 -
2.6 km Utrecht-Constant Erzeijstraat Traffic 4 5.9 - 59.5
24-uursgemiddelde, dichtstbijzijnde station dat de stof meet:
NO2 Utrecht-Griftpark 17.0 µg/m³ over 24 uur (WHO-advies 24 uur: 25)
PM25 Utrecht-Griftpark 5.2 µg/m³ over 24 uur (WHO-advies 24 uur: 15)
3. Samen Meten binnen ~500 m: 10 meetpunten, 2 met gekalibreerd PM2,5 van de laatste 3 uur
MJS_1000 2.2 µg/m³ om 20:00 UTC (indicatief, geen officiële meting)
MJS_791 1.4 µg/m³ om 20:00 UTC (indicatief, geen officiële meting)
4. Jaargemiddelde 2025 op je plek (RIVM GCN):
NO2 18.5 µg/m³ (WHO-advies jaar: 10, 1.8 keer zo hoog)
PM25 10.8 µg/m³ (WHO-advies jaar: 5, 2.2 keer zo hoog)
PM10 18.5 µg/m³ (WHO-advies jaar: 15, 1.2 keer zo hoog)
OK: geen drempel overschreden (LKI-alert vanaf 7)
Waar het misgaat
- Meer dan een week vragen bij Luchtmeetnet geeft geen fout maar een 302 naar een adres met een ingekorte start. Wie redirects blind volgt, denkt twee weken te hebben en heeft er één. Knip lange periodes zelf in weken.
- Alle tijden zijn UTC en timestamp_measured is het eind van het meetuur. In de zomertijd loopt Nederland twee uur voor: een melding 'om 21:00' moet 23:00 zeggen.
- De LKI van een station is het hoogste indexgetal van de stoffen die het meet. Een verkeersstation zonder ozon (Utrecht-Kardinaal de Jongweg: LKI 1) lijkt dan schoner dan een parkstation met ozon (Griftpark: LKI 4). Gebruik voor de alert de verwachting op je punt of kijk per stof.
- Samen Meten telt elk meetpunt dat ooit is aangemeld en elke oude standplaats. Bij de Dam in Amsterdam gaven 33 locaties binnen 500 meter geen enkele recente waarde; bij de Dom in Utrecht 2 van de 10. Filter op een meting van de laatste uren en gebruik alleen pm25_kal en pm10_kal.
- Samen Meten wordt traag of valt om bij brede vragen: een $expand van Observations over 200 locaties gaf na 30 seconden een 504, een filter op Thing/Locations vanuit Datastreams een leeg antwoord. Vraag per meetpunt (ongeveer 2 seconden) en bewaar de lijst met actieve meetpunten een dag in een cache.
- GCN-laagnamen zijn hoofdlettergevoelig (conc_NO2_2025, niet conc_no2_2025) en de bbox in WMS 1.3.0 met EPSG:4326 is breedte,lengte. Een punt op zee geeft een lege features-lijst: vang dat af.
- Een uurwaarde is geen jaargemiddelde. Een 'OK' op een winderige avond zegt niets over de jaarnorm: bij de Dom was alles laag, maar het jaargemiddelde PM2,5 (10,8) is ruim twee keer het WHO-advies. Laat beide zien.
Verder bouwen
- Pushmelding met ntfy.sh of een Telegram-bot, één keer per overgang (OK naar ALERT en terug), met de verwachte piek en de stof die hem veroorzaakt (vraag /concentrations ook voor NO2, O3 en PM10 en kijk welke het hoogste indexgetal geeft).
- Klopt de sensor van de buren? Zet een week pm25_kal van een meetpunt naast het station uit pm25closecode (MJS_1000 naast NL10643) en bereken de gemiddelde afwijking. Gebruik alleen sensoren die goed meelopen.
- Windstil is vies: haal achteraf de uurwind van het KNMI-station uit knmicode op (uurgegevens, FH in 0,1 m/s) en laat zien hoe vaak een alert samenviel met weinig wind.
- Kaart van je stad: de GCN-laag als achtergrond (WMS GetMap), de stations met hun LKI van dit uur en de actieve sensoren als stippen.
Meer met deze data
Meetpunten in Samen Meten per project: het meest Luftdaten (6.103 meetpunten), dan Palmes en Luchtclub
Meetpunten in Samen Meten per project, oktober 2026, in meetpunten. Totaal: 12.439; de grootste 12 van 60 categorieën staan in de grafiek. Luftdaten is 49% van het totaal. Een meetpunt is een sensorkastje of, bij de Palmes-projecten, een buisje dat stikstofdioxide meet. Geteld zijn alle meetpunten in de API, ook die niet meer meten.
Stikstofdioxide in Amsterdam: het hoogst langs de grote wegen, het laagst aan de randen van de stad
Het berekende jaargemiddelde NO2 over 2024 uit de kaartdienst van het RIVM, voor een gebied van 16 bij 12 kilometer rond het centrum. Volgens de legenda van de dienst is blauw minder dan 14 microgram per kubieke meter en grijs 14 tot 16; crème en geel 16 tot 20, oranje 20 tot 30 en rood 30 of meer. De oranje en rode lijnen zijn de drukke wegen. Het is een rekenkaart, geen meting: voor een waarde op je eigen adres vraag je de kaart op dat punt op (zie het script).
De bronnen4

LuchtmeetnetUitgewerkt
Metingen van stikstofdioxide, fijnstof en ozon, elk uur, van zo'n honderd meetstations in heel Nederland.

Samen Meten (RIVM)
Burgers, gemeenten en onderzoekers hangen zelf sensorkastjes op; het RIVM verzamelt hun metingen in het Samen Meten-dataportaal en maakt voor fijnstof gekalibreerde reeksen. Op 16 oktober 2026 stonden er 12.419 meetpunten in de API, met 56.391 meetreeksen: fijnstof (PM10, PM2,5), stikstofdioxide, ammoniak, temperatuur, luchtvochtigheid en luchtdruk, per uur. De grootste groep is Luftdaten (6.099 meetpunten).

Grootschalige concentratiekaarten (GCN)
Berekende jaargemiddelden van stikstofdioxide en fijnstof voor heel Nederland, in vakjes van een kilometer. Waar Luchtmeetnet meet, rekent GCN tussen de meetpunten in.

KNMI daggegevens van het weerUitgewerkt
Dagwaarden van ruim vijftig weerstations: temperatuur, neerslag, zon en wind. De reeks van De Bilt begint in 1901.
Meer om te bouwen
Alert als de rivier bij jou te hoog of te laag staatAlert
Klimaatrisico-check voor een adresTool
Natuur in je buurt: welke soorten zijn er gezien en hoe groen is het?App
Natuurbrandkaart: risico van vandaag, hittepunten en verbrande natuurKaart
Nederland naast Europa: een vergelijker voor elke indicatorTool
Weer-app voor je eigen plek: warmer, natter of zonniger dan normaal?App