// Weather Station 3 from AliExpress / TaoBao
//
// Code for ESP32-PoE-ISO
// 
// Highly inspired by https://github.com/kquinsland/ws3-to-esphome-bridge/
// 
// Maybe the code is not optimal but it works.
// 
// Configuration of WS3 :
//
// JP1 toggle betwen ARS inch mode and professional metric mode
//     short : inch
//     open  : metric
//
// JP2 baud rate
//     short : 2400
//     open  : 9600
//
// Configuration used for this code :
// JP1 : open
// JP2 : closed
//
// Data is in the form :
//
// A4095B000C0000D0000E0000F0000G0000H0000I0000J0000K0000L0218M515N09654O.....*52
//
#include <FS.h>
#include <DNSServer.h> 
#ifndef ESP32
 #include <ESP8266mDNS.h>
#endif
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
//#include <AddrList.h>
// EspSoftwareSerial (or plain SoftwareSerial)
#ifndef ESP32
 #include <SoftwareSerial.h>
#endif
// MQTT
#include <PubSubClient.h>
// WiFiManager
#include <WiFiManager.h>
#include <ArduinoJson.h> // Version 5 ONLY (TODO upgrade this)

#ifdef ESP32
  #include <SPIFFS.h>
#endif /* ESP32 */

#include "ws3_defs.h"
#include "ws3_vars.h"

#ifndef ESP32
SoftwareSerial WS3(RXD2,TXD2);
#else
#define WS3 Serial2
#endif

WiFiClient espClient;
PubSubClient client(espClient);

// After parsing the string of bytes, we'll have an easier to use struct
// TODO: this should be it's own file?
struct WS3Packet {

  // The 1st field is "A0000" - Wind direction AD value in real time (0-4096)
  unsigned int wind_dir;  
  // The 2nd field is "B000" - Wind direct angle value (16 direction)
  unsigned int wind_angle;  // new
  // The 3rd field is "C0000" - Real time wind speed frequency 1Hz
  unsigned int wind_freq;  // New
  // The 4th field is "D0000" - Real time wind speed, unit: 0.1 m/s
  unsigned int wind_speed;
  // The 5th field is "E0000" - Avg wind speed in the previous minute, unit: 0.1m/s
  unsigned int wind_speed_1m;
  // The 6th field is "F0000" - the highest wind speed in the last 5 minutes, unit: 0.1m/s
  unsigned int wind_speed_5m;
  // The 7th field is "G0000" - Real time rain bucket (0-9999), loop-count
  int rain_bucket_cnt; // New
  // The 8th field is "H0000" - Number of rain bucket in the last minute, (0-9999)
  int rt_rain_bucket;
  // The 9th field is "I0000" - Rain fall in 1 minute, unit: 0.1mm
  unsigned int rain_1m;
  // The 10th field is "J0000" - the previous hour's rainfall ( 0.1 mm)
  unsigned int rain_1h;
  // The 11th field is "K0000" - rainfall during the first 24 hours ( 0.1 mm)
  unsigned int rain_24h;
  // The 12th field is "L0000" - temperature, unit: degree C (unit 0.1 Degree)
  float temp_f;
  // The 13th field is "M000" - humidity ( 00 % - 99 %), unit 0.1% 
  float humidity;
  // The 14th field is "M10020" - air pressure ( 0.1 hpa )
  float air_pressure;    
};

// Return the index according to Wind Angle
String str_windir(unsigned int WinVal){
  if(WinVal >= 360)                 return "N";   //N
  if(WinVal >= 0   && WinVal < 22)  return "N";   //N
  if(WinVal >= 22  && WinVal < 45)  return "NNE"; //NNE
  if(WinVal >= 45  && WinVal < 67)  return "NE";  //NE
  if(WinVal >= 67  && WinVal < 90)  return "ENE"; //ENE
  if(WinVal >= 90  && WinVal < 112) return "E";   //E
  if(WinVal >= 112 && WinVal < 135) return "ESE"; //ESE
  if(WinVal >= 135 && WinVal < 157) return "SE";  //SE
  if(WinVal >= 157 && WinVal < 180) return "S";   //S
  if(WinVal >= 180 && WinVal < 202) return "S";   //S
  if(WinVal >= 202 && WinVal < 225) return "SSW"; //SSW
  if(WinVal >= 225 && WinVal < 247) return "SW";  //SW
  if(WinVal >= 247 && WinVal < 270) return "WSW"; //WSW
  if(WinVal >= 270 && WinVal < 292) return "W";   //W
  if(WinVal >= 292 && WinVal < 315) return "WNW"; //WNW
  if(WinVal >= 315 && WinVal < 337) return "NW";  //NW
  if(WinVal >= 337 && WinVal < 359) return "NNW"; //NNW
}


// Setup the stuff.
void setup() {  
  #ifndef ESP32
  Serial.begin(57600);
  #else
  Serial.begin(115200);
  #endif
  while (!Serial) ; // wait for Arduino Serial Monitor to open
  Serial.println("\n\n");
  setupSpiffs();
  Serial.println("Weather Station 3 Adapter by Kiwi");
  #ifndef ESP32
  Serial.println(ESP.getFullVersion());
  #endif

  WiFiManager wm;

  wm.setSaveConfigCallback(saveConfigCallback);

  // Setup custom parameters
  WiFiManagerParameter custom_idx_windir ("idx1",   "Domoticz Index for Wind Virtual Module",          idx_windir,      4);
  WiFiManagerParameter custom_idx_temp   ("idx2",   "Domoticz Index for Temp/Hum/Baro Virtual Module", idx_temp,        4);
  WiFiManagerParameter custom_idx_rain   ("idx3",   "Domoticz Index for Rain Virtual Module",          idx_rain,        4);

  // Add all parameters
  wm.addParameter(&custom_idx_windir);
  wm.addParameter(&custom_idx_temp);
  wm.addParameter(&custom_idx_rain);

  // Automatic connect using saved stuff otherwise start as an AP to configure it
  if (!wm.autoConnect("ESP-Weather-Station")) {
    Serial.println("Failer to connect and hit timeout");
    delay(3000);
    // if we still have not connected restard and try again
    ESP.restart();
    delay(5000);
  }

  // Always start configportal for a little while (2 m)
  wm.setConfigPortalTimeout(30);
//  wm.startConfigPortal("ESP-Weather-Station","ESP");
  wm.startConfigPortal("ESP-Weather-Station");

  // If we get here we are connected !
  Serial.println("Connected to WiFi !");

  // Read the updated parameters
  strcpy(idx_windir,      custom_idx_windir.getValue());
  strcpy(idx_temp,        custom_idx_temp.getValue());
  strcpy(idx_rain,        custom_idx_rain.getValue());

  // Save the custom parameters to FS
  if (shouldSaveConfig) {
    Serial.println("Saving config to FS...");
    DynamicJsonBuffer jsonBuffer;

    JsonObject& json = jsonBuffer.createObject();
    json["idx_windir"]  = idx_windir;
    json["idx_temp"]    = idx_temp;
    json["idx_rain"]    = idx_rain;
    
    File configFile = SPIFFS.open("/config.json", "w");
    if (!configFile) {
      Serial.println("failed to open config file for writing");
    }

    json.prettyPrintTo(Serial);
    json.printTo(configFile);
    configFile.close();
    //end save
    shouldSaveConfig = false;
  }

  // Launch OTA stuff
  setup_ota();
  
  #if LWIP_IPV6
  Serial.printf("IPV6 is enabled\n");
  #else
  Serial.printf("IPV6 is not enabled\n");
  #endif
  Serial.print("My IP address: ");
  Serial.print(WiFi.localIP());
  Serial.print("/");
  Serial.print(WiFi.subnetMask());
  Serial.print(" GW:");
  Serial.println(WiFi.gatewayIP());

  // Start the Software Serial for WS3
  #ifndef ESP32
  WS3.begin(WS3_BAUD);
  #else
  // We use Serial2 on ESP32 (hardware)
  WS3.begin(WS3_BAUD, SERIAL_8N1, RXD2, TXD2);
  while (!WS3) ; // wait for Arduino Serial Monitor to open
  #endif
  debugln("WS3 UART is ready...");

  // Allocate memory for packet
  pkt_str.reserve(WS3_PKT_LEN);
  debugln(" -> Packet memory allocated!");

  client.setServer(mqtt_server, mqtt_port);
  //client.setCallback(callback);
  debugln("MQTT started");
}

#ifdef DONT_CHKSUM
// Validate packet using the checksum.
// Work only APRS data on this 51W3 board. 
// Maybe the code on the board does not make the correct checksum ?
bool validate_packet(String pay, unsigned long chk) {
  // Print the payload and the checksum we want
  debugln("validate_packet:");
  debug(pay);
  debug(" * ");
  debugln(chk);

  // TEST DATA (actual packets)
  // c000s000g000t075r000p019h43b09940*32
  // String pay = "c000s000g000t075r000p019h43b09940";
  // byte chk = 0x32; // this will be in HEX)

  // c000s000g000t075r000p019h42b09940*33
  // String pay = "c000s000g000t075r000p019h42b09940";
  // byte chk = 0x33; // this will be in HEX)
  
  // c000s000g000t075r000p019h42b09939*3D
  // String pay = "c000s000g000t075r000p019h42b09939";
  // byte chk = 0x3D; // this will be in HEX)

  // SUPER grateful for the helpful https://toolslick.com/math/bitwise/xor-calculator to validate my
  // code!

  // Current byte
  byte i1=0;

  // the intermediate checksum
  byte tmp = 0;

  // starting from the second character, we begin XORing
  for (int x = 0; x < pay.length() ; x++) {

    i1=pay[x];
        
    // Do the xOR
    tmp = tmp^i1;

  }

  // do the check
  if(tmp == chk){
    return true;
  } else {
    debugln("INVALID!");
    debug("calculated:");
    debugln(tmp);
    return false;
  }
}
#endif /* DONT_CHKSUM */


// Parse the packet and fill the structure with data
void parse_packet(String payload, WS3Packet* p) { 

  // E.G.: A4095 B000 C0000 D0000 E0000 F0000 G0000 H0000 I0000 J0000 K0000 L0237 M502 N09810 O.....
  
  // Parse in order, starting with A0000 (wind dir real time, 0-4096)
  int wind_dir_idx = payload.indexOf('A');
  p->wind_dir = payload.substring(wind_dir_idx+1, wind_dir_idx+5).toInt();

  // Then move on to B000 - wind direction angle (16 direction)
  int wind_angle_idx = payload.indexOf('B');
  p->wind_angle = payload.substring(wind_angle_idx+1, wind_angle_idx+4).toInt();

  // Then move on to C0000 - wind speed frequency (1 Hz)
  int wind_freq_idx = payload.indexOf('C');
  p->wind_freq = payload.substring(wind_freq_idx+1, wind_freq_idx+5).toInt();

  // Then move on to D0000 - wind speed real time (unit 0.1 m/s)
  int wind_speed_idx = payload.indexOf('D');
  p->wind_speed = payload.substring(wind_speed_idx+1, wind_speed_idx+5).toInt();

  // Then move on to E0000 - wind speed avg in the last minute (unit 0.1 m/s)
  int wind_speed_1m_idx = payload.indexOf('D');
  p->wind_speed_1m = payload.substring(wind_speed_1m_idx+1, wind_speed_1m_idx+5).toInt();

  // Then move on to F0000 - wind speed over the last 5 min
  int wind_speed_5_idx = payload.indexOf('F');
  p->wind_speed_5m = payload.substring(wind_speed_5_idx+1, wind_speed_5_idx+5).toInt();

  // Then move on to G0000 - Rain in Realtime (0-9999 counter) bucket
  int rain_bucket_cnt_idx = payload.indexOf('G');
  p->rain_bucket_cnt = payload.substring(rain_bucket_cnt_idx+1, rain_bucket_cnt_idx+5).toInt();

  // Then move on to H0000 - Rain bucket in the last 1 minute (0-9999 counter)
  int rt_rain_bucket_idx = payload.indexOf('H');
  p->rt_rain_bucket = payload.substring(rt_rain_bucket_idx+1, rt_rain_bucket_idx+5).toInt();

  // Then move on to I0000 - rain last minute (0.1mm)
  int rain1m_idx = payload.indexOf('I');
  p->rain_1m = payload.substring(rain1m_idx+1, rain1m_idx+5).toInt();

  // Then move on to J0000 - rain last hour (0.1mm)
  int rain1h_idx = payload.indexOf('J');
  p->rain_1h = payload.substring(rain1h_idx+1, rain1h_idx+5).toInt();

  // Then move on to K0000 - rain last 24h (0.1mm)
  int rain24h_idx = payload.indexOf('K');
  p->rain_24h = payload.substring(rain24h_idx+1, rain24h_idx+5).toInt();

  // Then move on to L0200 - temp (0.1°C) 
  int temp_idx = payload.indexOf('L');
  p->temp_f = payload.substring(temp_idx+1, temp_idx+5).toInt()*.1;

  // Then move on to M611 - Humidity
  int humidity_idx = payload.indexOf('M');
  p->humidity = payload.substring(humidity_idx+1, humidity_idx+3).toInt();

  // Then move on to N10020 - air pressure
  int pressure_idx = payload.indexOf('N');
  p->air_pressure = payload.substring(pressure_idx+1, pressure_idx+6).toInt()*.1;

  // Handle Gust
  if(p->wind_speed > WindGust) {
    WindGust = p->wind_speed;
    debugln("Update WindGust");
  }
  
  if(loopcount >= 300) {
    loopcount = 0;
    WindGust = p->wind_speed;
    debugln("10 min expired -> reset counter");
  }
  // Increment loopcount for Gust
  loopcount++;
}

// Clear the packet before working on the next
void clear_pkt(WS3Packet* p) {
  p->wind_dir        = 0;
  p->wind_angle      = 0;
  p->wind_freq       = 0;
  p->wind_speed      = 0;
  p->wind_speed_1m   = 0;
  p->wind_speed_5m   = 0;
  p->rain_bucket_cnt = 0;
  p->rt_rain_bucket  = 0;
  p->rain_1m         = 0;
  p->rain_1h         = 0;
  p->rain_24h        = 0;
  p->temp_f          = 0;
  p->humidity        = 0;
  p->air_pressure    = 0;
}

#ifdef DEBUG
// Print the data
void print_weather(WS3Packet* p){
  Serial.print("Wind Direction (realtime): ");
  Serial.println(p->wind_dir, DEC);
  Serial.print("Wind direction angle : ");
  Serial.print(p->wind_angle, DEC);
  Serial.print(" degree ");
  Serial.println(str_windir(p->wind_angle)); 
  Serial.print("Wind speed Frequency: ");
  Serial.print(p->wind_freq, DEC);
  Serial.println(" Hz");
  Serial.print("Wind speed: ");
  Serial.print(p->wind_speed/10, DEC);
  Serial.println(" m/s");
  Serial.print("Wind speed 1m: ");
  Serial.print(p->wind_speed_1m/10, DEC);
  Serial.println(" m/s");
  Serial.print("Wind speed 5m: ");
  Serial.print(p->wind_speed_5m/10, DEC);
  Serial.println(" m/s");
  Serial.print("temp_f: ");
  Serial.print(p->temp_f, DEC); 
  Serial.println(" deg. C.");

  Serial.print("Rain buckets / buckets 1m: ");
  Serial.print(p->rain_bucket_cnt, DEC); 
  Serial.print(" / ");
  Serial.println(p->rt_rain_bucket, DEC); 
  
  Serial.print("Rain 1m / 1H / 24H: ");
  Serial.print(p->rain_1m*0.1, DEC); 
  Serial.print(" / ");
  Serial.print(p->rain_1h*0.1, DEC); 
  Serial.print(" / ");
  Serial.print(p->rain_24h*.1, DEC); 
  Serial.println(" mm");

  Serial.print("humidity: ");
  Serial.print(p->humidity, DEC); 
  Serial.println(" %");

  Serial.print("air_pressure: ");
  Serial.print(p->air_pressure, DEC); 
  Serial.println(" hpa");
}
#endif /* DEBUG */


// Print the data over MQTT
void push_weather(WS3Packet* p) {
  String MQPayload;
  byte humidity_status = 0;   // Domoticz humdity status
  byte bar_forecast    = 0;   // Domoticz baro forcast 
  float pression       = 0.0; // Pression
  float wct            = 0.0; // Windchill temperature
  
  // Rain 
  MQPayload = "{ \"idx\" : "+ String(idx_rain) +",\"nvalue\" : 0, \"svalue\": \"" + String(p->rain_1h*10) + ";" + String(p->rain_1m*0.1) + "\"}";
  sendMQTTPayload(MQPayload);

  // Temperature / Humidity / Baro

  // Humidity stuff
  // O = Normal
  // 1 = Confortable
  // 2 = Dry        (<35%)
  // 3 = Wet        (>70%)
  if (p->humidity < 35) {
    humidity_status = 2;  // Dry
  } else if (p->humidity >= 70) {
    humidity_status = 3;  // Wet
  } else {
    if ((p->humidity >=35) && (p->humidity <50)) { 
      humidity_status = 1; // Confortable
    } else {
      // >=50 -> 70%
      humidity_status = 0; // Normal
    }
  }
  // Correct the pression according to altitude
  pression = p->air_pressure + (ALTITUDE / 8.3);
  // Pression should be adapted as it should be on sea level
  
  // Prediction
  // 0 : No info
  // 1 : Sunny
  // 2 : Partly cloudy
  // 3 : Cloudy
  // 4 : Rain
  // Maybe have to store values for last 3 hours?
  // XXX: fix value with altitude
  if (pression < 1000) {
    bar_forecast = 4; // Rain
  } else if (pression < 1020) {
    bar_forecast = 3; // Cloudy
  } else if (pression < 1030) {
    bar_forecast = 2; // Partly cloudy 
  } else {
    bar_forecast = 1; // Sunny
  }
  // See : https://github.com/G6EJD/ESP32-Weather-Forecaster
  MQPayload = "{ \"idx\" : "+ String(idx_temp) +",\"nvalue\" : 0, \"svalue\": \"" + String(p->temp_f) + ";" +  String(p->humidity) + ";"+String(humidity_status)+ ";" + String(pression) +";"+String(bar_forecast)+"\"}";

  sendMQTTPayload(MQPayload);
  // Wind
  // Last temperature is WindChill
  // Have to compute it https://www.alpiniste.fr/windchill-calculateur/
  //wct = p->temp_f;

  wct = 13.12 + 0.6215*p->temp_f - 11,37*pow((p->wind_speed * 3.6),0.16)+0.3965*p->temp_f*pow((p->wind_speed * 3.6),0.16);
  MQPayload = "{ \"idx\" : "+ String(idx_windir) +",\"nvalue\" : 0, \"svalue\": \"" + String(p->wind_angle) + ";" +  String(str_windir(p->wind_angle)) + ";" + String(p->wind_speed) + ";" + String(WindGust) +";" + String(p->temp_f) + ";"+String(wct)+"\"}";
  sendMQTTPayload(MQPayload);
}


// Processing the packet.
bool process_packet(String pkt, WS3Packet* p) {
  debugln("[D] process_packet - ALive!");
  debugln(pkt);

  // Allocate bytes for the payload
  String payload;
  payload.reserve(WS3_PKT_LEN-WS3_CHK_LEN);

  #ifdef DONT_CHKSUM
  // everything after the * is checksum (2 char long)
  unsigned long chksum;
  #endif /* DONT_CHKSUM */
 
  // Check if the 75rd character is *
  if (pkt.charAt(CHK_SUM_DELINEATOR) != '*') {
    debugln("Packed invalid; no * character at position 75!");    
    return false;
  #ifdef DONT_CHKSUM
  } else {
    // The character indicating the checksum is coming is in the correct place. Yay.
    // Now, we need to pull the two ascii characters that are transmitted to us
    // and turn them into a single byte. E.G. Char 3, Char D should convert to 0x3D.
    //
    // We can do this with the strtoul() function; we indicate that we wante base 16
    
    chksum = strtoul(pkt.substring(CHK_SUM_DELINEATOR+1, CHK_SUM_DELINEATOR+2).c_str(),NULL,16);
  }
  #endif /* DONT_CHKSUM */

  // We have the checksum, Now we can bother to get the payload
  payload = pkt.substring(0, CHK_SUM_DELINEATOR);

  // And try to validate...
  #ifndef DONT_CHKSUM
  if(!validate_packet(payload, chksum)){
    debugln("invalid packet! :(");
    return false;
  } else {
    debugln("Valid packet!");
  }
  #endif /* DONT_CHKSUM */
  parse_packet(payload, p);
  return true;
}

void loop() {
  // MQTT
  if (!client.connected()) {
    reconnect();
  }
 
  // While data comes in and we don't have a pending packet to process...
  while (WS3.available() && pkt_ok !=true) {
    
    // Pull the bytes off the stream
    char inChar = (char)WS3.read();

    // And build up the packet
    pkt_str += inChar;
    
    // Until we hit the end
    if (inChar == '\n') {
      pkt_ok = true;
    }
  }

  // Yay, we now have a packet! 
  // Now, we attempt to parse out the string into a packet that we can work with
  if (pkt_ok) {
    debugln("pkt_ok!");

    // At this point, we have a string of characters that was probably a valid packet
    // We set get some memory and attempt to parse the string into the struct
    WS3Packet p = {};
  
    // Validate the payload, then parse it. 
    if (process_packet(pkt_str, &p)) {
      // print results if parse OK
      #ifdef DEBUG
      print_weather(&p);
      #endif /* DEBUG */
      // Push to MQTT
      push_weather(&p);
      debugln("processed");
    } else {
      debugln("unable to parse packet :(");
    }

   
    // clear so we can start again
    pkt_str = "";
    pkt_ok = false;
    clear_pkt(&p); 
 }
 ArduinoOTA.handle();
}
