DIY Heating Oil Sensor Solution

Hi All,

I wanted to share my solution for automating/tracking my heating oil tank level and consumption since I had to go outside in the cold to manually check the level. Yes, I know its lazy but isn’t that the whole point of home automation??? Just got it installed and it’s working pretty well but I’m sure there is much room for improvement.

Sensor hardware is an m5stack AtomS3 Lite with a VL53L1X laser time of flight sensor. The ToF sensor seems to be reading the oil level in the tank quite reliably. I looked into using all sorts of sensors to read the oil level (capacitive, ultrasonic, pressure, etc) but the laser seemed the easiest to install and most reliable.

I also designed a 3D printed cap that holds the hardware and screws into the 2” NPT bung at the top of the tank. I made it out of ASA which seems to be the best for outdoor UV exposure. The threads didn’t turn out great so I ended up caulking around the cap to be sure no water made it into the tank. I’m pretty inexperienced with 3d printing so there may be a way to improve the threads and make a water tight seal without the caulk. I’m working on uploading the model somewhere to share it so I’ll update this post when I get it uploaded (don’t think I can directly upload the file here). Holds the hardware neatly and allows for the power cord to pass through.

I experimented with using a battery to power the ESP32 and enabling deep sleep and MQTT reporting but even updating once per day, it didn’t seem like it would last all that long on a 200mAh battery. My HVAC unit isn’t too far away so I ended up just getting a 240VAC to 5VDC DIN power supply to power the esp32 from the outdoor unit. Made the yaml and reporting much easier. It would probably work with a bigger battery if you had to though. Here’s the final installation.

On the software side, the esphome yaml code reports the raw sensor measurement which is the distance from the top of the tank to the oil level. To get the actual oil level, you have to subtract that measurement from the height of your tank (in my case, about 43 inches or 1067mm). I then convert the oil level to volume by using a lookup table for my specific tank chart (275gal vertical tank). I am not a software developer in real life so I enlisted the help of my good buddy Claude to make this work so code may not be the best but seems to work great. Finally, I have a template sensor that converts the volume to percent oil remaining in the tank for use in automating low level alerts. Unfortunately, ESPHome doesn’t support the VL53L1X ToF sensor out of the box but someone has already created an external component that works great. Some of the yaml code below is left over from when I was trying to get the battery to work (such as static up and the substitutions) so not every line is required.


substitutions:
  internal_mode: True
  device_topic: OilLevel

esphome:
  name: oil-level-sensor
  friendly_name: Oil Level Sensor

esp32:
  board: m5stack-atoms3
  framework:
    type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "apikey"

ota:
  - platform: esphome
    password: "otapass"

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

  manual_ip: 
    static_ip: 192.168.1.92
    gateway: 192.168.1.1
    subnet: 255.255.255.0

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Oil-Level-Sensor"
    password: "fallbackpassword"

external_components:
  - source: github://mrtoy-me/esphome-vl53l1x@main
    components: [ vl53l1x ]
    refresh: 0s

i2c:
  sda: GPIO2
  scl: GPIO1
  frequency: 400kHz
  scan: True

i2c_device:
  - id: ADC_Sensor
    address: 0x29

sensor:
  - platform: vl53l1x
    distance_mode: long
    distance:
      name: Raw Distance
      id: raw_distance
      on_value:
        then:
          - component.update: distance_measurement
          - component.update: remaining_gallons
          - component.update: remaining_percent
    range_status:
      name: Range Status
      id: range_status
    update_interval: 10min

  - platform: template
    id: distance_measurement
    name: "Oil Level"
    unit_of_measurement: "mm"
    device_class: distance
    state_class: measurement
    accuracy_decimals: 0
    lambda: |-
       if (id(range_status).state > 0) {
         return NAN;
       } else {
         return 1067 - id(raw_distance).state;
       }

   - platform: template
    id: remaining_gallons
    name: "Remaining Oil"
    unit_of_measurement: "gal"
    state_class: measurement
    accuracy_decimals: 1
    lambda: |-
       float distance_mm = id(distance_measurement).state;
       if (isnan(distance_mm)) {
         return NAN;
       }
       
       // Convert mm to inches (1 inch = 25.4 mm)
       float inches = distance_mm / 25.4;
       
       // 275 gallon vertical tank chart (60" long, 27"x44" oval)
       // inches to gallons lookup table
       if (inches < 1) return 0;
       if (inches >= 44) return 274;
       
       // Lookup table based on manufacturer chart
       int inch_values[] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44};
       float gallon_values[] = {2,5,9,14,19,25,31,38,44,51,58,65,72,80,87,94,101,108,115,123,130,137,144,151,158,166,173,180,187,194,201,209,216,223,230,236,243,249,254,260,265,269,272,274};
       
       // Find the two closest values and interpolate
       int lower_idx = (int)inches - 1;
       if (lower_idx < 0) lower_idx = 0;
       if (lower_idx >= 43) lower_idx = 42;
       
       int upper_idx = lower_idx + 1;
       if (upper_idx >= 44) upper_idx = 43;
       
       // Linear interpolation
       float fraction = inches - inch_values[lower_idx];
       float gallons = gallon_values[lower_idx] + 
                      (gallon_values[upper_idx] - gallon_values[lower_idx]) * fraction;
       
       return gallons;

  - platform: template
    id: remaining_percent
    name: "Oil Tank Percent Full"
    unit_of_measurement: "%"
    state_class: measurement
    accuracy_decimals: 0
    lambda: |-
       float gallons = id(remaining_gallons).state;
       if (isnan(gallons)) {
         return NAN;
       }
       
       // Calculate percent based on 275 gallon capacity
       float percent = (gallons / 275.0) * 100.0;
       
       // Clamp between 0 and 100
       if (percent < 0) return 0;
       if (percent > 100) return 100;
       
       return percent;

Let me know if you have any questions and I’d be happy to help. I’m sure this could be adapted to any type of fluid holding tank as well. Also, there are COTS solutions out there but $160 USD plus no local api didn’t seem worth it. The 3d printed cap was about $31 USD because I don’t have a 3d printer and had to use craftcloud… The m5stack hardware and power supply was about $25 more so all in this project cost about $56! Plus local control!

4 Likes

I wonder how oil vapors affect the electronics in long term…

I just used a PVC pipe cap with the same threads as the holes in the top of the tank. I drilled a small hole for the wire and bedded an ultrasonic sensor in the underside. No 3D printing needed.

Mine has been working for going on three years now. I also have some logic to calculate fuel remaining based on the level readings.

Thank for the inspiration. And code. Your post came at a perfect time. As I’m snowed in and board to death. Badly needed a distraction. So I put together one.

It’s a esp-01 with a TOF400C, (vl53l1x compatible.) and a 4-24v to 3.3v buck.

I searched thingiverse for a tank cap model to print. And found three models that worked well together. And after a bit of resizing and manipulation in Cura slicer to fit the parts together and mesh together parts to print as one piece.

https://www.thingiverse.com/thing:6530435. Tank cap

https://www.thingiverse.com/thing:2088429. Extendable tube

https://www.thingiverse.com/thing:4930863. Saddle washer

Thanks again.