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.
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 lsb = data # Return the decimal value return (((msb << 8) | lsb) >> 4) * 0.0625
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.