Plants can't tell you they are thirsty, but a soil moisture probe can. In this lesson the ESP32 measures how wet the soil is and the temperature and humidity of the air around the plant. Arduino Cloud shows the moisture as a percentage on a gauge and a chart, and a dashboard slider sets the level at which the plant needs water. When the soil gets drier than that, a status LED on the pot turns red, and the dashboard says "Water me!".
What you'll learn
- How a soil moisture probe measures water
- How to turn a raw reading into a percentage with two calibration points
- How a slider in the dashboard can change a setting on the ESP32
- How to combine several readings in one Thing
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- Soil moisture module with probe (45-in-1 Sensor Kit)
- DHT11 temperature and humidity sensor (ESP32 IoT Starter Kit)
- Red LED and 1 resistor, 220 Ω (ESP32 IoT Starter Kit)
- A potted plant
- A 2.4 GHz Wi-Fi network
- The printed parts from the 3D printing box below (plant stake), or the cardboard version described there
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Wiring

| From | To |
|---|---|
| Soil module AO | ESP32 GPIO 34 |
| Soil module VCC and GND | 3V3 and GND |
| Soil module DO | Not used |
| DHT11 S (data) | ESP32 GPIO 27 |
| DHT11 + and − | 3V3 and GND |
| ESP32 GPIO 25 | 220 Ω resistor, then the red LED's long leg |
| LED short leg | GND |
Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.
Set up Arduino Cloud
In Arduino Cloud, open Things, create a new Thing called Plant, link it to your ESP32 device and fill in its network settings. Then add these variables. Lesson 1 of this path shows each step with pictures in words.
| Variable | Type | Permission | Update |
|---|---|---|---|
| moisture | Integer number (%) | Read only | Every 30 s |
| temperature | Temperature sensor (°C) | Read only | Every 30 s |
| humidity | Floating point number | Read only | Every 30 s |
| dryLimit | Integer number | Read & Write | On change |
| status | Character string | Read only | On change |
Open Dashboards, create a dashboard and add a Gauge and a Chart (moisture), two Values (temperature, humidity), a Slider from 10 to 60 (dryLimit) and a Messenger or Value widget (status), each linked to its variable. The same dashboard appears in the free Arduino IoT Remote app on your phone.
Calibrate the probe
- Upload the sketch and open the Serial Monitor at 115200 baud. It prints the raw reading.
- Hold the probe in the air: note the number and put it into
DRY_RAW. - Put the probe in a glass of water up to the line: note the number and put it into
WET_RAW. Upload again.
The sketch
Arduino Cloud writes the sketch's first version for you, with a thingProperties.h tab that lists your variables and a Secret tab with your Wi-Fi details and device key. In the Thing, open the Sketch tab, click Open full editor, replace the main sketch with the code below and upload. Leave the other tabs as they are.
Add the DHT sensor library by Adafruit under Libraries in the Arduino Cloud editor.
#include "thingProperties.h"
#include <DHT.h>
const int SOIL_PIN = 34, DHT_PIN = 27, LED_PIN = 25;
const int DRY_RAW = 3300; // reading in the air
const int WET_RAW = 1300; // reading in water
DHT dht(DHT_PIN, DHT11);
unsigned long lastRead = 0;
int soilPercent() {
long sum = 0;
for (int i = 0; i < 20; i++) sum += analogRead(SOIL_PIN);
int raw = sum / 20;
Serial.printf("Raw soil reading: %d\n", raw);
return constrain(map(raw, DRY_RAW, WET_RAW, 0, 100), 0, 100);
}
void update() {
moisture = soilPercent();
float t = dht.readTemperature(), h = dht.readHumidity();
if (!isnan(t)) temperature = t;
if (!isnan(h)) humidity = h;
bool thirsty = moisture < dryLimit;
digitalWrite(LED_PIN, thirsty);
status = thirsty ? "Water me!" : "I'm fine, thank you";
}
void setup() {
Serial.begin(115200);
delay(1500);
pinMode(LED_PIN, OUTPUT);
dht.begin();
dryLimit = 30; // until the cloud sends the slider's value
initProperties();
ArduinoCloud.begin(ArduinoIoTPreferredConnection);
setDebugMessageLevel(2); // connection messages in the Serial Monitor
ArduinoCloud.printDebugInfo();
}
void loop() {
ArduinoCloud.update();
if (millis() - lastRead >= 10000) {
lastRead = millis();
update();
}
}
void onDryLimitChange() {
Serial.printf("Dry limit is now %d %%\n", (int)dryLimit);
update(); // react at once
}
For reference, this is the thingProperties.h that Arduino Cloud creates from the variables above. Yours has your own device ID, and may use an older but equally valid style such as addProperty(led, READWRITE, ON_CHANGE, onLedChange).
// Code generated by Arduino Cloud (yours has your own device ID)
#include <ArduinoIoTCloud.h>
#include <Arduino_ConnectionHandler.h>
const char DEVICE_LOGIN_NAME[] = "YOUR_DEVICE_ID";
const char SSID[] = SECRET_SSID; // from the Secret tab
const char PASS[] = SECRET_OPTIONAL_PASS;
const char DEVICE_KEY[] = SECRET_DEVICE_KEY;
void onDryLimitChange();
int moisture;
CloudTemperatureSensor temperature;
float humidity;
int dryLimit;
String status;
void initProperties() {
ArduinoCloud.setBoardId(DEVICE_LOGIN_NAME);
ArduinoCloud.setSecretDeviceKey(DEVICE_KEY);
ArduinoCloud.addProperty(moisture, Permission::Read).publishEvery(30);
ArduinoCloud.addProperty(temperature, Permission::Read).publishEvery(30);
ArduinoCloud.addProperty(humidity, Permission::Read).publishEvery(30);
ArduinoCloud.addProperty(dryLimit, Permission::ReadWrite).onUpdate(onDryLimitChange);
ArduinoCloud.addProperty(status, Permission::Read).publishOnChange(0);
}
WiFiConnectionHandler ArduinoIoTPreferredConnection(SSID, PASS);How it works
The soil probe is two metal prongs: the wetter the soil, the better it conducts. On the module, that becomes a voltage on AO. On most modules a lower number means wetter, which is why map() goes from DRY_RAW to WET_RAW: it turns the raw number into 0 % (as dry as air) to 100 % (as wet as water), whichever way your module counts.
The slider shows how a dashboard can change a setting on the device. When you move it, Arduino Cloud sends the new dryLimit and onDryLimitChange() checks the plant again straight away.
This Thing uses all five variables that the free plan allows. Choosing what to send is part of designing an IoT device.
Try this next
- Add a pump with the relay, and water automatically for three seconds when the soil is too dry.
- Show how many days it has been since the last watering.
- Monitor two plants with two probes.
Troubleshooting
- The device stays "Offline": open the Serial Monitor at 115200 baud. Check the Wi-Fi name and password in the Secret tab (2.4 GHz only) and that the secret device key is the one you saved when you added the device.
- Compile error about a variable: the variable names in the sketch must match the ones in your Thing exactly, including capital letters. Rename the Thing's variable or the sketch's to match.
-
The moisture is always 0 or 100 %: calibrate
DRY_RAWandWET_RAWagain with the Serial Monitor. - The probe corrodes: cheap probes wear out in wet soil. Pull it out when you aren't measuring, or only power it during a reading.
- A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.