NL Weather Integration (forecast, observations, rain radar + nowcast graph and warnings)

Thanks for this update. It is used in a garden-sprinkler automation that starts sprinkling at 4:00 am under certain conditions such as: has it rained before (x days), is rain expected today or tomorrow and will start sprinkle accordingly and a dedicated dashboard with features from this update V0.10.0. AI integration helped me: Claude says: ¨Summary of all changes today:

  • NL Weather updated to v0.10.0 (via HACS, done by you)

  • Nowcast chart added to irrigation dashboard (Irrigation view + Precipitation radar view)

  • binary_sensor.weer_thuis_precipitation_forecasted added to the "Rain forecast" entities card

  • Nowcast chart added to Indoor and Outdoor Climate (after precipitation radar)¨

2 Likes

This week I switched from using the KNMI integration (the one with the intermediary weerlive API, I was having issues with receiving incorrect data) to this one and it seems to work great so far.
I did not yet have a KNMI API/developer account and since they’ve closed online registrations I had to send an email to KNMI. This was painless btw, they created an account the first business day after I sent it.

Thank you Paul for making and sharing this! :clap:

—
In case it may be of any help to someone. I’ve used the text below to request the account at [email protected]

Geachte heer/mevrouw,

Graag zou ik een account willen aanvragen voor het Developer Portal (developer.dataplatform.knmi.nl), aangezien zelfregistratie momenteel tijdelijk is uitgeschakeld.

Naam: your name
Organisatie: particulier / hobbyist (geen organisatie)
Reden voor aanvraag: Ik gebruik de KNMI Data Platform API's (EDR API, WMS en Notification Service) voor een persoonlijk Home Assistant dashboard, om actuele weersvoorspellingen en waarnemingen te tonen via de community-integratie 'NL Weather'. Het gaat hier om niet-commercieel, persoonlijk gebruik met beperkt datavolume.

Alvast hartelijk dank voor het in behandeling nemen van mijn verzoek.

Met vriendelijke groet,

Naam
Email adres

1 Like

For those who are already using Plotly for graphs (instead of Apex), you can use this code to create the nowcast graph with Plotly:

              - type: custom:plotly-graph
                title: Neerslag komende uren
                fn: |
                  $fn ({ hass, vars }) => {
                    const forecast = hass.states['binary_sensor.weer_thuis_neerslag_verwacht']?.attributes?.forecast || [];
                    vars.x = [];
                    vars.y = [];
                    vars.hover = [];
                    vars.ymax = 0;

                    forecast.map(e => {
                      const t = new Date(e.datetime).getTime();
                      const p = Number(e.precipitation || 0);
                      if (!Number.isFinite(t) || !Number.isFinite(p)) return;

                      vars.x.push(t);
                      vars.y.push(p);
                      vars.ymax = Math.max(vars.ymax, p);
                      vars.hover.push(
                        new Date(t).toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' }) +
                        ': <b>' + p.toFixed(1) + '</b> mm/u'
                      );
                    });

                    vars.ymax = Math.max(1, Math.ceil(vars.ymax * 2) / 2);
                    vars.xrange = vars.x.length
                      ? [vars.x[0], vars.x[vars.x.length - 1]]
                      : [Date.now(), Date.now() + (4 * 3600000)];
                  }
                layout:
                  height: 220
                  margin:
                    l: 62
                    r: 15
                    t: 15
                    b: 48
                  paper_bgcolor: rgba(0,0,0,0)
                  plot_bgcolor: rgba(0,0,0,0)
                  showlegend: false
                  yaxis:
                    fixedrange: true
                    rangemode: tozero
                    range: $fn ({ vars }) => [0, vars.ymax]
                    tickformat: ".1f"
                    title:
                      text: mm/u
                  xaxis:
                    fixedrange: true
                    range: $fn ({ vars }) => vars.xrange
                    tickformat: "%H:%M"
                config:
                  displayModeBar: false
                entities:
                  - entity: ""
                    name: Neerslag
                    showlegend: false
                    x: $ex vars.x
                    "y": $ex vars.y
                    customdata: $ex vars.hover
                    hovertemplate: "%{customdata}<extra></extra>"
                    type: scatter
                    mode: lines
                    line:
                      color: "#38bdf8"
                      width: 3
                      shape: spline
                    fill: tozeroy
                    fillcolor: rgba(56,189,248,0.22)

2 Likes

Thanks a lot Hmmbob! :clap:
Took me a while to get all spaces right I guess your output needs to be added to the complete dashboard view and not the single card.
For those who want to paste just the card this is my copy and don't forget to rename the binary-sensor to the one you are using in line 5

type: custom:plotly-graph
title: Neerslag komende uren
fn: |
  $fn ({ hass, vars }) => {
    const forecast = hass.states['binary_sensor.weer_thuis_neerslag_verwacht']?.attributes?.forecast || [];
    vars.x = [];
    vars.y = [];
    vars.hover = [];
    vars.ymax = 0;

    forecast.map(e => {
      const t = new Date(e.datetime).getTime();
      const p = Number(e.precipitation || 0);
      if (!Number.isFinite(t) || !Number.isFinite(p)) return;

      vars.x.push(t);
      vars.y.push(p);
      vars.ymax = Math.max(vars.ymax, p);
      vars.hover.push(
        new Date(t).toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' }) +
        ': <b>' + p.toFixed(1) + '</b> mm/u'
      );
    });

    vars.ymax = Math.max(1, Math.ceil(vars.ymax * 2) / 2);
    vars.xrange = vars.x.length
      ? [vars.x[0], vars.x[vars.x.length - 1]]
      : [Date.now(), Date.now() + (4 * 3600000)];
  }
layout:
  height: 220
  margin:
    l: 62
    r: 15
    t: 15
    b: 48
  paper_bgcolor: rgba(0,0,0,0)
  plot_bgcolor: rgba(0,0,0,0)
  showlegend: false
  yaxis:
    fixedrange: true
    rangemode: tozero
    range: $fn ({ vars }) => [0, vars.ymax]
    tickformat: .1f
    title:
      text: mm/u
  xaxis:
    fixedrange: true
    range: $fn ({ vars }) => vars.xrange
    tickformat: "%H:%M"
config:
  displayModeBar: false
entities:
  - entity: ""
    name: Neerslag
    showlegend: false
    x: $ex vars.x
    "y": $ex vars.y
    customdata: $ex vars.hover
    hovertemplate: "%{customdata}<extra></extra>"
    type: scatter
    mode: lines
    line:
      color: "#38bdf8"
      width: 3
      shape: spline
    fill: tozeroy
    fillcolor: rgba(56,189,248,0.22)

@Hmmbob
My X-axis only shows the hours up to current time and no future prediction. In your picture you got almost 4 hours while for me it is just 1 hour. Scrolling to the right makes the rest appear.
If I add 'hours_to_show: 4' it just adds time on the left without prediction off course.
If I add 'period: auto' no difference.
Any idea what is happening?

Oops, still had a whitespace in the xaxis part, corrected that one and now it's better, phffw.

1 Like

Here's my version of @Hmmbob's graph. :raising_hands:
Added some horizontal markings and made it look more like @aex351's ha-neerslag-card.
I also noticed the graph had its 'now' in the middle of a 4h width, so I moved the range from (now - 10 min) till the last data point.
Some other minor adjusments

type: custom:plotly-graph
title: Verwachte neerslag

fn: |
  $fn ({ hass, vars }) => {
    const forecast =
      hass.states['binary_sensor.weer_home_precipitation_forecasted']?.attributes?.forecast || [];

    vars.x = [];
    vars.y = [];
    vars.hover = [];
    vars.ymax = 0;

    forecast.forEach((e) => {
      const t = new Date(e.datetime).getTime();
      const p = Number(e.precipitation || 0);

      if (!Number.isFinite(t) || !Number.isFinite(p)) return;

      vars.x.push(t);
      vars.y.push(p);
      vars.ymax = Math.max(vars.ymax, p);
      vars.hover.push(
        `<b>${new Date(t).toLocaleTimeString('nl-NL', {
          hour: '2-digit',
          minute: '2-digit'
        })}</b><br>KNMI: ${p.toFixed(2)} mm/u`
      );
    });

    vars.ymax = Math.max(0.6, vars.ymax * 1.2);

    const startPunt = Date.now() - 600000;
    const eindPunt = vars.x.length
      ? vars.x[vars.x.length - 1]
      : Date.now() + 2 * 3600000;

    vars.xrange = [startPunt, eindPunt];
  }

layout:
  height: 240
  margin:
    l: 62
    r: 20
    t: 15
    b: 56
  paper_bgcolor: transparent
  plot_bgcolor: transparent
  showlegend: false

  yaxis:
    fixedrange: true
    rangemode: tozero
    range: $fn ({ vars }) => [0, vars.ymax]
    tickformat: .1f
    title:
      text: Regen (mm/u)

  xaxis:
    fixedrange: true
    range: $fn ({ vars }) => vars.xrange
    tickformat: "%H:%M"
    tickangle: -45
    tickmode: linear
    dtick: 600000

  shapes:
    - type: line
      xref: paper
      x0: 0
      x1: 1
      yref: y
      y0: 0.4
      y1: 0.4
      line:
        color: "#9ca3af"
        width: 1.5

    - type: line
      xref: paper
      x0: 0
      x1: 1
      yref: y
      y0: 2
      y1: 2
      line:
        color: "#9ca3af"
        width: 1.5

    - type: line
      xref: paper
      x0: 0
      x1: 1
      yref: y
      y0: 5
      y1: 5
      line:
        color: "#9ca3af"
        width: 1.5

  annotations:
    - text: licht
      xref: paper
      x: 1
      yref: y
      y: 0.45
      xanchor: right
      yanchor: bottom
      showarrow: false
      bgcolor: "#2b2b2b"
      bordercolor: "#2b2b2b"
      borderpad: 0
      borderwidth: 1
      font:
        size: 9
        color: white
        weight: bold

    - text: matig
      xref: paper
      x: 1
      yref: y
      y: 2.05
      xanchor: right
      yanchor: bottom
      showarrow: false
      bgcolor: "#2b2b2b"
      bordercolor: "#2b2b2b"
      borderpad: 0
      borderwidth: 1
      font:
        size: 9
        color: white
        weight: bold

    - text: zwaar
      xref: paper
      x: 1
      yref: y
      y: 4.95
      xanchor: right
      yanchor: top
      showarrow: false
      bgcolor: "#2b2b2b"
      bordercolor: "#2b2b2b"
      borderpad: 0
      borderwidth: 1
      font:
        size: 9
        color: white
        weight: bold

config:
  displayModeBar: false

entities:
  - entity: ""
    name: Neerslag
    showlegend: false
    x: $ex vars.x
    y: $ex vars.y
    customdata: $ex vars.hover
    hovertemplate: "%{customdata}<extra></extra>"
    hoverlabel:
      bgcolor: "#2b2b2b"
      font:
        color: white
      bordercolor: "#2b2b2b"
    type: scatter
    mode: lines
    line:
      color: "#008dfd"
      width: 2
      shape: spline
    fill: tozeroy
    fillcolor: "#008dfd2E"


('now' was at 12:00)

3 Likes

Thanks! Looks nice. Made me try out the Plotly card for the first time. I've made two more adjustments:

  shapes:
    - type: line
      xref: x
      x0: $fn () => Date.now()
      x1: $fn () => Date.now()
      yref: paper
      y0: 0
      y1: 1
      line:
        color: "#f44336"
        width: 2
        dash: dot
...
  annotations:
    - text: <b>Nu</b>
      xref: x
      x: $fn () => Date.now()
      yref: paper
      "y": 1
      yanchor: top
      xanchor: left
      xshift: 4
      textangle: -90
      showarrow: false
      font:
        size: 11
        color: "#f44336"

To show a dotted vertical line on "Now".

I've also just released version 0.12.0. This includes an update by @Hmmbob to make all weather alerts available as attribute to the alert sensor. Last week there were many weather alerts active at the same time, and the integration wasn't handling that very well. With the new alerts attribute on the alert sensor you can now print them. For example using the markdown chart:

type: markdown
content: |
  {% set alerts = state_attr('sensor.weer_home_alerts', 'alerts') or [] %}
  {% set titles = {'red': 'Code rood', 'orange': 'Code oranje', 'yellow': 'Code geel'} %}
  {% set levels = {'red': 'error', 'orange': 'warning', 'yellow': 'info'} %}
  {% if alerts %}
  {% for alert in alerts %}
  {% set code = alert.code | lower %}
  {% set title = titles.get(code, 'Weerwaarschuwing') %}
  {% set level = levels.get(code) %}
  {% set item = alert.description | trim %}
  {% set prefix = title | lower ~ ' voor ' %}
  {% set prefix_length = prefix | length %}
  {% set detail = item[prefix_length:] | trim if (item | lower).startswith(prefix) else item %}
  <ha-alert alert-type="{{ level }}" title="{{ title }}">
    {{ detail[:1] | upper }}{{ detail[1:] }}{{ '.' if not detail.endswith('.') else '' }}
  </ha-alert>
  {% endfor %}
  {% else %}
  <ha-alert alert-type="success" title="Waarschuwingen">
    Geen actieve waarschuwingen.
  </ha-alert>
  {% endif %}

Thanks @Hmmbob!

8 Likes

Nice, this allowed me to remove several scrape sensors. Thanks! :+1:

This incorporated to my custom:button-card and with fictional data it looks like attached now. It's barely visible here but it's a rounded card on a black background (it's inside a Bubble pop-up). I display 'none' as 'green' but the alert_active binary sensor could also be used to hide the card.

Card code if it's helpful to anyone:

    card:
      type: custom:button-card
      entity: sensor.weer_thuis_waarschuwingen
      show_name: true
      show_state: false
      show_icon: true
      name: |-
        [[[
          var c = states['sensor.weer_thuis_waarschuwingscode']?.state || 'none';
          var alerts = states['sensor.weer_thuis_waarschuwingen']?.attributes?.alerts || [];

          var kleurMap = {none: 'var(--green)', yellow: 'var(--yellow)', orange: 'var(--orange)', red: 'var(--red)'};
          var naamMap = {none: 'groen', yellow: 'geel', orange: 'oranje', red: 'Rood'};
          var titelMap = {red: 'Code rood', orange: 'Code oranje', yellow: 'Code geel'};

          function kleurNaam(code) {
            return kleurMap[code] || 'grey';
          }

          var html = '<div style="color:' + kleurNaam(c) + '; font-weight:500; text-align:left;">KNMI code: ' + (naamMap[c] || c) + '</div>';

          if (alerts.length > 0) {
            html += '<div style="height:10px;"></div>' +
                    '<div style="color:var(--contrast20); font-style: italic; font-weight:500; text-align:left; font-size:15px;">Waarschuwing' + (alerts.length > 1 ? 'en' : '') + ':</div>';
          }

          alerts.forEach(function (alert) {
            var code = (alert.code || '').toLowerCase();
            var titel = titelMap[code] || 'Weerwaarschuwing';
            var item = (alert.description || '').trim();
            var prefix = titel.toLowerCase() + ' voor ';
            var detail = item.toLowerCase().startsWith(prefix) ? item.substring(prefix.length).trim() : item;

            if (detail.length > 0) {
              detail = detail.charAt(0).toUpperCase() + detail.slice(1);
              if (!detail.endsWith('.')) {
                detail += '.';
              }
            }

            html += '<div style="height:3px;"></div>' + '<div style="color:' + kleurNaam(code) + '; font-weight:500; text-align:left; font-size:14px;">' + titel + '</div>' +
                    '<div style="color:var(--contrast20); font-weight:400; text-align:left; margin-bottom:4px; font-size:12px;">' + detail + '</div>';
          });

          return html;
        ]]]
      icon: mdi:alert-circle
      tap_action:
        action: more-info
      styles:
        card:
          - border-radius: 24px
          - padding: 12px 12px
        grid:
          - grid-template-areas: "\"i n\""
          - grid-template-columns: 40px 1fr
          - align-items: start
        icon:
          - color: |
              [[[
                var c = states['sensor.weer_thuis_waarschuwingscode']?.state || 'none';
                var kleurMap = {none: 'var(--green)', yellow: 'var(--yellow)', orange: 'var(--orange)', red: 'var(--red)'};
                return kleurMap[c] || 'grey';
              ]]]
          - width: 24px
          - align-self: start
          - margin-top: 2px
        name:
          - justify-self: start
          - font-size: 18px
          - text-align: left
          - white-space: normal
          - overflow-wrap: break-word
          - min-width: 0
          - width: 100%

1 Like

THX,
question: where to define the vars for the colors

Hi, these are usually defined in a theme but if you don't use one or if the variable is not present in yours you can also replace variables like 'var(--green)' in this code with default color names like light-green or use a RGB value like: 'rgb(206, 245, 149)' and the same in hex: '#CEF595'. :slight_smile:
One can pick these using a color picker like this one: #CEF595 Color Info - Coolors

1 Like

I’ve been using the integration for the past months now, great work here! It drives most of my weather depending automation. I only noticed I had problems because the rainradar didn’t update (should it this time?) but I also noticed my covers were not retracting anymore.

Anyway, I’ve been chasing the solution and I can’t get any further.

My installation

Error:
Error occurred loading flow for integration nl_weather: Error relocating /usr/local/lib/python3.14/site-packages/pyproj/../pyproj.libs/libproj-1e072d9e.so.25.9.5.1: icCRSTraitsEEE: symbol not found

I can’t find any other updates. I tried reinstalling, recloning, returning from a back-up. But everytime it has this issue. I am pretty sure it’s a local issue (otherwise it would have been solved already, this integration is pretty reactive to issues)

Something is messed up in your Python installation. This error indicates that there is an issue loading the c++ PROJ library. This library is needed to load pyproj which is the Python package I’m using to do geographic projections.

pyproj is automatically installed by HA when it first installs nl-weather. It feels like you’ve changed some system libraries, or did some other major changes that causes the system not to find this symbol in the shared libraries.

I would suggest to somehow trigger a removal and reinstall of pyproj. But how to do this depends a lot on your setup. You may be able to use pip or uv for this. But I’m really not sure with Home Assistant OS, which I haven’t used myself.

Maybe some other integration also need pyproj? Could that be the case? Which hardware are you running on?

Hi @PaulVanSchayck

So for those that are looking for the solution, it’s at least not with the app. I guess it had to do with updating Home Assistant CORE. Basically I had to remove indeed pyproj and rebuild it without cache. This resolved my issue.

Thanks for replying Paul!

I 'm bit curious here. If you run on haos, what do you rebuild without cache? Core container is filled with everything you need, so i do not understand your situation and explanation. Would you mind elaborate (just for understanding for me, no judgement :-))

How is the Alert location determined?
Previously with the heatwave I did have the alerts (maybe whole country alerts) but now not.

I live in Gelderland (Maurik) and have stations Herwijnen and The Bilt , being in Gelderland and Utrecht. The KNMI app does show NO alerts for Utrecht and alerts for Gelderland; Code geel

Komende dagen warm tot zeer warm weer ?
Anybody else having no alerts when there should be one..
I’m running Version 0.12.0

I think it based the alerts on the station used for the data as my sensors show this:


Just wanted to say, thank you for this integration. I’m loving it

Already created two new automations based on some of the available entities.

Thanks for the work, the predictions and actual weather reporting are much better then the foreign weather services :slight_smile: One question: you mentioned including UV index in 0.9.0, but i can’t get it to show in the weather forecast cards nor find it as a sensor. Can you help me where I can find the UV index?

It’s part of the daily forecast:

action: weather.get_forecasts
target:
  entity_id: weather.verwachtingen
data:
  type: daily

Result:

weather.verwachtingen:
  forecast:
    - datetime: '2026-08-17T00:00:00Z'
      condition: pouring
      precipitation_probability: 100
      wind_bearing: 292.5
      uv_index: null
      heat_force_index: 5
      temperature: 21
      templow: 15
      wind_gust_speed: 26
      wind_speed: 12
      precipitation: 13
    - datetime: '2026-08-18T00:00:00Z'
      condition: pouring
      precipitation_probability: 100
      wind_bearing: 247.5
      uv_index: 5
      heat_force_index: 3
      temperature: 19
      templow: 16
      wind_gust_speed: 40
      wind_speed: 19
      precipitation: 12
    - datetime: '2026-08-19T00:00:00Z'
      condition: rainy
      precipitation_probability: 90
      wind_bearing: 202.5
      uv_index: 5
      heat_force_index: 3
      temperature: 19
      templow: 15
      wind_gust_speed: 32
      wind_speed: 17
      precipitation: 3
    - datetime: '2026-08-20T00:00:00Z'
      condition: partlycloudy
      precipitation_probability: 100
      wind_bearing: 225
      uv_index: 4
      heat_force_index: 4
      temperature: 22
      templow: 14
      wind_gust_speed: 42
      wind_speed: 19
      precipitation: 4
    - datetime: '2026-08-21T00:00:00Z'
      condition: partlycloudy
      precipitation_probability: 100
      wind_bearing: 225
      uv_index: 5
      heat_force_index: 4
      temperature: 20
      templow: 13
      wind_gust_speed: 34
      wind_speed: 15
      precipitation: 4
    - datetime: '2026-08-22T00:00:00Z'
      condition: partlycloudy
      precipitation_probability: 80
      wind_bearing: 22.5
      uv_index: 4
      heat_force_index: 4
      temperature: 20
      templow: 11
      wind_gust_speed: 27
      wind_speed: 12
      precipitation: 1
    - datetime: '2026-08-23T00:00:00Z'
      condition: partlycloudy
      precipitation_probability: 40
      wind_bearing: 45
      uv_index: 4
      heat_force_index: 3
      temperature: 20
      templow: 11
      wind_gust_speed: 29
      wind_speed: 13
      precipitation: 0

(No UV anymore for today, as it is dark outside)

2 Likes

It’s determined from the region setting when you configure a location entry from the integration page. The region is not automatically calculated from your location, so you need to manually set this. The default is Drenthe.

Your screenshots are showing a bug. This [bug has been reported] by others as well (Alerts switches intermittently to none during upcoming code · Issue #107 · PaulVanSchayck/ha-nl-weather · GitHub) and causes the alert to switch on/off for a few minutes. The problem is within the KNMI app API, but I need to see if I can work around it.