feat(wmo): select the CAP <info> block by language - #72
Merged
Conversation
_select_info read infos[0] on the premise, stated in its own docstring, that "WMO feeds carry one language each". A sweep of all 140 SWIC sources on 2026-08-03 (RSS to first live CAP body, reading <language> off every <info>) found that false: of the 110 sources that sampled cleanly, 46 carry more than one block and 25 of those lead with a non-English one. Two consequences, one reported and one not. A user wanting Chinese on cn-cma-xx got en-US even though zh-CN sat in the same document — that is issue #59. Worse, at-zamg-en leads with de-DE, so the source whose ID ends "-en" served German, while ch-meteoswiss-de leads with en and served English; 15 of the 110 have a trailing ID segment that disagrees with their first block's primary subtag. Match the language option against each block: casefolded exact tag, then BCP 47 primary subtag, then any English block, then document order. The English step is what makes the fallback predictable — without it a German user on mo-smg-xx (zh-mo, pt-PT, en-US) would get Chinese. ECCC deliberately has no such step, since a French Canadian must not silently receive English, which is why the ladder lives here rather than in a shared helper. The whole block is selected, not just its text. Every multilingual source probed carries identical severity, expires, polygons and geocodes across its blocks, so there is nothing to merge and the GPS polygon filter is unaffected; a test pins that invariance. "auto" resolves from hass.config.language verbatim rather than truncated as MeteoAlarm does: WMO bodies carry full tags, so en-GB/en-US and pt-PT/pt-BR stay distinct, and the ladder degrades to the primary subtag on its own — which is what carries zh-Hans to zh-CN. The first non-selected block populates the *_alt fields, as ECCC and MeteoAlarm already do; the model fields, sparse serialization, and card rendering all already existed. Duplicate tags resolve first-match-wins. ca-aema-xx emits one <info> per area group (en-CA, fr-CA, en-CA, fr-CA), so only its first group survives — exactly what infos[0] did before, so this is a preserved limitation rather than a regression, and the same defect class as ECCC #45. Pinned by a test so a refactor cannot drift. The options flow gains a Language dropdown seeded from the 36 primary subtags observed in live bodies, accepting a custom value: 61 distinct tags appear across SWIC, and a fixed list would hard-reject zh-Hans, the tag this issue is about. Anything unmatched falls through the ladder to today's behavior rather than raising. Two nearby corrections the change forces. The dropdown label's language now comes from the registry's byLanguage rather than the source ID, which is wrong for those 15 sources; byLanguage is itself only a hint (it over-claims for 35 of the 110 and under-claims for 20), so it labels but never selects. And _WMO_SOURCE_RE encoded the same {country}-{agency}- {lang} assumption, rejecting three real registry IDs — lu-ana-meteo-fr, us-noaa-nws-en, us-noaa-nws-en-marine. Strictly widened, so nothing previously accepted is now rejected. No migration: both readers default to "auto", so existing entries pick up the behavior on upgrade. Single-language sources, sources whose blocks declare no <language>, and an unset option all still return infos[0]. The multilingual fixture is the reporter's own cn-cma-xx document. Fixes #59 Assisted-by: Claude:claude-opus-5
seevee
added a commit
that referenced
this pull request
Aug 4, 2026
* feat(coordinator): filter alerts by area-code prefix 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 * chore(scripts): add a provider probe for feed-shape questions 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.
Fixes #59.
The defect
_select_inforeadinfos[0]on the premise, stated in its own docstring, that "WMO feeds carry one language each". A sweep of all 140 SWIC sources on 2026-08-03 (RSS → first live CAP body, reading<language>off every<info>) found that false — of the 110 that sampled cleanly, 46 carry more than one block and 25 of those lead with a non-English one:<info>languagescn-cma-xxen-US → zh-CNat-zamg-ende-DE → en-GBch-meteoswiss-deen → de → fr → it → rmmo-smg-xxzh-mo → pt-PT → en-USbe-irm-ennl-BE → fr-BE → en-GB → de-DETwo consequences. The reported one: a user wanting Chinese on
cn-cma-xxgoten-US, thoughzh-CNsat in the same document. The unreported and worse one:at-zamg-enleads withde-DE, so the source whose ID ends-enserved German, whilech-meteoswiss-deleads withenand served English. 15 of the 110 have a trailing ID segment that disagrees with their first block's primary subtag.The change
_select_info(doc, language)matches each block: casefolded exact tag → BCP 47 primary subtag → any English block → document order. The English step is what makes the fallback predictable — without it a German user onmo-smg-xxwould get Chinese. ECCC deliberately has no such step (a French Canadian must not silently receive English), which is why the ladder lives inwmo.pyrather than a shared helper; consolidating the three providers' matchers is a follow-up, not this PR.The whole block is selected, not just its text. Every multilingual source probed carries identical
severity,expires, polygons and geocodes across its blocks, so there is nothing to merge and the GPS polygon filter is unaffected — pinned by an invariance test.autoresolves fromhass.config.languageverbatim rather than truncated as MeteoAlarm does: WMO bodies carry full tags, soen-GB/en-USandpt-PT/pt-BRstay distinct, and the ladder degrades to the primary subtag on its own — which is what carrieszh-Hans → zh-CN.The first non-selected block populates the
*_altfields, as ECCC and MeteoAlarm already do. The model fields, sparse serialization, and card rendering all already existed, somodel.pyis untouched.User-visible
zh-Hans, the tag this issue is about). Defaultauto.headline_alt/description_alt/instruction_alt/language_alt.Austria (ZAMG, de/en)instead of the currently-wrongAustria (ZAMG, en).lu-ana-meteo-fr,us-noaa-nws-en,us-noaa-nws-en-marinebecome enterable — the validator rejected these real registry IDs.No migration: both readers default to
"auto", so existing entries pick up the behavior on upgrade.Two nearby corrections the change forces
The label's language now comes from the registry's
byLanguagerather than the source ID.byLanguageis itself only a hint — it over-claims for 35 of the 110 and under-claims for 20 — so it labels but never selects. And_WMO_SOURCE_REencoded the same{country}-{agency}-{lang}assumption this PR corrects; it is strictly widened, so nothing previously accepted is now rejected.Preserved limitation
Duplicate tags resolve first-match-wins.
ca-aema-xxemits one<info>per area group (en-CA → fr-CA → en-CA → fr-CA), so only its first group survives — exactly whatinfos[0]did before, so this is preserved rather than regressed. Same defect class as #45; fixing it means emitting multiple alerts per document, an identity/lifecycle change. Pinned by a test so a refactor cannot drift silently. Follow-up issue to file.No regressions
Single-language sources, sources whose blocks declare no
<language>(nz-nms-en,ph-pagasa-en,sg-mss-en,tt-ttms-en), and an unset option all still returninfos[0], for every value oflanguage. Malformed tags found live (en-EN,mk-MKD,sr-Latn,TL) degrade to today's behavior rather than raising; covered by a parametrized test.Verification
pytest tests -qmain: 538)ruff check/ruff formatmypy custom_components/cap_alertsNew:
tests/test_coordinator_language.py,tests/test_wmo_config_flow.py, plus language coverage intest_wmo_provider.pyand label coverage intest_wmo_sources.py. No existing assertion is weakened; the only ones changed are thetest_wmo_sources.pylabel strings, which change because the label format deliberately does.The multilingual fixture is @MF-142's own
cn-cma-xxdocument, attached to the issue.Assisted-by: Claude:claude-opus-5