feat(env): add NASA FIRMS satellite fire hotspot detection

- Implement FIRMSAdapter polling NASA FIRMS area API for satellite hotspots
- Cross-reference hotspots against NIFC perimeters to identify new ignitions
- Add !hotspots command with --new flag for filtering new ignitions only
- Add FIRMSConfig dataclass with map_key, source, bbox, day_range options
- Add /api/env/hotspots endpoint for dashboard integration
- Add Satellite Hotspots section to Environment.tsx with NEW badges
- Add FIRMS configuration section to Config.tsx with source/confidence options
- Update config.example.yaml with FIRMS configuration template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
K7ZVX 2026-05-12 23:06:55 +00:00
commit 3d74eb92b0
13 changed files with 786 additions and 81 deletions

View file

@ -393,6 +393,20 @@ class Roads511Config:
bbox: list = field(default_factory=list) # [west, south, east, north]
@dataclass
class FIRMSConfig:
"""NASA FIRMS satellite fire hotspot settings."""
enabled: bool = False
tick_seconds: int = 1800 # 30 min default
map_key: str = "" # NASA FIRMS MAP_KEY, get at https://firms.modaps.eosdis.nasa.gov/api/area/
source: str = "VIIRS_SNPP_NRT" # VIIRS_SNPP_NRT, VIIRS_NOAA20_NRT, MODIS_NRT
bbox: list = field(default_factory=list) # [west, south, east, north]
day_range: int = 1 # 1-10 days of data
confidence_min: str = "nominal" # low, nominal, high
proximity_km: float = 10.0 # km to match known fire
@dataclass
class EnvironmentalConfig:
"""Environmental feeds settings."""
@ -407,6 +421,7 @@ class EnvironmentalConfig:
usgs: USGSConfig = field(default_factory=USGSConfig)
traffic: TomTomConfig = field(default_factory=TomTomConfig)
roads511: Roads511Config = field(default_factory=Roads511Config)
firms: FIRMSConfig = field(default_factory=FIRMSConfig)
@dataclass
@ -518,6 +533,8 @@ def _dict_to_dataclass(cls, data: dict):
kwargs[key] = _dict_to_dataclass(TomTomConfig, value)
elif key == "roads511" and isinstance(value, dict):
kwargs[key] = _dict_to_dataclass(Roads511Config, value)
elif key == "firms" and isinstance(value, dict):
kwargs[key] = _dict_to_dataclass(FIRMSConfig, value)
else:
kwargs[key] = value