/* SmartSwitch application Based on ESP_AsyncFSBrowser Temperature Control for heater with schedule Main purpose - for winter outside car block heater or battery charger Wide browser compatibility, no other server-side needed. HTTP server and WebSocket, single port Standalone, no JS dependencies for the browser from Internet (I hope) Based on ESP_AsyncFSBrowser Real Time (NTP) w/ Time Zones Memorized settings to EEPROM Multiple clients can be connected at same time, they see each other requests Use latest ESP core lib (from Github) */ // Defaulut is SPIFFS, FatFS: only on ESP32 // Comment 2 lines below or uncomment only one of them #define USE_LittleFS //#define USE_FatFS // select partition scheme w/ ffat! #define USE_WFM // to use ESPAsyncWiFiManager //#define DEL_WFM // delete Wifi credentials stored //(use once then comment and flash again), also HTTP /erase-wifi can do the same live // AUTH COOKIE uses only the password and unsigned long MY_SECRET_NUMBER #define http_username "smart" #define http_password "switch" #define MY_SECRET_NUMBER 0xA217B02F //See https://github.com/me-no-dev/ESPAsyncWebServer/pull/684 //SSWI or other 4 chars #define USE_AUTH_COOKIE #define MY_COOKIE_DEL "SSWI=;Max-Age=-1;Path=/;" #define MY_COOKIE_PREF "SSWI=" #define MY_COOKIE_SUFF ";Max-Age=31536000;Path=/;" #ifndef USE_AUTH_COOKIE #define USE_AUTH_STAT //Base Auth for stat, /commands and SPIFFSEditor //#define USE_AUTH_WS //Base Auth also for WS, not very supported #endif #include #ifdef ESP32 #include #ifdef USE_LittleFS #define HSTNM "ssw32-littlefs" #define MYFS LITTLEFS #include "LITTLEFS.h" #elif defined(USE_FatFS) #define HSTNM "ssw32-ffat" #define MYFS FFat #include "FFat.h" #else #define MYFS SPIFFS #include #define HSTNM "ssw32-spiffs" #endif #include #include #include #elif defined(ESP8266) #ifdef USE_LittleFS #include #define HSTNM "ssw8266-littlefs" #define MYFS LittleFS #include #elif defined(USE_FatFS) #error "FatFS only on ESP32 for now!" #else #define HSTNM "ssw8266-spiffs" #define MYFS SPIFFS #endif #include #include #include #endif #include #ifdef USE_WFM #include "ESPAsyncWiFiManager.h" #endif #include #include #include #include #ifdef USE_AUTH_COOKIE #include #include "Xtea.h" #endif #define RTC_UTC_TEST 1577836800 // Some Date #define MYTZ PSTR("EST5EDT,M3.2.0,M11.1.0") #define EESC 100 // fixed eeprom address for sched choice #define EECH 104 // fixed eeprom address to keep selected active channel, only for reference here #define EEBEGIN EECH + 1 #define EEMARK 0x5A #define MEMMAX 2 // 0,1,2... last max index (only 3 channels) #define EEALL 512 #define HYST 0.5 // C +/- hysteresis // DHT #define DHTTYPE DHT22 // DHT 11 // DHT 22, AM2302, AM2321 // DHT 21, AM2301 #define DHTPIN 4 //D2 #define DHT_T_CORR -0.3 //Temperature offset compensation of the sensor (can be -) #define DHT_H_CORR -2.2 //Humidity offset compensation of the sensor // SKETCH BEGIN MAIN DECLARATIONS DHT dht(DHTPIN, DHTTYPE); Ticker tim; AsyncWebServer server(80); //single port - easy for forwarding AsyncWebSocket ws("/ws"); #ifdef USE_WFM #ifdef USE_EADNS AsyncDNSServer dns; #else DNSServer dns; #endif //Fallback timeout in seconds allowed to config or it creates an own AP, then serves 192.168.4.1 #define FBTO 120 const char* fbssid = "FBSSW"; const char* fbpassword = "FBpassword4"; #else const char* ssid = "MYROUTERSSD"; const char* password = "MYROUTERPASSWD"; #endif const char* hostName = HSTNM; // RTC static timeval tv; static time_t now; // HW I/O const int btnPin = 0; //D3 const int ledPin = 2; //D4 #ifdef ESP32 #define LED_ON 0x1 #define LED_OFF 0x0 #elif defined(ESP8266) #define LED_ON 0x0 #define LED_OFF 0x1 #endif int btnState = HIGH; // Globals uint8_t count = 0; uint8_t sched = 0; // automatic schedule byte memch = 0; // select memory "channel" to work with float t = 0; float h = 0; bool udht = false; bool heat_enabled_prev = false; int ledState = LED_OFF; struct EE_bl { byte memid; //here goes the EEMARK stamp uint8_t hstart; uint8_t mstart; uint8_t hstop; uint8_t mstop; float tempe; }; EE_bl ee = {0,0,0,0,0,0.1}; //populate as initial // SUBS void writeEE() { ee.memid = EEMARK; //EEPROM.put(EESC, sched); // only separately when needed with commit() //EEPROM.put(EECH, memch); // not need to store and retrieve memch EEPROM.put(EEBEGIN + memch*sizeof(ee), ee); EEPROM.commit(); //needed for ESP8266? } void readEE() { byte ChkEE; if (memch > MEMMAX) memch = 0; EEPROM.get(EEBEGIN + memch*sizeof(ee), ChkEE); if (ChkEE == EEMARK){ //otherwise stays with defaults EEPROM.get(EEBEGIN + memch*sizeof(ee), ee); EEPROM.get(EESC, sched); if (sched > MEMMAX + 1) sched = 0; } } void showTime() { byte tmpch = 0; bool heat_enabled = false; gettimeofday(&tv, nullptr); now = time(nullptr); const tm* tm = localtime(&now); ws.printfAll("Now,Clock,%02d:%02d,%d", tm->tm_hour, tm->tm_min, tm->tm_wday); if ((2==tm->tm_hour )&&(2==tm->tm_min)) { configTzTime(MYTZ, "pool.ntp.org"); Serial.print(F("Sync Clock at 02:02\n")); } Serial.printf("RTC: %02d:%02d\n", tm->tm_hour, tm->tm_min); if (sched == 0) { // automatic if ((tm->tm_wday > 0)&&(tm->tm_wday < 6)) tmpch = 0; //Mon - Fri else if (tm->tm_wday == 6) tmpch = 1; //Sat else if (tm->tm_wday == 0) tmpch = 2; //Sun } else { // manual tmpch = sched - 1; //and stays } if (tmpch != memch) { // update if different memch = tmpch; readEE(); ws.printfAll("Now,Setting,%02d:%02d,%02d:%02d,%+2.1f", ee.hstart, ee.mstart, ee.hstop, ee.mstop, ee.tempe); } // process smart switch by time and temperature uint16_t xmi = (uint16_t)(60*tm->tm_hour) + tm->tm_min; // max 24h = 1440min, current time uint16_t bmi = (uint16_t)(60*ee.hstart) + ee.mstart; // begin in minutes uint16_t emi = (uint16_t)(60*ee.hstop) + ee.mstop; // end in minutes if (bmi == emi) heat_enabled = false; else { //enable smart if different if (((bmi < emi)&&(bmi <= xmi)&&(xmi < emi))|| ((emi < bmi)&&((bmi <= xmi)||(xmi < emi)))) { heat_enabled = true; } else heat_enabled = false; } if (heat_enabled_prev) { // smart control (delayed one cycle) if (((t - HYST) < ee.tempe)&&(ledState == LED_OFF)) { // OFF->ON once ledState = LED_ON; digitalWrite(ledPin, ledState); // apply change ws.textAll("led,ledon"); } if ((((t + HYST) > ee.tempe)&&(ledState == LED_ON))||(!heat_enabled)) { // ON->OFF once, also turn off at end of period. ledState = LED_OFF; digitalWrite(ledPin, ledState); // apply change ws.textAll("led,ledoff"); } Serial.printf(ledState == LED_ON ? "LED ON" : "LED OFF"); Serial.print(F(", Smart enabled\n")); } heat_enabled_prev = heat_enabled; //update } void updateDHT(){ float h1 = dht.readHumidity(); float t1 = dht.readTemperature(); //Celsius or dht.readTemperature(true) for Fahrenheit if (isnan(h1) || isnan(t1)) { Serial.println(F("Failed to read from DHT sensor!")); } else { h = h1 + DHT_H_CORR; t = t1 + DHT_T_CORR; } } void analogSample() { ws.printfAll("wpMeter,Arduino,%+2.1f,%2.1f,%d", t, h, heat_enabled_prev); Serial.printf("T/H.: %+2.1f°C/%2.1f%%,%d\n", t, h, heat_enabled_prev); } void checkPhysicalButton() { if (digitalRead(btnPin) == LOW) { if (btnState != LOW) { // btnState is used to avoid sequential toggles ledState = !ledState; digitalWrite(ledPin, ledState); if (ledState == LED_OFF) { ws.textAll("led,ledoff"); Serial.println(F("LED-OFF")); } else { ws.textAll("led,ledon"); Serial.println(F("LED-ON")); } } btnState = LOW; } else { btnState = HIGH; } } void mytimer() { ++count; //200ms increments checkPhysicalButton(); if ((count % 25) == 1) { // update temp every 5 seconds analogSample(); udht = true; } if ((count % 50) == 0) { // update temp every 10 seconds ws.cleanupClients(); } if (count >= 150) { // cycle every 30 sec showTime(); count = 0; } } #ifdef USE_AUTH_COOKIE unsigned long key[4] = {0x01F20304,0x05060708,0x090a0b0c,0x0d0e0f00}; Xtea x(key); void encip(String &mtk, unsigned long token){ unsigned long res[2] = {(unsigned long)random(0xFFFFFFFF),token}; x.encrypt(res); char buf1[18]; sprintf(buf1, "%08X_%08X",res[0],res[1]); //8 bytes for encryping the IP cookie mtk = (String)buf1; } unsigned long decip(const char *pch){ unsigned long res[2] = {0,0}; res[0] = strtoul(pch, NULL, 16); res[1] = strtoul(&pch[9], NULL, 16); x.decrypt(res); return res[1]; } bool myHandshake(AsyncWebServerRequest *request){ // false will 401 bool rslt = false; if (request->hasHeader("Cookie")){ String cookie = request->header("Cookie"); Serial.println(cookie); uint8_t pos = cookie.indexOf(MY_COOKIE_PREF); if (pos != -1){ unsigned long ix = decip(cookie.substring(pos+5, pos+22).c_str()); Serial.printf("Ask:%08X Got:%08X\n", MY_SECRET_NUMBER, ix); if (MY_SECRET_NUMBER == ix) rslt=true; } else rslt=false; } else rslt=false; Serial.printf(rslt ? "C-YES\n" : "C-NO\n"); return rslt; } #endif // server void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){ if(type == WS_EVT_CONNECT){ Serial.printf("ws[%s][%u] connect\n", server->url(), client->id()); //client->printf("Hello Client %u :)", client->id()); //client->ping(); IPAddress ip = client->remoteIP(); Serial.printf("[%u] Connected from %d.%d.%d.%d\n", client->id(), ip[0], ip[1], ip[2], ip[3]); showTime(); analogSample(); if (ledState == LED_OFF) ws.textAll("led,ledoff"); else ws.textAll("led,ledon"); ws.printfAll("Now,Setting,%02d:%02d,%02d:%02d,%+2.1f", ee.hstart, ee.mstart, ee.hstop, ee.mstop, ee.tempe); ws.printfAll("Now,sched,%d", sched); } else if(type == WS_EVT_DISCONNECT){ Serial.printf("ws[%s][%u] disconnect\n", server->url(), client->id()); ws.textAll("Now,remoff"); } else if(type == WS_EVT_ERROR){ Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data); } else if(type == WS_EVT_PONG){ Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len)?(char*)data:""); } else if(type == WS_EVT_DATA){ AwsFrameInfo * info = (AwsFrameInfo*)arg; String msg = ""; if(info->final && info->index == 0 && info->len == len){ //the whole message is in a single frame and we got all of it's data Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len); if(info->opcode == WS_TEXT){ for(size_t i=0; i < info->len; i++) { //debug msg += (char) data[i]; } if(data[0] == 'L') { // LED if(data[1] == '1') { ledState = LED_ON; ws.textAll("led,ledon"); // for others } else if(data[1] == '0') { ledState = LED_OFF; ws.textAll("led,ledoff"); } digitalWrite(ledPin, ledState); // apply change } else if(data[0] == 'T') { // timeset if (len > 11) { data[3] = data[6] = data[9] = data[12] = 0; // cut strings ee.hstart = (uint8_t) atoi((const char *) &data[1]); ee.mstart = (uint8_t) atoi((const char *) &data[4]); ee.hstop = (uint8_t) atoi((const char *) &data[7]); ee.mstop = (uint8_t) atoi((const char *) &data[10]); Serial.printf("[%u] Timer set %02d:%02d - %02d:%02d\n", client->id(), ee.hstart, ee.mstart, ee.hstop, ee.mstop); writeEE(); memch = 255; // to force showTime()to send Setting showTime(); } } else if(data[0] == 'W') { // temperatureset if (len > 3) { if (ee.tempe != (float) atof((const char *) &data[1])){ ee.tempe = (float) atof((const char *) &data[1]); Serial.printf("[%u] Temp set %+2.1f\n", client->id(), ee.tempe); writeEE(); memch = 255; // to force showTime()to send Setting showTime(); } } } else if ((data[0] == 'Z')&&(len > 2)) { // sched data[2] = 0; if (sched != (uint8_t) atoi((const char *) &data[1])){ sched = (uint8_t) atoi((const char *) &data[1]); EEPROM.put(EESC, sched); //separately EEPROM.commit(); //needed for ESP8266? ws.printfAll("Now,sched,%d", sched); showTime(); } } } else { char buff[3]; for(size_t i=0; i < info->len; i++) { sprintf(buff, "%02x ", (uint8_t) data[i]); msg += buff ; } } Serial.printf("%s\n",msg.c_str()); if(info->opcode == WS_TEXT) client->text("I got your text message"); else client->binary("I got your binary message"); } else { //message is comprised of multiple frames or the frame is split into multiple packets if(info->index == 0){ if(info->num == 0) Serial.printf("ws[%s][%u] %s-message start\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); Serial.printf("ws[%s][%u] frame[%u] start[%llu]\n", server->url(), client->id(), info->num, info->len); } Serial.printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT)?"text":"binary", info->index, info->index + len); if(info->opcode == WS_TEXT){ for(size_t i=0; i < len; i++) { msg += (char) data[i]; } } else { char buff[3]; for(size_t i=0; i < len; i++) { sprintf(buff, "%02x ", (uint8_t) data[i]); msg += buff ; } } Serial.printf("%s\n",msg.c_str()); if((info->index + len) == info->len){ Serial.printf("ws[%s][%u] frame[%u] end[%llu]\n", server->url(), client->id(), info->num, info->len); if(info->final){ Serial.printf("ws[%s][%u] %s-message end\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); if(info->message_opcode == WS_TEXT) client->text("I got your text message"); else client->binary("I got your binary message"); } } } } } // setup ----------------------------------- void setup(){ Serial.begin(115200); Serial.setDebugOutput(true); //Wifi #ifdef USE_WFM AsyncWiFiManager wifiManager(&server,&dns); #ifdef DEL_WFM wifiManager.resetSettings(); #endif wifiManager.setTimeout(FBTO); // seconds to config or it creates an own AP, then browse 192.168.4.1 if (!wifiManager.autoConnect(hostName)){ Serial.print(F("*FALLBACK AP*\n")); WiFi.mode(WIFI_AP); WiFi.softAP(fbssid, fbpassword); // MDNS.begin(fbssid); // MDNS.addService("http","tcp",80); // Core SVN 5179 use STA as default interface in mDNS (#7042) } #else // Manual simple STA mode to connect to known router //WiFi.mode(WIFI_AP_STA); // Core SVN 5179 use STA as default interface in mDNS (#7042) //WiFi.softAP(hostName); // Core SVN 5179 use STA as default interface in mDNS (#7042) WiFi.mode(WIFI_STA); // Core SVN 5179 use STA as default interface in mDNS (#7042) WiFi.begin(ssid, password); if (WiFi.waitForConnectResult() != WL_CONNECTED) { Serial.print(F("STA: Failed!\n")); WiFi.disconnect(false); delay(1000); WiFi.begin(ssid, password); } #endif Serial.print(F("*CONNECTED* OWN IP:")); Serial.println(WiFi.localIP()); //DHT dht.begin(); updateDHT(); //first reading takes time, hold here than the loop; //Real Time time_t rtc = RTC_UTC_TEST; timeval tv = { rtc, 0 }; //timezone tz = { 0, 0 }; //(insert) <#5194 settimeofday(&tv, nullptr); //settimeofday(&tv, &tz); // <#5194 configTzTime(MYTZ, "pool.ntp.org"); //MDNS (not needed) // MDNS.begin(hostName); // MDNS.addService("http","tcp",80); // Core SVN 5179 use STA as default interface in mDNS (#7042) //I/O & DHT pinMode(ledPin, OUTPUT); pinMode(btnPin, INPUT_PULLUP); //EE EEPROM.begin(EEALL); //EEPROM.get(EECH, memch); //current channel, no need readEE(); // populate structure if healthy digitalWrite(ledPin, ledState); Serial.printf("Timer set %02d:%02d - %02d:%02d\n", ee.hstart, ee.mstart, ee.hstop, ee.mstop); Serial.printf("Temp set %+2.1f\n", ee.tempe); //FS #ifdef USE_FatFS if (MYFS.begin(false,"/ffat",3)) { //limit the RAM usage, bottom line 8kb + 4kb takes per each file, default is 10 #else if (MYFS.begin()) { #endif Serial.print(F("FS mounted\n")); } else { Serial.print(F("FS mount failed\n")); } #ifdef USE_AUTH_WS ws.setAuthentication(http_username,http_password); #endif #ifdef USE_AUTH_COOKIE ws.handleHandshake(myHandshake); #endif ws.onEvent(onWsEvent); server.addHandler(&ws); #ifdef ESP32 #ifdef USE_AUTH_STAT server.addHandler(new SPIFFSEditor(MYFS, http_username,http_password)); #elif defined(USE_AUTH_COOKIE) server.addHandler(new SPIFFSEditor(MYFS)).setFilter(myHandshake); #else server.addHandler(new SPIFFSEditor(MYFS)); #endif #elif defined(ESP8266) #ifdef USE_AUTH_STAT server.addHandler(new SPIFFSEditor(http_username,http_password,MYFS)); #elif defined(USE_AUTH_COOKIE) server.addHandler(new SPIFFSEditor("","",MYFS)).setFilter(myHandshake); #else server.addHandler(new SPIFFSEditor("","",MYFS)); #endif #endif #ifdef USE_AUTH_COOKIE server.on("/lg2n", HTTP_POST, [](AsyncWebServerRequest *request){ String ckx; encip(ckx, MY_SECRET_NUMBER); AsyncWebServerResponse *response; if(request->hasParam("lg0f",true)){ response = request->beginResponse(200, "text/html;charset=utf-8", "

Logged Out! Back

"); response->addHeader("Cache-Control", "no-cache"); response->addHeader("Set-Cookie", MY_COOKIE_DEL); } else if(request->hasParam("pa2w",true) && (String(request->getParam("pa2w",true)->value().c_str()) == String(http_password))){ response = request->beginResponse(301); response->addHeader("Location", "/"); response->addHeader("Cache-Control", "no-cache"); response->addHeader("Set-Cookie", MY_COOKIE_PREF + ckx + MY_COOKIE_SUFF); } else response = request->beginResponse(200, "text/html;charset=utf-8", "

Wrong password! Back

"); request->send(response); }); #endif // below paths need individual auth //////////////////////////////////////////////// server.on("/free-ram", HTTP_GET, [](AsyncWebServerRequest *request){ // direct request->answer #ifdef USE_AUTH_STAT if(!request->authenticate(http_username, http_password)) return request->requestAuthentication(); #endif #ifdef ESP32 request->send(200, "text/plain", String(ESP.getMinFreeHeap()) + ':' + String(ESP.getFreeHeap()) + ':'+ String(ESP.getHeapSize())); #else request->send(200, "text/plain", String(ESP.getFreeHeap())); #endif #ifdef USE_AUTH_COOKIE }).setFilter(myHandshake); #else }); #endif server.on("/get-time", HTTP_GET, [](AsyncWebServerRequest *request){ #ifdef USE_AUTH_STAT if(!request->authenticate(http_username, http_password)) return request->requestAuthentication(); #endif if(request->hasParam("btime")){ time_t rtc = (request->getParam("btime")->value()).toInt(); timeval tv = { rtc, 0 }; settimeofday(&tv, nullptr); } request->send(200, "text/plain","Got browser time ..."); #ifdef USE_AUTH_COOKIE }).setFilter(myHandshake); #else }); #endif server.on("/hw-reset", HTTP_GET, [](AsyncWebServerRequest *request){ #ifdef USE_AUTH_STAT if(!request->authenticate(http_username, http_password)) return request->requestAuthentication(); #endif request->onDisconnect([]() { #ifdef ESP32 ESP.restart(); #elif defined(ESP8266) ESP.reset(); #endif }); request->send(200, "text/plain","Restarting ..."); #ifdef USE_AUTH_COOKIE }).setFilter(myHandshake); #else }); #endif server.on("/erase-wifi", HTTP_GET, [](AsyncWebServerRequest *request){ #ifdef USE_AUTH_STAT if(!request->authenticate(http_username, http_password)) return request->requestAuthentication(); #endif request->onDisconnect([]() { WiFi.disconnect(true); #ifdef ESP32 ESP.restart(); #elif defined(ESP8266) ESP.reset(); #endif }); request->send(200, "text/plain","Erasing WiFi data ..."); #ifdef USE_AUTH_COOKIE }).setFilter(myHandshake); #else }); #endif // above paths need individual auth //////////////////////////////////////////////// #ifdef USE_AUTH_COOKIE server.serveStatic("/", MYFS, "/").setDefaultFile("index.htm").setFilter(myHandshake); server.serveStatic("/", MYFS, "/login/").setDefaultFile("index.htm"); #else #ifdef USE_AUTH_STAT server.serveStatic("/", MYFS, "/").setDefaultFile("index.htm").setAuthentication(http_username,http_password); #else server.serveStatic("/", MYFS, "/").setDefaultFile("index.htm"); #endif #endif server.onNotFound([](AsyncWebServerRequest *request){ // nothing known Serial.print(F("NOT_FOUND: ")); if (request->method() == HTTP_OPTIONS) request->send(200); //CORS else request->send(404); }); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");//CORS server.begin(); //Timer tick tim.attach(0.2, mytimer); //every 0.2s //OTA ArduinoOTA.setHostname(hostName); ArduinoOTA.onStart([]() { Serial.print(F("OTA Started ...\n")); MYFS.end(); // Clean FS ws.textAll("Now,OTA"); // for all clients ws.enable(false); ws.closeAll(); }); ArduinoOTA.begin(); } // setup end // loop ----------------------------------- void loop(){ if (udht){ // 5sec updateDHT(); udht = false; } ArduinoOTA.handle(); }