feat(v0.5.12): usgs_nwis with minimal Idaho threshold curation (9 starter sites)
Final per-adapter handler before the live flip. Shape matches the v0.5.10 weather/quake/swpc family + the v0.5.9 WFIGS forward-only model that suits water-level data best: rising water is operationally meaningful (downstream warnings, evacuation calls); receding water is intentionally silent.
Components: (1) meshai/central/idaho_gauge_sites.py with a hardcoded 9-site dict covering Magic Valley + Treasure Valley + Salmon-Challis + Snake River system: Big Lost (Mackay), Snake at Heise + Idaho Falls, Big Wood (Hailey), Boise River, Payette at Banks, Henrys Fork (Rexburg), Salmon Falls Creek, Bear River at Border. Each entry carries gauge_name + lat/lon + per-threshold ft values (action / flood_minor / flood_moderate / flood_major; None means that threshold does not apply at that site). Site lookup normalizes incoming envelope monitoring_location_id (\'USGS-13186000\' or bare \'13186000\') to the canonical USGS- prefixed form. STARTER SUBSET clearly flagged in the module docstring -- expansion to full 20+ site coverage deferred to v0.6.x and likely migrated to a DB table editable via the GUI.
(2) meshai/central/nwis_handler.py filters non-curated sites at handler entrance (event_log handled=0, no gauge_readings UPSERT). Parameter filter: 00060 discharge (cfs) and 00065 gage height (ft) only; precipitation (00045) and other parameters skipped. threshold_state computed from value vs curated NWS-AHPS thresholds (high to low). UPSERT into v0.5.8b gauge_readings table (no schema migration needed; threshold_state column already there). Upward crossing detection by comparing current threshold to the most recent prior reading\'s threshold; ordered scale {normal < action < flood_minor < flood_moderate < flood_major}. If current > prior, fire \'New:\' broadcast; otherwise (unchanged, descending, or stays at same level for 96 polls/day), silent.
Wire format MEDIUM: \'🌊 New: {gauge_name}: {label} {value} ft, flow {flow_cfs:,} cfs, @ lat,lon\'. Label maps action->\"action stage\", flood_minor->\"minor flooding\", flood_moderate->\"moderate flooding\", flood_major->\"major flooding\". flow_cfs segment present only when a companion 00060 discharge reading is available. Coords segment dropped when both envelope and curated coords are missing (rare for curated sites which always have coords). Example outputs from the synthetic probe (all under 130-byte target):
🌊 New: Snake River at Heise: action stage 12.5 ft, @ 43.612,-111.654 (71 B)
🌊 New: Snake River at Heise: minor flooding 14.5 ft, @ 43.612,-111.654 (73 B)
🌊 New: Snake River at Heise: moderate flooding 16.5 ft, @ 43.612,-111.654 (76 B)
🌊 New: Boise River near Boise: action stage 8.5 ft, @ 43.690,-116.200 (72 B)
Tests: was 704 (v0.5.11 baseline), now 718 (+14 net new). Coverage: curated-site action-stage broadcasts, non-curated drop, normal-stage silent, normal->action upward crossing, action->normal downward suppression, same-threshold dedup (no broadcast every 15-min poll), flow_cfs companion from prior 00060 reading, coords fallback to curated dict when envelope lacks them, IDAHO_CURATED_SITES count + required-fields check, exact starter-set spot check, commit-callback flips event_log.handled to 1, action->flood_minor re-broadcast at the higher threshold, precip (00045) skipped, site_id normalization accepts bare \'13186000\'.
Synthetic probe over the 58,436 captured nwis envelopes from the v0.5.10 batched investigation: 3,292 hit the 9-site curation (5.6% of total volume); 1 produced a real upward-crossing broadcast detected in the captured stream (validating the dedup story -- subsequent synthesized broadcasts for the same site at the same threshold correctly silent-suppressed). 3 additional synthesized broadcasts from a rising Snake at Heise scenario (9.0->12.5->14.5->16.5 ft); receding step (15.5 ft) correctly produced no broadcast.
usgs_nwis closes the last per-adapter handler before live flip. WFIGS / incident-pipeline / weather / quake / swpc / band-conditions all unchanged. Master OFF in prod through this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 07:47:35 +00:00
|
|
|
"""Tests for v0.5.12 usgs_nwis handler."""
|
|
|
|
|
import pytest
|
|
|
|
|
|
feat(v0.6-4): gauge_sites + town_anchors curation tables + GUI CRUD
Closes Section A.5 (gauge_sites) and A.12 (town_anchors) of the audit
doc by lifting both Python-dict curation lists into editable SQLite
tables. Operators can add/edit/disable rows from the dashboard without
a deploy; runtime reads go through cached accessors that invalidate
when the REST API mutates state.
Schema:
v8.sql adds gauge_sites(site_id PK, gauge_name, lat, lon, action_ft,
flood_minor_ft, flood_moderate_ft, flood_major_ft, enabled, updated_at).
v9.sql adds town_anchors(anchor_id AUTOINC PK, name UNIQUE, lat, lon,
state, enabled, updated_at).
SCHEMA_VERSION 7 -> 9.
Seed (meshai/persistence/curation.py):
_GAUGE_SITES_SEED carries the original 9 Idaho rows from
IDAHO_CURATED_SITES verbatim.
_TOWN_ANCHORS_SEED carries the 29 Idaho-and-neighbor towns from
_TOWN_COORDS verbatim.
seed_gauge_sites() / seed_town_anchors() INSERT OR IGNORE -- safe to
re-run; never overwrites user edits.
Handler integration:
- meshai/central/idaho_gauge_sites.py: IDAHO_CURATED_SITES dict deleted.
lookup_site() now calls meshai.persistence.curation.lookup_gauge_site()
which reads the table. THRESHOLD_RANK, normalize_site_id, and
compute_threshold_state remain in this module (CODE per Matt s rule).
- meshai/central/nwis_handler.py drops IDAHO_CURATED_SITES from its
import list; the table-backed lookup_site() is API-compatible.
- meshai/central_normalizer.py: _TOWN_COORDS dict deleted.
_compute_distance_bearing() now calls
meshai.persistence.curation.lookup_town_anchor() with the same
lowercased-name semantics it always used.
REST API (meshai/dashboard/api/curation_routes.py):
/api/gauge-sites GET list, GET one, POST add, PUT update, DELETE
/api/town-anchors GET list, GET one, POST add, PUT update, DELETE
Every mutation calls invalidate_curation_cache() so handler reads see
the new state on the next call -- no container restart.
Dashboard (dashboard-frontend/src/pages/):
- GaugeSites.tsx: table view with Add row / Edit row inline / Delete
confirm + per-row enabled toggle. 8 columns mirror the schema.
- TownAnchors.tsx: same pattern, 5 columns. Name is lowercased on
save to match the lookup key.
- Left-nav entries "Gauge Sites" (Droplets icon) and "Town Anchors"
(MapPin icon) added to Layout.tsx; routes added to App.tsx.
Tests (tests/test_curation.py, 18 cases):
- v8/v9 tables exist
- Seed lands every row from both dicts
- Seed idempotent; never overwrites user edits
- lookup_gauge_site hits/miss, disabled rows are invisible
- lookup_town_anchor case-insensitive
- REST API: GET list, GET one, GET 404, POST add, PUT update, DELETE,
POST missing-field 400; both gauge_sites + town_anchors
- Accessor reflects API mutations after invalidate_curation_cache()
tests/test_nwis_handler.py back-compat: IDAHO_CURATED_SITES dict alias
points at _GAUGE_SITES_SEED so the existing assertion suite still passes.
tests/test_adapter_config_foundation.py schema_meta v7 -> v9 bump.
Test count: 797 -> 819 (+18 curation cases + 4 maintenance updates).
2026-06-05 20:19:13 +00:00
|
|
|
# v0.6-4: IDAHO_CURATED_SITES dict moved to gauge_sites SQLite table;
|
|
|
|
|
# import the seed data from the curation module as a back-compat alias.
|
|
|
|
|
from meshai.persistence.curation import _GAUGE_SITES_SEED as IDAHO_CURATED_SITES
|
feat(v0.5.12): usgs_nwis with minimal Idaho threshold curation (9 starter sites)
Final per-adapter handler before the live flip. Shape matches the v0.5.10 weather/quake/swpc family + the v0.5.9 WFIGS forward-only model that suits water-level data best: rising water is operationally meaningful (downstream warnings, evacuation calls); receding water is intentionally silent.
Components: (1) meshai/central/idaho_gauge_sites.py with a hardcoded 9-site dict covering Magic Valley + Treasure Valley + Salmon-Challis + Snake River system: Big Lost (Mackay), Snake at Heise + Idaho Falls, Big Wood (Hailey), Boise River, Payette at Banks, Henrys Fork (Rexburg), Salmon Falls Creek, Bear River at Border. Each entry carries gauge_name + lat/lon + per-threshold ft values (action / flood_minor / flood_moderate / flood_major; None means that threshold does not apply at that site). Site lookup normalizes incoming envelope monitoring_location_id (\'USGS-13186000\' or bare \'13186000\') to the canonical USGS- prefixed form. STARTER SUBSET clearly flagged in the module docstring -- expansion to full 20+ site coverage deferred to v0.6.x and likely migrated to a DB table editable via the GUI.
(2) meshai/central/nwis_handler.py filters non-curated sites at handler entrance (event_log handled=0, no gauge_readings UPSERT). Parameter filter: 00060 discharge (cfs) and 00065 gage height (ft) only; precipitation (00045) and other parameters skipped. threshold_state computed from value vs curated NWS-AHPS thresholds (high to low). UPSERT into v0.5.8b gauge_readings table (no schema migration needed; threshold_state column already there). Upward crossing detection by comparing current threshold to the most recent prior reading\'s threshold; ordered scale {normal < action < flood_minor < flood_moderate < flood_major}. If current > prior, fire \'New:\' broadcast; otherwise (unchanged, descending, or stays at same level for 96 polls/day), silent.
Wire format MEDIUM: \'🌊 New: {gauge_name}: {label} {value} ft, flow {flow_cfs:,} cfs, @ lat,lon\'. Label maps action->\"action stage\", flood_minor->\"minor flooding\", flood_moderate->\"moderate flooding\", flood_major->\"major flooding\". flow_cfs segment present only when a companion 00060 discharge reading is available. Coords segment dropped when both envelope and curated coords are missing (rare for curated sites which always have coords). Example outputs from the synthetic probe (all under 130-byte target):
🌊 New: Snake River at Heise: action stage 12.5 ft, @ 43.612,-111.654 (71 B)
🌊 New: Snake River at Heise: minor flooding 14.5 ft, @ 43.612,-111.654 (73 B)
🌊 New: Snake River at Heise: moderate flooding 16.5 ft, @ 43.612,-111.654 (76 B)
🌊 New: Boise River near Boise: action stage 8.5 ft, @ 43.690,-116.200 (72 B)
Tests: was 704 (v0.5.11 baseline), now 718 (+14 net new). Coverage: curated-site action-stage broadcasts, non-curated drop, normal-stage silent, normal->action upward crossing, action->normal downward suppression, same-threshold dedup (no broadcast every 15-min poll), flow_cfs companion from prior 00060 reading, coords fallback to curated dict when envelope lacks them, IDAHO_CURATED_SITES count + required-fields check, exact starter-set spot check, commit-callback flips event_log.handled to 1, action->flood_minor re-broadcast at the higher threshold, precip (00045) skipped, site_id normalization accepts bare \'13186000\'.
Synthetic probe over the 58,436 captured nwis envelopes from the v0.5.10 batched investigation: 3,292 hit the 9-site curation (5.6% of total volume); 1 produced a real upward-crossing broadcast detected in the captured stream (validating the dedup story -- subsequent synthesized broadcasts for the same site at the same threshold correctly silent-suppressed). 3 additional synthesized broadcasts from a rising Snake at Heise scenario (9.0->12.5->14.5->16.5 ft); receding step (15.5 ft) correctly produced no broadcast.
usgs_nwis closes the last per-adapter handler before live flip. WFIGS / incident-pipeline / weather / quake / swpc / band-conditions all unchanged. Master OFF in prod through this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 07:47:35 +00:00
|
|
|
from meshai.central.nwis_handler import handle_nwis
|
|
|
|
|
from meshai.persistence import close_thread_connection, init_db
|
|
|
|
|
from meshai.persistence import db as persistence_db
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def mem_db(monkeypatch, tmp_path):
|
|
|
|
|
db_path = str(tmp_path / "nwis-test.sqlite")
|
|
|
|
|
monkeypatch.setenv("MESHAI_DB_PATH", db_path)
|
|
|
|
|
persistence_db._initialised.clear()
|
|
|
|
|
close_thread_connection()
|
|
|
|
|
conn = init_db()
|
|
|
|
|
yield conn
|
|
|
|
|
close_thread_connection()
|
|
|
|
|
persistence_db._initialised.discard(db_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _nwis_env(*, site_id="USGS-13186000",
|
|
|
|
|
parameter_code="00065", value=13.0,
|
|
|
|
|
unit="ft", time_iso="2026-06-05T15:00:00Z",
|
|
|
|
|
lat=43.612, lon=-111.654,
|
|
|
|
|
envelope_id=None):
|
|
|
|
|
envelope_id = envelope_id or f"nwis_{site_id}_{time_iso}"
|
|
|
|
|
return {
|
|
|
|
|
"id": envelope_id, "subject": f"central.hydro.{parameter_code}.usgs.{site_id}.us.id",
|
|
|
|
|
"data": {
|
|
|
|
|
"id": envelope_id, "adapter": "nwis",
|
|
|
|
|
"category": f"hydro.{parameter_code}", "severity": 0,
|
|
|
|
|
"geo": {"centroid": [lon, lat], "primary_region": "US-ID"},
|
|
|
|
|
"data": {
|
|
|
|
|
"id": envelope_id,
|
|
|
|
|
"monitoring_location_id": site_id,
|
|
|
|
|
"parameter_code": parameter_code,
|
|
|
|
|
"time": time_iso,
|
|
|
|
|
"value": value,
|
|
|
|
|
"unit_of_measure": unit,
|
|
|
|
|
"latitude": lat, "longitude": lon,
|
|
|
|
|
"_enriched": {"geocoder": {
|
|
|
|
|
"name": IDAHO_CURATED_SITES.get(site_id, {}).get(
|
|
|
|
|
"gauge_name", "?"),
|
|
|
|
|
}},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _commit(data, t):
|
|
|
|
|
data["_on_broadcast_committed"](float(t))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- (a) curated site at action stage triggers broadcast -----------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_a_curated_site_action_stage_triggers(mem_db):
|
|
|
|
|
# Snake River at Heise: action=12.0ft, broadcast at 12.5ft.
|
|
|
|
|
env = _nwis_env(site_id="USGS-13186000", parameter_code="00065",
|
|
|
|
|
value=12.5)
|
|
|
|
|
data = {}
|
|
|
|
|
wire = handle_nwis(env, env["subject"], data=data, now=1_000_000)
|
|
|
|
|
assert wire is not None
|
|
|
|
|
assert wire.startswith("🌊 New:")
|
|
|
|
|
assert "Snake River at Heise" in wire
|
|
|
|
|
assert "action stage 12.5 ft" in wire
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- (b) non-curated site no broadcast + event_log handled=0 ------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_b_non_curated_site_dropped(mem_db):
|
|
|
|
|
env = _nwis_env(site_id="USGS-99999999", value=99.0)
|
|
|
|
|
data = {}
|
|
|
|
|
wire = handle_nwis(env, env["subject"], data=data, now=1_000_000)
|
|
|
|
|
assert wire is None
|
|
|
|
|
n_rows = mem_db.execute(
|
|
|
|
|
"SELECT COUNT(*) AS n FROM gauge_readings").fetchone()["n"]
|
|
|
|
|
assert n_rows == 0
|
|
|
|
|
n_log = mem_db.execute(
|
|
|
|
|
"SELECT COUNT(*) AS n FROM event_log WHERE source='nwis' AND handled=0"
|
|
|
|
|
).fetchone()["n"]
|
|
|
|
|
assert n_log == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- (c) curated site at normal stage no broadcast ----------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_c_curated_site_normal_stage_no_broadcast(mem_db):
|
|
|
|
|
# Heise normal is below 12.0ft.
|
|
|
|
|
env = _nwis_env(site_id="USGS-13186000", value=8.0)
|
|
|
|
|
data = {}
|
|
|
|
|
wire = handle_nwis(env, env["subject"], data=data, now=1_000_000)
|
|
|
|
|
assert wire is None
|
|
|
|
|
# The reading WAS persisted (time-series).
|
|
|
|
|
row = mem_db.execute(
|
|
|
|
|
"SELECT threshold_state FROM gauge_readings WHERE site_id=?",
|
|
|
|
|
("USGS-13186000",)).fetchone()
|
|
|
|
|
assert row["threshold_state"] == "normal"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- (d) upward threshold crossing (normal -> action) triggers ---------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_d_upward_crossing_normal_to_action_triggers(mem_db):
|
|
|
|
|
# First reading at normal.
|
|
|
|
|
env1 = _nwis_env(site_id="USGS-13186000", value=8.0,
|
|
|
|
|
time_iso="2026-06-05T10:00:00Z")
|
|
|
|
|
handle_nwis(env1, env1["subject"], data={}, now=1_000_000)
|
|
|
|
|
# Now rises to action.
|
|
|
|
|
env2 = _nwis_env(site_id="USGS-13186000", value=12.5,
|
|
|
|
|
time_iso="2026-06-05T10:15:00Z",
|
|
|
|
|
envelope_id="env_2")
|
|
|
|
|
data = {}
|
|
|
|
|
wire = handle_nwis(env2, env2["subject"], data=data, now=1_000_900)
|
|
|
|
|
assert wire is not None
|
|
|
|
|
assert "action stage 12.5 ft" in wire
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- (e) downward crossing (action -> normal) does NOT broadcast -------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_e_downward_crossing_does_not_broadcast(mem_db):
|
|
|
|
|
env_high = _nwis_env(site_id="USGS-13186000", value=12.5,
|
|
|
|
|
time_iso="2026-06-05T10:00:00Z")
|
|
|
|
|
handle_nwis(env_high, env_high["subject"], data={}, now=1_000_000)
|
|
|
|
|
env_low = _nwis_env(site_id="USGS-13186000", value=8.0,
|
|
|
|
|
time_iso="2026-06-05T11:00:00Z",
|
|
|
|
|
envelope_id="env_drop")
|
|
|
|
|
wire = handle_nwis(env_low, env_low["subject"], data={}, now=1_003_600)
|
|
|
|
|
assert wire is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_e_same_threshold_no_re_broadcast(mem_db):
|
|
|
|
|
"""Repeated readings at the same threshold (action -> action -> action)
|
|
|
|
|
must NOT re-broadcast every 15-min poll."""
|
|
|
|
|
env = _nwis_env(site_id="USGS-13186000", value=12.5,
|
|
|
|
|
time_iso="2026-06-05T10:00:00Z")
|
|
|
|
|
wire1 = handle_nwis(env, env["subject"], data={}, now=1_000_000)
|
|
|
|
|
assert wire1 is not None
|
|
|
|
|
|
|
|
|
|
env2 = _nwis_env(site_id="USGS-13186000", value=12.8,
|
|
|
|
|
time_iso="2026-06-05T10:15:00Z",
|
|
|
|
|
envelope_id="env_p2")
|
|
|
|
|
wire2 = handle_nwis(env2, env2["subject"], data={}, now=1_000_900)
|
|
|
|
|
assert wire2 is None # still in action band
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- (f) flow_cfs included for 00060, dropped for 00065-only -----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_f_flow_cfs_segment_from_companion_discharge(mem_db):
|
|
|
|
|
# First seed a stage reading at action.
|
|
|
|
|
env_stage = _nwis_env(site_id="USGS-13186000",
|
|
|
|
|
parameter_code="00065", value=12.5,
|
|
|
|
|
time_iso="2026-06-05T10:00:00Z")
|
|
|
|
|
wire1 = handle_nwis(env_stage, env_stage["subject"], data={}, now=1_000_000)
|
|
|
|
|
assert wire1 is not None
|
|
|
|
|
assert "flow" not in wire1 # no companion discharge yet
|
|
|
|
|
|
|
|
|
|
# Now a discharge reading arrives -- the handler should pick up the
|
|
|
|
|
# prior stage_ft for threshold context AND emit flow if upward crossing.
|
|
|
|
|
# In this case the stage didn't change, so no broadcast.
|
|
|
|
|
env_flow = _nwis_env(site_id="USGS-13186000",
|
|
|
|
|
parameter_code="00060", value=8400,
|
|
|
|
|
unit="ft^3/s",
|
|
|
|
|
time_iso="2026-06-05T10:01:00Z",
|
|
|
|
|
envelope_id="env_q")
|
|
|
|
|
wire2 = handle_nwis(env_flow, env_flow["subject"], data={}, now=1_000_060)
|
|
|
|
|
assert wire2 is None # same threshold; no re-broadcast
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- (g) site missing coords drops @ tail ------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_g_missing_coords_drops_at_tail(mem_db):
|
|
|
|
|
# Build a Heise envelope but blank out the latitude in inner.data so
|
|
|
|
|
# the handler must fall back to the curated coords (which DO exist).
|
|
|
|
|
env = _nwis_env(site_id="USGS-13186000", value=12.5)
|
|
|
|
|
env["data"]["data"]["latitude"] = None
|
|
|
|
|
env["data"]["data"]["longitude"] = None
|
|
|
|
|
wire = handle_nwis(env, env["subject"], data={}, now=1_000_000)
|
|
|
|
|
assert wire is not None
|
|
|
|
|
# Curated coords kick in -> @ segment still present.
|
|
|
|
|
assert "@ 43.612,-111.654" in wire
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- (h) IDAHO_CURATED_SITES has all 9 starter sites populated ---------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_h_curated_sites_count_and_required_fields():
|
|
|
|
|
assert len(IDAHO_CURATED_SITES) == 9
|
|
|
|
|
required_keys = {"gauge_name", "lat", "lon", "action_ft", "flood_minor_ft"}
|
|
|
|
|
for site_id, meta in IDAHO_CURATED_SITES.items():
|
|
|
|
|
assert site_id.startswith("USGS-"), site_id
|
|
|
|
|
missing = required_keys - set(meta.keys())
|
|
|
|
|
assert not missing, f"{site_id} missing {missing}"
|
|
|
|
|
assert isinstance(meta["action_ft"], (int, float))
|
|
|
|
|
assert isinstance(meta["flood_minor_ft"], (int, float))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_h_curated_sites_listed_starter_set():
|
|
|
|
|
"""Spot-check the 9 starter sites are exactly what spec listed."""
|
|
|
|
|
expected = {
|
|
|
|
|
"USGS-13139510", "USGS-13186000", "USGS-13037500",
|
|
|
|
|
"USGS-13135500", "USGS-13205000", "USGS-13247500",
|
|
|
|
|
"USGS-13057000", "USGS-13162225", "USGS-13083000",
|
|
|
|
|
}
|
|
|
|
|
assert set(IDAHO_CURATED_SITES.keys()) == expected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- commit callback flips event_log.handled = 1 -----------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_commit_callback_flips_event_log(mem_db):
|
|
|
|
|
env = _nwis_env(site_id="USGS-13186000", value=12.5)
|
|
|
|
|
data = {}
|
|
|
|
|
wire = handle_nwis(env, env["subject"], data=data, now=1_000_000)
|
|
|
|
|
assert wire is not None
|
|
|
|
|
pre = mem_db.execute(
|
|
|
|
|
"SELECT handled FROM event_log WHERE source='nwis' ORDER BY id DESC LIMIT 1"
|
|
|
|
|
).fetchone()
|
|
|
|
|
assert pre["handled"] == 0
|
|
|
|
|
_commit(data, 1_000_001)
|
|
|
|
|
post = mem_db.execute(
|
|
|
|
|
"SELECT handled FROM event_log WHERE source='nwis' ORDER BY id DESC LIMIT 1"
|
|
|
|
|
).fetchone()
|
|
|
|
|
assert post["handled"] == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- threshold escalation triggers a new broadcast --------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_action_to_flood_minor_triggers_re_broadcast(mem_db):
|
|
|
|
|
"""Reading rises action -> flood_minor: this is an upward crossing,
|
|
|
|
|
re-broadcast with the higher threshold label."""
|
|
|
|
|
env1 = _nwis_env(site_id="USGS-13186000", value=12.5,
|
|
|
|
|
time_iso="2026-06-05T10:00:00Z")
|
|
|
|
|
wire1 = handle_nwis(env1, env1["subject"], data={}, now=1_000_000)
|
|
|
|
|
assert wire1 is not None
|
|
|
|
|
assert "action stage" in wire1
|
|
|
|
|
|
|
|
|
|
env2 = _nwis_env(site_id="USGS-13186000", value=14.5,
|
|
|
|
|
time_iso="2026-06-05T11:00:00Z",
|
|
|
|
|
envelope_id="env_fm")
|
|
|
|
|
wire2 = handle_nwis(env2, env2["subject"], data={}, now=1_003_600)
|
|
|
|
|
assert wire2 is not None
|
|
|
|
|
assert "minor flooding" in wire2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- precipitation events skipped (parameter_code=00045) --------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_precip_parameter_skipped(mem_db):
|
|
|
|
|
env = _nwis_env(site_id="USGS-13186000", parameter_code="00045",
|
|
|
|
|
value=0.5, unit="in")
|
|
|
|
|
wire = handle_nwis(env, env["subject"], data={}, now=1_000_000)
|
|
|
|
|
assert wire is None
|
|
|
|
|
# No gauge_readings row written for precip.
|
|
|
|
|
n_rows = mem_db.execute(
|
|
|
|
|
"SELECT COUNT(*) AS n FROM gauge_readings").fetchone()["n"]
|
|
|
|
|
assert n_rows == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- site_id normalization -----------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_site_id_normalization_accepts_bare_id(mem_db):
|
|
|
|
|
"""'13186000' without USGS- prefix should still resolve to Heise."""
|
|
|
|
|
env = _nwis_env(site_id="13186000", value=12.5)
|
|
|
|
|
env["data"]["data"]["monitoring_location_id"] = "13186000"
|
|
|
|
|
wire = handle_nwis(env, env["subject"], data={}, now=1_000_000)
|
|
|
|
|
assert wire is not None
|
|
|
|
|
assert "Snake River at Heise" in wire
|