From f4930a388f0166cf0c4456eaf5d8beba4cc1199c Mon Sep 17 00:00:00 2001 From: "Matt Johnson (via Claude)" Date: Tue, 9 Jun 2026 17:45:41 +0000 Subject: [PATCH] feat: fire halt broadcast + tombstone all-clear (wildfire_halted / wildfire_closed) Enable _maybe_emit_halt in firms_handler (remove return None stub) so fires with no FIRMS hotspot activity beyond halt_minimum_seconds emit a routine wildfire_halted broadcast. Add tombstone all-clear in wfigs_handler: when a fire is tombstoned and has last_broadcast_at set (i.e. previously made it to mesh), broadcast a wildfire_closed message with acres, containment, and location. Fires that were never broadcast are silently consumed. Co-Authored-By: Claude Opus 4.6 --- meshai/central/firms_handler.py | 1 - meshai/central/wfigs_handler.py | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/meshai/central/firms_handler.py b/meshai/central/firms_handler.py index 5626025..64b058b 100644 --- a/meshai/central/firms_handler.py +++ b/meshai/central/firms_handler.py @@ -774,7 +774,6 @@ def _maybe_emit_halt(conn, *, data, now): eligible because we filter on `halt_broadcast_at IS NULL OR halt_broadcast_at < last_pass_at`. """ - return None minimum_s = int(adapter_config.fires.halt_minimum_seconds) cutoff = float(now) - float(minimum_s) row = conn.execute( diff --git a/meshai/central/wfigs_handler.py b/meshai/central/wfigs_handler.py index 9827c37..7ff9f63 100644 --- a/meshai/central/wfigs_handler.py +++ b/meshai/central/wfigs_handler.py @@ -116,6 +116,40 @@ def handle_wfigs(normalized: dict, envelope: dict, subject: str, ) except Exception: logger.exception("wfigs: tombstoned_at stamp failed irwin=%s", irwin_id) + + # All-clear broadcast: only fires that previously made it to mesh + # get a closure message. Silent for fires that were never broadcast. + if kind == "wfigs_tombstone" and irwin_id: + fire_row = conn.execute( + "SELECT incident_name, current_acres, current_contained_pct, " + "last_broadcast_at, county, state, lat, lon " + "FROM fires WHERE irwin_id = ?", (irwin_id,) + ).fetchone() + if fire_row is not None and fire_row["last_broadcast_at"] is not None: + name = fire_row["incident_name"] or "(unnamed fire)" + # Build line 2 parts + parts = [] + if fire_row["current_acres"] is not None: + parts.append(f"{int(fire_row['current_acres']):,} ac") + if fire_row["current_contained_pct"] is not None: + parts.append(f"{int(fire_row['current_contained_pct'])}% contained") + # Location via _location_anchor with a minimal normalized dict + loc_dict = { + "lat": fire_row["lat"], "lon": fire_row["lon"], + "county": fire_row["county"], "state": fire_row["state"], + } + anchor = _location_anchor(loc_dict) + if anchor and anchor != "(location unknown)": + parts.append(anchor) + lines = [f"✅ {name} — contained & closed"] + if parts: + lines.append(" | ".join(parts)) + wire = "\n".join(lines) + if isinstance(data, dict): + data["category"] = "wildfire_closed" + data["_severity_override"] = "routine" + return wire + return None # ---- active incident ----