If you have trouble keeping your plants alive or are an enthusiast who loves tracking and analyzing soil moisture over weeks and months, this article is just right for you.
I developed a sophisticated script that automatically calibrates conventional analog soil moisture sensors, outputs values in percent, and transmits the data via MQTT to Home Assistant and other systems.
What You Need
Hardware
- A microcontroller such as ESP32 or Arduino
Since the code does not use special libraries and — except for powering the sensor — requires only one pin, it is compatible with most devices.
However, if you want a battery-operated long-term monitoring solution for your plants, I recommend the FireBeetle 2 ESP32-E. With this model, I achieved battery runtimes of several months. - A soil moisture sensor (Soil Moisture Sensor)
Software
- Arduino IDE
- Home Assistant + MQTT Broker
Note: Amazon Affiliate
Wiring
Connect the positive cable (+) to a 3V or 5V pin.
If you use an ESP32 like I do, GPIO35 is a good choice.
If you use an ESP32 in D1 format or another microcontroller such as Arduino, check the specifications of your device for a suitable analog pin, as this is needed to receive data from the sensor.
Make sure to adjust the GPIO pin accordingly in the code.
Getting Started
At the top of the code, you need to set up your WiFi and MQTT configurations.
If you do not have an MQTT user in Home Assistant yet, simply create a new admin user and enter its credentials in the code.
Note that the permissions of Home Assistant users also apply to MQTT.
Energy Efficiency is Crucial
WiFi, Bluetooth, and sensor queries consume a lot of energy. These factors are decisive for long battery life.
In the code, you can adjust the variable sleepTimeSeconds to control how often moisture data is sent to Home Assistant.
- For debugging: short intervals
- For continuous operation: once per hour or less frequently
With this setting and a 3000 mAh battery, you can achieve a runtime of about six months.
Automatic Calibration
Most soil moisture sensors require manual calibration.
This is tedious, especially with multiple sensors, since values differ depending on cable length, pin, and solder joints.
Therefore, I implemented an automatic calibration:
- Keep the sensor completely dry while flashing
- After upload, wait 2–3 seconds
- Within 20 seconds, dip the sensor in water
The ESP32 automatically learns minimum and maximum values and then outputs moisture in percent.
These values are permanently stored in NVS and remain even after power loss.
The calibration time can be adjusted via the variable calibrationPeriod.
The serial console displays the determined values.
Recalibration
If you want to recalibrate, you can completely erase the ESP32's non-volatile storage with the following script.
#include <Preferences.h>
#include <Arduino.h>
Preferences preferences;
void setup() {
Serial.begin(115200);
preferences.begin("moisture", false);
bool isCleared = preferences.getBool("isCleared", false);
if (!isCleared) {
Serial.println("Clearing calibration data...");
preferences.clear();
preferences.putBool("isCleared", true);
Serial.println("Calibration data reset.");
} else {
Serial.println("Calibration data has already been cleared.");
}
preferences.end();
delay(2000);
Serial.println("System will restart.");
ESP.restart();
}
void loop() {
}
Code for Soil Moisture Monitoring
#include <WiFi.h>
#include <PubSubClient.h>
#include <Preferences.h>
#include <Arduino.h>
const int moistureSensorPin = 35;
Preferences preferences;
int airValue;
int waterValue;
unsigned long startTime;
const unsigned long calibrationPeriod = 20000;
const unsigned long sleepTimeSeconds = 20;
const char* ssid = "[SSID]";
const char* password = "[PASSWORD]";
const char* mqttServer = "[MQTT_SERVER_IP]";
const char* mqttUser = "[MQTT_USERNAME]";
const char* mqttPassword = "[MQTT_PASSWORD]";
const char* mqttTopic = "plants/plant01";
WiFiClient espClient;
PubSubClient client(espClient);
void setup() {
Serial.begin(115200);
preferences.begin("moisture", false);
airValue = preferences.getInt("airValue", 4095);
waterValue = preferences.getInt("waterValue", 0);
bool isCalibrated = preferences.getBool("isCalibrated", false);
if (!isCalibrated) {
Serial.println("Starting calibration...");
startTime = millis();
}
setup_wifi();
client.setServer(mqttServer, 1883);
}
void loop() {
bool isCalibrated = preferences.getBool("isCalibrated", false);
if (!client.connected()) {
reconnect();
}
client.loop();
if (!isCalibrated && millis() - startTime < calibrationPeriod) {
int sensorValue = analogRead(moistureSensorPin);
waterValue = max(waterValue, sensorValue);
airValue = min(airValue, sensorValue);
} else if (!isCalibrated) {
preferences.putInt("airValue", airValue);
preferences.putInt("waterValue", waterValue);
preferences.putBool("isCalibrated", true);
}
if (isCalibrated) {
int sensorValue = analogRead(moistureSensorPin);
int moisturePercent = map(sensorValue, airValue, waterValue, 100, 0);
moisturePercent = constrain(moisturePercent, 0, 100);
char msg[50];
sprintf(msg, "%d", moisturePercent);
client.publish(mqttTopic, msg);
esp_sleep_enable_timer_wakeup(sleepTimeSeconds * 1000000);
esp_deep_sleep_start();
}
}
void setup_wifi() {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
void reconnect() {
while (!client.connected()) {
client.connect("ArduinoClient", mqttUser, mqttPassword);
delay(5000);
}
}
Home Assistant Integration
In configuration.yaml add:
mqtt:
sensor:
- name: "Plant 1 Soil Moisture"
state_topic: "plants/plant01"
unit_of_measurement: "%"
Then check configuration and restart Home Assistant. The sensor will appear in the entities list.
Troubleshooting
The serial console provides detailed status messages about:
- WiFi connection
- MQTT connection
- Calibration values
- Calculated moisture values
If no data appears in Home Assistant, it is worth checking the MQTT broker log.
3D Printed Enclosure
If you use batteries and the FireBeetle 2 ESP32-E, I designed a matching case that protects the ESP32 somewhat from soil: