Receive NL-Alert

Does any one knows if it possible to receive NL-alerts.
Want to use the nl-alert to automate some systems, like ventilation and lights in the building.

Best regards
Ocean

3 Likes

NL-Alert is a Dutch mechanism of alerting civilians in the vicinity of a (potantial) crisis situation through their mobile phones using cell broadcast. Different from SMS, cell broadcast is anonymous and works even with a network overload. It’s sent out by the Dutch government.

We got an Alert yesterday, about a large harzadous fire, issuing the closing of doors and windows. I was away from my phone, doing some home improvement. A Home Assistant integration would be awesome. That way, I can have cloud_tts issue the alert through all speakers in the house and have the LEDs turn Red.

If they show up as notifications on the phone then I believe the app can read them.
Look at last_notification

Well there must be an API because of this… Uitbreiding van NL-Alert | Crisis.nl

" Op dit moment kun je NL-Alert pushnotificaties via de Yazula-app en NL-Alarm app ontvangen. In de toekomst zullen ook andere apps NL-Alert pushnotificaties delen. Het ministerie van Justitie en Veiligheid is in gesprek met regionale publieke omroepen om NL-Alert pushnotificaties ook te delen via hun apps."

Has scaped NL-alert

1 Like

You can use this now: https://api.public-warning.app/api/v1/providers/nl-alert/alerts. It looks like this:

{
  "data": [
    {
      "id": "feba2ed1e2d6",
      "message": "Brand met veel rook in Amsterdam, Vlothavenweg. Blijf uit de rook! Sluit ramen en deuren. Zet ventilatie uit. Meer informatie op https://www.brandweer.nl/nieuws/grote-brand-vrachtschip-aan-vlothavenweg/ *** Dutch Public Warning System. Fire with a lot of smoke in Amsterdam, Vlothavenweg. Stay out of the smoke! Close windows and doors. Switch ventilation systems off. For more info, see https://www.brandweer.nl/nieuws/grote-brand-vrachtschip-aan-vlothavenweg/",
      "type": "alert",
      "start_at": "2025-05-15T21:45:39Z",
      "area": [
        "52.40124,4.86918 52.40224,4.83122 52.33634,4.79426 52.25336,4.71471 52.21705,4.85392 52.28918,4.89195 52.29476,4.90032 52.27244,4.90564 52.2743,4.91325 52.28639,4.91325 52.31335,4.92086 52.3612,4.91781 52.39971,4.91477 52.40124,4.86918"
      ],
      "stop_at": "2025-05-15T22:45:39Z"
    },
    ...
  ]
}
3 Likes

Thank you for the link to the API. Could you also provide a link to the accompanying documentation or website?
Thank you,
Eric

I would also like to use this. But how can we use the geo data in area and determine if we are in it? Or perhaps show those areas on the map in HA?

Making a map can be easy done with some linux script that creates a map instance using the rest apt.

Hi Adriaan, who is the provider of this website?
I sent you a pm but maybe you didn’t see that?

I also use https://api.public-warning.app/api/v1/providers/nl-alert/alerts as the source for NL alert. This is the official NL-Alert endpoint also used by Actueel | NL Alert.

Using a REST-sensor I poll this endpoint every five minutes with this YAML configuration:

- resource: https://api.public-warning.app/api/v1/providers/nl-alert/alerts
  scan_interval: 300
  sensor:
    - name: NL-Alert
      icon: mdi:message-alert
      value_template: >
        {{ value_json.data[0].id }}
      json_attributes_path: $.data[0]
      json_attributes:
        - id
        - message
        - start_at
        - stop_at
        - area

Then I run an automation every time this sensor is updated to roughly determine if the alert is applicable, and turn off or on ventilation if it is specified in the message.

alias: NL-Alert
description: ""
triggers:
  - trigger: state
    entity_id:
      - sensor.nl_alert
conditions:
  - alias: NL-Alert applicable to Home zone
    condition: template
    value_template: |-
      {% set area_string = state_attr("sensor.nl_alert","area")[0] %}
      {% set area = area_string.split(" ") %}
      {% set coordinates = area[0].split(",") | map("float") | list %}
      {% set min = namespace(lat=coordinates[0],lon=coordinates[1]) %}
      {% set max = namespace(lat=coordinates[0],lon=coordinates[1]) %}


      {% for coordinate_string in area %}
      {% set coordinates = coordinate_string.split(",") | map("float") | list %}
      {% set min.lat = [min.lat, coordinates[0]] | min %}
      {% set max.lat = [max.lat, coordinates[0]] | max %}

      {% set min.lon = [min.lon, coordinates[1]] | min %}
      {% set max.lon = [max.lon, coordinates[1]] | max %}

      {% endfor %}

      {% set target = namespace(
        lat=state_attr("zone.home","latitude"),
        lon=state_attr("zone.home","longitude")
      ) %}
      {% set inside = (
        target.lat >= min.lat and target.lat <= max.lat and
        target.lon >= min.lon and target.lon <= max.lon
      ) %}

      {{ inside }}
actions:
  - alias: Check for fan control
    choose:
      - conditions:
          - condition: template
            value_template: >-
              {{ "ventilatie uit" in (state_attr("sensor.nl_alert","message") |
              lower) }}
        sequence:
          - action: automation.turn_off
            metadata: {}
            data:
              stop_actions: true
            target:
              entity_id: automation.fan_control
          - action: fan.turn_off
            metadata: {}
            data: {}
            target:
              entity_id: fan.itho_cve_fan
        alias: Ventilation off
      - conditions:
          - condition: template
            value_template: >-
              {{ "ventilatie aan" in (state_attr("sensor.nl_alert","message") |
              lower) }}
        sequence:
          - action: automation.turn_on
            metadata: {}
            data: {}
            target:
              entity_id: automation.fan_control
          - action: automation.trigger
            metadata: {}
            data:
              skip_condition: true
            target:
              entity_id: automation.fan_control
        alias: Ventilation on
  - action: notify.notify
    metadata: {}
    data:
      message: |-
        {{ state_attr("sensor.nl_alert","message") }}
      title: NL-Alert
  - action: tts.speak
    metadata: {}
    data:
      cache: true
      message: |-
        "NL Alert: {{ state_attr("sensor.nl_alert","message") }}
      language: nl_NL
      media_player_entity_id: media_player.alle_speakers
    target:
      entity_id: tts.piper
mode: single

This gives you a sensor that will look something like this:

And can be integrated into a dashboard with a markdown card:

I have this condition, which i think is a bit more accurate. (Not using min/max of area)

conditions:
  - condition: template
    value_template: |-
      {% set home_lat = state_attr('zone.home', 'latitude') %}
      {% set home_lon = state_attr('zone.home', 'longitude') %}

      {% set bounds = state_attr('sensor.nl_alert','area')[0].split() %}

      {% if home_lat is not none and home_lon is not none %}
        {% set inside = namespace(result=false) %}

        {% for i in range(bounds | length) %}
          {% set j = (i + 1) % (bounds | length) %}
          {% set temp_i = bounds[i].split(",") %}
          {% set temp_j = bounds[j].split(",") %}
          {% set lat_i, lon_i = temp_i[0] | float, temp_i[1] | float %}
          {% set lat_j, lon_j = temp_j[0] | float, temp_j[1] | float %}

          {% if ((lon_i > home_lon) != (lon_j > home_lon)) and
                (home_lat < (lat_j - lat_i) * (home_lon - lon_i) / (lon_j - lon_i) + lat_i) %}
             {% set inside.result = not inside.result %}
          {% endif %}
        {% endfor %}

        {% if inside.result %}
           True
        {% else %}
           False
        {% endif %}
      {% else %}
        Unknown
      {% endif %}
    alias: zone.home in alert area

Also: do not use the ‘stop at’. It is meaningless, as it is always one hour after start.

I think this solution works a lot better then my current solution,
however I think this only checks the first record in the JSON
am I right?
I think we need to loop through all.

Yes, only the newest. I don’t see a point in looping through older alerts.

it there are several wild fires in the country,
and 1 is close to me I want to know,
for this I can use the 24hour filter

  - resource: https://api.public-warning.app/api/v1/providers/nl-alert/alerts?filter=last-24h
    scan_interval: 300
    sensor:
      - name: NL-Alert
        value_template: "{{ value_json.data | length }}"
        json_attributes:
          - data

and in the automation this

{% set target = namespace(
        lat=state_attr("zone.home","latitude"),
        lon=state_attr("zone.home","longitude")
      ) %}
{% set alerts = state_attr('sensor.nl_alert', 'data') %}
{# If no alerts → return False #}
{% if not alerts %}
  false
{% elif home_lat is not none and home_lon is not none %}
  {% set inside = namespace(result=false) %}
  {% for alert in alerts %}
  {# Loop through all alert polygons #}
      {% for area in alert.area %}
        {% set min = namespace(lat=90,lon=90) %}
        {% set max = namespace(lat=-90,lon=-90) %}
        {% for coordinate_string in area.split(' ') %}
          {% set coordinates = coordinate_string.split(",") | map("float") | list %}
          {% set min.lat = [min.lat, coordinates[0]] | min %}
          {% set max.lat = [max.lat, coordinates[0]] | max %}
          {% set min.lon = [min.lon, coordinates[1]] | min %}
          {% set max.lon = [max.lon, coordinates[1]] | max %}
        {% endfor %}
        {% if target.lat >= min.lat and target.lat <= max.lat and
          target.lon >= min.lon and target.lon <= max.lon %}
          {% set inside.result = true %}
        {% endif %}
      {% endfor %}
  {% endfor %}
  {{ inside.result }}
{% else %}
  false
{% endif %}

I went with the square calculation as I understand that one a little better.
I think I will try to assign this to a variable instead of a condition so I can use it on more the one place,

but for now this will be my check against multiple alerts

[edit]
I decided to go for a template binary sensor

- binary_sensor:
    - name: nl-alert
      state: >-
        {% set target = namespace(
                lat=state_attr("zone.home","latitude"),
                lon=state_attr("zone.home","longitude")
              ) %}
        {% set alerts = state_attr('sensor.nl_alert', 'data') %}
        {# If no alerts → return False #}
        {% if not alerts %}
          false
        {% elif home_lat is not none and home_lon is not none %}
          {% set inside = namespace(result=false) %}
          {% for alert in alerts %}
          {# Loop through all alert polygons #}
              {% for area in alert.area %}
                {% set min = namespace(lat=90,lon=90) %}
                {% set max = namespace(lat=-90,lon=-90) %}
                {% for coordinate_string in area.split(' ') %}
                  {% set coordinates = coordinate_string.split(",") | map("float") | list %}
                  {% set min.lat = [min.lat, coordinates[0]] | min %}
                  {% set max.lat = [max.lat, coordinates[0]] | max %}
                  {% set min.lon = [min.lon, coordinates[1]] | min %}
                  {% set max.lon = [max.lon, coordinates[1]] | max %}
                {% endfor %}
                {% if target.lat >= min.lat and target.lat <= max.lat and
                  target.lon >= min.lon and target.lon <= max.lon %}
                  {% set inside.result = true %}
                {% endif %}
              {% endfor %}
          {% endfor %}
          {{ inside.result }}
        {% else %}
          false
        {% endif %}

I can use this same sensor in several automations, basically controlling the fan speeds,
only turning them off is not enough because if the fan speed changes the change should not enable them again.

I read the api every 5 minutes and process only the newest. The odds of having multiple NL Alerts within these 5 minutes are close to zero.

Then you can set a NL alert helper to ‘active’ or something like that. I only loop through the coordinates when a new id is found. This is more efficient.

that’s fair and a good idea,
still I am not sure only checking the last one suffice,
I think I still have to exclude test messages which are send 3 times a year,
however this is also a good way to test my automations

I split the action part in a case for “TESTBERICHT” and for real alerts. This way the automation (trigger and condition part) is tested as well.

I created a template sensor helper to produce the center coordinates of the area specified in the rest sensor. It took me quite a while to get it to work. Maybe it can be of help to someone. Allows you to update a device_tracker with the proper center location of the alarm.

{% set raw = state_attr('sensor.nl_alert','area') %}
{% if raw and raw | length > 0 %}
  {% set coord_string = raw[0] if raw is iterable else raw %}
  {% set coord_string = coord_string | string %}
  {% set points = coord_string.split() %}
  
  {% set lats = points | map('regex_replace', '(.*),(.*)', '\\1') | map('float') | list %}
  {% set lons = points | map('regex_replace', '(.*),(.*)', '\\2') | map('float') | list %}
  
  {% if lats and lons %}
    {% set center_lat = (lats | min + lats | max)/2 %}
    {% set center_lon = (lons | min + lons | max)/2 %}
    {{ center_lat | round(5) }},{{ center_lon | round(5) }}
  {% else %}
    unknown
  {% endif %}
{% else %}
  unknown
{% endif %}