Companion code for the blog post: Storm cell and hail alerts in Home Assistant from RainViewer radar https://harizanov.com/?p=8869
Three layers of lead time, plus the same thing for a car tracked via TeslaMate.
| File | What it is |
|---|---|
rainviewer_dbz.py |
Fetches RainViewer PNG radar tiles around a lat/lon, decodes pixels back to dBZ, clusters >=35 dBZ echoes into cells, tracks them across the last 3 frames and reports if one is on approach course (with ETA). Prints JSON. |
rainviewer_ub_palette.json |
RGB -> palette-index lookup for RainViewer's "Universal Blue" scheme (dBZ = index - 32). The tile server ignores the color parameter and always serves this palette. |
storm_alerts.yaml |
HA package: MeteoAlarm + Blitzortung + the command_line sensor running the script every 5 min, folded into ONE storm-level state machine (see below): escalation pushes, protective actions (pause EV charging, stop irrigation) and all-clear. |
storm_replay.py |
Replays the state machine over Home Assistant history (/api/history/period JSON) so you can see which pushes a threshold change WOULD have sent during your last storm, before you reload anything. |
tesla_storm_watch.yaml |
HA package: MQTT device tracker from TeslaMate, radar scan at the car's position every 10 min while away, lightning-near-the-car alert. |
- Copy
rainviewer_dbz.pyandrainviewer_ub_palette.jsonto/config/bin/. Needs Pillow – it is already present in the HA core container. - Test:
python3 /config/bin/rainviewer_dbz.py <LAT> <LON>should print JSON withdbz_home,dbz_max_10km,dbz_max_25km,approaching,approach_eta_min, ... - Install the Blitzortung integration (HACS) over
zone.home; its sensors are namedsensor.home_lightning_distance/azimuth/counterhere – rename to match yours. - Drop the two YAML files into
/config/packages/(withhomeassistant: packages: !include_dir_named packagesinconfiguration.yaml), replace<LAT> <LON>, themobile_app_*notify targets, the MeteoAlarmprovince, and the irrigation/wallbox entities (or delete the protective-actions automation). tesla_storm_watch.yamlassumes TeslaMate publishing to MQTT underteslamate/cars/1/andsensor.tesla_latitude/longitudefrom the TeslaMate MQTT integration.
The first version had six independent automations, each with its own cooldown. The first real storm showed why that is wrong: 12 pushes in one evening (three of them overnight on a 30 dBZ drizzle band), while the single alert that mattered – a 57 dBZ cell 7 km out, ETA 10 minutes – was silenced by a cooldown from an earlier, weaker alert. A cooldown cannot tell "same storm again" from "same storm, now dangerous".
v2 is a state machine:
| level | means | criteria (any) | push |
|---|---|---|---|
| 0 clear | – | – | – |
| 1 watch | official warning / distant cell | MeteoAlarm thunder/hail/orange+ · tracked cell ≥45 dBZ, ETA ≤120 min, ≤60 km | active (passive 23–07) |
| 2 warning | it is coming | lightning <25 km (≥3 strikes) · ≥45 dBZ inside 25 km · cell ≥45 dBZ, ETA ≤60 min, ≤40 km | time-sensitive (passive 23–07) |
| 3 imminent | tracker says minutes away | cell ≥50 dBZ, ETA ≤20 min | time-sensitive + protective actions |
| 4 severe | measured overhead | lightning <10 km (≥3 strikes) · ≥55 dBZ inside 25 km | critical (bypasses DND) + protective actions |
sensor.storm_levelis a pure template function of the inputs – no memory.input_number.storm_alerted_levelremembers the highest level already announced. A push goes out only when the level rises above it. No cooldowns anywhere, so escalation always gets through and repeats never do.- Downgrades are silent. 45 minutes with no radar/lightning threat (level ≤1) ends the episode: one "storm passed" push, EV charging resumed if it was paused. A MeteoAlarm warning that is still active does not hold the episode open.
- Level 3 vs 4 is the difference between extrapolated and measured. The cell tracker works on coarser zoom-6 tiles and occasionally over-calls (it announced a "50 dBZ, ETA 5 min" cell at 01:11 that never showed up in the 25 km ring) – extrapolation alone should not wake the house.
- The car package only alerts when the car is more than 25 km from home; nearer than that the home alerts already cover it.
Replayed over the storm that motivated it, v2 sends 7 pushes instead of 12 and both the 7 km cell and the measured hail core get through. To tune thresholds, change them in the storm_level template AND in storm_replay.py (they are mirrored by hand), then:
curl -H "Authorization: Bearer $TOKEN" \
"http://homeassistant:8123/api/history/period/2026-08-29T00:00:00Z?end_time=2026-08-30T08:00:00Z&filter_entity_id=sensor.radar_dbz_home,sensor.home_lightning_distance,sensor.home_lightning_counter,binary_sensor.meteoalarm_sofia" > history.json
python3 storm_replay.py history.json
The first published rainviewer_dbz.py located home on the tile with standard XYZ-tile fraction math. RainViewer's /{size}/{z}/{lat}/{lon}/... endpoint does not return the XYZ tile that contains the coordinate – it returns a tile centred on it, so home is always at pixel (128, 128). The old maths put "home" ~80 km north / 25 km west of the real one at zoom 7 and ~140 km east at zoom 6: the 25 km ring was measuring the wrong province and the cell tracker reported cells on the wrong side of the house. It surfaced when a 47 dBZ cell sat 30 km WNW of home in plain view on the radar card while the sensor read 0 dBZ and "nothing tracked". Fixed here (px, py = 128, 128). If you copied the earlier version, every dBZ value it produced was for a point far from your coordinates – including the storm replay above, which will need re-running on data collected after the fix.
No warranty – it is a hobby project, check the radar yourself when it matters.