#include <EEPROM.h>
#include <WiFi.h>
#include <PubSubClient.h>

#define EEPROM_SIZE 110
#define sensor_distance 10

//GPIO y canal PWM. Ajustar bien los pines al esp32!!!!
#define pin_temp A0 // Potenciometro de temperatura (el wifi puede llegar a interferir con algunos pines ADC)
#define pin_lights 22 // LED de iluminación (PWM, cualquier GPIO con OUTPUT puede)      //23
#define lights_ch 0 // Canal PWM para las luces (acá se mandan los comandos PWM)
#define pin_pump 15 // LED de la bomba
#define pump_button 23 // boton de control de la bomba
#define TRIGGER 32 // Trigger ultrasonido
#define ECHO 33 // Echo ultrasonido
#define pin_dose A3 // Potenciometro de dosis de cloro
 

//Posiciones de la EEPROM, los params se definen desde 100 en adelante (el ID está en la posicion 0 y no debe tener mas de 99 caracteres)
#define pos_width 100            //Ancho, largo y altura deben ser enteros (Para poder guardarlos en la EEPROM), entre 0 y 255
#define pos_length 101
#define pos_height 102
#define pos_pumptimer 103	       //Timer de la bomba, en segundos (0 sin timer)


//-----------------------------------Topics------------------------------------------------
//Topics de estado y usuario
#define t_temp "user/temperatura"         //Solo escritura (respecto a la placa) Return: temperatura (int)
#define t_dose "user/dosis"               //Solo escritura                       Return: Valor de dosis
#define t_lights "user/luces"             //Solo lectura
#define t_webpump "user/bomba"            //Lectura y Escritura                  Recibe: ON-OFF (String)
#define t_wlevel "user/nivel-agua"        //Solo escritura                       Return: porcentaje (int)
#define t_message "user/messages"         //Mensajes en general (logs), solo escritura
#define t_alarm "status/levelalarm"       //Solo escritura                       Return: ACTIVATE (String)
#define t_delay "status/delay"            //Solo lectura, muestreo               Recibe: segundos en milis   1seg = 1000
#define t_start "status/start"            //Solo lectura                         Recibe: ON-OFF
//Configuraciones EEPROM (Esritura)
#define w_name "config/name"              //Solo lectura
#define w_width "config/ancho"            //Solo lectura
#define w_length "config/largo"           //Solo lectura
#define w_height "config/altura"          //Solo lectura
#define w_timer "config/pumptimer"        //Solo lectura

//------------------------------Timers, Interrupts, Variables----------------------------------------

int current_timer = 0;
int lights = 0;
boolean pump_running = false;
boolean pump_manual = false;
boolean defrost_mode = false;
int water_level = 0;                                          //Porcentaje de agua
int sample_delay = 2000;
float prev_dose = 0;
boolean system_start = false;


//------------------------Configuración de la red Wi-Fi--------------------------------------

//const char* ssid = "Personal 200 2.4 GHz";
//const char* password = "01427234780307";
const char* ssid = "Wokwi-GUEST";
const char* password = NULL;

//------------------------Configuración del broker MQTT---------------------------------------

//const char* mqtt_server = "broker.hivemq.com";
//const char* mqttUser = "Martin";
//const char* mqttPassword = "123456";
const char* mqtt_server = "test.mosquitto.org";
const char* mqttUser = NULL;
const char* mqttPassword = NULL;
WiFiClient espClient;
PubSubClient client(espClient);

//------------------------------Funciones de la piscina------------------------------------

void setDelay(int delay);                            //Establece el tiempo entre muestras
void setStart(String msg);
void sendmessage(String msg);                        //Manda mensajes en formato Log 
void eepromloader(String param, String value);       //Escritura en EEPROM
void eepromread(String param);                       //Lectura de parametros en EEPROM
String GetName();                                    //Obtener nombre en EEPROM
void SetName(String name);                           //Cambio de Nombre en EEPROM
void UpdateTemp();                                   //Verifica temperatura, activa modo descongelamiento
void LowTempDefrost();                               //Modo descongelamiento a baja temperatura
void SetLights(int intensity);                       //Control de las luces
void PoolLoad();                                     //Verifica nivel de agua respecto a la capacidad  --- Agregar alarma ---
int PoolCap();                                       //Obtiene capacidad de la piscina
float WaterLevel();                                  //Obtiene cantidad de agua en la piscina
void SwitchPump(boolean run);                        //Cambia el estado de la bomba encendido/apagado, activa timers si aplica
void manualPumpControl(String pos, String source);   //Activa la bomba desde web o interruptor
void onTimer();                                      //Activa Timer para la bomba
void button_interrupt();                             //Permite interrumpir el timer si se desactiva la bomba antes

void setup() {
  EEPROM.begin(EEPROM_SIZE);
  //WIFI y MQTT
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
	//Pins y timers
  //ledcAttachPin(pin_lights, lights_ch);              //Conecta LED al canal para control PWM
  //ledcSetup(lights_ch, 1000, 8);                     //1Khz frecuencia, rango de 8 bits (0-255)
  pinMode(pump_button, INPUT_PULLUP);
	pinMode(TRIGGER, OUTPUT);
	pinMode(ECHO, INPUT);
  pinMode(pin_pump, OUTPUT);
  pinMode(pin_temp, INPUT);
  pinMode(pin_dose, INPUT);
}

void loop() {
  //MQTT
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
  //Actualizacion de estado
  if (system_start){
    UpdateTemp();
    UpdateDose();
    PoolLoad();
    if (digitalRead(pump_button) == LOW)
      button_interrupt();
    if (pump_running && !defrost_mode && current_timer > 0){
      if (sample_delay > 0)
        current_timer -= sample_delay;
      else
        current_timer -= 100;
      if (current_timer <= 0){
        onTimer();
      }
    }
    if (defrost_mode)
      current_timer = 1000;
  }
  //Delay
  delay(sample_delay);
}

void setup_wifi() {                            //Establece conexión WIFI
  delay(10);
  Serial.println();
  Serial.print("Conectando a ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi conectado");
  Serial.println("Direccion IP: ");
  Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* message, unsigned int length) {    //Recibe mensajes e imprime en serial
  Serial.println("Mensaje recibido en topic: ");
  Serial.print(topic);
  Serial.print(". Mensaje: ");
  String messageTemp;

  for (int i = 0; i < length; i++) {
    Serial.print((char)message[i]);
    messageTemp += (char)message[i];
  }
  Serial.println();
  //Invocaciones
  if ((String(topic) == (t_lights)) && system_start){
    SetLights(messageTemp.toInt());
    lights = messageTemp.toInt();
  }
  else
    if ((String(topic) == (t_webpump)) && system_start)
      manualPumpControl(messageTemp, "Aplicacion Web");
  else
    if (String(topic) == (t_delay))
      setDelay(messageTemp.toInt());
  else
    if (String(topic) == (t_start))
      setStart(messageTemp);
  else
    if (String(topic) == (w_name))
      eepromloader("ID", messageTemp);
  else
    if (String(topic) == (w_width))
      eepromloader("WIDTH", messageTemp);
  else
    if (String(topic) == (w_length))
      eepromloader("LENGTH", messageTemp);
  else
    if (String(topic) == (w_height))
      eepromloader("HEIGHT", messageTemp);
  else
    if (String(topic) == (w_timer))
      eepromloader("TIMER", messageTemp);
}
                               
void reconnect() {                                                  //Conecta a MQTT y reconecta si la conexion falla
  while (!client.connected()) {
    Serial.print("Intentando conectar a MQTT...");
    if (client.connect("Martin", mqttUser, mqttPassword )) {
      Serial.println("Conectado");
      //client.subscribe("test/topic");
      setup_subs();
    } else {
      Serial.print("Error, rc=");
      Serial.print(client.state());
      Serial.println(" Reintentando en 5 segundos ");
      delay(5000);
    }
  }
}

void setup_subs(){
  client.subscribe(t_lights, 1);
  client.subscribe(t_webpump, 1);
  client.subscribe(t_delay, 1);
  client.subscribe(t_start, 1);
  client.subscribe(w_name, 1);
  client.subscribe(w_width, 1);
  client.subscribe(w_length, 1);
  client.subscribe(w_height, 1);
  client.subscribe(w_timer, 1);
}

void setStart(String msg){
  if ((msg == "ON") && !system_start){
    system_start = true;
    sendmessage("Sistema activado");
    SetLights(lights);
  }
  else
    if (msg == "OFF" && system_start){
      if (defrost_mode){
        sendmessage("Desactivando modo descongelamiento");
        defrost_mode = false;
        if (!pump_manual)                                                     //Desactivar bomba si se activo automaticamente, si fue manual sigue
          SwitchPump(false);
      }
      if (pump_manual){
        pump_manual = false;
        SwitchPump(false);
      }
      SetLights(0);
      system_start = false;
      sendmessage("Sistema desactivado");
    }
}

void setDelay(int delay){
    sample_delay = delay;
    sendmessage("Tiempo entre muestras cambiado a: " + String(delay) + " milisegundos");
}

void sendmessage(String msg) {                                //Usar para logs
  int minutos = millis()/60000;
  int horas = minutos / 60;
  int segundos = (millis()/1000) % 60;
  String tiempo = timeFormat(horas) + ":" + timeFormat(minutos) + ":" + timeFormat(segundos);
  String log = ("Time: " + tiempo + " ---- Evento: " + msg);
  client.publish((t_message), log.c_str());
  Serial.println(log);
  delay(1000);
}

String timeFormat(int val){
  if (val < 10)
    return ("0" + String(val));
  else
    return String(val);
}

void eepromloader(String param, String value){                 // Carga valores en EEPROM
	param.toUpperCase();
	if (param == "ID"){
    if ((value.length() < 100) && (value.length() > 0)){
		  SetName(value);
      sendmessage("Nombre cambiado a: " + value);
    }
    else
      sendmessage("Error: No se puede cambiar nombre, debe ser menor a 100 caracteres");
  }
	else
		if (param == "WIDTH" && (value.toInt() >= 0) && (value.toInt() < 256)){     //EEPROM en esp32 es Flash, no hay update()
			EEPROM.write(pos_width, (byte)value.toInt());
      EEPROM.commit();
      sendmessage("Ancho cambiado a: " + value);
    }
	else
		if (param == "HEIGHT" && (value.toInt() >= 0) && (value.toInt() < 256)){
			EEPROM.write(pos_height, (byte)value.toInt());
      EEPROM.commit();
      sendmessage("Altura cambiada a: " + value);
    }
	else
		if (param == "LENGTH" && (value.toInt() >= 0) && (value.toInt() < 256)){
			EEPROM.write(pos_length, (byte)value.toInt());
      EEPROM.commit();
      sendmessage("Largo cambiado a: " + value);
    }
	else
		if (param == "TIMER" && (value.toInt() >= 0) && (value.toInt() < 256)){
			EEPROM.write(pos_pumptimer, (byte)value.toInt());
      EEPROM.commit();
      if (value.toInt() != 0)
        sendmessage("Timer cambiado a: " + value + " segundos");
      else
        sendmessage("Timer desactivado");
    }
  else
    if ((value.toInt() < 0) && (value.toInt() >= 256))
      sendmessage("Error: El valor ingresado debe ser mayor a 0 y menor a 256");
  else
    sendmessage("Error: Parametro inexistente");
}

String GetName(){                                 //Valor en posicion 0 es longitud del nombre
	int index = 1;
	int length = EEPROM.read(0);
	String cad = "";
	while (index <= length){
		cad += (char)EEPROM.read(index);
		index++;
	}
  return cad;
}

void SetName(String name){                        //Valor en posicion 0 es longitud del nombre
	int length;
	length = name.length();
	EEPROM.write(0,length);
	int i = 0;
	while (i < length){ 
		EEPROM.write(i+1,name.charAt(i));
		i++;
	}
  EEPROM.commit();
}

void UpdateTemp(){
	int info = analogRead(pin_temp);
  int temp = map(info, 0, 4095, -20, 50);
  client.publish(t_temp, String(temp).c_str());
	if (temp <= 0){
    if (!defrost_mode){
      defrost_mode = true;
      sendmessage("Temperatura muy baja, activando modo de descongelamiento");
		  LowTempDefrost();                                                     //Activar bomba si la temperatura baja
    }
  }
  else
  if (defrost_mode){
    sendmessage("Desactivando modo descongelamiento");
    defrost_mode = false;
    if (!pump_manual)                                                     //Desactivar bomba si se activo automaticamente, si fue manual sigue
      SwitchPump(false);
  }
}

void UpdateDose(){
	int info = analogRead(pin_dose);
  int val = map(info, 0, 4095, 5, 30);
  float dose = val/10.0;
  client.publish(t_dose, String(dose).c_str());
  if (prev_dose != dose){
    sendmessage("Dosificación cambiada a " + String(dose) + " ppm");
    prev_dose = dose;
  }
}

void LowTempDefrost(){
	  if (!pump_running)
      SwitchPump(true);
}

void SetLights(int intensity){
  if (intensity >= 0 && intensity <= 100){
    int light_value = map(intensity, 0, 100, 0, 255);
	  ledcWrite(lights_ch, light_value);
    if (intensity != 0)
      sendmessage("Luces ajustadas a intensidad: " + String(intensity) + "%");
    else
      sendmessage("Luces desactivadas");
  }
  else
    sendmessage("Error: El valor de intensidad de la luz debe estar entre 0 y 100");
}

void PoolLoad(){             //Asume que el sensor se encuentra algunos cm por encima del nivel máximo
  long duration;
  int distance;
  int percentage;
  int w_distance;
  digitalWrite(TRIGGER, LOW);      //Asegurarse de limpiar la señal
  delayMicroseconds(2);
  digitalWrite(TRIGGER, HIGH);     //Trigger por 10 ms
  delayMicroseconds(10);
  digitalWrite(TRIGGER, LOW);      //Desactivar Trigger
  duration = pulseIn(ECHO, HIGH);  //Activar Echo
  distance = duration*0.034/2;
  if (distance < 10)
    distance = 10;
  w_distance = map(distance, 10, EEPROM.read(pos_height)+sensor_distance, 0, 100);  //Altura máxima 245cm + 10 del sensor
  percentage = 100 - w_distance;
  if (percentage < 0)
    percentage = 0;
  client.publish(t_wlevel, String(percentage).c_str());
  //Alarma de nivel
  if ((water_level - percentage) >= 10){                           //Alarma al bajar 10% o más
    client.publish(t_alarm, "ACTIVATE");
    sendmessage("Atencion: El nivel de agua en la piscina está reduciéndose");
    water_level = percentage;
  }
  if (water_level < percentage)              //Si la pileta sube de nivel se reemplaza
    water_level = percentage;
}

void SwitchPump(boolean run){
	if (run){
    if(!pump_running){
      if ((byte)EEPROM.read(pos_pumptimer) > 0 && !defrost_mode){
        current_timer = EEPROM.read(pos_pumptimer)*1000;
        sendmessage("Encendiendo Timer: " + String(EEPROM.read(pos_pumptimer)) + " segundos");
      }
      digitalWrite(pin_pump, HIGH);
      pump_running = true;
      client.publish(t_webpump, "ON");
      sendmessage("La bomba entro en funcionamiento");
    }
	}
  else
    if (!defrost_mode){
      current_timer = 0;
      digitalWrite(pin_pump, LOW);
      pump_running = false;
      client.publish(t_webpump, "OFF");
      sendmessage("La bomba se ha desactivado");
    }
    else{
      sendmessage("Modo descongelamiento activado, la bomba se mantendra encendida");
      client.publish(t_webpump, "ON");
      current_timer = 0;
    }
}

void manualPumpControl(String pos, String source){
  if(pos == "OFF"){
    if(pump_running){
      sendmessage("Apagado manual de bomba, Origen: " + source);
      pump_manual = false;
      SwitchPump(false);
    }
  }
  else
  if (pos == "ON"){
    if (!pump_running){
      sendmessage("Encendido manual de Bomba, Origen: " + source);
      pump_manual = true;
      SwitchPump(true);
    }
  }
}

void onTimer(){
  sendmessage("Temporizador cumplido, desactivando bomba");
	SwitchPump(false);
}

void button_interrupt(){
  if (pump_running)
    manualPumpControl("OFF", "Switch de bomba");
  else
    manualPumpControl("ON", "Switch de bomba");
}
$abcdeabcde151015202530354045505560fghijfghij