Skip to content

About

Python script for daily rainfall forecast emails at a specific location

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

rainfall_alerts

A daily email with the rainfall forecast for one or more fieldwork sites, to help plan sampling of storm runoff. Each site gets one email a day:

  • Subject: a one-line summary, e.g. Roding Rainfall Updates: ⛈️ HIGH - 24 mm on Sat 15 Nov (38 mm over 7 days). If the longer-range model forecasts something wetter beyond day 7, a note is added, e.g. (ECMWF: 🌧️ 14 mm Thu 20 Nov).

  • Figure with two panels:

    • a: 3-hourly totals, with lines at the Met Office moderate and heavy rain rates.
    • b: daily totals, with the low/medium/high bands shaded.

    Both y-axes are fixed (0–15 mm per 3 h, 0–30 mm per day) so drizzle never fills the panel. The axes only grow if the forecast exceeds them.

  • Body: a daily table, the scale explained in one sentence, and a robot 🤖 greeting and sign-off.

The first site is the Roding at Charlie Brown's Roundabout, set up for the TfL road-runoff fieldwork. To add more sites, edit config.yaml.

Where the forecast comes from

All data come from the free Open-Meteo forecast API. Each model is named explicitly, never best_match:

Days Model Notes
past 3 UK Met Office (ukmo_seamless) modelled, not gauge observations; shown shaded
1–7 UK Met Office (ukmo_seamless) UKV 2 km for ~2 days, then Global 10 km. Hourly at first, 3-hourly after 54 h, 6-hourly after 144 h
8–14 ECMWF IFS 0.25° (ecmwf_ifs025) fills the period after UKMO ends; lower confidence

The subject line and headline numbers use only the UKMO days. The figure uses 3-hour bins because that is the finest resolution that holds across the whole UKMO window. UKMO open data reaches Open-Meteo about 4 h after each run.

Open-Meteo's free tier is for non-commercial use and the data are CC BY 4.0. If you have a commercial key, set OPENMETEO_APIKEY and requests go to the paid host automatically.

What the categories mean

The category comes from the wettest single day (local calendar day):

Category Max day total
none < 1 mm ☀️
low 1–10 mm 🌦️
medium 10–20 mm 🌧️
high ≥ 20 mm ⛈️

These bands are the ETCCDI climate-extremes indices: a wet day is ≥1 mm, a heavy precipitation day ≥10 mm, and a very heavy one ≥20 mm. This is a method choice. You can change the bands for all sites, or for one site, in config.yaml.

Install

conda env create -f environment.yml
conda activate rainfall_alerts
pip install -e . --no-deps   # makes `rainfall_alerts` importable by the script
pre-commit install           # anyone cloning the repo needs to run this once too
cp recipients.example.yaml recipients.yaml   # then fill in the real addresses

Two files are git-ignored on purpose and must be copied by hand to any new machine (or recreated from their .example templates):

  • .env: the SMTP login and key (see "Email setup")
  • recipients.yaml: who gets each site's email (see "Configure sites")

Email setup

The script sends mail through Brevo, a mail service built for sending from scripts. The free plan allows about 300 emails a day. It uses only Python's standard library (smtplib): the script logs in to smtp-relay.brevo.com:465 over SSL and hands over one message per site, and Brevo delivers it. Recipients are in Bcc, so they don't see each other's addresses. The figure is embedded inline.

The emails come from smtp.sender_email in config.yaml (alertsrain@gmail.com), and replies go there. That Gmail inbox is not used to send anything; it is only the verified sender identity.

  1. Sign up at brevo.com, using the sender address, and choose the Free plan. New accounts can go through a short activation review before they're allowed to send.
  2. Verify the sender: Settings → Senders, Domains & Dedicated IPs → Senders → Add a sender. Enter the address in smtp.sender_email, then click the link or enter the code Brevo sends to that inbox.
  3. Get the SMTP login: Settings → SMTP & API → SMTP. Note the Login, a generated address like 8a1b2c001@smtp-brevo.com (not your email), and click Generate a new SMTP key. The key is shown once.
  4. In the repo's top folder, cp .env.example .env, then fill in:
    SMTP_USER=8a1b2c001@smtp-brevo.com
    SMTP_PASSWORD=<the SMTP key>
    
    .env is a local, git-ignored file. Never commit it. Variables already set in the environment take precedence over .env.
  5. Send a test with python scripts/send_rainfall_update.py --force --site Roding. Transactional → Logs in Brevo shows whether each email was delivered, bounced or blocked.
  6. A @gmail.com sender relayed by Brevo fails the receiving server's check that Google's own servers sent it. Early emails may therefore land in junk or show "via brevo"; mark them "not junk". The long-term fix is to send from an address on a domain you own and verify that domain in Brevo.

Any other relay that accepts a username and password works too: change smtp.host (and port, if it doesn't offer SSL on 465) and the two .env values.

Configure sites

Sites are set up in two files:

  • config.yaml (committed): shared defaults and a list of sites. Each site needs a name, lat and lon, and any default can be overridden inside a site block.
  • recipients.yaml (git-ignored): each site's distribution list, i.e. who gets its email. Addresses are kept out of git so personal data never enters the repo history.
# config.yaml
sites:
  - name: Roding
    description: TfL road-runoff outfall, Charlie Brown's Roundabout (A406/M11)
    lat: 51.5980
    lon: 0.0390
  - name: Another site
    lat: 51.75
    lon: -1.26
    thresholds_mm_per_day: {high: 15}   # override one value for this site only
# recipients.yaml
Roding:
  - you@ucl.ac.uk
  - collaborator@example.ac.uk
Another site:
  - someone@example.org

Both files are checked on load. The run stops with a message saying what to fix if:

  • a site has no recipients
  • recipients.yaml names a site that doesn't exist (e.g. a typo)
  • addresses appear in config.yaml instead of recipients.yaml
  • an address is malformed
  • a coordinate is out of range
  • a site name is duplicated

To use different files, pass --recipients other.yaml.

Run

python scripts/send_rainfall_update.py              # every site, at most once per day
python scripts/send_rainfall_update.py --dry-run    # write files only, send nothing
python scripts/send_rainfall_update.py --force      # send again even if sent today
python scripts/send_rainfall_update.py --site Roding --config other.yaml

Each run writes, per site:

  • outputs/<site>/<date>_hourly.csv: the stitched hourly forecast with its source model
  • outputs/<site>/<date>_email.html: a browser preview of the email
  • plots/<site>/<date>_forecast.png: the figure

inputs/, outputs/, plots/ and logs/ are git-ignored and can be regenerated at any time.

If the forecast can't be fetched, the script sends a short ⚠️ forecast unavailable email (at most once a day). A missing forecast is then never mistaken for "no rain", and later runs that morning keep retrying.

Schedule (cron)

Run hourly through the morning. A stamp file (outputs/<site>/last_sent.txt) limits each site to one email a day. If the machine was asleep at 06:30, the email goes out at the next hour it's awake, until noon:

30 6-12 * * * cd /home/alex/Documents/research/projects/rainfall_alerts && /home/alex/anaconda3/envs/rainfall_alerts/bin/python scripts/send_rainfall_update.py >> logs/rainfall.log 2>&1

Create logs/ once with mkdir logs, then add the line with crontab -e.

Tests

pytest

The tests use canned Open-Meteo responses and a fake SMTP server, so they need no network and send no email.

Layout

config.yaml                     sites, models, thresholds, plot limits, messages
recipients.example.yaml         template for recipients.yaml (git-ignored)
.env.example                    template for .env (git-ignored)
scripts/send_rainfall_update.py single entry point (loops over sites)
src/rainfall_alerts/
  config.py        load + validate config and recipients, read .env
  forecast.py      Open-Meteo requests, parsing, stitching models (data loading)
  summary.py       binned totals, categories, subject line (computation)
  plot.py          the two-panel figure (plotting)
  email_report.py  email bodies, MIME message, SMTP send (output)
tests/

About

Python script for daily rainfall forecast emails at a specific location

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages