/*
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."
"
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();
}