Below is the Arduino program (sketch) used on the central heating timer. Beware, this code window has the scrollbar at the bottom of the page which can be a pain as a few lines are too long – sorry! – I’ll get round to sorting it! Sorry, also, for poor programming style and not enough comments!
/*
CH_timer_29
TIMER PROGRAM FOR ARDUINO CENTRAL HEATING
Credit to Michael Margolis and John Boxall
temp102 by Arduino Playground author
timer control developed 4.7.15 onwards by Julian Rogers
*/
//timer with two ons and offs for water and heat manual advance and manual over-ride
//Arduino Uno with Ethernet Shield with SD card
//temperature by TMP102 (sockets for 2 wired sensors - could use network connected sensors
//clock is DS3231
//clock should be set to GMT, conversion to BST is by software
//reads manual switch positions
//turns on status LEDs as appropriate
//water and heat LEDs will flash if manual off during an "on" timed period
//on off times and thermostat value stored on SD card
//communication via UDP
//timer times, clock setting, thermostat setting set remotely by UDP
//includes data logger function now enough RAM available
/*
STATUS LIGHTS
Switch | Timed On | Timed Off
_______________________________
T | Cont Red | Faint Green
_______________________________
O |Flash Red | Off
_______________________________
C |Cont Green| Cont Green
_______________________________
OUTPUT FROM SERIAL MONITOR
Cryptic to reduce RAM usage!
T plus temp * 10, S or G (Summer or GM time) plus hh:mm, on/off times, ON or OFF depending
whether roomstat setting is satisfied.
OUTPUT FROM UDP
Cryptic to reduce RAM usage!
time in minutes, / plus on/off times, T plus temp * 10, S plus switch code,
A plus advance code
*/
//Assignment of Arduino pins:
//D0 = serial RX (not connected)
//D1 = "running" (serial comms TX)
//D2 = advance LED - will flash if advance is activated
//D3 = water LED
//D4 = used by Ethernet Shield, SD card
//D5 = heat LED
//D6 = water advance button
//D7 = heat advance button
//D8 = heat relay
//D9 = water relay
//D10= SPI (Ethernet Shield, SD card)
//D11= SPI
//D12= SPI
//D13= SPI
//D16 = output to turn off motorised valve
//D17 = output to self-reset (via an NPN transistor)
//A0 = gives heat switch position
//A1 = gives water switch position
//A2, A3 = unallocated sensor inputs or digital input/outputs
// (eg. to turn off power to motorised valve when boiler not operating)
//A2 becomes D16 and A3 becomes D17
//A2 now allocated as digital output to turn off motorised valve.
//A4 = I2C SDA (clock and temp sensors}
//A5 = I2C SCL
//Libraries:
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h> // UDP library from: bjoern@cs.stanford.edu 12/30/2008
#define UDP_TX_PACKET_MAX_SIZE 64 //increase UDP size
#include <Wire.h>
#define DS3231_I2C_ADDRESS 0x68
#define TMP102_I2C_ADDRESS 0x48 // I2C address TMP102 A0 to GND (0x48 = 72 = 1001000 for GND, 73 for vcc)
#include <SD.h>
#define SERIAL_BUFFER_SIZE 16 // reduce buffer size
//global variables:
byte advCode; //holds manual advance status
int tim; //current (hours x 60 + minutes) for daily timed periods
int setTemp = 180; //180 is default temp for thermostat if SD card fails
const byte hysteresis = 3; //used in thermostat function - is this enough?
char gmtBst[2]; //holds GMT or BST
//char tNowStr[7];
//boolean sndRec = false;
boolean newChangeH = false;
boolean newChangeW = false;
boolean oldChangeH = false;
boolean oldChangeW = false;
boolean advanceH = false;
boolean advanceW = false;
boolean heat1 = false;
boolean heat2 = false;
boolean water1 = false;
boolean water2 = false;
boolean heat = false;
boolean water = false;
boolean dataAdded = true;
//needed for data logging which does not work!
// MAC address for "genuine" ethernet shield is 90:A2:DA:0D:2C:EC
// The IP address will be dependent on the local network:
byte mac[] = {
0x90, 0xA2, 0xDA, 0x0D, 0x2C, 0xEC }; // test Ethernet Shield
IPAddress ip(xxxx); // enter your address
unsigned int localPort = xxxx; // enter your local port to listen on
char packetBuffer[UDP_TX_PACKET_MAX_SIZE]; //buffer to hold incoming packet,
//char timStr[7];
char hourStr[7];
char dayStr[7];
char monthStr[7];
char clockSetString[18];
char onOffString[48];
char newonOffString[64];
char tempSetString[4];
char tNowStr[7];
byte sStart[5];
byte sEnd[5];
File myFile;
// An EthernetUDP instance to let us send and receive packets over UDP
EthernetUDP Udp;
/////////////////////////////////////////////////////////////////////
void setup() {
pinMode(8, OUTPUT);
pinMode(9, OUTPUT);
pinMode(5, OUTPUT);
pinMode(3, OUTPUT);
pinMode(2, OUTPUT);
pinMode(6, INPUT);
pinMode(7, INPUT);
pinMode(16, OUTPUT); // formally analog 2
pinMode(17, OUTPUT); // formally analog 3
digitalWrite(6, HIGH);
digitalWrite(7, HIGH);
//digitalWrite(16, HIGH); // high only at end of day to deactivate valve
// load dates for summer time
// start 2015, end 2018
// start in March, end in October
sStart[0] = 29;
sStart[1] = 27;
sStart[2] = 26;
sStart[3] = 25;
sEnd[0] = 25;
sEnd[1] = 30;
sEnd[2] = 29;
sEnd[3] = 28;
// start the Ethernet, UDP, Serial and I2C:
Ethernet.begin(mac,ip);
Udp.begin(localPort);
Wire.begin();
Serial.begin(9600);
//Serial.println("Initializing SD card...");
// On the Ethernet Shield, CS is pin 4. It's set as an output by default.
// Note that even if it's not used as the CS pin, the hardware SS pin
// (10 on most Arduino boards, 53 on the Mega) must be left as an output
// or the SD library functions will not work.
pinMode(10, OUTPUT);
SD.begin(4);
//////////////////////////////////////////////////
// open file and get values
myFile = SD.open("TEMPROG1.txt");
if (myFile) {
//Serial.println("TEMPROG1.txt:");
// read from the file until there's nothing else in it:
byte index = 0;
while (myFile.available()) {
tempSetString[index] = myFile.read();
index++;
//Serial.println("reading..");
}
// close the file:
myFile.close();
setTemp = atoi(tempSetString);
}
//////////////////////////////////////////////////
// open file and get values
myFile = SD.open("CHPROG1.txt");
if (myFile) {
//Serial.println("CHPROG1.txt:");
// read from the file until there's nothing else in it:
byte index = 0;
while (myFile.available()) {
newonOffString[index] = myFile.read();
index++;
//Serial.println("reading..");
}
// close the file:
myFile.close();
for(index = 0; index < 48; index++){
onOffString[index] = newonOffString[index];
}
////////////////////////////////////////////////////////
// set the initial time here ONLY USE GMT!!:
// DS3231 seconds, minutes, hours, day, date, month, year
//setDS3231time(0,14,22,2,17,8,15);
}
}
// End of setup()
//Functions start here
///////////////////////////////////////////////////////////////////////////////////
// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val)
{
return( (val/10*16) + (val%10) );
}
////////////////////////////////////////////////////////////////////////////////////
// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val)
{
return( (val/16*10) + (val%16) );
}
////////////////////////////////////////////////////////////////////////////////
void readDS3231time(byte *second,
byte *minute,
byte *hour,
byte *dayOfWeek,
byte *dayOfMonth,
byte *month,
byte *year)
{
Wire.beginTransmission(DS3231_I2C_ADDRESS);
Wire.write(0); // set DS3231 register pointer to 00h
Wire.endTransmission();
Wire.requestFrom(DS3231_I2C_ADDRESS, 7);
// request seven bytes of data from DS3231 starting from register 00h
*second = bcdToDec(Wire.read() & 0x7f);
*minute = bcdToDec(Wire.read());
*hour = bcdToDec(Wire.read() & 0x3f);
*dayOfWeek = bcdToDec(Wire.read());
*dayOfMonth = bcdToDec(Wire.read());
*month = bcdToDec(Wire.read());
*year = bcdToDec(Wire.read());
}
///////////////////////////////////////////////////////////////////////////////////
//function to extract values from onOffString and convert to minutes
int getTimes(byte index) {
char selectorString[3];
selectorString[0] = onOffString[index];
index++;
selectorString[1] = onOffString[index];
int result = atoi(selectorString);
index++;
index++;
selectorString[0] = onOffString[index];
index++;
selectorString[1] = onOffString[index];
result = result*60 + atoi(selectorString);
return result;
}
//////////////////////////////////////////////////////////////////////////
//function to extract values from clockSetString
//see function "adjClock()"
byte getClock(byte index) {
char selectorString[3];
selectorString[0] = clockSetString[index];
index++;
selectorString[1] = clockSetString[index];
int i = atoi(selectorString);
byte result = (byte) i;
return result;
}
/////////////////////////////////////////////////////////////////////////
//function to return the two switch positions coded to numbers 0 to 8
//see documentation / circuit diagram for explanation
//analog val for continuous is >500
//analog val for off is < 50
//analog value for timed is somewhere in between 50 and 500 (approx 317)
byte getSwitchPositions() {
int valHeat = analogRead(0);
int valWater = analogRead(1);
if(valHeat < 50){
valHeat = 2; //off
}
else if(valHeat > 500){
valHeat = 3; // continuous
}
else {
valHeat = 1; // timed
}
if(valWater < 50){
valWater = 2;
}
else if(valWater > 500){
valWater = 3;
}
else {
valWater = 1;
}
return valWater * 3 + valHeat - 4;
}
/////////////////////////////////////////////////////////////////////////////
// determine whether it's BST or GMT
boolean isItSummer(byte year, byte month, byte day, byte hour) {
boolean summer = false;
year = year - 15; // list of dates starts in 2015, array index starts at 0
byte startDate = sStart[year];
byte endDate = sEnd[year];
if(month > 3 && month < 10) {
summer = true;
}
if(month == 3 && day > startDate) {
summer = true;
}
if(month == 3 && day == startDate && hour > 1){
summer = true;
}
if(month == 10 && day < endDate){
summer = true;
}
if(month == 10 && day == endDate && hour < 2){
summer = true;
}
return summer;
}
///////////////////////////////////////////////////////////////////////////
int getTemp102(){
byte firstbyte, secondbyte; //these are the bytes we read from the TMP102 temperature registers
int val; /* an int is capable of storing two bytes, this is where we "chuck" the two bytes together. */
float convertedtemp; /* We then need to multiply our two bytes by a scaling factor, mentioned in the datasheet. */
//float correctedtemp;
// The sensor overreads? I don't think it does!
/* Reset the register pointer (by default it is ready to read temperatures)
You can alter it to a writeable register and alter some of the configuration -
the sensor is capable of alerting you if the temperature is above or below a specified threshold. */
Wire.beginTransmission(TMP102_I2C_ADDRESS); // start talking to sensor
Wire.write(0x00);
Wire.endTransmission();
Wire.requestFrom(TMP102_I2C_ADDRESS, 2);
Wire.endTransmission();
firstbyte = (Wire.read());
/*read the TMP102 datasheet - here we read one byte from
each of the temperature registers on the TMP102*/
secondbyte = (Wire.read());
/*The first byte contains the most significant bits, and
the second the less significant */
val = firstbyte;
if ((firstbyte & 0x80) > 0) {
val |= 0x0F00;
}
val <<= 4;
/* MSB */
val |= (secondbyte >> 4);
// LSB is ORed into the second 4 bits of our byte.
convertedtemp = val*0.625; // temp x 10
//correctedtemp = convertedtemp - 0; //should be 5 according to playground author
int temp = (int)convertedtemp;
return temp;
}
/////////////////////////////////////////////////////////////////////////////
//function incorporates a thermostatic function
boolean thermostat(int targetT){
boolean heating;
int tmp = getTemp102();
//Serial.println(tmp);
if(tmp >= (targetT + hysteresis)){
heating = false;
}
if(tmp < (targetT - hysteresis)){
heating = true;
}
return heating;
}
//////////////////////////////////////////////////////////////////////////////
// adjusts clock
void adjClock(){
byte minutes = getClock(0);
byte hours = getClock(3);
byte day = getClock(6);
byte date = getClock(9);
byte month = getClock(12);
byte year = getClock(15);
minutes = minutes/10 * 16 + minutes % 10;
hours = hours /10 * 16 + hours % 10;
day = day / 10 * 16 + day % 10;
date = date / 10 * 16 + date % 10;
month = month / 10 * 16 + month % 10;
year = year / 10 * 16 + year % 10;
Wire.beginTransmission(DS3231_I2C_ADDRESS);
Wire.write(0); // set next input to start at the seconds register
Wire.write(0); // set seconds
Wire.write(minutes); // set minutes
Wire.write(hours); // set hours
Wire.write(day); // set day of week (1=Sunday, 7=Saturday)
Wire.write(date); // set date (1 to 31)
Wire.write(month); // set month
Wire.write(year); // set year (0 to 99)
Wire.endTransmission();
//setDS3231time(0, minutes, hours, day, date, month, year);
//setDS3231time(0, minutes, 22, 6, 14, 8, 15);
}
//////////////////////////////////////////////////////////////////////////////
void loop() {
int tempNow = getTemp102();
Serial.print("T");
Serial.print(tempNow);
//Serial.print(" deg C");
itoa(tempNow, tNowStr, 10);
//Serial.print(tNowStr);
// Here is where the progam checks to see if data is being sent
// and sends back status data
// if there's data available, read a packet
int packetSize = Udp.parsePacket();
if(packetSize)
{
//Serial.print("Received packet of size ");
//Serial.println(packetSize);
//Serial.print("From ");
//sndRec = true;
IPAddress remote = Udp.remoteIP();
for (byte i =0; i < 4; i++)
{
Serial.print(remote[i], DEC);
if (i < 3)
{
Serial.print(".");
}
}
Serial.print(",P");
Serial.println(Udp.remotePort());
if(packetSize == 47 || packetSize == 17 || packetSize == 4 || packetSize == 3 || packetSize == 2){
// read the packet into packetBufffer
Udp.read(packetBuffer,UDP_TX_PACKET_MAX_SIZE);
//Serial.println("Contents:");
Serial.println(packetBuffer);
//get new timer on/off times and save to SD card
if(packetSize == 47){
//save to SD card
SD.remove("CHPROG1.txt"); //first delete previous file
myFile = SD.open("CHPROG1.txt", FILE_WRITE);
if (myFile) {
myFile.print(packetBuffer);
//int numChars;
//numChars = myFile.print(packetBuffer);
//Serial.write("number of characters written is ");
//Serial.println(numChars);
myFile.close();
for (byte x = 0; x<48; x++){
onOffString[x] = packetBuffer[x];
}
}
}
/////////////////////////////////////////////////////////////
//get new clock settings
//format: mm,hh,dd,dd,mm,yy
if(packetSize == 17){
for(byte x = 0; x < 18; x++){
clockSetString[x] = packetBuffer[x];
}
//Serial.println(clockSetString);
//already printed!
adjClock();
}
///////////////////////////////////////////////////////////
//reset Arduino
if(packetSize == 4){
digitalWrite(17, HIGH); // thats all folks!
}
////////////////////////////////////////////////////////////
//get new thermostat setting
//format: ttt (TdegC x 10)
if(packetSize == 3){
setTemp = atoi(packetBuffer);
//Serial.println(setTemp);
//save to SD card
SD.remove("TEMPROG1.txt"); //first delete previous file
myFile = SD.open("TEMPROG1.txt", FILE_WRITE);
if (myFile) {
//myFile.print(tempSetString);
myFile.print(packetBuffer);
myFile.close();
}
}
////////////////////////////////////////////////////////////////////////
//respond to advance command
if(packetSize == 2){
if(packetBuffer[0] == 'Y'){
advanceH = true;
}
else{
advanceH = false;
}
if(packetBuffer[1] == 'Y'){
advanceW = true;
}
else{
advanceW = false;
}
}
}
// send a reply, to the IP address and port that sent us the packet we received
int switches = getSwitchPositions();
char swStr[7];
itoa(switches, swStr, 10);
char tNowStr[7];
itoa(tempNow, tNowStr, 10);
char setTempStr[7];
itoa(setTemp, setTempStr, 10);
char advCodeStr[7];
itoa(advCode, advCodeStr, 10);
char timStr[7];
itoa(tim, timStr, 10);
Udp.beginPacket(Udp.remoteIP(), Udp.remotePort());
//Udp.write("*");
Udp.write(timStr);
Udp.write("/");
Udp.write(onOffString);
Udp.write("T");
Udp.write(tNowStr);
Udp.write("S");
Udp.write(swStr);
Udp.write("A");
Udp.write(advCodeStr);
Udp.write("t");
Udp.write(setTempStr);
//should develop this to report status, errors etc
Udp.endPacket();
}
// End of section checking whether data is being sent
////////////////////////////////////////////////////////////////////
//displayTime();
Serial.println();
byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
// retrieve data from DS3231
readDS3231time(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year);
boolean summer;
summer = isItSummer(year,month,dayOfMonth,hour);
if(summer == true){
strcpy(gmtBst, "S");
if(hour == 23){
hour = 0;
}
else {
hour = hour + 1;
}
}
else{
strcpy(gmtBst, "G");
}
Serial.print(gmtBst);
Serial.print(hour);
Serial.print(":");
Serial.print(minute);
Serial.println();
Serial.println(onOffString);
tim = hour*60 + minute;
//itoa(tim, timStr, 10);
//char hourStr[7];
itoa(hour, hourStr, 10);
itoa(dayOfMonth, dayStr, 10);
itoa(month, monthStr, 10);
//char minuteStr[7];
//itoa(minute, minuteStr, 10);
//////////////////////////////////////////////////////////////////
//data log section
//data logged every hour, one minute past hour
if(minute == 1 && dataAdded == false){
//save to SD card
myFile = SD.open("DATALOG1.txt", FILE_WRITE);
if (myFile) {
myFile.print(hour);
myFile.print(":");
myFile.print(dayStr);
myFile.print(":");
myFile.print(monthStr);
myFile.print(":");
myFile.print(tNowStr);
myFile.print(",");
}
dataAdded = true;
}
myFile.close();
if(minute != 1){
dataAdded = false;
}
///////////////////////////////////////////////////////////////
//Main logic for interaction with hardware starts here!
// ensures valve is deactivated at end of day
if(tim == 0){
digitalWrite(16, HIGH);
}
else{
digitalWrite(16, LOW);
}
//initialise variables holding on/off times
int waterOn1 = getTimes(0);
int waterOff1 = getTimes(6);
int waterOn2 = getTimes(12);
int waterOff2 = getTimes(18);
int heatingOn1 = getTimes(24);
int heatingOff1 = getTimes(30);
int heatingOn2 = getTimes(36);
int heatingOff2 = getTimes(42);
digitalWrite (2, LOW); //advance LED
// get switch positions
// if off or cont, LEDs will flash during a timed period set in memory
int valHeat = analogRead(0);
int valWater = analogRead(1);
if(valHeat < 50 || valHeat > 500) //off or cont
{
digitalWrite(5,LOW); //heat LED off - LED will flash during an "on" timed period
delay(200);
}
if(valWater < 50 || valWater > 500) //off or cont
{
digitalWrite(3,LOW); //water LED off - LED will flash during an "on" timed period
delay(200);
}
////////////////////////////////////////////////////////////////
//Check to see if advance button (heating) is pressed and
//check times to see if boiler should be on
//set various boolean flags accordingly viz. heat1, heat2 advanceH, oldChangeH and newChangeH
byte val = digitalRead(7); //heat advance button
delay(100);
if (val == LOW){
advanceH = true;
}
oldChangeH = newChangeH; //used to detect status chages due to the timer
//to decide if advance is valid
if (tim >= heatingOn1 && tim < heatingOff1)
{
heat1 = true;
}
else
{
heat1 = false;
}
if (tim >= heatingOn2 && tim < heatingOff2)
{
heat2 = true;
}
else
{
heat2 = false;
}
/////////////////////////////////////////////////////////////////////
//Do the same for water
val = digitalRead(6); //water advance button
delay(100);
if (val == LOW){
advanceW = true;
}
oldChangeW = newChangeW;
if (tim >= waterOn1 && tim < waterOff1)
{
water1 = true;
}
else
{
water1 = false;
}
if (tim >= waterOn2 && tim < waterOff2)
{
water2 = true;
}
else
{
water2 = false;
}
////////////////////////////////////////////////////
//Work out if heat should be on according to timed periods and advance
if (heat1 == true || heat2 == true)
{
heat = true;
}
else
{
heat = false;
}
newChangeH = heat;
if (newChangeH != oldChangeH) //on/off status has changed
{
advanceH = false;
}
if (advanceH)
{
heat = !heat; //if advance is true, negate the boiler state set by the state of the timer
}
// check to see if boiler should be on comparing temp sensor against target temp
if (heat)
{
digitalWrite(5, HIGH); // heat LED
boolean thermo = thermostat(setTemp);
if(thermo){ // if temperature below target less hysteresis
digitalWrite(8,HIGH); // heat relay on
Serial.println("ON");
}
if(!thermo) {
digitalWrite(8, LOW); // heat relay off
Serial.println("OFF");
}
}
if(!heat)
{
digitalWrite(8, LOW); // heat relay off
digitalWrite(5, LOW); // heat LED off
}
///////////////////////////////////////////////
//Similarly for water except that, with regard to temperature,
//the themostat is wired into the boiler circuitry
//and not subject to computer control
if (water1 == true || water2 == true)
{
water = true;
}
else
{
water = false;
}
newChangeW = water;
//check to see if advance is still valid
if (newChangeW != oldChangeW)
{
advanceW = false;
}
if (advanceW)
{
water = !water;
}
if (water)
{
digitalWrite(9, HIGH);
digitalWrite(3, HIGH);
}
else
{
digitalWrite(9, LOW);
digitalWrite(3, LOW);
}
if (advanceH || advanceW)
{
digitalWrite (2, HIGH);
}
//encode the switch positions ready to broadcast over the network
advCode = 0;
if(advanceH){
advCode = 1;
}
if(advanceW){
advCode = 2;
}
if(advanceH && advanceW){
advCode = 3;
}
}
//end of loop
//and program!