
In this article, I’ll show you how to build a rain sensor that not only detects when it’s raining but also tells you how heavy the rainfall is. And the best part? It’s more accurate and reliable than any online service out there.
If you have a piece of land where you’re growing anything that needs watering, this information is invaluable. For example, I’ve got a lawn and a few fruit bushes in my backyard, and watering them on a daily basis is a must during summer.
That’s where my own, local, independent-of-the-internet rain gauge comes in. It tells me exactly how much rain has fallen right where I live.
Video version

Source files
The project is totally open-source, which means you can grab it and tweak it however you please. You can snag my design, the PCB layout, the 3D model, the bill of materials, and firmware – and make it exactly as is. Or, you can use it as a jumping-off point, make changes, and whip up something totally fresh.
If you want to receive all of them put the email below – I’ll send a link to you straight away.
Where can I buy it?
If you don’t feel like building it yourself or simply don’t have the time, I invite you to visit my online store. You will find assembled, and ready-to-use device, or just the circuit board itself.
Electronics

The sponsor of this project is JLCPCB. They made that beautiful board for this project. For a few bucks, and in a matter of days, you can have a professional PCB on your deck, ready to solder 🙂 And if you don’t feel up to it, they can solder all SMD components for you as well.

The schematic isn’t exactly mind-blowing, but that’s the beauty of it – it’s stripped down to only the essentials. While creating it, I followed the YAGNI (You Aren’t Gonna Need It) principle. It basically means that you should only add features when you really need them.
The ESP32 is the main computing unit, and it’s great for low power consumption in battery-powered devices.
I also added a circuit to monitor the battery level, so I can swap them out before they completely die.
But the real star of the show is the HALL sensor. It’s a magnetic sensor that detects rainfall. I’ll go into more detail once we get to the mechanical part of things.
PCB


I designed the PCB to be no bigger than the battery box. I wanted it to be as small as possible since it will be sealed in a container inside the housing. And I don’t want the Rain Gauge to be any bigger than it needs to be.
Soldering
When it comes to component assembly, it’s easy. Start with SMD components, and then move on to THT.
However, you need to solder them on the TOP side, not on the BOTTOM as you would normally do. The reason is that we’ll later put the battery basket on the bottom, and it needs to fit snugly against the PCB. That’s why you should cut the THT components as close to the board as possible after assembly.


The Rain Gauge (as the name implies) is gonna be functioning in a damp setting. So, aside from enclosing the PCB in a reasonably airtight case, you can coat it with a waterproof varnish. I used PVB16 and it works pretty well, but you can use whatever is available locally to you.
Mechanical Part
The enclosure’s outer appearance resembles a basic cylinder topped with a funnel. This funnel connects to one of the two small containers, let’s call them “buckets.” Once a sufficient amount of water has gathered, the bucket tips over, allowing the water to flow through the holes. Consequently, the other bucket begins to collect water.
And that’s how it will continue swinging back and forth repeatedly…



How does a rain gauge work?

Since a picture is worth a thousand words, a short animation consisting of a dozen or so images should fully explain how it works.
By knowing the diameter of the funnel’s upper part, the capacity of one bucket, and the time interval between each tipping, we can accurately determine the intensity of the current rainfall.
But how does ESP32 know when the overflow takes place?
There’s a magnet at the top part of the tipping bucket. Inside this tightly sealed container, we’ve got our PCB with a HALL sensor.
It detects the change in the magnetic field when the bucket swings, and notifies the EPS about it.
The microcontroller measures how much time passed since the last tick, converts the units to l/m2, and sends that info to the Home Assistant.
If there’s no tipping for an hour or so, we can safely assume that the rain has stopped.
Calibration
To make the results as precise as possible, both buckets must be identical. They need to react to exactly the same amount of water. Even though they are identical in Fusion 360, it may not be the case after printing.
To calibrate the sensor, use the two screws located beneath the tipping bucket.
Just pour a measured amount of water (e.g. 8 ml) and slowly turn the screw until the bucket tips. Do the same for the other side.
This will ensure that both buckets respond to the same weight.
You can also use these screws to adjust the sensitivity of the Rain Gauge. The less water you need to trigger it, the faster response you’ll get.
But keep in mind that the battery life will be shorter due to more frequent tipping.
Software
The software part applies to both Home Assistant and ESPHome, so I divided this paragraph into two stages. In the first one, we’ll handle the ESPHome configuration, and then I’ll show you what else needs to be done in HA.
ESPHome
The vast majority of handling is done on the ESPHome side. It’s where we physically receive the information that the tipping bucket has been triggered, calculate the time between events, and convert units to “mm/h”. It’s the same with battery measurement. The ESP32 measures and converts the ADC result into percentages, which, in my opinion, are more convenient.
substitutions:
#Deep Sleep
wake_time: '5s'
#Battery
max_battery_vlotage: '6'
min_battery_voltage: '3.6'
#Rain
one_tick: '0.841808' #[mm]
This first segment contains variables that will potentially be modified most frequently. They are placed at the top of the configuration file for easy access and convenience.
- wake_time: the duration for which the ESP chip will stay active after being awakened by the tipping bucket.
- max(min)_battery_vlotage: at this point, you determine what percentage represents a fully charged battery and what percentage represents 0%. For regular AAA batteries, the setting I’ve chosen should be fine, but if you’re using rechargeable batteries, you might want to consider adjusting it.
- one_tick: one “tick” corresponds exactly to the value of rainfall. And 1 mm equals 1 liter per square meter. I calculated this value taking into account that the top diameter of the funnel is 110 mm, and one bucket can hold 8 ml of water. If any of these parameters are different for you, you will need to recalculate accordingly.
globals:
- id: old_time
type: time_t
initial_value: "0"
restore_value: yes
- id: rg_flag
type: bool
initial_value: "false"
restore_value: no
In this section, I store global variables, which means they are accessible throughout the configuration file and can be retained after waking up from deep sleep mode.
esphome:
name: rain-gauge-v1-0
esp32:
board: esp32dev
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "XXX"
ota:
password: "XXX"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: True
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Rain-Gauge-V1-0 Fallback Hotspot"
password: "ahypg3nlyxHN"
captive_portal:
These are standard things that can be found in every freshly generated configuration file, so I don’t think I need to explain them.
time:
- platform: homeassistant
id: homeassistant_time
setup_priority: 900
on_time_sync:
then:
- lambda: |-
if(id(rg_flag) == false)
{
time_t current_time = id(homeassistant_time).now().timestamp;
time_t delta_time = current_time - id(old_time);
if(delta_time < 3600)
id(precipitation_id).publish_state($one_tick * (3600 / delta_time));
else
id(precipitation_id).publish_state(0.01);
id(old_time) = current_time;
id(rg_flag) = true;
}
This here is the most important piece of code. Right here, we calculate how much time has passed since the last “tick” of the bucket.
First thing, we set a high priority to make sure this component is high up on the priority list during the initialization process.
Then, once it’s fully up and running, it checks the current time (timestamp) and subtracts it from the previous one recorded during the last bucket trigger.
However, if this value is greater than one hour, we consider it to have just started raining. In such a case, we send a value of 0.01 mm. This is to let Home Assistant know that it’s raining, but we will be able to calculate the exact value with the next tick.
If the value of time is smaller than one hour, it converts seconds into “mm” of water column, taking into account the variable “one_tick”.
switch:
- platform: gpio
pin: GPIO27
name: "RG Battery Switch"
id: rg_battery_switch
restore_mode: ALWAYS_ON
- platform: gpio
pin: GPIO12
name: "RG LED"
id: rg_led
Here we’re just initializing two GPIOs as outputs.
“RG Battery Switch” disconnects the ADC line from the microcontroller during Deep Sleep mode to save battery power.
“RG LED” is connected to an LED. I use it to indicate that the Deep Sleep mode is blocked. (I’ll explain why we do this in a moment).
binary_sensor:
- platform: status
name: "RG Status"
- platform: homeassistant
id: rg_deep_sleep
name: "RG Deep Sleep"
entity_id: input_boolean.rg_deep_sleep
publish_initial_state: True
on_state:
then:
if:
condition:
lambda: return x;
then:
- deep_sleep.allow: init_deep_sleep
else:
- switch.turn_on: rg_led
- deep_sleep.prevent: init_deep_sleep
In this section, we set up the binary sensors.
The “Status” sensor allows Home Assistant to check if the Rain Gauge is online.
As for the “RG Deep Sleep” sensor, it requires further explanation. I’ll tell you about it in the subsection “How to use OTA?“
sensor:
- platform: adc
pin: GPIO32
name: "RG Battery Level"
id: rg_battery_level
device_class: "battery"
unit_of_measurement: "%"
accuracy_decimals: 0
setup_priority: -200
filters:
- lambda: |-
float current_voltage = x * 5.94;
if(current_voltage < $min_battery_voltage)
return 0;
else if(current_voltage > $max_battery_vlotage)
return 100;
else
return ((current_voltage - $min_battery_voltage) / ($max_battery_vlotage - $min_battery_voltage)) * 100;
- platform: template
name: "precipitation"
id: precipitation_id
unit_of_measurement: "mm/h"
update_interval: never
The analog sensor “RG Battery Level” is responsible for measuring the voltage of the batteries and converting it into a percentage.
The “precipitation” sensor, as the name suggests, stores data about current rainfall. It’s where the calculated value based on time and the amount of water in a single bucket is recorded.
deep_sleep:
run_duration: $wake_time
id: init_deep_sleep
esp32_ext1_wakeup:
pins: GPIO25
mode: ALL_LOW
Finally, we configure the Deep Sleep mode. We don’t set a specific time for the ESP to enter this mode because the only wake-up option is the tipping bucket.
Home Assistant (I’m working on it 🙂 )
In Home Assistant itself, besides reading the battery level data and current rainfall, you also need to set up a thing called “Helper” and one automation.