#include <Adafruit_SSD1306.h>
#include <DHT.h>
#include <ESPDateTime.h>
#include <WiFi.h>
#define DEBUG_ON 1
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
//Constants
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define PIN_DHT 15 // what pin we're connected to
DHT dht(PIN_DHT, DHT22); // Initialize DHT 22 (AM2302) sensor for normal 16mhz Arduino
#define PIN_RELAY_HOT 2
#define PIN_RELAY_COLD 0
#define PIN_ENCODER_CLK 12
#define PIN_ENCODER_DT 14
#define PIN_ENCODER_SW 27
float hum;
float temp;
float outputTemp; // either C or F depending on mode
float lastHum;
float lastTemp;
float targetTemp = 36.0f;
float lastTargetTemp = targetTemp;
float targetTempRange = 3.0f;
bool isCelsius = true; // display mode
// logged readings as formatted char buffers
char tempBuffer[5];
char humBuffer[4];
// rotary encoder
int lastClk = HIGH;
int rotValue = 0;
// thermostat vars
bool isHeatOn = false;
bool lastHeatStatus = false;
bool isCoolOn = false;
bool lastCoolStatus = false;
bool isHumidOn = false;
bool isDehumidOn = false;
// menu
#define MENU_ITEMS 4
int menuCursorPosition = 0;
bool menuCursorSelected = false;
bool menuCursorValueSelected = false;
void drawMenu() {
// manually position items to avoid writing a library
display.drawLine(0, 16, SCREEN_WIDTH-1, 16, WHITE);
display.setCursor(0, 18);
switch (menuCursorPosition) {
case 0:
drawMenuCoolMode(true);
drawMenuHeatMode(false);
drawMenuTempSet(false, menuCursorValueSelected);
drawMenuTempRange(false, menuCursorValueSelected);
drawMenuUnitMode(false);
break;
case 1:
drawMenuCoolMode(false);
drawMenuHeatMode(true);
drawMenuTempSet(false, menuCursorValueSelected);
drawMenuTempRange(false, menuCursorValueSelected);
drawMenuUnitMode(false);
break;
case 2:
drawMenuCoolMode(false);
drawMenuHeatMode(false);
drawMenuTempSet(true, menuCursorValueSelected);
drawMenuTempRange(false, menuCursorValueSelected);
drawMenuUnitMode(false);
break;
case 3:
drawMenuCoolMode(false);
drawMenuHeatMode(false);
drawMenuTempSet(false, menuCursorValueSelected);
drawMenuTempRange(true, menuCursorValueSelected);
drawMenuUnitMode(false);
break;
case 4:
drawMenuCoolMode(false);
drawMenuHeatMode(false);
drawMenuTempSet(false, menuCursorValueSelected);
drawMenuTempRange(false, menuCursorValueSelected);
drawMenuUnitMode(true);
break;
}
}
// click handler
void clickMenu() {
if (lastClk == LOW) return;
switch (menuCursorPosition) {
case 0:
isCoolOn = !isCoolOn;
break;
case 1:
isHeatOn = !isHeatOn;
break;
case 2:
case 3:
menuCursorValueSelected = !menuCursorValueSelected;
break;
case 4:
isCelsius = !isCelsius;
break;
}
}
// rotation handler
void rotateMenu(bool isClockwise) {
if (isClockwise) {
if (!menuCursorValueSelected) {
menuCursorPosition++;
}
if (menuCursorPosition > MENU_ITEMS) menuCursorPosition = 0;
rotValue++;
} else {
if (!menuCursorValueSelected) {
menuCursorPosition--;
}
if (menuCursorPosition < 0) menuCursorPosition = MENU_ITEMS;
rotValue--;
}
// fire off any rotation-based things
switch (menuCursorPosition) {
case 2:
if (menuCursorValueSelected) {
targetTemp += (isClockwise ? 0.5f : -0.5f);
if (targetTemp > 105) targetTemp = 105;
if (targetTemp < -10) targetTemp = -10;
}
break;
case 3:
if (menuCursorValueSelected) {
targetTempRange += (isClockwise ? 0.5f : -0.5f);
if (targetTempRange > 15) targetTempRange = 15;
if (targetTempRange < 1) targetTempRange = 1;
}
break;
}
}
void drawMenuCoolMode(bool isCurrent) {
if (isCurrent) {
display.setTextColor(BLACK, WHITE);
}
display.print("Cool mode: ");
display.setTextColor(WHITE);
if (isCoolOn) {
display.println("On");
} else {
display.println("Off");
}
}
void drawMenuHeatMode(bool isCurrent) {
if (isCurrent) {
display.setTextColor(BLACK, WHITE);
}
display.print("Heat mode: ");
display.setTextColor(WHITE);
if (isHeatOn) {
display.println("On");
} else {
display.println("Off");
}
}
void drawMenuTempSet(bool isCurrent, bool valueSelected) {
if (isCurrent && !valueSelected) {
display.setTextColor(BLACK, WHITE);
}
display.print("Temp. set: ");
if (isCurrent && valueSelected) {
display.setTextColor(BLACK, WHITE);
} else {
display.setTextColor(WHITE);
}
float outputTargetTemp = isCelsius ? targetTemp : (targetTemp * 9 / 5) + 32;
display.println(outputTargetTemp);
display.setTextColor(WHITE);
}
void drawMenuTempRange(bool isCurrent, bool valueSelected) {
if (isCurrent && !valueSelected) {
display.setTextColor(BLACK, WHITE);
}
display.print("Temp. range: ");
display.print(char(240));
if (isCurrent && valueSelected) {
display.setTextColor(BLACK, WHITE);
} else {
display.setTextColor(WHITE);
}
display.println(targetTempRange);
display.setTextColor(WHITE);
}
void drawMenuUnitMode(bool isCurrent) {
if (isCurrent) {
display.setTextColor(BLACK, WHITE);
}
display.print("Temp. units: ");
display.setTextColor(WHITE);
if (isCelsius) {
display.println("C");
} else {
display.println("F");
}
}
// display.print((char)247);
// display.print("C");
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* ntpServer = "pool.ntp.org";
int16_t counter = 0;
String climateDisplayString = "";
void drawProgress(int x, int y, int width, int height, float val) {
height = max(2, height);
width = max(10, width);
display.drawRoundRect(x, y, width, height, 2, WHITE);
display.fillRect(x, y+1, map(val, 0, 100, 0, width), height-2, WHITE);
}
String climateString(float humidity, float temperature) {
if (isnan(humidity) || isnan(temperature)) {
return "--DHT22 Init--";
}
String output = String("");
output.concat(dtostrf(outputTemp, 5, 1, tempBuffer));
output.concat((char)247);
if (isCelsius) {
output += "C, ";
} else {
output += "F, ";
}
output.concat(dtostrf(humidity, 4, 1, humBuffer));
output += "%";
return output;
}
void displayString(int16_t x, int16_t y, String str) {
display.setCursor(x,y);
display.println(str);
}
void setHeatMode(bool isOn) {
if (isOn && digitalRead(PIN_RELAY_HOT) != HIGH) {
digitalWrite(PIN_RELAY_HOT, HIGH);
}
if (!isOn && digitalRead(PIN_RELAY_HOT) != LOW) {
digitalWrite(PIN_RELAY_HOT, LOW);
}
}
void setCoolMode(bool isOn) {
if (isOn && digitalRead(PIN_RELAY_COLD) != HIGH) {
digitalWrite(PIN_RELAY_COLD, HIGH);
}
if (!isOn && digitalRead(PIN_RELAY_COLD) != LOW) {
digitalWrite(PIN_RELAY_COLD, LOW);
}
}
void climateCycle() {
hum = dht.readHumidity();
temp = dht.readTemperature();
if (isCelsius) {
outputTemp = temp;
} else {
outputTemp = (temp * 9.0f / 5.0f) + 32.0f;
}
if (
!isnan(temp) &&
(
temp != lastTemp ||
isHeatOn != lastHeatStatus ||
isCoolOn != lastCoolStatus ||
targetTemp != lastTargetTemp
)
) {
lastTemp = temp;
float tempDiff = temp - targetTemp;
if (isHeatOn) {
if (temp - targetTemp < 0) {
if (DEBUG_ON) Serial.println("Heat on.");
setHeatMode(true);
} else {
Serial.println("Heat off.");
setHeatMode(false);
}
} else {
if (isHeatOn != lastHeatStatus) {
setHeatMode(false);
Serial.println("Heat off.");
}
}
if (isCoolOn) {
if (temp + targetTemp < 0) {
Serial.println("Cool on.");
setCoolMode(true);
} else {
Serial.println("Cool off.");
setCoolMode(false);
}
} else {
if (isCoolOn != lastCoolStatus) {
setCoolMode(false);
Serial.println("Cool off.");
}
}
}
lastHeatStatus = isHeatOn;
lastCoolStatus = isCoolOn;
lastTemp = temp;
lastTargetTemp = targetTemp;
}
void connectWifi() {
Serial.printf("Connecting to %s..", ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("WiFi Connected.");
}
void disconnectWifi() {
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
Serial.println("WiFi Disconnected.");
}
void setupDateTime() {
// DateTime.setTimeZone("CST-8");
// DateTime.setServer("asia.pool.ntp.org");
// DateTime.begin(15 * 1000);
Serial.print("Setting up date/time...");
DateTime.setTimeZone("MST-7");
DateTime.setServer(ntpServer);
DateTime.begin(5 * 1000);
if (!DateTime.isTimeValid()) {
Serial.println("Failed to get time from server.");
return;
}
Serial.println("Done.");
}
void printLocalTime() {
struct tm timeinfo;
if(!getLocalTime(&timeinfo)) {
display.println(" NTP error");
return;
}
display.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Serial.println("Starting boot.");
if (DEBUG_ON) Serial.println("DEBUG_ON.");
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed!"));
for(;;); // stop here
}
// relay pin
pinMode(PIN_RELAY_HOT, OUTPUT); // set the pin as output
// rotary encoder
pinMode(PIN_ENCODER_CLK, INPUT);
pinMode(PIN_ENCODER_DT, INPUT);
pinMode(PIN_ENCODER_SW, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_CLK), readEncoder, FALLING);
Serial.println("Boot complete.");
connectWifi();
setupDateTime();
disconnectWifi();
}
// interrupt callback
void readEncoder() {
int dtValue = digitalRead(PIN_ENCODER_DT);
if (dtValue == HIGH) {
rotateMenu(false);
}
if (dtValue == LOW) {
rotateMenu(true);
}
rotValue = constrain(rotValue, 0, 100);
}
void loop() {
climateCycle();
climateDisplayString = climateString(hum, outputTemp);
display.clearDisplay();
display.setTextSize(1); // Normal 1:1 pixel scale
display.setTextColor(WHITE); // Draw white text
displayString(10, 0, climateDisplayString);
printLocalTime();
//drawProgress(0, 50, SCREEN_WIDTH - 1, 10, rotValue);
drawMenu();
if (digitalRead(PIN_ENCODER_SW) == LOW) {
clickMenu();
}
lastClk = digitalRead(PIN_ENCODER_SW);
// display the things
display.display();
}