Plant sensor for Home Assistant on an ESP32-C3
A capacitive soil sensor mapped between two calibration readings, a light level, and a thirsty LED with two thresholds so it does not flicker, on an ESP32-C3. The whole build, an ESP32-C3 and 4 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Beginner Runs in your browser. Free, and no account needed.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Home Assistant plant sensor: soil moisture and light, calibrated, with a thirsty alarm.
//
// capacitive soil sensor on GPIO 3 wetter soil reads LOWER, which surprises everyone once
// light module on GPIO 4 the LDR board's analog output, brighter reads lower
// red LED on GPIO 19 lights while the plant needs water
//
// A plant sensor is mostly calibration. The raw number means nothing until you
// have measured two points with your own sensor: the stake in dry air and the
// stake in a glass of water. Put those two readings in DRY_RAW and WET_RAW and
// the sketch maps everything between them to 0 to 100 percent, the same job
// ESPHome's calibrate_linear filter does.
//
// The thirsty alarm uses two thresholds, not one, so a reading that hovers near
// the line does not switch it on and off every few seconds.
//
// A battery sensor would read once and deep sleep for many minutes. Here it reads
// every 5 seconds so you can watch it, and stays awake. Mokxi has no MQTT broker:
// the sketch prints what it would publish and serves JSON at /state.
#include <WebServer.h>
#include <WiFi.h>
const int SOIL_PIN = 3;
const int LIGHT_PIN = 4;
const int THIRSTY_LED_PIN = 19;
// Your sensor's two calibration readings (0 to 4095 on the ESP32-C3).
const int DRY_RAW = 3164; // in air
const int WET_RAW = 1576; // in water
const int THIRSTY_BELOW = 30; // percent
const int HAPPY_ABOVE = 40; // percent
const unsigned long READ_EVERY_MS = 5000;
WebServer server(80);
int moisture = 0;
int lightLevel = 0;
bool thirsty = false;
unsigned long lastRead = 0;
void publish(const char *topic, int value) {
Serial.print("publish home/plant/");
Serial.print(topic);
Serial.print(' ');
Serial.println(value);
}
// The average of a few readings: a single ADC sample is noisy on real hardware.
int readAverage(int pin) {
long total = 0;
for (int i = 0; i < 8; i++) total += analogRead(pin);
return total / 8;
}
void readPlant() {
int raw = readAverage(SOIL_PIN);
moisture = constrain(map(raw, DRY_RAW, WET_RAW, 0, 100), 0, 100);
// The light module reads lower in more light; flip it so bigger means brighter.
lightLevel = map(readAverage(LIGHT_PIN), 4095, 0, 0, 100);
Serial.print("soil raw ");
Serial.print(raw);
Serial.print(" -> ");
Serial.print(moisture);
Serial.println("%");
publish("moisture", moisture);
publish("light", lightLevel);
if (!thirsty && moisture < THIRSTY_BELOW) {
thirsty = true;
Serial.println("publish home/plant/thirsty ON");
} else if (thirsty && moisture > HAPPY_ABOVE) {
thirsty = false;
Serial.println("publish home/plant/thirsty OFF");
}
digitalWrite(THIRSTY_LED_PIN, thirsty ? HIGH : LOW);
}
void sendState() {
String json = "{\"moisture\":";
json += String(moisture);
json += ",\"light\":";
json += String(lightLevel);
json += ",\"thirsty\":";
json += thirsty ? "true" : "false";
json += "}";
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
pinMode(THIRSTY_LED_PIN, OUTPUT);
Serial.println("Plant sensor: simulated WiFi, no MQTT broker in Mokxi.");
WiFi.begin("Mokxi-Lab", "breadboard");
while (WiFi.status() != WL_CONNECTED) {
delay(250);
}
Serial.print("online at http://");
Serial.print(WiFi.localIP());
Serial.println("/state");
server.on("/", sendState);
server.on("/state", sendState);
server.begin();
readPlant();
lastRead = millis();
}
void loop() {
server.handleClient();
if (millis() - lastRead >= READ_EVERY_MS) {
lastRead = millis();
readPlant();
}
delay(10);
}
Parts list
6 parts, plus the jumper wires. Every one is in the editor's parts bin.
How it is wired
7 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- Ground: ESP32-C3-DevKitM-1 pin GND; Photoresistor sensor module pin GND; Soil moisture sensor, capacitive pin GND
- ESP32-C3-DevKitM-1 pin 3V3; Photoresistor sensor module pin VCC; Soil moisture sensor, capacitive pin VCC
- ESP32-C3-DevKitM-1 pin 3; Soil moisture sensor, capacitive pin AOUT
- ESP32-C3-DevKitM-1 pin 4; Photoresistor sensor module pin AO
- Ground: ESP32-C3-DevKitM-1 pin GND; LED, red pin C
- ESP32-C3-DevKitM-1 pin 19; Resistor, 220 Ω pin 1
- Resistor, 220 Ω pin 2; LED, red pin A
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.