Skip to content

Commit 516e41f

Browse files
Feature: river levels (#7)
1 parent c7e5ff6 commit 516e41f

38 files changed

Lines changed: 3389 additions & 1 deletion

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ DATABASE_PORT=
99
DATABASE_USERNAME=
1010
DATABASE_PASSWORD=
1111
ALERT_ZONE=D1
12+
# Hysteresis deadband in metres around river thresholds (anti-flapping). Default 0.05.
13+
RIVER_THRESHOLD_MARGIN=0.05

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@ dist/
1111
.eslintcache
1212

1313
# IDE
14-
.vscode/settings.json
14+
.vscode/settings.json
15+
16+
# Local agent / preview config
17+
.claude/

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ The system interfaces with various official and unofficial sources to provide a
2121
- **Details:** Dynamically retrieves forecast maps for the following day via the Pretemp archive.
2222
- **Notification:** If conditions require it (presence of ongoing alerts), it sends the thunderstorm forecast image to the Telegram channel.
2323

24+
4. **River levels (Allerta Meteo hydrometric stations)**
25+
- **Feature:** Monitoring of water levels at configured hydrometric stations.
26+
- **Details:** Periodically polls the Allerta Meteo time-series endpoint for each registered station and compares the latest reading against operator-defined thresholds (`soglia1`, `soglia2`, `soglia3`). Station metadata and thresholds live in the `rivers` table; each new reading is appended to `river_levels` (de-duplicated by measurement time). A hysteresis deadband (`RIVER_THRESHOLD_MARGIN`, default 0.05 m) avoids alert flapping when a level hovers around a threshold.
27+
- **Notification:** Sends a Telegram message only on crossing events — when a reading rises above or falls below a threshold relative to the previous check.
28+
29+
5. **Flood prediction (empirical precursor model)**
30+
- **Feature:** Warns that a downstream point of interest is likely to exceed a threshold, with an estimated lead time, based on what upstream gauges are doing now.
31+
- **Details:** For each configured `river_link` (an upstream gauge → a downstream point), a daily calibration mines the accumulated `river_levels` history for past downstream exceedances and learns, from those events only, the upstream `precursor_level` and the typical `lead_time`. A link stays inactive until it has enough historical events. Online, when the upstream gauge reaches the learned level while rising, a single prediction is emitted and recorded in `link_predictions` for later scoring. No AI — event detection plus robust statistics.
32+
- **Bootstrap:** Since the live API only retains ~2.4 days, historical data is backfilled from the free ARPAE-SIMC open archive (see below). Without a backfill the model simply stays dormant until enough live events accrue.
33+
2434
## Scheduled Tasks (Crons)
2535

2636
The application relies on scheduled tasks (crons) to automate the weather monitoring flow:
@@ -31,6 +41,52 @@ The application relies on scheduled tasks (crons) to automate the weather monito
3141
- Verifies and sends the Pretemp map for the following day, provided there is an ongoing alert and the map hasn't been sent yet.
3242
- **Estofex Report Check**
3343
- Verifies and sends the Estofex map for the following day, following the same conditional logic based on ongoing alerts.
44+
- **River Levels Check**
45+
- Every 5 minutes, for each row in the `rivers` table, fetches the latest hydrometric reading and appends it to `river_levels`. Sends a Telegram message only when the reading crosses one of the configured thresholds since the previous check.
46+
- **Flood Prediction Check**
47+
- Every 5 minutes, evaluates each calibrated `river_link`: if the upstream gauge has reached its learned precursor level while rising, emits a single de-duplicated flood-arrival prediction.
48+
- **Flood Calibration**
49+
- Daily, re-learns every link's model (lead time + precursor level) from the accumulated `river_levels` history. Also runnable on demand after a backfill.
50+
51+
## Database bootstrap
52+
53+
Schema is applied manually (no migration tooling). The required tables are in [sql/rivers.sql](sql/rivers.sql):
54+
55+
```bash
56+
psql "$DATABASE_URL" -f sql/rivers.sql
57+
```
58+
59+
## River stations CRUD
60+
61+
Stations are managed via HTTP (port 3000):
62+
63+
- `GET /rivers` — list registered stations
64+
- `POST /rivers` — create; body `{ station_id, river_name, station_name, soglia1?, soglia2?, soglia3? }`
65+
- `PATCH /rivers/:id` — update any of `river_name`, `station_name`, `soglia1`, `soglia2`, `soglia3`
66+
- `DELETE /rivers/:id` — delete (cascades to `river_levels`)
67+
- `POST /river-levels` — trigger an on-demand check (returns `{ checked, crossings, skipped, unchanged }`)
68+
- `GET /rivers/nearest?lat=&lon=&limit=` — discovery: nearest stations to a point, with coordinates and the official soglie the Allerta sensor list reports (suggestions only)
69+
70+
The `station_id` is the Allerta Meteo `idstazione`; threshold values are operator-defined (the `nearest` endpoint surfaces the portal's official soglie as suggestions, but they are not auto-applied).
71+
72+
## Flood prediction (links, calibration, backfill)
73+
74+
Both ends of a link must be registered as `rivers` so their readings accumulate in `river_levels`.
75+
76+
- `GET /river-links` — list links and their learned models
77+
- `POST /river-links` — create; body `{ upstream_river_id, downstream_river_id, target_threshold? }` (`target_threshold` 1–3, default 1)
78+
- `DELETE /river-links/:id` — delete a link
79+
- `POST /flood-calibration` — re-learn all link models from history (returns `{ calibrated, active, skipped }`)
80+
- `POST /flood-prediction` — evaluate all links now (returns `{ evaluated, predictions, skipped }`)
81+
- `POST /flood-backfill` — body `{ from: "YYYY-MM", to: "YYYY-MM" }`; backfills `river_levels` from the ARPAE-SIMC open archive (`https://dati-simc.arpae.it/opendata/osservati/meteo/storico/`) for every registered station. Returns `202` immediately and runs in the background (it streams ~20 MB/month). Run it once, then `POST /flood-calibration`.
82+
83+
Example bootstrap for the Molinella stations (Idice S. Antonio + Reno Gandazzolo and their upstream gauges):
84+
85+
```bash
86+
# register stations, link them, then:
87+
curl -X POST localhost:3000/flood-backfill -H 'content-type: application/json' -d '{"from":"2020-01","to":"2024-12"}'
88+
curl -X POST localhost:3000/flood-calibration
89+
```
3490

3591
## Configuration and Installation
3692

0 commit comments

Comments
 (0)