Here is an example of how to measure the level of a rainwater tank using a pressure sensor.
My inspiration comes from here.
For my 5000L tank with a height of 160cm I ordered a pressure sensor that measures 2m height, with a cable of 10m. It transmits the data with a 4-20mA current loop. The cable contains 2 wires and also a small tube to compensate the atmospheric pressure.
I use an ESP32 board, because it is cheap, fast and easy to program with Esphome. The Esphome binding from @Seime makes it very easy to communicate with OH.
Here is the layout of the 8 x 12 cm PCB board I made:
The module on the right bottom is a voltage booster that transforms the 5V to 24V. This has to be adjusted with the potentiometer.
The module on the left bottom provides the current loop.
- The two jumpers must be removed to provide a measuring range from 0 to 3,3V.
- The zero potentiometer must be adjusted so that the measured voltage (green wire) is 0V when the pressure sensor is out of the water (empty rainwater tank).
- I adjusted the span potentiometer to maximum (turn clockwise): if the tank is full (160cm), then I want to have as much volts as possible.
The module in the middle is a 16 bit ADC converter. It measures the voltage precisely and with low noise. The result is sent to the ESP32 via a I2C bus. The default address of the module is 0X48. The ESP32 can also measure voltages, but the linearity is not good and there is a lot of noise (variations of the measured level).
The small module on top left has nothing to do with this project. It is a BME280 board that can measure air pressure, temperature and humidity in my cellar. It is also connected with the I2C bus on address 0x76.
Here is the Esphome code:
esphome:
name: esp3
comment: esp32 controller for rainwater tank
esp32:
board: az-delivery-devkit-v4
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API (to be able to send commands; not necessary here)
api:
encryption:
key: !secret encryption_key
ota:
# to send over the air updates to the esp32
- platform: esphome
password: !secret ota_pw
time:
# synchronise time from OH
- platform: homeassistant
id: openhab_time
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_pw
fast_connect: true # necessary for hidden networks,
# faster for other networks as there is no scanning
# esp3 with a manual ip address
manual_ip:
static_ip: 192.168.1.20
gateway: 192.168.1.1
subnet: 255.255.255.0
# i2c initialisation for BME280 and ADS1115
i2c:
id: bus_a
sda: GPIO21
scl: GPIO22
scan: True
# ADC initialization, ADDR pin is not connected, so address is 0x48
ads1115:
- address: 0x48
sensor:
# ADC channel A0 voltage measurement
- platform: ads1115
multiplexer: 'A0_GND' # measure between pin A0 and GND
gain: 4.096 # measure maximum 4.096V
name: "niveauV"
id: niveau_V
update_interval: 5s
filters:
- median:
window_size: 61 # mediaan of 61 measurements
send_every: 61 # every 5*61 = 365s
# convert voltage to cm
- platform: copy
source_id: niveau_V
name: "niveauCm"
id: niveau_cm
filters:
- calibrate_linear:
- 0.115 -> 6 # the sensor is at about 6cm above the bottom = 200L
- 2.415 -> 160 # the height of the tank is 160cm
- round: 1
accuracy_decimals: 0
unit_of_measurement: cm
# convert voltage to liter
- platform: copy
source_id: niveau_V
name: "niveauL"
filters:
- calibrate_linear:
- 0.115 -> 200
- 2.415 -> 5000
- round: 0
accuracy_decimals: 0
unit_of_measurement: l
# BME280 air pressure and humidity measurement
- platform: bme280_i2c
address: 0x76
pressure:
name: "luchtdruk"
filters:
- offset: 6.5
- round: 1
humidity:
name: "kelderVochtigheid"
filters:
- round: 0
update_interval: 60s
The measurement was still a bit noisy, so in the code I added a median filter:
- Every 5s a measurement is made.
- After 61 measurements the median value is taken (has to be uneven otherwise there is no median).
- This is sent every 61 measurements or about 5 minutes.
Now the noise is less than 1mm or about 3L. This gives a stable reading:
The channels used for the rainwater tank are:
- niveauV: the measured voltage
- niveauCm: the corresponding water level in cm
- niveauL: the level in liters
I used a widget to show the level:
uid: regenwaterput
tags: []
props:
parameters:
- default: Regenwaterput
description: Title
label: Title
name: title
required: false
type: TEXT
- context: item
default: regenwaterL
description: Item that contains the level in %
label: Level Item
name: levelItem
required: true
type: TEXT
- default: blue
description: "Fill color: red, blue,... or rgb(200,10,65) or '#ff0066'"
label: Fill color
name: fillColor
required: false
type: TEXT
- default: Regenwater
description: Text label abover the tank
label: tankLabel
name: tankLabel
required: false
type: TEXT
- default: 110px
description: "Height in px (needed for responsive layout)"
label: Height
name: height
required: false
type: TEXT
- default: "160"
description: "Width (no units)"
label: Width
name: width
required: false
type: TEXT
parameterGroups: []
timestamp: Mar 2, 2025, 6:31:10 PM
component: f7-card
config:
outline: true
style:
background-color: "#1c1c1d"
margin-left: 0px
margin-right: 0px
noShadow: true
padding: 0px
title: =props.title
slots:
default:
- component: f7-block
config:
style:
display: flex
height: =props.height
justify-content: center
margin-left: 0px
margin-right: 0px
padding: 0px
slots:
default:
- component: svg
config:
preserveAspectRatio: xMidYMin meet
viewBox: ='0 0 180 160'
xlmns: http://www.w3.org/2000/svg
slots:
default:
- component: defs
slots:
default:
- component: linearGradient
config:
comment: the id can only be used once in a page; so we have to make it unique if
we want to use the widget more than once
id: ='Grad'+props.levelItem
x1: 0%
x2: 0%
y1: 100%
y2: 0%
slots:
default:
- component: stop
config:
comment: in order to have a hard line, both stops must use the same offset
offset: =items[props.levelItem].numericState / 50 + '%'
stop-color: =items[props.levelItem].numericState>500?'blue':'red'
- component: stop
config:
comment: the upper part is transparent
offset: =items[props.levelItem].numericState / 50 + '%'
stop-color: rgba(0,0,0,0)
- component: animate
config:
attributeName: y2
dur: 1s
from: 100%
repeatCount: 1
to: 0%
- component: path
config:
d: ='M 30 20 q -10 0 -10 10 l 0 100 q 0 10 10 10 l 100 0 q 10 0 10 -10 l 0 -100
q 0 -10 -10 -10 l -100 0 m -10 24 l 10 0 m -10 24 l 10 0 m
-10 24 l 10 0 m -10 24 l 10 0'
fill: ='url(#Grad'+props.levelItem+')'
stroke: =themeOptions.dark=='dark'?'white':'black'
stroke-width: 2
- component: oh-label-card
config:
action: navigate
actionPage: page:regenwaterL
fontSize: 18px
label: =items[props.levelItem].numericState + ' L'
stylesheet: |
.item-inner {
margin-top: 30px;
margin-bottom: 0px;
}
.card-content-padding {
padding: 0px;
}
The only drawback is, that you need to do some digging and drilling to install the pressure sensor in the tank. There is no OH binding yet for this…