feat(coordinator): filter alerts by area-code prefix - #74
Merged
Conversation
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
This was referenced Aug 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
cn-cma-xx(China, CMA) is the confirmed case — 0 of 60 sampled bodies carried a<polygon>— and it is also the source carrying thezh-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: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:11314413Why provider-neutral, not a WMO filter mode
The original plan scoped this to
providers/wmo.pyas a fourth filter mode. It landed incoordinator._applyinstead, because every provider already populates thegeocodescontainer — nothing in the matcher is WMO-specific — andexclude_marineis 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
13= Hebei,1307= Zhangjiakou,130709000000= Chongli. Exact matching would make the user enumerate every county code — 28 of them for Hebei alone.CPEAS Geographic Codeappears in no existing table), so every value under everyvalueNameis compared. A colliding prefix over-matches rather than dropping wanted alerts.startswithwith 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.UpdateFailedfires 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-shotWARNINGnaming codes the feed actually publishes, re-armed on the first match — the pattern_tracker_resolve_warnedalready uses.Configuration
Two entry points, one setting:
The reconfigure step merges into existing options rather than passing
options=wholesale, which would silently dropscan_interval,timeout, and the language selection. A test pins that.The prefix stays out of the entry title on purpose:
_compute_device_titlereads 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 includingzh-Hansunder-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--activefor 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=123coerced tointbecause 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_20260804103516→522727, 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