/* etrofit Fan with timer V1.3 added 5min time out for config so fan does not stay on forever V1.2 5sec off 5sec on when powering up to detect resets, if any http://www.forward.com.au/pfod/HomeAutomation/FanTimer/index.html (c)2016 Forward Computing and Control Pty. Ltd. NSW Australia, www.forward.com.au This generated code may be freely used for both private and commerical use Circuit uses about 30mA while idle i.e. 0.15W or about 1.3KWhrs per year (less the $0.50 at our current electricity price) */ #include #include #include //Local DNS Server used for redirecting all requests to the configuration portal #include #include #include void fan_init(); void processSwitchOffTimer(); void processFanSwitchOffTimer(); void processFanTimeout(); void processSwitch(); void processSwitchChange(); void processSwitchState(); void stopWiFiAndSleep(); void writeStorage(struct EEPROM_storage * storagePtr); void readStorage(struct EEPROM_storage * storagePtr); void terminateStorage(struct EEPROM_storage* storagePtr); void printStorage(struct EEPROM_storage* storagePtr); void checkStorageInitialized(); void initializeStorage(); void startWebServer(); void handleRoot(); void handleConfig(); void handleNotFound(); //#define DEBUG //#define TIMEOUT_OVERRIDE // =============================== extra library and defininitions for push button =============================== // add DeboucedSwitch library #include // Download library from http://www.forward.com.au/pfod/ArduinoProgramming/DebouncedSwitch/DebouncedSwitch.html const int gndPin = 2; // supplies gnd for Opto pin 5 which will gnd GPIO0 opto fires // define push button const int pushbutton_pin = 0; // GPIO0 set as input ALSO grounded to program, so no faults is programming pin left place after programmin // define DeboucedSwitch DebouncedSwitch sw(pushbutton_pin, true); // monitor a switch on input pushbutton_pin, second ARG true -> expecing half wave AC input via opto-isolator // ===================================================================================================== // give the board pins names, if you change the pin number here you will change the pin controlled int fan = 0; // name the variable for 'Extension Power is ' const int fan_pin = 3; // name the output pin for 'Relay ' unsigned long fanTimerStart = 0; unsigned long FAN_TIMEOUT = 0; // set from eeprom bool fanTimerRunning = false; // if true fan is turned ON unsigned long fanSwitchOffTimerStart = 0; unsigned const long FAN_SWITCH_OFF_TIME = 5000; // if switch on and off within this time just turn fan off 5sec bool fanSwitchOffTimerRunning = false; // if switch turn on and not turned off yet or not timed out unsigned long switchOffTimerStart = 0; unsigned const long SWITCH_OFF_TIME = 5000; // if switch off start timer and after 5 sec clear configSwitchCounter to 0 bool switchOffTimerRunning = false; // if switch turned off and not 5 sec yet int configSwitchCounter = 0; // when this gets to 3 start accesspoint for 10mins // reset to 0 if fanSwitchOffTimer timesout // once reach 3, start fan, start Accesspoint. and accesspoint timer (10mins) and reset configSwitchCounter to 0 // if in accesspoint mode and switch switched on again then cancel Accesspoint. // when process /config page turn off accesspoint and turn fan off. // when accesspoint turned off, reset configSwitchCounter to 0 // no access point password as nothing important to protect #define fanTimerConfigAP "FanTimerConfig" // 10.1.1.1 will be added later const char ROOT_IP[] = " 10.1.1.1"; // includes leading space const char STORAGE_WRITTEN_FLAG[] = "ABC"; const size_t STORAGE_WRITTEN_FLAG_LEN = strlen(STORAGE_WRITTEN_FLAG); const byte DNS_PORT = 53; IPAddress apIP(pfodESP8266Utils::ipStrToNum(ROOT_IP)); DNSServer dnsServer; ESP8266WebServer webserver(80); const uint8_t webConfigEEPROMStartAddress = 20; bool webserverStarted = false; // if true process web server unsigned long webserverSwitchOffTimerStart = 0; unsigned const long WEB_SERVER_SWITCH_OFF_TIME = 5 * 60 * 1000; // switch web server off in 5min if nothing else happens // set the EEPROM structure struct EEPROM_storage { uint32_t timeout; // fan timeout char writtenFlag[STORAGE_WRITTEN_FLAG_LEN + 1]; // set to ABC when storage first written char ssid[pfodESP8266Utils::MAX_SSID_LEN + 1]; // WIFI ssid for config Access Point defaults to FanTimerConfig + null } storage; const int EEPROM_storageSize = sizeof(EEPROM_storage); void setup() { #ifdef DEBUG //Serial.begin(115200, SERIAL_8N1, SERIAL_TX_ONLY); // does not work with Arduino ESP board library V2.2.0 needs V2.3.0+ // BUT V2.3.0 has WiFi connection problems so.. Serial.begin(115200); // works with Arduino ESP board library V2.2.0 Serial.println(); #endif pinMode(fan_pin, OUTPUT); // output for fan relay overrides RX setting above fan = 0; // turn fan off for 5 sec digitalWrite(fan_pin, fan); // set output EEPROM.begin(512); // reserve 512 WiFi.setSleepMode(WIFI_MODEM_SLEEP); WiFi.mode(WIFI_OFF); checkStorageInitialized(); // initialize EEPROM to defaults if needed, default fan timeout 0 (1sec) fan_init(); // set globals fan fan_initially off and calls stopWiFiAndSleep(); pinMode(gndPin, OUTPUT); digitalWrite(gndPin, LOW); // make gndPin low after ESP8266 has fan_initialized for (int i = 5; i > 0; i--) { // wait 5sec with fan on at start up to indicate sucessful reset on powerup delay(1000); } fan = 1; // turn fan on on start up digitalWrite(fan_pin, fan); // set output for (int i = 10; i > 0; i--) { // wait 5sec with fan on at start up to indicate sucessful reset on powerup #ifdef DEBUG Serial.print(i); Serial.print(' '); #endif delay(500); } #ifdef DEBUG Serial.println(); #endif fan = 0; // turn fan off after 10sec digitalWrite(fan_pin, fan); // set output #if defined(DEBUG) && defined(TIMEOUT_OVERRIDE) FAN_TIMEOUT = 10000; Serial.print("Overriding timeout to "); Serial.print(FAN_TIMEOUT / 1000); Serial.println(" sec for testing"); #endif } void loop() { sw.update(); // call this every loop to update switch state processSwitch(); // this may increment configSwitchCounter if (configSwitchCounter >= 3) { configSwitchCounter = 0; // clear it //turn fan on fan = 1; digitalWrite(fan_pin, fan); // set fan output startWebServer(); // sets webserverStarted } if (webserverStarted) { configSwitchCounter = 0; // clear it if already running dnsServer.processNextRequest(); webserver.handleClient(); if ((millis() - webserverSwitchOffTimerStart) > WEB_SERVER_SWITCH_OFF_TIME) { // cancel access point because no config within 5mins fan_init(); } return; } //else normal processing stopWiFiAndSleep(); processSwitchOffTimer(); processFanSwitchOffTimer(); // do this before processFanTimeout processFanTimeout(); // this sets fan on or off } void fan_init() { #ifdef DEBUG Serial.println(F("fan_init called")); #endif fan = 0; // name the variable for 'Extension Power is ' fanTimerStart = 0; FAN_TIMEOUT = 0; // set from eeprom fanTimerRunning = false; // if true fan is turned ON fanSwitchOffTimerStart = 0; fanSwitchOffTimerRunning = false; // if switch turn on and not turned off yet or not timed out switchOffTimerStart = 0; switchOffTimerRunning = false; // if switch turned off and not 5 sec yet configSwitchCounter = 0; // when this gets to 3 start accesspoint for 10mins webserverStarted = false; webserverSwitchOffTimerStart = 0; dnsServer.stop(); webserver.stop(); stopWiFiAndSleep(); struct EEPROM_storage storageRead; readStorage(&storageRead); FAN_TIMEOUT = storageRead.timeout * 60 * 1000; if (FAN_TIMEOUT < 1000) { FAN_TIMEOUT = 1000; // min is 1sec } digitalWrite(fan_pin, fan); // set fan output } void processSwitchOffTimer() { if (!switchOffTimerRunning) { } else { // timer running if ((millis() - switchOffTimerStart) > SWITCH_OFF_TIME) { switchOffTimerRunning = false; #ifdef DEBUG Serial.println(F("Switch Off timer timed out clear counter.")); #endif configSwitchCounter = 0; // when this gets to 3 start accesspoint for 10mins } } } void processFanSwitchOffTimer() { if (!fanSwitchOffTimerRunning) { } else { // timer running if ((millis() - fanSwitchOffTimerStart) > FAN_SWITCH_OFF_TIME) { fanSwitchOffTimerRunning = false; #ifdef DEBUG Serial.println(F("Fan Switch Off timer timed out clear counter.")); #endif configSwitchCounter = 0; // when this gets to 3 start accesspoint for 10mins } } } void processFanTimeout() { if (!fanTimerRunning) { fan = 0; // off } else { // timer running fan = 1; if ((millis() - fanTimerStart) > FAN_TIMEOUT) { // turn fan off fanTimerRunning = false; fan = 0; #ifdef DEBUG Serial.println(F("Fan Timer Timed out ")); #endif } } digitalWrite(fan_pin, fan); // set fan output } // called each loop void processSwitch() { // do switch changed first because it looks at fanSwitchOffTimerRunning to increment configSwitchCounter if (sw.isChanged()) { // debounced switch changed state Up or Down // isChanged() is only true for one loop(), cleared when update() called again processSwitchChange(); } processSwitchState(); // handle current state of switch on or off } void processSwitchChange() { if (webserverStarted) { // cancel access point because switch turned on again fan_init(); } if (sw.isDown()) { switchOffTimerRunning = false; // stop timer // start fan turnoff timer fanSwitchOffTimerRunning = true; // if switch turn on and not turned off yet or not timed out fanSwitchOffTimerStart = millis(); } else { // switch chenged to off // start switch off timer if switch off for 5 sec clear configSwitchCounter switchOffTimerRunning = true; switchOffTimerStart = millis(); // if fanSwitchOffTimerRunning increment count if (fanSwitchOffTimerRunning) { configSwitchCounter++; // when this gets to 3 start accesspoint for 10mins } } } void processSwitchState() { if (sw.isDown()) { // if switch down turn on fan output and restart fan timeout #ifdef DEBUG if (sw.isChanged()) { // debounced switch changed state to Down Serial.print(F("Fan Timer Started ")); Serial.print(FAN_TIMEOUT); Serial.println(" mS"); } #endif fanTimerRunning = true; // << this variable turns fan on in processFanTimeout() fanTimerStart = millis(); // reset timer } else { // switch is up/off if (fanSwitchOffTimerRunning) { // turned switch on and off within FAN_SWITCH_OFF_TIME (5sec) #ifdef DEBUG Serial.println(F("Fan Switched off")); #endif fanSwitchOffTimerRunning = false; fanTimerRunning = false; // turns fan off fan = 0; // but do it here also digitalWrite(fan_pin, fan); // set fan output } // if switch off just leave fan timeout to run } } void stopWiFiAndSleep() { WiFi.disconnect(); WiFi.mode(WIFI_OFF); WiFi.forceSleepBegin(); delay(1); } void startWebServer() { if (webserverStarted) { return; } webserverSwitchOffTimerStart = millis(); // start off timer WiFi.forceSleepWake(); WiFi.mode(WIFI_AP); #ifdef DEBUG Serial.println(F("Setting up Access Point for fan timer config")); #endif struct EEPROM_storage storageRead; struct EEPROM_storage* storagePtr = &storageRead; readStorage(storagePtr); char ssid[pfodESP8266Utils::MAX_SSID_LEN + 1]; pfodESP8266Utils::strncpy_safe(ssid, storagePtr->ssid, pfodESP8266Utils::MAX_SSID_LEN - strlen(ROOT_IP)); pfodESP8266Utils::strncpy_safe(ssid + strlen(ssid), ROOT_IP, strlen(ROOT_IP)); #ifdef DEBUG Serial.print("Configuring access point..."); #endif WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0)); // if DNSServer is started with "*" for domain name, it will reply with // provided IP to all DNS request dnsServer.start(DNS_PORT, "*", apIP); WiFi.softAP(ssid); // no password #ifdef DEBUG Serial.println("done"); IPAddress myIP = WiFi.softAPIP(); Serial.print(F("AP IP address: ")); Serial.println(myIP); #endif delay(10); webserver.on("/", handleRoot); webserver.on ( "/config", handleConfig ); webserver.onNotFound ( handleNotFound ); webserver.begin(); #ifdef DEBUG Serial.println("HTTP webserver started"); #endif webserverStarted = true; } void checkStorageInitialized() { struct EEPROM_storage storageRead; struct EEPROM_storage* storagePtr = &storageRead; readStorage(storagePtr); #ifdef DEBUG Serial.println(); printStorage(storagePtr); #endif if (strncmp(storagePtr->writtenFlag, STORAGE_WRITTEN_FLAG, STORAGE_WRITTEN_FLAG_LEN) != 0) { initializeStorage(); } } void initializeStorage() { struct EEPROM_storage storage; struct EEPROM_storage *storagePtr = &storage; // storage not written write default values storagePtr->timeout = 0; pfodESP8266Utils::strncpy_safe(storagePtr->ssid, fanTimerConfigAP, pfodESP8266Utils::MAX_SSID_LEN - strlen(ROOT_IP)); pfodESP8266Utils::strncpy_safe(storagePtr->writtenFlag, STORAGE_WRITTEN_FLAG, STORAGE_WRITTEN_FLAG_LEN); #ifdef DEBUG Serial.println(); printStorage(storagePtr); #endif writeStorage(storagePtr); } void writeStorage(struct EEPROM_storage * storagePtr) { terminateStorage(storagePtr); uint8_t * byteStorage = (uint8_t *)storagePtr; for (size_t i = 0; i < EEPROM_storageSize; i++) { EEPROM.write(webConfigEEPROMStartAddress + i, byteStorage[i]); } delay(0); bool committed = EEPROM.commit(); #ifdef DEBUG Serial.print(F("EEPROM.commit() = ")); Serial.println(committed ? "true" : "false"); #endif } void readStorage(struct EEPROM_storage * storagePtr) { uint8_t * byteStorageRead = (uint8_t *)storagePtr; for (size_t i = 0; i < EEPROM_storageSize; i++) { byteStorageRead[i] = EEPROM.read(webConfigEEPROMStartAddress + i); } terminateStorage(storagePtr); } void terminateStorage(struct EEPROM_storage* storagePtr) { storagePtr->writtenFlag[STORAGE_WRITTEN_FLAG_LEN] = '\0'; storagePtr->ssid[pfodESP8266Utils::MAX_SSID_LEN - strlen(ROOT_IP)] = '\0'; } void printStorage(struct EEPROM_storage* storagePtr) { terminateStorage(storagePtr); #ifdef DEBUG Serial.print("timeout:"); Serial.println(storagePtr->timeout); Serial.print("storageWritten:"); Serial.println(storagePtr->writtenFlag); Serial.print("ssid:"); Serial.println(storagePtr->ssid); #endif } void handleRoot() { struct EEPROM_storage storageRead; readStorage(&storageRead); #ifdef DEBUG Serial.println("handleRoot:"); printStorage(&storageRead); #endif String msg; msg = "" "" "Fan Timer Setup" "" "" "" "" "

Fan Timer Setup

" "

Use this form to set the time the fan will run after the light is switched off." "

" "

" "
" ""; msg += "

You can also change the name of this WiFi hotspot, if you wish.

" "" "

" "
" "" "" "

" "
" "" ""; webserver.send ( 200, "text/html", msg ); } void handleConfig() { char tempStr[pfodESP8266Utils::MAX_SSID_LEN]; struct EEPROM_storage storageRead; readStorage(&storageRead); if (webserver.args() > 0) { #ifdef DEBUG String message = "Config results\n\n"; message += "URI: "; message += webserver.uri(); message += "\nMethod: "; message += ( webserver.method() == HTTP_GET ) ? "GET" : "POST"; message += "\nArguments: "; message += webserver.args(); message += "\n"; for ( uint8_t i = 0; i < webserver.args(); i++ ) { message += " " + webserver.argName ( i ) + ": " + webserver.arg ( i ) + "\n"; } Serial.println(message); Serial.println(); #endif uint8_t numOfArgs = webserver.args(); const char *strPtr; uint8_t i = 0; for (; (i < numOfArgs); i++ ) { // check field numbers if (webserver.argName(i)[0] == '2') { pfodESP8266Utils::strncpy_safe(tempStr, (webserver.arg(i)).c_str(), pfodESP8266Utils::MAX_SSID_LEN); pfodESP8266Utils::urldecode2(tempStr, tempStr); // result is always <= source so just copy over pfodESP8266Utils::strncpy_safe(storageRead.ssid, pfodESP8266Utils::trim(tempStr), pfodESP8266Utils::MAX_SSID_LEN); // store in eeprom always truncates this to allow for ROOT_IP } else if (webserver.argName(i)[0] == '1') { // convert timeout to int32_t // then *60*1000 to make mS and store as uint32_t pfodESP8266Utils::strncpy_safe(tempStr, (webserver.arg(i)).c_str(), sizeof(tempStr)); #ifdef DEBUG Serial.print(F("timeoutStr:")); Serial.println(tempStr); #endif long timeoutMin = 0; pfodESP8266Utils::parseLong((byte*)tempStr, &timeoutMin); if (timeoutMin > 71582) { timeoutMin = 71582; } if (timeoutMin < 0) { timeoutMin = 0; } storageRead.timeout = (uint32_t)(timeoutMin); } } #ifdef DEBUG Serial.println("After parsing config return:"); printStorage(&storageRead); #endif writeStorage(&storageRead); } // else if no args just return current settings delay(0); struct EEPROM_storage returnStorage; readStorage(&returnStorage); #ifdef DEBUG Serial.println("Returning config result:"); printStorage(&returnStorage); #endif String rtnMsg = "" "" "Fan Timer Setup" "" "" "" "" "

Fan Timer setting saved.

"; rtnMsg += "

Fan will run for "; rtnMsg += returnStorage.timeout; rtnMsg += " mins after light is switched off."; rtnMsg += "

The WiFi hotspot for setting fan timeout will is now
"; rtnMsg += returnStorage.ssid; rtnMsg += ROOT_IP; rtnMsg += "

The WiFi hotspot has been turned off and the Fan will returned to normal operation in a few seconds."; rtnMsg += ""; webserver.send(200, "text/html", rtnMsg); delay(2000); fan_init(); // clear vars for normal operation and load new fan timeout } void handleNotFound() { handleRoot(); }