Erik de Ruiter
Published © GPL3+

Bathroom Ventilation Fan Controller

Now a few weeks in test and performing so well... ! An Arduino controller to *very effectively* keep the humidity to an acceptable level.

IntermediateFull instructions provided8 hours2,370
Bathroom Ventilation Fan Controller

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
Adafruit BME280 sensor
×1
OLED display 128x64 SPI
BUYING A CHINESE DISPLAY WILL WORK but you need to figure out which setting of the library you need. See the end part of the sketch!
×1
Axxatronic case with ventilation holes
Amazing case, small and with ventilation holes.
×1
Button
you'll have to find or make the key-caps yourself... Mine are 18mm tall
×3
wire to pcb connector
×1
NPN transistor
×1
2K2 resistor (or 3K3 or 4K7)
×1
Relay - Panasonic APF10205
×1

Story

Read more

Custom parts and enclosures

Files to make the project on strip-board

the board will fit in the mentioned Axxatronic case

Schematics

Bathroom Fan Controller Schematic

Connect the High voltage parts at your own risk! Be sure to work safely with 230V grid voltage

Code

Bathroom Fan Controller v 1.11

Arduino
see decription
//  888888b.            888    888
//  888  "88b           888    888
//  888  .88P           888    888
//  8888888K.   8888b.  888888 88888b.  888d888 .d88b.   .d88b.  88888b.d88b.
//  888  "Y88b     "88b 888    888 "88b 888P"  d88""88b d88""88b 888 "888 "88b
//  888    888 .d888888 888    888  888 888    888  888 888  888 888  888  888
//  888   d88P 888  888 Y88b.  888  888 888    Y88..88P Y88..88P 888  888  888
//  8888888P"  "Y888888  "Y888 888  888 888     "Y88P"   "Y88P"  888  888  888
//
//
//
//  8888888888                      .d8888b.                    888                    888 888
//  888                            d88P  Y88b                   888                    888 888
//  888                            888    888                   888                    888 888
//  8888888  8888b.  88888b.       888         .d88b.  88888b.  888888 888d888 .d88b.  888 888  .d88b.  888d888
//  888         "88b 888 "88b      888        d88""88b 888 "88b 888    888P"  d88""88b 888 888 d8P  Y8b 888P"
//  888     .d888888 888  888      888    888 888  888 888  888 888    888    888  888 888 888 88888888 888
//  888     888  888 888  888      Y88b  d88P Y88..88P 888  888 Y88b.  888    Y88..88P 888 888 Y8b.     888
//  888     "Y888888 888  888       "Y8888P"   "Y88P"  888  888  "Y888 888     "Y88P"  888 888  "Y8888  888
//
/*

* Version 1.11 - Last change: 2021-03-25

WHY (why oh why...?)
Well I looked for a good solution to keep the humidity level down in our bathroom.  We already have (already >20 years) a very good and silent fan but it is operated manually and sometimes we forgot to turn it on and/or off.
So looking around, I found there are only a few options. Yes you can buy a fan with build in controller but they are expensive and the manual settings are very limited. 
A stand alone humidity controller/switch was much harder to find! I only found a mechanical switch for just under 100 euros.  

The solution and description

As I am very fond of the Arduino, I (again) decided to make myself the things I need, in this case a "Bathroom Fan Controller" (for lack of a better name)
The controller has the following function and options:

    Measure Relative Humidity and temperature. (duh.)
    Turn a Fan on (via a relay) and switching it off when the humidity has dropped.
    OPTIONAL: The Fan will stay on for a selectable time after humidity has dropped. (decrease the humidity a bit more)
    Manually turn the Fan ON for 15m, 30m, 1, 2, 3, 4, 5, 6 or 12 hours.
    (useful for smelly events...)
    Manually turn the Fan Controller system OFF for 30m, 1, 2, 4, 8 or 12 hours.
    (want to go to bed but the noisy fan is on?  turn it off!)
    Turn the Fan Controller system OFF completely until turning ON manually.
    (vacation time!)
    User settings are stored in EEPROM and preserved after reset/power fail.

USER SETTINGS MENU:

    - Threshold: from 40%RH to 95%RH
    - Hysteresis: from 3%RH to 9%RH
    - Fan off delay: from 0 (NO delay) to 60 minutes.

BUTTONS:

    There are 3 buttons, from top to bottom these are:
    - ON / UP
    - OFF / DOWN
    - SELECT
    - on the side of the case: system RESET button


Explanation of the display of the controller...

    At the top-left on the display you see the CURRENT HUMIDITY value, updated every second. The percent (%) sign will blink to indicate this.
    on the top-right we have the humidity THRESHOLD value.
    below the threshold value you'll see the set HYSTERESIS value (optional)
    at the bottom right, the current TEMPERATURE is displayed.
    at the bottom left a Fan icon will indicate when the Fan is turned on. right of that icon a text 'DELAY' is displayed if the fan-off delay is activated.


Explanation of the system

No event / system IDLE:
The humidity and temperature is measured and updated every second, indicated by a blinking '%' character next to the measured humidity value.

The sensor is *very* sensitive and also *very* accurate!  So it will react fast and reliable on changing conditions. 

    NOTE: If you decide to use a sensor from China then this will be a different matter. Cheap AND reliable/precise is simply not possible. 

 
Event: humidity has risen equal or above the threshold:
When the current humidity reaches the threshold value, the Fan (relais) will switch on, indicated by a FAN SYMBOL at the lower left of the display.
The Fan will stay on until the humidity level has dropped below the threshold *minus the Hysteresis value*.  So if the threshold is 70% and the Hysteresis is 5, then the fan will shut off at 65% Relative humidity. 
NOTE: Obviously the hysteresis is very important! If not used you would have a fan switching off and on around the threshold value.

Event: humidity has dropped below the threshold value *minus Hysteresis*:
When the humidity level drop below the threshold plus hysteresis value, the fan will turn OFF.  Example: threshold=70 and hysteresis=5, then the fan will stop at a threshold level of 65. 
EXCEPT: if you have set a FAN OFF DELAY time then the fan will remain on for a user determined time (menu setting)

Manual interventions:
I purposely build in several useful features not found in commercial controllers (AFAIK). For example: 
- you have made the WC happy but the smell is not to be desired... Then you can turn on the fan manually for a set time.
- You want to go to bed but the fan is on because the humidity level is too high but the noise of the fan is disturbing. Then you can turn the system off for a set time, after which it will continue to measure and switch on when needed. Ventilation is important to keep mould away so this way you can't forget to turn the system on again.
- You are going on holiday: turn the system off completely. This seems obvious but with build in sensors in a fan this is not always possible

Explanation of the BUTTONS

    ON / UP: 
    - SYSTEM IDLE (fan OFF): when pressed the fan will turn ON for a set time, starting at 15 minutes. press UP again to increase fan ON time in pre-determined steps. (Maximum 12 hours)
    - SYSTEM OFF: turn system ON again
    - SYSTEM MANUALY SWITCHED OFF: system returns to SYSTEM IDLE state
    - MENU ACTIVE: when pressed the value is increased, hold to fast increase value.
    OFF / DOWN:
    - SYSTEM IDLE (fan OFF): when pressed, the system will SHUT DOWN for the set time, starting at 30 minutes. Press DOWN again to increase the shut down time in pre-determined steps. (Maximum 12 hours)
    - FAN IS ON or  FAN OFF DELAY active: stop the fan, then same as SYSTEM IDLE
    - ANY STATE (except MENU): when button is pressed for >1 second, the system is turned off completely until being turned ON again by pressing ON button.
    - MENU ACTIVE: when pressed the value is decreased, hold to fast decrease value.
    SELECT:
    when the button is pressed for >1 second, the user MENU is displayed
    - Set threshold: from 40%RH to 95%RH
    - Hysteresis: from 3%RH to 9%RH
    - Fan off delay: from 0 (no delay) to 60 minutes.


I had 2 cheap I2C 128x64pixel OLED screens in a drawer. maybe a bit tiny but way better than a 
20x2 LCD screen... Very bright an crisp displays, these OLED things... 

To get descent fonts, I use the amazing 8U2G font library from Oli Kraus
https://github.com/olikraus/U8g2_Arduino

This font library consumes a *lot* of memory but the result is great... I managed to get all
code in the Arduino Uno (atmega328). 

I maybe over-commented this sketch but I'm a NOT a programma myself so I want to: 1. make changes
in the future easier for myself and 2. help others to understand what the heck the code means.
Experienced programmers may make this sketch way better but it does it's job, that's the beauty
of the Arduino plaform: even beginners can enjoy coding and grow and be more efficient later on.



============ BSD License for Bathroom Fan Controller ============

Copyright (c) 2021, Erik de Ruiter, The Netherlands
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, 
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list 
  of conditions and the following disclaimer.
  
* Redistributions in binary form must reproduce the above copyright notice, this 
  list of conditions and the following disclaimer in the documentation and/or other 
  materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  
*/

#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <EEPROMex.h>
#include <U8g2lib.h>
#include <Adafruit_BME280.h>
#include <OneButton.h>

// *comment-out the #define line below if you don't want to see the
// *Hysteresis value and symbol on the OLED display
#define DISPLAY_HYSTERESIS

// home made icons for the display defined here. I used GIMP: made a new file,
// say 20x20 pixels, used the 'pen' to paint the image. When finished: menu
// [IMAGE]>[crop to selection]. Then menu [FILE]>[export as] and renamed the
// file, CHANGING THE EXTENSION TO .XBM (!)
// Then open this saved file with a text editor and paste all in the sketch.
// NOTE!!: I added in the line starting with 'static' this:
// 'const' and 'U8X8_PROGMEM', see below.

// percent icon
#define percent_width 10
#define percent_height 9
static const unsigned char percent_bits[] U8X8_PROGMEM = {
    0x0c, 0x02, 0x12, 0x01, 0x92, 0x00, 0x4c, 0x00, 0x20, 0x00, 0x90, 0x01,
    0x48, 0x02, 0x44, 0x02, 0x82, 0x01};
// percent icon BLACK (to 'erase' the icon for blinking it)
#define percent_width 10
#define percent_height 9
static const unsigned char black_bits[] U8X8_PROGMEM = {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
// degree + celcius icon
#define celcius_width 12
#define celcius_height 13
static const unsigned char celcius_bits[] U8X8_PROGMEM = {
    0x0e, 0x00, 0x91, 0x07, 0x51, 0x08, 0x51, 0x00, 0x4e, 0x00, 0x40, 0x00,
    0x40, 0x00, 0x40, 0x08, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00};
// fan icon
#define fan_width 16
#define fan_height 16
static const unsigned char fan_bits[] U8X8_PROGMEM = {
    0xf0, 0x00, 0xf8, 0x01, 0xf8, 0x03, 0xf0, 0x63, 0xe0, 0xf3, 0xc0, 0xf9,
    0xdc, 0xff, 0x7e, 0xfe, 0x7f, 0x7e, 0xff, 0x3b, 0x9f, 0x03, 0xcf, 0x07,
    0xc6, 0x0f, 0xc0, 0x1f, 0x80, 0x1f, 0x00, 0x0f};
// hand icon for MANUAL_ON mode indicator
#define hand_width 33
#define hand_height 41
static const unsigned char hand_bits[] U8X8_PROGMEM = {
    0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x00, 0x00, 0xc0,
    0xc7, 0x01, 0x00, 0x00, 0xc7, 0xe7, 0x03, 0x00, 0x80, 0xcf, 0xe7, 0x03,
    0x00, 0x80, 0xcf, 0xe7, 0x03, 0x00, 0x80, 0xcf, 0xe7, 0xe3, 0x00, 0x80,
    0xcf, 0xe7, 0xf3, 0x01, 0x80, 0xcf, 0xe7, 0xf3, 0x01, 0x80, 0xcf, 0xe7,
    0xf3, 0x01, 0x80, 0xcf, 0xe7, 0xf3, 0x01, 0x80, 0xcf, 0xe7, 0xf3, 0x01,
    0x80, 0xcf, 0xe7, 0xf3, 0x01, 0x80, 0xcf, 0xe7, 0xf3, 0x01, 0x80, 0xcf,
    0xe7, 0xf3, 0x01, 0x8e, 0xcf, 0xe7, 0xf3, 0x01, 0x9f, 0xcf, 0xe7, 0xf3,
    0x01, 0x9f, 0xff, 0xff, 0xf3, 0x01, 0x9f, 0xff, 0xff, 0xff, 0x01, 0xbf,
    0xff, 0xff, 0xff, 0x01, 0xbf, 0xff, 0xff, 0xff, 0x01, 0xff, 0xff, 0xff,
    0xff, 0x01, 0xfe, 0xc3, 0x7f, 0xf8, 0x01, 0xfe, 0x83, 0x3f, 0xf8, 0x01,
    0xfe, 0x03, 0x1f, 0xf8, 0x01, 0xfc, 0x03, 0x0e, 0xf8, 0x01, 0xfc, 0x23,
    0x84, 0xf8, 0x01, 0xfc, 0x63, 0xc0, 0xf8, 0x01, 0xf8, 0xe3, 0xe0, 0xf8,
    0x01, 0xf8, 0xe3, 0xf1, 0xf8, 0x01, 0xf0, 0xe3, 0xfb, 0xf8, 0x01, 0xf0,
    0xe3, 0xff, 0xf8, 0x01, 0xe0, 0xe3, 0xff, 0xf8, 0x00, 0xe0, 0xe3, 0xff,
    0xf8, 0x00, 0xc0, 0xe3, 0xff, 0xf8, 0x00, 0xc0, 0xff, 0xff, 0x7f, 0x00,
    0x80, 0xff, 0xff, 0x7f, 0x00, 0x80, 0xff, 0xff, 0x3f, 0x00, 0x00, 0xff,
    0xff, 0x1f, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0x00, 0x00, 0xfc, 0xff, 0x07,
    0x00};
// up arrow for menu
#define upArrow_width 12
#define upArrow_height 15
static const unsigned char upArrow_bits[] U8X8_PROGMEM = {
    0x60, 0x00, 0xf0, 0x00, 0xf8, 0x01, 0xfc, 0x03, 0xfe, 0x07, 0xff, 0x0f,
    0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01,
    0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01};
// down arrow for menu
#define downArrow_width 12
#define downArrow_height 15
static const unsigned char downArrow_bits[] U8X8_PROGMEM = {
    0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01,
    0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xff, 0x0f, 0xfe, 0x07, 0xfc, 0x03,
    0xf8, 0x01, 0xf0, 0x00, 0x60, 0x00};
// hysteresis icon
#ifdef DISPLAY_HYSTERESIS
#define hysteresis_width 11
#define hysteresis_height 11
static const unsigned char hysteresis_bits[] U8X8_PROGMEM = {
    0xf8, 0x07, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00,
    0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0x88, 0x00, 0xff, 0x00};
#endif
// stopwatch icon
#define stopwatch_width 24
#define stopwatch_height 24
static const unsigned char stopwatch_bits[] U8X8_PROGMEM = {
    0x00, 0x7e, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x3c, 0x00, 0x18, 0x18, 0x18,
    0x0c, 0x7e, 0x30, 0x9e, 0x81, 0x79, 0x7a, 0x18, 0x5e, 0x10, 0x00, 0x08,
    0x10, 0x18, 0x08, 0x08, 0x18, 0x10, 0x08, 0x18, 0x10, 0x04, 0x18, 0x20,
    0x04, 0x18, 0x20, 0x14, 0xf8, 0x2b, 0x14, 0xf8, 0x2b, 0x04, 0x00, 0x20,
    0x04, 0x00, 0x20, 0x08, 0x00, 0x10, 0x08, 0x00, 0x10, 0x10, 0x00, 0x08,
    0x10, 0x00, 0x08, 0x60, 0x18, 0x06, 0x80, 0x81, 0x01, 0x00, 0x7e, 0x00};

//  8888888b. 8888888 888b    888                                 .d888 d8b
//  888   Y88b  888   8888b   888                                d88P"  Y8P
//  888    888  888   88888b  888                                888
//  888   d88P  888   888Y88b 888       .d8888b .d88b.  88888b.  888888 888  .d88b.
//  8888888P"   888   888 Y88b888      d88P"   d88""88b 888 "88b 888    888 d88P"88b
//  888         888   888  Y88888      888     888  888 888  888 888    888 888  888
//  888         888   888   Y8888      Y88b.   Y88..88P 888  888 888    888 Y88b 888
//  888       8888888 888    Y888       "Y8888P "Y88P"  888  888 888    888  "Y88888
//                                                                               888
//                                                                          Y8b d88P
//                                                                           "Y88P"

// These are all the Arduino PIN connections... Of course definition of the I2C pins
// A4 and A5 are not needed but added here for convenience.
#define PIN_RELAIS 13
#define PIN_DISPLAY_CLOCK 12
#define PIN_DISPLAY_DATA 11
#define PIN_DISPLAY_CS 10
#define PIN_DISPLAY_DC 9
#define PIN_DISPLAY_RESET 8
#define PIN_BUTTON_DOWN 5
#define PIN_BUTTON_UP 7
#define PIN_BUTTON_SELECT 6
#define PIN_I2C_CLOCK A5
#define PIN_I2C_DATA A4

//                            d8b          888      888
//                            Y8P          888      888
//                                         888      888
//  888  888  8888b.  888d888 888  8888b.  88888b.  888  .d88b.  .d8888b
//  888  888     "88b 888P"   888     "88b 888 "88b 888 d8P  Y8b 88K
//  Y88  88P .d888888 888     888 .d888888 888  888 888 88888888 "Y8888b.
//   Y8bd8P  888  888 888     888 888  888 888 d88P 888 Y8b.          X88
//    Y88P   "Y888888 888     888 "Y888888 88888P"  888  "Y8888   88888P'
//

float sensorTemp = 0;
int sensorHumidity = 0;
int sensorHumidityFraction = 0;
byte humidityHysteresis = 5 /*Rel.Humidity*/;
byte humidityThreshold = 0;

bool btnSelectClickEvent = false;
bool btnUpClickEvent = false;
bool btnDownClickEvent = false;
bool btnSelectHoldEvent = false;
bool btnUpHoldEvent = false;
bool btnDownHoldEvent = false;
bool btnUpDuringHoldEvent = false;
bool btnDownDuringHoldEvent = false;

unsigned long previousSensorReadTime = 0;
bool sensorIsRead = false;
bool humidityLevelTooHigh = false;

unsigned int fanCountdown = 0;
unsigned int fanRunTime = 0;
unsigned int fanRunStartTime = 0;
unsigned int fanManualOnRunTime = 15 /*minutes*/;

unsigned int fanDisabledTime = 0;
unsigned int fanDisabledStartTime = 0;
unsigned int fanDisabledRunTime = 30 /*minutes*/;

byte fanSwitchOffDelayTime = 30 /*minutes*/;

int timeHours = 0;
int timeMinutes = 0;
int timeSeconds = 0;

char bufferTime[6];

// set Arduino Uno EEPROM membase to store the user data
const int memBase = 350;

//           888       d8b                   888
//           888       Y8P                   888
//           888                             888
//   .d88b.  88888b.  8888  .d88b.   .d8888b 888888 .d8888b
//  d88""88b 888 "88b "888 d8P  Y8b d88P"    888    88K
//  888  888 888  888  888 88888888 888      888    "Y8888b.
//  Y88..88P 888 d88P  888 Y8b.     Y88b.    Y88b.       X88
//   "Y88P"  88888P"   888  "Y8888   "Y8888P  "Y888  88888P'
//                     888
//                    d88P
//                  888P"

// Setup new OneButton Objects
OneButton buttonSelect(/*PIN*/ PIN_BUTTON_SELECT, /*INPUT_PULLUP*/ true);
OneButton buttonUP(/*PIN*/ PIN_BUTTON_UP, /*INPUT_PULLUP*/ true);
OneButton buttonDown(/*PIN*/ PIN_BUTTON_DOWN, /*INPUT_PULLUP*/ true);

// Object OLED screen 128x64 pixels with SPI interface
// ! NOTE:  In my case connecting the display using I2C resulted in erratic behaviour
// ! due to static electricity(??). SPI proved to be much more stable in my case.
//
// NOTE2: Some displays support FLIP MODE so you can rotate the display output.
// change the 'U8G2_R0' in the constructor below: 
//
// U8G2_R0 = no rotation, 
// U8G2_R1 = 90 degree clockwise rotation,
// U8G2_R2 = 180 degree clockwise rotation,
// U8G2_R3 = 270 degree clockwise rotation.
//
U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI u8g2(U8G2_R0,
                                            /* clock=*/PIN_DISPLAY_CLOCK,
                                            /* data=*/PIN_DISPLAY_DATA,
                                            /* cs=*/PIN_DISPLAY_CS,
                                            /* dc=*/PIN_DISPLAY_DC,
                                            /* reset=*/PIN_DISPLAY_RESET);

// Object BME280 sensor
Adafruit_BME280 bme; // I2C

// State Machine States
typedef enum FSM
{
  IDLE_FAN_OFF,
  FAN_ON,
  MANUAL_ON,
  MANUAL_OFF,
  DISABLE,
  FAN_OFF_DELAY,
  SET_THRESHOLD,
  SET_HYSTERESIS,
  SET_SWITCH_OFF_DELAY,
} FSM;

FSM state = IDLE_FAN_OFF; // no action when starting

//   .d8888b.           888                        .d88 88b.
//  d88P  Y88b          888                       d88P" "Y88b
//  Y88b.               888                      d88P     Y88b
//   "Y888b.    .d88b.  888888 888  888 88888b.  888       888
//      "Y88b. d8P  Y8b 888    888  888 888 "88b 888       888
//        "888 88888888 888    888  888 888  888 Y88b     d88P
//  Y88b  d88P Y8b.     Y88b.  Y88b 888 888 d88P  Y88b. .d88P
//   "Y8888P"   "Y8888   "Y888  "Y88888 88888P"    "Y88 88P"
//                                      888
//                                      888
//                                      888
/******************************************************************************/
void setup()
{
  bme.begin();
  u8g2.begin();
  Wire.begin();
  /*clockFrequency: the value (in Hertz) of desired communication clock. 
  Accepted values are 100000 (standard mode) and 400000 (fast mode). 
  Some processors also support 10000 (low speed mode), 
  1000000 (fast mode plus) and 3400000 (high speed mode). 
  Please refer to the specific processor documentation to make sure 
  the desired mode is supported. */
  // if you set at 400000, display will mess up occasionally
  Wire.setClock(100000);

  pinMode(PIN_DISPLAY_RESET, OUTPUT);
  pinMode(PIN_RELAIS, OUTPUT);

  // Buttons...
  // link the myClickFunction function to be called on a button click event.
  buttonSelect.attachClick(buttonSelectClick);
  buttonUP.attachClick(buttonUpClick);
  buttonDown.attachClick(buttonDownClick);

  // link the myClickFunction function to be called on a button hold event.
  buttonUP.attachDuringLongPress(buttonUpDuringLongPress);
  buttonDown.attachDuringLongPress(buttonDownDuringLongPress);

  // link the myClickFunction function to be called on a button START hold event.
  buttonSelect.attachLongPressStart(buttonSelectLongPress);
  buttonUP.attachLongPressStart(buttonUpLongPress);
  buttonDown.attachLongPressStart(buttonDownLongPress);

  // set 50 msec. debouncing time. Default is 50 msec.
  buttonSelect.setDebounceTicks(50);
  buttonUP.setDebounceTicks(50);
  buttonDown.setDebounceTicks(50);

  // read EEPROM values. new memory often has 255 as memory content so we perform a rudimentary
  // check to see if the memory locations has never been used before. if so, set default values
  // memBase is the start EEPROM address (see variables)
  // An Interger value take 2 Bytes to store it in EEPROM so we need to take that in account.
  EEPROM.readInt(memBase) > 100 ? humidityThreshold = 65 : humidityThreshold = EEPROM.readInt(memBase);
  EEPROM.readInt(memBase + 2) > 10 ? humidityHysteresis = 4 : humidityHysteresis = EEPROM.readInt(memBase + 2);
  EEPROM.readInt(memBase + 4) > 60 ? fanSwitchOffDelayTime = 30 : fanSwitchOffDelayTime = EEPROM.readInt(memBase + 4);

}

//  888                                  .d88 88b.
//  888                                 d88P" "Y88b
//  888                                d88P     Y88b
//  888      .d88b.   .d88b.  88888b.  888       888
//  888     d88""88b d88""88b 888 "88b 888       888
//  888     888  888 888  888 888  888 Y88b     d88P
//  888     Y88..88P Y88..88P 888 d88P  Y88b. .d88P
//  88888888 "Y88P"   "Y88P"  88888P"    "Y88 88P"
//                            888
//                            888
//                            888

/******************************************************************************/
void loop()
{
  // keep watching the push button:
  buttonSelect.tick();
  buttonUP.tick();
  buttonDown.tick();
  // update sensor IDLE_FAN_OFFment
  readSensor();
  // run Finite State Machine
  runFSM();
}

//  8888888888   .d8888b.      888b     d888
//  888         d88P  Y88b     8888b   d8888
//  888         Y88b.          88888b.d88888
//  8888888      "Y888b.       888Y88888P888
//  888             "Y88b.     888 Y888P 888
//  888               "888     888  Y8P  888
//  888     d8b Y88b  d88P d8b 888   "   888 d8b
//  888     Y8P  "Y8888P"  Y8P 888       888 Y8P
//
/******************************************************************************/
void runFSM()
{
  switch (state)
  {
    //  d8b      888 888
    //  Y8P      888 888
    //           888 888
    //  888  .d88888 888  .d88b.
    //  888 d88" 888 888 d8P  Y8b
    //  888 888  888 888 88888888
    //  888 Y88b 888 888 Y8b.
    //  888  "Y88888 888  "Y8888
    //
    /***************************************************************************/
  case IDLE_FAN_OFF:
    // default state, show main display
    displayUpdate();

    //check for need to turn fan on
    checkHumidityLevel();
    if (humidityLevelTooHigh == true)
    {
      state = FAN_ON;
    }

    // check if MANUAL_ON mode is required ('ON' button click event)
    if (btnUpClickEvent == true)
    {
      // set Fan start timer before switching state
      fanRunStartTime /*=seconds*/ = (millis() / 1000);
      state = MANUAL_ON;
    }

    // check if MANUAL_OFF mode is required, so turning OFF the humidity
    // controller for a selected time
    if (btnDownClickEvent == true)
    {
      // set Fan start timer before switching state
      fanDisabledStartTime /*=seconds*/ = (millis() / 1000);
      state = MANUAL_OFF;
    }

    // check if DOWN button is being hold, turning OFF the system
    if (btnDownHoldEvent == true)
    {
      state = DISABLE;
    }

    if (btnSelectHoldEvent == true)
    {
      state = SET_THRESHOLD;
    }

    // reset all button events
    // * You NEED to have this button reset code in every state else the
    // * button event(s) will transfer over to the new state with unwanted resuls
    btnSelectClickEvent = false;
    btnSelectHoldEvent = false;
    btnUpClickEvent = false;
    btnUpHoldEvent = false;
    btnDownClickEvent = false;
    btnDownHoldEvent = false;
    btnUpDuringHoldEvent = false;
    btnDownDuringHoldEvent = false;

    break;

    //   .d888
    //  d88P"
    //  888
    //  888888 8888b.  88888b.        .d88b.  88888b.
    //  888       "88b 888 "88b      d88""88b 888 "88b
    //  888   .d888888 888  888      888  888 888  888
    //  888   888  888 888  888      Y88..88P 888  888
    //  888   "Y888888 888  888       "Y88P"  888  888
    //
  /***************************************************************************/
  case FAN_ON:
    displayUpdate();
    turnFanOn();
    checkHumidityLevel();

    //check for need to turn fan off
    if (humidityLevelTooHigh == false)
    {
      // set Fan off-delay timer before switching state
      fanRunStartTime /*=seconds*/ = (millis() / 1000);
      state = FAN_OFF_DELAY;
    }

    if (btnSelectHoldEvent == true)
    {
      turnFanOff();
      state = SET_THRESHOLD;
    }

    // check if MANUAL_OFF mode is required, so turning OFF the humidity
    // controller for a selected time
    if (btnDownClickEvent == true)
    {
      // set Fan start timer before switching state
      fanDisabledStartTime /*=seconds*/ = (millis() / 1000);
      state = MANUAL_OFF;
    }
    // check if DOWN button is being hold, turning OFF the system
    if (btnDownHoldEvent == true)
    {
      state = DISABLE;
    }

    // reset all button events
    // * You NEED to have this button reset code in every state else the
    // * button event(s) will transfer over to the new state with unwanted resuls
    btnSelectClickEvent = false;
    btnSelectHoldEvent = false;
    btnUpClickEvent = false;
    btnUpHoldEvent = false;
    btnDownClickEvent = false;
    btnDownHoldEvent = false;
    btnUpDuringHoldEvent = false;
    btnDownDuringHoldEvent = false;

    break;

    //                                                    888
    //                                                    888
    //                                                    888
    //  88888b.d88b.   8888b.  88888b.  888  888  8888b.  888       .d88b.  88888b.
    //  888 "888 "88b     "88b 888 "88b 888  888     "88b 888      d88""88b 888 "88b
    //  888  888  888 .d888888 888  888 888  888 .d888888 888      888  888 888  888
    //  888  888  888 888  888 888  888 Y88b 888 888  888 888      Y88..88P 888  888
    //  888  888  888 "Y888888 888  888  "Y88888 "Y888888 888       "Y88P"  888  888
    //
  /***************************************************************************/
  case MANUAL_ON:
    displayUpdate();
    turnFanOn();
    // fanRunStartTime was set to the current millis() value in the previous State
    // so now we can compare this 'start' time with the time passed using the current
    // millis() value. the max. time possible is 12 hours which is 12hx3600s=43200s
    // so it will fit in the unsigned INT variables.  If you want longer run times,
    // be sure to use LONG variables.
    fanRunTime /*=seconds*/ = (millis() / 1000) - (fanRunStartTime /*=seconds*/);

    // check the FAN 'ON' duration timer
    if ((fanRunTime /*=seconds*/ / 60) /*=converted to minutes*/ >= fanManualOnRunTime /*=minutes*/)
    {
      turnFanOff();
      // reset to default value before exit
      fanManualOnRunTime = 15;
      // go to new state
      state = IDLE_FAN_OFF;
    }

    // check if we want to exit the MANUAL ON mode
    if (btnDownClickEvent == true)
    {
      turnFanOff();
      // reset to default value before exit
      fanManualOnRunTime = 15;
      // go to new state
      state = IDLE_FAN_OFF;
    }

    // check if DOWN button is being hold, turning OFF the system
    if (btnDownHoldEvent == true)
    {
      // reset to default value before exit
      fanManualOnRunTime = 15;
      // go to new state
      state = DISABLE;
    }

    // if UP button is pressed while in MANUAL_ON mode, cycle through different off delay times
    if (btnUpClickEvent == true)
    {
      switch (fanManualOnRunTime)
      {
      case 15:
        fanManualOnRunTime = 30;
        break;
      case 30:
        fanManualOnRunTime = 60;
        break;
      case 60:
        fanManualOnRunTime = 90;
        break;
      case 90:
        fanManualOnRunTime = 120;
        break;
      case 120:
        fanManualOnRunTime = 180;
        break;
      case 180:
        fanManualOnRunTime = 240;
        break;
      case 240:
        fanManualOnRunTime = 300;
        break;
      case 300:
        fanManualOnRunTime = 360;
        break;
      case 360:
        fanManualOnRunTime = 720;
        break;
      case 720:
        fanManualOnRunTime = 15;
        break;
      }
    }

    // reset all button events
    // * You NEED to have this button reset code in every state else the
    // * button event(s) will transfer over to the new state with unwanted resuls
    btnSelectClickEvent = false;
    btnSelectHoldEvent = false;
    btnUpClickEvent = false;
    btnUpHoldEvent = false;
    btnDownClickEvent = false;
    btnDownHoldEvent = false;
    btnUpDuringHoldEvent = false;
    btnDownDuringHoldEvent = false;

    break; //case MANUAL_ON

    //                                                    888                .d888  .d888
    //                                                    888               d88P"  d88P"
    //                                                    888               888    888
    //  88888b.d88b.   8888b.  88888b.  888  888  8888b.  888       .d88b.  888888 888888
    //  888 "888 "88b     "88b 888 "88b 888  888     "88b 888      d88""88b 888    888
    //  888  888  888 .d888888 888  888 888  888 .d888888 888      888  888 888    888
    //  888  888  888 888  888 888  888 Y88b 888 888  888 888      Y88..88P 888    888
    //  888  888  888 "Y888888 888  888  "Y88888 "Y888888 888       "Y88P"  888    888
    //
    //
  /***************************************************************************/
  case MANUAL_OFF:
    displayUpdate();
    turnFanOff();
    // fanRunStartTime was set to the current millis() value in the previous State
    // so now we can compare this 'start' time with the time passed using the current
    // millis() value. the max. time possible is 12 hours which is 12hx3600s=43200s
    // so it will fit in the unsigned INT variables.  If you want longer run times,
    // be sure to use LONG variables.
    fanDisabledTime /*=seconds*/ = (millis() / 1000) - (fanDisabledStartTime /*=seconds*/);

    // check the FAN 'OFF' duration timer
    if ((fanDisabledTime /*=seconds*/ / 60) /*=converted to minutes*/ >= fanDisabledRunTime /*=minutes*/)
    {
      // reset to default value before exit
      fanDisabledRunTime = 30;
      // go to new state
      state = IDLE_FAN_OFF;
    }

    // check if return to normal operational mode is required ('ON' button click event)
    if (btnUpClickEvent == true)
    {
      // reset to default value before exit
      fanDisabledRunTime = 30;
      // go to new state
      state = IDLE_FAN_OFF;
    }

    // check if DOWN button is being hold, turning OFF the system
    if (btnDownHoldEvent == true)
    {
      // reset to default value before exit
      fanDisabledRunTime = 30;
      // go to new state
      state = DISABLE;
    }

    // if DOWN button is pressed while in MANUAL_OFF mode, cycle through different off delay times
    if (btnDownClickEvent == true)
    {
      switch (fanDisabledRunTime)
      {
      case 30:
        fanDisabledRunTime = 60;
        break;
      case 60:
        fanDisabledRunTime = 120;
        break;
      case 120:
        fanDisabledRunTime = 240;
        break;
      case 240:
        fanDisabledRunTime = 480;
        break;
      case 480:
        fanDisabledRunTime = 720;
        break;
      case 720:
        fanDisabledRunTime = 30;
        break;
      }
    }

    // reset all button events
    // * You NEED to have this button reset code in every state else the
    // * button event(s) will transfer over to the new state with unwanted resuls
    btnSelectClickEvent = false;
    btnSelectHoldEvent = false;
    btnUpClickEvent = false;
    btnUpHoldEvent = false;
    btnDownClickEvent = false;
    btnDownHoldEvent = false;
    btnUpDuringHoldEvent = false;
    btnDownDuringHoldEvent = false;
    break;

    //       888 d8b                   888      888
    //       888 Y8P                   888      888
    //       888                       888      888
    //   .d88888 888 .d8888b   8888b.  88888b.  888  .d88b.
    //  d88" 888 888 88K          "88b 888 "88b 888 d8P  Y8b
    //  888  888 888 "Y8888b. .d888888 888  888 888 88888888
    //  Y88b 888 888      X88 888  888 888 d88P 888 Y8b.
    //   "Y88888 888  88888P' "Y888888 88888P"  888  "Y8888
    //
  /***************************************************************************/
  case DISABLE:
    displayUpdate();
    turnFanOff();
    // check if UP button is clicked to turn the system on again
    if (btnUpClickEvent == true)
    {
      // go to new state
      state = IDLE_FAN_OFF;
    }
    // reset all button events
    // * You NEED to have this button reset code in every state else the
    // * button event(s) will transfer over to the new state with unwanted resuls
    btnSelectClickEvent = false;
    btnSelectHoldEvent = false;
    btnUpClickEvent = false;
    btnUpHoldEvent = false;
    btnDownClickEvent = false;
    btnDownHoldEvent = false;
    btnUpDuringHoldEvent = false;
    btnDownDuringHoldEvent = false;

    break;

    //   .d888                                 .d888  .d888           888          888
    //  d88P"                                 d88P"  d88P"            888          888
    //  888                                   888    888              888          888
    //  888888 8888b.  88888b.        .d88b.  888888 888888       .d88888  .d88b.  888  8888b.  888  888
    //  888       "88b 888 "88b      d88""88b 888    888         d88" 888 d8P  Y8b 888     "88b 888  888
    //  888   .d888888 888  888      888  888 888    888         888  888 88888888 888 .d888888 888  888
    //  888   888  888 888  888      Y88..88P 888    888         Y88b 888 Y8b.     888 888  888 Y88b 888
    //  888   "Y888888 888  888       "Y88P"  888    888          "Y88888  "Y8888  888 "Y888888  "Y88888
    //                                                                                               888
    //                                                                                          Y8b d88P
  /***************************************************************************/
  case FAN_OFF_DELAY:
    displayUpdate();

    fanRunTime /*=seconds*/ = (millis() / 1000) - (fanRunStartTime /*=seconds*/);

    // check if humidity level did rise above the threshold level *during* delay.
    // if so, cancel FAN_OFF_DELAY and go to FAN_ON state again.
    checkHumidityLevel();
    if (humidityLevelTooHigh == true)
    {
      state = FAN_ON;
    }

    // check the FAN off-delay timer
    if ((fanRunTime /*=seconds*/ / 60) >= fanSwitchOffDelayTime /*=minutes*/)
    {
      turnFanOff();
      // go to new state
      state = IDLE_FAN_OFF;
    }

    // check if MANUAL_ON fan off is equired ('OFF' button click event)
    if (btnDownClickEvent == true)
    {
      turnFanOff();
      // go to new state
      state = IDLE_FAN_OFF;
    }

    // check if DOWN button is being hold, turning OFF the system
    if (btnDownHoldEvent == true)
    {
      state = DISABLE;
    }

    // reset all button events
    // * You NEED to have this button reset code in every state else the
    // * button event(s) will transfer over to the new state with unwanted resuls
    btnSelectClickEvent = false;
    btnSelectHoldEvent = false;
    btnUpClickEvent = false;
    btnUpHoldEvent = false;
    btnDownClickEvent = false;
    btnDownHoldEvent = false;
    btnUpDuringHoldEvent = false;
    btnDownDuringHoldEvent = false;
    break;

    //                    888         888    888                               888               888      888
    //                    888         888    888                               888               888      888
    //                    888         888    888                               888               888      888
    //  .d8888b   .d88b.  888888      888888 88888b.  888d888 .d88b.  .d8888b  88888b.   .d88b.  888  .d88888
    //  88K      d8P  Y8b 888         888    888 "88b 888P"  d8P  Y8b 88K      888 "88b d88""88b 888 d88" 888
    //  "Y8888b. 88888888 888         888    888  888 888    88888888 "Y8888b. 888  888 888  888 888 888  888
    //       X88 Y8b.     Y88b.       Y88b.  888  888 888    Y8b.          X88 888  888 Y88..88P 888 Y88b 888
    //   88888P'  "Y8888   "Y888       "Y888 888  888 888     "Y8888   88888P' 888  888  "Y88P"  888  "Y88888
    //
    //
  /***************************************************************************/
  case SET_THRESHOLD:
    displayUpdate();

    if ((btnUpClickEvent == true || btnUpDuringHoldEvent == true) && humidityThreshold < 95)
    {
      humidityThreshold += 1;
    }
    if ((btnDownClickEvent == true || btnDownDuringHoldEvent == true) && humidityThreshold > 40)
    {
      humidityThreshold -= 1;
    }
    if (btnSelectClickEvent == true)
    {
      // save to eeprom
      EEPROM.writeInt(memBase, humidityThreshold);
      // go to new state, next menu item
      state = SET_HYSTERESIS;
    }

    // reset all button events
    // * You NEED to have this button reset code in every state else the
    // * button event(s) will transfer over to the new state with unwanted resuls
    btnSelectClickEvent = false;
    btnSelectHoldEvent = false;
    btnUpClickEvent = false;
    btnUpHoldEvent = false;
    btnDownClickEvent = false;
    btnDownHoldEvent = false;
    btnUpDuringHoldEvent = false;
    btnDownDuringHoldEvent = false;
    break;

    //                    888         888                        888
    //                    888         888                        888
    //                    888         888                        888
    //  .d8888b   .d88b.  888888      88888b.  888  888 .d8888b  888888 .d88b.  888d888
    //  88K      d8P  Y8b 888         888 "88b 888  888 88K      888   d8P  Y8b 888P"
    //  "Y8888b. 88888888 888         888  888 888  888 "Y8888b. 888   88888888 888
    //       X88 Y8b.     Y88b.       888  888 Y88b 888      X88 Y88b. Y8b.     888  d8b
    //   88888P'  "Y8888   "Y888      888  888  "Y88888  88888P'  "Y888 "Y8888  888  Y8P
    //                                              888
    //                                         Y8b d88P
    //                                          "Y88P"
  /***************************************************************************/
  case SET_HYSTERESIS:
    displayUpdate();

    if (btnUpClickEvent == true && humidityHysteresis <= 8)
    {
      humidityHysteresis += 1;
    }
    if (btnDownClickEvent == true && humidityHysteresis >= 4)
    {
      humidityHysteresis -= 1;
    }
    if (btnSelectClickEvent == true)
    {
      // save to eeprom
      EEPROM.writeInt(memBase + 2, humidityHysteresis);
      // go to new state, next menu item
      state = SET_SWITCH_OFF_DELAY;
    }

    // reset all button events
    // * You NEED to have this button reset code in every state else the
    // * button event(s) will transfer over to the new state with unwanted resuls
    btnSelectClickEvent = false;
    btnSelectHoldEvent = false;
    btnUpClickEvent = false;
    btnUpHoldEvent = false;
    btnDownClickEvent = false;
    btnDownHoldEvent = false;
    btnUpDuringHoldEvent = false;
    btnDownDuringHoldEvent = false;
    break;

    //                    888                   .d888  .d888           888          888
    //                    888                  d88P"  d88P"            888          888
    //                    888                  888    888              888          888
    //  .d8888b   .d88b.  888888       .d88b.  888888 888888       .d88888  .d88b.  888  8888b.  888  888
    //  88K      d8P  Y8b 888         d88""88b 888    888         d88" 888 d8P  Y8b 888     "88b 888  888
    //  "Y8888b. 88888888 888         888  888 888    888         888  888 88888888 888 .d888888 888  888
    //       X88 Y8b.     Y88b.       Y88..88P 888    888         Y88b 888 Y8b.     888 888  888 Y88b 888
    //   88888P'  "Y8888   "Y888       "Y88P"  888    888          "Y88888  "Y8888  888 "Y888888  "Y88888
    //                                                                                                888
    //                                                                                           Y8b d88P
    //                                                                                            "Y88P"
  /***************************************************************************/
  case SET_SWITCH_OFF_DELAY:
    displayUpdate();

    if ((btnUpClickEvent == true || btnUpDuringHoldEvent) && fanSwitchOffDelayTime < 60)
    {
      fanSwitchOffDelayTime += 1;
    }
    if ((btnDownClickEvent == true || btnDownDuringHoldEvent) && fanSwitchOffDelayTime > 0)
    {
      fanSwitchOffDelayTime -= 1;
    }
    if (btnSelectClickEvent == true)
    {
...

This file has been truncated, please download it to see its full contents.

Credits

Erik de Ruiter

Erik de Ruiter

7 projects • 99 followers

Comments