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.

 

Leave a Reply