/*
* Ολοκληρωμένο Σύστημα Ελέγχου Κλίματος
* ARD:icon II IoT - DHT11 + OLED + 2xRelay + Buzzer + Web Server
*/
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
#include <DHT.h>
// ========== ΡΥΘΜΙΣΕΙΣ WiFi ==========
const char* ssid = "Redmi";
const char* password = "12345678";
// ========== OLED (I2C) ==========
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// ========== DHT11 ==========
#define DHTPIN 33
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
// ========== ΕΞΟΔΟΙ ==========
#define BUZZER_PIN 5
#define RELAY_HEAT_PIN 16
#define RELAY_COOL_PIN 17
// ========== ΟΡΙΑ ΛΕΙΤΟΥΡΓΙΑΣ ==========
const float TEMP_LOW = 18.0;
const float TEMP_HIGH = 28.0;
const float ALARM_LOW = 13.0;
const float ALARM_HIGH = 33.0;
// ========== ΜΕΤΑΒΛΗΤΕΣ ==========
float temperature = 0.0;
float humidity = 0.0;
unsigned long lastRead = 0;
const long readInterval = 2000;
bool manualOverride = false;
bool manualHeat = false;
bool manualCool = false;
WebServer server(80);
// ========== HTML ΣΕΛΙΔΑ (ίδια, δεν αλλάζει) ==========
String getHTML() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>ARD:icon II Climate Control</title>
<style>
body { font-family: 'Segoe UI', Arial; text-align: center; background: linear-gradient(135deg, #1e3c72, #2a5298); margin: 0; padding: 20px; color: white; }
.container { max-width: 700px; margin: 0 auto; background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); padding: 25px; border-radius: 30px; box-shadow: 0 8px 20px rgba(0,0,0,0.3); }
h1 { font-size: 28px; margin-bottom: 5px; }
h2 { font-size: 18px; font-weight: normal; opacity: 0.9; margin-top: 0; }
.sensor-box { background: rgba(0,0,0,0.3); border-radius: 20px; padding: 20px; margin: 20px 0; display: flex; justify-content: space-around; flex-wrap: wrap; }
.sensor { font-size: 28px; margin: 10px; }
.sensor span { font-weight: bold; background: rgba(255,255,255,0.2); padding: 8px 15px; border-radius: 50px; }
.unit { font-size: 16px; }
.status { background: #ff8c00; border-radius: 20px; padding: 15px; margin: 20px 0; font-weight: bold; }
.relay-buttons { display: flex; gap: 15px; justify-content: center; margin: 20px 0; }
button { background: #2ecc71; border: none; padding: 12px 25px; font-size: 16px; border-radius: 50px; cursor: pointer; font-weight: bold; transition: 0.2s; }
button:hover { transform: scale(1.05); opacity: 0.9; }
.btn-off { background: #e74c3c; }
.btn-auto { background: #3498db; }
.footer { font-size: 12px; margin-top: 20px; opacity: 0.7; }
.alarm { color: #ff4444; animation: blink 1s infinite; }
@keyframes blink { 50% { opacity: 0.5; } }
</style>
<script>
function fetchData() {
fetch('/data')
.then(response => response.json())
.then(data => {
document.getElementById('temp').innerHTML = data.temp.toFixed(1);
document.getElementById('hum').innerHTML = data.hum.toFixed(1);
document.getElementById('heatStatus').innerHTML = data.heat ? 'ON 🔥' : 'OFF ❄️';
document.getElementById('coolStatus').innerHTML = data.cool ? 'ON 💨' : 'OFF ⚪';
document.getElementById('autoMode').innerHTML = data.auto ? 'AUTO ✓' : 'MANUAL';
let alarmDiv = document.getElementById('alarmMsg');
if (data.alarm) alarmDiv.innerHTML = '⚠️ ΑΚΡΑΙΑ ΘΕΡΜΟΚΡΑΣΙΑ! ⚠️';
else alarmDiv.innerHTML = 'Κανονική λειτουργία';
})
.catch(err => console.log('Error:', err));
}
function setMode(auto) {
fetch('/mode?auto=' + auto)
.then(() => fetchData());
}
function setRelay(relay, state) {
fetch('/relay?r=' + relay + '&s=' + state)
.then(() => fetchData());
}
setInterval(fetchData, 2000);
window.onload = fetchData;
</script>
</head>
<body>
<div class='container'>
<h1>ARD:icon II IoT</h1>
<h2>Σύστημα Ελέγχου Κλίματος</h2>
<div class='sensor-box'>
<div class='sensor'>🌡️ Θερμοκρασία: <span id='temp'>--</span> <span class='unit'>°C</span></div>
<div class='sensor'>💧 Υγρασία: <span id='hum'>--</span> <span class='unit'>%</span></div>
</div>
<div class='status'>
Θέρμανση: <span id='heatStatus'>--</span> | Ψύξη: <span id='coolStatus'>--</span><br>
Λειτουργία: <span id='autoMode'>--</span>
</div>
<div id='alarmMsg' class='alarm'></div>
<div class='relay-buttons'>
<button onclick='setMode(true)'>🤖 AUTO</button>
<button onclick='setMode(false)'>✋ MANUAL</button>
</div>
<div class='relay-buttons'>
<button class='btn-off' onclick='setRelay("heat",1)'>🔥 Heat ON</button>
<button class='btn-off' onclick='setRelay("heat",0)'>🔥 Heat OFF</button>
<button class='btn-off' onclick='setRelay("cool",1)'>❄️ Cool ON</button>
<button class='btn-off' onclick='setRelay("cool",0)'>❄️ Cool OFF</button>
</div>
<div class='footer'>Live ανανέωση κάθε 2 sec | 1ο ΕΠΑΛ Συκεών</div>
</div>
</body>
</html>
)rawliteral";
return html;
}
// ========== ΑΝΑΓΝΩΣΗ DHT11 ==========
void readSensor() {
if (millis() - lastRead >= readInterval) {
float h = dht.readHumidity();
float t = dht.readTemperature();
if (!isnan(h) && !isnan(t)) {
humidity = h;
temperature = t;
Serial.print("Temp: "); Serial.print(temperature); Serial.print("°C Hum: "); Serial.println(humidity);
} else {
Serial.println("DHT read error");
}
lastRead = millis();
}
}
// ========== ΕΛΕΓΧΟΣ ΡΕΛΕ & BUZZER ==========
void controlSystem() {
if (!manualOverride) {
if (temperature < TEMP_LOW) {
digitalWrite(RELAY_HEAT_PIN, HIGH);
digitalWrite(RELAY_COOL_PIN, LOW);
} else if (temperature > TEMP_HIGH) {
digitalWrite(RELAY_HEAT_PIN, LOW);
digitalWrite(RELAY_COOL_PIN, HIGH);
} else {
digitalWrite(RELAY_HEAT_PIN, LOW);
digitalWrite(RELAY_COOL_PIN, LOW);
}
} else {
// manual mode: τα ρελέ ελέγχονται μόνο από τα API calls
}
// Συναγερμός
if (temperature < ALARM_LOW || temperature > ALARM_HIGH) {
static unsigned long lastBuzzer = 0;
if (millis() - lastBuzzer > 5000) {
for (int i = 0; i < 3; i++) {
tone(BUZZER_PIN, 2000);
delay(200);
noTone(BUZZER_PIN);
delay(100);
}
lastBuzzer = millis();
}
} else {
noTone(BUZZER_PIN);
}
}
// ========== ΕΝΗΜΕΡΩΣΗ OLED ==========
void updateDisplay() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("=== CLIMATE CTRL ===");
display.setTextSize(2);
display.setCursor(0, 18);
display.print(temperature, 1);
display.println(" C");
display.setTextSize(1);
display.setCursor(0, 42);
display.print("Hum: ");
display.print(humidity, 1);
display.println("%");
display.setCursor(0, 52);
if (digitalRead(RELAY_HEAT_PIN) == HIGH)
display.println("HEAT ON ");
else if (digitalRead(RELAY_COOL_PIN) == HIGH)
display.println("COOL ON");
else
display.println("IDLE ");
display.display();
}
// ========== WEB SERVER ROUTES ==========
void setupWebServer() {
server.on("/", []() {
server.send(200, "text/html", getHTML());
});
server.on("/data", []() {
bool heatState = digitalRead(RELAY_HEAT_PIN);
bool coolState = digitalRead(RELAY_COOL_PIN);
bool alarm = (temperature < ALARM_LOW || temperature > ALARM_HIGH);
String json = "{\"temp\":" + String(temperature) +
",\"hum\":" + String(humidity) +
",\"heat\":" + String(heatState) +
",\"cool\":" + String(coolState) +
",\"auto\":" + String(!manualOverride) +
",\"alarm\":" + String(alarm) + "}";
server.send(200, "application/json", json);
});
// ΔΙΟΡΘΩΜΕΝΟΣ /mode (χωρίς auto keyword)
server.on("/mode", []() {
if (server.hasArg("auto")) {
int autoModeVal = server.arg("auto").toInt();
manualOverride = (autoModeVal == 0);
if (manualOverride) {
digitalWrite(RELAY_HEAT_PIN, LOW);
digitalWrite(RELAY_COOL_PIN, LOW);
manualHeat = false;
manualCool = false;
}
server.send(200, "text/plain", "OK");
} else {
server.send(400, "text/plain", "Missing param");
}
});
server.on("/relay", []() {
if (manualOverride && server.hasArg("r") && server.hasArg("s")) {
String relay = server.arg("r");
int state = server.arg("s").toInt();
if (relay == "heat") {
digitalWrite(RELAY_HEAT_PIN, state);
manualHeat = state;
} else if (relay == "cool") {
digitalWrite(RELAY_COOL_PIN, state);
manualCool = state;
}
server.send(200, "text/plain", "OK");
} else if (!manualOverride) {
server.send(403, "text/plain", "Not in manual mode");
} else {
server.send(400, "text/plain", "Bad request");
}
});
server.begin();
Serial.println("Web server started");
}
// ========== SETUP ==========
void setup() {
Serial.begin(115200);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("OLED failed");
while (true);
}
display.clearDisplay();
display.println("Booting...");
display.display();
dht.begin();
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RELAY_HEAT_PIN, OUTPUT);
pinMode(RELAY_COOL_PIN, OUTPUT);
digitalWrite(RELAY_HEAT_PIN, LOW);
digitalWrite(RELAY_COOL_PIN, LOW);
WiFi.begin(ssid, password);
display.clearDisplay();
display.setCursor(0,0);
display.print("Connecting WiFi");
display.display();
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 40) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nConnected! IP: " + WiFi.localIP().toString());
display.clearDisplay();
display.setCursor(0,0);
display.print("IP: ");
display.println(WiFi.localIP().toString());
display.display();
delay(2000);
} else {
Serial.println("\nWiFi Failed");
display.println("WiFi Error");
display.display();
while (true);
}
setupWebServer();
display.clearDisplay();
}
// ========== LOOP ==========
void loop() {
server.handleClient();
readSensor();
controlSystem();
updateDisplay();
delay(50);
}