Oil Tank Level Sensor

Recent project that a combination of HA and ESPHome made much easier than I expected. Central heating is oil fired - and our tank has no sight tube. When installed it did have a remote level sensor, but the batteries in the sender died a while back and I hadn’t got round to replacing them - and the inside element was a rather bulky ugly plug.

Time for a proper sensor…a waterproof SR04 compatible sensor TE501 , a D1 mini and a 7 Neopixel disc (left over from previous projects) makes a compact oil level sensor.

SR04 was mounted in the filler cap with plenty of hot glue to seal. D1Mini (powered by an old USB charger) and the Neopixels are in a water proof box on the adjacent garage. The neopixels are bright enough to shine through without a problem. I added a DS1820 temp sensor because why not.

The d1mini converts the distance to a % - I dont allow for the odd shape of the tank just a simple % of distance to empty and luckily that is more or less exactly 25cm to 125cm for this tank so its very simple.

HA converts the % fill to an “OK”/“Alert”/“Critical” badge and turns on the light/sets color when it changes status via NodeRed. The light is also turned for a few minutes when the door/gate adjacent is opened as a reminder of current state.

Capture

Testing…

And Installed…

ESPHome yaml

esphomeyaml:
  name: esp_oilsensor
  platform: ESP8266
  board: d1_mini

wifi:
  domain: .local
  ssid: xxxxx
  password: xxxxxxx
  manual_ip:
    static_ip: 192.168.1.50
    gateway: 192.168.1.254
    subnet: 255.255.255.0

# Enable logging
logger:
# Enable Home Assistant API
api:

ota:

substitutions:
# Unique ID
  esp_id: "esp_oil"

dallas:
  - pin: D1

sensor:
  - platform: ultrasonic
    trigger_pin: D6
    echo_pin: D5
    name: "Oil Tank SR04"
    unit_of_measurement: "cm"
    id: ${esp_id}_sr04
    update_interval: 5s
    accuracy_decimals: 1
    filters:
      - multiply: 100
      - sliding_window_moving_average:
          window_size: 12
          send_every: 12
  - platform: dallas
    address: 0x890008008D53E610
    name: "Oil Tank Temperature"
    id: ${esp_id}_temp
  - platform: template
    name: "Oil Tank Level"
    id: ${esp_id}_level
    accuracy_decimals: 0
    unit_of_measurement: "%"
    icon: "mdi:oil"
    lambda: |-
      if (id(${esp_id}_sr04).state > 160 ) {
        return 999;
      } else if (id(${esp_id}_sr04).state > 125) {
        return 0;
      } else {
        return (125 - (int) id(${esp_id}_sr04).state);
      }
    update_interval: 30s
    

light:
  - platform: neopixelbus
    name: "Oil Tank Light"
    id: ${esp_id}_light
    type: GRB
    method: BIT_BANG
    default_transition_length: 0s
    pin: D2
    num_leds: 7

HA Template Sensor

  - platform: template
    sensors:
      oil_status:
        friendly_name: "Oil Status"
        unit_of_measurement: ""
        value_template: >
          {% if states.sensor.oil_tank_level.state == 999 %}
            Error
          {% elif states.sensor.oil_tank_level.state | int > 15 %}
            OK
          {% elif states.sensor.oil_tank_level.state | int > 7 %}
            Warning
          {% else %} 
            Alert
          {% endif %}
        icon_template: >
          {% if states.sensor.oil_tank_level.state == 999 %}
            mdi:alert-decagram
          {% elif states.sensor.oil_tank_level.state | int > 15 %}
            mdi:check-circle-outline
          {% elif states.sensor.oil_tank_level.state | int > 7 %}
            mdi:alert-circle-outline
          {% else %} 
            mdi:alert-circle
          {% endif %}
        entity_picture_template: >-
          {% if states.sensor.oil_tank_level.state == 999 %}
            /local/cancel.png
          {% elif states.sensor.oil_tank_level.state | int > 15 %}
            /local/ok.png
          {% elif states.sensor.oil_tank_level.state | int > 7 %}
            /local/warning-shield.png
          {% else %} 
            /local/high-priority.png
          {% endif %}
10 Likes

Great write-up, waiting on similar items for a esp32.

Would you have a schematic of how the TE501 and d1mini go together?
Do you need the board that comes with the TE501?

Yes you need the little board. That’s then just 4 pins to the D1mini 5v,Ground, echo and trigger. The latter go straight to a pair of GPIO and you just tell ESPhome which 2 pins you’ve used.

2 Likes

Hi,

Thanks for posting, this is what i am looking to build for a watertank… (just without the light).

I have only recently been introduced to ESPHome (have a lot of components running MQTT), and would like to build your solution. Could i please ask for your guidance and assistance which others could potentially benefit from as well.

  1. I can work out how to get esphome onto the D1Mini (youtube is a lot of help here), and i am guessing the Template Sensor will slot into my configuration.yaml file. Is this all the config which is needed?

  2. Could you please post a schematic of your wiring and the components you used, as i can see what look like two resistors on the breadboard which i need help with.

Thank you in advance for all of your help

Michael

Hi Mikkaat, I’m doing the same project - just waiting on a single part to finalise it. Here’s the resources I used:

Water Softener Salt Level w/ ESPhome & Using a Tablet as a Network Camera - this video shows the D1 Mini and ultrasonic for a similar purpose - start watching at 18:23 I think.

https://randomnerdtutorials.com/power-esp32-esp8266-solar-panels-battery-level-monitoring/ - this is if you want to use a solar setup rather than powered.

And here is my YAML:

esphome:
  name: water_tank_sensor
  platform: ESP8266
  board: d1_mini

wifi:
  ssid: "****"
  password: "****"
  fast_connect: on

# Enable logging
logger:

mqtt:
  broker: 192.168.1.39
  username: ****
  password: ****
  birth_message:
    topic: water_tank_level_sensor/state
    payload: online
  will_message:
    topic: water_tank_level_sensor/state
    payload: online

ota:

sensor:
  - platform: ultrasonic
    trigger_pin: D1
    echo_pin: D2
    name: "Water Tank Level Sensor"
    update_interval: 3h
    filters:
      - offset: -0.15
      - lambda: return (1.0 - (x/2.0)) * 100.0;
    unit_of_measurement: "%"

deep_sleep:
  run_duration: 30s
  sleep_duration: 3h
2 Likes

Thank you… will give this a try and let you know how I go.

Thanks to @DeanoX for the inspiration and super simple level sensor.
Up and running @ < €15

Using ESPhome makes it super simple.

Wiring if just level sensor like mine is 4 wires, no resistors etc needed
echo, trigger, vcc and gnd

Many thanks for this @DeanoX! Managed to get it incorporated into Home Assistant without the LED light with no issues apart from a few niggling things - Now I can rest assured that we will never run out of Oil for Heating and Hot Water!

36
The Oil Tank level graph always shows a value of 0 which is I think what it uses for the OK/Warning, is there a way for this to show the percentage graph which clicking on the Oil Tank Level indicator? Funnily enough in the History state, it does show the oil level in percentage.

I just need to wire in the thermometer, didn’t have the required resistor to wire that in, so just awaiting that arriving and it will be fully commissioned.

Again thanks for this, was a breeze to setup!

FWIW, I have this implemented with ESP32, ESPHome and HC-SR04. Getting readings for water tanks as well as my salt softener.

The code in ESPHome is as follows (for the unit measuring 2 water tanks)

esphome:
  name: esp32_2
  platform: ESP32
  board: nodemcu-32s

wifi:
  ssid: "XXXXXXXX"
  password: "XXXXXXXX"

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

sensor:
  - platform: ultrasonic
    trigger_pin: GPIO19
    echo_pin: GPIO18
    name: "House Water"
    accuracy_decimals: 0
    unit_of_measurement: "%"
    update_interval: 30min
    filters:
      - filter_out: nan
      - lambda: return (2.03-x+0.23)/2.03*100;
  - platform: ultrasonic
    trigger_pin: GPIO23
    echo_pin: GPIO22
    name: "Irrigation Water"
    accuracy_decimals: 0
    unit_of_measurement: "%"
    update_interval: 30min
    filters:
      - filter_out: nan
      - lambda: return (1.78-x+0.23)/1.78*100;

In the “lambda” calculation, 2.03 (and 1.78) is the height of my water tanks in meters. 0.23m is the distance between the highest water level and the sensor. From there i calculate roughly the % full right in the NodeMCU.

In lovelace, it looks like this :

Hope this helps ! :slight_smile:

I have had this on for a few months now and it’s been doing its job. One sensor failed (looked rusted a bit) a few weeks back and i have had to replace it…If this happens too often i will look at the suggestion above to replace them with the TE 501 (thanks for the tip !)

why don’t you calibrate the measured distance of the sensor since you have installed also a temp sensor? SR04 always assumes speed of sound to be 340m/s and calculates range with time * Speed / 2
Did something similar but i haven’t installed yet a temp sensor in the tank… I use for now exterior temp from weather sensor:
measured SR04T distance:
{{states(‘sensor.ch_oil_sr04_distance’) }}

calibrated_sr_distance:
friendly_name: “Calibrated oil level”
unit_of_measurement: ‘cm’
value_template: >-
{% set value = (states(‘sensor.ch_oil_sr04_distance’) | float) /340 * (331+0.6* state_attr(‘weather.home’, ‘temperature’)) %}
{{ ‘{:.2f}’.format(value) }}

calibated_oil_quantity:
friendly_name: “Real Oil quantity in tank”
unit_of_measurement: ‘litres’
icon_template: mdi:car-coolant-level
value_template: >-
{% set value =(93.000 - states(‘sensor.calibrated_sr_distance’) | float | round (1) )*10 %}
{{ ‘{:.0f}’.format(value) }}

oil_quantity:
friendly_name: “Oil quantity in tank”
unit_of_measurement: ‘litres’
icon_template: mdi:car-coolant-level
value_template: >-
{% set value =(93.000 - states(‘sensor.ch_oil_sr04_distance’) | float | round (1) )*10 %}
{{ ‘{:.0f}’.format(value) }}

Hi, I tried to use your formula to calculate the level, but the problem is if the level falls to the bottom, then it still shows + - 5%. I have different dimensions but the principle is the same (1.43-x + 0.08) /1.43x100.
If the level is full (1.43-0,08 + 0,08) /1.43x100, it is beautiful 100% but if it falls to the bottom (1.43-1.43 + 0,08) /1.43x100 the result is 5,594% which is wrong . I haven’t improved the formula, but can you look at it? Thanks

Hi @Jiri_Stransky : let me understand your case -
Your sensor is placed 0.08 over the maximum level of liquid?
The Maximum level of liquid is 1.43 between zero and full ?
If that is the case, then if the liquid goes to zero, the sensor will measure a distance of 1.43+0.08 or 1.51…right ? So (1.43-x+0.08)/ 1.43*100 becomes (1.43-1.51+0.08) and that is zero…

Makes sense ?

1 Like

My sensor is located 0.08 above the maximum level. 1.43 is the height where the sensor is located, so the maximum level is 1.43 - 0.08. So I probably didn’t understand in the description that this number is the height of your water tanks. I understood it as the height of your tank. So it’s the height of your water column.
I just tried this formula as you described it and it works. So thank you.
Otherwise one person from the Internet advised me one more elegant solution and that use this filter, which calculates everything automatically.
filters:

      - calibrate_linear:
        - 0 -> 0
        - 6 -> 4000

And then another came up with this formula:
value_template: '{{(1 - ((value_json ["SR04")] "Float-8) / 143)) | multiply (100) round (0)}} '

It may be useful. Otherwise, for example, I’m still sending my code, where I converted everything into liters and for sure I measure the level in cm oil and the distance of the sensor to the surface.

# Ultrasonic sensor - tank measurement
sensor:
  [platform]: ultrasonic
    id: sensor_cm1
    trigger_pin: D1
    echo_pin: D2
    name: "Sensor1"
    icon: "mdi: arrow"
    update_interval: 10s
    accuracy_decimals: 2
    unit_of_measurement: "cm"
    filters:
      filter_out: nan
      - multiply: 100

# Example configuration entry
  - Platform: template
    name: "Oil level1"
    icon: "mdi: arrow"
    update_interval: 10s
    accuracy_decimals: 2
    unit_of_measurement: "cm"
    lambda: -
      return 143- id (sensor_cm1) .state;

# Example configuration entry
  - Platform: template
    name: "Oil status1"
    icon: "mdi: water"
    update_interval: 10s
    accuracy_decimals: 2
    unit_of_measurement: "%"
    lambda: -
      return id (sensor_cm1) .state;
    filters:
      - calibrate_linear:
        - 0 -> 0
        - 6 -> 100

# Example configuration entry
  - Platform: template
    name: "Oil liter1"
    icon: "mdi: oil"
    update_interval: 10s
    accuracy_decimals: 0
    unit_of_measurement: "l"
    lambda: -
      return id (sensor_cm1) .state;
    filters:
      - calibrate_linear:
        - 0 -> 0
        - 6 -> 4000
1 Like

@ DeanoX
Did the sensor survive the oiltank longterm?

Still going strong - been through a couple of cycles of tank fill/empty. I wish it was a better / more steady reading as it can fluctuate. But haven’t got round to improving the smoothing yet.

1 Like

Is it possube to run HC sr04 from 3 pins only using Esphome?

found useful link for a couple of oil tank calibration info
https://www.alnevillage.co.uk/oil_tank_information.html

I have it working, but readings go from 1.2m to 1.02m in height. with results in 38% to 51% in the readings.
Anyway to do an average sensor or something like that?
How do you calibrate the screw on the ultrasound PCB?
any other thing to improve readings?

I’m finding similar… hours of accurate readings followed by hours of inaccurate short readings.

  • there are no obvious physical obstructions
  • tried alternate power supplies but no change.
  • I’ve tried to protect the sensor from damp - but still a possible cause.
  • recent cold snap may be a factor - no strong evidence of better performance in warmth!
  • Was near empty (1m reading) with initial findings; after a fill (0.2m) still seeing same inconsistencies.

I guess the next step is to try replace the sensor

In my case it is just inaccurate by seconds, trying to use the “filter” sensor to clean it up… but havnt quite got into the filtering part with success :S