How to build a Capacitive Soil Moisture Sensor (HW-390) for ESP8266, ESP32, Raspberry Pi Pico W for Alexa

In this part, we’ll guide you on building a Capacitive Soil Moisture Sensor that can be connected to either an ESP32, ESP8266, or Raspberry Pi Pico W. Once it’s set up, you’ll be able to check the soil moisture level using Amazon Alexa, ask Alexa for information about it, and also monitor it through the Sinric Pro app and send a push notification when soil moisture is dry or wet.

Sinric Pro HW-390 capacitive soil moisture sensor

Prerequisites :

  1. ESP32, ESP8266 or Raspberry Pi Pico W x 1.
  2. Capacitive Soil Moisture Sensor (HW-390) x 1.
  3. Plant with a pot x 1.
  4. Jumper Wires.

Note: on ESP32, ADC2 (GPIO04, GPIO02, GPIO15) is unstable when Wi-Fi is being used.

Quick introduction to Capacitive Soil Moisture Sensor

Capacitive soil moisture sensors measure the electrical resistance between two probes. The resistance changes depending on the amount of water in the soil. The sensor is connected to a microcontroller (MCU), which measures the capacitance between the probes. The HW-390 circuit converts the capacitance measurement to a voltage signal (ADC), which is then sent to the MCU. The MCU can use this signal to display the soil moisture level or to control other devices.

Note: Fertilizers and corrosion can reduce the accuracy of the sensor over time.

Wiring Capacitive Soil Moisture Sensor

Sinric Pro HW-390 esp8266 capacitive soil moisture sensor wiring

MCU GPIO Pin
ESP32 34 (Analog ADC1_CH6)
ESP8266 A0
Pico W GP26 (ADC0)

Let’s verify that sensor is wired correctly and working.

#if defined(ESP8266)
  const int adcPin = A0;  
#elif defined(ESP32) 
  const int adcPin = 34;  
#elif defined(ARDUINO_ARCH_RP2040)
  const int adcPin = 26;  
#endif

void setup() {
  Serial.begin(9600);
  pinMode(adcPin, INPUT);
}

void loop() {
 Serial.println(analogRead(adcPin)); 
 delay(1000);
}

HW-390 capacitive soil moisture sensor arduino readings

Let’s convert the analog input to a precentage value.

#if defined(ESP8266)
  const int adcPin = A0;  
#elif defined(ESP32) 
  const int adcPin = 34;  
#elif defined(ARDUINO_ARCH_RP2040)
  const int adcPin = 26;  
#endif

const int DRY = 720; // TODO: This is value when soil is dry. Adjust according to your usecase
const int WET = 560; // TODO: This is value when soil is wet. Adjust according to your usecase

void setup() {
  Serial.begin(9600);
  pinMode(adcPin, INPUT);
}

void loop() {
 int sensorVal = analogRead(adcPin);
 int percentage = map(sensorVal, WET, DRY, 100, 0); 
 Serial.printf("Sensor value: %d percentage: %d%%\r\n", sensorVal, percentage); 
 
 delay(1000);
}

Sinric Pro moisture sensor esp8266 readings

Step 1 : Connect to Sinric Pro

Step 1.1 : Creating a custom device type for Capacitive Soil Moisture Sensor.

Sinric Pro does not have a built-in device type for Capacitive Soil Moisture Sensor so we are going to create a custom device type for Capacitive Soil Moisture Sensor using Device Template feature to see.

  1. Soil is Wet or Dry

  2. Moisture level.

Note: You can use the device template import feature mentioned below to skip creating the full template.

Sinric Pro capacitive soil moisture sensor device template

Here we must select the features of our Soil Moisture Sensor. We want to know whether Soil is Wet or Dry and the Moisture level. So let’s drag a Range, a Mode and Push Notification capability.

Sinric Pro custom device type for capacitive soil moisture sensor

Click on Configure button and setup the two capabilities like below.

Sinric Pro moisture sensor template mode and range settings

Click on Save to save.

Sinric Pro moisture sensor template mode and range settings

Click on Save to save the template.

Sinric Pro moisture saved template

Now you can see the template we just created.

Import an existing template?

If you are feeling lazy setup all the Modes and Range values, you can use the import feature.

Sinric Pro capacitive soil moisture sensor import template

Paste this Template:

{
  "name": "Capacitive Soil Moisture Sensor",
  "description": "Capacitive Soil Moisture Sensor",
  "deviceTypeId": "6522320f2b090cc39f39fb32",
  "capabilities": [
    {
      "id": "5ff0b41b994fd31b7d5e8961",
      "mode": {
        "instanceId": "modeInstance1",
        "locale": "en-US",
        "modeName": "Moisture",
        "nonControllable": true,
        "modeValues": [
          "Wet",
          "Dry"
        ]
      }
    },
    {
      "id": "5ff0b45f994fd31b7d5e89c2",
      "range": {
        "instanceId": "rangeInstance1",
        "locale": "en-US",
        "rangeName": "Moisture Level",
        "minimumValue": 1,
        "maximumValue": 100,
        "precision": 1,
        "unitOfMeasure": "5ffbeefbfaa11451974c6bbd",
        "presets": [],
        "nonControllable": true
      }
    },
    {
      "id": "62a707bd9e2f39090569e2d3"
    }
  ]
}

View this code on GitHub Gist

Sinric Pro create device alexa

Sinric Pro copy device id

Sinric Pro copy device id

Step 2 : Connect to Sinric Pro

Step 2.1 Install Sinric Pro Library

Sinric Pro install SinricPro library

2.2 Complete Code

CapacitiveSoilMoistureSensor.h

#ifndef _CAPACITIVESOILMOISTURESENSOR_H_
#define _CAPACITIVESOILMOISTURESENSOR_H_

#include <SinricProDevice.h>
#include <Capabilities/ModeController.h>
#include <Capabilities/RangeController.h>
#include <Capabilities/PushNotification.h>

class CapacitiveSoilMoistureSensor 
: public SinricProDevice
, public ModeController<CapacitiveSoilMoistureSensor>
, public RangeController<CapacitiveSoilMoistureSensor>
, public PushNotification<CapacitiveSoilMoistureSensor> {
  friend class ModeController<CapacitiveSoilMoistureSensor>;
  friend class RangeController<CapacitiveSoilMoistureSensor>;
  friend class PushNotification<CapacitiveSoilMoistureSensor>;
public:
  CapacitiveSoilMoistureSensor(const String &deviceId) : SinricProDevice(deviceId, "CapacitiveSoilMoistureSensor") {};
};

#endif

sinricpro-capacitive-soil-moisture-sensor.ino

// #define ENABLE_DEBUG

#ifdef ENABLE_DEBUG
  #define DEBUG_ESP_PORT Serial
  #define NODEBUG_WEBSOCKETS
  #define NDEBUG
#endif

#include <Arduino.h>
#ifdef ESP8266
  #include <ESP8266WiFi.h>
#endif
#ifdef ESP32
  #include <WiFi.h>
#endif

#include <SinricPro.h>
#include "CapacitiveSoilMoistureSensor.h"

// Grab it from https://portal.sinric.pro.
#define APP_KEY    "" //Should look like "de0bxxxx-1x3x-4x3x-ax2x-5dabxxxxxxxx"
#define APP_SECRET "" //Should look like "5f36xxxx-x3x7-4x3x-xexe-e86724a9xxxx-4c4axxxx-3x3x-x5xe-x9x3-333d65xxxxxx"
#define DEVICE_ID  "" //Should look like "6534xxxxxxxc08c6ea1b"

#define SSID       "" // Your WiFi SSID
#define PASS       "" // Your WiFi Password

#define BAUD_RATE         115200              // Change baudrate to your need (used for serial monitor)
#define EVENT_WAIT_TIME   60000               // send event every 60 seconds

#if defined(ESP8266)
  const int adcPin = A0;  
#elif defined(ESP32) 
  const int adcPin = 34;  
#elif defined(ARDUINO_ARCH_RP2040)
  const int adcPin = 26;  
#endif

const int VERY_DRY = 720; // TODO: This is when soil is dry. Adjust according to your sensor
const int VERY_WET = 560; // TODO: This is when soil is wet. Adjust according to your sensor
const int NEITHER_DRY_OR_WET = 620; // TODO: This is when soil is neither wet or dry. Adjust according to your sensor
const int DRY_PUSH_NOTIFICATION_THRESHHOLD = 700; // TODO: This is when to send a push notification to the mobile phone. Adjust according to your sensor
const int UNPLUGGED = 735; // This is the value sensor produce when it's not in the ground.

int lastSoilMoisture = 0;
String lastSoilMoistureStr = "";

CapacitiveSoilMoistureSensor &capacitiveSoilMoistureSensor = SinricPro[DEVICE_ID];

void updateSoilState(String mode) {
  capacitiveSoilMoistureSensor.sendModeEvent("modeInstance1", mode, "PHYSICAL_INTERACTION");
}

void updateMoistureLevel(int value) {
  capacitiveSoilMoistureSensor.sendRangeValueEvent("rangeInstance1", value);
}

void sendPushNotification(String message) {
  capacitiveSoilMoistureSensor.sendPushNotification(message);
}

void handleCapacitiveSoilMoistureSensor() {
  if (SinricPro.isConnected() == false) {
    return; 
  }

  static unsigned long last_millis;
  unsigned long        current_millis = millis();
  if (last_millis && current_millis - last_millis < EVENT_WAIT_TIME) return;
  last_millis = current_millis;
  
  int soilMoisture = analogRead(adcPin);
  int percentage = map(soilMoisture, VERY_WET, VERY_DRY, 100, 0); 
  percentage = constrain(percentage, 1, 100);

  Serial.printf("Soil Moisture: %d. as a percentage: %d%%\r\n", soilMoisture, percentage); 
 
  if (isnan(soilMoisture)) { // reading failed... 
    Serial.printf("Capacitive Soil Moisture Sensor reading failed!\r\n");  // print error message
    return;                                    // try again next time
  } 

  if (soilMoisture == lastSoilMoisture) {
    Serial.printf("Soil moisture did not changed. do nothing...!\r\n");
    return; 
  }

  // Determine Wet or Dry.
  String soilMoistureStr = "";
  if(soilMoisture > NEITHER_DRY_OR_WET) {
    soilMoistureStr = "Dry"; // NOTE: must be Wet, Dry. This is from modes list in template.
  } else {
    soilMoistureStr = "Wet";
  }

  // Update Moisture label
  if(lastSoilMoistureStr != soilMoistureStr) {
    lastSoilMoistureStr = soilMoistureStr;
    updateSoilState(soilMoistureStr);
  }
  
  delay(500);

  // Update Moisture level label
  updateMoistureLevel(percentage); 

  // Send push notifications
  if(soilMoisture > DRY_PUSH_NOTIFICATION_THRESHHOLD) {
    sendPushNotification("Plants are too dry. Time to water the plants!");
  }

  if(soilMoisture > UNPLUGGED) {
    sendPushNotification("Sensor seems to be unhooked. Please check!");
  }

  lastSoilMoisture = soilMoisture;  // save to compare next time.
}


/********* 
 * Setups *
 *********/

void setupSinricPro() {
  SinricPro.onConnected([]{ Serial.printf("[SinricPro]: Connected\r\n"); });
  SinricPro.onDisconnected([]{ Serial.printf("[SinricPro]: Disconnected\r\n"); });
  SinricPro.begin(APP_KEY, APP_SECRET);
};

void setupSensor() {
  pinMode(adcPin, INPUT);
}

void setupWiFi() {
  #if defined(ESP8266)
    WiFi.setSleepMode(WIFI_NONE_SLEEP); 
    WiFi.setAutoReconnect(true);
  #elif defined(ESP32)
    WiFi.setSleep(false); 
    WiFi.setAutoReconnect(true);
  #endif

  WiFi.begin(SSID, PASS);
  Serial.printf("[WiFi]: Connecting to %s", SSID);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.printf(".");
    delay(250);
  }
  Serial.printf("connected\r\n");
}

void setup() {
  Serial.begin(BAUD_RATE);
  setupSensor();
  setupWiFi();
  setupSinricPro();
}

/********
 * Loop *
 ********/

void loop() {
  SinricPro.handle();
  handleCapacitiveSoilMoistureSensor();
}

View this code on GitHub Gist

Now you should be able to see the soil moisture via Alexa, Sinric Pro App

Alexa, What’s the moisture (mode name) in garden(device name)

Alexa, What’s the moisture level(range name) in garden(device name)

Sinric Pro Alexa water sensor

Sinric Pro portal

Step 3 : Voice Announcements!

✅ How it works:

1️⃣ Add a Switch in SinricPro (your secret trigger!)

2️⃣ Create an Alexa Routine that speaks your message when a “Time to water the garden!” when Switch turned on

3️⃣ Send a power state event to turn ON that switch from your code

void triggerVoiceAnnouncement() {
    SinricProSwitch& mySwitch = SinricPro[SWITCH_ID];
    mySwitch.sendPowerStateEvent(true);
}

Troubleshooting

  1. Google Home or SmartThings are not Supported.

  2. Please refer to our Troubleshooting page for more details.

This document is open source. See a typo? Please create an issue