Skip to content

feat(meteoalarm): merge MeteoFrance forecast days into one episode - #70

Merged
seevee merged 2 commits into
mainfrom
feat/meteofrance-episode-merge
Aug 3, 2026
Merged

feat(meteoalarm): merge MeteoFrance forecast days into one episode#70
seevee merged 2 commits into
mainfrom
feat/meteofrance-episode-merge

Conversation

@seevee

@seevee seevee commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Closes #37. Follows #68, which removed the green markers that would otherwise collide inside this merge.

Problem

MeteoFrance publishes one warning per calendar day, each running roughly 00:00 → 00:00 local, and the next day's bulletin goes live alongside the current day's for most of the day — live sampling found the overlap in 170 of 227 samples, spread across every hour rather than confined to an afternoon window.

The MeteoFrance content key from #41 includes a forecast-day component, so one multi-day heat or storm episode became one entity per day, with the id rolling over at midnight. That is what the reporter sees: two Vigilance jaune canicule entities, contiguous at the midnight boundary, and a fresh entity every morning.

Fix

Collapse a run of consecutive forecast days into one alert, keyed without the day component.

Region-picker mode explodes the bulletin first, into one alert per configured department, using the <area> blocks that already carry one areaDesc and one NUTS3 code each. The key becomes (sender, phenomenon, one department).

That choice is the crux, and it is measured rather than assumed. The department set a bulletin covers moves overnight:

awareness_type 08-03 08-04 delta
12 3 3 identical
13 1 1 identical
3 (thunderstorm) 83 54 +5 / −34
5 (heat) 57 54 +0 / −3

So a set-derived key — or its intersection with a multi-department config — splits the episode anyway. Per-department scoping is stable, and is safe because live sampling shows at most one live warning per (department, phenomenon, day) across 203 samples. It also replaces an area_desc listing up to 83 departments with the one the user selected.

The most severe day supplies the content wholesale, tie-broken to the earliest onset; the window widens to span the run. Blending fields would let the record contradict itself, since severity_normalized derives from awareness_level and the icon from event — a blend could read "Vigilance jaune canicule" while carrying an orange level. Per-day truth moves to a new episode_days attribute.

Result on live data

Replayed against a real France feed at a fixed instant:

entities per department BEFORE: {1: 22, 2: 6, 3: 20, 4: 39, 6: 1}
entities per department AFTER:  {1: 28, 2: 59, 3: 1}
total across 88 departments: 256 -> 149
FR211 (Ardennes), after:
  'Vigilance jaune canicule'  2026-08-03T16:04 -> 2026-08-05T00:00  days=['08-03','08-04']
  'Vigilance jaune orages'    2026-08-03T16:04 -> 2026-08-05T00:00  days=['08-03','08-04']

Nothing is lost in the process: 88 → 88 departments covered, 149 → 149 (department, phenomenon) cells, and zero departments losing a phenomenon in region mode. I checked this explicitly because a wholesale dominant-day copy could plausibly have dropped coverage.

Two things the plan got wrong, found while building

  • Gap-day runs collided. Two live runs in one group both got the day-free id, so the alert store would have silently dropped one — the exact failure the merge exists to avoid. Second and later runs now re-add their first day to the key, which churns only the pending entity and never the one in effect. The case has never been observed live (0/227 samples), but it must not lose a warning if it happens.
  • episode_days on single-day runs was pure noise restating the alert's own fields. Now empty unless something actually merged, keeping attributes sparse.

Scope and limitations

  • Country-wide mode keeps the full-set key and still splits an episode when the footprint moves. Documented rather than silent: exploding per department there would turn France into roughly 150 entities.
  • A gap of more than one calendar day starts a new run, read as a separate episode. That reading is unproven — 0 of 227 samples show a gap — and getting it wrong costs an extra entity rather than a lost warning.
  • GPS mode is unreachable for MeteoFrance: the France feed carries zero geometry (0 polygons across 6742 area blocks), so _filter_by_polygon fails loud first. The branch is defensive only.
  • A horizon/outlook filter was rejected — the merge subsumes it. Live depth is two forecast days, not the four the J/J+1/J+2/J+3 framing implies; that larger figure counted superseded messages.

Breaking change

MeteoFrance entity ids change once on upgrade; stale entities are safe to delete. Multi-department configurations also get a granularity change — one entity per configured department instead of one per bulletin — so they will see more entities than before, each stably named and identified. Both need announcing in the release notes and on #37.

Every other authority and provider is untouched, guarded by pass-through tests.

Also in this PR

  • _filter_by_regions returns to pure filtering; identity is owned by the merge that runs after it.
  • normalize exports SEVERITY_RANK and meteoalarm_awareness_severity so the provider ranks days on the same ladder normalization applies, instead of a second copy that can drift.

Verification

Check Result
pytest tests -q 538 passed (was 521)
ruff check / ruff format --diff passed / 58 files already formatted
mypy custom_components/cap_alerts no issues, 22 source files

17 new tests in tests/test_meteoalarm_episodes.py. Payloads are built rather than stored as JSON fixtures — the shapes differ from one another by a field or two, so a builder keeps the difference legible where five near-identical fixture files would bury it. Every test injects now, since the merge is now clock-dependent.

Two existing tests changed, both anticipated: test_region_scope_stable_across_set_churn became test_region_filter_is_pure_filtering (its scope-stability assertion is now covered end-to-end by test_episode_stable_across_footprint_churn), and test_region_picker_intersects_nuts3 pins now inside its fixture's window.

Refs #37

Assisted-by: Claude:claude-opus-5

seevee added 2 commits August 3, 2026 16:13
MeteoFrance publishes one warning per calendar day, each running roughly
00:00 to 00:00 local, and the next day's bulletin goes live alongside the
current day's for most of the day — live sampling found the overlap in
170 of 227 samples, spread across every hour rather than confined to an
afternoon window. Because the MeteoFrance content key from #41 includes
a forecast-day component, a single multi-day heat or storm episode
became one entity per day and the id rolled over at midnight, breaking
whatever referenced it. That is the defect #37 reports.

Collapse a run of consecutive forecast days into one alert, keyed
without the day component so it survives midnight.

Region-picker mode explodes the bulletin first, into one alert per
configured department, using the <area> blocks that already carry one
areaDesc and one NUTS3 code each. The episode key becomes (sender,
phenomenon, one department), which is what makes it stable: the set of
departments a bulletin covers moves overnight — a thunderstorm bulletin
was measured going from 83 departments to 54 — so a set-derived key, or
its intersection with a multi-department config, splits the episode
anyway. It also replaces an area_desc listing up to 83 departments with
the one the user actually selected.

The most severe day supplies the content wholesale, tie-broken to the
earliest onset, and the window widens to span the run. Blending fields
would let the record contradict itself, since severity_normalized comes
from awareness_level and the icon from event. Per-day truth moves to a
new episode_days attribute, absent on single-day runs where it would
only restate the alert's own fields.

Finished days are dropped before merging: otherwise a finished run and
an upcoming run share the day-free id and the alert store, which keys by
id, silently drops one. This makes the provider clock-dependent, so
async_fetch takes an injectable now. A gap of more than one calendar day
starts a new run; second and later runs re-add their first day to the
key so two live runs can never collide, churning only the pending entity
and never the one in effect. That gap case has never been observed live
(0 of 227 samples), so getting the reading wrong costs an extra entity
rather than a lost warning.

Country-wide mode keeps the full-set key and still splits an episode
when the footprint moves. Documented, not silent: exploding per
department there would turn France into roughly 150 entities.

_filter_by_regions goes back to pure filtering — identity is now owned
by the merge, which runs after it — and normalize exports SEVERITY_RANK
and meteoalarm_awareness_severity so the provider ranks days on the same
ladder normalization applies, rather than a second copy that can drift.

Measured against a live France feed at a fixed instant: 256 entities
across 88 departments become 149, the most any one department carries
drops from 6 to 3, and no (department, phenomenon) cell is lost.

MeteoFrance entity ids change once on upgrade; stale entities are safe
to delete. Other authorities and providers are untouched.

Refs #37

Assisted-by: Claude:claude-opus-5
Review of the episode merge found three places where the words no longer
matched the code, plus one framing gap. No behavior change.

- events.md claimed an episode losing its earliest day is invisible. It
  usually is not: the dominant-day tie-break prefers the earliest day, so
  when that day finishes the content flips to the next day's headline and
  description — both allowlisted — and incident_updated fires. Describe
  the actual cadence, and scope the incident_removed sentence to the
  whole episode finishing rather than any single day.

- The pre-merge identity docstrings (_forecast_window_key,
  _meteofrance_id, _compute_alert_id) still described forecast days as
  distinct entities. Shipped MeteoFrance ids are minted by the merge
  with an empty window_key; the day component survives only as the
  collision tie-breaker for a second live run. Say so, and mark the
  parse-time MeteoFrance id as provisional.

- test_distinct_forecast_day_distinct_id's comment said the 4-day
  outlook "is not collapsed" — the opposite of shipped behavior, and an
  explicit acceptance item in the plan. Reword to per-warning parsing
  before the merge.

- architecture.md now acknowledges per-day publication is MeteoFrance's
  deliberate product model (the two-panel vigilance map), not a feed
  quirk: the defect was this integration's 1:1 mapping of that model
  onto durable HA entities, and the merge re-maps it rather than
  corrects it.

Refs #37

Assisted-by: Claude:claude-fable-5
@seevee
seevee merged commit a4066a6 into main Aug 3, 2026
5 checks passed
@seevee
seevee deleted the feat/meteofrance-episode-merge branch August 3, 2026 23:07
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.

MeteoFrance: one alert entity per forecast day instead of per episode (MeteoAlarm)

1 participant