Getting All Active MeteoAlarm Alerts + Weather Alerts Card Integration

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_sensor that 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.

1 Like

Maintainer of weather_alerts_card here - glad to see this! This is very much the direction I’ve been converging on.

What you’ve implemented lines up with a broader issue across weather providers in Home Assistant: there isn’t a consistent or scalable way to represent multiple concurrent CAP alerts within a single entity.

Some things I have observed while building adapters for various providers across the HA weather alerts ecosystem:

  • NWS aggregates all active alerts into a single alerts attribute on one entity. This works, but pushes against attribute size limits and requires consumers to fully parse a nested structure.
  • ECCC follows a similar pattern, but with much less structured / lower-fidelity alert data.
  • MeteoAlarm effectively truncates to a single alert in its current HA integration, which avoids size issues at the cost of completeness.
  • DWD takes a different approach, exposing multiple alerts via indexed attributes, which is more complete but still constrained and not especially ergonomic.

HA’s entity state + attributes model is not designed for these structured, arbitrarily large (unbounded, even) datasets. The commonly cited ~16 KB limit on state payloads (practically enforced via recorder, state machine, and frontend constraints) means any “all alerts in one entity” approach will eventually degrade, especially considering the additional impact of polygons/geometry.

I’ve been exploring a similar idea, but leaning toward fully dynamic entities (one entity per alert, managed under a device). It’s experimental, incomplete, and not especially idiomatic for HA, but it avoids the data loss and size constraints entirely: GitHub - seevee/cap_alerts: Proof-of-concept HA weather alerts integration exploring an alert entity pattern: one entity per active weather alert, modeled on CAP fields. Solves the 16KB attribute limit. · GitHub

1 Like

Thanks for sharing @seevee, you’ve clearly given lots of thought to this and invested quite some effort.

I’ve looked at cap_alerts and am definitely interested in trying it.

The entity model is elegant — sensors created and destroyed dynamically as alerts come and go. No fixed slots, no empty binary sensors sitting around, my implementation is more of a workaround by comparison.

Is the additional condition I also had to address for AEMET/METEOALARM (contiguous alerts when an alert crosses midnight, with some code to detect this and merge into one) something unique to my use case or have you found it happens with other providers too?

1 Like

Appreciate the kind words! Also very much appreciate your effort wiring the slot solution together. Both of our implementations demonstrate a growing need for first-class core support for emergency alerts/warnings/incidents - sensors aren’t really intended for this type of dynamic, temporal, externally-issued data.

On the midnight discontinuity: this is a behavioral quirk specific to Spain (and possibly some other meteoalarm-covered countries). It’s the same category of problem as the Denmark “green alert” behavior (green alerts always issued and active for no weather). Provider-specific interpretations of CAP don’t always map cleanly to a continuous event model.

In cap_alerts I’ve been trying to keep the provider layer as faithful as possible and avoid over-normalizing these quirks away, since they vary quite a bit by country. cap_alerts currently treats green alerts as “unknown” severity, but does not yet account for the midnight discontinuity. I’m still debating whether this data smoothing really belongs integration-side or client-side.

Side note - you may also be interested in the RFC I’ve been working on here: docs: RFC for the incident integration domain by seevee · Pull Request #4 · seevee/cap_alerts · GitHub. would value your perspective given what you’ve already put together!

1 Like

I appreciate you sharing the RFC @seevee — it’s a really impressive piece of work, both in scope and in the care taken to anticipate objections (the issue_registry comparison in point 3.5 is particularly sharp).

Reminds me of my previous life as a corporate IT director with RFPs, RFIs, and all sorts of design documents.

Some thoughts:

On point 6.1 (static pool vs dynamic): The costs you describe are real. Filtering empty slots client-side is friction that adds up — every automation, every template, every card has to guard against unknown state. And the slot-flapping problem under concurrent churn is subtle; I hadn’t thought through the assignment stability requirement until reading your analysis. Dynamic is clearly the right default if the AWG will accept it. I am at a reasonable level with only 4 entities so far, but it remains to be seen if more may be needed, and I have not started automating based on these yet…

On point 2.6 (no dismiss/acknowledge service): This hadn’t occurred to me as a design decision to make explicit, but you’re absolutely right. A tornado warning is a fact about the world, not a task in someone’s inbox. Keeping the entity as a pure mirror of upstream reality and pushing dismissal UX to the card layer seems like the correct separation of concerns.

On the midnight split: I’m comfortable with it living client-side given your reasoning about provider faithfulness, it makes total sense. My fetcher handles it as a Spain/AEMET-specific wrapper, which feels like the right place for it — close to the data source, opt-in, and not something that should be necessarily normalized away for providers that don’t exhibit this behaviour. If it turns out other MeteoAlarm countries do the same, it could eventually become a configurable option in the MeteoAlarm provider rather than a core concern maybe.

Looking forward to seeing this move upstream. Is there a HA community forum thread or AWG discussion where this is being tracked that would be worth following?

1 Like

Already excellent feedback, and agreed on the midnight-split being an integration level concern. I’m adding this more clear distinction between the core and integration layers based on this discussion:

Provider-specific quirks remain the integration layer’s responsibility under this design. CAP authorities interpret the protocol differently across jurisdictions, and core and custom integrations already absorb those differences today. The incident domain narrows what an integration author has to build by lifting state management out of their concern, so they can focus on feed semantics. The multi-provider layout in cap_alerts exists for prototyping and cross-feed investigation; in practice we recommend one focused integration per upstream service.

No AWG discussion as of yet, I want to leave time for interested parties like yourself and some of the other referenced folks to chime in before I submit. A dedicated forum thread seems like a good place to start!

1 Like

I will stay tuned - keep me in the loop, happy to help where possible