Simple and Cheap Water Level Sensor

Hello everyone!

Here’s my take on a simple and cheap water level sensor. I’m using this in a camper/RV build. I know I’m not the first person to do this. Nonetheless, hope this helps someone!

The Water Level Sensor described here features a 0-40kPa sensor, 2 physical buttons for calibrating the 0% and 100% levels, and a small OLED display to show the level. This will report report percentage and raw values to Home Assistant and exposes buttons for setting the 0% and 100% levels as well. However, it is not necessary to hook it up to HA. The device is also built on an ESP8266 board that sports 2 relays and 12V power input. I don’t use the relays in this project, but you could!

Parts:

3D printed housing:
This housing holds the ESP8266 relay board, pressure sensor, and OLED display.


Here’s a link to the housing design in Onshape: Water Level Sensor.
You can export an STL for printing directly from that link, just right click on the Top and Bottom Housing Part in the bottom left and select Export. No user account is needed.

Note that the I2C OLED display is still being sold by Adafruit, but they have limited stock. If you buy a different product, you may need to adjust the housing design.

Steps for programming:

  1. Connect your FTDI to GND, RX<->TX, and TX<->RX.
  2. Set your FTDI for 3v programming, not 5v.
  3. Leave the FTDI VCC disconnected.
  4. Connect an external power supply to the board, such as a 12V source. I like to have a switch so I can easily power-on the device.
  5. Important: Add a jumper between GND pin and IO0.
  6. Load the YAML into your ESPHome web interface.
  7. Hold the button on the board.
  8. Connect power to the board.
  9. Connect FTDI to your PC.
  10. In ESPHome, click install via wired connection and select the COM port.
  11. Release the button on the board.
  12. The upload should begin. If not, try again.
  13. Power off the board and disconnect the FTDI.
  14. Remove the IO0 jumper.
  15. Plug it back in and you are done!

My ESPHome YAML configuration:

esphome:
  name: water-level-sensor
  friendly_name: Water Level Sensor

esp8266:
  board: esp01_1m
  restore_from_flash: True

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_key

ota:
  - platform: esphome
    password: !secret ota_password

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Water-Level-Sensor"
    password: !secret fallback_password

captive_portal:
    
# Note: a jumper is required between IO5 and RY1, IO4 and RY2.
switch:
  - platform: gpio
    pin: GPIO5   
    name: "Relay 1" 
    id: R1
    restore_mode: ALWAYS_OFF
  - platform: gpio
    pin: GPIO4   
    name: "Relay 2" 
    id: R2
    restore_mode: ALWAYS_OFF

sensor:
  - platform: hx711
    name: "Raw Value"
    id: raw_value
    dout_pin: GPIO2
    clk_pin: GPIO15
    gain: 128
    update_interval: 60s
    on_value: 
      - sensor.template.publish:
          id: water_level
          state: !lambda "return 100.0*((x-id(zero_value))/(id(full_value)-id(zero_value)));"
  - platform: template
    name: "Water Level"
    id: water_level
    unit_of_measurement: percent

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO1
      mode:
        input:  True
        pullup: True
    name: "Tare Zero Button"
    id: tzb
    internal: True
    on_press: 
      then: 
        - button.press: tarezero
  - platform: gpio
    pin:
      number: GPIO3
      mode:
        input:  True
        pullup: True
    name: "Tare Full Button"
    id: tfb
    internal: True
    on_press: 
      then: 
        - button.press: tarefull

globals:
   - id: zero_value
     type: int
     restore_value: True
     initial_value: '-347000'
   - id: full_value
     type: int
     restore_value: True
     initial_value: '2400000'

button:
  - platform: template
    name: "Tare Zero"
    id: tarezero
    on_press:
      then:
        - lambda: |-
            id(zero_value) = id(raw_value).state;     
  - platform: template
    name: "Tare Full"
    id: tarefull
    on_press:
      then:
        - lambda: |-
            id(full_value) = id(raw_value).state;

i2c:
  sda: GPIO13
  scl: GPIO12

display:
  - platform: ssd1306_i2c
    model: "SSD1306 128x32"
    reset_pin: GPIO14
    address: 0x3C
    update_interval: 60s
    lambda: |-
      it.print(0, 0, id(roboto_16), "WATER");
      it.print(0, 16, id(roboto_16), "LEVEL");
      it.printf(127, 0, id(roboto_32), TextAlign::TOP_RIGHT, "%.0f%%", id(water_level).state);

font:
  - file:
      type: gfonts
      family: Roboto
    id: roboto_16
    size: 16
  - file:
      type: gfonts
      family: Roboto
    id: roboto_32
    size: 32

It exposes these entities in Home Assistant:

Issues I had along the way:

First, the initial batch of devices I ordered, well they leaked air slowly, which caused what appeared to be sensor drift. Just to confirm the sensor was leaking, I submerged it in water and looked for bubbles. The pressure head was only about 1 meter or 1.4 psi. You can see the small bubble slowly forming in this photo, indicating a leak.

It is critical that the plumbing and device do not leak. I would not recommend using this measurement technique in a critical application or inaccessible location. I ordered a second batch from a different reseller and they did not leak. The leaky units were sold by DWEII and the good units were sold by ATNSINC on Amazon. YMMV.

There is one other complication with using this technique to measure head pressure. At first, I figured I could just submerge the small tube (4mm OD, 2.5mm ID) to sense pressure at the bottom of the tank. That appears to work at first, but as the level in the tank slowly changes, it was not reporting a change in pressure. Sometimes the pressure change was happening in an oddly stepwise fashion. Turns out, surface tension of the water tends to keep it stationary in the tube. It resists small pressure changes simply by sticking to the walls. A quick google search indicated a tube needs to be something like 11mm in diameter to prevent surface tension from retaining the water. (Of course, we all know this phenomena too well. Any time you’ve ever lifted water out of a glass by placing your finger over the end of a straw. It is the same.) So anyways, I affixed the 4mm tube to the top of a 1" diameter pipe with a push-to-connect fitting. Any time the tank is fully emptied, the 1" pipe is also emptied. So the measurement can be zeroed at this time if needed.

IMPORTANT: read the paragraph above. You cannot simply submerge the 4mm tube and expect reliable measurements.

Of course, there are many other ways to measure water level in a tank. Many people have posted their solutions. If you want to spend a little more money, you could use a drop-in submerged pressure sensor like in this post, or use a threaded pressure sensor if you have a port that is located at the bottom of the tank.

Here’s a bonus picture of the prototype before I designed the housing for it:

Good luck making your own!

2 Likes

Is the sensor end differential with respect to atmospheric pressure?

If not, you should need to adjust for that too.

Hey @tom_l, great question. We wouldn’t want our measured “water level” to change when we drive the camper up into the mountains, right?

So what we want in this application is a gauge pressure sensor, which measures the pressure relative to external atmosphere. Most people are familiar with gauge pressure sensors, which read 0 psi when they are just open to the atmosphere.

Differential pressure sensors are very similar to gauge pressure sensors, except the reference pressure is not external atmosphere, but instead a second connected pressure source. These types of sensors are used to measure the difference in pressure. For example, differential pressure sensors are sometimes used to measure the pressure increase across an air filter in your home. One side of the pressure sensor measures upstream of the filter, and one side measures downstream. You are measuring the difference in pressure. Once it reaches a certain threshold, you change the filter. A differential pressure sensor could be used in this application, but you would want to leave the second pressure port open to atmospheric pressure, hence it would just act like a simple gauge pressure sensor.

We don’t want an absolute pressure sensor. These types of sensors measure pressure relative to a fixed pressure, which is often a vacuum. At sea-level, an absolute pressure sensor would read 14.7 psi (1 atmosphere, 1 bar, or 100 kPa).

Here’s a diagram I made to help explain the concept with a picture that is relevant to our situation:

All pressure sensors measure the difference in forces acting on each side of a diaphragm. The diaphragm usually has strain gauges on it to measure the force acting on it. In the case of the gauge pressure sensor the forces are (atmospheric pressure + hydrostatic pressure) on one side and (atmospheric pressure) on the other side. So you can see, if the atmospheric pressure changes, it does not change measured difference in pressure. The measured hydrostatic pressure will remain the same.

But, how do we know this is a gauge pressure sensor and not an absolute pressure sensor? Well, for starters, absolute pressure sensors are more expensive because manufacturing a sealed reference volume is more difficult.

Just to be sure however, I whipped up a test:
I assembled the prototype and powered it with a 9V battery.
I then put it in a jar and pulled a partial vacuum on it, equal to 0.5 atmospheres or standing on top of Mt Kilimanjaro.
I left the pressure sensor port open inside the jar, so it is also sensing the reduced pressure.
If it is a gauge pressure sensor, the measurement will not change.
If it is an absolute pressure sensor, the measurement will change.
Conclusion: The measurement did not change as the pressure was reduced, therefore this is a gauge pressure transducer, and it is appropriate for our application. Cool.

Here’s a fun picture of the test setup:


You can see the vacuum gauge sitting there at -50 kPa (gauge pressure again aha!), while the measured pressure remains constant.

I also found and fixed a small bug in the yaml, and added some detail about how to program this board.

1 Like