/* 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); } } }