mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
feat(coverage): wire nws + traffic to the coverage bbox
Phase 2c: nws derives its area= state filter from the bbox (states it overlaps) AND drops alerts whose centroid falls outside the box for precise scoping; traffic samples a 3x3 grid of flow points across the bbox instead of hand-listed corridors. Coverage governs when set; each adapter's own config is the fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ef8977b172
commit
00036a0928
4 changed files with 256 additions and 6 deletions
43
work/meshai/env/nws.py
vendored
43
work/meshai/env/nws.py
vendored
|
|
@ -19,8 +19,23 @@ logger = logging.getLogger(__name__)
|
|||
class NWSAlertsAdapter:
|
||||
"""NWS Active Alerts -- polls api.weather.gov"""
|
||||
|
||||
def __init__(self, config: "NWSConfig"):
|
||||
self._areas = config.areas or ["ID"]
|
||||
def __init__(self, config: "NWSConfig", coverage: dict = None):
|
||||
if coverage is not None:
|
||||
derived_areas = coverage["areas"]
|
||||
if not derived_areas:
|
||||
# bbox overlaps no state — keep config areas so the API call
|
||||
# remains well-formed, but retain the bbox for geometry filter.
|
||||
logger.debug(
|
||||
"NWS coverage: derived area list is empty (bbox outside all states); "
|
||||
"falling back to config areas for API query"
|
||||
)
|
||||
self._areas = config.areas or ["ID"]
|
||||
else:
|
||||
self._areas = derived_areas
|
||||
self._coverage_bbox = coverage["bbox"]
|
||||
else:
|
||||
self._areas = config.areas or ["ID"]
|
||||
self._coverage_bbox = None
|
||||
self._user_agent = config.user_agent or "(meshai, ops@example.com)"
|
||||
self._severity_min = config.severity_min or "moderate"
|
||||
self._tick_interval = config.tick_seconds or 60
|
||||
|
|
@ -157,6 +172,24 @@ class NWSAlertsAdapter:
|
|||
self._last_tick = now
|
||||
return self._fetch()
|
||||
|
||||
def _in_coverage(self, event: dict) -> bool:
|
||||
"""Return True iff the event should be kept under the coverage bbox filter.
|
||||
|
||||
Rules:
|
||||
- If no coverage bbox is set, always keep.
|
||||
- If the event has a numeric lat/lon centroid, drop it when outside the box.
|
||||
- If the event has no centroid (lat/lon absent or None), keep it — the
|
||||
state area= filter already scopes it; we can't box-filter without coords.
|
||||
"""
|
||||
if not self._coverage_bbox:
|
||||
return True
|
||||
lat = event.get("lat")
|
||||
lon = event.get("lon")
|
||||
if lat is None or lon is None:
|
||||
return True # no geometry — pass through
|
||||
from meshai.coverage import point_in_bbox
|
||||
return point_in_bbox(lat, lon, self._coverage_bbox)
|
||||
|
||||
def _fetch(self) -> bool:
|
||||
"""Fetch alerts from NWS API.
|
||||
|
||||
|
|
@ -266,6 +299,12 @@ class NWSAlertsAdapter:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# Geometry filter: drop alerts whose centroid falls outside the
|
||||
# coverage bbox. Alerts with no centroid are kept (state area=
|
||||
# filter already scopes them; we can't box-filter without coords).
|
||||
if not self._in_coverage(event):
|
||||
continue
|
||||
|
||||
new_events.append(event)
|
||||
|
||||
# Check if data changed
|
||||
|
|
|
|||
4
work/meshai/env/store.py
vendored
4
work/meshai/env/store.py
vendored
|
|
@ -79,7 +79,7 @@ class EnvironmentalStore:
|
|||
|
||||
# Create adapter instances with error isolation
|
||||
self._register_adapter("nws", config.nws, ".nws", "NWSAlertsAdapter",
|
||||
lambda cfg: (cfg,))
|
||||
lambda cfg: (cfg, self._coverage_for("nws")))
|
||||
self._register_adapter("swpc", config.swpc, ".swpc", "SWPCAdapter",
|
||||
lambda cfg: (cfg,))
|
||||
self._register_adapter("ducting", config.ducting, ".ducting", "DuctingAdapter",
|
||||
|
|
@ -93,7 +93,7 @@ class EnvironmentalStore:
|
|||
self._register_adapter("usgs_quake", config.usgs_quake, ".usgs_quake", "USGSQuakeAdapter",
|
||||
lambda cfg: (cfg, self._coverage_for("usgs_quake")))
|
||||
self._register_adapter("traffic", config.traffic, ".traffic", "TomTomTrafficAdapter",
|
||||
lambda cfg: (cfg,))
|
||||
lambda cfg: (cfg, self._coverage_for("traffic")))
|
||||
self._register_adapter("roads511", config.roads511, ".roads511", "Roads511Adapter",
|
||||
lambda cfg: (cfg, self._coverage_for("roads511")))
|
||||
self._register_adapter("wzdx", config.wzdx, ".wzdx", "WZDxAdapter",
|
||||
|
|
|
|||
10
work/meshai/env/traffic.py
vendored
10
work/meshai/env/traffic.py
vendored
|
|
@ -22,9 +22,15 @@ class TomTomTrafficAdapter:
|
|||
|
||||
BASE_URL = "https://api.tomtom.com/traffic/services/4/flowSegmentData/relative0/10/json"
|
||||
|
||||
def __init__(self, config: "TomTomConfig"):
|
||||
def __init__(self, config: "TomTomConfig", coverage: dict = None):
|
||||
self._api_key = self._resolve_env(config.api_key or "")
|
||||
self._corridors = config.corridors or []
|
||||
if coverage is not None:
|
||||
self._corridors = [
|
||||
{"name": f"grid_{i}", "lat": la, "lon": lo}
|
||||
for i, (la, lo) in enumerate(coverage["points"])
|
||||
]
|
||||
else:
|
||||
self._corridors = config.corridors or []
|
||||
self._tick_interval = config.tick_seconds or 300
|
||||
self._last_tick = 0.0
|
||||
self._events = []
|
||||
|
|
|
|||
205
work/tests/test_coverage_wiring_2c.py
Normal file
205
work/tests/test_coverage_wiring_2c.py
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
"""Tests for Phase 2c coverage-bbox wiring: nws + traffic.
|
||||
|
||||
Verifies that:
|
||||
1. traffic: coverage wins (9 grid corridors derived from bbox); None falls back
|
||||
to config.corridors.
|
||||
2. nws: coverage wins for areas + coverage_bbox; None falls back to config.areas
|
||||
and leaves _coverage_bbox as None.
|
||||
3. nws geometry filter: alerts inside bbox are kept; outside are dropped; alerts
|
||||
with no centroid (lat/lon None) are always kept.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from meshai.coverage import resolve_adapter_coverage
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared reference bboxes
|
||||
# ---------------------------------------------------------------------------
|
||||
IDAHO_BOX = [-116.5, 42.0, -112.0, 44.0] # Magic Valley / south-central Idaho
|
||||
|
||||
|
||||
def _cov(adapter_name: str, bbox: list = None) -> dict | None:
|
||||
return resolve_adapter_coverage(adapter_name, bbox or IDAHO_BOX, "native")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# traffic (TomTomTrafficAdapter)
|
||||
# ===========================================================================
|
||||
|
||||
def _traffic_cfg(corridors=None):
|
||||
cfg = MagicMock()
|
||||
cfg.api_key = "test-key"
|
||||
cfg.corridors = corridors if corridors is not None else []
|
||||
cfg.tick_seconds = 300
|
||||
return cfg
|
||||
|
||||
|
||||
def test_traffic_coverage_wins():
|
||||
"""When coverage is provided, _corridors is a 3x3 grid from the bbox."""
|
||||
from meshai.env.traffic import TomTomTrafficAdapter
|
||||
cov = _cov("traffic")
|
||||
assert cov is not None
|
||||
assert "points" in cov
|
||||
cfg = _traffic_cfg(corridors=[])
|
||||
adapter = TomTomTrafficAdapter(cfg, coverage=cov)
|
||||
# Must have exactly 9 corridors (3x3 grid)
|
||||
assert len(adapter._corridors) == 9
|
||||
# Each corridor must be a dict with name, lat, lon
|
||||
for i, corridor in enumerate(adapter._corridors):
|
||||
assert isinstance(corridor, dict), f"corridor {i} is not a dict"
|
||||
assert "name" in corridor, f"corridor {i} missing 'name'"
|
||||
assert "lat" in corridor, f"corridor {i} missing 'lat'"
|
||||
assert "lon" in corridor, f"corridor {i} missing 'lon'"
|
||||
# All names must follow grid_N pattern
|
||||
names = [c["name"] for c in adapter._corridors]
|
||||
assert names == [f"grid_{i}" for i in range(9)]
|
||||
|
||||
|
||||
def test_traffic_coverage_corridors_inside_bbox():
|
||||
"""All grid corridor points must fall inside the coverage bbox."""
|
||||
from meshai.env.traffic import TomTomTrafficAdapter
|
||||
from meshai.coverage import point_in_bbox
|
||||
cov = _cov("traffic")
|
||||
assert cov is not None
|
||||
cfg = _traffic_cfg()
|
||||
adapter = TomTomTrafficAdapter(cfg, coverage=cov)
|
||||
west, south, east, north = IDAHO_BOX
|
||||
for corridor in adapter._corridors:
|
||||
lat, lon = corridor["lat"], corridor["lon"]
|
||||
assert point_in_bbox(lat, lon, IDAHO_BOX), (
|
||||
f"corridor {corridor['name']} ({lat},{lon}) outside bbox"
|
||||
)
|
||||
|
||||
|
||||
def test_traffic_fallback_to_config():
|
||||
"""When coverage=None, _corridors falls back to config.corridors."""
|
||||
from meshai.env.traffic import TomTomTrafficAdapter
|
||||
config_corridors = [
|
||||
{"name": "US-93 Twin Falls", "lat": 42.56, "lon": -114.47},
|
||||
{"name": "I-84 Bliss", "lat": 42.93, "lon": -114.98},
|
||||
]
|
||||
cfg = _traffic_cfg(corridors=config_corridors)
|
||||
adapter = TomTomTrafficAdapter(cfg, coverage=None)
|
||||
assert adapter._corridors == config_corridors
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# nws (NWSAlertsAdapter)
|
||||
# ===========================================================================
|
||||
|
||||
def _nws_cfg(areas=None):
|
||||
cfg = MagicMock()
|
||||
cfg.areas = areas if areas is not None else ["ID"]
|
||||
cfg.user_agent = "(meshai-test, test@example.com)"
|
||||
cfg.severity_min = "moderate"
|
||||
cfg.tick_seconds = 60
|
||||
return cfg
|
||||
|
||||
|
||||
def test_nws_coverage_areas():
|
||||
"""When coverage is provided, _areas comes from coverage['areas']."""
|
||||
from meshai.env.nws import NWSAlertsAdapter
|
||||
cov = _cov("nws")
|
||||
assert cov is not None
|
||||
assert "areas" in cov
|
||||
assert "ID" in cov["areas"], "Idaho box should overlap ID state"
|
||||
cfg = _nws_cfg(areas=["WY"]) # deliberately different config default
|
||||
adapter = NWSAlertsAdapter(cfg, coverage=cov)
|
||||
assert adapter._areas == cov["areas"]
|
||||
|
||||
|
||||
def test_nws_coverage_bbox_set():
|
||||
"""When coverage is provided, _coverage_bbox is set from coverage['bbox']."""
|
||||
from meshai.env.nws import NWSAlertsAdapter
|
||||
cov = _cov("nws")
|
||||
assert cov is not None
|
||||
cfg = _nws_cfg()
|
||||
adapter = NWSAlertsAdapter(cfg, coverage=cov)
|
||||
assert adapter._coverage_bbox == cov["bbox"]
|
||||
assert adapter._coverage_bbox == IDAHO_BOX
|
||||
|
||||
|
||||
def test_nws_fallback_to_config():
|
||||
"""When coverage=None, _areas falls back to config.areas and _coverage_bbox is None."""
|
||||
from meshai.env.nws import NWSAlertsAdapter
|
||||
cfg = _nws_cfg(areas=["ID", "OR"])
|
||||
adapter = NWSAlertsAdapter(cfg, coverage=None)
|
||||
assert adapter._areas == ["ID", "OR"]
|
||||
assert adapter._coverage_bbox is None
|
||||
|
||||
|
||||
def test_nws_fallback_config_areas_default():
|
||||
"""When coverage=None and config.areas is empty/None, falls back to ['ID']."""
|
||||
from meshai.env.nws import NWSAlertsAdapter
|
||||
cfg = _nws_cfg(areas=None)
|
||||
cfg.areas = None # force the falsy case
|
||||
adapter = NWSAlertsAdapter(cfg, coverage=None)
|
||||
assert adapter._areas == ["ID"]
|
||||
assert adapter._coverage_bbox is None
|
||||
|
||||
|
||||
def test_nws_empty_areas_fallback_for_api():
|
||||
"""When derived areas is empty, config areas are used for the API call but bbox is kept."""
|
||||
from meshai.env.nws import NWSAlertsAdapter
|
||||
# Synthesise a coverage dict whose areas list is empty (bbox outside all states)
|
||||
empty_areas_cov = {"areas": [], "bbox": [-1.0, 0.0, 1.0, 1.0]}
|
||||
cfg = _nws_cfg(areas=["ID"])
|
||||
adapter = NWSAlertsAdapter(cfg, coverage=empty_areas_cov)
|
||||
# API area= query falls back to config areas
|
||||
assert adapter._areas == ["ID"]
|
||||
# but the geometry filter bbox is still set
|
||||
assert adapter._coverage_bbox == [-1.0, 0.0, 1.0, 1.0]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# nws geometry filter (_in_coverage helper)
|
||||
# ===========================================================================
|
||||
|
||||
def _make_nws_with_bbox(bbox=None):
|
||||
"""Create a NWSAlertsAdapter with a specific coverage_bbox, no live config needed."""
|
||||
from meshai.env.nws import NWSAlertsAdapter
|
||||
cfg = _nws_cfg()
|
||||
if bbox is not None:
|
||||
cov = {"areas": ["ID"], "bbox": bbox}
|
||||
return NWSAlertsAdapter(cfg, coverage=cov)
|
||||
return NWSAlertsAdapter(cfg, coverage=None)
|
||||
|
||||
|
||||
def test_nws_in_coverage_inside_bbox_kept():
|
||||
"""Alert whose centroid is inside the bbox passes the filter."""
|
||||
adapter = _make_nws_with_bbox(IDAHO_BOX)
|
||||
# Twin Falls is inside the Idaho box
|
||||
event = {"lat": 42.56, "lon": -114.47}
|
||||
assert adapter._in_coverage(event) is True
|
||||
|
||||
|
||||
def test_nws_in_coverage_outside_bbox_dropped():
|
||||
"""Alert whose centroid is outside the bbox is filtered out."""
|
||||
adapter = _make_nws_with_bbox(IDAHO_BOX)
|
||||
# Seattle (47.6, -122.3) is well outside south-central Idaho box
|
||||
event = {"lat": 47.6, "lon": -122.3}
|
||||
assert adapter._in_coverage(event) is False
|
||||
|
||||
|
||||
def test_nws_in_coverage_no_coords_kept():
|
||||
"""Alert with no lat/lon is always kept (can't filter without coords)."""
|
||||
adapter = _make_nws_with_bbox(IDAHO_BOX)
|
||||
event = {} # no lat/lon keys at all
|
||||
assert adapter._in_coverage(event) is True
|
||||
|
||||
event_none = {"lat": None, "lon": None}
|
||||
assert adapter._in_coverage(event_none) is True
|
||||
|
||||
|
||||
def test_nws_in_coverage_no_bbox_always_passes():
|
||||
"""When no coverage bbox is set, _in_coverage always returns True."""
|
||||
adapter = _make_nws_with_bbox(bbox=None)
|
||||
assert adapter._coverage_bbox is None
|
||||
# Even an "outside" coord passes when there is no bbox filter
|
||||
event = {"lat": 47.6, "lon": -122.3}
|
||||
assert adapter._in_coverage(event) is True
|
||||
Loading…
Add table
Add a link
Reference in a new issue