Skip to content

seevee/cap_alerts

Repository files navigation

CAP Alerts

A Home Assistant custom integration that creates one entity per active weather alert, solving the 16 KB attribute limit that affects single-entity alert integrations.

Alert data is modeled using CAP (Common Alerting Protocol) 1.2 field names via a CAPAlert frozen dataclass. Ships with providers for:

  • NWS — U.S. National Weather Service (GeoJSON API)
  • ECCC — Environment and Climate Change Canada (NAAD Atom feed)
  • MeteoAlarm — EUMETNET European aggregator (per-country CAP JSON, ~37 member services)
  • WMO — World Meteorological Organization Severe Weather Information Centre (per-source RSS → CAP XML), covering ~100 national services without a dedicated provider

Additional providers (BoM, DWD, …) can be added behind the same AlertProvider protocol.

A companion Lovelace card lives at weather_alerts_card; its cap.ts adapter is a thin passthrough because normalization happens here.


Installation

HACS (custom repository)

  1. HACS → Integrations → ⋮ → Custom repositories
  2. Add this repo, category "Integration"
  3. Install CAP Alerts, restart Home Assistant

Manual

Copy custom_components/cap_alerts/ into your HA config's custom_components/ directory and restart.


Configuration

Settings → Devices & Services → Add IntegrationCAP Alerts.

Pick a provider, then a location mode:

Provider Modes
NWS Zone ID (e.g. ILZ014, or comma-separated), GPS (lat,lon), device_tracker entity
ECCC Province code (AB, BC, ON, …), GPS (lat,lon)
MeteoAlarm Country (ISO 3166-1 alpha-2, e.g. DE), with optional GPS polygon filter or EMMA_ID region multi-select
WMO Source ID picked from the live SWIC registry (e.g. mx-smn-es; custom IDs accepted), country-wide or with optional GPS polygon filter

Options (per entry)

  • Scan interval — 60–3600 s, default 300
  • Timeout — 5–120 s, default 30
  • Language — ECCC: auto / en-CA / fr-CA. MeteoAlarm: 2-letter prefix (en, de, fr, …) used to pick the primary <cap:info> block. NWS and WMO have no language option (English-only / one language per source).

Polygons are never emitted in entity attributes — instead, each alert carries a geometry_ref handle plus a bbox. Fetch the full GeoJSON via:

  • REST: GET /api/cap_alerts/geometry/{geometry_ref} (HA auth required)
  • Websocket: {type: "cap_alerts/geometry", geometry_ref: "<ref>"}

Both return a GeoJSON FeatureCollection. See docs/frontend_hints.md for a card-side snippet.

Both reconfigure (identity/location/provider) and options (behavior) flows are supported.


Entities

Every config entry produces one device (named CAP Alerts <PROVIDER>, e.g. CAP Alerts ECCC) that groups these entities:

Entity Purpose State
sensor.cap_alerts_<provider>_alert_count Diagnostic. Number of active alerts. integer
sensor.cap_alerts_<provider>_last_updated Diagnostic. Last successful poll. ISO timestamp
sensor.cap_alert_<event_slug>_<hash> One per active alert; created/removed dynamically each poll. normalized severity (minor | moderate | severe | extreme | unknown)

The device name is intentionally stable across reconfigures so entity_ids don't drift when you change GPS, zone, or region. The per-entry friendly label (with location detail) remains visible in the integrations list as the entry title; users running multiple entries of the same provider can set name_by_user on the device for a personalized label.

Alert entity extra_state_attributes is a sparse dict of CAP fields — only populated fields are included. See model.py::CAPAlert for the full schema.

Integration domain vs. entity IDs

This trips up new HA users, so worth stating explicitly:

  • Integration domain (cap_alerts) — identifies the integration itself, used in hass.data, config entries, device identifiers, fired event types (incident_created, etc.).
  • Entity platform domain (sensor) — every entity this integration produces is a sensor, so its entity_id starts with sensor., never cap_alerts..

So the integration is cap_alerts, but you refer to its entities as sensor.cap_alert_<event_slug>_<hash>, sensor.cap_alerts_<provider>_alert_count, sensor.cap_alerts_<provider>_last_updated in automations, templates, and the frontend.

Per-alert entity IDs are derived from the alert's event text (e.g. sensor.cap_alert_tornado_warning). If multiple active alerts share an event name, HA appends _2, _3, … Unique IDs are stable across restarts ({entry_id}_{provider}_{alert_id}), so the registry keeps identity even when the entity_id suffix shifts.


Events

For automation use, the integration fires three event types on the HA bus:

Event When
incident_created A new alert ID appears.
incident_updated An existing alert's lifecycle phase or other tracked fields changed.
incident_removed An alert moved to a terminal phase (cancel / expired) or disappeared from the feed.

Full payload schema and semantics are documented in docs/events.md. incident_removed payloads carry the terminal phase (cancel or expired) so automations can distinguish an upstream cancel from a natural expiry without re-deriving it from timestamps.

History UI tradeoff

Once an alert ends, its entity is removed from the entity registry. This means Home Assistant's History dashboard renders past alerts with only a slugified entity_id rather than a friendly name. Recorder rows are preserved at the database level, but the UI has no friendly-name context to paint. Wire up an automation that listens for incident_removed and forwards the payload to your archival store of choice (InfluxDB, Postgres, a notify service) — see blueprints/cap_alerts_archive_incident_removed.yaml for a reference blueprint.


Architecture

Data flow per poll:

Weather API → Provider.async_fetch() → list[CAPAlert]
                ↑ (NWS: GeoJSON, ECCC: Atom→CAP XML, MeteoAlarm: JSON, WMO: RSS→CAP XML)
  Coordinator._async_update_data()
    normalize_alerts() → sets severity_normalized, phase
    store.process()    → diffs vs previous, sets phase_changed, fires HA events
    ├─ CountSensor (state = len)
    └─ coordinator listener → diffs alert IDs vs tracked entities
         → async_add_entities / registry remove
           └─ AlertEntity (finds own CAPAlert by ID in coordinator.data)

Files

custom_components/cap_alerts/
  __init__.py       # entry setup, coordinator wiring, platform forwarding
  const.py          # domain, defaults, user-agent format
  config_flow.py    # setup + reconfigure + options flows
  coordinator.py    # orchestrates provider, feeds list[CAPAlert] to entities
  sensor.py         # CountSensor, LastUpdatedSensor, AlertEntity, dynamic lifecycle
  model.py          # CAPAlert dataclass + to_attributes()
  normalize.py      # shared normalization: severity, phase, Buddhist-Era year fix, state truncation
  store.py          # inter-poll diffing, transition detection, HA event firing
  providers/
    __init__.py             # AlertProvider protocol + get_provider() factory
    cap.py                  # shared, provider-neutral CAP 1.2 XML parsing (used by eccc + wmo)
    cap_content_cache.py    # LRU cache for immutable CAP XML bodies
    nws.py                  # NWS GeoJSON API — zone / GPS / tracker
    eccc.py                 # Environment Canada NAAD Atom feed
    meteoalarm.py           # EUMETNET per-country CAP JSON + region listing
    wmo.py                  # WMO SWIC per-source RSS → CAP XML + source registry

Deeper reference: docs/architecture.md (alert identity hashing, field mappings, provider rationale, future providers). Planned work: docs/roadmap.md.

Provider notes

  • ECCC fetches the linked CAP XML body (not just the Atom envelope), so alerts carry full headline/description/instruction, accurate timestamps, and one card per alert series — revision chains (NEW → UPDATE → CANCEL) collapse to the current leaf via CAP <references>.
  • WMO reuses the same RSS-index → per-item CAP two-step (and the shared CAP parser), populates its source dropdown from the live SWIC registry, and pre-filters already-expired RSS items so high-volume feeds don't blow the poll timeout. EU/US users are better served by the dedicated MeteoAlarm/NWS providers.

See docs/architecture.md for the full ECCC and WMO sections — CAP body fetch, concurrency and caching, the expiry pre-filter, and per-field mappings.

Key design decisions

  • CAPAlert has all fields optional except id — tolerates providers with varying completeness.
  • to_attributes() emits only non-empty fields (sparse attributes).
  • Dynamic entity lifecycle via _sync_alert_entities() in sensor.py: add on new ID, remove from entity registry on disappearance.
  • Severity, zones, and phase are normalized at the integration level, not in the card.
  • entry.runtime_data (typed CAPAlertsConfigEntry) is used instead of the legacy hass.data[DOMAIN] dict.
  • async_config_entry_first_refresh() gates setup so startup surfaces connection errors properly.
  • No CONF_NAME — entry title is derived programmatically from provider + location.

Development

This is a standard Home Assistant custom integration. It lives entirely under custom_components/cap_alerts/ and follows HA custom component conventions.

pytest                             # run all tests
pytest tests/test_coordinator.py   # single file
pytest -k test_parse_alerts        # pattern

mypy custom_components/cap_alerts/
ruff check custom_components/cap_alerts/
ruff format custom_components/cap_alerts/

Workflow

  • main is protected; all changes go through PRs.
  • Branches: feat/<slug>, fix/<slug>, chore/<slug>.
  • Commits: type(scope): description (feat, fix, docs, refactor, test, chore).
  • Dependency order when modifying code: model → providers → coordinator → sensor → config_flow → __init__.

Adding a provider

  1. Implement the AlertProvider protocol in providers/<name>.py — an async_fetch() returning list[CAPAlert].
  2. Register it in providers/__init__.py::get_provider().
  3. Add a config-flow branch in config_flow.py (a menu step plus one form per location mode).
  4. Add translations under translations/ and matching keys in strings.json.
  5. Normalization lives in normalize.py; extend severity mapping there rather than in the provider.

License

See repository for license details.

About

HA multi-region weather alerts integration: one entity per active weather alert, modeled on CAP fields. Solves the 16KB attribute limit.

Topics

Resources

License

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages