Page 1 of 1

Automatic Thermostat

Posted: Sat Dec 15, 2012 2:57 pm
by Paul Versteeg
This post will discuss the building of an automatic thermostat system, replacing the usual simple ones installed in many US homes. They typically allow you to set the temperature, switch the system from heating to cooling, and set the fan to manual or automatic. These thermostats do not use any fancy protocols, like those you find in Europe, so it makes life easy in that respect.

The requirements for this project are a bit more than just replacing the thermostat, although that can be achieved as well by stripping some of the functionality.

Here are the requirements for my project.
The system needs to run all by itself, because for many months at a time, we're not in the home. When we're away, we're really away, meaning in Europe. Because of this, the system has to be designed as a Mission Critical system, because you just can't hop over and do a reset, or keep en eye on the system.
Mission Critical also means that we need to have an answer for every eventuality, and thoroughly test all code, including all possible error conditions, also those generated by Python! No instruction or condition should be left untested.

This also means that the Raspberry Pi and it's OS must be robust, even when it hangs. There are some smart people that have been able to turn on the watchdog function, however I can't get it to work properly yet. I'll wait for somebody to figure it out for nubies like myself. That will only take time...

There have been plenty of examples hooking-up the DS18B20 sensor and relais to the Pi, so I won't cover that here. Right now, I have everything breadboarded, and will eventually move to a circuit board.

So far so good.

Here are some enhancements that I'm currently planning to add now that this functionality is done.
- Add a web interface, so I can program the parameters from a browser. Look at the topic "Web enabled thermostat project" in this forum from David. I'll be using a lot of his expertise and code here later.
- Look at the working system from a browser.
- Eventually, I would like to add a 2x16 display, so the Pi can display some key information like current temperature, set temperature (for heating and cooling) what system mode (cooling or heating) it's in. A bit like what a normal thermostat can do.
- Add a few buttons to program the thermostat by hand.
- Add more sensors to add functionality like pre-empting based on the outside temperature, maybe using a different Pi to avoid lengthy wiring.
- Add more sensors (garage, attic) and trend all these sensors on a web server running on the Pi (this is ready, I'm using the RRDTOOL functionality, but I would like to finish my web server stuff first)

Now to the small print. :|
I'm not a programmer, as a matter of fact I have not written a line of code for more than 30 years. I started on Python a few weeks ago, and I know virtually nothing about HTML, PHP etc. I did have a background in embedded systems and some mission critical system stuff, but my programming experience was based on assembly code, Pascal en C. Structured, not OO, so there you have it.
I do have en education as an hardware engineer, but that was even longer ago and I used some Unix way back when too. So now you know what to expect. I have a steep, long and rocky learning curve ahead of me, so please keep that in mind. Keep it simple and we'll get along just fine! ;)

In any case, here is the program as it is right now. You'll see a lot of commented out code, I used that for debugging and understanding what I tried to accomplish. It's easier to take out, then put it in, so it's up to you what you do with it.

Feel free to ask questions, I'll try to explain what I did and how, although the documentation should be pretty clear.

Code: Select all

#!/usr/bin/python
#-------------------------------------------------------------------------------
# FileName:     thermostat.py
# Purpose:      This program controls an HVAC based on a DS18B20 temperature
#               sensor.
#               We use an indeterminate non-linear control algorithm to
#               control the HVAC system, and we automatically switch from
#               the heating mode to the cooling mode.
#               Parameters can be loaded into the system while it is running.
#               Errors and warnings are recorded in a log file, and severe
#               problems are sent by email.
#
# Note:         Alle degrees are in Celcius, dates are in European format D-M-Y
#
# Planned
# Improvements: Night time setting, web interface, pre-empting the system based
#               on other (outside) temperature sensor(s)
#
# Author:      Paul Versteeg
#
# Created:     08-12-2012
# Copyright:   (c) Paul Versteeg 2012
# Licence:     <free for all if you reference the source>
#-------------------------------------------------------------------------------

import os
import string
import re
from time import time, sleep, gmtime, strftime, localtime
import RPi.GPIO as GPIO
import subprocess
import smtplib
import socket
from email.mime.text import MIMEText

# Global Variables
OFF = 0
ON = 1
delay_time = 2.0        # delay loop, should be 1 second
away = False            # when we're away, we have a different min/max temp setting
min_h_tmp_away = 10     # minimum temp while away
max_c_tmp_away = 30     # maximum temp while away
comfort_h_temp = 21     # comfort temperature while heating
comfort_c_temp = 25     # comfort temperature while cooling
night_temp_c = 18       # Minimum night time temperature
H = 0.5                 # Hysteresis to avoid rattling the system relay
ac_mode = True          # syst_mode setting
heat_mode = False       # sys_mode setting
new_parms = False       # initial condition for the main loop, do we have manual changes
current_temp = 18       # DEBUG so I can test without getting real data, and autoincrement

# assign the DS18B20 data locations
# The inside and outside sensors will be connected to this pi, the master.
dsintemp = "/sys/bus/w1/devices/28-0000042b3fce/w1_slave"
dsouttemp = "/sys/bus/w1/devices/28-0000042b3fce/w1_slave" # for now

path= os.path.abspath(os.path.dirname(__file__)) # get current path of executable

file_name = path+"/thermos_data_file.ini" # this is where we store the incoming parameters
logfile_name = path+"/thermos_log_file.log" # this is where we store the errors and warnings

def init():
    '''
    Initializes a number of settings.

    '''
    global set_temp_h, set_temp_c, system_mode, H_C_RELAIS, HVAC
    GPIO.setwarnings(False) # comment out during DEBUG
    # Use the Raspberry GPIO connector pins instead of the SOC pins
    GPIO.setmode(GPIO.BOARD)
    # Setup the GPIO pins we'll use
    # Connector pin 7 is reserved for the W1 interface to the DS18B20 sensors
    # GPIO 22 is on connector pin 15
    H_C_RELAIS = 15 # this connector pin controls the relais that switches between heating or cooling
    # Setup the channel as output
    GPIO.setup(H_C_RELAIS, GPIO.OUT)
    HVAC = 13 # this connector pin controls the relais that switches the system
                # from cooling to heating and back
    # Setup the channel as output
    GPIO.setup(HVAC, GPIO.OUT)
    #
    # see if the one-wire "w1" interfaces are loaded
    tfile = open("/proc/modules")
    moduletext = tfile.read()
    tfile.close()
    if not (re.search("w1_gpio", moduletext) and re.search("w1_therm", moduletext)):
        # modules not found, install them
        os.system('sudo modprobe w1-gpio')
        os.system('sudo modprobe w1-therm')

    set_temp_h = comfort_h_temp # setup the initial temperature references for; heating
    set_temp_c = comfort_c_temp # and cooling
    system_mode = heat_mode # Initial value, will determine later in set_mode()
    hc_relais (system_mode) # start in heating mode
    hvac(False) # Switch off the system
    outside_temp = 16 # DEBUG because I have no sensor installed yet
    #
    # make sure we start with a clean and empty log file
    try:
        fout = open(logfile_name, "w")
    except Exception, e:
        print "\tOutput file open error: %s" % str(e)

    fout.write (str("\n"))
    fout.close()


def mail_alarm(msg, t):
   '''
   This function emails a warning or error message from the raspberry pi to myself.

   I also send the IP addresss, just in case we're going to use
   more Pi's later on. The IP address portion can be commented out.
   Here is where I found the information:
   http://elinux.org/RPi_Email_IP_On_Boot_Debian

   '''
   to = 'your email address'
   gmail_user = 'Raspberry Pi email address@gmail.com' # get one from Google
   gmail_password = 'XXXXXXXX'
   smtpserver = smtplib.SMTP('smtp.gmail.com', 587)
   smtpserver.ehlo()
   smtpserver.starttls()
   smtpserver.ehlo
   smtpserver.login(gmail_user, gmail_password)
   today = datetime.date.today()
   arg='ip route list'
   p=subprocess.Popen(arg,shell=True,stdout=subprocess.PIPE)
   data = p.communicate()
   split_data = data[0].split()
   ipaddr = split_data[split_data.index('src')+1]
   my_msg = 'Message from RaspberryPi at IP address %s' %  ipaddr +'\n\nTemperature warning: '+ msg + str(t)
   msg = MIMEText(my_msg)
   msg['Subject'] = 'Msg from RaspberryPi on %s' % today.strftime('%b %d %Y')
   msg['From'] = gmail_user
   msg['To'] = to
   smtpserver.sendmail(gmail_user, [to], msg.as_string())
   smtpserver.quit()


def hc_relais(mode):
    '''
    Function to toggle the relais from heat mode to cooling mode.

    '''
    # Set the pin of the GPIO connector
    if mode == True  : GPIO.output(H_C_RELAIS, GPIO.HIGH)
    if mode == False : GPIO.output(H_C_RELAIS, GPIO.LOW)
# DEBUG
#    if mode == True : print "H_C_RELAIS on"
    if mode == True : write_log ( "H_C_Relais on -> " + str(current_temp))
#    if mode == False : print "H_C_RELAIS off"
    if mode == False : write_log ( "H_C_Relais off -> " + str(current_temp))

def hvac(mode):
    '''
    Function to switch the cooling/heating system on or off.

    '''
    if mode == True  : GPIO.output(HVAC, GPIO.HIGH) # on
    if mode == False : GPIO.output(HVAC, GPIO.LOW) # off
# DEBUG
#    if mode == True : print "HVAC on"
    if mode == True : write_log ( "HVAC on -> " + str(current_temp))
#    if mode == False : print "HVAC off"
    if mode == False : write_log ( "HVAC off -> " + str(current_temp))


def write_log(err_str):
    '''
    Function to write errors and messages to a log file.

    So we can look at them later and also off-line while the program is running.
    '''
    global set_temp_h, set_temp_c, current_temp
    try:
        fout = open(logfile_name, "a")
    except Exception, e:
        print "\tOutput file open error: %s" % str(e)

    tstamp = strftime("%d-%m-%Y %H:%M:%S", localtime())
    fout.write (tstamp + " -> " + err_str)
    fout.write (str("\n"))
    fout.close()


def get_parms():
    '''
    Function to collect new parameters from a file.

    So they can be changed manually on the fly
    or by other programs and loaded in the system.
    '''
    global set_temp_h, set_temp_c, night_temp_c, new_parms
    try:
        fin = open(file_name, "r")
    except Exception, e:
        print "\tOutput file open error: %s" % str(e)
        write_log ("Get_parms() Output file open error: %s" + str(e))

    new_parms = fin.readline()
    if new_parms[0] == "1": # If "1" in the first field, there are new parameters
        hvac(False) # switch off the HVAC
        write_log ( "get_parms() we have new parameters ")
        set_temp_h = float(fin.readline()) # Read the parameters and load them
#        print "set_temp_h: \t", set_temp_h #DEBUG
        set_temp_c = float(fin.readline())
#        print "set_temp_c: \t", set_temp_c #DEBUG
        night_temp_c = float(fin.readline()) # Not used yet
#        print "Night low: \t", night_temp_c #DEBUG
    fin.close()
    # reset the "new_parms" flag
    try:
        fout = open(file_name, "w")
    except Exception, e:
        print "\tOutput file open error: %s" % str(e)
        write_log ("New_parms() Output file open error: %s" + str(e))

    update = 0    # Reset the flag : no new parameters
#    print "update flag reset :", update #DEBUG
    fout.write (str(update))
    fout.write (str("\n"))
    fout.close()


def get_sensor_data(ds):
    '''
    Function to get temperatures from one of the DS18B20's.

    Test for a proper CRC from the DS18B20 for a YES in the result.
    Average the readings for a few times to get a stable return.
    And make sure we have no bogus readings.
    '''
    avg_tmp = 0.0
    x = 1
    while ( x <= 5):
        tfile = open(ds)
        text = tfile.read()
        tfile.close()
        if re.search ("YES", text):
            # there is a valid crc in the recording
            # strip the rubbish to get to the temperature reading
            temperature_data = text.split()[-1]
            temp = float(temperature_data[2:])
            temp = (temp / 1000)- 2.5 #DEBUG I substract a bit so I can hand warm the sensor
            if temp < 5 or temp > 33:
                print "\t\tTemp out of range : ", temp
                write_log ("Get_sensor_data() Temp out of range : " + str(temp))
            else:
                avg_tmp = avg_tmp + temp
                x += 1
        else:
            print "Get_Sensor() rcvd currupted data from sensor"
            write_log ("Get_sensor_data() -> bad crc from sensor : ")
            # continue the loop but skip this reading
        sleep(delay_time/10)
#        print x, "\t", temp, "\t", avg_tmp / (x-1) #DEBUG
    return round(avg_tmp/(x-1), 2) # average, and round result to 2 decimal digits


def get_temps():
    '''
    Function to get the temperature from the sensors.

    '''
    global current_temp, outside_temp
# DEBUG
    current_temp = get_sensor_data(dsintemp) #DEBUG I comment this out during testing
#    print current_temp #DEBUG
# DEBUG
#    current_temp += .1 #DEBUG Here is where I do my autoincrement/decrement to test everything
#    outside_temp = get_sensor_data(dsouttemp) #DEBUG, I don't have that sensor installed yet
    outside_temp = 26.123 #DEBUG initially set in init()
#    write_log ("Temp is : " + str(current_temp)) #DEBUG
    return current_temp, outside_temp


def heating():
    '''
    Function to determine if we need to switch the heater on or off.

    While the temperature is within the
    set_temp_h +/- hysteresis (H) range, the current
    state stays unchanged.
    '''
    if current_temp >= set_temp_h + H:
#        print "stop furnace", current_temp, set_temp_h + H #DEBUG
        hvac(False)
    if current_temp <= set_temp_h - H:
#        print "start furnace", current_temp, set_temp_h - H #DEBUG
        hvac(True)


def cooling():
    '''
    Function to determine if we need to turn the cooling on or off.

    While the temperature is within the
    set_temp_c +/- hysteresis (H) range, the current
    state stays unchanged.
    '''
    if current_temp <= set_temp_c - H:
#        print "stop A/C", current_temp, set_temp_c - H #DEBUG
        hvac(False)
    if current_temp >= set_temp_c + H:
#        print "start A/C", current_temp, set_temp_c + H #DEBUG
        hvac(True)


def set_mode():
    '''
    This function automates the setting for the cooling or heating mode of the HVAC system.

    This is normally done by manually flipping a switch on the thermostat.
    When you're away for longer periods, this could potentially pose a problem
    if the outside temperatures drop or rises significantly - needing a change in the mode.

    The second main reason for going thru this is that you cannot have separate "comfort"
    temperatures for cooling and heating. We prefer the heating temperature at 20 to 22 degrees,
    but the cooling temperature not lower than 25 degrees.

    To avoid rattling the system_mode relais, we will only change modes when needed.
    The hysteresis for the system_mode needs to be set dynamically, to account for changes in
    the settings by runtime changes to parameters.
    '''
    global system_mode
    if (set_temp_c - H) - (set_temp_h + H) < 2 * H :
        H2 = 2 * H # minimum hysteresis to keep system from rattling
    else:
        H2 = (set_temp_c - H) - (set_temp_h + H)
#    print H2 #DEBUG
    if system_mode == heat_mode: # do we need to switch to AC mode?
#        print "Heat mode - do we need to switch to cooling?" #DEBUG
        if current_temp < set_temp_h + H2:
#            print "not switching to cooling (yet)", current_temp, set_temp_h + H2 #DEBUG
            pass # Does nothing, is only here for syntax reasons when the print function is commented out
        if current_temp > set_temp_h + H2:
#            print "switch system to cooling!", current_temp, set_temp_h + H2 #DEBUG
            write_log ( "Set_mode() switch to ac mode " + str(current_temp))
            system_mode = ac_mode
            hc_relais (system_mode) # set the relais
            sleep(delay_time*2) # wait a bit before switching the HVAC on
#            print "system_mode = ac_mode", system_mode #DEBUG
    if system_mode == ac_mode: # do we need to switch to heat mode?
#        print "AC mode - do we need to switch to heating?" #DEBUG
        if current_temp > set_temp_c - H2:
#        print "not switching to heating (yet)", current_temp, set_temp_c - H2 #DEBUG
            pass # Does nothing, is only here for syntax reasons when the print function is commented out
        if current_temp < set_temp_c - H2:
#            print "switch system to heating!", current_temp, set_temp_c - H2 #DEBUG
            write_log ( "Set_mode() switch to heat mode " + str(current_temp))
            system_mode = heat_mode
            hc_relais (system_mode) # set the relais
            sleep(delay_time*2) # wait a bit before switching the HVAC on
#            print "system_mode = heat_mode", system_mode #DEBUG
    return system_mode


def BangBang():
    '''
    The workhorse function where we do most of the work.

    So called because it is a known indeterminate non-liniar control concept.
    We just bang (toggle) the relay from on to off or from off to on
    There is no PWM or other gradual change (PID) possible with this kind of HVAC system anyway.

    There are two loops, the outer sets up the conditions,
    and the inner does the work. We branch out regularly to check for new parameters.
    Some of the information came from the book "Real World Instrumentation with Python"
    by J.M. Hughes. O'Reilly Media. Recommended reading for nubies on interfacing computers to the outside
    world. (there really is one out there!)
    '''
    global current_temp, outside_temp, system_mode, ac_mode
    #
    do_loop = 1
    while do_loop <=50: # After "do_loop" readings break out and check if there are new parameters
#        print "Inside BangBang inner loop", do_loop #DEBUG
        get_temps() # get temperature readings from the sensors
#        print "current_temp = ", current_temp, "outside temp = ", outside_temp #DEBUG
        # check to see if we have a working system
        if (current_temp <= 7 or current_temp >= 33): # we have a problem
            print "\t\tWE SEEM TO HAVE A PROBLEM", current_temp
            mail_alarm("sensor out of range ", current_temp)
            write_log("BangBang() : Temperature out of range "+ str(current_temp))
            current_temp = 20.5 # see if we can recover with a valid value, no heating, no cooling #DEBUG
        system_mode = set_mode() # determine what the system needs to do
        if system_mode == ac_mode: # true based on the system mode, we either cool or heat
#            print "in cooling mode" #DEBUG
            cooling()
        else:
#            print "in heating mode" #DEBUG
            heating()
        #
        # we're going through the while loop and then check for new parameters
        do_loop += 1
        #
        # Are new parameters entered outside of this program (web interface or command line)?
    get_parms()
# End of BangBang


init()
running = True
while running == True:
    '''
    The main routine.

    We regularly return here to collect the new parameters and
    then go back to BangBang to do the actual work.
    '''
    try:
        BangBang()
#        print "In main()" #DEBUG
    except KeyboardInterrupt:
#        print "clean up" #DEBUG
        GPIO.cleanup() # This resets all the input/output pins to default.
Here is the small program I use to load new parameters in the file

Code: Select all

#!/usr/bin/python
#-------------------------------------------------------------------------------
# Name:         w_thermos_parms.py
# Purpose:      write thermostat values to a file such that the thermostate
#               program can use it. This allows us to change the parameters
#               while the program is running
#
# Author:      Paul Versteeg
#
# Created:     07-12-2012
# Copyright:   (c) Paul Versteeg 2012
# Licence:     <free for all if you reference the source>
#-------------------------------------------------------------------------------

import os, string
import subprocess
import time

# get current path
path= os.path.abspath(os.path.dirname(__file__))

file_name = path+"/thermos_data_file.ini"

try:
    fout = open(file_name, "w")
except Exception, e:
    print "Output file open error: %s" % str(e)

update = 1    # is there an update (T or F)
set_temp_c = 29.99 # Cooling  temp
set_temp_h = 22.0 # Heating temp
night_temp_c = 18.88
#print "writing: ", comfort_h_temp, comfort_c_temp #, req_temp_c
fout.write (str(update))
fout.write (str("\n"))
fout.write (str(set_temp_h))
fout.write (str("\n"))
fout.write (str(set_temp_c))
fout.write (str("\n"))
fout.write (str(night_temp_c))
fout.write (str("\n"))
fout.close()

fin = open(file_name, "r")
new_parms = fin.readline()
print "Update: ", new_parms
print new_parms[0]
if new_parms[0] == "1":
    set_temp_h = float(fin.readline())
    if set_temp_h == 29.99: print "OK"
    print "set_temp_h: \t", set_temp_h
    set_temp_c = float(fin.readline())
    if set_temp_c == 11.22: print "OK"
    print "set_temp_c: \t", set_temp_c
    night_temp_c = float(fin.readline())
    print "Night low: \t", night_temp_c
fin.close()
Enjoy!