mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-22 15:44:39 +02:00
feat(env): NWS weather alerts, NOAA space weather, tropospheric ducting
- Environmental feed system with tick-based adapters - NWS Active Alerts: polls api.weather.gov, zone-based filtering - NOAA SWPC: Kp, SFI, R/S/G scales, band assessment, alert detection - Tropospheric ducting: Open-Meteo GFS refractivity profile, duct classification - !alerts command for active weather warnings - !solar / !hf commands for RF propagation (HF + UHF ducting) - Alert engine integration: severe weather, R3+ blackout, ducting events - LLM context injection for weather/propagation queries - Dashboard RF Propagation card with HF + UHF ducting display - EnvironmentalConfig with per-feed toggles in config.yaml
This commit is contained in:
parent
374fb835c5
commit
549ae4bdfb
20 changed files with 4142 additions and 2652 deletions
49
meshai/commands/alerts_cmd.py
Normal file
49
meshai/commands/alerts_cmd.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"""Alerts command handler."""
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from .base import CommandContext, CommandHandler
|
||||
|
||||
|
||||
class AlertsCommand(CommandHandler):
|
||||
"""Active weather alerts for mesh area."""
|
||||
|
||||
name = "alerts"
|
||||
description = "Active weather alerts for mesh area"
|
||||
usage = "!alerts"
|
||||
|
||||
def __init__(self, env_store):
|
||||
self._env_store = env_store
|
||||
|
||||
async def execute(self, args: str, context: CommandContext) -> str:
|
||||
"""Execute the alerts command."""
|
||||
if not self._env_store:
|
||||
return "Environmental feeds not enabled."
|
||||
|
||||
zones = self._env_store._mesh_zones
|
||||
alerts = self._env_store.get_for_zones(zones)
|
||||
|
||||
if not alerts:
|
||||
alerts = self._env_store.get_active(source="nws")
|
||||
|
||||
if not alerts:
|
||||
return "No active weather alerts for the mesh area."
|
||||
|
||||
lines = [f"Active Alerts ({len(alerts)}):"]
|
||||
for a in alerts[:5]:
|
||||
# Format expiry time
|
||||
expires = a.get("expires", 0)
|
||||
if expires:
|
||||
try:
|
||||
dt = datetime.fromtimestamp(expires)
|
||||
expires_str = dt.strftime("%b %d %H:%MZ")
|
||||
except Exception:
|
||||
expires_str = "Unknown"
|
||||
else:
|
||||
expires_str = "Unknown"
|
||||
|
||||
lines.append(f"* {a['event_type']} -- {a.get('area_desc', '')[:60]}")
|
||||
lines.append(f" Until {expires_str}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
|
@ -161,6 +161,7 @@ def create_dispatcher(
|
|||
data_store=None,
|
||||
health_engine=None,
|
||||
subscription_manager=None,
|
||||
env_store=None,
|
||||
) -> CommandDispatcher:
|
||||
"""Create and populate command dispatcher with default commands.
|
||||
|
||||
|
|
@ -172,6 +173,7 @@ def create_dispatcher(
|
|||
data_store: MeshDataStore for neighbor data
|
||||
health_engine: MeshHealthEngine for infrastructure detection
|
||||
subscription_manager: SubscriptionManager for subscription commands
|
||||
env_store: EnvironmentalStore for weather/propagation commands
|
||||
|
||||
Returns:
|
||||
Configured CommandDispatcher
|
||||
|
|
@ -243,6 +245,27 @@ def create_dispatcher(
|
|||
alias_handler.name = alias
|
||||
dispatcher.register(alias_handler)
|
||||
|
||||
# Register environmental commands
|
||||
if env_store:
|
||||
from .alerts_cmd import AlertsCommand
|
||||
from .solar_cmd import SolarCommand
|
||||
|
||||
alerts_cmd = AlertsCommand(env_store)
|
||||
dispatcher.register(alerts_cmd)
|
||||
|
||||
solar_cmd = SolarCommand(env_store)
|
||||
dispatcher.register(solar_cmd)
|
||||
|
||||
# Register !hf as an alias for !solar
|
||||
hf_cmd = SolarCommand(env_store)
|
||||
hf_cmd.name = "hf"
|
||||
dispatcher.register(hf_cmd)
|
||||
|
||||
# Register !wx-alerts as an alias for !alerts
|
||||
wx_cmd = AlertsCommand(env_store)
|
||||
wx_cmd.name = "wx-alerts"
|
||||
dispatcher.register(wx_cmd)
|
||||
|
||||
# Register custom commands
|
||||
if custom_commands:
|
||||
for name, response in custom_commands.items():
|
||||
|
|
|
|||
63
meshai/commands/solar_cmd.py
Normal file
63
meshai/commands/solar_cmd.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""Solar/RF propagation command handler."""
|
||||
|
||||
from .base import CommandContext, CommandHandler
|
||||
|
||||
|
||||
class SolarCommand(CommandHandler):
|
||||
"""Space weather & RF propagation."""
|
||||
|
||||
name = "solar"
|
||||
description = "Space weather & RF propagation"
|
||||
usage = "!solar"
|
||||
|
||||
def __init__(self, env_store):
|
||||
self._env_store = env_store
|
||||
|
||||
async def execute(self, args: str, context: CommandContext) -> str:
|
||||
"""Execute the solar command."""
|
||||
if not self._env_store:
|
||||
return "Environmental feeds not enabled."
|
||||
|
||||
lines = []
|
||||
|
||||
# HF section
|
||||
s = self._env_store.get_swpc_status()
|
||||
if s:
|
||||
assessment = s.get("band_assessment", "Unknown")
|
||||
kp = s.get("kp_current", "?")
|
||||
sfi = s.get("sfi", "?")
|
||||
r = s.get("r_scale", 0)
|
||||
s_sc = s.get("s_scale", 0)
|
||||
g = s.get("g_scale", 0)
|
||||
|
||||
lines.append(f"HF: {assessment} -- SFI {sfi}, Kp {kp}")
|
||||
lines.append(f" R{r}/S{s_sc}/G{g} scales")
|
||||
|
||||
if assessment in ("Excellent", "Good"):
|
||||
lines.append(" 10m-20m open, solid DX")
|
||||
elif assessment == "Fair":
|
||||
lines.append(" 20m-40m usable, upper bands marginal")
|
||||
else:
|
||||
lines.append(" Degraded -- lower bands only")
|
||||
|
||||
warnings = s.get("active_warnings", [])
|
||||
for w in warnings[:2]:
|
||||
lines.append(f" Warning: {w[:100]}")
|
||||
else:
|
||||
lines.append("HF: Data not available")
|
||||
|
||||
# UHF ducting section
|
||||
d = self._env_store.get_ducting_status()
|
||||
if d:
|
||||
cond = d.get("condition", "unknown")
|
||||
if cond == "normal":
|
||||
lines.append("UHF: Normal propagation (906 MHz)")
|
||||
else:
|
||||
gradient = d.get("min_gradient", "?")
|
||||
lines.append(f"UHF: {cond.replace('_', ' ').title()} (906 MHz)")
|
||||
lines.append(f" dM/dz: {gradient} M-units/km")
|
||||
lines.append(" Extended range -- expect distant nodes")
|
||||
else:
|
||||
lines.append("UHF: Ducting data not available")
|
||||
|
||||
return "\n".join(lines)
|
||||
Loading…
Add table
Add a link
Reference in a new issue