#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <DHT.h>
#include <Preferences.h>
Preferences preferences; // Para guardar umbrales en memoria no volátil
// Configuración WiFi
const char* ssid = "TuWiFi";
const char* password = "TuPassword";
// Hardware (pines para ESP32 38 pines)
#define DHTPIN 4
#define DHTTYPE DHT22
#define COOLING_PIN 5
#define LED_ON_PIN 18
#define LED_OFF_PIN 19
DHT dht(DHTPIN, DHTTYPE);
LiquidCrystal_I2C lcd(0x27, 16, 2);
WebServer server(80);
// Variables y umbrales
float temp, hum;
bool coolingState = false;
bool manualMode = false;
float umbralAlto = 28.0; // Valores por defecto
float umbralBajo = 20.0;
void setup() {
Serial.begin(115200);
// Cargar umbrales desde memoria
preferences.begin("umbrales", false);
umbralAlto = preferences.getFloat("alto", 28.0);
umbralBajo = preferences.getFloat("bajo", 20.0);
preferences.end();
// Inicializar hardware
lcd.init();
lcd.backlight();
dht.begin();
pinMode(COOLING_PIN, OUTPUT);
pinMode(LED_ON_PIN, OUTPUT);
pinMode(LED_OFF_PIN, OUTPUT);
digitalWrite(COOLING_PIN, LOW);
digitalWrite(LED_ON_PIN, LOW);
digitalWrite(LED_OFF_PIN, HIGH);
// Conectar WiFi
WiFi.begin(ssid, password);
lcd.setCursor(0, 0);
lcd.print("Conectando WiFi...");
while (WiFi.status() != WL_CONNECTED) delay(500);
lcd.clear();
lcd.print("IP:");
lcd.print(WiFi.localIP());
delay(2000);
// Rutas web
server.on("/", handleRoot);
server.on("/data", handleData);
server.on("/control", handleControl);
server.on("/umbrales", handleUmbrales); // Nueva ruta para configurar umbrales
server.begin();
}
void loop() {
server.handleClient();
static unsigned long lastRead = 0;
if (millis() - lastRead >= 2000) {
lastRead = millis();
temp = dht.readTemperature();
hum = dht.readHumidity();
// Control automático (si no está en manual)
if (!manualMode) {
if (temp > umbralAlto && !coolingState) {
setCooling(true);
} else if (temp < umbralBajo && coolingState) {
setCooling(false);
}
}
// Actualizar LCD
updateLCD();
}
}
// ================= FUNCIONES PRINCIPALES =================
void setCooling(bool state) {
coolingState = state;
digitalWrite(COOLING_PIN, state ? HIGH : LOW);
digitalWrite(LED_ON_PIN, state ? HIGH : LOW);
digitalWrite(LED_OFF_PIN, state ? LOW : HIGH);
}
void updateLCD() {
lcd.setCursor(0, 0);
lcd.print("T:");
lcd.print(temp, 1);
lcd.print("C H:");
lcd.print(hum, 1);
lcd.print("%");
lcd.setCursor(0, 1);
lcd.print("Enf:");
lcd.print(coolingState ? "ON " : "OFF");
lcd.print(manualMode ? "(M)" : "(A)");
lcd.print(" ");
lcd.print(umbralAlto, 0);
lcd.print("/");
lcd.print(umbralBajo, 0);
}
// ================= HANDLERS WEB =================
void handleRoot() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Control Climático</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--bg-color: #ffffff;
--text-color: #000000;
--card-bg: #f0f0f0;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #121212;
--text-color: #e0e0e0;
--card-bg: #1e1e1e;
}
}
body {
font-family: Arial;
margin: 20px;
background-color: var(--bg-color);
color: var(--text-color);
}
.card {
background: var(--card-bg);
padding: 20px;
margin: 10px;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.on { color: #4CAF50; font-weight: bold; }
.off { color: #F44336; }
button {
padding: 10px 15px;
margin: 5px;
cursor: pointer;
background: #2196F3;
color: white;
border: none;
border-radius: 5px;
}
input {
padding: 8px;
margin: 5px;
width: 60px;
}
</style>
</head>
<body>
<h1>🌡️ Control Climático ESP32</h1>
<div class="card">
<canvas id="tempChart" width="400" height="200"></canvas>
</div>
<div class="card">
<h2>Datos Actuales</h2>
<p>Temperatura: <span id="tempValue">)rawliteral" + String(temp) + R"rawliteral(</span>°C</p>
<p>Humedad: <span id="humValue">)rawliteral" + String(hum) + R"rawliteral(</span>%</p>
<p>Enfriamiento: <span id="coolingStatus" class=")rawliteral" + (coolingState ? "on" : "off") + R"rawliteral(">
)rawliteral" + (coolingState ? "❄️ ENCENDIDO" : "🔥 APAGADO") + R"rawliteral(</span>
</p>
<p>Modo: <strong id="modeStatus">)rawliteral" + (manualMode ? "🛠️ MANUAL" : "🤖 AUTOMÁTICO") + R"rawliteral(</strong></p>
</div>
<div class="card">
<h2>Umbrales</h2>
<form action="/umbrales" method="GET">
<label>Alto: <input type="number" step="0.1" name="alto" value=")rawliteral" + String(umbralAlto) + R"rawliteral("> °C</label><br>
<label>Bajo: <input type="number" step="0.1" name="bajo" value=")rawliteral" + String(umbralBajo) + R"rawliteral("> °C</label><br>
<button type="submit">Guardar</button>
</form>
</div>
<div class="card">
<h2>Control</h2>
<button onclick="sendCommand('manual')">Modo Manual</button>
<button onclick="sendCommand('auto')">Modo Auto</button>
<button onclick="sendCommand('toggle')">Toggle Enfriamiento</button>
</div>
<script>
// Gráfico
const ctx = document.getElementById('tempChart').getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'Temperatura (°C)',
data: [],
borderColor: 'rgb(255, 99, 132)',
tension: 0.1
},
{
label: 'Humedad (%)',
data: [],
borderColor: 'rgb(54, 162, 235)',
tension: 0.1
}
]
},
options: {
scales: { y: { beginAtZero: false } },
responsive: true,
maintainAspectRatio: false
}
});
// Actualizar datos
function fetchData() {
fetch('/data')
.then(res => res.json())
.then(data => {
document.getElementById('tempValue').textContent = data.temperature.toFixed(1);
document.getElementById('humValue').textContent = data.humidity.toFixed(1);
document.getElementById('coolingStatus').className = data.cooling ? 'on' : 'off';
document.getElementById('coolingStatus').textContent = data.cooling ? '❄️ ENCENDIDO' : '🔥 APAGADO';
document.getElementById('modeStatus').textContent = data.manualMode ? '🛠️ MANUAL' : '🤖 AUTOMÁTICO';
// Actualizar gráfico
if (chart.data.labels.length > 15) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
chart.data.datasets[1].data.shift();
}
chart.data.labels.push(new Date().toLocaleTimeString());
chart.data.datasets[0].data.push(data.temperature);
chart.data.datasets[1].data.push(data.humidity);
chart.update();
});
}
// Comandos
function sendCommand(cmd) {
fetch('/control?cmd=' + cmd)
.then(() => fetchData());
}
setInterval(fetchData, 2000);
fetchData();
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
void handleData() {
String json = String() +
"{\"temperature\":" + temp +
",\"humidity\":" + hum +
",\"cooling\":" + (coolingState ? "true" : "false") +
",\"manualMode\":" + (manualMode ? "true" : "false") +
",\"umbralAlto\":" + umbralAlto +
",\"umbralBajo\":" + umbralBajo + "}";
server.send(200, "application/json", json);
}
void handleControl() {
if (server.hasArg("cmd")) {
String cmd = server.arg("cmd");
if (cmd == "manual") manualMode = true;
else if (cmd == "auto") manualMode = false;
else if (cmd == "toggle") setCooling(!coolingState);
}
server.send(200, "text/plain", "OK");
}
void handleUmbrales() {
if (server.hasArg("alto") && server.hasArg("bajo")) {
umbralAlto = server.arg("alto").toFloat();
umbralBajo = server.arg("bajo").toFloat();
// Guardar en memoria no volátil
preferences.begin("umbrales", false);
preferences.putFloat("alto", umbralAlto);
preferences.putFloat("bajo", umbralBajo);
preferences.end();
}
server.sendHeader("Location", "/");
server.send(303);
}