
// pre compiletime config

//#define DEBUG_VERBOSE

#define SPIFFS_DBG
#define SPIFFS_USE_MAGIC

#define FIRMWARE_NAME "WiFiThermostat"
#define VERSION "0.1.2"
#define COPYRIGHT " by Flo Kra"

// default values, can later be overridden via configuration
#define DEVICE_NAME "WiFi-Thermostat-1"
#define MQTT_SERVER "10.1.1.11"
#define MQTT_PORT 1883
#define MQTT_TOPIC_IN "Test/Thermostat/cmd"
#define MQTT_TOPIC_OUT "Test/Thermostat/status"
#define BUTTON_DEBOUNCE_TIME 120
#define BUTTON_HOLD_TIME 750
#define DOMOTICZ_IN_TOPIC "domoticz/in"
#define DOMOTICZ_OUT_TOPIC "domoticz/out"
#define OUTTEMP_TOPIC_IN "wetter/atemp"
#define OUTHUM_TOPIC_IN "wetter/ahum"

#define CLEARCONF_TOKEN "TUES"


// pin assignments and I2C addresses
#define PIN_DHTSENSOR 13
#define PIN_RELAIS 16
#define PIN_BUTTON_PLUS 2
#define PIN_BUTTON_MINUS 0
#define PIN_BUTTON_MODE 14
#define PIN_PIRSENSOR 12

#define DHTTYPE DHT22 // DHT sensor type

#define LCDADDR 0x27  // I2C address LCD
#define LCDCOLS 16
#define LCDLINES 2


// default logic levels
#define RELAISONSTATE LOW
#define BUTTONONSTATE LOW




#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 <DNSServer.h>
#include <FS.h>
#include <Wire.h>
#include "LiquidCrystal_I2C.h"
#include "DHT.h"


#ifndef MESSZ
#define MESSZ                405          // Max number of characters in JSON message string (4 x DS18x20 sensors)
#endif
// Max message size calculated by PubSubClient is (MQTT_MAX_PACKET_SIZE < 5 + 2 + strlen(topic) + plength)
#if (MQTT_MAX_PACKET_SIZE -TOPSZ -7) < MESSZ  // If the max message size is too small, throw an error at compile time
// See pubsubclient.c line 359
#error "MQTT_MAX_PACKET_SIZE is too small in libraries/PubSubClient/src/PubSubClient.h, increase it to at least 512"
#endif



// config variables - do not change here!
//conf
char deviceName[31];  // device name - just for web interface
char http_user[31];
char http_pass[31];
char mqtt_server[41];
int mqtt_port = MQTT_PORT;
char mqtt_user[31];
char mqtt_pass[31];
char mqtt_topic_in[51];   // MQTT in topic for commands
char mqtt_topic_out[51];  // MQTT out base topic, will be extended by various value names
boolean mqtt_outRetain = false; // send MQTT out with retain flag
char mqtt_willTopic[51];  // MQTT Last Will topic
int mqtt_willQos = 2;     // MQTT Last Will topic QOS
boolean mqtt_willRetain = false;  // MQTT Last Will retain
char mqtt_willMsg[31];    // MQTT Last Will payload
char domoticz_out_topic[55];  // domoticz out topic to subscribe to (only applicable if domoticzIdx_Thermostat and/or domoticzIdx_ThermostatMode is set to >0)

//conf2
int domoticzIdx_Thermostat = 0;
int domoticzIdx_ThermostatMode = 0;
int domoticzIdx_TempHumSensor = 0;
int domoticzIdx_PIR = 0;
char outTemp_topic_in[51];
char outHum_topic_in[51];
bool autoSaveSetTemp = true;
bool autoSaveHeatingMode = true;
int heatingMinOffTime = 10;       // minimal time the heating keeps turned off in s
float setTempMin = 14.0;        // minimal temperature that can be set
float setTempMax = 29.0;        // maximal temperature that can be set
float setTempLow = 18.0;        // set temperature in night/low mode
float hysteresis = 0.5;
float tempCorrVal = 0.0;        // correction value for temperature sensor reading
int humCorrVal = 0;         // correction value for humidity sensor reading
int measureInterval = 15;       // interval for temp/hum measurement
int displayInterval = 5;        //
int displayTimeout = 120;


//set values
float setTemp = 21.5;
byte heatingMode = 1; // 0 = off, 1 = normal/day, 2 = night/reduction

float setTempSaved;
byte heatingModeSaved; // 0 = off, 1 = normal/day, 2 = night/reduction

// not changeable via configuration
float setTempLowMin = 14.0;
float setTempLowMax = 19.0;
boolean debug = true;
int debounceTime = BUTTON_DEBOUNCE_TIME;
int buttonHoldTime = BUTTON_HOLD_TIME;

// global variables
float temperature; // last reading from DHT sensor
int humidity; // last reading from DHT sensor
bool turnHeatingOn = false; // true if heating is active (relais switched on)
unsigned long heatingLastOnMillis; // last time heating was switched on
unsigned long heatingLastOffMillis; // last time heating was switched off
float outTemp; // outside temp (via MQTT if enabled and in-topic configured)
int outHum; // outside temp (via MQTT if enabled and in-topic configured)
long outTempHumLastUpdate; // last reading from out temp/hum source
byte whichTempToDisplay; // 1=temp inside (from DHT sensor), 2= temp outside (via MQTT) - if out temp/hum available this value and the displayed value pair toggles with every displayInterval
unsigned long lastMeasure = 0; // millis of last temp/hum measurement
unsigned long lastDisplayUpdate = 0; // millis of last display update
unsigned long lastDisplayToggle = 0; // millis of last display toggle
unsigned long lastTempUpdate = 0; // last update time of DHT reading
char msg[50];   // buffer MQTT in payload
char topic[50]; // buffer MQTT in topic
bool displayActive = false; // gets true when button is pressed. display light gets switched on until timeout. button actions are only performed while display is active

unsigned long heatingOnTime, heatingOffTime;

boolean useDomoticz = false; // will be set to true in setup() if idx-values other than 0 are configured
boolean domoticzOutParseData = false; // indicates that domoticz/out json data is buffered, will then be parsed in next loop() run
boolean domoticzOutParserBusy = false; // indicates that domoticz/out json data is currently processed - no futher data will be accepted until finished
char domoticzOutPayload[450]; // buffer for domoticz/out data
int dismissUpdateFromDomoticzTimeout = 2500; // after a value was changed by data from domoticz/out, domoticz/out parsing for this device will be turned off for this time to prevent infinite loops
unsigned long lastUpdate_setTemp = 0; // set to millis() every time setTemp value is changed. next update from domoticz/out will be rejected for dismissUpdateFromDomoticzTimeout in this case 
unsigned long lastUpdate_heatingMode = 0; // set to millis() every time heatingMode value is changed. next update from domoticz/out will be rejected for dismissUpdateFromDomoticzTimeout in this case 
bool lastUpdateFromDomoticz_setTemp = false;
bool lastUpdateFromDomoticz_heatingMode = false;
int domoticzUpdateInterval = 30; // interval in min to force update of domoticz devices

char cmdPayload[101]; // buffer for commands
boolean cmdInQueue = false; // command is queued and will be processed next loop() run
bool saveConfigToFlash = false; // conf is saved in next loop() run
bool saveConfig2ToFlash = false; // conf2 is saved in next loop() run

unsigned int saveValuesTimeout = 5000;
unsigned long lastValueChange; // is set to millis() whenever setTemp value and/or heatingMode value is changed. used for autoSave function with hardcoded 5s timeout 
bool setTempAlreadySaved = true; // only save if not yet done
bool heatingModeAlreadySaved = true; // only save if not yet done

byte mqttMode = 0; 
unsigned long mqttLastReconnectAttempt = 0;
int mqttReconnects = 0;


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

WiFiClient espClient;

void mqttCallback(char* topic, byte* payload, unsigned int length);
PubSubClient mqttclient(espClient);

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

ESP8266HTTPUpdateServer httpUpdater;


void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println();
  Serial.print(FIRMWARE_NAME);
  Serial.print(" v");
  Serial.print(VERSION);
  Serial.println(COPYRIGHT);
  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);


  strlcpy(deviceName, DEVICE_NAME, 31);
  strlcpy(mqtt_server, MQTT_SERVER, 41);
  strlcpy(mqtt_topic_in, MQTT_TOPIC_IN, 51);
  strlcpy(mqtt_topic_out, MQTT_TOPIC_OUT, 51);
  strlcpy(domoticz_out_topic, DOMOTICZ_OUT_TOPIC, 51); // changeable subscription topic, as domoticz supports different flat/hierarchical out-topics
  strlcpy(outTemp_topic_in, OUTTEMP_TOPIC_IN, 51);
  strlcpy(outHum_topic_in, OUTHUM_TOPIC_IN, 51);

  Serial.println("default config values loaded..");


  Serial.println("Mounting FS...");
  if (!SPIFFS.begin()) {
    Serial.println("Failed to mount file system");
    return;
  }

  //SPIFFS.format();
  //Serial.print("Format SPIFFS complete.");

  if (!SPIFFS.exists("/formatComplete.txt")) {
    Serial.println("Please wait 30 secs for SPIFFS to be formatted");
    SPIFFS.format();
    Serial.println("Spiffs formatted");

    File f = SPIFFS.open("/formatComplete.txt", "w");
    if (!f) {
      Serial.println("file open failed");
    } else {
      f.println("Format Complete");
    }
    f.close();
  } else {
    Serial.println("SPIFFS is formatted. Moving along...");
  }


  //  // load config from SPIFFS if files exist
  if (!loadConfig()) {
    Serial.println("Failed to load conf.json");
  } else {
    Serial.println("conf.json loaded");
  }

  if (!loadConfig2()) {
    Serial.println("Failed to load conf2.json");
  } else {
    Serial.println("conf2.json loaded");
  }

  if (!loadSetTemp()) {
    Serial.println("Failed to load file 'setTemp'");
  } else {
    Serial.println("file 'setTemp' loaded");
  }

  if (!loadHeatingMode()) {
    Serial.println("Failed to load file 'heatingMode'");
  } else {
    Serial.println("file 'heatingMode' loaded");
  }

  setTempSaved = setTemp;
  heatingModeSaved = heatingMode;

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

  checkUseDomoticz();
  delay(500);


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

  //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();
  delay(500);

  httpServerInit();

  mqttPrepareConnection();
  mqttClientInit();

  initDisplay();

  Serial.println("setup complete.");
  delay(1000);
} //void setup

void loop() {
  checkMillis();

  persWM.handleWiFi(); //in non-blocking mode, handleWiFi must be called in the main loop
  yield();
  dnsServer.processNextRequest();
  httpServer.handleClient();

  mqttHandleConnection();

  checkButtonStates();

  yield();

  evalCmd();

  if ( domoticzOutParseData ) {
    parseDomoticzOut();
    yield();
  }

  if (Serial.available()) {
    serialEvent();
    yield();
  }
  
} //void loop

