Getting All Active MeteoAlarm Alerts + Weather Alerts Card Integration
The Problem
The built-in MeteoAlarm integration has a known limitation: when multiple alerts are active
for your region simultaneously, only the first one is retrieved and shown. If you’re in an
area like, for example, the Sierra de Madrid where AEMET can issue concurrent rain, thunderstorm, wind,
and snow warnings, you’re only ever seeing part of the picture.
This has already beeen raised in other posts here and here and, as far as I was able to see, no solution was implemented to address this (unless I missed something).
This post describes a custom sensor stack that:
- Fetches all active alerts for your region directly from the MeteoAlarm Atom feed
- Handles deduplication and merges contiguous time-period alerts issued by some agencies
- Exposes each alert as a properly-shaped
binary_sensorthat the
Weather Alerts Card can consume natively
Architecture Overview
MeteoAlarm Atom Feed (meteoalarm.org)
↓
meteo_fetcher.py — fetches all alerts for your region, deduplicates,
merges contiguous periods, returns clean JSON
↓
sensor.meteoalarm_raw — command_line sensor, state = alert count,
attributes contain the full alerts list
↓
binary_sensor.meteoalarm_alert_1..N — one per alert slot, each shaped
to match the native meteoalarm
binary_sensor attribute schema
↓
weather_alerts_card — renders all active alerts with severity, certainty,
progress bars, descriptions, and source links
Step 1 — The Python Fetcher
Save this as /config/python_scripts/meteo_fetcher.py.
Requires xmltodict and requests — both are available in the standard HA Python environment.
import sys
import xmltodict
import requests
import json
from datetime import datetime, timedelta
def _dt(iso):
"""Parse an ISO 8601 string to a timezone-aware datetime, or return None."""
try:
return datetime.fromisoformat(iso) if iso else None
except ValueError:
return None
def fetch_live_alerts(country, province, lang='en-GB'):
base_url = f"https://feeds.meteoalarm.org/feeds/meteoalarm-legacy-atom-{country.lower()}"
now = datetime.now().astimezone()
try:
r = requests.get(base_url, timeout=10)
r.raise_for_status()
feed_data = xmltodict.parse(r.text)
entries = feed_data.get('feed', {}).get('entry', [])
if not isinstance(entries, list):
entries = [entries]
live_alerts = []
for entry in entries:
area_desc = entry.get('cap:areaDesc', '')
if province.lower() not in area_desc.lower():
continue
links = entry.get('link', [])
if not isinstance(links, list):
links = [links]
cap_url = next(
(l.get('@href') for l in links if l.get('@type') == 'application/cap+xml'),
None
)
if not cap_url:
continue
cap_res = requests.get(cap_url, timeout=10)
cap_res.raise_for_status()
alert_info = xmltodict.parse(cap_res.text).get('alert', {}).get('info', [])
if not isinstance(alert_info, list):
alert_info = [alert_info]
for info in alert_info:
if lang not in info.get('language', ''):
continue
expiry_str = info.get('expires')
if not expiry_str or now >= datetime.fromisoformat(expiry_str):
continue
alert = {
"event": info.get("event"),
"severity": info.get("severity"),
"certainty": info.get("certainty"),
"urgency": info.get("urgency"),
"category": info.get("category", "Met"),
"responseType": info.get("responseType"),
"effective": info.get("effective"),
"onset": info.get("onset"),
"expires": expiry_str,
"headline": info.get("headline"),
"description": info.get("description"),
"instruction": info.get("instruction"),
"senderName": info.get("senderName"),
"web": info.get("web"),
"contact": info.get("contact"),
"attribution": "Information provided by MeteoAlarm",
"language": info.get("language"),
}
# Extract awareness_level and awareness_type parameters
parameters = info.get('parameter', [])
if not isinstance(parameters, list):
parameters = [parameters]
for param in parameters:
name, val = param.get('valueName'), param.get('value')
if name == 'awareness_level':
alert['level_raw'] = val
parts = val.split(';')
if len(parts) >= 2:
alert['color'] = parts[1].strip()
if name == 'awareness_type':
alert['type_raw'] = val
parts = val.split(';')
if len(parts) >= 2:
alert['type'] = parts[1].strip()
# Exact-duplicate guard (same alert can appear in multiple feed entries)
dedup_key = (alert.get("event", ""), alert.get("expires", ""))
if not any(
(a.get("event", ""), a.get("expires", "")) == dedup_key
for a in live_alerts
):
live_alerts.append(alert)
# Merge contiguous periods: some agencies (e.g. AEMET) split a single
# continuous warning at calendar-day boundaries, producing two entries
# where alert B's onset is exactly 1 second after alert A's expires.
# We merge these into one alert keeping the earliest onset and latest expires.
live_alerts.sort(key=lambda a: (a.get("event", ""), a.get("onset", "")))
merged = []
for alert in live_alerts:
onset_dt = _dt(alert.get("onset"))
matched = None
if onset_dt:
for m in merged:
if m.get("event") == alert.get("event"):
m_expires_dt = _dt(m.get("expires"))
if m_expires_dt and onset_dt - m_expires_dt == timedelta(seconds=1):
matched = m
break
if matched:
matched["expires"] = alert.get("expires")
else:
merged.append(alert)
return json.dumps({"count": len(merged), "alerts": merged})
except Exception as e:
return json.dumps({"count": 0, "error": str(e), "alerts": []})
if __name__ == "__main__":
if len(sys.argv) < 3:
print(json.dumps({"count": 0, "error": "Usage: script.py <country> <province>"}))
else:
print(fetch_live_alerts(sys.argv[1], sys.argv[2]))
Usage: python3 meteo_fetcher.py spain "Sierra de Madrid"
NOTE: Adapt country and province to match your region. The province string must match
the areaDesc field in the MeteoAlarm feed for your country — check
https://feeds.meteoalarm.org/feeds/meteoalarm-legacy-atom-<country> to find yours.
Step 2 — Home Assistant Configuration
Add this to your configuration.yaml or a packages file. Replace the four
REPLACE_WITH_UUID_V7 placeholders with generated UUIDs before restarting (or any other unique ID of your choice) so as to allow these sensors to play nicely with the UI .
Adjust the country/province in the command to match your region.
command_line:
- sensor:
name: Meteoalarm Raw
unique_id: REPLACE_WITH_UUID_V7
command: "python3 /config/python_scripts/meteo_fetcher.py spain 'Sierra de Madrid'"
scan_interval: 600
value_template: "{{ value_json.count }}"
json_attributes:
- alerts
template:
- binary_sensor:
# One slot per possible concurrent alert. Each is 'off' when empty,
# so unused slots cost nothing and the card ignores them automatically.
# Add more slots if your region regularly issues more than 4 simultaneous alerts.
- name: "Meteoalarm Alert 1"
unique_id: REPLACE_WITH_UUID_V7
device_class: safety
state: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a is list and a | count >= 1 }}
attributes:
language: "en-GB"
category: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].category if a is list and a | count >= 1 else 'Met' }}
event: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].event if a is list and a | count >= 1 else '' }}
responseType: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].responseType if a is list and a | count >= 1 else 'Monitor' }}
urgency: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].urgency if a is list and a | count >= 1 else '' }}
severity: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].severity if a is list and a | count >= 1 else '' }}
certainty: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].certainty if a is list and a | count >= 1 else '' }}
effective: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].effective if a is list and a | count >= 1 else '' }}
onset: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].onset if a is list and a | count >= 1 else '' }}
expires: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].expires if a is list and a | count >= 1 else '' }}
senderName: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].senderName if a is list and a | count >= 1 else '' }}
headline: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].headline if a is list and a | count >= 1 else '' }}
description: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].description if a is list and a | count >= 1 else '' }}
instruction: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].instruction if a is list and a | count >= 1 else '' }}
web: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].web if a is list and a | count >= 1 else '' }}
contact: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].contact if a is list and a | count >= 1 else '' }}
awareness_level: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].level_raw if a is list and a | count >= 1 else '' }}
awareness_type: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[0].type_raw if a is list and a | count >= 1 else '' }}
attribution: "Information provided by MeteoAlarm"
- name: "Meteoalarm Alert 2"
unique_id: REPLACE_WITH_UUID_V7
device_class: safety
state: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a is list and a | count >= 2 }}
attributes:
language: "en-GB"
category: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].category if a is list and a | count >= 2 else 'Met' }}
event: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].event if a is list and a | count >= 2 else '' }}
responseType: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].responseType if a is list and a | count >= 2 else 'Monitor' }}
urgency: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].urgency if a is list and a | count >= 2 else '' }}
severity: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].severity if a is list and a | count >= 2 else '' }}
certainty: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].certainty if a is list and a | count >= 2 else '' }}
effective: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].effective if a is list and a | count >= 2 else '' }}
onset: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].onset if a is list and a | count >= 2 else '' }}
expires: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].expires if a is list and a | count >= 2 else '' }}
senderName: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].senderName if a is list and a | count >= 2 else '' }}
headline: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].headline if a is list and a | count >= 2 else '' }}
description: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].description if a is list and a | count >= 2 else '' }}
instruction: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].instruction if a is list and a | count >= 2 else '' }}
web: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].web if a is list and a | count >= 2 else '' }}
contact: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].contact if a is list and a | count >= 2 else '' }}
awareness_level: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].level_raw if a is list and a | count >= 2 else '' }}
awareness_type: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[1].type_raw if a is list and a | count >= 2 else '' }}
attribution: "Information provided by MeteoAlarm"
- name: "Meteoalarm Alert 3"
unique_id: REPLACE_WITH_UUID_V7
device_class: safety
state: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a is list and a | count >= 3 }}
attributes:
language: "en-GB"
category: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].category if a is list and a | count >= 3 else 'Met' }}
event: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].event if a is list and a | count >= 3 else '' }}
responseType: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].responseType if a is list and a | count >= 3 else 'Monitor' }}
urgency: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].urgency if a is list and a | count >= 3 else '' }}
severity: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].severity if a is list and a | count >= 3 else '' }}
certainty: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].certainty if a is list and a | count >= 3 else '' }}
effective: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].effective if a is list and a | count >= 3 else '' }}
onset: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].onset if a is list and a | count >= 3 else '' }}
expires: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].expires if a is list and a | count >= 3 else '' }}
senderName: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].senderName if a is list and a | count >= 3 else '' }}
headline: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].headline if a is list and a | count >= 3 else '' }}
description: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].description if a is list and a | count >= 3 else '' }}
instruction: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].instruction if a is list and a | count >= 3 else '' }}
web: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].web if a is list and a | count >= 3 else '' }}
contact: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].contact if a is list and a | count >= 3 else '' }}
awareness_level: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].level_raw if a is list and a | count >= 3 else '' }}
awareness_type: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[2].type_raw if a is list and a | count >= 3 else '' }}
attribution: "Information provided by MeteoAlarm"
- name: "Meteoalarm Alert 4"
unique_id: REPLACE_WITH_UUID_V7
device_class: safety
state: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a is list and a | count >= 4 }}
attributes:
language: "en-GB"
category: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].category if a is list and a | count >= 4 else 'Met' }}
event: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].event if a is list and a | count >= 4 else '' }}
responseType: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].responseType if a is list and a | count >= 4 else 'Monitor' }}
urgency: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].urgency if a is list and a | count >= 4 else '' }}
severity: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].severity if a is list and a | count >= 4 else '' }}
certainty: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].certainty if a is list and a | count >= 4 else '' }}
effective: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].effective if a is list and a | count >= 4 else '' }}
onset: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].onset if a is list and a | count >= 4 else '' }}
expires: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].expires if a is list and a | count >= 4 else '' }}
senderName: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].senderName if a is list and a | count >= 4 else '' }}
headline: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].headline if a is list and a | count >= 4 else '' }}
description: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].description if a is list and a | count >= 4 else '' }}
instruction: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].instruction if a is list and a | count >= 4 else '' }}
web: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].web if a is list and a | count >= 4 else '' }}
contact: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].contact if a is list and a | count >= 4 else '' }}
awareness_level: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].level_raw if a is list and a | count >= 4 else '' }}
awareness_type: >
{% set a = state_attr('sensor.meteoalarm_raw', 'alerts') %}
{{ a[3].type_raw if a is list and a | count >= 4 else '' }}
attribution: "Information provided by MeteoAlarm"
Step 3 — Weather Alerts Card
Install Weather Alerts Card via HACS,
then add this to your dashboard:
type: custom:weather-alerts-card
provider: meteoalarm
title: Weather Alerts
entities:
- binary_sensor.meteoalarm_alert_1
- binary_sensor.meteoalarm_alert_2
- binary_sensor.meteoalarm_alert_3
- binary_sensor.meteoalarm_alert_4
The card will automatically hide any slot whose state is off (no alert in that slot),
so having 4 slots defined costs nothing during quiet periods.
Notes & Caveats
Polling interval: The command_line sensor defaults to scan_interval: 600 (10 minutes).
The MeteoAlarm feed itself typically updates every 15–30 minutes, so this is a reasonable
balance. You can trigger an immediate refresh via
Developer Tools → Actions → homeassistant.update_entity on sensor.meteoalarm_raw.
Contiguous period merging: Some national agencies (confirmed with AEMET in Spain) split
a single continuous warning at calendar-day midnight, publishing two consecutive entries
where the second one’s onset is exactly 1 second after the first one’s expires. The fetcher
detects and merges these automatically so they appear as one alert with the full time range.
Language: Pass a different lang parameter to fetch_live_alerts() if you want alerts
in your local language rather than English. Not all agencies translate all fields — AEMET
for example leaves some description fields in Spanish regardless of the language parameter.
Number of slots: 4 slots covers the most complex scenarios encountered in practice
(multiple concurrent alert types across multiple time periods). Add more slots to the
template config if your region regularly exceeds this.
Coexistence: This stack coexists happily with the native MeteoAlarm binary sensor
(binary_sensor.meteoalarm) — there is no need to remove it.