ESP32 DHT11 CP2104 Soil Moisture Sensor Problem

Hello

i have the below ESP32 DHT11 CP2104 WIFI Bluetooth Temperature Humidity Soil Moisture Sensor:

Can somebody help me how to integrate it with Home Assistant?

I cannot find any code

Thanks in advance

It shouldn’t be too difficult. There is an instructables tutorial with arduino. You can use it to figure out, to which pins the sensors are connected.

I can share with you yaml file which I’m using to integrate soil moisture sensor readings into Home Assistant. It’s going to need some adjustments/removal of unnecessary code to make it work with your module. I’m using standalone sensors, 3 of them connected to single ESP32. My YAML file has also integrated calibration, which requires OLED display and rotary encoder. I’m also using a mosfet to cut power from display and sensors while in deep sleep, connected to GPIO12.

esphome:
  name: soil2
  platformio_options:
    upload_speed: 921600  
  on_boot:
    - priority: 1000
      then:
        lambda: |-
            # turn on power to display and sensors
            pinMode(12, OUTPUT);
            digitalWrite(12, LOW);
            delay(50); 
            // ESP_LOGI("on_boot", "prio1000");
esp32:
  board: lolin32_lite
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true

api:
  encryption:
    key: !secret api_encryption_key

ota:
  password: !secret ota_password


  # Enable fallback hotspot (captive portal) in case wifi connection fails
#  ap:
#    ssid: "Blink Fallback Hotspot"
#    password: !secret fallback_ap_password

 
globals:
  - id: gcal_wet
    type: float
    restore_value: true
    initial_value: "1.2"
  - id: gcal_dry
    type: float
    restore_value: true
    initial_value: "2.8"
  - id: last_menu_action
    type: int

deep_sleep:
  id: deep_sleep_1
  run_duration: 10s
  sleep_duration: 30min
#  wakeup_pin: 
#    number: GPIO25
    # inverted: true  

# output:
#   - platform: gpio
#     id: status_led
#     pin: GPIO22
time:
  - platform: sntp
    id: sntp_time
    timezone: Europe/Prague
    servers:
     - 0.pool.ntp.org
     - 1.pool.ntp.org
     - 2.pool.ntp.org

font:
  - file: "fonts/Roboto-Thin.ttf"
    id: roboto
    size: 12
i2c:
  sda: GPIO16
  scl: GPIO17
  frequency: 800kHz

        # const auto display_width = it.get_width();
        # const auto display_height = it.get_height();
        # // This will render the menu to the right half of the screen leaving the left half for other drawing purposes
        # // Arguments: it.menu(x, y, menu, width, height);
        # it.menu(display_width, 0, id(my_menu), display_width, display_height);

text_sensor:
  - platform: template
    name: "${name}_MAC_Address"
    lambda: 'return {WiFi.macAddress().c_str()};'
    icon: mdi:fingerprint
    id: soil2_mac
    update_interval: 1d 
  - platform: wifi_info
    ip_address:
      name: Adress_IP  
      id: soil2_ip

display:
  - platform: ssd1306_i2c
    id: my_oled
    model: "SSD1306 128x64"
    #auto_clear_enabled: false
    address: 0x3C
    lambda: |-
          //it.printf(0, 0, id(roboto), "Vb:%.2f", id(vbat).state);
          //it.printf(0, 14, id(roboto), "MC:%s", id(soil2_mac).state.c_str());
          //it.printf(0, 26, id(roboto), "IP:%s", id(soil2_ip).state.c_str());
          //it.printf(0, 38, id(roboto), "MC:%s", id(soil2_mac).state.c_str());
          //it.printf(0, 50, id(roboto), "IP:%s", id(soil2_ip).state.c_str());
          if (id(my_menu).is_active()) {
          const auto width = it.get_width();
          const auto height = it.get_height();          
          it.menu(0, 0, id(my_menu), width, height);
          } else {
          if (id(sntp_time).now().is_valid()) {
            it.strftime(0, 0, id(roboto), "%H:%M:%S", id(sntp_time).now());
          } else {
            it.print(0, 0, id(roboto), "sync in progress ...");
          }
          // it.printf(0, 20, id(roboto), "Vbat:%.2f", id(vbat).state);
          it.printf(0, 20, id(roboto), "%.0f %.0f %.0f Vb:%.2f", id(soil1).state, id(soil2).state, id(soil3).state, id(vbat).state);
          //it.printf(0, 20, id(roboto), "%.0f %.0f %.0f Vb:%.2f", id(soil1).state, id(soil2).state, id(soil3).state, id(vbat).state);
          //it.printf(0, 40, id(roboto), "%.2f %.2f %.2f", id(soil1).raw_state, id(soil2).raw_state, id(soil3).raw_state);
          }

#it.strftime(0, 0, id(roboto), "%Y-%m-%d", id(sntp_time).now());
switch:
  - platform: template
    id: sleep_switch
    optimistic: true

script:
  - id: menu_closer
    then:
      - while:
          condition:
            display_menu.is_active: my_menu
          then:
            - lambda: |-
                int mtime = id(uptime1).state - id(last_menu_action);
                ESP_LOGI("menu_closer", "menu_idle_time=%d", mtime);              
            - delay: 1s

graphical_display_menu:
  id: my_menu
  font: roboto
  active: false
  mode: rotary
  on_redraw:
    then:
      component.update: my_oled  


  items:
    - type: command
      text: Cal DRY
      on_value:
        then:
          lambda: |-
            id(gcal_dry)=id(soil1).raw_state;
            ESP_LOGI("menu_item", "Cal DRY: %f", id(gcal_dry));
            id(my_menu).hide();
    - type: command
      text: Cal WET
      on_value:
        then:
          lambda: |-
            id(gcal_wet)=id(soil1).raw_state;
            ESP_LOGI("menu_item", "Cal WET: %f", id(gcal_wet));
            id(my_menu).hide();
    - type: command
      text: 'Hide'
      on_value:
        then:
          - display_menu.hide: my_menu
          - lambda: |-
              ESP_LOGI("display_menu", "menu leave");
              id(last_menu_action)=id(uptime1).state;
          - script.stop: menu_closer
          - deep_sleep.allow: deep_sleep_1      

# Encoder to provide navigation
sensor:
  - platform: uptime
    update_interval: 1s
    id: uptime1
  - platform: rotary_encoder
    id: rotary_encoder1
    restore_mode: ALWAYS_ZERO 
    pin_a:
      number: GPIO23
      mode:
        input: true
        pullup: true
    pin_b: 
      number: GPIO18
      mode:
        input: true
        pullup: true
    on_anticlockwise:
      - display_menu.up: my_menu
      - lambda: 'id(last_menu_action)=id(uptime1).state;'
    on_clockwise:
      - display_menu.down: my_menu
      - lambda: 'id(last_menu_action)=id(uptime1).state;'
  - platform: adc
    pin: GPIO32
    name: "soil1"
    id: soil1
    attenuation: auto
    unit_of_measurement: '%'
    update_interval: 3s
    filters:
      lambda: |-
        if (x<=id(gcal_wet)) return 100;
        if (x>=id(gcal_dry)) return 0;
        return (id(gcal_dry)-x)*100/(id(gcal_dry)-id(gcal_wet));
  - platform: adc
    pin: GPIO35
    name: "soil2"
    id: soil2
    attenuation: auto
    unit_of_measurement: '%'
    update_interval: 10s
    filters:
      lambda: |-
        if (x<=id(gcal_wet)) return 100;
        if (x>=id(gcal_dry)) return 0;
        return (id(gcal_dry)-x)*100/(id(gcal_dry)-id(gcal_wet));
  - platform: adc
    pin: GPIO34
    name: "soil3"
    id: soil3
    attenuation: auto
    unit_of_measurement: '%'
    update_interval: 10s
    filters:
      lambda: |-
        if (x<=id(gcal_wet)) return 100;
        if (x>=id(gcal_dry)) return 0;
        return (id(gcal_dry)-x)*100/(id(gcal_dry)-id(gcal_wet));
  - platform: adc
    pin: GPIO33
    name: "vbat"
    id: vbat
    attenuation: auto
    unit_of_measurement: 'V'
    update_interval: 10s
    filters:
      lambda: |-
        return x*2;
  

# A de-bounced GPIO is used to 'click'
binary_sensor:
  - platform: gpio
    id: rotary_encoder_btn
    pin:
      number: GPIO19
      mode:
        input: true
        pullup: true
    filters:
      - invert:
      - delayed_on: 10ms
      - delayed_off: 10ms
    on_press:
      - lambda: 'id(last_menu_action)=id(uptime1).state;'
      - if:
          condition:
            display_menu.is_active: my_menu
          then:
            - display_menu.enter:  my_menu    
          else:
            - display_menu.show:  my_menu    
            - lambda: |-
                ESP_LOGI("display_menu", "menu enter");
                id(last_menu_action)=id(uptime1).state;
            - deep_sleep.prevent: deep_sleep_1      
            - script.execute: menu_closer


1 Like

Hey, since you got it running already in HA, what is your average battery / rechargeable battery lifetime?

Thinking of trying these. I suppose it is also possible to set the interval when the data is read, right?

Is all that config really necessary? I thought once connected to the ESP32 with wifi you just set the MQTT broker and receive the data

1 Like

I got it working, based on Tasmota config

GPIO16 LEDLink
GPIO22 DHT11
GPIO32 Humidity Sensor
GPIO34 Battery Voltage/Level

esphome:
  name: soil
  friendly_name: "DIY MORE ESP32 DHT11 Soil Sensor"

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "VoKBaLID9YyLMnm/OKelMY9WMRWhHF4FHSNhKc0iGXY="

ota:
  - platform: esphome
    password: "97ab7d88b6087dfd43c80440fc9129da"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Soil Fallback Hotspot"
    password: "ufgw71Q348ny"

# LEDLink
output:
  - platform: gpio
    pin: GPIO16 
    id: led_output

sensor:
# DHT 11
  - platform: dht
    pin: GPIO22
    model: DHT11
    temperature:
      name: "DHT11 Temperature"
    humidity:
      name: "DHT11 Humidity"
    update_interval: 60s

# SOIL MOISTURE
  - platform: adc
    pin: GPIO32
    name: "Soil Moisture Sensor Voltage"
    id: soil_moisture_voltage
    unit_of_measurement: "V"
    device_class: MOISTURE
    update_interval: 5s
    attenuation: 11db
    accuracy_decimals: 3

  - platform: template
    name: "Soil Moisture"
    id: smoothed_voltage
    unit_of_measurement: "%"
    lambda: |-
      return id(soil_moisture_voltage).state;
    update_interval: 1s
    filters:
    - calibrate_linear: #set your own values here
        - 1.29 ->  100.00
        - 2.8 ->  0.00
    - lambda: |
       if (x < 0) return 0; 
       else if (x > 100) return 100;
       else return (x);
    accuracy_decimals: 0

# Battery
  - platform: adc
    pin: GPIO34
    name: "Battery Voltage"
    id: battery_voltage
    update_interval: 60s
    accuracy_decimals: 2
    filters:
      - multiply: 35  # Adjust this based on your voltage divider circuit

  - platform: template
    name: "Battery Level"
    unit_of_measurement: '%'
    accuracy_decimals: 0
    lambda: |-
      float voltage = id(battery_voltage).state;
      // Customize the voltage range according to your battery
      if (voltage > 4.2) return 100;
      else if (voltage < 3.0) return 0;
      else return (voltage - 3.0) / (4.2 - 3.0) * 100;
#    update_interval: 60s
    icon: "mdi:battery"


captive_portal:
    
web_server:  
  port: 80

However, the calibration of humidity and battery level should be tweaked more.

Next is to implement deep sleep in a way.

  1. Wake every X minutes
  2. Connect to Wifi
  3. Send MQTT (battery level, temperature, humidity, soil moisture)
  4. Sleep

I’m pretty much new to Esphome, so things take time. If someone can contribute or improve it, would be most welcome.

Thank you

Br

I think you should rethink your battery level calculation. Li-ion battery voltage is far from linear. Your code would give 60% level when your battery is at 90% charge.

image

Thank you, will try to find a better solution.

You cold use calibrate_linear like you do with your moisture sensor.
Something like this:

filters:
    - calibrate_linear: 
       method: exact
       datapoints:
         - 3.0 -> 0
         - 3.4 -> 10
         - 4.1 -> 99
    - clamp:
         min_value: 0
         max_value: 100

maybe something like this:

lambda: |-
      float voltage = id(battery_voltage).state;
      if (voltage > 4.2) {
        return 100.0;
      } else if (voltage > 4.1) {
        return 90.0 + 10.0 * (voltage - 4.1) / 0.1;
      } else if (voltage > 3.9) {
        return 80.0 + 10.0 * (voltage - 3.9) / 0.2;
      } else if (voltage > 3.7) {
        return 60.0 + 20.0 * (voltage - 3.7) / 0.2;
      } else if (voltage > 3.5) {
        return 30.0 + 30.0 * (voltage - 3.5) / 0.2;
      } else if (voltage > 3.3) {
        return 10.0 + 20.0 * (voltage - 3.3) / 0.2;
      } else if (voltage > 3.0) {
        return 0.0 + 10.0 * (voltage - 3.0) / 0.3;
      } else {
        return 0.0;
      }

Yep.
Just look at your battery datasheet to match with the curve of lowest discharge current. Or tune it by actual measurements…

will do thanks.

Right now I’m figuring out how to use on_boot so it:

  • turns on
  • connect to wigi
  • connect to mqtt
  • send soil moisture, humidity, temperature and a battery level
  • enters deep sleep.

Since this is a few second operation I would need to add a longer deep sleep run_duration on the first boot or reset (EN button) 2 minutes for an example, so I can do OTA.

Thank you

You could make a condition to check if OTA is asked and prevent deep sleep is so.
You can trigger that from HA or Mqtt.

Maybe that"s the best option, as I dont know how can I separate deep sleep wakeup from power on boot or by pressing N3 reset button.

So, then how would esp know when OTA is asked? Im missing that part.how does the process work?

How can I trigger that from HA?

Thank you

You could use deep_sleep.prevent

There is Mqtt example.

On home assistant you could make binary sensor or switch for OTA.
Then check that state on your on_boot component after other stuff you have there, and if state is true, keep awake, if not go to sleep.

Just don’t ask me how to create Home assistant switch, I use Esphome barebones without HA.