mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 09:24:44 +02:00
feat(notifications): Phase 2.14 USGS earthquake adapter (new) -- closes Rule 16 Seismic standalone path
First net-new environmental adapter (prior phases wired existing ones). Adds meshai/env/usgs_quake.py with USGSQuakeAdapter + USGSQuakeConfig, polling a keyless USGS earthquake GeoJSON feed and emitting one Event per qualifying quake. Establishes the standalone Seismic path (Rule 16); Central becomes the dual-source in v0.4. Adapter (mirrors the fires/usgs-water per-event pattern): - Feed: https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson (M2.5+ past day -- M1.0 too noisy, M4.5+ too sparse for the region). Tick 300s. - Filters each feature by min_magnitude AND a geographic bbox. - Per quake: source=usgs_quake, category=earthquake_event, stable event_id = the USGS feature id (e.g. "us6000abcd"), lat/lon from geometry.coordinates[1],[0], region tag from config (default "magic_valley"). - to_event(): category earthquake_event, magnitude-binned severity passed through, group_key = inhibit_key = the USGS id. Defensive None for missing id / coords / magnitude. get_events()/health_status mirror the other adapters. MAGNITUDE -> SEVERITY BINS (as proposed): M < 3.5 -> routine 3.5 <= M < 5.0 -> priority M >= 5.0 -> immediate ('sig' is captured in the event dict as metadata but severity is magnitude-binned -- clearer and matches the spec's primary suggestion.) GEOGRAPHIC BBOX (as proposed) -- [west, south, east, north]: [-115.5, 42.0, -110.0, 45.2] Covers Magic Valley / Twin Falls (SW), the Lost River Range / Borah Peak and Sawtooths (central Idaho, seismically active -- 1983 M6.9), the eastern Snake River Plain / INL, and the Yellowstone caldera (NW Wyoming). An empty bbox disables the geographic filter (accepts all). Wiring: - config.py: new USGSQuakeConfig dataclass; usgs_quake field on EnvironmentalConfig; loader branch in _dict_to_dataclass. - store.py __init__: registers self._adapters["usgs_quake"] when enabled -- this is what grows the live adapter count 7 -> 8. - store._ingest: NO dedicated branch added. usgs_quake is a standard per-event adapter, so the existing generic "else" loop (dedup on (source, event_id) + _emit_event) already routes it. (The swpc/ducting branches are special only because they also maintain status blobs.) - env_feeds.yaml (live /data/config): added usgs_quake block, enabled:true, default bbox/min_mag/region. Rule 17: GUI-editable config (env_feeds.yaml). Rule 18 N/A -- USGS earthquake feed is keyless (no .env entry; .ref credentials has no USGS/ArcGIS/quake key). Rule 16: standalone path established + validated in-container. Tests: tests/test_adapter_usgs_quake.py (15 tests) mirrors the 2.12/2.13 shape -- severity bins, _fetch severity assignment, magnitude filter, geographic filter (in-bbox vs California/out), empty-bbox-accepts-all, dedup id stable across ticks for the same quake id, category, severity pass-through, group_key/inhibit_keys, field population, defensive cases (missing id/coords/magnitude/corrupted -> None), and malformed-feature skipping. _fetch tests patch urlopen with synthetic FeatureCollections. Full suite: 248 passed. Live smoke test (prod container, rebuilt): clean startup, adapter count grew 7 -> 8 ("EnvironmentalStore initialized with 8 adapters"), healthy, no traceback, no usgs_quake errors. In-container standalone tick over the real feed succeeded (is_loaded=true, last_error=null, consecutive_errors=0); the feed returned 54 global M2.5+ quakes, 0 inside the Magic Valley->Yellowstone bbox right now (quiet) -- so no Event is emitted, acceptable, and it exercises the fetch + magnitude + geographic filter + no-emit path on live data. The emission path (in-region quake -> earthquake_event) is unit-validated and uses the same store->bus path emitting live for NWS, traffic, and NIFC fires. Note (.gitignore): line 36 `env/` (a virtualenv pattern under "Virtual environments") collaterally matches meshai/env/, so this NEW file required `git add -f` (untracked files there are otherwise ignored and hidden from status). Existing tracked env files are unaffected. Recommended follow-up: anchor the rule to `/env/` so future net-new env adapters don't need -f. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d3b62ad3c5
commit
8b2cdeee0b
4 changed files with 503 additions and 0 deletions
|
|
@ -377,6 +377,19 @@ class USGSConfig:
|
|||
flood_thresholds: dict = field(default_factory=dict) # {site_id: {flow: X, height: Y}}
|
||||
|
||||
|
||||
@dataclass
|
||||
class USGSQuakeConfig:
|
||||
"""USGS earthquake feed settings (Phase 2.14)."""
|
||||
|
||||
enabled: bool = False
|
||||
tick_seconds: int = 300
|
||||
feed_url: str = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson"
|
||||
min_magnitude: float = 2.5
|
||||
# [west, south, east, north] -- Magic Valley -> Borah Peak -> Yellowstone
|
||||
bbox: list = field(default_factory=lambda: [-115.5, 42.0, -110.0, 45.2])
|
||||
region: str = "magic_valley"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TomTomConfig:
|
||||
"""TomTom traffic flow settings."""
|
||||
|
|
@ -425,6 +438,7 @@ class EnvironmentalConfig:
|
|||
fires: NICFFiresConfig = field(default_factory=NICFFiresConfig)
|
||||
avalanche: AvalancheConfig = field(default_factory=AvalancheConfig)
|
||||
usgs: USGSConfig = field(default_factory=USGSConfig)
|
||||
usgs_quake: USGSQuakeConfig = field(default_factory=USGSQuakeConfig)
|
||||
traffic: TomTomConfig = field(default_factory=TomTomConfig)
|
||||
roads511: Roads511Config = field(default_factory=Roads511Config)
|
||||
firms: FIRMSConfig = field(default_factory=FIRMSConfig)
|
||||
|
|
@ -673,6 +687,8 @@ def _dict_to_dataclass(cls, data: dict):
|
|||
kwargs[key] = _dict_to_dataclass(AvalancheConfig, value)
|
||||
elif key == "usgs" and isinstance(value, dict):
|
||||
kwargs[key] = _dict_to_dataclass(USGSConfig, value)
|
||||
elif key == "usgs_quake" and isinstance(value, dict):
|
||||
kwargs[key] = _dict_to_dataclass(USGSQuakeConfig, value)
|
||||
elif key == "traffic" and isinstance(value, dict):
|
||||
kwargs[key] = _dict_to_dataclass(TomTomConfig, value)
|
||||
elif key == "roads511" and isinstance(value, dict):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue