Now that I’ve got a sensor unit for measuring the heating oil left in my tank and the interface board and Raspberry Pi needed to do something with it, it’s time to write software that can actually do something with all this hardware!
As a preface, let me say I’ve not written anything in Python before I got my first Pi for Christmas. Perl, sure. Shell script, yep. But I hadn’t had need to learn Python yet. The GPIO libraries for Perl on the Pi aren’t all that great, so Python it is. People who program Python for a living, please be kind.
Temperature Sensor
First, I wrote a very quick and dirty module for reading the temperature sensor:
#!/usr/bin/python
# tmp102.py
# A python module for interfacing with a SparkFun TMP102 breakout module
# or other implementation of a TMP102 chip via I2C bus
import smbus
# Default definitions
module_address = 0x48
system_i2cbus = 1
def read():
# Reads the temperature from the Pi's default i2c bus using a
# TMP102 set at the default i2c address. Returns decimal temperature
# in Celsius.
# Create an object for reading from the system i2c bus
bus = smbus.SMBus(system_i2cbus)
# Read the temperature data block from the TMP102 module
data = bus.read_i2c_block_data(module_address, 0)
# Extract the least and most significant bytes from the data block
msb = data[0]
lsb = data[1]
# Return the decimal value
return (((msb << 8) | lsb) >> 4) * 0.0625
Tank Calculations
Next, a basic program for reading the tank sensor and calculating how much oil is left:
#!/usr/bin/python
# Oil Tank Monitor Script
import time
import math
import RPi.GPIO as GPIO
import signal
import atexit
import tmp102
#
# CONSTANTS
#
timeout = 0.020 # Longest time to wait for a ping response (sec)
gpio_pin = 7 # Pin where the Ping))) is connected (Board numbering)
num_pings = 40 # Number of times to read the distance
fudge_factor = 2 # Distance to subtract from measurement (cm)
tank_width = 68 # Width of one tank in centimeters
tank_height = 111.5 # Height of one tank in centimeters
tank_length = 151 # Length of one tank in centimeters
num_tanks = 2 # Number of tanks in system
oil_burn_rate = 1 # Rate at which oil is burned (gal/hr)
daily_runtime = 5 # Average daily hours of operation
# The oil burn rate is defined by the nozzle used on the oil burner.
# The furnace documentation should list the rated nozzle size in GPH
# (gallons per hour).
def cleanup():
print "Cleaning up GPIO."
GPIO.cleanup()
def termhandler(signum, frame):
print "Terminating script."
global shutdown_flag
shutdown_flag = True
atexit.register(cleanup)
def add(x,y): return x+y
def init_sensor():
""" Prepares the Ping))) sensor for use """
GPIO.setmode(GPIO.BOARD)
GPIO.setup(gpio_pin, GPIO.OUT)
GPIO.output(gpio_pin, 0)
time.sleep(0.000002)
def read_distance():
"""
Determines the distance from the sensor to the oil in the tank.
"""
# For improved accuracy, we will take many measurements:
distances = []
for ping_number in range(1, num_pings):
successful=False
while not successful:
# Signal the Ping))) to send a pulse
GPIO.setup(gpio_pin, GPIO.OUT)
GPIO.output(gpio_pin, 0)
time.sleep(0.000002)
GPIO.output(gpio_pin, 1)
time.sleep(0.000005)
GPIO.output(gpio_pin, 0)
# Wait for the Ping))) to send back a timing pulse on the GPIO pin
GPIO.setup(gpio_pin, GPIO.IN)
goodread=True # Assume a good read until we learn otherwise
watchtime=time.time()
starttime=0
endtime=0
while GPIO.input(gpio_pin)==0 and goodread:
starttime=time.time()
if (starttime-watchtime > timeout):
# Bad read - timed out
goodread=False
# Okay, we now have detected the start of the timing pulse and
# noted the time it was received. Now look for the end of the pulse
# so we can determine the overall length of the pulse.
if goodread:
watchtime=time.time()
while GPIO.input(gpio_pin)==1 and goodread:
endtime=time.time()
if (endtime-watchtime > timeout):
# Bad read - timed out
goodread=False
else:
# Timed out before we saw the start of the timing pulse
# Try again...
continue
# We've detected the end of the timing pulse (or a timeout);
# if it was a timeout, try another measurement.
if not goodread:
continue
if goodread:
# Get the current air temperature. The speed of sound in air
# varies according to temperature, so having an accurate
# temperature allows us to calculate distance accurately.
air_temp=tmp102.read()
# Calculate the current speed of sound
c_air_m_s=331.5 + (0.6 * air_temp)
# Calculate duration of the return pulse from the Ping)))
duration=endtime-starttime # seconds
# Calculate the distance in centimeters
# The duration is multiplied by the speed of sound in air
# (as meters per second), then divided by two (because the
# ping is there-and-back, so the pulse had to travel twice
# the distance we're measuring), and then multiply by 100
# to convert from meters to centimeters.
distance=((duration * c_air_m_s )/2)*100
# Add the distance to our array of reads
distances.append(distance)
# If we got this far, we had a good measurement
successful=True
time.sleep(0.05)
# Okay, at this point we should have sixty "good" measurements.
# Some may be outliers. This is where some real statistics math would
# come in handy. For now, we'll simply take the median.
distances.sort
onethird=int(len(distances)/3)
twothirds=onethird * 2
mid_distances=distances[onethird:twothirds]
tank_air_space=(reduce(add, mid_distances)) / len(mid_distances)
# Round it off, this thing has 1cm resolution anyway
tank_air_space=round(tank_air_space)
tank_air_space = tank_air_space - fudge_factor
print "Adjusted air space in tank is ", tank_air_space, " cm"
return tank_air_space
def get_fill_height():
""" Returns the height of oil in the tank. """
air_space = read_distance()
oil_level = tank_height - air_space
print "Oil level calculated as ", oil_level, " cm"
return oil_level
def get_tank_volume():
"""
Returns the total volume of one vertical oval oil tank
Math in this section courtesy of:
http://www.calculatorsoup.com/calculators/construction/tank.php
"""
# The radius of the curved parts of the tank will be equal to
# one-half the width of the tank. (Radius = diameter / 2; the diameter
# of the curved part of the tank is necessarily the width of the widest
# part of the tank.)
tank_radius=tank_width/2
# The height of the section of tank that isn't curved will be equal to
# the height of the tank minus the height of the curved sections.
# The curved section at the top and the bottom are each semicircles,
# so each section's height is equal to the section's radius. We've got
# two curved areas, so subtracting 2r from the overall height gets us
# just the height of the straight section. Since the tank's width is
# 2r, we just use that.
tank_square_height=tank_height-tank_width
# Now calculate the surface area of the side of the tank. It will be
# equal to the area of a circle with the tank's radius (e.g., pi*r^2)
# plus the area of the square describing the middle part of the tank.
tank_end_area=math.pi*(tank_radius**2)+(2*tank_radius*tank_square_height)
# Now multiply that surface area times the length to get the volume
# in cubic centimeters.
tank_volume=tank_end_area*tank_length
return tank_volume
def get_oil_quantity():
"""
Returns the filled volume of one vertical oval oil tank in cubic
centimeters
Math in this section courtesy of:
http://www.calculatorsoup.com/calculators/construction/tank.php
"""
oil_level=get_fill_height()
tank_radius=tank_width/2
tank_square_height=tank_height-tank_width
# The math varies depending on how much oil there is...
if oil_level < tank_radius:
# Use circular segment method
tank_m=tank_radius-oil_level
theta=2*math.acos(tank_m/tank_radius)
oil_volume=0.5 * tank_radius**2 * (theta - math.sin(theta)) * tank_length
elif (oil_level > tank_radius) and (oil_level < (tank_radius + tank_square_height)):
# Half of circular portion plus volume in rectangular portion
vol_circle=math.pi * (tank_radius**2) * tank_length
height_in_square = oil_level - tank_radius
vol_cube=height_in_square * tank_width * tank_height
oil_volume=(vol_circle/2) + vol_cube
else:
# Use circular segment method for empty portion, then apply
# V(tank) - V(segment)
vol_tank=get_tank_volume()
air_space=tank_height-oil_level
tank_m=tank_radius-air_space
air_volume=0.5 * tank_radius**2 * (theta - math.sin(theta)) * tank_length
oil_volume = vol_tank - air_volume
return oil_volume
def cc_to_gallons(cubic_cm):
"""
Given a volume in cubic centimeters, return the equivalent in
U.S. gallons
"""
return cubic_cm * 0.000264172052358
#################################
#
# MAIN PROGRAM LOOP
#
init_sensor()
# Register handlers for abrupt termination, to make sure we clean up
# the GPIO pins
signal.signal(signal.SIGTERM, termhandler)
signal.signal(signal.SIGINT, termhandler)
# Get the amount of oil (in cubic centimeters)
oil_remaining_cc_1tank=get_oil_quantity()
oil_remaining_cc=oil_remaining_cc_1tank * num_tanks
# Get the volume of all tanks in the system
oil_capacity_1tank=get_tank_volume()
oil_capacity=oil_capacity_1tank * num_tanks
oil_remaining_gal=cc_to_gallons(oil_remaining_cc)
oil_capacity_gal=cc_to_gallons(oil_capacity)
# Calculate percentage
orp=(oil_remaining_cc / oil_capacity) * 100
print "%.1f of %.1f gallons remaining ( %3d%% )" % (oil_remaining_gal, oil_capacity_gal, orp)
# Calculate remaining runtime
hours_left=oil_remaining_gal / oil_burn_rate
print "%d hours of runtime remaining at %.2f gal/hr" % (hours_left, oil_burn_rate)
days_left=hours_left / daily_runtime
print "%d days of operation at %.2f hours per day average" % (days_left, daily_runtime)
cleanup
When executed, this program returns output like this:
Adjusted air space in tank is 69.0 cm Oil level calculated as 42.5 cm 178.9 of 525.7 gallons remaining ( 34% ) 178 hours of runtime remaining at 1.00 gal/hr 35 days of operation at 5.00 hours per day average
In the future, I’ll write code that integrates this with my Nagios monitoring system, which will send me notifications when the oil gets low. I may also write code that keeps track of consumption to send me predictive warnings based on how quickly I’m burning oil.