mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
feat(coverage): wire quake/roads511/wzdx/avalanche to the coverage bbox (#60)
Phase 2a: plumb config.coverage.bbox through EnvironmentalStore to adapter constructors (mirroring region_anchors) and wire the four straightforward native adapters to derive their scope from it — usgs_quake/roads511 bbox, wzdx states+bbox, avalanche center_ids — via resolve_adapter_coverage(). Coverage bbox (when set) governs; the adapter's own config field is the fallback when unset. Central-fed adapters are unaffected (never native- constructed). fires/firms/hydro/traffic/satpass/ducting/nws follow. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
997729241e
commit
dd90f4a961
7 changed files with 268 additions and 14 deletions
4
work/meshai/env/avalanche.py
vendored
4
work/meshai/env/avalanche.py
vendored
|
|
@ -32,8 +32,8 @@ class AvalancheAdapter:
|
|||
5: ("extreme", "Extreme"),
|
||||
}
|
||||
|
||||
def __init__(self, config: "AvalancheConfig"):
|
||||
self._center_ids = config.center_ids
|
||||
def __init__(self, config: "AvalancheConfig", coverage: dict = None):
|
||||
self._center_ids = coverage["center_ids"] if coverage is not None else config.center_ids
|
||||
self._tick_interval = config.tick_seconds or 1800
|
||||
self._season_months = config.season_months or [12, 1, 2, 3, 4]
|
||||
self._last_tick = 0.0
|
||||
|
|
|
|||
4
work/meshai/env/roads511.py
vendored
4
work/meshai/env/roads511.py
vendored
|
|
@ -24,11 +24,11 @@ logger = logging.getLogger(__name__)
|
|||
class Roads511Adapter:
|
||||
"""511 road conditions polling adapter."""
|
||||
|
||||
def __init__(self, config: "Roads511Config"):
|
||||
def __init__(self, config: "Roads511Config", coverage: dict = None):
|
||||
self._api_key = self._resolve_env(config.api_key or "")
|
||||
self._base_url = (config.base_url or "").rstrip("/")
|
||||
self._endpoints = config.endpoints or ["/get/event"]
|
||||
self._bbox = config.bbox or [] # [west, south, east, north]
|
||||
self._bbox = coverage["bbox"] if coverage is not None else (config.bbox or []) # [west, south, east, north]
|
||||
self._tick_interval = config.tick_seconds or 300
|
||||
self._last_tick = 0.0
|
||||
self._events = []
|
||||
|
|
|
|||
15
work/meshai/env/store.py
vendored
15
work/meshai/env/store.py
vendored
|
|
@ -38,6 +38,7 @@ class EnvironmentalStore:
|
|||
config: "EnvironmentalConfig",
|
||||
region_anchors: list = None,
|
||||
event_bus: Optional["EventBus"] = None,
|
||||
coverage_bbox: list = None,
|
||||
):
|
||||
self._adapters = {} # name -> adapter instance
|
||||
self._failed_adapters = {} # name -> last_error string
|
||||
|
|
@ -47,6 +48,7 @@ class EnvironmentalStore:
|
|||
self._ducting_status = {} # tropo ducting assessment
|
||||
self._mesh_zones = config.nws_zones or []
|
||||
self._region_anchors = region_anchors or []
|
||||
self._coverage_bbox = coverage_bbox or []
|
||||
|
||||
# ── Received-delta gate (NATIVE-only) ────────────────────────────
|
||||
# The model the operator demanded: a native adapter broadcasts an item
|
||||
|
|
@ -85,17 +87,17 @@ class EnvironmentalStore:
|
|||
self._register_adapter("nifc", config.fires, ".fires", "NICFFiresAdapter",
|
||||
lambda cfg: (cfg, self._region_anchors))
|
||||
self._register_adapter("avalanche", config.avalanche, ".avalanche", "AvalancheAdapter",
|
||||
lambda cfg: (cfg,))
|
||||
lambda cfg: (cfg, self._coverage_for("avalanche")))
|
||||
self._register_adapter("usgs", config.usgs, ".usgs", "USGSStreamsAdapter",
|
||||
lambda cfg: (cfg,))
|
||||
self._register_adapter("usgs_quake", config.usgs_quake, ".usgs_quake", "USGSQuakeAdapter",
|
||||
lambda cfg: (cfg,))
|
||||
lambda cfg: (cfg, self._coverage_for("usgs_quake")))
|
||||
self._register_adapter("traffic", config.traffic, ".traffic", "TomTomTrafficAdapter",
|
||||
lambda cfg: (cfg,))
|
||||
self._register_adapter("roads511", config.roads511, ".roads511", "Roads511Adapter",
|
||||
lambda cfg: (cfg,))
|
||||
lambda cfg: (cfg, self._coverage_for("roads511")))
|
||||
self._register_adapter("wzdx", config.wzdx, ".wzdx", "WZDxAdapter",
|
||||
lambda cfg: (cfg,))
|
||||
lambda cfg: (cfg, self._coverage_for("wzdx")))
|
||||
# Native satpass TLE fetcher (storage-only: populates sat_tles, emits
|
||||
# no events). Gated on satpass.feed_source=="native" like the rest.
|
||||
self._register_adapter("satpass_tle", config.satpass, ".tle_fetch", "TLEFetchAdapter",
|
||||
|
|
@ -145,6 +147,11 @@ class EnvironmentalStore:
|
|||
logger.warning("Failed to initialize %s adapter: %s", name, err_msg)
|
||||
self._failed_adapters[name] = err_msg
|
||||
|
||||
def _coverage_for(self, adapter: str):
|
||||
"""Derived coverage scope for a NATIVE adapter, or None to use its own config (override/fallback)."""
|
||||
from meshai import coverage as _cov
|
||||
return _cov.resolve_adapter_coverage(adapter, self._coverage_bbox, "native")
|
||||
|
||||
def refresh(self) -> bool:
|
||||
"""Called every second from main loop. Ticks each adapter.
|
||||
|
||||
|
|
|
|||
4
work/meshai/env/usgs_quake.py
vendored
4
work/meshai/env/usgs_quake.py
vendored
|
|
@ -29,10 +29,10 @@ class USGSQuakeAdapter:
|
|||
geometry.coordinates [lon, lat, depth_km].
|
||||
"""
|
||||
|
||||
def __init__(self, config: "USGSQuakeConfig"):
|
||||
def __init__(self, config: "USGSQuakeConfig", coverage: dict = None):
|
||||
self._feed_url = config.feed_url
|
||||
self._min_magnitude = config.min_magnitude
|
||||
self._bbox = config.bbox or [] # [west, south, east, north]
|
||||
self._bbox = coverage["bbox"] if coverage is not None else (config.bbox or []) # [west, south, east, north]
|
||||
self._region = config.region or "magic_valley"
|
||||
self._tick_interval = config.tick_seconds or 300
|
||||
self._last_tick = 0.0
|
||||
|
|
|
|||
10
work/meshai/env/wzdx.py
vendored
10
work/meshai/env/wzdx.py
vendored
|
|
@ -95,14 +95,18 @@ def _unwrap_url(v) -> str:
|
|||
class WZDxAdapter:
|
||||
"""FHWA WZDx work-zone polling adapter (native ``work_zone`` source)."""
|
||||
|
||||
def __init__(self, config: "WZDxConfig"):
|
||||
def __init__(self, config: "WZDxConfig", coverage: dict = None):
|
||||
self._api_key = self._resolve_env(config.api_key or "") # unused (keyless)
|
||||
self._base_url = (config.base_url or "").strip().rstrip("/")
|
||||
self._registry_url = (config.registry_url or "").strip()
|
||||
self._registry_ttl = config.registry_ttl or 21600
|
||||
self._tick_interval = config.tick_seconds or 300
|
||||
self._states = [str(s).strip() for s in (config.states or ["ID"]) if str(s).strip()]
|
||||
self._bbox = config.bbox or [] # [west, south, east, north]
|
||||
if coverage is not None:
|
||||
self._states = coverage["states"] # [west, south, east, north] derived state codes
|
||||
self._bbox = coverage["bbox"]
|
||||
else:
|
||||
self._states = [str(s).strip() for s in (config.states or ["ID"]) if str(s).strip()]
|
||||
self._bbox = config.bbox or [] # [west, south, east, north]
|
||||
|
||||
self._last_tick = 0.0
|
||||
self._events = []
|
||||
|
|
|
|||
|
|
@ -616,8 +616,11 @@ class MeshAI:
|
|||
from .env.store import EnvironmentalStore
|
||||
# Pass region anchors for fire proximity calculation
|
||||
region_anchors = self.config.mesh_intelligence.regions if self.config.mesh_intelligence.enabled else []
|
||||
cov = self.config.coverage
|
||||
coverage_bbox = cov.bbox if (cov.enabled and cov.bbox) else []
|
||||
self.env_store = EnvironmentalStore(
|
||||
config=env_cfg, region_anchors=region_anchors, event_bus=self.event_bus
|
||||
config=env_cfg, region_anchors=region_anchors,
|
||||
coverage_bbox=coverage_bbox, event_bus=self.event_bus,
|
||||
)
|
||||
logger.info(f"Environmental feeds enabled ({len(self.env_store._adapters)} adapters)")
|
||||
else:
|
||||
|
|
|
|||
240
work/tests/test_coverage_wiring.py
Normal file
240
work/tests/test_coverage_wiring.py
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
"""Tests for Phase 2a coverage-bbox wiring.
|
||||
|
||||
Verifies that:
|
||||
1. Each of the four simple adapters respects the `coverage` kwarg — derived
|
||||
scope wins when coverage is provided, config field is the fallback when None.
|
||||
2. EnvironmentalStore._coverage_for returns a dict for a valid bbox and None
|
||||
for an empty bbox.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from meshai.coverage import resolve_adapter_coverage
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared reference bbox (Magic Valley / south-central Idaho)
|
||||
# ---------------------------------------------------------------------------
|
||||
IDAHO_BOX = [-116.5, 42.0, -112.0, 44.0]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers — derive the expected coverage dicts from the module itself
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _cov(adapter_name: str, bbox: list = None) -> dict | None:
|
||||
return resolve_adapter_coverage(adapter_name, bbox or IDAHO_BOX, "native")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# usgs_quake
|
||||
# ===========================================================================
|
||||
|
||||
def _quake_cfg(bbox=None):
|
||||
cfg = MagicMock()
|
||||
cfg.feed_url = "https://example.test/feed.geojson"
|
||||
cfg.min_magnitude = 2.5
|
||||
cfg.bbox = bbox if bbox is not None else [-115.5, 42.0, -110.0, 45.2]
|
||||
cfg.region = "magic_valley"
|
||||
cfg.tick_seconds = 300
|
||||
return cfg
|
||||
|
||||
|
||||
def test_usgs_quake_coverage_wins():
|
||||
"""When coverage is provided, adapter._bbox comes from coverage['bbox']."""
|
||||
from meshai.env.usgs_quake import USGSQuakeAdapter
|
||||
cov = _cov("usgs_quake")
|
||||
assert cov is not None
|
||||
adapter = USGSQuakeAdapter(_quake_cfg(), coverage=cov)
|
||||
assert adapter._bbox == cov["bbox"]
|
||||
|
||||
|
||||
def test_usgs_quake_fallback_to_config():
|
||||
"""When coverage=None, adapter._bbox falls back to config.bbox."""
|
||||
from meshai.env.usgs_quake import USGSQuakeAdapter
|
||||
cfg = _quake_cfg(bbox=[-115.5, 42.0, -110.0, 45.2])
|
||||
adapter = USGSQuakeAdapter(cfg, coverage=None)
|
||||
assert adapter._bbox == cfg.bbox
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# roads511
|
||||
# ===========================================================================
|
||||
|
||||
def _roads_cfg(bbox=None):
|
||||
cfg = MagicMock()
|
||||
cfg.api_key = ""
|
||||
cfg.base_url = ""
|
||||
cfg.endpoints = ["/get/event"]
|
||||
cfg.bbox = bbox if bbox is not None else [-115.0, 42.5, -111.0, 44.0]
|
||||
cfg.tick_seconds = 300
|
||||
return cfg
|
||||
|
||||
|
||||
def test_roads511_coverage_wins():
|
||||
"""When coverage is provided, adapter._bbox comes from coverage['bbox']."""
|
||||
from meshai.env.roads511 import Roads511Adapter
|
||||
cov = _cov("roads511")
|
||||
assert cov is not None
|
||||
adapter = Roads511Adapter(_roads_cfg(), coverage=cov)
|
||||
assert adapter._bbox == cov["bbox"]
|
||||
|
||||
|
||||
def test_roads511_fallback_to_config():
|
||||
"""When coverage=None, adapter._bbox falls back to config.bbox."""
|
||||
from meshai.env.roads511 import Roads511Adapter
|
||||
cfg = _roads_cfg(bbox=[-115.0, 42.5, -111.0, 44.0])
|
||||
adapter = Roads511Adapter(cfg, coverage=None)
|
||||
assert adapter._bbox == cfg.bbox
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# wzdx
|
||||
# ===========================================================================
|
||||
|
||||
def _wzdx_cfg(states=None, bbox=None):
|
||||
cfg = MagicMock()
|
||||
cfg.api_key = ""
|
||||
cfg.base_url = ""
|
||||
cfg.registry_url = "https://example.test/registry.json"
|
||||
cfg.registry_ttl = 21600
|
||||
cfg.tick_seconds = 300
|
||||
cfg.states = states if states is not None else ["ID"]
|
||||
cfg.bbox = bbox if bbox is not None else []
|
||||
return cfg
|
||||
|
||||
|
||||
def test_wzdx_coverage_wins():
|
||||
"""When coverage is provided, both _states and _bbox come from coverage."""
|
||||
from meshai.env.wzdx import WZDxAdapter
|
||||
cov = _cov("wzdx")
|
||||
assert cov is not None
|
||||
assert "states" in cov
|
||||
assert "bbox" in cov
|
||||
adapter = WZDxAdapter(_wzdx_cfg(), coverage=cov)
|
||||
assert adapter._states == cov["states"]
|
||||
assert adapter._bbox == cov["bbox"]
|
||||
|
||||
|
||||
def test_wzdx_fallback_to_config():
|
||||
"""When coverage=None, _states and _bbox fall back to config fields."""
|
||||
from meshai.env.wzdx import WZDxAdapter
|
||||
cfg = _wzdx_cfg(states=["ID", "MT"], bbox=[-116.0, 42.0, -111.0, 45.0])
|
||||
adapter = WZDxAdapter(cfg, coverage=None)
|
||||
assert "ID" in adapter._states
|
||||
assert "MT" in adapter._states
|
||||
assert adapter._bbox == cfg.bbox
|
||||
|
||||
|
||||
def test_wzdx_empty_coverage_states_wins():
|
||||
"""Empty derived states list still wins over config (coverage governs)."""
|
||||
from meshai.env.wzdx import WZDxAdapter
|
||||
# Provide a bbox that maps to no states (unlikely but logically possible)
|
||||
empty_states_cov = {"states": [], "bbox": [-1.0, 0.0, 1.0, 1.0]}
|
||||
cfg = _wzdx_cfg(states=["ID"])
|
||||
adapter = WZDxAdapter(cfg, coverage=empty_states_cov)
|
||||
assert adapter._states == [] # coverage governs even when empty
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# avalanche
|
||||
# ===========================================================================
|
||||
|
||||
def _avy_cfg(center_ids=None):
|
||||
cfg = MagicMock()
|
||||
cfg.center_ids = center_ids if center_ids is not None else ["SNFAC"]
|
||||
cfg.tick_seconds = 1800
|
||||
cfg.season_months = [12, 1, 2, 3, 4]
|
||||
return cfg
|
||||
|
||||
|
||||
def test_avalanche_coverage_wins():
|
||||
"""When coverage is provided, adapter._center_ids comes from coverage['center_ids']."""
|
||||
from meshai.env.avalanche import AvalancheAdapter
|
||||
cov = _cov("avalanche")
|
||||
assert cov is not None
|
||||
assert "center_ids" in cov
|
||||
adapter = AvalancheAdapter(_avy_cfg(), coverage=cov)
|
||||
assert adapter._center_ids == cov["center_ids"]
|
||||
|
||||
|
||||
def test_avalanche_fallback_to_config():
|
||||
"""When coverage=None, adapter._center_ids falls back to config.center_ids."""
|
||||
from meshai.env.avalanche import AvalancheAdapter
|
||||
cfg = _avy_cfg(center_ids=["SNFAC", "PAC"])
|
||||
adapter = AvalancheAdapter(cfg, coverage=None)
|
||||
assert adapter._center_ids == ["SNFAC", "PAC"]
|
||||
|
||||
|
||||
def test_avalanche_empty_coverage_center_ids_wins():
|
||||
"""Empty derived center_ids list wins over config (coverage governs)."""
|
||||
from meshai.env.avalanche import AvalancheAdapter
|
||||
# A bbox in the Pacific would yield no centers
|
||||
no_center_cov = {"center_ids": []}
|
||||
cfg = _avy_cfg(center_ids=["SNFAC"])
|
||||
adapter = AvalancheAdapter(cfg, coverage=no_center_cov)
|
||||
assert adapter._center_ids == [] # coverage governs even when empty
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# EnvironmentalStore._coverage_for
|
||||
# ===========================================================================
|
||||
|
||||
def _make_env_cfg(enabled=False):
|
||||
"""Minimal EnvironmentalConfig mock with all adapters disabled."""
|
||||
env_cfg = MagicMock()
|
||||
for attr in ("nws", "swpc", "ducting", "fires", "avalanche", "usgs",
|
||||
"usgs_quake", "traffic", "roads511", "wzdx", "satpass", "firms"):
|
||||
sub = MagicMock()
|
||||
sub.enabled = False
|
||||
sub.feed_source = "central"
|
||||
setattr(env_cfg, attr, sub)
|
||||
env_cfg.nws_zones = []
|
||||
return env_cfg
|
||||
|
||||
|
||||
def test_coverage_for_returns_dict_for_valid_bbox():
|
||||
"""_coverage_for returns a dict when coverage_bbox is a valid box."""
|
||||
from meshai.env.store import EnvironmentalStore
|
||||
env_cfg = _make_env_cfg()
|
||||
store = EnvironmentalStore(config=env_cfg, coverage_bbox=IDAHO_BOX)
|
||||
result = store._coverage_for("usgs_quake")
|
||||
assert isinstance(result, dict)
|
||||
assert "bbox" in result
|
||||
assert result["bbox"] == IDAHO_BOX
|
||||
|
||||
|
||||
def test_coverage_for_returns_none_for_empty_bbox():
|
||||
"""_coverage_for returns None when coverage_bbox is empty."""
|
||||
from meshai.env.store import EnvironmentalStore
|
||||
env_cfg = _make_env_cfg()
|
||||
store = EnvironmentalStore(config=env_cfg, coverage_bbox=[])
|
||||
result = store._coverage_for("usgs_quake")
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_coverage_for_avalanche_returns_center_ids():
|
||||
"""_coverage_for('avalanche') returns center_ids for a valid Idaho box."""
|
||||
from meshai.env.store import EnvironmentalStore
|
||||
env_cfg = _make_env_cfg()
|
||||
store = EnvironmentalStore(config=env_cfg, coverage_bbox=IDAHO_BOX)
|
||||
result = store._coverage_for("avalanche")
|
||||
assert result is not None
|
||||
assert "center_ids" in result
|
||||
assert isinstance(result["center_ids"], list)
|
||||
# SNFAC (Sawtooth) covers south-central Idaho — should be in the result
|
||||
assert "SNFAC" in result["center_ids"]
|
||||
|
||||
|
||||
def test_coverage_for_wzdx_returns_states_and_bbox():
|
||||
"""_coverage_for('wzdx') returns both states and bbox."""
|
||||
from meshai.env.store import EnvironmentalStore
|
||||
env_cfg = _make_env_cfg()
|
||||
store = EnvironmentalStore(config=env_cfg, coverage_bbox=IDAHO_BOX)
|
||||
result = store._coverage_for("wzdx")
|
||||
assert result is not None
|
||||
assert "states" in result and "bbox" in result
|
||||
assert "ID" in result["states"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue