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