mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
139 lines
6.1 KiB
Python
139 lines
6.1 KiB
Python
"""SWPC space-weather event formatter — Phase-1 refactor.
|
|
|
|
Reads canonical event.data schema:
|
|
driver : "kp" | "flare" | None — what drove the event (proton/S-scale
|
|
events carry driver=None from env/swpc.py; dispatched below
|
|
via scale_code prefix "S", not driver)
|
|
scalar : float (Kp value) | str (flare class | proton "N pfu") | None
|
|
scale_code : "G3" | "R3" | "S1" | etc. — NOAA scale code
|
|
message : str — raw SWPC alert message body (or "")
|
|
issued_at : str | None — ISO timestamp for the time tag line
|
|
|
|
Wire format (multi-line, identical structure to swpc_handler._render()):
|
|
Geomag: 🧲 New: G3 Geomagnetic Storm — Kp7
|
|
HF degraded, aurora possible
|
|
SWPC · 2026-07-04 05:09
|
|
|
|
Flare: ☀️ New: X1.0 Solar Flare — R3
|
|
HF radio fading, GPS may glitch
|
|
SWPC · 2026-06-03 11:59
|
|
|
|
Proton: ☢️ New: S1 Radiation Storm — 10 pfu
|
|
Polar HF radio affected
|
|
SWPC · 2026-07-04 06:00
|
|
|
|
(when scalar is None the dash-separated tail is omitted; native env/swpc.py
|
|
never supplies a proton scalar, so live S-scale wires omit the tail)
|
|
|
|
Tier-b fix note: `_severity_override` is set by the decider (gating/swpc.py)
|
|
so geomag/flare events dispatch at priority/immediate severity instead of the
|
|
"routine" default the old swpc_handler always produced.
|
|
|
|
All other formatting is identical to swpc_handler._render().
|
|
|
|
Time contract: `now` is accepted but not used for rendering (structural seam
|
|
for future relative-time annotations). All time reads MUST go through
|
|
meshai.notifications.clock — never the stdlib equivalents — so golden-file
|
|
tests can freeze the clock via monkeypatch.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from meshai.notifications.formatters._budget import fit_to_budget
|
|
|
|
if TYPE_CHECKING:
|
|
from meshai.notifications.events import Event
|
|
|
|
|
|
def _trunc(s: str, limit: int = 120) -> str:
|
|
"""Truncate *s* at the last word boundary at or before *limit* chars."""
|
|
if len(s) <= limit:
|
|
return s
|
|
cut = s[:limit].rsplit(" ", 1)[0]
|
|
if not cut:
|
|
cut = s[:limit]
|
|
return cut + "…"
|
|
|
|
|
|
def format(event: "Event", *, now: float, budget: int) -> str:
|
|
"""Render SWPC wire string from canonical event.data.
|
|
|
|
Args:
|
|
event: Pipeline Event — reads from event.data (canonical schema).
|
|
now: Frozen-clock epoch (structural seam; not used in rendering).
|
|
budget: Mesh-packet character budget.
|
|
|
|
Returns:
|
|
UTF-8 string fitting within *budget* characters.
|
|
"""
|
|
d = event.data or {}
|
|
|
|
driver = d.get("driver") # "kp" | "flare"
|
|
scalar = d.get("scalar") # float (Kp) | str (flare class) | None
|
|
scale_code = d.get("scale_code") or "" # "G3", "R3", etc.
|
|
|
|
# ── Detail line (line 2) ─────────────────────────────────────────────────
|
|
message = d.get("message") or ""
|
|
if isinstance(message, str):
|
|
message = _trunc(message.strip())
|
|
else:
|
|
message = ""
|
|
|
|
# ── Time tag (line 3) ────────────────────────────────────────────────────
|
|
time_tag = ""
|
|
issued_at = d.get("issued_at") or d.get("time_tag") or ""
|
|
if isinstance(issued_at, str) and issued_at:
|
|
time_tag = issued_at[:16].replace("T", " ")
|
|
|
|
prefix = "New:" # SWPC events are point-in-time; "Update:" not used
|
|
|
|
if driver == "kp":
|
|
# ── Geomagnetic storm ────────────────────────────────────────────────
|
|
if isinstance(scalar, (int, float)):
|
|
scalar_str: str | None = f"Kp{int(round(scalar))}"
|
|
else:
|
|
scalar_str = None
|
|
|
|
if scalar_str:
|
|
line1 = f"🧲 {prefix} {scale_code} Geomagnetic Storm — {scalar_str}"
|
|
else:
|
|
line1 = f"🧲 {prefix} {scale_code} Geomagnetic Storm"
|
|
|
|
line2 = message if message else "HF degraded, aurora possible"
|
|
line3 = f"SWPC · {time_tag}" if time_tag else "SWPC"
|
|
|
|
elif driver == "flare":
|
|
# ── Solar flare ──────────────────────────────────────────────────────
|
|
if isinstance(scalar, str) and scalar:
|
|
line1 = f"☀️ {prefix} {scalar} Solar Flare — {scale_code}"
|
|
else:
|
|
line1 = f"☀️ {prefix} {scale_code} Solar Flare"
|
|
|
|
line2 = message if message else "HF radio fading, GPS may glitch"
|
|
line3 = f"SWPC · {time_tag}" if time_tag else "SWPC"
|
|
|
|
elif scale_code.startswith("S"):
|
|
# ── Solar radiation storm (proton) ──────────────────────────────────────
|
|
# Dispatched on scale_code, not driver: env/swpc.py's native adapter
|
|
# always sets driver=None for S-scale events (no per-reading pfu value
|
|
# available from noaa-scales.json), so there is no "proton" driver value
|
|
# to switch on. scalar (when present, e.g. from a future producer with a
|
|
# real pfu reading) is a pre-formatted string like "10 pfu", matching
|
|
# central/swpc_handler.py's scalar_str convention for this event kind.
|
|
if isinstance(scalar, str) and scalar:
|
|
line1 = f"☢️ {prefix} {scale_code} Radiation Storm — {scalar}"
|
|
else:
|
|
line1 = f"☢️ {prefix} {scale_code} Radiation Storm"
|
|
|
|
line2 = message if message else "Polar HF radio affected"
|
|
line3 = f"SWPC · {time_tag}" if time_tag else "SWPC"
|
|
|
|
else:
|
|
# ── Unknown driver (fallback — should not occur in normal operation) ─
|
|
line1 = f"⚠️ {prefix} Space Weather Event — {scale_code or '?'}"
|
|
line2 = message if message else None
|
|
line3 = f"SWPC · {time_tag}" if time_tag else "SWPC"
|
|
|
|
msg = "\n".join(l for l in [line1, line2, line3] if l)
|
|
return fit_to_budget(msg, budget)
|