17 Comments
User's avatar
inzane's avatar

If you want to use multiple sensors and use the potential of i2c you could change the following part of the code:

// Constructor

StemmaSoilSensor(int addr) : PollingComponent (30000)

{

this->i2c_addr = addr;

}

within esphome you can now do the following (max. 4 sensors :-) ):

sensor:

- platform: custom

lambda: |-

auto soil_sensor = new StemmaSoilSensor(0x36); // no bridge 0x36

App.register_component(soil_sensor);

return {soil_sensor->temperature_sensor, soil_sensor->moisture_sensor};

sensors:

- name: "Plant Temperature"

- name: "Plant Moisture"

- platform: custom

lambda: |-

auto soil_sensor = new StemmaSoilSensor(0x37); // sensor with AD0 bridged 0x37

App.register_component(soil_sensor);

return {soil_sensor->temperature_sensor, soil_sensor->moisture_sensor};

sensors:

- name: "Plant Temperature 2"

- name: "Plant Moisture 2"

- platform: custom

lambda: |-

auto soil_sensor = new StemmaSoilSensor(0x38); // sensor with AD1 bridged 0x38

App.register_component(soil_sensor);

return {soil_sensor->temperature_sensor, soil_sensor->moisture_sensor};

sensors:

- name: "Plant Temperature 3"

- name: "Plant Moisture 3"

- platform: custom

lambda: |-

auto soil_sensor = new StemmaSoilSensor(0x39); // sensor with AD0 and AD1 bridged 0x39

App.register_component(soil_sensor);

return {soil_sensor->temperature_sensor, soil_sensor->moisture_sensor};

sensors:

- name: "Plant Temperature 4"

- name: "Plant Moisture 4"

As you can see you can use 4 Sensors now.

Also if you have trouble while compiling,

watch that you have includes and libraries

esphome:

name: here-your-name

includes:

- stemma_soil_sensor.h

libraries:

- Wire

Expand full comment
Alistair Young's avatar

Works well, indeed. When I tweaked mine this way, I also gave the constructor a default argument

StemmaSoilSensor(int addr - 0x36) : PollingComponent (30000)

{

which lets you use multiple sensors in new stuff without having to modify any of your existing configurations.

Expand full comment
Brian Wilson's avatar

This is what I did to get it working with a raspberry pi pico w

soil_sensor.h

```

#include "esphome.h"

#include "Adafruit_seesaw.h"

class SoilSensor : public PollingComponent, public Sensor {

public:

Adafruit_seesaw ss;

Sensor *TemperatureSensor = new Sensor();

Sensor *MoistureSensor = new Sensor();

SoilSensor() : PollingComponent(60000) {}

void setup() override {

ss.begin(0x36);

}

void update() override {

auto temp = ss.getTemp();

auto capread = ss.touchRead(0);

TemperatureSensor->publish_state(temp);

MoistureSensor->publish_state(capread);

}

};

```

```

esphome:

includes:

- soil_sensor.h

libraries:

- adafruit/Adafruit seesaw Library

```

```

# pico w

i2c:

sda: 8

scl: 9

```

```

sensor:

- platform: custom

lambda: |-

auto soil_sensor = new SoilSensor();

App.register_component(soil_sensor);

return {soil_sensor->TemperatureSensor, soil_sensor->MoistureSensor};

sensors:

- name: "Temperature"

unit_of_measurement: '°C'

- name: "Moisture"

```

Expand full comment
CJ's avatar

That does not work for me...

Could you share your yaml and .h?

Expand full comment
Aaron's avatar

Came across this post trying to get my moisture sensors integrated. I noticed in the ESPHome documentation that custom components are going away in the 2025.1 release, so I rewrote this as an external component. I'll put a pull request in eventually, but I need to make documentation for it first. Meanwhile, the component is here:

https://github.com/asolochek/HA-config/tree/main/esphome/components/adafruit_soil_sensor

You can use it like this:

external_components:

- source:

type: git

url: https://github.com/asolochek/HA-config

components: [ adafruit_soil_sensor ]

sensor:

- platform: adafruit_soil_sensor

address: 0x36

temperature:

name: "Temperature 1"

moisture:

name: "Moisture 1"

update_interval: 10s

- platform: adafruit_soil_sensor

address: 0x37

temperature:

name: "Temperature 2"

offset: -6.0

moisture:

name: "Moisture 2"

update_interval: 10s

- platform: adafruit_soil_sensor

address: 0x38

temperature:

name: "Temperature 3"

moisture:

name: "Moisture 3"

min_value: 350

update_interval: 10s

- platform: adafruit_soil_sensor

address: 0x39

temperature:

name: "Temperature 4"

moisture:

name: "Moisture 4"

min_value: 350

max_value: 1015

update_interval: 10s

The temperatures can be adjusted with a gain and offset parameter. The moisture value is mapped from 350-1015 (1015 is the highest raw value I've seen, which was in mud) by default, but you can set those with min_value and max_value so it scales to your liking.

Expand full comment
pnad's avatar

Unfortunately I couldn't get this working for me. Any help appreciated

log:

INFO Reading configuration /config/esphome/touch-sensor.yaml...

INFO Starting log output from /dev/ttyUSB0 with baud rate 115200

[11:06:06][I][ota:109]: Boot seems successful, resetting boot loop counter.

[11:06:06][D][esp32.preferences:113]: Saving 1 preferences to flash...

[11:06:06][D][esp32.preferences:142]: Saving 1 preferences to flash: 1 cached, 0 written, 0 failed

[11:06:06][E][ota:476]: No OTA attempt made, restarting.

[11:06:06][I][app:127]: Forcing a reboot...

[11:06:06]ets Jun 8 2016 00:22:57

[11:06:06]

[11:06:06]rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)

[11:06:06]configsip: 0, SPIWP:0xee

[11:06:06]clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00

[11:06:06]mode:DIO, clock div:2

[11:06:06]load:0x3fff0018,len:4

[11:06:06]load:0x3fff001c,len:1044

[11:06:06]load:0x40078000,len:10124

[11:06:06]load:0x40080400,len:5828

[11:06:06]entry 0x400806a8

[11:06:06][I][logger:243]: Log initialized

[11:06:06][C][ota:465]: There have been 0 suspected unsuccessful boot attempts.

[11:06:06][D][esp32.preferences:113]: Saving 1 preferences to flash...

[11:06:06][D][esp32.preferences:142]: Saving 1 preferences to flash: 0 cached, 1 written, 0 failed

[11:06:06][I][app:029]: Running through setup()...

[11:06:07][E][soil_sensor:060]: Failed to connect to soil sensor.

[11:06:07][I][soil_sensor:065]: Successfully reset soil sensor.

[11:06:07][C][wifi:037]: Setting up WiFi...

[11:06:07][D][wifi:384]: Starting scan...

[11:06:12]E (6448) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:

Expand full comment
Alistair Young's avatar

Honestly, I'm not sure what the problem might be, here. I haven't been able to reproduce it, but I'm wondering if there might be some bitness issue, as I don't have an ESP32 to test on, just ESP8266s. Maybe try it on one of those if you have one handy, see if it still happens?

Expand full comment
pnad's avatar

#include "esphome.h"

#include "Wire.h"

#define SEESAW_HW_ID_CODE 0x55 ///< seesaw HW ID code

/** Module Base Addreses

* The module base addresses for different seesaw modules.

*/

enum

{

SEESAW_STATUS_BASE = 0x00,

SEESAW_TOUCH_BASE = 0x0F,

};

/** status module function address registers

*/

enum

{

SEESAW_STATUS_HW_ID = 0x01,

SEESAW_STATUS_VERSION = 0x02,

SEESAW_STATUS_OPTIONS = 0x03,

SEESAW_STATUS_TEMP = 0x04,

SEESAW_STATUS_SWRST = 0x7F,

};

/** touch module function address registers

*/

enum {

SEESAW_TOUCH_CHANNEL_OFFSET = 0x10,

};

class StemmaSoilSensor : public PollingComponent

{

public:

// Constructor

StemmaSoilSensor() : PollingComponent (300000)

{

this->i2c_addr = 0x36;

}

Sensor * temperature_sensor = new Sensor();

Sensor * moisture_sensor = new Sensor();

uint8_t i2c_addr;

bool setupFailed = true;

// This will be called by App.setup ()

void setup() override

{

// Start the seesaw.

// Perform software reset.

this->write8 (SEESAW_STATUS_BASE, SEESAW_STATUS_SWRST, 0xFF);

delay (500);

// Check for seesaw.

uint8_t c = this->read8(SEESAW_STATUS_BASE, SEESAW_STATUS_HW_ID);

if (c != SEESAW_HW_ID_CODE)

{

ESP_LOGE("soil_sensor", "Failed to connect to soil sensor.");

// TODO: inform of failure

}

this->setupFailed = false;

ESP_LOGI("soil_sensor", "Successfully reset soil sensor.");

// TODO: inform of success

this->temperature_sensor->set_unit_of_measurement("°F");

this->temperature_sensor->set_accuracy_decimals(0);

this->moisture_sensor->set_unit_of_measurement("moistcap");

this->moisture_sensor->set_accuracy_decimals(0);

}

// This will be called every update_interval milliseconds.

void update() override

{

if (this->setupFailed)

return;

float tempC = this->getTemp();

uint16_t capread = this->touchRead(0);

float tempF = (tempC * 1.8) + 32.0;

this->temperature_sensor->publish_state(tempF);

this->moisture_sensor->publish_state(capread);

}

// Get the temperature of the seesaw board in degrees Celsius

float getTemp()

{

uint8_t buf[4];

this->read(SEESAW_STATUS_BASE, SEESAW_STATUS_TEMP, buf, 4, 1000);

int32_t ret = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) |

((uint32_t)buf[2] << 8) | (uint32_t)buf[3];

return (1.0 / (1UL << 16)) * ret;

}

// Read the current analog value of the capacitative sensor.

uint16_t touchRead(uint8_t pin)

{

uint8_t buf[2];

uint8_t p = pin;

uint16_t ret = 65535;

do {

delay(1);

this->read(SEESAW_TOUCH_BASE, SEESAW_TOUCH_CHANNEL_OFFSET + p, buf, 2, 1000);

ret = ((uint16_t)buf[0] << 8) | buf[1];

} while (ret == 65535);

return ret;

}

// Read a specified number of bytes into a buffer from the seesaw.

void read(uint8_t regHigh, uint8_t regLow, uint8_t *buf, uint8_t num, uint16_t delay)

{

uint8_t pos = 0;

// on arduino we need to read in 32 byte chunks

while (pos < num) {

uint8_t read_now = min(32, num - pos);

Wire.beginTransmission((uint8_t)this->i2c_addr);

Wire.write((uint8_t)regHigh);

Wire.write((uint8_t)regLow);

Wire.endTransmission();

// TODO: tune this

delayMicroseconds(delay);

Wire.requestFrom((uint8_t)this->i2c_addr, read_now);

for (int i = 0; i < read_now; i++) {

buf[pos] = Wire.read();

pos++;

}

}

}

// Read 1 byte from the specified seesaw register

uint8_t read8(byte regHigh, byte regLow, uint16_t delay = 125)

{

uint8_t ret;

this->read(regHigh, regLow, &ret, 1, delay);

return ret;

}

// Write a specified number of bytes to the seesaw from the passed buffer.

void write(uint8_t regHigh, uint8_t regLow, uint8_t *buf, uint8_t num)

{

Wire.beginTransmission((uint8_t)this->i2c_addr);

Wire.write((uint8_t)regHigh);

Wire.write((uint8_t)regLow);

Wire.write((uint8_t *)buf, num);

Wire.endTransmission();

}

// Write one byte to specified seesaw register.

void write8(byte regHigh, byte regLow, byte value)

{

this->write(regHigh, regLow, &value, 1);

}

};

Expand full comment
pnad's avatar

Config:

esphome:

name: touch-sensor

includes:

- stemma_soil_sensor.h

libraries:

- Wire

esp32:

board: esp32doit-devkit-v1

framework:

type: arduino

# Enable logging

logger:

# Enable Home Assistant API

api:

encryption:

key: "6IAM8YTPQZGi+ZFWqSq1BN5Tqs04bE25EMdIdof99fk="

ota:

password: "d1405acfc142cf9771243cdd90d677f4"

wifi:

ssid: !secret wifi_ssid

password: !secret wifi_password

# Enable fallback hotspot (captive portal) in case wifi connection fails

ap:

ssid: "Touch-Sensor Fallback Hotspot"

password: "ImveQJvIQvP0"

captive_portal:

sensor:

- platform: custom

lambda: |-

auto soil_sensor = new StemmaSoilSensor();

App.register_component(soil_sensor);

return {soil_sensor->temperature_sensor, soil_sensor->moisture_sensor};

sensors:

- name: "Plant Temperature"

- name: "Plant Moisture"

Expand full comment
Ben's avatar

Hey this works brilliantly! Thanks for putting this together. I'm having trouble getting the update interval to work.

- platform: custom

lambda: |-

auto soil_sensor = new StemmaSoilSensor();

App.register_component(soil_sensor);

return {soil_sensor->temperature_sensor, soil_sensor->moisture_sensor};

sensors:

- name: "Plant Temperature"

- name: "Plant Moisture"

update_interval: 60s

If I included update_interval on the custom sensor, it complains that update_interval isn't a valid option for custom sensor. Any ideas?

Expand full comment
Alistair Young's avatar

Yeah, ESPHome doesn't support that option for custom sensors. (You can see the details here: https://www.esphome.io/components/sensor/custom.html .)

All's not lost, though. For custom sensors which poll, you set the polling/update interval in the source code. If you look in the stemma_sensor_.h file, you'll see this line:

// Constructor

StemmaSoilSensor() : PollingComponent (300000)

{

The number in the constructor there sets the update interval. Changing that to 60000 and recompiling should get you what you want.

Expand full comment
David Beck's avatar

Appreciate your work and you sharing it! Been struggling with cheap moisture sensors for a couple seansons now with generally unsatisfactory results. Wanted to try lady ada's version using your solution into home assistant. It's working but with issues. Only one of every 5 or 10 measurements are valid. It reads nearly max (1015 or so) and then a few values reflective of what the soil condition actually is then back to max. It repeats. A min filter has make it usable to get a fairly steady decline as moisture is decreasing, but it probably isn't intended to work that way. I'm not a programmer so my debug abilites are limited. Seem to be missing something. Got any ideas??

Expand full comment
huskyte's avatar

Hi there, thx a lot. Copied your code and installed the .h in the ESPHome directory, but getting the following error "/config/esphome/solarfeuchte-v01.yaml: In lambda function:

/config/esphome/solarfeuchte-v01.yaml:64:30: error: expected type-specifier before 'StemmaSoilSensor'

auto soil_sensor = new StemmaSoilSensor();

^

/config/esphome/solarfeuchte-v01.yaml:66:76: error: could not convert '{<expression error>, <expression error>}' from '<brace-enclosed initializer list>' to 'std::vector<esphome::sensor::Sensor*>'

return {soil_sensor->temperature_sensor, soil_sensor->moisture_sensor};

^

*** [/data/solarfeuchte-v01/.pioenvs/solarfeuchte-v01/src/main.cpp.o] Error 1"

Any idea? What am I missing?

Expand full comment
Alistair Young's avatar

Unfortunately, I can't seem to reproduce this myself, so I can't say for certain. I've seen some people out there with similar issues with different custom components in ESPHome, but...

If you want to stick your YAML file in a gist and send me a link, I'll see if I can reproduce it with that?

Expand full comment
Et's avatar

@huskyte, I faced a similar problem as I had forgotten to include the c file at the start of the yaml:

```

esphome:

# ... [Other options]

includes:

- stemma_soil_sensor.h

libraries:

- "Wire"

```

Expand full comment
Joe's avatar

Question for you. I'm looking to use this with outdoor compost bins. I don't have power out there, and was thinking I could have the probe mounted to the inside of the bins, and then the board itself mounted on the outside.

Do you think this would work with the setup? I'm assuming I need power to get there, but I really don't know.

Expand full comment
Alistair Young's avatar

I'm doing something similar to that with my indoor plants (probe inside the pot, buried in the soil; board mounted to the outside). With a case to protect the board from wind and weather and a bit of care to make sure the compost doesn't get onto the top of the stick that's not supposed to get wet, I don't see any reason it wouldn't work.

While in theory you can run an ESP8266 off battery - especially if you revise the ESPHome configuration a bit to put it to sleep between measurements, saving power - I haven't tried it myself and can't really speak to it.

The only cautionary note I'd add is that here (Wichita, KS) in summer, the outdoor temperature and sunlight combined have been a pretty big problem for some of my outdoor sensors, heat-wise, and I haven't worked out a great solution for that yet. Hopefully you either have less heat or a nice shady, breezy spot to put the boards in. :)

Expand full comment