project: oil tank level sensor

Make a heating oil tank level sensor that’s more useful than anything you can buy.

  • Measures oil level in any unit – litres, gallons or days until I need to refill the tank
  • Displays readings on a gauge or time graph (uses Home Assistant gauge & history graph).
  • Receive readings or alerts as an email or phone notification or flashing smart bulb in the house.
  • Uses an ultrasound sensor JSN-SR04T Ultrasonic distance measuring sensor, waterproof (~£6 ebay)
  • Uses an ESP32 wifi chip (~ £6 ebay).
  • With a waterproof housing could work outdoors and on battery power.
  • The project with little modification would monitor the salt level in a water softener or the water in a water barrel. (Ref Dr Zzz – youtube)

Some homes use oil as a fuel to keep warm in winter. Over the course of a winter I might need to order two or three deliveries of oil but till now I’ve no way to predict the day when I’ll need to top it up. And I really don’t ever want to run out of oil. The oil is burned in a central heating boiler in the same way that many homes in the UK burn methane gas. Although we call the liquid ‘heating oil’ it’s more runny than the oil you know: it’s more like diesel or white spirits or turps or paraffin or kerosine.

I asked local firm Shelford Heating to quote a price for an oil level sensor. Their solution involved drilling the steel tank to fit an Apollo wireless kit – but £220 seemed excessive for this need. I could also have bought the kit, which sends an approximate oil level to a display but I really wanted something that could alert me better.

How it connects up

A waterproof ultrasonic sensor, looking like a car bumper sensor, arrives with a long lead and a blue board. Female-female Dupont jumper wires will connect the four pins on the board to an ESP32. Two of the pins connect to ~ 5v power and ground. The other two, labelled TRIG and ECHO go to any suitable GPIO pin on the ESP32. I connected them to GPIO 15 and GPIO 16. Thus far we’ve four wires joining an ESP32 and a sensor board. We now need a lead (from a USB charger) to bring 5v power and GND to the ESP32.

(I used an ESP32-CAM instead of an ESP32 with a USB socket. This meant that I needed to hack a USB cable and make the wiring less elegant. I just happened to have a spare ESP-CAM without its camera).

Underside of the home-made oil cap where the ultrasonic sensor sits, pointing at the oil in the tank.
Download the STL model file here

The ultrasound sensor needs to be fixed so that it points down at the surface of the oil in the tank. I used a tank hole that housed the dipstick used for the last 40 years. A suitable cap, as shown below, was made on a 3D printer. You might instead drill a hole for it but I’ve no advice on this.

The sensor installed on an oil tank – it’s messy around here! I lined the sensor holder with sticky felt to ensure it couldn’t slip or shift easily

Program the ESP32

For this step you need an already working Home Assistant installation on a Raspberry Pi as shown in an earlier project. In Home Assistant go to the Esphome plugin and add a new ‘node’. A wizard asks you a few questions after which you can edit your answers. Add the code below but change the items in bold to suit your board, your wifi, your network and the GPIO pins you want to connect to.


   name: oil
   platform: ESP32
   board: esp-wrover-kit
Enable logging
Enable Home Assistant API
   password: "Your HA API password"

   password: "Any password here"

   ssid: "YOUR WIFI SSID"
   password: "YOUR WIFI PASSWORD"
     # Set the IP of the ESP
     # Set this to the IP address of the router. 
     # The subnet of the network

 port: 80
 platform: ultrasonic
 trigger_pin: GPIO15
 echo_pin: GPIO16
 name: "oil level"
 accuracy_decimals: 0
 update_interval: 10s 

The sensor section at the bottom of the code tells the sensor to say how far the sensor is from an object (in cm). The sensor doesn’t work well at distances ~30 cm but at distances up to a couple of metres it’s surprisingly good. A reflective surface nearby might affect your result.

Here is the final code. I call the sensor “oil used” because the number it returns is the amount of oil I need to buy to fill the tank. Next I really don’t need an oil level reading every 10 seconds so I increased the update interval. Lastly, I needed to somehow calibrate the reading so that it gave me a litre volume. My tank has a perfectly regular shape from top to bottom – so by measuring its dipstick, I found that 100 mm of tank depth contains 105.3 litres. The last two lines (filter and lamda) do this calibration. The two lines can be edited or entirely omitted.

UPDATE (January 2020) On the oil level graph below, you’ll notice that the red trace is noisy. On my tank there’s noise when the tank is full (sensor is too close) and when it’s nearly empty (sound bounces inside the tank). The code below now delivers a ‘sliding-average’ of the oil level readings taken over 20 minutes. What the hec, the readings are free.

 platform: ultrasonic
 trigger_pin: GPIO15
 echo_pin: GPIO16
 name: "oil used"
 accuracy_decimals: 0
 update_interval: 60s 
 unit_of_measurement: "litre" 
  - lambda: return x * 1053;
  - sliding_window_moving_average: 
    window_size: 20 
    send_every: 10
# When tank is irregular in shape calibrate the distance into the tank
# SEE THE POST FROM 'Kev' below where we elaborate on the filter section
#  – calibrate_linear:
#  – 110 -> 0
#  – 95 -> 204
#  – 75 -> 544
#  – 55 -> 900
#  – 35 -> 1320
#  – 15 -> 1660
#  – 0 -> 1800

An electrician’s junction box like this fits my ESP32-CAM perfectly. A slightly longer box is needed for an ESP32 Dev kit. (HomeDepot /B&Q / Toolstation)

  • In the ESPhome section of Home Assistant VALIDATE your code and then COMPILE it. The compile operation takes a few minutes as ESPhome assembles the code libraries it needs and creates a firmware ‘.bin’ file.
  • Download this bin file. Flash the bin file using the ESPHome-flasher tool (see here).
  • When the flashing is done, power-up the ESP32 and go to Home Assistant > Integrations > + > ESPHome > add node. You might have to enter the IP address you chose in the code. You’ll then see the oil level entity that has been added to Home Assistant.
  • Finally, go to the Home Assistant overview – the page that displays your stuff – and choose Configure UI in the top corner. You might use a history graph and a gauge to display your oil level.

The code for the items above is shown below. You can edit the bold items to suit.

entity: sensor.oil_level
 max: 1100
 min: 0
 name: heating oil used
   green: 100
   red: 800
   yellow: 600
 theme: default
 type: gauge
entity: sensor.oil_level
 graph: line
 hours_to_show: 96
 name: of 1000 L oil used
 type: sensor
 unit: litres
 entity: sensor.oil_level
 hours_to_show: 24
 refresh_interval: 0
 type: history-graph 

Check your calibration!

The experience of couple of tank refills shows that

  • When the tank is full I see no readings ‘higher’ than 196 litres (180mm from sensor to the oil)
  • When the tank is partly empty I see no readings lower than 760 litres (720mm from sensor) even through the tank holds 1200 litres. This is hard to understand as on the bench the sensor measured beyond a metre. I always watch the oil level graph – and am wary as it plateaus.
  • When the tank level was low the readings were a bit noisy.
  • It was suggested that the ‘oil used’ graph should drop so I made the reading negative. That was lazier (but easier for ordering oil) than measuring the amount of oil left in the tank.
 # To produce a falling oil level, add a negative sign!
   - lambda: return x * -1053; 

So when do I need to order some more oil?

The first nice consequence is that I now know how much oil I need to order and how much I use. In a cold spell two households used 30 litres a day – I’ll be posting my metrics in a future post. And I can see I would need to order an extra 80 litres as it will take 9 days to arrive. As I write I’m thinking about a way to predict the date when I’ll need to order more oil. For the present I’ve put the data in a spreadsheet and it looks like I’ll order in mid January – unless I start to use oil faster.

Create a notification that alerts when oil needs to be ordered

I order 900 litres of oil and I use 100 litres of oil as I wait for it to be delivered. Therefore I need an alert when the tank has 850 – 100 or 750 litres of oil. Home Assistant offers numerous alerts including send an email, show a message in the browser or pop-up a ‘toast’ message on a phone. You can set a notification to persist so that you can’t easily swipe it away. You can set an urgent notification to wake up a phone to tell you, or one that waits quietly until you next pick up the phone. Here is the automation code – if you’re starting out, the easiest method to get working is the email notification:


 alias: heating oil level alert
 above: '750'
 entity_id: sensor.oil_level
 platform: numeric_state
 after: 07:00
 condition: time
   message: at {{ states ('sensor.time') }}
   title: '{{ states (''sensor.oil_level'') }} L now. 850L oil to be ordered'
 service: notify.notify_html5
   message: at {{ states ('sensor.time') }}
   title: '{{ states (''sensor.oil_level'') }} L now. 850L oil to be ordered'

Automatically record your daily readings in a Google spreadsheet using IFTTT to mediate

Update March 2020

For a while it was OK to manually record the daily oil level in a spreadsheet. I soon saw that I was using 80 – 100 litres of oil a week during December. I therefore set up a connection between Home Assistant and IFTTT so that the daily level could be recorded in a spreadsheet. This has been happening daily all through 2020 and hasn’t broken yet.

The code for the automation routine that takes the reading is below. The procedure to link Home Assistant and IFTTT is here: send sensor data from Home Assistant to a spreadsheet

# that is /config/automations/oil_level.yaml
id: '12399999'
alias: oil_level_spreadsheet
description: daily readings in oil_level.yaml
at: "23:55:00"
platform: time
condition: []
data_template: { "event": "oil_level", "value1": "{{ states('sensor.oil_level')}}", "value2": "{{ states('sensor.temperature_weather')}}", "value3": "{{ states('sensor.power_today')}}" }
service: ifttt.trigger

14 Responses

  1. Ash says:

    Excellent write up. Thank you for taking the time to do this. It has helped with my own, very similar, project.

    One thing I am experiencing is timeouts. I see this in the log:

    [09:08:13][D][ultrasonic.sensor:025]: ‘Oil level in cm’ – Distance measurement timed out!
    [09:08:13][D][sensor:092]: ‘Oil level in cm’: Sending state nan cm with 0 decimals of accuracy

    This results in a ‘non-numeric’ error in Home Assistant. Have you experienced this? Any idea of the cause or a solution?

    • roger says:

      I have seen this error. At times I thought I had two many sensors on board so I slimmed down the code; at others I changed the ESP pins used or the ESP itself. Ultimately it seemed to be related to the strained connection of the sensor plug and the board. It went away so I took care not to touch the thing again.

      Generally errors happen everywhere – eg when trying to upload the firmware – so it’s easy to be discouraged. My tips would be a) experiment a lot b) take notes in a book c) buy two of everything if that’s affordable.

  2. Graham says:

    As an avid user of Home Assistant, this is a fantastic help guide for monitoring an oil tank here in the UK, thank you. To save hacking up a USB cable, would something like this board work for the ESP module ? Can you recommend a housing for the ESP and the sensor board?

    • roger says:

      Cheers Graham. Thank you for reminding that some ESP32’s have a USB socket.

      For a housing on this occasion I used a £4 (electronics) project box from ebay. There are dozens in all sizes, with see-through lids and waterproofness. Since then I’ve bought (from Screwfix or Toolstation) electrician’s junction / connection boxes also called chocboxes. This one fits an ESP32-CAM at £1; others are larger and waterproof

  3. Dan says:

    This is excellent. Do you have the plans for the oil cap that you custom printed? I’d love to order one like yours.

    • Douglas Telford says:

      I am just starting a similar project on my oil tank having managed to run out of oil on the coldest days. I don’t have access to a 3D printer and so have been trying to find a housing for the sensor to fit on the tank top pipe. I have found on my 600 Gallon tank a Sunpat Peanut butter jar lid fits the top rather well and will just need to have a hole cut to size for the sensor. I have yet to do this!

      • roger says:

        Sorry to hear this Dan but your/this project is worthwhile. It helps that my sensor cannot be displaced. For sure there’s a jar lid somewhere to suit. I’ve now updated the post with a hint to check the calibration and watch the graph as I now get a graph plateau at 0.72 m from the top. The actual reading therefore can mislead. As 760 litres was the trigger for ordering more oil the issue was never spotted – but something may have changed in the tank/sensor. I’ll investigate that as well as use collected data to somehow predict when oil might need a visual check. In the meantime thanks for reminding on a vital issue. (And while I ordered oil ahead of disaster last week, suppliers were offering dates three weeks ahead so an emergency delivery had to be bought).

  4. Dan says:

    Regarding the non-numeric issue that Ash inquired about; the fix is to add a filter to your code that ignores non-numeric readings. I added it after the lambda, so the code in your demo would be:
    – lambda: return x * 1053;
    – filter_out: nan

    • roger says:

      Oh thank you so much! I wish I’d known about that (– filter_out: nan) as a way to remove non-zero readings should they occur.

      There’s another need for a filter in my TRAIN TIMES* project. The Home Assistant overview shows (something like “Non-numeric” or “Not available”) after midnight when there are no local times. A filter is probably the answer.

      I’m currently posting projects on using unmodified Sonoff devices; using RFlink and 433MHz sensors. Pop back should these be of value. Thank you again Dan.


  5. sooty says:

    Hi Roger – Sincere thanks for posting this!

    I’ve now got a cost effective remote Oil Monitoring system for my overseas vacation home. In the past I have always been somewhat concerned not being able to see how much oil remains in the tank as we go through the cold winter months…

    • roger says:

      Thank you too for your feedback Lance and best wishes from here. Wow … measuring oil levels remotely offers a big gain. Thinking about the oil tank level as you’ll realise is an unnecessary worry. Even ordering oil is easier when you know you use say, 100 litres in a week. I will soon be posting a very easy project to constantly display the oil level on a TTGO T-Display.

  6. Youtube Mr12v says:

    Hi Roger. Thanks for the great write up on the oil level sensor. I have replaced my broken Apollo sensor with this setup so I can integrate it into my home assistant. I just thought I’d share here about the calibration that I used to convert the distance reading of the sensor to approximate oil in litres. ESPHome has a calibrate_linear function. Doing it this way let me calibrate better to my cylindrical shaped tank (1800 litres) and have a countdown of litres remaining instead of counting oil used. This would obviously work on a square tank too with much easier values! I have 1000 litres of oil coming next week so I’ll be able to test to see how accurate the readings are and adjust accordingly. The filters code for the sensor is below:

    – lambda: return x * 100;
    – filter_out: nan
    – sliding_window_moving_average:
    send_first_at: 7
    window_size: 7
    send_every: 7
    – calibrate_linear:
    # # Map 0.0 (from sensor) to 0.0 (true value)
    – 110 -> 0
    – 95 -> 204
    – 75 -> 544
    – 55 -> 900
    – 35 -> 1320
    – 15 -> 1660
    – 0 -> 1800

    I hope this provides some use to the readers of this blog! Keep up the good work!
    Best regards

    • roger says:

      Thank you. That’s a brilliant use of a feature that I couldn’t quite get myself. I see your need for a calibration table. I’ll guess that you’ve worked that from the volume of a regular cylinder while my tank was a steel box. As an obsessive measurer I’m stuck on this *** do please let’s know if you find a solution!
      o Keep your eye on the oil graph and if the readings plateaux, as mine seemed to, investigate the range of this sensor.
      o Take oil and average temperature readings each night for a spreadsheet using IFTT
      o Create a daily oil use (in litres per day) graph (still working on this) ***
      o Use an hour meter or electric monitoring smart plug to monitor boiler burning time

Leave a Reply

Your email address will not be published. Required fields are marked *