Mida-Sync is a backend service designed to monitor, aggregate, and notify meteorological alerts and reports. The system analyzes alerts issued by the Emilia-Romagna Region, integrating them with convective forecasts at national and European levels, to send timely and detailed notifications via Telegram.
The system interfaces with various official and unofficial sources to provide a complete meteorological overview:
-
Allerta Meteo Regione Emilia-Romagna
- Feature: Daily monitoring of the meteorological alert status.
- Details: Extracts data via the open APIs of the civil protection, focusing particularly on the "D1" alert zone. It detects critical alerts (yellow, orange, red) for hydraulic risk, hydrogeological risk, thunderstorms, wind, extreme temperatures, and snow/ice.
- Notification: Sends a Telegram message formatted with the alert colors and a direct link to the official PDF bulletin.
-
Estofex (European Storm Forecast Experiment)
- Feature: Integration of European severe storm bulletins.
- Details: Analyzes the Estofex XML feed to check for level 1, 2, or 3 forecasts for the following day.
- Notification: In case of critical alerts in the local territory, it sends the European convective forecast map to the Telegram channel.
-
Pretemp (Previsione Temporali)
- Feature: Integration of Italian thunderstorm forecast bulletins.
- Details: Dynamically retrieves forecast maps for the following day via the Pretemp archive.
- Notification: If conditions require it (presence of ongoing alerts), it sends the thunderstorm forecast image to the Telegram channel.
-
River levels (Allerta Meteo hydrometric stations)
- Feature: Monitoring of water levels at configured hydrometric stations.
- 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 theriverstable; each new reading is appended toriver_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. - Notification: Sends a Telegram message only on crossing events — when a reading rises above or falls below a threshold relative to the previous check.
-
Flood prediction (empirical precursor model)
- 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.
- Details: For each configured
river_link(an upstream gauge → a downstream point), a daily calibration mines the accumulatedriver_levelshistory for past downstream exceedances and learns, from those events only, the upstreamprecursor_leveland the typicallead_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 inlink_predictionsfor later scoring. No AI — event detection plus robust statistics. - 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.
The application relies on scheduled tasks (crons) to automate the weather monitoring flow:
- Meteo Alerts Check
- Periodically checks the status of the Emilia-Romagna weather alerts for the following day. If it detects a critical alert that has not been notified yet, it sends it on Telegram and saves it in the database.
- Pretemp Report Check
- Verifies and sends the Pretemp map for the following day, provided there is an ongoing alert and the map hasn't been sent yet.
- Estofex Report Check
- Verifies and sends the Estofex map for the following day, following the same conditional logic based on ongoing alerts.
- River Levels Check
- Every 5 minutes, for each row in the
riverstable, fetches the latest hydrometric reading and appends it toriver_levels. Sends a Telegram message only when the reading crosses one of the configured thresholds since the previous check.
- Every 5 minutes, for each row in the
- Flood Prediction Check
- 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.
- Every 5 minutes, evaluates each calibrated
- Flood Calibration
- Daily, re-learns every link's model (lead time + precursor level) from the accumulated
river_levelshistory. Also runnable on demand after a backfill.
- Daily, re-learns every link's model (lead time + precursor level) from the accumulated
Schema is applied manually (no migration tooling). The required tables are in sql/rivers.sql:
psql "$DATABASE_URL" -f sql/rivers.sqlStations are managed via HTTP (port 3000):
GET /rivers— list registered stationsPOST /rivers— create; body{ station_id, river_name, station_name, soglia1?, soglia2?, soglia3? }PATCH /rivers/:id— update any ofriver_name,station_name,soglia1,soglia2,soglia3DELETE /rivers/:id— delete (cascades toriver_levels)POST /river-levels— trigger an on-demand check (returns{ checked, crossings, skipped, unchanged })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)
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).
Both ends of a link must be registered as rivers so their readings accumulate in river_levels.
GET /river-links— list links and their learned modelsPOST /river-links— create; body{ upstream_river_id, downstream_river_id, target_threshold? }(target_threshold1–3, default 1)DELETE /river-links/:id— delete a linkPOST /flood-calibration— re-learn all link models from history (returns{ calibrated, active, skipped })POST /flood-prediction— evaluate all links now (returns{ evaluated, predictions, skipped })POST /flood-backfill— body{ from: "YYYY-MM", to: "YYYY-MM" }; backfillsriver_levelsfrom the ARPAE-SIMC open archive (https://dati-simc.arpae.it/opendata/osservati/meteo/storico/) for every registered station. Returns202immediately and runs in the background (it streams ~20 MB/month). Run it once, thenPOST /flood-calibration.
Example bootstrap for the Molinella stations (Idice S. Antonio + Reno Gandazzolo and their upstream gauges):
# register stations, link them, then:
curl -X POST localhost:3000/flood-backfill -H 'content-type: application/json' -d '{"from":"2020-01","to":"2024-12"}'
curl -X POST localhost:3000/flood-calibration- Node.js (v22+)
- PostgreSQL
- Docker (optional, for deployment)
- A valid Telegram Bot token and a destination Chat ID.
The project uses a .env file for configuration. Copy .env.example to .env and fill in the values:
cp .env.example .envRequired keys include:
- PostgreSQL Database credentials (Host, User, Password, DB Name)
TELEGRAM_TOKEN: Telegram Bot token.CHAT_ID: ID of the Telegram chat or channel where alerts will be sent.ALERT_ZONE: alert zone code to monitor (defaultD1).
The .env file is git-ignored and is injected into the container at runtime via docker-compose.yml (env_file). It is not baked into the Docker image. If credentials leak, rotate the Telegram bot token via @BotFather and reset the PostgreSQL password.
# Install dependencies
npm install
# Start development server (on port 3000)
npm run start# TypeScript compilation
npm run dist
# Start via Docker Compose
npm run deployThe project uses mocha and chai for testing. Tests can be run with:
npm run test