mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 01:14:45 +02:00
Sixth family of the v0.5.7 NATS-and-categories campaign. RF family = native ducting calculator + three Central SWPC adapters (swpc_alerts, swpc_kindex, swpc_protons), all umbrella-subscribed under `central.space.>`.
Per the family-by-family pattern: cross-checked every prompt assumption against the Central v0.10.0 guide before implementing. The big surprise this phase: FIX 1 was already correct (no NATS-syntax bug to fix), and FIX 2 was a non-bug too (severity=0 already routes safely). The real work was FIX 3 -- four missing registry entries that meshai emits but the rule editor couldn't target.
FIX 1 -- SWPC subject pattern (already correct; pinned). Per Central v0.10.0 guide §swpc_alerts / §swpc_kindex / §swpc_protons, all three adapters publish under the `central.space.>` umbrella with no region in subject (space weather is planetary):
swpc_alerts: central.space.alert.<product_id> (4 tokens, product_id tail)
swpc_kindex: central.space.kindex (3 tokens, fixed)
swpc_protons: central.space.proton_flux (3 tokens, fixed)
`_subjects_for("swpc", region)` already returned `["central.space.>"]` ignoring region (v0.5.4 work got this right). Added an explanatory inline comment near the table entry calling out each adapter's concrete subject + the universal severity=0 contract (next fix), plus a test pinning the umbrella + region-ignored behavior + coverage of each per-adapter subject form. Future "let me add a region tail here" refactors will fail loudly.
FIX 2 -- swpc_protons severity=0 routing (non-bug; regression-guard pin). The prompt described a "severity=0 silently dropped" failure mode. Investigation: no such bug exists in current code.
- All three SWPC adapters publish severity=0 in the live guide samples.
- consumer.map_severity already maps 0 -> "routine" (the `if sev >= 3:`
immediate clamp doesn't hit; falls through to the default return).
- NotificationToggle.severity_channels is dict-keyed by severity STRING
(locked in by v0.5.7-seismic test_severity_channels_is_string_keyed_no_int_indexerror_risk);
"routine" is a valid key with no IndexError vector.
Three things tightened anyway: (a) inline comment near the swpc subject entry documenting "all three publish severity=0 -> routine per guide examples"; (b) end-to-end synthetic envelope test for swpc_protons injection (severity=0 in, ev.severity="routine" / ev.category="solar_radiation_storm" / ev.source="swpc" out, no exception); (c) parallel test for swpc_kindex confirming a second SWPC adapter wires identically.
FIX 3 -- ALERT_CATEGORIES rf_propagation audit. Pre-v0.5.7-rf registry had three entries under toggle="rf_propagation": hf_blackout, geomagnetic_storm, tropospheric_ducting. Audit:
Native ducting.py emits via _TIER_CATEGORY:
super_refraction -> rf_anomalous_propagation
duct -> rf_ducting_enhancement
surface_duct -> rf_ducting_enhancement
Central path via map_category:
space.alert.* -> rf_propagation_alert (swpc_alerts)
space.kindex -> geomagnetic_storm (swpc_kindex; already in registry)
space.proton_flux -> solar_radiation_storm (swpc_protons)
space.* catchall -> geomagnetic_storm
Four categories emitted but missing from the registry -- rule editor couldn't target them. Added all four under toggle="rf_propagation" with name + description + default_severity + example_message matching the guide-documented behavior:
rf_anomalous_propagation (routine, ducting super_refraction tier)
rf_ducting_enhancement (priority, ducting duct + surface_duct tiers)
rf_propagation_alert (priority, NOAA SWPC space-weather product)
solar_radiation_storm (priority, GOES proton flux S-scale)
composer.py emoji + label tables gained matching entries so live LoRa rendering shows the right glyphs (📡 for ducting forms, ⚠ for SWPC alerts, 🌐 for solar radiation, all labelled "RF").
Legacy entries kept (forward-compat / no current emitter): hf_blackout and tropospheric_ducting remain in the registry as selectable rule targets even though no current code path emits them. Reasoning:
- hf_blackout: HF-specific R-scale parsing of swpc_alerts.message could
re-introduce this emission in a future phase; removing the registry
entry would break any user rule currently configured to target it.
- tropospheric_ducting: legacy name superseded by rf_ducting_enhancement
in native ducting.py; same forward-compat concern -- a future phase
may emit a "tropospheric" specialization separate from generic ducts.
If either remains un-emitted by v0.6, file a follow-up cleanup phase to remove. Test_alert_categories_rf_complete uses a SUBSET assertion (emit set ⊆ registry) rather than equality so legacy entries are allowed.
Audit table after v0.5.7-rf:
Registry rf_propagation (7):
hf_blackout (legacy, no current emitter)
geomagnetic_storm (central swpc_kindex + catchall)
tropospheric_ducting (legacy, no current emitter)
rf_anomalous_propagation [v0.5.7-rf NEW] (native ducting super_refraction)
rf_ducting_enhancement [v0.5.7-rf NEW] (native ducting duct + surface_duct)
rf_propagation_alert [v0.5.7-rf NEW] (central swpc_alerts)
solar_radiation_storm [v0.5.7-rf NEW] (central swpc_protons)
Emit set ⊆ Registry: TRUE (no orphan emissions).
Tests
-----
PYTHONPATH=. pytest -q: 431 passed (was 413; +18 net).
- tests/test_rf_v057.py (new): umbrella subject is `central.space.>` for all regions; per-adapter published subjects all match; map_severity(0) -> "routine"; NotificationToggle.severity_channels dict-keyed (no IndexError); synthetic swpc_protons + swpc_kindex envelopes route cleanly with severity=0; four new rf_propagation entries all registry-present with required fields; geomagnetic_storm still mapped from space.kindex; map_category routing pinned for each SWPC adapter; native ducting + central SWPC emit sets are subsets of registry rf entries.
Safe-mode preserved (master off, all family toggles off, all adapters native, central disabled). No live toggle flipped. Not tagging yet -- v0.5.7 tag waits until all families ship.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
498 lines
23 KiB
Python
498 lines
23 KiB
Python
"""Alert category registry.
|
||
|
||
Defines all alertable conditions with human-readable names, descriptions,
|
||
and example messages showing what users will receive.
|
||
|
||
Severity levels (military/intelligence precedence):
|
||
routine - Informational, no time pressure
|
||
priority - Needs attention soon
|
||
immediate - Act now, drop everything
|
||
|
||
Toggle categories (for v0.3 notification routing):
|
||
mesh_health - infrastructure, power, utilization, coverage, health-score
|
||
weather - NWS-sourced alerts, stream flooding
|
||
fire - NIFC perimeters, FIRMS hotspots
|
||
rf_propagation - solar, geomagnetic, ducting, band conditions
|
||
roads - 511, TomTom traffic
|
||
avalanche - avalanche advisories
|
||
seismic - USGS quakes (Phase 3)
|
||
tracking - ADS-B, AIS, satellite passes (Phase 7)
|
||
"""
|
||
|
||
from typing import Optional
|
||
|
||
|
||
# Valid toggle values for v0.3 pipeline
|
||
VALID_TOGGLES = frozenset({
|
||
"mesh_health",
|
||
"weather",
|
||
"fire",
|
||
"rf_propagation",
|
||
"roads",
|
||
"avalanche",
|
||
"seismic",
|
||
"tracking",
|
||
})
|
||
|
||
|
||
# Prefix fallback for categories not enumerated in ALERT_CATEGORIES (resolves the
|
||
# v0.4 "category -> other" gap for phases 2.7-2.14 emitted categories).
|
||
_TOGGLE_PREFIX_FALLBACK = [
|
||
("weather", "weather"),
|
||
# v0.5.2: stream_* (USGS hydro) belongs with Geohazards, not weather
|
||
("stream", "seismic"),
|
||
("wildfire", "fire"),
|
||
("fire", "fire"),
|
||
("earthquake", "seismic"),
|
||
("quake", "seismic"),
|
||
("traffic", "roads"),
|
||
("road", "roads"),
|
||
("geomagnetic", "rf_propagation"),
|
||
("solar_radiation", "rf_propagation"),
|
||
("rf_", "rf_propagation"),
|
||
("avalanche", "avalanche"),
|
||
]
|
||
|
||
|
||
ALERT_CATEGORIES = {
|
||
# Infrastructure alerts
|
||
"infra_offline": {
|
||
"name": "Infrastructure Node Offline",
|
||
"description": "An infrastructure node (router/repeater) stopped responding",
|
||
"default_severity": "priority",
|
||
"example_message": "⚠ Infrastructure Offline: MHR — Mountain Harrison Rptr has not been heard for 2 hours",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"critical_node_down": {
|
||
"name": "Critical Node Down",
|
||
"description": "A node you marked as critical went offline",
|
||
"default_severity": "immediate",
|
||
"example_message": "🚨 Critical Node Down: HPR — Hayden Peak Rptr offline for 1 hour",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"infra_recovery": {
|
||
"name": "Infrastructure Recovery",
|
||
"description": "An offline infrastructure node came back online",
|
||
"default_severity": "routine",
|
||
"example_message": "✅ Recovery: MHR — Mountain Harrison Rptr back online after 2h outage",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"new_router": {
|
||
"name": "New Router",
|
||
"description": "A new router appeared on the mesh",
|
||
"default_severity": "routine",
|
||
"example_message": "📡 New Router: Snake River Relay appeared in Wood River Valley",
|
||
"toggle": "mesh_health",
|
||
},
|
||
|
||
# Power alerts
|
||
"battery_warning": {
|
||
"name": "Battery Warning",
|
||
"description": "Infrastructure node battery below 30% (3.60V)",
|
||
"default_severity": "routine",
|
||
"example_message": "🔋 Battery Warning: BLD-MTN at 28% (3.58V), solar not charging",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"battery_critical": {
|
||
"name": "Battery Critical",
|
||
"description": "Infrastructure node battery below 15% (3.50V)",
|
||
"default_severity": "priority",
|
||
"example_message": "🔋 Battery Critical: BLD-MTN at 12% (3.48V) — shutdown in hours",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"battery_emergency": {
|
||
"name": "Battery Emergency",
|
||
"description": "Infrastructure node battery below 5% (3.40V) — shutdown imminent",
|
||
"default_severity": "immediate",
|
||
"example_message": "🚨 Battery Emergency: BLD-MTN at 4% (3.38V) — shutdown imminent",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"battery_trend": {
|
||
"name": "Battery Declining",
|
||
"description": "Battery showing declining trend over 7 days — possible solar or charging issue",
|
||
"default_severity": "routine",
|
||
"example_message": "🔋 Battery Trend: HPR declining 85% → 62% over 7 days (-3.3%/day)",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"power_source_change": {
|
||
"name": "Power Source Change",
|
||
"description": "Node switched from USB to battery — possible power outage at site",
|
||
"default_severity": "priority",
|
||
"example_message": "⚡ Power Source: MHR switched from USB to battery — possible outage",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"solar_not_charging": {
|
||
"name": "Solar Not Charging",
|
||
"description": "Solar panel not charging during daylight hours — panel issue or obstruction",
|
||
"default_severity": "priority",
|
||
"example_message": "☀️ Solar Issue: BLD-MTN not charging during daylight (12:00 MDT)",
|
||
"toggle": "mesh_health",
|
||
},
|
||
|
||
# Utilization alerts
|
||
"high_utilization": {
|
||
"name": "Channel Airtime High",
|
||
"description": "LoRa channel airtime exceeding threshold — mesh congestion",
|
||
"default_severity": "routine",
|
||
"example_message": "📊 Channel Airtime: 47% utilization (threshold: 40%). Reliability may degrade.",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"sustained_high_util": {
|
||
"name": "Sustained High Utilization",
|
||
"description": "Channel airtime elevated for extended period — ongoing congestion",
|
||
"default_severity": "priority",
|
||
"example_message": "📊 Sustained Congestion: 45% channel utilization for 2+ hours. Consider reducing telemetry.",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"packet_flood": {
|
||
"name": "Packet Flood",
|
||
"description": "A single node sending excessive radio packets (NOT water flooding) — possible firmware bug or stuck transmitter",
|
||
"default_severity": "priority",
|
||
"example_message": "📻 Packet Flood: Node 'BKBS' transmitting 42 packets/min (threshold: 10/min). Firmware bug?",
|
||
"toggle": "mesh_health",
|
||
},
|
||
|
||
# Coverage alerts
|
||
"infra_single_gateway": {
|
||
"name": "Single Gateway",
|
||
"description": "Infrastructure node dropped to single gateway coverage — reduced redundancy",
|
||
"default_severity": "priority",
|
||
"example_message": "📶 Reduced Coverage: HPR dropped to single gateway. Previously had 3 paths.",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"feeder_offline": {
|
||
"name": "Feeder Offline",
|
||
"description": "A feeder gateway stopped responding — coverage gap possible",
|
||
"default_severity": "priority",
|
||
"example_message": "📡 Feeder Offline: AIDA-N2 gateway not responding. 5 nodes may lose uplink.",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"region_total_blackout": {
|
||
"name": "Region Blackout",
|
||
"description": "All infrastructure in a region is offline — complete coverage loss",
|
||
"default_severity": "immediate",
|
||
"example_message": "🚨 REGION BLACKOUT: All infrastructure in Magic Valley offline!",
|
||
"toggle": "mesh_health",
|
||
},
|
||
|
||
# Health score alerts
|
||
"mesh_score_low": {
|
||
"name": "Mesh Health Low",
|
||
"description": "Overall mesh health score dropped below threshold — multiple issues likely",
|
||
"default_severity": "priority",
|
||
"example_message": "📉 Mesh Health: Score 62/100 (threshold: 65). Infrastructure: 71, Connectivity: 58.",
|
||
"toggle": "mesh_health",
|
||
},
|
||
"region_score_low": {
|
||
"name": "Region Health Low",
|
||
"description": "A region's health score below threshold — localized issues",
|
||
"default_severity": "priority",
|
||
"example_message": "📉 Region Health: Magic Valley at 55/100 (threshold: 60). 2 nodes offline.",
|
||
"toggle": "mesh_health",
|
||
},
|
||
|
||
# Environmental - Weather
|
||
# v0.5.7-weather audit: nws.py._derive_category() emits exactly these four
|
||
# category IDs (suffix dispatch on the NWS event_type: warning/watch/
|
||
# advisory/{anything else -> statement}). The set is in lockstep —
|
||
# test_alert_categories_weather_complete enforces parity if nws.py changes.
|
||
# If a new branch is added there, add the matching entry here too.
|
||
"weather_warning": {
|
||
"name": "Severe Weather Warning",
|
||
"description": "NWS Warning affecting your mesh area — highest urgency weather alert",
|
||
"default_severity": "priority",
|
||
"example_message": "⚠ Red Flag Warning — Twin Falls, Cassia counties. Gusty winds, low humidity. Until May 13 04:00Z",
|
||
"toggle": "weather",
|
||
},
|
||
"weather_watch": {
|
||
"name": "Weather Watch",
|
||
"description": "NWS Watch affecting your mesh area — conditions favorable for hazardous weather",
|
||
"default_severity": "routine",
|
||
"example_message": "⏳ Winter Storm Watch — Wood River Valley. Heavy snow possible Thu night through Fri.",
|
||
"toggle": "weather",
|
||
},
|
||
"weather_advisory": {
|
||
"name": "Weather Advisory",
|
||
"description": "NWS Advisory affecting your mesh area — weather may cause inconvenience",
|
||
"default_severity": "routine",
|
||
"example_message": "ℹ Wind Advisory — Magic Valley. SW winds 25-35 mph with gusts to 50 mph.",
|
||
"toggle": "weather",
|
||
},
|
||
"weather_statement": {
|
||
"name": "Weather Statement",
|
||
"description": "NWS Special Weather Statement — general awareness, no specific hazard",
|
||
"default_severity": "routine",
|
||
"example_message": "📋 Special Weather Statement — Isolated thunderstorms possible this afternoon.",
|
||
"toggle": "weather",
|
||
},
|
||
|
||
# Environmental - Space Weather / RF Propagation
|
||
# v0.5.7-rf audit (test_alert_categories_rf_complete enforces parity):
|
||
# Native: ducting.py -> rf_anomalous_propagation (super_refraction tier);
|
||
# ducting.py -> rf_ducting_enhancement (duct + surface_duct tiers).
|
||
# Central path (via map_category): space.alert -> rf_propagation_alert
|
||
# (swpc_alerts); space.kindex -> geomagnetic_storm (swpc_kindex);
|
||
# space.proton_flux -> solar_radiation_storm (swpc_protons);
|
||
# catchall space.* -> geomagnetic_storm.
|
||
# All three SWPC adapters publish severity=0 -> "routine" per guide
|
||
# §swpc_alerts/§swpc_kindex/§swpc_protons live samples.
|
||
#
|
||
# Legacy entries kept: hf_blackout and tropospheric_ducting have no
|
||
# current emitter (ducting.py was renamed to rf_ducting_enhancement,
|
||
# and HF-blackout-specific parsing of swpc_alerts.message is deferred
|
||
# to a future phase). They remain UI-selectable as forward-compatible
|
||
# rule targets; queued for cleanup if no emitter materializes.
|
||
"rf_anomalous_propagation": {
|
||
"name": "RF Anomalous Propagation",
|
||
"description": "Super-refractive atmospheric layer affecting VHF/UHF propagation — sub-standard refractive conditions, mostly affects line-of-sight links",
|
||
"default_severity": "routine",
|
||
"example_message": "📡 Anomalous Propagation: Super-refraction detected, dM/dz -45 M-units/km, ~80m thick layer. VHF/UHF links may show enhanced range.",
|
||
"toggle": "rf_propagation",
|
||
},
|
||
"rf_ducting_enhancement": {
|
||
"name": "RF Ducting Enhancement",
|
||
"description": "Tropospheric duct trapping VHF/UHF signals — extended range, signals propagate well beyond the normal radio horizon",
|
||
"default_severity": "priority",
|
||
"example_message": "📡 Ducting Enhancement: Surface duct detected, base 0 m, ~120 m thick. VHF/UHF extended range, expect signals well beyond horizon.",
|
||
"toggle": "rf_propagation",
|
||
},
|
||
"rf_propagation_alert": {
|
||
"name": "Space Weather Alert",
|
||
"description": "NOAA SWPC space weather alert/watch/warning — geomagnetic storm scales (G1-G5), radiation storms (S), radio blackouts (R), or summaries. Full operational text in event body.",
|
||
"default_severity": "priority",
|
||
"example_message": "⚠ Space Weather Alert (A20F): WATCH — Geomagnetic Storm Category G1 Predicted. Apr 25: G1 (Minor). Aurora possible at high latitudes.",
|
||
"toggle": "rf_propagation",
|
||
},
|
||
"solar_radiation_storm": {
|
||
"name": "Solar Radiation Storm",
|
||
"description": "GOES proton flux above S-scale threshold — S1 ≥10 pfu at ≥10 MeV; S2 ≥100; S3 ≥1000; impacts HF over polar regions and satellite operations",
|
||
"default_severity": "priority",
|
||
"example_message": "🌐 Solar Radiation Storm: GOES-19 proton flux 12.5 pfu at ≥10 MeV (S1 threshold). HF over polar regions degraded.",
|
||
"toggle": "rf_propagation",
|
||
},
|
||
"hf_blackout": {
|
||
"name": "HF Radio Blackout",
|
||
"description": "R3+ solar flare degrading HF propagation on sunlit side",
|
||
"default_severity": "priority",
|
||
"example_message": "⚠ R3 Strong Radio Blackout — X1.2 flare. Wide-area HF blackout ~1 hour on sunlit side.",
|
||
"toggle": "rf_propagation",
|
||
},
|
||
"geomagnetic_storm": {
|
||
"name": "Geomagnetic Storm",
|
||
"description": "G2+ geomagnetic storm — HF degraded at higher latitudes, aurora possible",
|
||
"default_severity": "priority",
|
||
"example_message": "🌐 G2 Moderate Geomagnetic Storm — Kp=6. HF fades at high latitudes, aurora to ~55°.",
|
||
"toggle": "rf_propagation",
|
||
},
|
||
|
||
# Environmental - Tropospheric
|
||
"tropospheric_ducting": {
|
||
"name": "Tropospheric Ducting",
|
||
"description": "Atmospheric conditions trapping VHF/UHF signals — extended range",
|
||
"default_severity": "routine",
|
||
"example_message": "📡 Tropospheric Ducting: Surface duct detected, dM/dz -45 M-units/km, ~120m thick. VHF/UHF extended range.",
|
||
"toggle": "rf_propagation",
|
||
},
|
||
|
||
# Environmental - Fire
|
||
# v0.5.7-fire audit (test_alert_categories_fire_complete enforces parity):
|
||
# Native: firms.py -> {new_ignition, wildfire_hotspot};
|
||
# fires.py -> wildfire_incident.
|
||
# Central path (via map_category): fire.hotspot.* -> wildfire_hotspot;
|
||
# fire.incident.* / fire.perimeter.* / fire.* -> wildfire_incident.
|
||
#
|
||
# REMOVED in v0.5.7-fire:
|
||
# - fire_proximity (Matt: "fire near mesh has its own set of parameters
|
||
# that I don't even know what they could be. like how far is near mesh?
|
||
# I don't know I can't set that.") -- parameterized distance_max_km on
|
||
# rules is queued for v0.5.8, not a registry entry.
|
||
# - wildfire_proximity (duplicate of fire_proximity, same parametric flaw)
|
||
"new_ignition": {
|
||
"name": "New Fire Ignition",
|
||
"description": "Satellite hotspot detected NOT near any known fire — potential new wildfire",
|
||
"default_severity": "priority",
|
||
"example_message": "🛰 New Ignition: Satellite fire at 42.32°N, 114.30°W — high confidence, 47 MW FRP. Not near any known fire.",
|
||
"toggle": "fire",
|
||
},
|
||
"wildfire_hotspot": {
|
||
"name": "Wildfire Hotspot",
|
||
"description": "Satellite thermal-anomaly detection (NASA FIRMS VIIRS/MODIS pixel) — not necessarily a new ignition",
|
||
"default_severity": "routine",
|
||
"example_message": "🔥 Wildfire Hotspot: VIIRS NOAA-20 pixel at 43.12°N, 114.85°W — high confidence, 22 MW FRP, daytime overpass.",
|
||
"toggle": "fire",
|
||
},
|
||
"wildfire_incident": {
|
||
"name": "Wildfire Incident",
|
||
"description": "Active wildfire incident from NIFC WFIGS — official incident record with size, containment, cause",
|
||
"default_severity": "priority",
|
||
"example_message": "🔥 Wildfire Incident: Rochelle 2 — 1,240 ac, 15% contained, Custer County ID. WF, Natural cause.",
|
||
"toggle": "fire",
|
||
},
|
||
|
||
# Environmental - Seismic (Geohazards family)
|
||
# v0.5.7-seismic audit (test_alert_categories_seismic_complete enforces parity):
|
||
# Native: usgs_quake.py -> earthquake_event.
|
||
# Central path: map_category('quake.event.<tier>') -> earthquake_event
|
||
# for any of the 6 tiers (minor/light/moderate/strong/major/great).
|
||
# The hydro entries below also live under toggle='seismic' per the v0.5.2
|
||
# USGS-water migration (see comment on stream_flood_warning) and are
|
||
# OUT OF SCOPE for v0.5.7-seismic -- they belong to the water/geohazards
|
||
# phase that follows. Verified-unchanged here.
|
||
"earthquake_event": {
|
||
"name": "Earthquake",
|
||
"description": "USGS-catalogued earthquake — magnitude, depth, and PAGER alert level surfaced from the USGS earthquake feed",
|
||
"default_severity": "routine",
|
||
"example_message": "🌐 Earthquake: M 4.2 — 23 km ESE of Stanley, ID. Depth 8 km. USGS automatic.",
|
||
"toggle": "seismic",
|
||
},
|
||
|
||
# Environmental - Flood / Water (Geohazards family per v0.5.2 migration)
|
||
# v0.5.7-water audit (test_alert_categories_water_complete enforces parity):
|
||
# Native: usgs.py applies NWPS flood-stage thresholds CLIENT-SIDE and emits
|
||
# - stream_flood_warning (reading at/above flood stage)
|
||
# - stream_high_water (reading at action stage)
|
||
# Routine gauge readings below action stage are silently dropped on the
|
||
# native path (no spam).
|
||
# Central path: every NWIS reading arrives with category="hydro.<pcode>.
|
||
# <agency>.<site>" at severity=0. consumer._CATEGORY_MAP maps `hydro.*` to
|
||
# `stream_flow` (added below in v0.5.7-water so the rule editor can target
|
||
# raw central readings). NOTE: meshai does NOT currently re-apply NWPS
|
||
# threshold logic to central-delivered readings; future work to bring
|
||
# the central path to parity with the native threshold-classification
|
||
# is queued for v0.5.8+.
|
||
"stream_flow": {
|
||
"name": "Stream Gauge Reading",
|
||
"description": "Raw USGS NWIS stream gauge reading (discharge, gage height, water temp) — no threshold classification. Use stream_flood_warning / stream_high_water for threshold-triggered alerts.",
|
||
"default_severity": "routine",
|
||
"example_message": "🌊 Stream Reading: Snake River nr Twin Falls — 7,420 ft³/s discharge at 2026-06-04T12:00Z (provisional).",
|
||
"toggle": "seismic",
|
||
},
|
||
"stream_flood_warning": {
|
||
"name": "Stream Flood Warning",
|
||
"description": "River gauge exceeds NWS flood stage threshold",
|
||
"default_severity": "priority",
|
||
"example_message": "🌊 Stream Flood Warning: Snake River nr Twin Falls at 12.8 ft — Minor Flood Stage is 10.5 ft.",
|
||
# v0.5.2: moved weather→seismic to match the GUI Geohazards family tab
|
||
# (Environment.tsx FAMILIES key='geohazards' groups usgs_quake+usgs+avalanche).
|
||
# 'seismic' is the canonical Geohazards toggle in VALID_TOGGLES; backend still
|
||
# has separate avalanche/seismic toggles, but USGS hydro lives with USGS quake.
|
||
"toggle": "seismic",
|
||
},
|
||
"stream_high_water": {
|
||
"name": "Stream High Water",
|
||
"description": "River gauge approaching flood stage — monitoring recommended",
|
||
"default_severity": "routine",
|
||
"example_message": "🌊 High Water: Snake River at 9.8 ft — Action Stage is 9.0 ft. Monitor conditions.",
|
||
# v0.5.2: moved weather→seismic — see stream_flood_warning above
|
||
"toggle": "seismic",
|
||
},
|
||
|
||
# Environmental - Roads
|
||
# v0.5.7-traffic audit (test_alert_categories_roads_complete enforces parity):
|
||
# Native: traffic.py -> traffic_congestion; roads511.py -> road_closure.
|
||
# Central path (via map_category): work_zone (wzdx), road_incident
|
||
# (tomtom_incidents + state_511_atis/itd_511 'incident'), road_closure
|
||
# (state_511_atis/itd_511 'closure'), traffic_congestion (traffic. catchall).
|
||
"road_closure": {
|
||
"name": "Road Closure",
|
||
"description": "Full road closure on a monitored corridor",
|
||
"default_severity": "priority",
|
||
"example_message": "🚧 Road Closure: I-84 EB at MP 173 — full closure, construction. Detour via US-30.",
|
||
"toggle": "roads",
|
||
},
|
||
"traffic_congestion": {
|
||
"name": "Traffic Congestion",
|
||
"description": "Traffic speed dropped below congestion threshold on a monitored corridor",
|
||
"default_severity": "routine",
|
||
"example_message": "🚗 Traffic Congestion: I-84 Twin Falls — 35 mph (free-flow 70 mph), 50% speed ratio",
|
||
"toggle": "roads",
|
||
},
|
||
"work_zone": {
|
||
"name": "Work Zone",
|
||
"description": "Active construction or maintenance work zone affecting traffic — possible lane closures, reduced speed, or detour",
|
||
"default_severity": "routine",
|
||
"example_message": "🚧 Work Zone: I-84 EB MP 168-173 — right lane closed, 55 mph zone. Expect delays.",
|
||
"toggle": "roads",
|
||
},
|
||
"road_incident": {
|
||
"name": "Road Incident",
|
||
"description": "Reported incident on a monitored corridor (crash, disabled vehicle, debris, hazard)",
|
||
"default_severity": "priority",
|
||
"example_message": "🚨 Road Incident: US-93 NB at MP 47 — crash blocking left lane, expect 30-min delay.",
|
||
"toggle": "roads",
|
||
},
|
||
|
||
# Environmental - Avalanche
|
||
"avalanche_warning": {
|
||
"name": "Avalanche Danger High",
|
||
"description": "Avalanche danger level 4 (High) or 5 (Extreme) in your area",
|
||
"default_severity": "priority",
|
||
"example_message": "⛷ Avalanche Danger HIGH: Sawtooth Zone — avoid avalanche terrain. Natural avalanches likely.",
|
||
"toggle": "avalanche",
|
||
},
|
||
"avalanche_considerable": {
|
||
"name": "Avalanche Danger Considerable",
|
||
"description": "Avalanche danger level 3 (Considerable) — most fatalities occur at this level",
|
||
"default_severity": "routine",
|
||
"example_message": "⛷ Avalanche Danger CONSIDERABLE: Sawtooth Zone — dangerous conditions on steep slopes.",
|
||
"toggle": "avalanche",
|
||
},
|
||
}
|
||
|
||
|
||
def get_category(category_id: str) -> dict:
|
||
"""Get category info by ID, with fallback for unknown categories."""
|
||
if category_id in ALERT_CATEGORIES:
|
||
return ALERT_CATEGORIES[category_id]
|
||
return {
|
||
"name": category_id.replace("_", " ").title(),
|
||
"description": f"Alert type: {category_id}",
|
||
"default_severity": "routine",
|
||
"example_message": f"Alert: {category_id}",
|
||
"toggle": "mesh_health", # Default unknown to mesh_health
|
||
}
|
||
|
||
|
||
def list_categories() -> list[dict]:
|
||
"""List all categories with their IDs."""
|
||
return [
|
||
{"id": cat_id, **cat_info}
|
||
for cat_id, cat_info in ALERT_CATEGORIES.items()
|
||
]
|
||
|
||
|
||
def categories_for_toggle(toggle: str) -> list[str]:
|
||
"""Return all category names that route to this toggle.
|
||
|
||
Args:
|
||
toggle: Toggle name (e.g., "mesh_health", "weather")
|
||
|
||
Returns:
|
||
List of category IDs that have this toggle assigned
|
||
"""
|
||
if toggle not in VALID_TOGGLES:
|
||
return []
|
||
|
||
return [
|
||
cat_id
|
||
for cat_id, cat_info in ALERT_CATEGORIES.items()
|
||
if cat_info.get("toggle") == toggle
|
||
]
|
||
|
||
|
||
def get_toggle(category_name: str) -> Optional[str]:
|
||
"""Return the toggle name for a category, or None if unknown.
|
||
|
||
Args:
|
||
category_name: Category ID (e.g., "infra_offline")
|
||
|
||
Returns:
|
||
Toggle name (e.g., "mesh_health") or None if category unknown
|
||
"""
|
||
cat_info = ALERT_CATEGORIES.get(category_name)
|
||
if cat_info:
|
||
return cat_info.get("toggle")
|
||
for prefix, toggle in _TOGGLE_PREFIX_FALLBACK:
|
||
if category_name.startswith(prefix):
|
||
return toggle
|
||
return None
|