/*
UNO MQTT HVAC Controller
Aaron Cake
http://www.aaroncake.net
Runs on Nano, Uno, etc.
Works with ENC28J60, W5100, W5500 etc. Any standard Arduino Ethernet library
Controls 3 relays on 4 relays shield to switch fan, heat and cool on furnace via MQTT commands
Relay 1: Pin 7: Humidifier
Relay 2: Pin 6: A/C
Relay 3: Pin 5: Heat
Relay 4: Pin 4: Blower
Uses 3 MQTT topics:
/furnace/blower/
/furnace/heat/
/furnace/cool/
/fuernace/humidifier/
Payload to turn on each item is 1, off is 0
Publishes states with 1 for on, 0 for off. States depend on the mode.
/furnace/blowerstate ....will be on (1) if /blower or /cooling is on (1)
/furnace/heatstate .....will only be on (1) if /heat is on (1)
/furnace/coolstate ....will only be on (1) if /cool is on (1)
Example:
/furnace/cool/ payload 1 is received
Controller will:
-turn off heat relay
-turn on blower relay
-turn on cool relay
-read the state of all three relays from their pins by digitalRead
-publish:
-/furnace/heatstate/ = 0
-/furnace/blowerstate = 1
-/furnace/coolstate = 1
Each mode mutually exclusive. Cannot have /furnace/cool/ = 1 and /furnace/heat/ = 1 at same time. If /furnace/heat/ = 1 and /furnace/cool/ = 1 is published
then controller will turn off /heat and turn on /cool
Humidifier control is independent of other states and simply switches relay on and off
Requires PubSubClient found here: https://github.com/knolleary/pubsubclient
Version 1 - 7/10/2018:
-Initial functionality
-Allow mutually exclusive control of 3 HVAC relays via MQTT
Version 2 - Dec. 2020:
-Add humidifier control (non-mutually exclusive)
-Memory optimization - change pin ints to byte
-Add telemetry
-Rewrite to take all MQTT publishing out of MQTT callback function
*/
#include <avr/wdt.h> //AVR built in watch dog timer
#include <MemoryFree.h> //library with free memory function
#include <PubSubClient.h> //MQTT client
//#include <UIPEthernet.h> //Ethernet library for ENC28J60
//libraries needed for W5500 or 5100
#include <SPI.h>
//#include <Ethernet2.h> //Ethernet library for W5500
#include <Ethernet.h> //Ethernet library for W5100
void callback(char* topic, byte* payload, unsigned int length);
//MQTT sever address
#define MQTT_SERVER "192.168.107.11"
// define these because some relay boards are active LOW, some active HIGH
#define RELAY_ON HIGH
#define RELAY_OFF LOW
//firmware version
const float FirmwareVersion = 2.00;
const byte BlowerPin = 4; //relay for blower on pin 4
const byte HeatPin = 5; //relay for heat on pin 5
const byte CoolPin = 6; //relay for A/C on pin 6
const byte HumidifierPin = 7; //relay for humidifier on pin 7
char const* MQTTClientName = "furnace1"; //MQTT client name. Keep short, don't waste RAM
const char BlowerTopic[] = "/furnace/blower/";
const char HeatTopic[] = "/furnace/heat/";
const char CoolTopic[] = "/furnace/cool/";
const char HumidifierTopic[] = "/furnace/humidifier/";
const char BlowerState[] = "/furnace/blowerstate/"; //MQTT topic to publish blower state on, 1=on, 0 = off
const char HeatState[] = "/furnace/heatstate/"; //MQTT topic to publish heat state on, 1=on, 0 = off
const char CoolState[] = "/furnace/coolstate/"; //MQTT topic to publish cool state on, 1=on, 0 = off
const char HumidifierState[] = "/furnace/humidifierstate/"; //MQTT topic to publish humidifier state, 1 = on, 0 = off
const char StateOff[] = "0"; //constant chars to hold states off and on
const char StateOn[] = "1";
bool DoPublish = false; //variable to track when states should be published via MQTT
//telemetry and uptime variables
int UptimeSeconds = 0; //uptime counter seconds, 0-60
int UptimeMinutes = 0; //uptime counter minutes, 0-60
int UptimeHours = 0; //uptime counter hours, 0 - 24
int UptimeDays = 0; //uptime counter days, 0 - maxint...32,000 day uptime? Unlikely
unsigned long UptimePreviousMillis = 0; //the last time the uptime counter updated
bool SendHeartbeat = false; //flag to send the heartbeat. Set true in uptime loop to send a heartbeat
const char TelemetryTopic[] = "/furnace/tele/"; //telemetry heartbeat topic
char HeartBeatPublishString[100]; //char array to hold heartbeat for MQTT publish, in JSON
//*************************************************
// Make sure to assign a unique MAC to each board!
// I usually just randomly make one up
//*************************************************
//Used:
uint8_t mac[6] = { 0xAA, 0xC1, 0x6B, 0xA3, 0x7D, 0xD5 };
//Initialize ethernet client as ethClient
EthernetClient ethClient;
//initialize PubSubClient as "MQTTClient"
PubSubClient MQTTClient(MQTT_SERVER, 1883, callback, ethClient);
//function to reset the watchdog timer
void PetWatchdog() {
wdt_reset();
//Serial.println("Pet");
}
void UptimeCounter() {
UptimeSeconds++; //increase uptime seconds by 1
if (UptimeSeconds >= 60) { //if seconds are greater than 60 that's a minute
UptimeMinutes++; //increase minutes by 1
UptimeSeconds = 0; //reset seconds to zero
SendHeartbeat = true; //set the flag to send the heartbeat publish
}
if (UptimeMinutes >= 60) { //if minutes > 60 that's an hour
UptimeHours++; //increase hour count
UptimeMinutes = 0; //reset minutes to zero
}
if (UptimeHours >= 24) {
UptimeDays++;
UptimeHours = 0;
}
if (SendHeartbeat == true) { //if the heartbeat flag is set (on minute changeover), send heartbeat
Heartbeat();
SendHeartbeat = false; //set the flag false to it doesn't run until next minute
}
//Serial.print(F("Uptime (DD:HH:MM:SS): ")); //print out uptime every time function called
//Serial.print(UptimeDays);
//Serial.print(F(":"));
//Serial.print(UptimeHours);
//Serial.print(F(":"));
//Serial.print(UptimeMinutes);
//Serial.print(F(":"));
//Serial.println(UptimeSeconds);
}
//function to generate heartbeat ping on topic /boardname/tele/
void Heartbeat() {
//ArduinoJSON exists as a library to generate and serialise JSON. Not doing anything complicated so can do it in less memory manually
//Bunch of sprintfs/strcats to build the heartbeat string.
//create an uptime in ISO 8601 format:
/*P is the duration designator(for period) placed at the start of the duration representation.
Y is the year designator that follows the value for the number of years.
M is the month designator that follows the value for the number of months.
W is the week designator that follows the value for the number of weeks.
D is the day designator that follows the value for the number of days.
T is the time designator that precedes the time components of the representation.
H is the hour designator that follows the value for the number of hours.
M is the minute designator that follows the value for the number of minutes.
S is the second designator that follows the value for the number of seconds.
For example, "P3Y6M4DT12H30M5S" represents a duration
of "three years, six months, four days, twelve hours, thirty minutes, and five seconds".*/
//generate JSON string with sprintf_p . Format string stored in PROGMEM
sprintf_P(HeartBeatPublishString, PSTR("{\"ip\":\"%d.%d.%d.%d\",\"uptime\":\"P%dDT%dH%dM%dS\",\"ver\":\"%d.%02d\",\"freemem\":\"%d\"}"),
Ethernet.localIP()[0], Ethernet.localIP()[1], Ethernet.localIP()[2], Ethernet.localIP()[3],
UptimeDays, UptimeHours, UptimeMinutes, UptimeSeconds,
(int)FirmwareVersion, (int)(FirmwareVersion * 100) % 100, //sprintf_p doesn't do floats, so need to convert it to two ints to retain decimal
(int)freeMemory());
Serial.print(F("HEARTBEAT: ")); //print it out to the serial for debugging
Serial.println(HeartBeatPublishString);
//publish the heartbeat string
Serial.print(F("MQTT: Heartbeat publish: "));
Serial.print(TelemetryTopic);
Serial.print(F(" : "));
Serial.println(HeartBeatPublishString);
MQTTClient.publish(TelemetryTopic, HeartBeatPublishString);
}
void setup() {
//open the serial port for debugging
Serial.begin(9600);
delay(100);
Serial.println(F("BOOT..."));
//Serial.println(strlen(StateOff));
wdt_enable(WDTO_8S); //enable watchdog with 8 second timeout
Serial.println(F("WATCHDOG: Enabled. Pet every 8 seconds please"));
//initialize all the pins used for relays and turn them off
digitalWrite(BlowerPin, RELAY_OFF); //turn relay pins to RELAY_OFF state before setting pinMode
digitalWrite(HeatPin, RELAY_OFF);
digitalWrite(CoolPin, RELAY_OFF);
digitalWrite(HumidifierPin, RELAY_OFF);
pinMode(BlowerPin, OUTPUT); //set relay pins to mode OUTPUT
pinMode(HeatPin, OUTPUT);
pinMode(CoolPin, OUTPUT);
pinMode(HumidifierPin, OUTPUT);
Serial.println(F("ETHERNET: Begin..."));
PetWatchdog(); //pet the dog
// start the Ethernet and obtain an address via DHCP
if (Ethernet.begin(mac) == 0) {
Serial.println(F("ETHERNET: Failed. No DHCP. Rebooting."));
delay(2000);
for (;;); //loop forever so the watchdog resets the processor
}
Serial.print(F("ETHERNET: DHCP Success. IP: "));
Serial.println(Ethernet.localIP());
//attempt to connect to MQTT broker
ConnectToMQTTBroker();
//wait a bit before starting the main loop
delay(2000);
}
void loop() {
PetWatchdog(); //pet the dog
unsigned long CurrentMillis = millis(); //get the current time count
if (!MQTTClient.connected()) {
ConnectToMQTTBroker();
}
PetWatchdog(); //pet the dog
//maintain MQTT connection
MQTTClient.loop();
//if publish flag is set, then publish states
if (DoPublish == true) {
PublishStates();
}
//uptime counter timer
if (CurrentMillis - UptimePreviousMillis >= 1000) { //if 1 second (1000ms) has passed since last run, increase the uptime
UptimePreviousMillis = CurrentMillis; //update the time uptime was last updated
UptimeCounter(); //run the uptime function
}
}
void PublishStates() {
//publish the states of blower, heat, cool, humidifier
if (digitalRead(BlowerPin) == RELAY_OFF) { //read BlowerPin and publish 0 if off, 1 if on
MQTTClient.publish(BlowerState, StateOff);
}
else if (digitalRead(BlowerPin) == RELAY_ON) {
MQTTClient.publish(BlowerState, StateOn);
}
Serial.print(F("MQTT: Publish: ")); //write MQTT publish info to serial
Serial.print(BlowerState);
Serial.print(F(" : "));
Serial.println(digitalRead(BlowerPin));
PetWatchdog(); //pet the dog
//heating
if (digitalRead(HeatPin) == RELAY_OFF) { //and do the same for heat and cool
MQTTClient.publish(HeatState, StateOff);
}
else if (digitalRead(HeatPin) == RELAY_ON) {
MQTTClient.publish(HeatState, StateOn);
}
Serial.print(F("MQTT: Publish: ")); //write MQTT publish info to serial
Serial.print(HeatState);
Serial.print(F(" : "));
Serial.println(digitalRead(HeatPin));
//cooling
if (digitalRead(CoolPin) == RELAY_OFF) {
MQTTClient.publish(CoolState, StateOff);
}
else if (digitalRead(CoolPin) == RELAY_ON) {
MQTTClient.publish(CoolState, StateOn);
}
Serial.print(F("MQTT: Publish: ")); //write MQTT publish info to serial
Serial.print(CoolState);
Serial.print(F(" : "));
Serial.println(digitalRead(CoolPin));
//humidifier
if (digitalRead(HumidifierPin) == RELAY_OFF) {
MQTTClient.publish(HumidifierState, StateOff);
}
else if (digitalRead(HumidifierPin) == RELAY_ON) {
MQTTClient.publish(HumidifierState, StateOn);
}
Serial.print(F("MQTT: Publish: ")); //write MQTT publish info to serial
Serial.print(HumidifierState);
Serial.print(F(" : "));
Serial.println(digitalRead(HumidifierPin));
DoPublish = false; //states have been published so set the publish flag to false
}
void callback(char* topic, byte* payload, unsigned int length) {
//callback is fired whenever PubSubClient (client) receives an MQTT message from MQTT_SERVER
//Note: the "topic" value gets overwritten everytime it receives confirmation (callback) message from MQTT
//Print out some debugging info
Serial.print(F("MQTT: Callback. Topic: "));
Serial.println(topic);
PetWatchdog(); //pet the dog
//handle the callback and payload. Turn on/off appropriate outputs
//blower
if (strcmp(topic, BlowerTopic) == 0) { //payload is b, so turn everything off but blower
if (payload[0] == '1') {
digitalWrite(HeatPin, RELAY_OFF); //heat off
digitalWrite(CoolPin, RELAY_OFF); // A/C off
delay(1000); //relays are slow
digitalWrite(BlowerPin, RELAY_ON); //blower ON
}
else if (payload[0] == '0') {
digitalWrite(BlowerPin, RELAY_OFF); //blower OFF
}
DoPublish = true; //publish states
}
//heat
if (strcmp(topic, HeatTopic) == 0) {
if (payload[0] == '1') {
digitalWrite(BlowerPin, RELAY_OFF); //blower off...furnace turns on automatically for heat
digitalWrite(CoolPin, RELAY_OFF); // A/C off
delay(1000); //relays are slow
digitalWrite(HeatPin, RELAY_ON); //heat ON
}
else if (payload[0] == '0') {
digitalWrite(HeatPin, RELAY_OFF); //heat OFF
}
DoPublish = true; //publish states
}
//cooling
if (strcmp(topic, CoolTopic) == 0) {
if (payload[0] == '1') {
digitalWrite(HeatPin, RELAY_OFF); //heat off
delay(1000); //relays are slow
digitalWrite(BlowerPin, RELAY_ON); //turn on the blower, AC doesn't do automatically
delay(1000); //relays are slow
digitalWrite(CoolPin, RELAY_ON); //cool ON
}
else if (payload[0] == '0') {
digitalWrite(CoolPin, RELAY_OFF); //cool OFF
digitalWrite(BlowerPin, RELAY_OFF);
}
DoPublish = true; //publish states
}
//humidifier
if (strcmp(topic, HumidifierTopic) == 0) { //humidifier. independently controlled
if (payload[0] == '1') {
digitalWrite(HumidifierPin, RELAY_ON); //humidifier ON
}
else if (payload[0] == '0') {
digitalWrite(HumidifierPin, RELAY_OFF); //humidifier OFF
}
DoPublish = true; //publish states
}
Serial.print(F("PIN: "));
Serial.print(HeatPin);
Serial.print(F(": "));
Serial.println(digitalRead(HeatPin));
Serial.print(F("PIN: "));
Serial.print(CoolPin);
Serial.print(F(": "));
Serial.println(digitalRead(CoolPin));
Serial.print(F("PIN: "));
Serial.print(BlowerPin);
Serial.print(F(": "));
Serial.println(digitalRead(BlowerPin));
Serial.print(F("PIN: "));
Serial.print(HumidifierPin);
Serial.print(F(": "));
Serial.println(digitalRead(HumidifierPin));
PetWatchdog(); //pet the dog
}
void ConnectToMQTTBroker() {
// Loop until connected to MQTT broker
while (!MQTTClient.connected()) {
Serial.print(F("MQTT: Attempting MQTT connection: "));
Serial.println(MQTT_SERVER);
Serial.print(F("."));
//if connected, subscribe to the furnace topics
if (MQTTClient.connect((char*)MQTTClientName)) {
Serial.println(F("MQTT: Connected."));
PetWatchdog(); //pet the dog
MQTTClient.subscribe(BlowerTopic); //Subscribe to BlowerTopic
Serial.print(F("MQTT: Subscribe: "));
Serial.println(BlowerTopic);
MQTTClient.subscribe(HeatTopic); //Subscribe to HeatTopic
Serial.print(F("MQTT: Subscribed: "));
Serial.println(HeatTopic);
MQTTClient.subscribe(CoolTopic); //subscribe to CoolTopic
Serial.print(F("MQTT: Subscribed: "));
Serial.println(CoolTopic);
MQTTClient.subscribe(HumidifierTopic); //subscribe to Humidifier
Serial.print(F("MQTT: Subscribed: "));
Serial.println(HumidifierTopic);
PublishStates();
}
//otherwise print failed for debugging
else {
PetWatchdog(); //pet the dog
Serial.println(F("\tMQTT: Subscribe failed. Retry in 3 seconds..."));
delay(3000);
}
}
}