Automatic Thermostat
Posted: Sat Dec 15, 2012 2:57 pm
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.
Here is the small program I use to load new parameters in the file
Enjoy!
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.
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()