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:
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.
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?
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.
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??
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*>'
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?
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.
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. :)
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
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.
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"
```
That does not work for me...
Could you share your yaml and .h?
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.
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:
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?
#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);
}
};
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"
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?
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.
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??
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?
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?
@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"
```
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.
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. :)