Skip to main content

Getting Started with XIAO Soil Moisture Sensor


Introduction

The XIAO Soil Moisture Sensor is a compact, low-power environmental monitor powered by the XIAO ESP32-C6. Running on a single AA battery, it offers long-lasting operation and real-time soil condition updates. It features Pre-Calibrated Adaptive Soil Moisture Sensing for precise soil monitoring. Meanwhile, it enables dynamic monitoring intervals and instant readouts for accurate, responsive data. Fully compatible with Home Assistant, it’s ideal for smart gardening and precision agriculture—efficient, reliable, and made for sustainable plant care.

Features

1.Three-Level Soil Moisture Monitoring

  • 🌿 Normal: Soil moisture is optimal, no watering needed.
  • 🌤 Almost Dry: Moisture is decreasing, prepare to water soon.
  • 🌵 Dry: Critically low, water immediately.

Default thresholds:

  • 60% → Green to Yellow transition.
  • 20% → Yellow to Red transition.

2.Plug-and-Play with Home Assistant
Preloaded with ESPHome — works out of the box with Home Assistant, letting you monitor and automate directly from your smart home dashboard.

3.Adaptive Monitoring & Instant Readout

  • Automatically adjusts check intervals (8h → 1h → 15min) depending on moisture level.
  • Press the button once for an instant moisture reading anytime.

4.Simple Calibration (Optional)
Quick triple-press the button to recalibrate for your specific soil: dry reading + wet reading → system adjusts automatically.

  • Triple short press → Enter calibration mode:
    • Red LED flashes → Within 10 seconds, insert the sensor into completely dry soil.
    • Wait until the red LED stops flashing, then wait 3 seconds.
    • Green LED flashes → Within 10 seconds, insert the sensor into fully wet soil.
    • Wait until the green LED stops flashing, then wait 3 seconds.
    • Calibration result:
      • Two quick green flashes → Success.
      • Two quick red flashes → Failure (likely due to swapped dry/wet readings).

Note: During calibration, initial readings may be unstable if the sensor is not inserted promptly. The system will take multiple samples, apply filtering, and average the readings for reliable calibration.

5.Long Battery Life
Powered by a single AA battery, designed to last up to three months thanks to optimized power management.

Hardware Overview

Getting Started

This section will guide you through setting up your XIAO Soil Moisture Sensor for the first time.

Materials Required

Before the tutorial content of this article begins, you may need to have the following hardware ready.

XIAO Soil Moisture SensorHome Assistant Green

Home Assistant Green is the easiest and most privacy-focused way to automate your home. It offers an effortless setup and allows you to control all the smart devices with just one system, where all the data is stored locally by default. This board benefits from the thriving Home Assistant ecosystem and it will be improved every month by open source.

We recommend using Home Assistant Green as the Home Assistant host for this tutorial, or you can use any Home Assistant host with a Supervisor.

install Home Assistant

We have also written how to install Home Assistant for some of Seeed Studio products, please refer to them.

If you are not using a Seeed Studio product, you can also check and learn how to install Home Assistant for other products on the official Home Assistant website.

Step 1. Install ESPHome

If you have already installed ESPHome, you can skip this step.

Go to Settings -> Add-ons -> ADD-ON STORE

Search ESPHome and click on it. Click on INSTALL and START.

tip

If you can't find ESPHome in the add-on store, make sure you're using a Home Assistant installation that supports add-ons (like Home Assistant OS or supervised installations). For other installation types (like Home Assistant Container), you may need to run the ESPHome Device Builder independently using Docker. See the official ESPHome documentation for more details.

And then, ESPHome Builder will appear at the sidebar.

Step 2: Preparing the Soil Moisture Sensor

By default, your device (XIAO ESP32C6) comes pre-flashed with firmware for XIAO Soil Moisture Sensor.However, if you need to modify or upgrade the default firmware, a factory YAML configuration file is available in the Resources section below. You can customize the logic as needed and flash it via Home Assistant.

tip

To ensure accurate readings, just perform a quick calibration of the sensor before use.

Step 3: Network Configuration

  1. Enable Access Point:
    • Upon powering up for the first time, the module will create a Wi-Fi network (SSID: Xiao-Soil-Moisture-Monitor).
  1. Access Configuration:
    • Connect to the network using a phone or PC.
    • Open a browser and navigate to http://192.168.4.1.
    • Enter the SSID and password of your home Wi-Fi network.
  1. Home Assistant Integration:
    • Once connected to the home network, the module will be discoverable in Home Assistant under Settings -> Devices & Services.

This way, you can connect the module to your Home Assistant network and let Home Assistant discover it.

Step 4: Add the module device

  1. Automatic Discovery:

    • Ensure the ESPHome is installed in Home Assistant.
    • Navigate to Settings -> Devices & Services -> Integrations and look for the device.
  2. Manual Configuration:

    • If not automatically discovered, manually add the device by specifying its IP address.

After adding the device, you will see a new sensor card named Solid_sensor on the Home Assistant Overview page, displaying both the battery measurement and the current soil moisture status.

Now that your soil sensor is up and running, go ahead and have fun monitoring your plants!

Advanced Usage

You can modify the original firmware logic and flash your customized version of the soil sensor directly through Home Assistant.

Step 1. Install ESPHome

See the installation guide in Step 1 above.

Step 2. Add a new device

Go to ESPHome and click on NEW DEVICE.

Give the device a name you like and click on NEXT.

After you create a new device, click EDIT.

Step 3. Install firmware

Here's Factory firmware:


Here’s a ready-to-use ESPHome YAML configuration for Home Assistant:

Click here to preview the full code
esphome:
name: soil-moisture-monitor
friendly_name: XIAO Soil Moisture Monitor
platformio_options:
platform: https://github.com/mnowak32/platform-espressif32.git#boards/seeed_xiao_esp32c6
on_boot:
then:
# - output.turn_off: gpio_3_output
- output.turn_on: gpio_14_output
- light.turn_on:
id: pwm_led
brightness: 68% # Set 68% duty cycle
- if:
condition:
lambda: 'return id(wifi_net_status) == 0;'
then:
- logger.log: "The device has not been set to the network"
- deep_sleep.prevent: deep_sleep_control
else:
- logger.log: "The device has been networked"
- delay: 1s
- script.execute: check_moisture_once

esp32:
board: seeed_xiao_esp32c6
variant: ESP32C6
flash_size: 4MB
framework:
type: esp-idf
version: "5.2.1"
platform_version: 6.6.0
sdkconfig_options:
CONFIG_ESPTOOLPY_FLASHSIZE_4MB: y

# LED Yellow D10 18
# LED RED D9 20
# LED Green D8 19
# button D2 2

# Battery D0 0
# PWM out D3 21
# Soil sensor D1 1

output:
- platform: gpio
pin: GPIO18
id: yellow_led_output

- platform: gpio
pin: GPIO19
id: green_led_output

- platform: gpio
pin: GPIO20
id: red_led_output

- platform: ledc
pin: GPIO21
id: pwm_output
frequency: 200kHz # Set the frequency to 200kHz

- platform: gpio
pin: GPIO14
id: gpio_14_output

light:
- platform: binary
id: yellow_led
output: yellow_led_output

- platform: binary
id: green_led
output: green_led_output

- platform: binary
id: red_led
output: red_led_output

- platform: monochromatic
output: pwm_output
id: pwm_led
name: "200kHz PWM"
internal: true
default_transition_length: 0s

script:
- id: red_led_blink
mode: restart
then:
- repeat:
count: 10
then:
- light.turn_on: red_led
- delay: 500ms
- light.turn_off: red_led
- delay: 500ms

- id: green_led_blink
mode: restart
then:
- repeat:
count: 10
then:
- light.turn_on: green_led
- delay: 500ms
- light.turn_off: green_led
- delay: 500ms

- id: fast_blink_green
then:
- repeat:
count: 5
then:
- light.turn_on: green_led
- delay: 200ms
- light.turn_off: green_led
- delay: 200ms

- id: fast_blink_red
then:
- repeat:
count: 5
then:
- light.turn_on: red_led
- delay: 200ms
- light.turn_off: red_led
- delay: 200ms

- id: red_led_blink_3_times
then:
- repeat:
count: 1
then:
- light.turn_on: red_led
- delay: 1000ms
- light.turn_off: red_led
- delay: 100ms
- id: yellow_led_blink_3_times
then:
- repeat:
count: 1
then:
- light.turn_on: yellow_led
- delay: 1000ms
- light.turn_off: yellow_led
- delay: 100ms

- id: green_led_blink_3_times
then:
- repeat:
count: 1
then:
- light.turn_on: green_led
- delay: 1000ms
- light.turn_off: green_led
- delay: 100ms

- id: do_calibration
then:
- deep_sleep.prevent: deep_sleep_control
- logger.log: "Starting calibration"
- script.execute: red_led_blink
- delay: 10s
- script.stop: red_led_blink
- lambda: |-
float sum = 0;
for (int i = 0; i < 10; i++) {
id(soil_sensor).update();
sum += id(soil_sensor).state;
delay(200);
}
id(dry_value) = sum / 10.0;
ESP_LOGI("calibration", "Dry value: %f", id(dry_value));

- delay: 3s

- script.execute: green_led_blink
- delay: 10s
- script.stop: green_led_blink
- lambda: |-
float sum = 0;
for (int i = 0; i < 10; i++) {
id(soil_sensor).update();
sum += id(soil_sensor).state;
delay(200);
}
id(wet_value) = sum / 10.0;
ESP_LOGI("calibration", "Wet value: %f", id(wet_value));

- delay: 3s

- lambda: |-
if (id(dry_value) > id(wet_value)) {
ESP_LOGI("calibration", "Calibration success");
id(fast_blink_green).execute();
} else {
ESP_LOGW("calibration", "Calibration failed");
id(fast_blink_red).execute();
}

- delay: 3s
- script.execute: check_moisture_once
- delay: 3s
- deep_sleep.enter: deep_sleep_control

- id: check_moisture_once
then:
- lambda: |-
for(int i = 0; i < 10; i++){
id(soil_sensor).update();
delay(200);
}
float moisture = id(soil_sensor).state;
ESP_LOGI("moisture_check", "Moisture reading: %f", moisture);
float Diff = id(dry_value) - id(wet_value);
ESP_LOGI("moisture_check", "Diff is: %f", Diff);
ESP_LOGI("moisture_check", "ref_dry Diff is: %f",id(dry_value) - Diff * id(ref_dry));
ESP_LOGI("moisture_check", "ref_wet Diff is: %f",id(dry_value) - Diff * id(ref_wet));
if (moisture >= (id(dry_value) - Diff * id(ref_dry))) { // The drier -> the higher the voltage
id(red_led_blink_3_times).execute();
id(deep_sleep_control).set_sleep_duration(900000);
} else if(moisture > (id(dry_value) - Diff * id(ref_wet)) && moisture < (id(dry_value) - Diff * id(ref_dry))){
id(yellow_led_blink_3_times).execute();
id(deep_sleep_control).set_sleep_duration(3600000);
}else{
// moisture > (id(dry_value) - Diff * id(ref_wet))
id(green_led_blink_3_times).execute();
id(deep_sleep_control).set_sleep_duration(28800000);
}

globals:
- id: button_press_count
type: int
restore_value: no
initial_value: '0'
- id: dry_value
type: float
restore_value: yes
initial_value: '2.75'
- id: wet_value
type: float
restore_value: yes
initial_value: '1.2'
- id: wifi_net_status
type: int
restore_value: yes
initial_value: "0"
- id: ref_dry
type: float
restore_value: no
initial_value: "0.23"
- id: ref_wet
type: float
restore_value: no
initial_value: "0.58"

binary_sensor:
- platform: gpio
pin:
number: GPIO2
mode: INPUT_PULLUP
allow_other_uses: true
id: my_button
on_press:
- lambda: |-
id(button_press_count)++;
- delay: 1s # Delay 1 second to see if the button is pressed 3 times in a row
- lambda: |-
if (id(button_press_count) == 3) {
id(button_press_count) = 0;
id(do_calibration).execute(); // Trigger calibration process
} else if (id(button_press_count) == 1) {
id(button_press_count) = 0;
id(check_moisture_once).execute(); // Perform an ADC decision
} else {
id(button_press_count) = 0;
}

deep_sleep:
id: deep_sleep_control
run_duration: 120s
sleep_duration: 180min
wakeup_pin:
number: GPIO2
inverted: true
allow_other_uses: true
mode: INPUT_PULLUP


external_components:
- source: github://pr#7942
components: [ "adc" ]

- source:
type: git
url: https://github.com/ackPeng/esphome.git
ref: api
components: [ api ]
refresh: 0s

sensor:
- platform: adc
id: soil_sensor
pin: GPIO1
name: "Soil moisture measurement"
update_interval: 4s
internal: true
attenuation: 12db


- platform: adc
pin: GPIO0
name: "Battery measurement"
attenuation: 12db
filters: # When the battery drops below 1V, it is dead.
- lambda: |-
if (x < 1.0) {
return 0.0;
} else {
return ((x - 1.0) / (1.5 - 1.0)) * 100.0;
}
unit_of_measurement: "%"
update_interval: 5s
force_update: True

- platform: wifi_signal
name: "wifi singnal strength"
update_interval: 10s

text_sensor:
- platform: template
name: "Soil Moisture Status"
id: soil_status
lambda: |-
float value = id(soil_sensor).state;
float Diff = id(dry_value) - id(wet_value);
if (value >= (id(dry_value) - Diff * id(ref_dry))) {
return {"Dry"};
} else if (value > (id(dry_value) - Diff * id(ref_wet)) && value < (id(dry_value) - Diff * id(ref_dry))) {
return {"Almost Dry"};
} else {
return {"Normal Moisture"};
}
update_interval: never # 不让自动触发上报,我们自己控制

interval:
- interval: 5s
then:
- text_sensor.template.publish:
id: soil_status
state: !lambda |-
return "";
- delay: 10ms
- text_sensor.template.publish:
id: soil_status
state: !lambda |-
float value = id(soil_sensor).state;
float Diff = id(dry_value) - id(wet_value);
if (value >= (id(dry_value) - Diff * id(ref_dry))) {
id(deep_sleep_control).set_sleep_duration(900000);
return "Dry";
} else if (value > (id(dry_value) - Diff * id(ref_wet)) && value < (id(dry_value) - Diff * id(ref_dry))) {
id(deep_sleep_control).set_sleep_duration(3600000);
return "Almost Dry";
} else {
id(deep_sleep_control).set_sleep_duration(28800000);
return "Normal Moisture";
}

# Enable logging
logger:

improv_serial:

# Enable Home Assistant API
api:
# encryption:
# key: "YVjz+1l5zHXeyXFVinhaJkqh8RnG0gUVjaWniPEzCj4="

ota:
- platform: esphome
password: "dcad8df988971d761bc72a30d7878a40"

wifi:
# ssid: "my68k"
# password: "1143590135"
on_connect:
then:
- if:
condition:
lambda: 'return id(wifi_net_status) == 0;'
then:
- logger.log: "The device has not been configured yet, but now it is successfully configured"
- globals.set:
id: wifi_net_status
value: '1'
- delay: 5s
- deep_sleep.allow: deep_sleep_control
else:
- logger.log: "The device has been networked"

on_disconnect:
then:
- globals.set:
id: wifi_net_status
value: '0'
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Xiao-Soil-Moisture-Monitor"
password: ""

captive_portal:
Here’s an overview of the key functions and logic used in the YAML configuration.

on_boot – Defines what happens when the device boots.

  • Input parameters: None.
  • Action: Turns on GPIO 14, sets PWM LED brightness, checks Wi-Fi status, and triggers the first moisture check.

scripts (red_led_blink, green_led_blink, fast_blink_green, fast_blink_red, etc.) – Predefined LED blinking patterns.

  • Input parameters: None.
  • Action: Blinks LEDs in various patterns to indicate status or calibration steps.

do_calibration – Runs the calibration process for dry and wet soil.

  • Input parameters: None.
  • Action: Blinks red LED, waits for dry reading; then blinks green LED, waits for wet reading; stores average values and confirms success or failure.

check_moisture_once – Reads and evaluates soil moisture levels.

  • Input parameters: None.
  • Action: Takes multiple ADC readings, averages them, compares against calibrated thresholds, decides moisture state, triggers LED and deep sleep settings accordingly.

binary_sensor (GPIO2) – Handles physical button press logic.

  • Input parameters: None.
  • Action: Counts button presses; single press triggers a moisture check, triple press triggers calibration.

globals – Stores system state and calibration data.

  • Variables:
    • button_press_count: Tracks button press count.
    • dry_value, wet_value: Stores calibrated dry/wet ADC values.
    • wifi_net_status: Tracks Wi-Fi connection state.
    • ref_dry, ref_wet: Reference scaling factors for threshold calculations.

deep_sleep – Manages power-saving sleep cycles.

  • Input parameters: None.
  • Action: Runs for 120 seconds, then sleeps for up to 180 minutes; wakes up on button press or interval.

sensor (ADC) – Reads analog values from the soil sensor and battery.

  • Input parameters: None.
  • Action: Measures soil moisture and battery voltage; battery is scaled to show percentage.

text_sensor – Publishes human-readable soil moisture status.

  • Input parameters: None.
  • Action: Displays "Dry", "Almost Dry", or "Normal Moisture" in Home Assistant.

wifi + api + ota – Manages network connection, Home Assistant integration, and over-the-air firmware updates.

  • Input parameters: Wi-Fi SSID and password.
  • Action: Connects the device to the network, exposes its API, and enables remote updates.

Click INSTALL to install the code to the device and you will see the following image.

tip

If your Home Assistant Host (Raspberry PI/Green/Yellow etc.) is far away from you, we recommend using this method. You can install it with the computer you have on hand.

First, you need to click Manual download to download the compiled firmware.

Open this website where we will upload the firmware to the ePaper panel.

Go back to ESPHome to download the firmware.

Select Factory format.

Use USB cable to connect the ePaper panel to your computer and click CONNECT.

Select usbmodemxxx(Windows is COMxxx) and click connect. Encountered a problem? Click here.

Click INSTALL and select the firmware you just downloaded.

Your firmware will be flashed shortly ~

Reset

If the firmware needs to be reflashed, you can use the following link to restore the default firmware:
https://gadgets.seeed.cc/

Connect your device to your computer first.
Then, find XIAO Soil Moisture Monitor on the page and click Connect to proceed with reflashing.

Resources

Tech Support & Product Discussion

Thank you for choosing our products! We are here to provide you with different support to ensure that your experience with our products is as smooth as possible. We offer several communication channels to cater to different preferences and needs.

Loading Comments...