I killed a lot of houseplants before I admitted the problem was me, not them. I would either forget to water for two weeks or, overcorrecting, drown a pot that was still damp three inches down. The finger-in-the-soil test only checks the top layer, which is exactly the part that dries out first and tells you nothing about the root zone. So I did what I do with every recurring annoyance in this house: I wired a sensor to it. This is a fully local ESPHome soil moisture monitor that reports every plant’s actual root-zone moisture straight into Home Assistant, costs a few dollars per plant, and never touches the cloud. Here is exactly how I built it.
Why not just buy a plant sensor
There are plenty of bluetooth plant monitors on Amazon, and I owned two of them before this build. The problem is always the same: they route through a vendor app, the bluetooth range is measured in feet, the battery dies in a season, and getting the data into Home Assistant means fighting a flaky bluetooth proxy or scraping a cloud API that could vanish in a firmware update. For a device whose entire job is to sit in a pot and report one number, that is an absurd amount of dependency.
An ESPHome build inverts all of that. The sensor is wired, so there is no battery to die and no bluetooth to drop. It reports over WiFi directly to Home Assistant with no app in between. The firmware is yours, the calibration is yours, and if Nabu Casa disappeared tomorrow the whole thing would keep running on your LAN. Once you have built one ESPHome sensor node, adding plant monitoring to it is almost free.
The hardware
The parts list is short and cheap, and most of it you may already have in a parts bin if you have done any ESPHome work.
- An ESP32 dev board. I use the ESP32 over an ESP8266 here because it has several usable ADC pins and I2C, which matters when you want more than one or two plants per node. They come cheap in multi-packs.
- One capacitive soil moisture sensor v2.0 per plant. Insist on the capacitive v2.0 boards. The resistive ones with two exposed metal prongs corrode within weeks and are a waste of money for a permanent install.
- Optionally, an ADS1115 16-bit ADC module if you want to monitor more than three or four plants from one ESP32. It reads four analog channels over I2C with far less noise than the ESP32’s built-in ADC.
- A can of conformal coating spray to protect the top of each sensor board from humidity.
- A JST connector assortment kit if you want tidy, removable plugs so you can lift a plant out for repotting without desoldering anything.
The capacitive sensor is a three-pin device: VCC, ground, and an analog output that swings roughly between 1.2 volts (wet) and 3.0 volts (dry). You power it from the ESP32’s 3.3 volt rail. Powering it at 5 volts pushes the analog output above the ESP32’s 3.3 volt ADC ceiling and can damage the pin, so keep it on 3.3 volts and everything stays in range.
Wiring it up
For a small setup, wire each sensor’s output straight to an ADC-capable GPIO. On the ESP32 the ADC1 pins (GPIO32 through GPIO39) are the ones to use, because ADC2 pins conflict with WiFi and will give you garbage readings the moment the radio is active. That single detail trips up almost everyone the first time. Stick to ADC1 pins and your readings stay clean.
For my six-plant setup I wired three sensors to ADC1 GPIOs directly and the other three to an ADS1115. The ADS1115 sits on the I2C bus (SDA and SCL, plus power and ground) and gives me four extra channels with 16-bit resolution, which is dramatically steadier than the ESP32’s jittery internal ADC. If you only have two or three plants, skip the ADS1115 entirely and wire straight to the GPIOs.
The ESPHome configuration
Here is the core of the config for a single plant on an ADC1 pin. Put your real WiFi and API values in secrets.yaml and reference them with !secret so nothing sensitive lands in the YAML itself.
esphome:
name: plant-monitor
esp32:
board: esp32dev
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
api:
encryption:
key: !secret api_key
ota:
platform: esphome
sensor:
- platform: adc
pin: GPIO32
name: "Fiddle Leaf Fig Moisture"
update_interval: 60s
attenuation: 12db
unit_of_measurement: "%"
filters:
- median:
window_size: 7
- calibrate_linear:
- 3.0 -> 0.0
- 1.2 -> 100.0
- lambda: |-
if (x < 0) return 0;
if (x > 100) return 100;
return x;
A few things are doing real work here. The median filter with a seven-sample window throws out the electrical spikes the bare ADC loves to produce, so your reading does not jump around by ten points between updates. The calibrate_linear block is where your two-point calibration lives: 3.0 volts maps to 0 percent (dry) and 1.2 volts maps to 100 percent (saturated). The closing lambda just clamps the output to a clean 0 to 100 range so a slightly out-of-range voltage does not print a moisture of 104 percent.
For the ADS1115 channels the pattern is nearly identical, except you declare the ads1115 hub once and then use the ads1115 sensor platform with a multiplexer line instead of a raw pin. Everything downstream, the filters and calibration, stays the same.
Calibrating each probe
Calibration is the step people skip, and it is the step that decides whether your numbers mean anything. Every sensor and every soil mix reads slightly differently, so a shared calibration will lie to you. It takes two minutes per probe.
First, get the dry reading. Hold the probe in open air or push it into completely dry soil and note the voltage ESPHome reports before you apply the calibrate_linear filter. Comment the filter out temporarily so you see raw volts. That number, usually right around 3.0 volts, is your 0 percent anchor.
Second, get the wet reading. Stand the probe in a glass of water up to (but not past) the marked line, or push it into freshly saturated soil, and note the voltage. That is typically near 1.2 volts and becomes your 100 percent anchor.
Plug those two exact numbers into the calibrate_linear block, re-enable the filter, and reflash. Now the percentage tracks real moisture in your actual pots. Do this once per probe and you never think about it again.
Sealing the boards so they survive
The single most common failure with these cheap capacitive sensors is corrosion at the top of the board, where the header pins and the little voltage regulator sit. The capacitive sensing area is fine buried in soil, but the electronics up top hate constant humidity.
The fix is thirty seconds of work. Mask off the sensing zone below the printed waterline, then hit the top third of the board, the pins, and the solder joints with a couple of light coats of conformal coating spray. If you do not have coating, a generous blob of hot glue over the header and regulator works nearly as well. Leave the sensing area completely bare. Coated up top and bare down low, these boards last for years instead of months.
The Home Assistant automation
Raw moisture numbers are nice on a dashboard, but the payoff is automation. In Home Assistant I keep it simple: each plant gets a threshold, and when moisture drops below it for a sustained period, I get a notification naming the exact plant.
automation:
- alias: "Plant needs water"
trigger:
- platform: numeric_state
entity_id: sensor.fiddle_leaf_fig_moisture
below: 25
for:
hours: 2
action:
- service: notify.mobile_app_phone
data:
title: "Thirsty plant"
message: "The fiddle leaf fig is at low moisture. Time to water."
The for: hours: 2 is important. Without it, a single noisy dip below the threshold would fire a false alert the moment you brushed the pot. Requiring the reading to stay low for two hours means you only get pinged when the soil is genuinely dry, not when a stray sample twitched. Set the below value per plant based on what each species actually likes: I water the fiddle leaf fig around 25 percent but let a snake plant coast down to 10 percent before it cares.
The thresholds are the fun part to tune. After a couple of weeks of watching the graphs in Home Assistant, you learn each plant’s real drydown curve, and you can set thresholds that match how the plant behaves rather than a generic number. A pothos that drinks fast gets a higher trigger; a succulent that sulks in wet soil gets a low one.
What this changed in practice
I have not lost a plant since I built this, which after years of steady casualties feels close to miraculous. More than that, it changed how I think about watering. I stopped watering on a schedule and started watering on data. Some plants that I used to hit every Sunday turned out to need water twice a week in summer and barely once every ten days in winter. The graphs made the seasonal shift obvious in a way my calendar never did.
The dashboard is genuinely useful too. A single Home Assistant view with a gauge card per plant, color coded green to red, tells me at a glance which pots are fine and which are creeping toward the threshold. It takes the guesswork out entirely, and because it is all local, it loads instantly and works even when the internet is down.
The Bottom Line
For the price of a few capacitive sensors and an ESP32 you probably already own, you can turn plant care from a guessing game into a solved problem, with every reading living on your own hardware and nothing depending on a vendor app. Buy the capacitive v2.0 boards, never the resistive ones. Power them at 3.3 volts and wire only to ADC1 pins. Calibrate each probe with a two-point dry-and-wet reading so the percentages mean something. Seal the tops with conformal coating so they survive the humidity. And add an ADS1115 the moment you outgrow three or four plants.
Do those five things and you get a monitor that outlasts and out-integrates anything you can buy, for a fraction of the cost. My plants are healthier, my watering is finally rational, and I have not scraped a dead fern out of a pot in over a year. That is a good return on a weekend and twenty dollars of parts.