mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 17:34:44 +02:00
feat: researched defaults + USGS auto-lookup + category documentation
- Battery thresholds: 30%/15%/5% with voltage equivalents (3.60V/3.50V/3.40V)
- Channel utilization threshold: 40% (firmware throttles GPS at 25%)
- Packet flood threshold: 10 packets/min per node (was 500/day)
- Mesh health threshold: 65 (was 70)
- USGS adapter with NWS NWPS flood stage auto-lookup
- API endpoint: GET /api/env/usgs/lookup/{site_id}
- Alert categories with detailed descriptions and example messages
- Packet flood vs stream flood terminology fully disambiguated
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7286c9ab44
commit
64faf33e3b
4 changed files with 545 additions and 237 deletions
|
|
@ -1,163 +1,192 @@
|
|||
"""Environmental data API routes."""
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
router = APIRouter(tags=["environment"])
|
||||
|
||||
|
||||
@router.get("/env/status")
|
||||
async def get_env_status(request: Request):
|
||||
"""Get environmental feeds status."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"enabled": False, "feeds": []}
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
"feeds": env_store.get_source_health(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/env/active")
|
||||
async def get_active_env(request: Request):
|
||||
"""Get active environmental events."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return []
|
||||
|
||||
return env_store.get_active()
|
||||
|
||||
|
||||
@router.get("/env/swpc")
|
||||
async def get_swpc_data(request: Request):
|
||||
"""Get SWPC space weather data."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"enabled": False}
|
||||
|
||||
status = env_store.get_swpc_status()
|
||||
if not status:
|
||||
return {"enabled": False}
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
**status,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/env/propagation")
|
||||
async def get_rf_propagation(request: Request):
|
||||
"""Get combined HF + UHF propagation data for dashboard."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"hf": {}, "uhf_ducting": {}}
|
||||
|
||||
return env_store.get_rf_propagation()
|
||||
|
||||
|
||||
@router.get("/env/ducting")
|
||||
async def get_ducting_data(request: Request):
|
||||
"""Get tropospheric ducting assessment."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"enabled": False}
|
||||
|
||||
status = env_store.get_ducting_status()
|
||||
if not status:
|
||||
return {"enabled": False}
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
**status,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/env/fires")
|
||||
async def get_fires_data(request: Request):
|
||||
"""Get active wildfire perimeters."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return []
|
||||
|
||||
return env_store.get_active(source="nifc")
|
||||
|
||||
|
||||
@router.get("/env/avalanche")
|
||||
async def get_avalanche_data(request: Request):
|
||||
"""Get avalanche advisories."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"off_season": True, "advisories": []}
|
||||
|
||||
adapters = getattr(env_store, "_adapters", {})
|
||||
avy_adapter = adapters.get("avalanche")
|
||||
|
||||
if avy_adapter and avy_adapter.is_off_season():
|
||||
return {"off_season": True, "advisories": []}
|
||||
|
||||
return {
|
||||
"off_season": False,
|
||||
"advisories": env_store.get_active(source="avalanche"),
|
||||
}
|
||||
|
||||
@router.get("/env/streams")
|
||||
async def get_streams_data(request: Request):
|
||||
"""Get USGS stream gauge readings."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return []
|
||||
|
||||
return env_store.get_active(source="usgs")
|
||||
|
||||
|
||||
@router.get("/env/traffic")
|
||||
async def get_traffic_data(request: Request):
|
||||
"""Get TomTom traffic flow data."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return []
|
||||
|
||||
return env_store.get_active(source="traffic")
|
||||
|
||||
|
||||
@router.get("/env/roads")
|
||||
async def get_roads_data(request: Request):
|
||||
"""Get 511 road conditions."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return []
|
||||
|
||||
return env_store.get_active(source="511")
|
||||
|
||||
|
||||
@router.get("/env/hotspots")
|
||||
async def get_hotspots_data(request: Request):
|
||||
"""Get NASA FIRMS satellite fire hotspots."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"hotspots": [], "new_ignitions": 0}
|
||||
|
||||
firms_adapter = getattr(env_store, "_firms", None)
|
||||
|
||||
if not firms_adapter:
|
||||
return {"hotspots": [], "new_ignitions": 0, "enabled": False}
|
||||
|
||||
hotspots = env_store.get_active(source="firms")
|
||||
new_ignitions = [h for h in hotspots if h.get("properties", {}).get("new_ignition")]
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
"hotspots": hotspots,
|
||||
"new_ignitions": len(new_ignitions),
|
||||
}
|
||||
"""Environmental data API routes."""
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
|
||||
router = APIRouter(tags=["environment"])
|
||||
|
||||
|
||||
@router.get("/env/status")
|
||||
async def get_env_status(request: Request):
|
||||
"""Get environmental feeds status."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"enabled": False, "feeds": []}
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
"feeds": env_store.get_source_health(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/env/active")
|
||||
async def get_active_env(request: Request):
|
||||
"""Get active environmental events."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return []
|
||||
|
||||
return env_store.get_active()
|
||||
|
||||
|
||||
@router.get("/env/swpc")
|
||||
async def get_swpc_data(request: Request):
|
||||
"""Get SWPC space weather data."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"enabled": False}
|
||||
|
||||
status = env_store.get_swpc_status()
|
||||
if not status:
|
||||
return {"enabled": False}
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
**status,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/env/propagation")
|
||||
async def get_rf_propagation(request: Request):
|
||||
"""Get combined HF + UHF propagation data for dashboard."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"hf": {}, "uhf_ducting": {}}
|
||||
|
||||
return env_store.get_rf_propagation()
|
||||
|
||||
|
||||
@router.get("/env/ducting")
|
||||
async def get_ducting_data(request: Request):
|
||||
"""Get tropospheric ducting assessment."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"enabled": False}
|
||||
|
||||
status = env_store.get_ducting_status()
|
||||
if not status:
|
||||
return {"enabled": False}
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
**status,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/env/fires")
|
||||
async def get_fires_data(request: Request):
|
||||
"""Get active wildfire perimeters."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return []
|
||||
|
||||
return env_store.get_active(source="nifc")
|
||||
|
||||
|
||||
@router.get("/env/avalanche")
|
||||
async def get_avalanche_data(request: Request):
|
||||
"""Get avalanche advisories."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"off_season": True, "advisories": []}
|
||||
|
||||
adapters = getattr(env_store, "_adapters", {})
|
||||
avy_adapter = adapters.get("avalanche")
|
||||
|
||||
if avy_adapter and avy_adapter.is_off_season():
|
||||
return {"off_season": True, "advisories": []}
|
||||
|
||||
return {
|
||||
"off_season": False,
|
||||
"advisories": env_store.get_active(source="avalanche"),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/env/streams")
|
||||
async def get_streams_data(request: Request):
|
||||
"""Get USGS stream gauge readings."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return []
|
||||
|
||||
return env_store.get_active(source="usgs")
|
||||
|
||||
|
||||
@router.get("/env/usgs/lookup/{site_id}")
|
||||
async def lookup_usgs_site(request: Request, site_id: str):
|
||||
"""Lookup USGS site metadata and NWS flood stages.
|
||||
|
||||
Returns site name, location, and flood stage thresholds from NWS NWPS.
|
||||
Used by the config UI to auto-populate fields when adding a new gauge.
|
||||
"""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"error": "Environmental feeds not enabled"}
|
||||
|
||||
adapters = getattr(env_store, "_adapters", {})
|
||||
usgs_adapter = adapters.get("usgs")
|
||||
|
||||
if not usgs_adapter:
|
||||
# Create a temporary adapter for lookup
|
||||
from meshai.env.usgs import USGSStreamsAdapter
|
||||
from meshai.config import USGSConfig
|
||||
usgs_adapter = USGSStreamsAdapter(USGSConfig())
|
||||
|
||||
try:
|
||||
result = usgs_adapter.lookup_site(site_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
return {"error": str(e), "site_id": site_id}
|
||||
|
||||
|
||||
@router.get("/env/traffic")
|
||||
async def get_traffic_data(request: Request):
|
||||
"""Get TomTom traffic flow data."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return []
|
||||
|
||||
return env_store.get_active(source="traffic")
|
||||
|
||||
|
||||
@router.get("/env/roads")
|
||||
async def get_roads_data(request: Request):
|
||||
"""Get 511 road conditions."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return []
|
||||
|
||||
return env_store.get_active(source="511")
|
||||
|
||||
|
||||
@router.get("/env/hotspots")
|
||||
async def get_hotspots_data(request: Request):
|
||||
"""Get NASA FIRMS satellite fire hotspots."""
|
||||
env_store = getattr(request.app.state, "env_store", None)
|
||||
|
||||
if not env_store:
|
||||
return {"hotspots": [], "new_ignitions": 0}
|
||||
|
||||
firms_adapter = getattr(env_store, "_firms", None)
|
||||
|
||||
if not firms_adapter:
|
||||
return {"hotspots": [], "new_ignitions": 0, "enabled": False}
|
||||
|
||||
hotspots = env_store.get_active(source="firms")
|
||||
new_ignitions = [h for h in hotspots if h.get("properties", {}).get("new_ignition")]
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
"hotspots": hotspots,
|
||||
"new_ignitions": len(new_ignitions),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue