
// pre compiletime config
#define VERSION "0.1.2"

#define DEVICE_NAME "WIFI-THERMOSTAT"

#define PIN_DHTSENSOR 13
#define PIN_RELAIS 16
#define PIN_BUTTON_PLUS 0
#define PIN_BUTTON_MINUS 2
#define PIN_BUTTON_MODE 14
#define PIN_PIRSENSOR 12

#define RELAISONSTATE LOW
#define BUTTONONSTATE LOW

#define DHTTYPE DHT22 // DHT sensor type
#define LCDADDR 0x27  // I2C address LCD


#define DEBUG_SERIAL //uncomment for Serial debugging statements

#ifdef DEBUG_SERIAL
#define DEBUG_BEGIN Serial.begin(115200)
#define DEBUG_PRINT(x) Serial.println(x)
#else
#define DEBUG_PRINT(x)
#define DEBUG_BEGIN
#endif

#include "PersWiFiManager.h"

#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <ESP8266HTTPUpdateServer.h>
#include "PubSubClient.h"

#include <Wire.h>
#include "LiquidCrystal_I2C.h"

#include <DNSServer.h>
#include <FS.h>

//#include <AS_BH1750.h> // BH1750 lux sensor
#include "DHT.h"



// global variables
float humidity, temperature;
bool turnHeatingOn = false;
bool heatingEnabled = true;
byte heatingMode = 1; // 0 = off, 1 = default/day, 2 = night/reduction

float setTemp;
long heatingLastOnMillis;
long heatingLastOffMillis;

float aTemp, aHum;
long aTempHumLastUpdate;
byte whichTempToDisplay; // 1=temp inside (from DHT sensor), 2= temp outside (via MQTT)

long lastMeasure = 0;
long lastDisplayUpdate = 0;
long lastDisplayToggle = 0;
long lastTempUpdate = 0;
char msg[50];   // buffer MQTT in payload
char topic[50]; // buffer MQTT in topic

// global variables from other modules
//extern bool displayActive;
bool displayActive = false;

// functions from other modules/files
//extern void httpServerHandleNotFound();
//extern void httpServerHandleRoot();
//extern void enableDisplay();
//extern void disableDisplay();
//extern void extendDisplayTimeout();
//extern void handleDisplayTimeout();
//extern void updateDisplay();
//extern void initDisplay();
//extern void plusButtonAction();
//extern void minusButtonAction();
//extern void modeButtonAction();
//extern void modeButtonHoldAction();
//extern void checkButtonStates();

// config variables

const char* mqtt_server = "10.1.1.11";
const int mqtt_port = 1883;
long minOffTime = 10; // s

float setTempDay = 21.5;
float setTempNight = 18.0;
float setTempMin = 14.0;
float setTempMax = 29.0;
float setTempMinLow = 10.0;
float setTempMaxLow = 19.0;
float hysteresis = 0.5;
int measureinterval = 60000; // ms
int displayinterval = 5000;
int displayTimeout = 60000;


//const char* mqtt_topic_prefix = "Test/Thermostat/";
const char* mqtt_topic_temp = "Test/Thermostat/Temp";
const char* mqtt_topic_settemp = "Test/Thermostat/SetTemp";
//const char* mqtt_topic_setmode = "Test/Thermostat/SetMode";
const char* mqtt_topic_hum = "Test/Thermostat/Hum";
const char* mqtt_topic_lux = "Test/Thermostat/Lux";
const char* mqtt_topic_heating = "Test/Thermostat/Heating";
const char* mqtt_topic_heating_lastofftime = "Test/Thermostat/HeatingLastOffTime";
const char* mqtt_topic_heating_lastontime = "Test/Thermostat/HeatingLastOnTime";

const char* mqtt_topic_in = "Test/Thermostat/in";   // subscribe topic





DHT dht(PIN_DHTSENSOR, DHTTYPE);
LiquidCrystal_I2C lcd(0x27, 16, 2); // set the LCD address to 0x27 for a 16 chars and 2 line display

WiFiClient espClient;
PubSubClient mqttclient(espClient);

ESP8266WebServer httpServer(80);
DNSServer dnsServer;
PersWiFiManager persWM(httpServer, dnsServer);

ESP8266HTTPUpdateServer httpUpdater;


// MQTT callback
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  if (strcmp(topic, mqtt_topic_in) == 0) { //if topic = mqtt_topic_in
    Serial.println("TOPIC!!");

    char tmpPayload[length + 1];
    for (int i = 0; i < length; i++) {
      tmpPayload[i] = (char)payload[i];
    }
    tmpPayload[sizeof(tmpPayload) - 1] = '\0';

    Serial.print("tmpPayload:");
    Serial.println(tmpPayload);

    if (strncmp(tmpPayload, "set temp ", 9) == 0) {
      char inValue[length - 9 + 1];
      for (int i = 0; i < length; i++) {
        inValue[i] = (char)payload[i + 9];
      }
      inValue[sizeof(inValue) - 1] = '\0';
      Serial.print(inValue);
      Serial.println();
      float invalueFloat = round(atof(inValue) * 2.0) / 2.0;

      Serial.print(invalueFloat);
      if (invalueFloat >= setTempMin && invalueFloat <= setTempMax) {
        setTempDay = invalueFloat;
        updateDisplay();
      }
      else if (invalueFloat > setTempMax) {
        setTempDay = setTempMax;
        updateDisplay();
      }
      else if (invalueFloat < setTempMin) {
        setTempDay = setTempMin;
        updateDisplay();
      }
    }// if payload=="set temp "

    else if (strncmp(tmpPayload, "set tlow ", 9) == 0) {
      char inValue[length - 9 + 1];
      for (int i = 0; i < length; i++) {
        inValue[i] = (char)payload[i + 9];
      }
      inValue[sizeof(inValue) - 1] = '\0';
      Serial.print(inValue);
      Serial.println();
      float invalueFloat = round(atof(inValue) * 2.0) / 2.0;

      Serial.print(invalueFloat);
      if (invalueFloat >= setTempMinLow && invalueFloat <= setTempMaxLow) {
        setTempNight = invalueFloat;
        updateDisplay();
      }
      else if (invalueFloat > setTempMaxLow) {
        setTempNight = setTempMaxLow;
        updateDisplay();
      }
      else if (invalueFloat < setTempMinLow) {
        setTempNight = setTempMinLow;
        updateDisplay();
      }
    }

    else if (strncmp(tmpPayload, "set mode ", 9) == 0) {
      if ((char)payload[9] == '1') {
        // switch to mode default/day
        heatingMode = 1;
        updateDisplay();
      }
      else if ((char)payload[9] == '2') {
        // switch to mode night/reduction
        heatingMode = 2;
        updateDisplay();
      }
    }

    else if (strncmp(tmpPayload, "set enab ", 9) == 0) {
      if ((char)payload[9] == '0') {
        // switch heating off
        heatingEnabled = false;
        updateDisplay();
      }
      else if ((char)payload[9] == '1') {
        // switch heating on
        heatingEnabled = true;
        updateDisplay();
      }
    }
  }//if topic = mqtt_topic_in

  if (strcmp(topic, "wetter/atemp") == 0) { //if topic = "wetter/atemp"
    char inValue[length + 1];
    for (int i = 0; i < length; i++) {
      inValue[i] = (char)payload[i];
    }
    inValue[sizeof(inValue) - 1] = '\0';
    //Serial.print(inValue);
    //Serial.println();
    float invalueFloat = atof(inValue);
    aTemp = invalueFloat;
    aTempHumLastUpdate = millis();
    //Serial.print("atemp=");
    //Serial.println(invalueFloat);
  }//if topic = "wetter/atemp"

  if (strcmp(topic, "wetter/ahum") == 0) { //if topic = "wetter/atemp"
    char inValue[length + 1];
    for (int i = 0; i < length; i++) {
      inValue[i] = (char)payload[i];
    }
    inValue[sizeof(inValue) - 1] = '\0';
    //Serial.print(inValue);
    //Serial.println();
    float invalueFloat = atof(inValue);
    aHum = invalueFloat;
    aTempHumLastUpdate = millis();
    //Serial.println("ahum=");
    //Serial.println(invalueFloat);
  }//if topic = "wetter/ahum"

}//mqttCallback



long mqttLastReconnectAttempt = 0;

boolean mqttReconnect() {
  // Create a random MQTT client ID
  String mqttClientId = "ESP8266Client-";
  mqttClientId += String(random(0xffff), HEX);
  if (mqttclient.connect(mqttClientId.c_str())) {
    Serial.println("connected");
    // Once connected, publish an announcement...
    //mqttclient.publish("TestoutTopic", "hello world");
    // ... and resubscribe
    mqttclient.subscribe(mqtt_topic_in);
    mqttclient.subscribe("wetter/atemp");
    mqttclient.subscribe("wetter/ahum");
  }
  return mqttclient.connected();
} //mqttReconnect


void setup() {
  DEBUG_BEGIN; //for terminal debugging
  DEBUG_PRINT();



  Serial.print("WiFi Thermostat v");
  Serial.print(VERSION);
  Serial.println(" by Flo Kra");
  Serial.println("starting...");

  pinMode(PIN_RELAIS, OUTPUT);
  digitalWrite(PIN_RELAIS, HIGH);
  pinMode(PIN_BUTTON_PLUS, INPUT_PULLUP);
  pinMode(PIN_BUTTON_MINUS, INPUT_PULLUP);
  pinMode(PIN_BUTTON_MODE, INPUT_PULLUP);

  // initialize DHT11/22 temp/hum sensor
  dht.begin();


  //optional code handlers to run everytime wifi is connected...
  persWM.onConnect([]() {
    DEBUG_PRINT("wifi connected");
    DEBUG_PRINT(WiFi.SSID());
    DEBUG_PRINT(WiFi.localIP());
  });
  //...or AP mode is started
  persWM.onAp([]() {
    DEBUG_PRINT("AP MODE");
    DEBUG_PRINT(persWM.getApSsid());
  });

  //allows serving of files from SPIFFS
  //SPIFFS.begin();
  //SPIFFS.format();
  //sets network name for AP mode
  persWM.setApCredentials(DEVICE_NAME);
  //persWM.setApCredentials(DEVICE_NAME, "password"); optional password

  //make connecting/disconnecting non-blocking
  persWM.setConnectNonBlock(true);

  //in non-blocking mode, program will continue past this point without waiting
  persWM.begin();

  //handles commands from webpage, sends live data in JSON format
  httpServer.on("/api", []() {
    DEBUG_PRINT("httpServer.on /api");
    if (httpServer.hasArg("plusBtn")) {
      plusButtonAction();
      DEBUG_PRINT(P("web plusBtn"));
    } //if
    if (httpServer.hasArg("minusBtn")) {
      minusButtonAction();
      DEBUG_PRINT(P("web minusBtn"));
    } //if
    if (httpServer.hasArg("modeBtn")) {
      modeButtonAction();
      DEBUG_PRINT(P("web modeBtn"));
    } //if
    if (httpServer.hasArg("onoffBtn")) {
      modeButtonHoldAction();
      DEBUG_PRINT(P("web onoffBtn"));
    } //if

    //build json object of program data
    StaticJsonBuffer<200> jsonBuffer;
    JsonObject &json = jsonBuffer.createObject();
    json["ssid"] = WiFi.SSID();
    json["setTemp"] = setTempDay;
    json["temp"] = temperature;
    json["hum"] = int(humidity);
    json["mode"] = heatingMode;
    json["heatingEnabled"] = heatingEnabled;
    json["heating"] = turnHeatingOn;

    char jsonchar[200];
    json.printTo(jsonchar); //print to char array, takes more memory but sends in one piece
    httpServer.send(200, "application/json", jsonchar);

  }); //httpServer.on /api

  //get heap status, analog input value and all GPIO statuses in one json call
  httpServer.on("/info", HTTP_GET, []() {
    String json = "{";
    json += "\"wifissid\":\"" + WiFi.SSID() + "\"";
    json += "\"heap\":" + String(ESP.getFreeHeap());
    //json += ", \"analog\":" + String(analogRead(A0));
    //json += ", \"gpio\":" + String((uint32_t)(((GPI | GPO) & 0xFFFF) | ((GP16I & 0x01) << 16)));
    json += "}";
    httpServer.send(200, "text/json", json);
    json = String();
  }); //httpServer.on /info

  httpServer.on("/", []() {
    httpServerHandleRoot();
  });

  httpServer.on("/config", []() {
    httpServerHandleConfPage();
  });

  httpServer.onNotFound([]() {
    httpServerHandleNotFound();
  }); //httpServer.onNotFound

  // HTTP Updater at /update
  httpUpdater.setup(&httpServer);


  httpServer.begin();

  DEBUG_PRINT("setup complete.");

  mqttclient.setServer(mqtt_server, mqtt_port);
  mqttclient.setCallback(mqttCallback);
  mqttLastReconnectAttempt = 0;

  initDisplay();

} //void setup

void loop() {
  persWM.handleWiFi(); //in non-blocking mode, handleWiFi must be called in the main loop

  dnsServer.processNextRequest();
  httpServer.handleClient();

  // MQTT reconnect if not connected (nonblocking)
  if (!mqttclient.connected()) {
    long now = millis();

    if (now - mqttLastReconnectAttempt > 5000) {
      mqttLastReconnectAttempt = now;
      // Attempt to reconnect
      if (mqttReconnect()) {
        mqttLastReconnectAttempt = 0;
      }
    }
  } else {
    // Client connected
    mqttclient.loop();
  }//else

  checkButtonStates();
  handleDisplayTimeout();


  // set target temp for heating mode
  if (heatingMode == 1) { // heating on - default/day mode
    setTemp = setTempDay;
  }
  else if (heatingMode == 2) { // heating of - night/reduction mode
    setTemp = setTempNight;
  }


  if ( (millis() - lastMeasure) > measureinterval) {
    lastMeasure = millis();

    float tmpHum = round(dht.readHumidity());
    float tmpTemp = dht.readTemperature();  // Read temperature as Celsius (the default)

    // Check if any reads failed
    if (isnan(tmpHum) || isnan(tmpTemp)) {
      Serial.println("Failed to read from DHT sensor!");
    }
    else {
      if (tmpTemp < 50.0 && tmpTemp > -20.0) {
        temperature = tmpTemp;
        humidity = tmpHum;
        lastTempUpdate = millis();
      }
    }

    unsigned long heatingOnTime, heatingOffTime;
    if (heatingEnabled && turnHeatingOn) {
      heatingOnTime = (millis() - heatingLastOffMillis) / 1000;
      Serial.print("heatingOnTime: ");
      Serial.print(heatingOnTime);
      Serial.println();
    }
    else if (heatingEnabled && !turnHeatingOn) {
      heatingOffTime = (millis() - heatingLastOnMillis) / 1000;
      Serial.print("heatingOffTime: ");
      Serial.print(heatingOffTime);
      Serial.println();
    }


    Serial.print("lastTempUpdate: ");
    Serial.println(lastTempUpdate);

    long test1 = millis() - lastTempUpdate;
    Serial.print("lastTempUpdate test1: ");
    Serial.println(test1);

    
    if ( (millis() - lastTempUpdate) < 120000 ) {
      // thermostat - only active if measured temperature is < 2 min old

      // thermostat with hysteresis
      if ( temperature >= setTemp ) {
        turnHeatingOn = false;
        heatingLastOnMillis = millis();
        digitalWrite(PIN_RELAIS, !RELAISONSTATE);
        updateDisplay();
        Serial.println("heating off");

        Serial.print("last onTime: ");
        Serial.print(heatingOnTime);
        Serial.println();

        mqttclient.publish(mqtt_topic_heating, "off");

        //mqttclient.publish(mqtt_topic_heating_lastontime, heatingOnTime);
      }
      else if ( heatingEnabled && ( temperature < (setTemp - hysteresis) ) && ( heatingOffTime > minOffTime ) ) {
        turnHeatingOn = true;
        digitalWrite(PIN_RELAIS, RELAISONSTATE);
        updateDisplay();
        Serial.println("heating on");

        Serial.print("last offTime: ");
        Serial.print(heatingOffTime);
        Serial.println();

        mqttclient.publish(mqtt_topic_heating, "on");

        //mqttclient.publish(mqtt_topic_heating_lastofftime, heatingOffTime);
      }
    }
    else {
      turnHeatingOn = false;
      digitalWrite(PIN_RELAIS, !RELAISONSTATE);
      Serial.println("heating off");
      mqttclient.publish(mqtt_topic_heating, "off");
    }



  }




  //    if (lightMeter_present) {
  //      delay(50);
  //      lux = lightMeter.readLightLevel();
  //
  //      String lux_str = String(lux, 1); //converting humidity (the float variable above) to a string with 0 decimals
  //      char lux_chararr[lux_str.length() + 1];
  //      lux_str.toCharArray(lux_chararr, lux_str.length() + 1); //packaging up the data to publish to mqtt whoa...
  //
  //      Serial.print("Lux: ");
  //      Serial.println(lux_str);
  //      mqttclient.publish(mqtt_topic_lux, lux_chararr);
  //    }


  long now = millis();
  if ( ((now - aTempHumLastUpdate) < 300000) && whichTempToDisplay == 1 && ((now - lastDisplayToggle) > displayinterval) ) {
    // A-temp has been updated < 5 min ago, last displayed temp was I and display interval is overdue
    whichTempToDisplay = 2; // 1= I, 2= A
    lastDisplayToggle = now;
  }
  else if ( whichTempToDisplay != 1 && ((now - lastDisplayToggle) > displayinterval) ) {
    whichTempToDisplay = 1; // 1= I, 2= A
    lastDisplayToggle = now;
  }

  // update display?
  if ( (now - lastDisplayUpdate) > displayinterval) {
    lastDisplayUpdate = now;

    //    char temp_chararr[6];
    //    char hum_chararr[3];
    //    if ( (millis() - lastTempUpdate) < 120000) {
    //      String temp_str = String(temperature, 1); //converting Temperature (the float variable above) to a string with 1 decimal
    //      //char temp_chararr[temp_str.length() + 1];
    //      temp_str.toCharArray(temp_chararr, temp_str.length() + 1); //packaging up the data to publish to mqtt whoa...
    //
    //      String hum_str = String(humidity, 0); //converting humidity (the float variable above) to a string with 0 decimals
    //      //char hum_chararr[hum_str.length() + 1];
    //      hum_str.toCharArray(hum_chararr, hum_str.length() + 1); //packaging up the data to publish to mqtt whoa...
    //
    //      mqttclient.publish(mqtt_topic_temp, temp_chararr);
    //      mqttclient.publish(mqtt_topic_hum, hum_chararr);
    //    }
    //    else {
    //      char temp_chararr[] = {'-', '-', ',', '-', '\0'};
    //      char hum_chararr[] = {'-', '-', '\0'};
    //    }



    String temp_str = String(temperature, 1); //converting Temperature (the float variable above) to a string with 1 decimal
    char temp_chararr[temp_str.length() + 1];
    temp_str.toCharArray(temp_chararr, temp_str.length() + 1); //packaging up the data to publish to mqtt whoa...

    String hum_str = String(humidity, 0); //converting humidity (the float variable above) to a string with 0 decimals
    char hum_chararr[hum_str.length() + 1];
    hum_str.toCharArray(hum_chararr, hum_str.length() + 1); //packaging up the data to publish to mqtt whoa...

    if ( (now - lastTempUpdate) < 120000 ) {
      mqttclient.publish(mqtt_topic_temp, temp_chararr);
      mqttclient.publish(mqtt_topic_hum, hum_chararr);
    }
    else {
      temp_str = "--.-";
      hum_str = "--";
    }


    String atemp_str = String(aTemp, 1); //converting Temperature (the float variable above) to a string with 1 decimal
    char atemp_chararr[atemp_str.length() + 1];
    atemp_str.toCharArray(atemp_chararr, atemp_str.length() + 1); //packaging up the data to publish to mqtt whoa...

    String ahum_str = String(aHum, 0); //converting humidity (the float variable above) to a string with 0 decimals
    char ahum_chararr[ahum_str.length() + 1];
    ahum_str.toCharArray(ahum_chararr, ahum_str.length() + 1); //packaging up the data to publish to mqtt whoa...



    char setTemp_chararr[5];     //result string 4 positions + \0 at the end
    // convert float to fprintf type string format 2 positions with 1 decimal place
    dtostrf(setTemp, 2, 1, setTemp_chararr );

    Serial.print("set temp: ");
    Serial.println(setTemp_chararr);
    Serial.print("current temp: ");
    Serial.println(temp_chararr);


    mqttclient.publish(mqtt_topic_settemp, setTemp_chararr);


    if (whichTempToDisplay == 2) {
      // print inside temperature incl = and ° symbol + humidity to lcd, line 1, first 11 chars
      lcd.setCursor(0, 0);
      //lcd.write(0x3D); // = Zeichen
      lcd.print("A");

      if (aTemp <= -10);
      else if (aTemp < 0) lcd.print(" ");
      else if (aTemp < 10) lcd.print("  ");
      else lcd.print(" ");

      lcd.print(atemp_str); // inside temperature should hopefully always be 2.1 chars, so no special formatting necessary
      lcd.write(0xDF); // degree symbol
      lcd.print(" ");

      //lcd.print(humstr2);
      lcd.print(ahum_str); // always 2 chars
      lcd.print("%");
      lcd.print("  ");
    }
    else {
      // print outside temperature incl = and ° symbol + humidity to lcd, line 1, first 11 chars
      lcd.setCursor(0, 0);
      //lcd.write(0x3D); // = Zeichen
      lcd.print("I");

      //lcd.print(" "); // inside temperature should hopefully always be 2.1 chars, so no special formatting/spaces handling necessary

      if (temperature <= -10);
      else if (temperature < 0) lcd.print(" ");
      else if (temperature < 10) lcd.print("  ");
      else lcd.print(" ");

      //if (temperature < -9) lcd.print(" ");
      //else if (temperature < 0) lcd.print("");
      //else if (temperature < 10) lcd.print("  ");
      //else lcd.print(" ");
      //lcd.print(tempstr2);
      //lcd.print(temp_str);
      lcd.print(temp_chararr);
      lcd.write(0xDF); // degree symbol
      lcd.print(" ");

      //lcd.print(humstr2);
      //lcd.print(hum_str); // always 2 chars
      lcd.print(hum_chararr); // always 2 chars
      lcd.print("%");
      lcd.print("  ");
    }



    // display current mode on LCD
    lcd.setCursor(13, 0); // to char 15, line 1
    if (heatingEnabled) {
      if ( heatingMode == 1 ) { // day/normal mode
        lcd.print(" ");
        lcd.write((uint8_t)1); // sun symbol if mode is day/normal
        lcd.print(" ");
      }
      else if ( heatingMode == 2 ) { // night/reduction mode
        lcd.print(" ");
        lcd.write((uint8_t)0); // moon symbol if mode is night/reduction
        lcd.print(" ");
      }
    }
    else lcd.print("   "); // mode is heating off


    // display target temperature to line 2, 8 chars length incl space at the end
    lcd.setCursor(0, 1);

    // when the heating mode is OFF, do not display target temp - instead show "Heating off" info in line 2
    if ( !heatingEnabled ) {
      //         1234567890123456
      lcd.print("  Heizung aus   ");
    }
    else {
      lcd.write((uint8_t)2); // Pfeil rechts
      lcd.print(" ");
      lcd.print(setTemp_chararr);
      lcd.write(0xDF); // degree symbol
      lcd.print(" ");

      // display status info to line 2 from char 9 -> 8 chars length
      lcd.setCursor(8, 1);
      if (turnHeatingOn) {
        //         12345678
        lcd.print("heizen..");
      }
      else if ( heatingMode == 1 ) { // day/normal mode
        //         12345678
        lcd.print("        ");
      }
      else if ( heatingMode == 2 ) { // night/reduction mode
        //         12345678
        lcd.print("N-Absenk");
      }
      else lcd.print("        ");
    }


  }

} //void loop

