{"id":50058335,"url":"https://github.com/seevee/cap_alerts","last_synced_at":"2026-05-21T16:18:29.320Z","repository":{"id":353385644,"uuid":"1219194973","full_name":"seevee/cap_alerts","owner":"seevee","description":"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.","archived":false,"fork":false,"pushed_at":"2026-05-08T16:47:19.000Z","size":211,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-08T18:40:38.957Z","etag":null,"topics":["alerts","cap","eccc","home-assistant","nws","weather"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/seevee.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/roadmap.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-23T16:17:12.000Z","updated_at":"2026-05-08T16:47:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/seevee/cap_alerts","commit_stats":null,"previous_names":["seevee/cap_alerts"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/seevee/cap_alerts","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seevee%2Fcap_alerts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seevee%2Fcap_alerts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seevee%2Fcap_alerts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seevee%2Fcap_alerts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/seevee","download_url":"https://codeload.github.com/seevee/cap_alerts/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seevee%2Fcap_alerts/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33307159,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-21T12:23:38.849Z","status":"ssl_error","status_checked_at":"2026-05-21T12:22:11.673Z","response_time":62,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alerts","cap","eccc","home-assistant","nws","weather"],"created_at":"2026-05-21T16:18:28.360Z","updated_at":"2026-05-21T16:18:29.314Z","avatar_url":"https://github.com/seevee.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CAP Alerts\n\nA Home Assistant custom integration that creates **one entity per active weather alert**, solving the 16 KB attribute limit that affects single-entity alert integrations.\n\nAlert data is modeled using [CAP (Common Alerting Protocol) 1.2](https://docs.oasis-open.org/emergency/cap/v1.2/CAP-v1.2.html) field names via a `CAPAlert` frozen dataclass. Ships with providers for:\n\n- **NWS** — U.S. National Weather Service (GeoJSON API)\n- **ECCC** — Environment and Climate Change Canada (NAAD Atom feed)\n- **MeteoAlarm** — EUMETNET European aggregator (per-country CAP JSON, ~37 member services)\n\nAdditional providers (BoM, DWD, WMO CAP, …) can be added behind the same `AlertProvider` protocol.\n\nA companion Lovelace card lives at [`weather_alerts_card`](../weather_alerts_card); its `cap.ts` adapter is a thin passthrough because normalization happens here.\n\n---\n\n## Installation\n\n### HACS (custom repository)\n\n1. HACS → Integrations → ⋮ → Custom repositories\n2. Add this repo, category \"Integration\"\n3. Install **CAP Alerts**, restart Home Assistant\n\n### Manual\n\nCopy `custom_components/cap_alerts/` into your HA config's `custom_components/` directory and restart.\n\n---\n\n## Configuration\n\nSettings → Devices \u0026 Services → **Add Integration** → *CAP Alerts*.\n\nPick a provider, then a location mode:\n\n| Provider | Modes |\n|---|---|\n| NWS         | Zone ID (e.g. `ILZ014`, or comma-separated), GPS (`lat,lon`), `device_tracker` entity |\n| ECCC        | Province code (`AB`, `BC`, `ON`, …), GPS (`lat,lon`) |\n| MeteoAlarm  | Country (ISO 3166-1 alpha-2, e.g. `DE`), with optional GPS polygon filter or `EMMA_ID` region multi-select |\n\n### Options (per entry)\n\n- **Scan interval** — 60–3600 s, default 300\n- **Timeout** — 5–120 s, default 30\n- **Language** — ECCC: `auto` / `en-CA` / `fr-CA`. MeteoAlarm: 2-letter prefix (`en`, `de`, `fr`, …) used to pick the primary `\u003ccap:info\u003e` block.\n\nPolygons are **never** emitted in entity attributes — instead, each alert\ncarries a `geometry_ref` handle plus a `bbox`. Fetch the full GeoJSON via:\n\n- REST: `GET /api/cap_alerts/geometry/{geometry_ref}` (HA auth required)\n- Websocket: `{type: \"cap_alerts/geometry\", geometry_ref: \"\u003cref\u003e\"}`\n\nBoth return a GeoJSON `FeatureCollection`. See\n[`docs/frontend_hints.md`](docs/frontend_hints.md) for a card-side snippet.\n\nBoth **reconfigure** (identity/location/provider) and **options** (behavior) flows are supported.\n\n---\n\n## Entities\n\nEvery config entry produces one **device** (named `CAP Alerts \u003cPROVIDER\u003e`, e.g. `CAP Alerts ECCC`) that groups these entities:\n\n| Entity | Purpose | State |\n|---|---|---|\n| `sensor.cap_alerts_\u003cprovider\u003e_alert_count` | Diagnostic. Number of active alerts. | integer |\n| `sensor.cap_alerts_\u003cprovider\u003e_last_updated` | Diagnostic. Last successful poll. | ISO timestamp |\n| `sensor.cap_alert_\u003cevent_slug\u003e_\u003chash\u003e` | One per active alert; created/removed dynamically each poll. | normalized severity (`minor` \\| `moderate` \\| `severe` \\| `extreme` \\| `unknown`) |\n\nThe 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.\n\nAlert entity `extra_state_attributes` is a sparse dict of CAP fields — only populated fields are included. See `model.py::CAPAlert` for the full schema.\n\n### Integration domain vs. entity IDs\n\nThis trips up new HA users, so worth stating explicitly:\n\n- **Integration domain** (`cap_alerts`) — identifies the integration itself, used in `hass.data`, config entries, device identifiers, fired event types (`incident_created`, etc.).\n- **Entity platform domain** (`sensor`) — every entity this integration produces is a *sensor*, so its `entity_id` starts with `sensor.`, never `cap_alerts.`.\n\nSo the integration is `cap_alerts`, but you refer to its entities as `sensor.cap_alert_\u003cslug\u003e`, `sensor.cap_alerts_count`, `sensor.cap_alerts_last_updated` in automations, templates, and the frontend.\n\nPer-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.\n\n---\n\n## Events\n\nFor automation use, the integration fires three event types on the HA bus:\n\n| Event | When |\n|---|---|\n| `incident_created` | A new alert ID appears. |\n| `incident_updated` | An existing alert's lifecycle **phase** or other tracked fields changed. |\n| `incident_removed` | An alert moved to a terminal phase (`cancel` / `expired`) or disappeared from the feed. |\n\nFull payload schema and semantics are documented in [`docs/events.md`](docs/events.md).\n`incident_removed` payloads carry the terminal `phase` (`cancel` or `expired`)\nso automations can distinguish an upstream cancel from a natural expiry\nwithout re-deriving it from timestamps.\n\n### History UI tradeoff\n\nOnce an alert ends, its entity is removed from the entity registry. This\nmeans Home Assistant's **History** dashboard renders past alerts with only\na slugified `entity_id` rather than a friendly name. Recorder rows are\npreserved at the database level, but the UI has no friendly-name context\nto paint. Wire up an automation that listens for `incident_removed` and\nforwards the payload to your archival store of choice (InfluxDB,\nPostgres, a notify service) — see\n[`blueprints/cap_alerts_archive_incident_removed.yaml`](blueprints/cap_alerts_archive_incident_removed.yaml)\nfor a reference blueprint.\n\n---\n\n## Architecture\n\nData flow per poll:\n\n```\nWeather API → Provider.async_fetch() → list[CAPAlert]\n                ↑ (NWS: GeoJSON, ECCC: Atom XML, future: varies)\n  Coordinator._async_update_data()\n    normalize_alerts() → sets severity_normalized, phase\n    store.process()    → diffs vs previous, sets phase_changed, fires HA events\n    ├─ CountSensor (state = len)\n    └─ coordinator listener → diffs alert IDs vs tracked entities\n         → async_add_entities / registry remove\n           └─ AlertEntity (finds own CAPAlert by ID in coordinator.data)\n```\n\n### Files\n\n```\ncustom_components/cap_alerts/\n  __init__.py       # entry setup, coordinator wiring, platform forwarding\n  const.py          # domain, defaults, user-agent format\n  config_flow.py    # setup + reconfigure + options flows\n  coordinator.py    # orchestrates provider, feeds list[CAPAlert] to entities\n  sensor.py         # CountSensor, LastUpdatedSensor, AlertEntity, dynamic lifecycle\n  model.py          # CAPAlert dataclass + to_attributes()\n  normalize.py      # shared normalization: severity, phase, state truncation\n  store.py          # inter-poll diffing, transition detection, HA event firing\n  providers/\n    __init__.py             # AlertProvider protocol + get_provider() factory\n    cap_content_cache.py    # LRU cache for immutable CAP XML bodies\n    nws.py                  # NWS GeoJSON API — zone / GPS / tracker\n    eccc.py                 # Environment Canada NAAD Atom feed\n```\n\nDeeper reference: [`docs/architecture.md`](docs/architecture.md) (alert identity hashing, field mappings, provider rationale, future providers). Planned work: [`docs/roadmap.md`](docs/roadmap.md).\n\n### ECCC — CAP body fetch\n\nECCC alerts now fetch the linked CAP XML (`\u003catom:link type=\"application/cap+xml\"\u003e`) to provide the full alert body. This eliminates the empty `description`/`headline`/`instruction`, wrong/stale timestamps, and duplicate cards for the same alert series.\n\n**What changed:**\n- `headline`, `description`, `instruction` are populated from the CAP `\u003cinfo\u003e` block.\n- `sent`, `effective`, `onset`, `expires` reflect CAP-body values. Past-`expires` alerts are correctly phased as `expired` and filtered from the active set.\n- Revision chains (NEW → UPDATE → CANCEL) collapse to the current leaf via CAP `\u003creferences\u003e`, so one card per alert series is shown.\n- `event` uses the CAP title-case form (e.g. `\"Freezing Drizzle Advisory\"` instead of the lowercase Atom category term). Icon dispatch is unaffected.\n- ECCC CAP-CP eventCodes (e.g. `profile:CAP-CP:Event:0.4 → freezing-drizzle`) are exposed via `attributes.parameters`.\n\n**Operational details:** CAP files are fetched with bounded concurrency (`asyncio.Semaphore(5)`) and cached in a shared LRU-256 `CAPContentCache` on `hass.data[DOMAIN]`. Since each CAP revision has a unique URL, the cache requires no TTL. On fetch failure the alert surfaces with Atom-only metadata (empty long-form text) rather than being dropped.\n\n### Key design decisions\n\n- `CAPAlert` has all fields optional except `id` — tolerates providers with varying completeness.\n- `to_attributes()` emits only non-empty fields (sparse attributes).\n- Dynamic entity lifecycle via `_sync_alert_entities()` in `sensor.py`: add on new ID, remove from entity registry on disappearance.\n- Severity, zones, and phase are normalized at the integration level, not in the card.\n- `entry.runtime_data` (typed `CAPAlertsConfigEntry`) is used instead of the legacy `hass.data[DOMAIN]` dict.\n- `async_config_entry_first_refresh()` gates setup so startup surfaces connection errors properly.\n- No `CONF_NAME` — entry title is derived programmatically from provider + location.\n\n---\n\n## Development\n\nThis is a standard Home Assistant custom integration. It lives entirely under `custom_components/cap_alerts/` and follows [HA custom component conventions](https://developers.home-assistant.io/docs/creating_integration_manifest).\n\n```bash\npytest                             # run all tests\npytest tests/test_coordinator.py   # single file\npytest -k test_parse_alerts        # pattern\n\nmypy custom_components/cap_alerts/\nruff check custom_components/cap_alerts/\nruff format custom_components/cap_alerts/\n```\n\n### Workflow\n\n- `main` is protected; all changes go through PRs.\n- Branches: `feat/\u003cslug\u003e`, `fix/\u003cslug\u003e`, `chore/\u003cslug\u003e`.\n- Commits: `type(scope): description` (`feat`, `fix`, `docs`, `refactor`, `test`, `chore`).\n- Dependency order when modifying code: **model → providers → coordinator → sensor → config_flow → `__init__`**.\n\n### Adding a provider\n\n1. Implement the `AlertProvider` protocol in `providers/\u003cname\u003e.py` — an `async_fetch()` returning `list[CAPAlert]`.\n2. Register it in `providers/__init__.py::get_provider()`.\n3. Add a config-flow branch in `config_flow.py` (a menu step plus one form per location mode).\n4. Add translations under `translations/` and matching keys in `strings.json`.\n5. Normalization lives in `normalize.py`; extend severity mapping there rather than in the provider.\n\n---\n\n## License\n\nSee repository for license details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseevee%2Fcap_alerts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fseevee%2Fcap_alerts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseevee%2Fcap_alerts/lists"}