From 820d7070df1abb7c5710fa7b1323595d81ef2457 Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Mon, 6 Jul 2026 14:34:39 +0000 Subject: [PATCH] feat(coverage): wire quake/roads511/wzdx/avalanche to the coverage bbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: Claude Opus 4.8 (1M context) --- work/meshai/env/avalanche.py | 4 +- work/meshai/env/roads511.py | 4 +- work/meshai/env/store.py | 15 +- work/meshai/env/usgs_quake.py | 4 +- work/meshai/env/wzdx.py | 10 +- work/meshai/main.py | 5 +- work/tests/test_coverage_wiring.py | 240 +++++++++++++++++++++++++++++ 7 files changed, 268 insertions(+), 14 deletions(-) create mode 100644 work/tests/test_coverage_wiring.py diff --git a/work/meshai/env/avalanche.py b/work/meshai/env/avalanche.py index 8d6c937..e50e57f 100644 --- a/work/meshai/env/avalanche.py +++ b/work/meshai/env/avalanche.py @@ -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 diff --git a/work/meshai/env/roads511.py b/work/meshai/env/roads511.py index 80e7af7..ba90bde 100644 --- a/work/meshai/env/roads511.py +++ b/work/meshai/env/roads511.py @@ -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 = [] diff --git a/work/meshai/env/store.py b/work/meshai/env/store.py index b61f661..a360da0 100644 --- a/work/meshai/env/store.py +++ b/work/meshai/env/store.py @@ -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. diff --git a/work/meshai/env/usgs_quake.py b/work/meshai/env/usgs_quake.py index 84ce92a..943e1af 100644 --- a/work/meshai/env/usgs_quake.py +++ b/work/meshai/env/usgs_quake.py @@ -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 diff --git a/work/meshai/env/wzdx.py b/work/meshai/env/wzdx.py index f4825e4..d822e1e 100644 --- a/work/meshai/env/wzdx.py +++ b/work/meshai/env/wzdx.py @@ -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 = [] diff --git a/work/meshai/main.py b/work/meshai/main.py index a54d448..cc96513 100644 --- a/work/meshai/main.py +++ b/work/meshai/main.py @@ -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: diff --git a/work/tests/test_coverage_wiring.py b/work/tests/test_coverage_wiring.py new file mode 100644 index 0000000..1ff234f --- /dev/null +++ b/work/tests/test_coverage_wiring.py @@ -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"]