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.
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.
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.
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")
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.
- 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.
- 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. - 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. - In the repo's top folder,
cp .env.example .env, then fill in:SMTP_USER=8a1b2c001@smtp-brevo.com SMTP_PASSWORD=<the SMTP key>.envis a local, git-ignored file. Never commit it. Variables already set in the environment take precedence over.env. - 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. - A
@gmail.comsender 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.
Sites are set up in two files:
config.yaml(committed): shareddefaultsand a list ofsites. Each site needs aname,latandlon, 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.orgBoth files are checked on load. The run stops with a message saying what to fix if:
- a site has no recipients
recipients.yamlnames a site that doesn't exist (e.g. a typo)- addresses appear in
config.yamlinstead ofrecipients.yaml - an address is malformed
- a coordinate is out of range
- a site name is duplicated
To use different files, pass --recipients other.yaml.
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 modeloutputs/<site>/<date>_email.html: a browser preview of the emailplots/<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
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.
pytest
The tests use canned Open-Meteo responses and a fake SMTP server, so they need no network and send no email.
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/