Skip to content

feat(coordinator): filter alerts by area-code prefix - #74

Merged
seevee merged 2 commits into
mainfrom
feat/geocode-prefix-filter
Aug 4, 2026
Merged

feat(coordinator): filter alerts by area-code prefix#74
seevee merged 2 commits into
mainfrom
feat/geocode-prefix-filter

Conversation

@seevee

@seevee seevee commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Closes #73.

Problem

A SWIC source that publishes no per-alert geometry has exactly one usable location mode. Selecting GPS trips the fail-loud guard on every poll and the entry never initializes:

Failed setup, will retry: WMO cn-cma-xx: GPS filter requested but 491 alerts
carry no polygons; this source does not publish per-alert geometry

cn-cma-xx (China, CMA) is the confirmed case — 0 of 60 sampled bodies carried a <polygon> — and it is also the source carrying the zh-CN <info> blocks #59/#72 exist to select. So a Chinese-language user is forced into country-wide, which yields 261 active alerts for a single entry. That is not merely unusable; it destabilizes the frontend, observed while one such entry populated:

Client unable to keep up with pending messages. Reached 4096 pending messages.

Change

An opt-in narrowing keyed on CAPAlert.geocodes: an alert is kept when any code it publishes starts with any configured prefix. Measured live against the real feed on 2026-08-04:

prefix scope active alerts kept
country-wide 261
11 Beijing 0
31 Shanghai 4
44 Guangdong 8
13 Hebei 30

Why provider-neutral, not a WMO filter mode

The original plan scoped this to providers/wmo.py as a fourth filter mode. It landed in coordinator._apply instead, because every provider already populates the geocodes container — nothing in the matcher is WMO-specific — and exclude_marine is the exact precedent: a cross-provider, options-configured, post-fetch narrowing applied in the same place.

That makes it a layer rather than a mode: it composes with NWS zones, ECCC provinces, MeteoAlarm regions, and WMO GPS instead of being mutually exclusive with them. It also came out to less code than the per-provider shape would have.

Design notes

  • Prefix, not exact match. Area codes are hierarchical, so a prefix is a scope: 13 = Hebei, 1307 = Zhangjiakou, 130709000000 = Chongli. Exact matching would make the user enumerate every county code — 28 of them for Hebei alone.
  • Scheme-agnostic. There is no cross-provider scheme-priority registry to resolve one code from (MeteoAlarm's is country-scoped; CMA's CPEAS Geographic Code appears in no existing table), so every value under every valueName is compared. A colliding prefix over-matches rather than dropping wanted alerts.
  • Variable code lengths. Of 488 sampled CMA codes, 481 are 12 characters and 7 are 6 (Chongqing districts). Matching is a plain startswith with no zero-padding either way. The consequence — a pasted full-length code will not match a shorter sibling — is documented in the field description and pinned by a test.
  • Fail-loud only on a capability failure. UpdateFailed fires only when the feed returned alerts and none carry any geocode, the exact parallel of "publishes no per-alert geometry". Zero matches is not a failure: "no alerts in my area" is the normal steady state, and failing there would leave the entry unavailable most of the time. A typo'd prefix surfaces instead as a one-shot WARNING naming codes the feed actually publishes, re-armed on the first match — the pattern _tracker_resolve_warned already uses.

Configuration

Two entry points, one setting:

  • Options flow (all providers) — Configure → Area codes (prefix match). Applies on save via the existing update listener; no reload.
  • WMO setup + reconfigure menu — a step that writes the same option at creation time. Without it, narrowing a high-volume source means creating hundreds of entities and immediately removing most of them, which is the condition this PR exists to avoid.

The reconfigure step merges into existing options rather than passing options= wholesale, which would silently drop scan_interval, timeout, and the language selection. A test pins that.

The prefix stays out of the entry title on purpose: _compute_device_title reads entry data only, and options are editable independently, so a title showing the prefix would go stale the moment it changed under Configure.

Compatibility

Additive and unversioned. The key is absent on every existing entry and both readers use .get(), so no migration is needed and no existing mode changes behavior. The card consumes alert attributes, not entry config, so no frontend change.

Testing

41 new tests across two files — matching, off/empty behavior, the fail-loud contract, the one-shot warning, validator table, and both flow surfaces. Full suite 640 passed; ruff, ruff format, and mypy clean; zero translation drift including zh-Hans under -W error::UserWarning.

Verified live against the real CMA feed and deployed to a dev HA instance.

Also in this branch

scripts/provider_probe.py (second commit) — dev tooling that answers feed-shape questions against the real provider path in seconds. It gained --geocode-prefix, --codes, and --active for this work. Two bugs found in it while validating: Counter.update() on a {scheme: (codes…)} mapping concatenated the tuples as counts instead of counting keys, and an all-numeric list value like -c regions=123 coerced to int because the digit check preceded the list check.

Notes

The SWIC mirror supports no query-side filtering — it is a static file behind CloudFront, and arbitrary query parameters return a byte-identical body — so client-side filtering is the only option.

Separately, each RSS <item>'s <guid> leads with the GB/T 2260 code (52272741600000_20260804103516522727, Pingtang County), and all 500 items follow the pattern. A prefix filter could in principle run on the envelope and skip ~500 CAP-body fetches. Not built on here: the layout is CMA-specific, undocumented, and a 14-digit serial that merely starts with the 6-digit code. Noted as a possible future optimization.

Assisted-by: Claude:claude-opus-5

seevee added 2 commits August 3, 2026 21:23
A SWIC source that publishes no per-alert geometry has exactly one usable
location mode, country-wide: selecting GPS trips _filter_by_polygon's
fail-loud guard on every poll and the entry never initializes. cn-cma-xx
is the confirmed case — 0 of 60 sampled bodies carried a <polygon> —
and it is also the source carrying the zh-CN <info> blocks #59/#72 exist
to select, so a Chinese-language user was forced into a mode that yields
261 active alerts for one entry. That is not merely unusable; it
destabilizes the frontend, which reports "Client unable to keep up with
pending messages" while the entry populates.

Add an opt-in narrowing keyed on CAPAlert.geocodes: an alert is kept when
any code it publishes starts with any configured prefix. Measured live on
2026-08-04, prefixes cut the 261 to 30 (13, Hebei), 4 (31, Shanghai), and
8 (44, Guangdong).

Deliberately provider-neutral rather than a WMO filter mode. Every
provider already populates the geocodes container, so nothing in the
matcher is WMO-specific, and the existing exclude_marine option is the
exact precedent: a cross-provider, options-configured, post-fetch
narrowing applied in coordinator._apply. Landing it there rather than in
providers/wmo.py also makes it a layer rather than a mode — it composes
with NWS zones, ECCC provinces, MeteoAlarm regions, and WMO GPS instead
of being mutually exclusive with them — and costs less code than the
per-provider shape would have.

Prefix rather than exact match because area codes are hierarchical, so a
prefix is a scope: 13 is Hebei, 1307 Zhangjiakou, 130709000000 Chongli.
Exact matching would make the user enumerate every county code, 28 of
them for Hebei alone. Matching is scheme-agnostic: there is no
cross-provider scheme-priority registry to resolve one code from
(MeteoAlarm's is country-scoped, and CMA's "CPEAS Geographic Code"
appears in no existing table), so every value under every valueName is
compared. A colliding prefix over-matches, keeping extra alerts, rather
than dropping wanted ones.

Codes vary in length within a single scheme — of 488 sampled CMA codes,
481 are 12 characters and 7 are 6 (Chongqing districts) — so the match is
a plain startswith with no zero-padding in either direction. The
consequence, that a pasted full-length code will not match a shorter
sibling, is documented in the field description and pinned by a test so a
future refactor cannot silently introduce padding.

The fail-loud contract is the subtle part. UpdateFailed fires only when
the feed returned alerts and not one carries any geocode — a source
capability failure, the exact parallel of "publishes no per-alert
geometry". Zero matches is not a failure: "no alerts in my area" is the
normal steady state, and failing there would leave the entry unavailable
most of the time, inverting what unavailability means. That leaves a
typo'd prefix indistinguishable from a quiet area, so it surfaces instead
as a one-shot WARNING naming codes the feed actually publishes, re-armed
on the first match — the pattern _tracker_resolve_warned already uses.

Configurable in two places for one reason. The options flow is the home
of the setting, offered for every provider. The WMO filter menu gains a
step that writes the same option at creation time, because setting it
afterwards means creating hundreds of entities and immediately removing
most of them — precisely the condition this change exists to avoid. The
reconfigure step merges into existing options rather than passing
options= wholesale, which would silently drop scan_interval, timeout, and
the language selection; a test pins that.

A free-text field rather than a picker: enumerating areas would need a
full CAP-body sweep (~500 fetches for CMA) since the RSS envelope carries
no geocode, which is unacceptable latency inside a config flow. Worth
revisiting if SWIC ever exposes an area index.

Additive and unversioned — the key is absent on every existing entry, and
both readers use .get(), so no migration is needed and no existing mode
changes behavior. The prefix stays out of the entry title on purpose:
_compute_device_title reads entry data only, and options are editable
independently, so a title showing the prefix would go stale the moment it
changed under Configure.

Two adjacent doc errors the change forces. docs/architecture.md claimed
WMO has no GPS-tracker mode, which async_step_wmo_gps_tracker has always
contradicted, and README claimed WMO has no language option, which #72
added.

Closes #73

Assisted-by: Claude:claude-opus-5
Answers "does this source publish polygons?", "which <info> language does
this option select?", "how many entities would this entry create?" against
the real provider code path in seconds. The alternative — reconfiguring a
live entry and waiting for a coordinator cycle — costs minutes per
question and conflates provider behavior with HA setup and timeout
behavior.

Carries the geocode filter (#73) even though that is a coordinator step
rather than a provider one, applied in the same position via the same
function, because sizing an entry before creating it is exactly the
question the filter exists to answer. --active drops cancel/expired so
the count matches the entities HA would create: without it cn-cma-xx
reads 485 alerts for prefix 13, of which 119 are already cancelled, and
the number that matters is 30. --codes buckets published area codes by
prefix width, standing in for the region picker the filter deliberately
does not have.

Dev tooling only, not shipped in the integration — but scripts/ is in the
CI ruff scope, so it is linted and formatted like the rest.

Assisted-by: Claude:claude-opus-5
@seevee
seevee merged commit 7c0379a into main Aug 4, 2026
5 checks passed
@seevee
seevee deleted the feat/geocode-prefix-filter branch August 4, 2026 03:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WMO: add a geocode-prefix filter — country-wide is the only mode for sources without polygons (cn-cma-xx: 116 entities)

1 participant