Sunday 15 September 2013

Pi Central Heating

Pi Central Heating

I decided to replace my very annoying central heating controller with a Raspberry Pi. I thought it would be a fun project but would also give me a better and more controllable central heating system.



My central heating is very simple. It really comes down to two switches : one to turn on a pump which drives water round the system which in turn triggers the boiler to heat the water up and the other to divert that hot water round the radiators. So the control of this system (and in effect all the controller does) is to switch these two switches ‘on’ or ‘off’ at particular times of day.
This is very easy to do with a Raspberry Pi. It comes equipped with a GPIO (General Purpose Input Output) connector which gives several Input/Output control lines. Each of thesecan be programmed to either produce an output voltage or sense an input voltage.
For my system I needed four I/O lines. Two were needed to control the two switches and two more I used to sense the current state of the heating & hot water.
In order to actually control the heating you have to be a bit careful. The switches that control the system are switching mains electricity (240v) and so you can’t simply tie these into the Pi. It would melt. You have to interface to these switches somehow.
I chose to do this by buying another central heating controller. That sounds daft but I figured it gave me two advantages. Firstly I could wire my control lines into this and use it as a ‘buffer’ to protect the Pi from the mains voltages. Secondly, these controllers plug onto a standard back plate so  it meant that if I screwed it all up I could simply plug the old controller back in place while I fixed the problem. And in the meanwhile the wife could still have a hot bath.


Plus I found just the right type - a Drayton LP522 - on ebay for £2! Bargain.

So schematically it looks like this:


The controller has two switches on the front. These are override switches that allow you to manually turn the water/heating on and off. All the Pi has to do is short out the connections on the back of these switches momentarily and it appears to the controller as though someone has pressed the switch. 

So I soldered wires to the contact points on the circuit board of the controller and ran them out to the Pi.
Next I located to points on the controller board that changed voltage when the LED lights on the front were on or off. This gives an indication of the current state of the heating and hot-water. These are then connected to two input lines on the Pi GPIO.


 

In order to connect to the Pi it’s best to run through a bit of electronics to protect the Pi. So I built an interface board with two buffered outputs and two buffered inputs:

 

 


After that it was just a matter of putting it all in a box and wiring it up. I then drilled a hole through the bathroom wall so that I could mount the Pi outside the bathroom (I figured the Pi wouldn’t like being in a hot, wet, steamy environment. Plus it means I can check on it any time – even if someone is using the bathroom!).




I also added some lights and switches to the box – these allow me to override the HW & CH manually and the lights tell me the current state.
The rest is code. 
The Pi is connected to my WiFi router using a WiFi dongle. It is also set-up to run ‘headless’ (that is with no keyboard, mouse or screen plugged in). This means I can log on to the Pi from anywhere on the network – in fact anywhere in the world after I had forwarded Port 22 on my router to the Pi.
So I can program it from the laptop whilst sitting on the couch (in fact I did some of the programming when bored one afternoon sitting in a guest house in the Philippines!).


The software to control the heating is written in Python. The schedule for the on/off times is in a MySQL database which the Python controller reads and acts upon every fifteen seconds. There is also another Python process that checks the current state of the CH/HW and updates a row in the DB.
I then wrote a bunch of screens in PHP so that I can control the heating and edit the schedule. These screens are served up by an Apache Web server installed on the Pi. This means that you can edit, view and control the heating system from any PC in the house across the wireless network. In fact – after I forwarded some more ports on the router – I can now control it completely from any PC, anywhere. Even my phone. So I now have an internet-controlled heating system (I had to add some cookie controlled log-in pages when I did this just in case someone decided to take control of my heating!) 


The controller reads the schedule from a MySQL table and also looks for 'overrides' which are events triggered from the two buttons on the front of the box or virtual buttons presented on the browser. The effect of the buttons is to insert a new row in this override table each time there's a key press.

The tables look like this:

 create table schedule (
id INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY,
day VARCHAR(9), 

time TIME, 
hot_water VARCHAR(3), 
heating VARCHAR(3));

CREATE TABLE `override` (
  `heating` varchar(3) DEFAULT NULL,
  `hotwater` varchar(3) DEFAULT NULL,
  `status` varchar(1) DEFAULT NULL,
  `source` varchar(20) default null,
  `time` timestamp default current_timestamp
);

CREATE TABLE `current_state` (
  `heating` varchar(3) DEFAULT NULL,
  `hotwater` varchar(3) DEFAULT NULL,
  `mode` varchar(6) DEFAULT NULL
)


 CREATE TABLE `log` (
  `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `source` varchar(20) DEFAULT NULL,
  `message` varchar(255) DEFAULT NULL
)

 CREATE TABLE `template` (
  `name` varchar(30) DEFAULT NULL,
  `day` varchar(9) DEFAULT NULL,
  `time` time DEFAULT NULL,
  `hot_water` varchar(3) DEFAULT NULL,
  `heating` varchar(3) DEFAULT NULL
)

The 'Log' table is to allow the software to log various events for debugging/monitoring. The template table allows me to save schedules as templates (e.g. 'holiday', 'summer', winter'). The 'current_state' table only has one row and holds the current state of the HW/Heating as detected by another background process. This looks at the GPIO lines and updates the DB so that the browser can display the state without connecting to the GPIO. In this way all the browser PHP stuff only has to talk to the DB - this keeps things cleaner. The 'mode' column is for switching between 'AUTO' (running from the schedule) and 'manual' (where it just obeys the override buttons).

The controller python looks like this:


#! /usr/bin/env python
import wiringpi
import MySQLdb
import sys
from GPIOpins import GPIO_HWstate
from GPIOpins import GPIO_CHstate
from GPIOpins import GPIO_switchHW
from GPIOpins import GPIO_switchCH
from time import sleep

intervalTime=7

gpio = wiringpi.GPIO(wiringpi.GPIO.WPI_MODE_GPIO) 
gpio.pinMode(GPIO_switchHW,gpio.OUTPUT) 
gpio.pinMode(GPIO_switchCH,gpio.OUTPUT)
gpio.pinMode(GPIO_CHstate,gpio.INPUT)
gpio.pinMode(GPIO_HWstate,gpio.INPUT)

###############################################################################

def log(logmessage):
    # Open database connection
    logdb = MySQLdb.connect("localhost","root","xxxxxxx","heating" )

    # prepare a cursor object using cursor() method
    logcursor = logdb.cursor()

    # Prepare SQL query to INSERT a record into the database.
    sql = """INSERT INTO log(source,
             message)
             VALUES ('controller','"""+logmessage+"""')"""
    try:
       logcursor.execute(sql)
       logdb.commit()
    except:
       logdb.rollback()

    # disconnect from server
    logdb.close()
       

###############################################################################
   
def switch(switchpin, statepin, desiredstate):
    print "switch pin ", switchpin, " statepin ", statepin, " desired state ", desiredstate

    # first check the state of the channel (HW or CH)
    currState = gpio.digitalRead(statepin)

    # '1' indicates off, '0' indicates on!
    if (currState==1):
        currState = "OFF"
    else:
        currState = "ON"

    print "current state is ", currState

    # check if there is anything needed to do:
    if (desiredstate!=currState):
        print "switching"
        gpio.digitalWrite(switchpin,gpio.HIGH)
        sleep(0.25)
        gpio.digitalWrite(switchpin,gpio.LOW)

    return

###############################################################################

print "controller starting"
log("controller starting")

firsttime=True

# open the database
connection = MySQLdb.connect(host="localhost", user="root", passwd="xxxxxx", db="heating")

cursor = connection.cursor ()

cursor.execute("select ucase(dayname(curdate()))")
row = cursor.fetchone()
last_day = row[0]

cursor.execute("select curtime()")
row = cursor.fetchone()
last_time=row[0]
cursor.close()
connection.close()

print "start is ", last_day, " time=", last_time

sleep(intervalTime)

while True:
    # open the database
    connection = MySQLdb.connect(host="localhost", user="root", passwd="xxxxxx", db="heating")

    cursor = connection.cursor ()

    # get today's day
    cursor.execute("select ucase(dayname(curdate()))")
    row = cursor.fetchone()
    day = row[0]

    # get the current time
    cursor.execute("select curtime()")
    row = cursor.fetchone()
    time=row[0]
    #print "now it is ", day, " time=", time

    # if the day has changed then we will just run a query from the last point in time up to midnight...
    if last_day != day:
        print "day change"
        # save the name of 'today' so that we can put it back later:
        tomorrow=day

        # now pretend that the current time is 1 second to midnight 'yesterday':
        day=last_day
        time='23:59:59'
        daychange=True
    else:
        daychange=False

    # first see if the mode is 'AUTO' or 'MANUAL'
    query = "select mode from current_state"
    cursor.execute(query)
    data = cursor.fetchall()
    for row in data:
        mode = row[0]

    #print "select * from schedule where day='"+day+"' and time>'" + str(last_time) + "' and time<='" + str(time) + "' order by time"
    query = "select * from schedule where day='"+day+"' and time>'" + str(last_time) + "' and time<='" + str(time) + "' order by time"
    cursor.execute( query)
    data = cursor.fetchall()
    for row in data:
        row_id = row[0]
        day = row[1]
        time = row[2]
        hot_water = row[3]
        heating = row[4]
        print "id=", row_id, " day=", day, " time=", time, " hot_water=", hot_water, " heating=", heating
        if mode=='AUTO':
            print "executing at ", day, time
            logline="scheduled event id="+str(row_id)+" day="+day+" time="+str(time)+" hot_water="+hot_water+" heating="+heating
            log(logline)
            switch(GPIO_switchHW, GPIO_HWstate, hot_water)
            switch(GPIO_switchCH, GPIO_CHstate, heating)

            #update_query= "update current_state set heating='"+ heating +"', hotwater='"+ hot_water + "'"
            #cursor.execute(update_query)
            #connection.commit()


    # if this was a day change then reset the 'last' point in time to midnight:
    if daychange :
       last_day=tomorrow
       last_time='00:00:00'
    else:
       # record the last point in time ready for the next interval
       last_time=time
       last_day=day


    # check for overrides:
    query = "select * from override where status='P' order by time"
    cursor.execute(query)
    data = cursor.fetchall()
    for row in data:
        heating=row[0]
        hotwater=row[1]

        print "execute override"
        logline="override  hot_water="+hotwater+" heating="+heating
        log(logline)
        if heating=='ON':
            switch(GPIO_switchCH, GPIO_CHstate, "ON")
        if heating=='OFF':
            switch(GPIO_switchCH, GPIO_CHstate, "OFF")

        if hotwater=='ON':
            switch(GPIO_switchHW, GPIO_HWstate, "ON")
        if hotwater=='OFF':
            switch(GPIO_switchHW, GPIO_HWstate, "OFF")
    cursor.execute("update override set status='C'")
    connection.commit()
    cursor.close()
    connection.close()

    sleep(intervalTime)


  The GPIOpins.py merely holds the pin assignments do that they are common between the various python scripts.


GPIO_HWstate =10
GPIO_CHstate =9
GPIO_btnCH = 8
GPIO_btnHW = 11
GPIO_switchHW = 3
GPIO_switchCH = 2
GPIO_ledHWred = 27
GPIO_ledCHred = 22
GPIO_ledHWgreen = 4
GPIO_ledCHgreen = 17







 
Below is an example of the web page. This is the main page (and is in fact a later version that includes the changes for the thermostat stuff detailed in the next post). There are also login, logout and various other bits of php that are fairly typical examples of simple security using php - you can find them all over the web (I did!)

<html>
 <head>
 <title>Pi Central Heating</title>
 <link rel="stylesheet" type="text/css" href="heatingstyle.css">
 <meta http-equiv="refresh" content="9" >
 </head>
 <body>
 <?php
    session_start();
    if (!(isset($_SESSION['user']) && $_SESSION['user'] != '')) {
       header ("Location: login.html");
    }

    $height = $_GET['height'];
    $width  = $_GET['width'];

    //echo("<p>" . $height . $width . "</p");
    // connect to the DB
    include('connect.php');
    if (! @mysql_select_db("heating") ) {
      echo( "<P>Unable to locate the heating " .       
            "database at this time.</P>" ); 
      exit();
    }

    $result = mysql_query("select hotwater, heating, mode from current_state");
    if (!$result)
    {
      echo("<P>Error performing query: " .
            mysql_error() . "</P>"); 
      exit();
    }
    $row = mysql_fetch_array($result);
    $currHeating = $row["heating"];
    $currHotwater = $row["hotwater"];
    $mode = $row["mode"];
    if ($width>1000)
    {
       if ($currHotwater=="ON")
       {
         ?>
         <TD><a href="override.php?service=HW&onoff=OFF"><img src="images/HWon.jpg" width=150 height=70/></a></TD>
         <?php
       }
       else
       {
         ?>
         <TD><a href="override.php?service=HW&onoff=ON"><img src="images/HWoff.jpg" width=150 height=70/></a></TD>
         <?php
       }
       if ($currHeating=="ON")
       {
         ?>
         <TD><a href="override.php?service=CH&onoff=OFF"><img src="images/CHon.jpg" width=150 height=70/></a></TD>
         <?php
       }
       else
       {
         ?>
         <TD><a href="override.php?service=CH&onoff=ON"><img src="images/CHoff.jpg" width=150 height=70/></a></TD>
         <?php
       }
    }
    else
    {
       if ($currHotwater=="ON")
       {
         ?>
         <TD><a href="override.php?service=HW&onoff=OFF"><img src="images/HWon.jpg" width=40% height=25%/></a></TD>
         <?php
       }
       else
       {
         ?>
         <TD><a href="override.php?service=HW&onoff=ON"><img src="images/HWoff.jpg" width=40% height=25%/></a></TD>
         <?php
       }
       if ($currHeating=="ON")
       {
         ?>
         <TD><a href="override.php?service=CH&onoff=OFF"><img src="images/CHon.jpg" width=40% height=25%/></a></TD>
         <?php
       }
       else
       {
         ?>
         <TD><a href="override.php?service=CH&onoff=ON"><img src="images/CHoff.jpg" width=40% height=25%/></a></TD>
         <?php
       }
    }
    echo('<p></p>');
    $result = mysql_query("SELECT curtime() curtime, dayname(curdate()) daynm");
    if (!$result)
    {
      echo("<P>Error retrieving time and date" . mysql_error() . "</P>");
    }
    $row = mysql_fetch_array($result);
    $currtime = $row["curtime"];
    $dayname = $row["daynm"];
   
    if ($mode=='AUTO')
    {
      echo('<a href="override.php?service=MODE&mode=MANUAL"><img src="images/slideAuto.gif" width=100 height=50/></a>');
    }
    else
    {
      echo('<a href="override.php?service=MODE&mode=AUTO"><img src="images/slideManual.gif" width=100 height=50/></a>');
    }
    echo("<p>" . $dayname . "  " . $currtime . "</p>");
?>
<br><br>
<a href="schedule.php">Edit Schedule</a>
<br>
<a href="showlog.php">Show Log</a>
<br>
<a href="logout.php">Logout</a>
</body>
</html>