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!




