#include <WiFi.h>
#include <PubSubClient.h>
#include "home_secrets.h"  /* home wifi passwords and MQTT info  */
#include "time.h"  /* Pull time from NTP server to timestamp readings */
#include <Wire.h>  /* Needed for I2C interface for Battery Fuel Gauge */
#include <SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library.h>  /* Library for MAX17044 Fuel Gauge */

// Base Code borrowed from Steve Murch's gas tank monitor
//  www.stevemurch.com

//SFE_MAX1704X lipo; // Defaults to the MAX17043
//SFE_MAX1704X lipo(MAX1704X_MAX17043); // Create a MAX17043
SFE_MAX1704X lipo(MAX1704X_MAX17044); // Create a MAX17044
//SFE_MAX1704X lipo(MAX1704X_MAX17048); // Create a MAX17048
//SFE_MAX1704X lipo(MAX1704X_MAX17049); // Create a MAX17049

double voltage = 0; // Variable to keep track of LiPo voltage
double soc = 0; // Variable to keep track of LiPo state-of-charge (SOC)

// Using ADC7 Pin which is GPIO35 on ESP32-WROOM-32 board for 250Gal propane tank
// Using ADC6 Pin which is GPIO34 on ESP32-WROOM-32 board for 500Gal propane tank

const int GPIO_PIN_HALL_EFFECT_SENSOR_READ_INPUT = 35; // 250Gal Tank
const int GPIO_PIN_HALL_EFFECT_SENSOR2_READ_INPUT = 34; // 500Gal Tank

/* Manuall read tank gauge values in 5% increments to use for CASE and MAP statements below
Gauge Reading % Full	250gal mounted sensor	500gal mounted sensor
    05	1837	1680
    10	1822	1661
    15	1827	1665
    20	1837	1677
    25	1846	1695
    30	1861	1715
    35	1874	1735
    40	1893	1758
    45	1907	1786
    50	1928	1812
    55	1948	1840
    60	1969	1870
    65	1988	1897
    70	2001	1922
    75	2012	1940
    80	2025	1955
    85	2031	1966
    90	2028	1967
    95	2005	1936
  Propane tanks are never filled above 80% becasue the liquid needs space to
  turn into a gas to be consumed, so any readings above 85% are truncated.  Also 
  note that the values for above 85% start declining so they aren't trustworthy anyway.
  Likewise, the values below 10% begin climbing again due to the location of the Hall
  Sensor so those aren't trustworthy either.  Hence once the sensors hit 15%, the software 
  sets a "low" switch variable that is passed during deep sleep through the RTC memory
  location.  Once the tank reads above 15% again, then the low value is reset in memory again.
  NOTE - if the ESP32 looses all power or is hardware reset, the memory valus are lost. 
*/

// Set Hall Sensor minimum values taken from testing the sensors manually
const int Tank1_Low_Cutoff = 1827; // 15% reading of guage
const int Tank2_Low_Cutoff = 1665; // 15% reading of guage
const int Tank1_15_Percent = 1827; // 15% reading of guage
const int Tank2_15_Percent = 1665; // 15% reading of guage
int Tank1_Percentage = 0; // Set percentage full of 250gal tank to 0
int Tank2_Percentage = 0; // Set percentage full of 500gal tank to 0
const int mqtt_port = 1883;  // Default MQTT port number
const char* ntpServer = "pool.ntp.org";  // Server to pull time from
const long  gmtOffset_sec = -21600;  // GMT offset in seconds for Central Time (6 hrs)
const int   daylightOffset_sec = 3600; // seconds offset for daylight savings

#define uS_TO_S_FACTOR 1000000  /* Conversion factor for micro seconds to seconds */
#define TIME_TO_SLEEP  30        /* Seconds. Time ESP32 will go to sleep */

// Setup RTC memory variables to compensate for hall sensor readings climbing
// after they drop below 10% of full.  These memory values are used as an integer
// value to determine if the tank has been filled to 15.  They are set to a "1"
// when they get below the tank-low-cutoff value and back to "0" once they hit 15%.
RTC_DATA_ATTR int Tank1_Low = 0;
RTC_DATA_ATTR int Tank2_Low = 0;
RTC_DATA_ATTR int BootCount = 0;  // For tracking boot count

WiFiClient espClient;
PubSubClient client(espClient);

void setup() {
    // Set software serial baud to 115200;
    Serial.begin(115200);
    Serial.println("Waking up...");
    // Define GPIO Pins are input
    pinMode(GPIO_PIN_HALL_EFFECT_SENSOR_READ_INPUT, INPUT);
    pinMode(GPIO_PIN_HALL_EFFECT_SENSOR2_READ_INPUT, INPUT); 
    // Increase boot count
    ++BootCount;
    Serial.print("Boot cycle: ");
    Serial.println(BootCount);
    
    Wire.begin(); // Initialize I2C

    lipo.enableDebugging(); // Uncomment this line to enable helpful debug messages on Serial

    // Initialize Fuel Gauge
    // Set up the MAX17043 LiPo fuel gauge:
    if (lipo.begin() == false) // Connect to the MAX17043 using the default wire port
    {
      Serial.println(F("MAX17043 not detected. Please check wiring."));
    }

    // Step1 - Get sensor values and average them
    int sum = 0;
    int sum2 = 0;
    int val = 0;
    int val2 = 0;
    int averageRead = 0;
    int averageRead2 = 0;
    Serial.print("Reading 100 values both sensors...");
    for (int i = 1; i <= 100; i++) {
      // Read 250gal tank sensor
      int val = analogRead(GPIO_PIN_HALL_EFFECT_SENSOR_READ_INPUT);
      sum = sum + val;
      // Read 500gal tank sensor
      int val2 = analogRead(GPIO_PIN_HALL_EFFECT_SENSOR2_READ_INPUT);
      sum2 = sum2 + val2;
      delay(100);
    }

    // now get the average of reads
    averageRead = sum / 100;
    averageRead2 = sum2 / 100;
    Serial.print("Average digital representation -- ");
    Serial.print(averageRead);
    Serial.print(", ");
    Serial.println(averageRead2);

    // Step2 - Translate the digital readings into a percentage tank full to report
  
    // Once the readings drop below 10% the Hall sensor increases value which results
    // in false higher tank readings.  So, use RTC memory to store previous cycle reads
    // to catch when the amount of porpane has dropped below 10% until it is filled again.
    // NOTE - if power is lost and deep sleep broken, RTC memory is cleared

    // Check to see if the previous cycle reading was at or below 10% by checking RTC memory
    if (Tank1_Low == 1 && averageRead > Tank1_15_Percent) {
      Tank1_Low = 0;
    }
    if (Tank1_Low == 0  && averageRead <= Tank1_Low_Cutoff) {
      Tank1_Low = 1;
    }
    if (Tank2_Low == 1 && averageRead2 > Tank2_15_Percent) {
      Tank2_Low = 0;
    }
    if (Tank2_Low == 0  && averageRead2 <= Tank2_Low_Cutoff) {
      Tank2_Low = 1;
    }
    // If tank is already below 10%, just setup a 7% value
    // and skip all the selection processing
    if (Tank1_Low == 1){
      averageRead = Tank1_Low_Cutoff;
      Tank1_Percentage=10;
    } else {
    // Now use the case statement to map the read values to ranges already determined through testing
    // and assign a percentage.  Since the readings aren't linear and there are only collected values
    // between 15% and 85% in 5% increments, we use the map function to get the asignments.
      switch (averageRead) {
        case 1828 ... 1837 : {
          Tank1_Percentage=map(averageRead,1827,1837,15,20);
          break;
        }
        case 1838 ... 1846 : {
          Tank1_Percentage=map(averageRead,1838,1846,20,25);
          break;
        }
        case 1847 ... 1861 : {
          Tank1_Percentage=map(averageRead,1847,1861,25,30);
          break;
        }
        case 1862 ... 1874 : {
          Tank1_Percentage=map(averageRead,1862,1874,30,35);
          break;
        }
        case 1875 ... 1893 : {
          Tank1_Percentage=map(averageRead,1875,1893,35,40);
          break;
        }
        case 1894 ... 1907 : {
          Tank1_Percentage=map(averageRead,1894,1907,40,45);
          break;
        }
        case 1908 ... 1928 : {
          Tank1_Percentage=map(averageRead,1908,1928,45,50);
          break;
        }
        case 1929 ... 1948 : {
          Tank1_Percentage=map(averageRead,1929,1948,50,55);
          break;
        }
        case 1949 ... 1969 : {
          Tank1_Percentage=map(averageRead,1949,1969,55,60);
          break;
        }
        case 1970 ... 1988 : {
          Tank1_Percentage=map(averageRead,1970,1988,60,65);
          break;
        }
        case 1989 ... 2001 : {
          Tank1_Percentage=map(averageRead,1989,2001,65,70);
          break;
        }
        case 2002 ... 2012 : {
          Tank1_Percentage=map(averageRead,2002,2012,70,75);
          break;
        }
        case 2013 ... 2025 : {
          Tank1_Percentage=map(averageRead,2013,2025,75,80);
          break;
        }
        case 2026 ... 2031 : {
          Tank1_Percentage=map(averageRead,2026,2031,80,85);
          break;
        }
        case 2032 ... 6000 : {
          Tank1_Percentage=90;
          break;
        }
      }
    }

    // If tank2 is already below 10%, just setup a 7% value
    // and skip all the selection processing
    if (Tank2_Low == 1){
      averageRead2 = Tank2_Low_Cutoff;
      Tank2_Percentage=10;
    } else {
      // Now use the case statement to map the read values to ranges already determined through testing
      // and assign a percentage.  Since the readings aren't linear and there are only collected values
      // between 15% and 85% in 5% increments, we use the map function to get the asignments.
      switch (averageRead2) {
        case 1665 ... 1677 : {
          Tank2_Percentage=map(averageRead2,1665,1677,15,20);
          break;
        }
        case 1678 ... 1695 : {
          Tank2_Percentage=map(averageRead2,1678,1695,20,25);
          break;
        }
        case 1696 ... 1715 : {
          Tank2_Percentage=map(averageRead2,1696,1715,25,30);
          break;
        }
        case 1716 ... 1735 : {
          Tank2_Percentage=map(averageRead2,1716,1735,30,35);
          break;
        }
        case 1736 ... 1758 : {
          Tank2_Percentage=map(averageRead2,1736,1758,35,40);
          break;
        }
        case 1759 ... 1786 : {
          Tank2_Percentage=map(averageRead2,1759,1786,40,45);
          break;
        }
        case 1787 ... 1812 : {
          Tank2_Percentage=map(averageRead2,1787,1812,45,50);
          break;
        }
        case 1813 ... 1840 : {
          Tank2_Percentage=map(averageRead2,1813,1840,50,55);
          break;
        }
        case 1841 ... 1870 : {
          Tank2_Percentage=map(averageRead2,1841,1870,55,60);
          break;
        }
        case 1871 ... 1897 : {
          Tank2_Percentage=map(averageRead2,1871,1897,60,65);
          break;
        }
        case 1898 ... 1922 : {
          Tank2_Percentage=map(averageRead2,1898,1922,65,70);
          break;
        }
        case 1923 ... 1940 : {
          Tank2_Percentage=map(averageRead2,1923,1940,70,75);
          break;
        }
        case 1941 ... 1955 : {
          Tank2_Percentage=map(averageRead2,1941,1955,75,80);
          break;
        }
        case 1956 ... 1966 : {
          Tank2_Percentage=map(averageRead2,1956,1966,80,85);
          break;
        }
        case 1967 ... 6000 : {
          Tank2_Percentage=90;
        }
      }
    }
    // Quick start restarts the MAX17043 in hopes of getting a more accurate
	  // guess for the SOC.
	  lipo.quickStart();

	  // We can set an interrupt to alert when the battery SoC gets too low.
	  // We can alert at anywhere between 1% - 32%:
	  //lipo.setThreshold(20); // Set alert threshold to 20%.

    // Get voltage and State of Charge (SOC) from Fuel Gauge
    // lipo.getVoltage() returns a voltage value (e.g. 3.93)
    voltage = lipo.getVoltage();
    // lipo.getSOC() returns the estimated state of charge (e.g. 79%)
    soc = lipo.getSOC();
    // print out battery voltage and SOC
    Serial.print("Voltage:       "); Serial.print(voltage); Serial.println(" mV");
    Serial.print("Percent:       "); Serial.print(soc); Serial.println("%");
    
    // Connect to WiFi
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.println("Connecting to WiFi..");
    }
    Serial.println("Connected to the Wi-Fi network");
    // Now that WiFi is connected, Init and get the time
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
    struct tm timeinfo;
    while (!getLocalTime(&timeinfo)){
    Serial.println("Failed to obtain time");
      delay(500);
    }
    Serial.println(&timeinfo, "%d-%B-%Y-%H:%M:%S");
    char timeCheck[32];
    strftime(timeCheck,32,"%d-%B-%Y-%H:%M:%S",&timeinfo);
    //Serial.print("Value of timeCheck -->");
    //Serial.println(timeCheck);

    // Connect to MQTT service
    client.setServer(mqtt_broker, mqtt_port);
    while (!client.connected()) {
        String client_id = "esp32-client-";
        client_id += String(WiFi.macAddress());
        Serial.printf("The client %s connects to the public MQTT broker\n", client_id.c_str());
        if (client.connect(client_id.c_str(), mqtt_username, mqtt_password)) {
            Serial.println("Public EMQX MQTT broker connected");
        } else {
            Serial.print("failed with state ");
            Serial.println(client.state());
            delay(2000);
        }
    }
    // Send 250gal tank data to MQTT service
    char msg_out[32];
    sprintf(msg_out, "%d", Tank1_Percentage);
    client.publish(outputTopicPercentage, msg_out);
    sprintf(msg_out, "%d", averageRead);
    client.publish(outputTopicVoltage, msg_out);
    sprintf(msg_out, "%s", timeCheck);
    client.publish(outputTopicTime, msg_out);
    
    // Send 500gal tank data to MQTT service
    sprintf(msg_out, "%d", Tank2_Percentage);
    client.publish(outputTopic2Percentage, msg_out);
    sprintf(msg_out, "%d", averageRead2);
    client.publish(outputTopic2Voltage, msg_out);
    sprintf(msg_out, "%s", timeCheck);
    client.publish(outputTopic2Time, msg_out);

    // Send battery information to MQTT service
    // Since values are float, use dtostrf command to conver to string
    char floatBuffer[10];
    // Convert double to string (dtostrf(value, width, decimal_places, buffer))
    dtostrf(soc, 6, 2, floatBuffer);  // Width: 6, Decimal places: 2
    sprintf(msg_out, "%s%%", floatBuffer);
    client.publish(outputTopicBatteryPercentage, msg_out);
    Serial.print("BatterySOC MsgOut-->");
    Serial.println(msg_out);
    // Convert double to string (dtostrf(value, width, decimal_places, buffer))
    dtostrf(voltage, 6, 2, floatBuffer);  // Width: 6, Decimal places: 2
    sprintf(msg_out, "%sV", floatBuffer);
    client.publish(outputTopicBatteryVoltage, msg_out);
    Serial.print("BatteryVoltage MsgOut-->");
    Serial.println(msg_out);

    client.loop();
  
    Serial.println("Good night. Going to sleep.");
    
    // Close Wifi and MQTT service
    delay(1000);
    client.disconnect();
    delay(1000);
    WiFi.disconnect();
    
    // Go to sleep mode
    esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
    esp_deep_sleep_start();
  
}

void loop() {
  // loop isn't used since we are using deep seep mode
  }