Craig Gibson
Published © GPL3+

Garage MC (Monitor & Controller)

Garage Door IOT Monitor and Controller, using Blynk App on your cell phone. Also OTA updates and MQTT to Home Assistant.

IntermediateShowcase (no instructions)668

Things used in this project

Hardware components

NodeMCU ESP8266 Breakout Board
NodeMCU ESP8266 Breakout Board
Any ESP8266 board will work, but I went with KeeYees Development Board WiFi WLAN Wireless Module for ESP8266 NodeMCU ESP-12E from Amazon
×1
Hall Effect Sensor - A3144
digital output, non-latching, normally HIGH
×2
Relay - 1-channel
5V 1 Channel Level Trigger Optocoupler Relay Module - Normally Open
×1
LED (generic)
LED (generic)
×2
Buzzer, Piezo
Buzzer, Piezo
×1
Resistor 221 ohm
Resistor 221 ohm
One for each LED
×2
Resistor 100 ohm
Resistor 100 ohm
For Piezo buzzer
×1
2-pin Screw Terminals
Optional
×2
Neodymium Magnet
Although any magnets will work, 2 neodymium magnets allow the Hall Sensors to be 3 cm or more from the magnet and still work.
×2
Breadboard (generic)
Breadboard (generic)
×1
Jumper wires (generic)
Jumper wires (generic)
×1
Perfboard (Prototyping Board)
×1

Software apps and online services

Arduino IDE
Arduino IDE
Fritzing
Blynk
Blynk
MQTT
MQTT

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Schematics

Fritzing breadboard diagram

Breadboard diagram used for testing

Fritzing Perf/Proto Board Diagram

Perf/Proto Board diagram for the final product.
Note: the NodeMCU goes on the header rails and the top pins were not used/connected

Code

Garage MC Code

Arduino
Updated 2021-01-06
/* ----------------------------------------------------------------------------
 * Garage MC (Monitor & Controller)
 * 
 * Monitors the position of the garage door using 2 x Hall Effect Sensors
 * Connects to Blynk App to display the door status, send notifications, 
 * and a button to open/close the door
 * 
 * Board: NodeMCU - ESP8266-12E
 * Sensors: 
 *  2x A3144 Hall Effect Sensor
 *    - Uses a neodymium magnet on the garage door to trigger the sensors
 *    - digital output, non-latching
 *    - normally HIGH, magnet draws it LOW
 *  Relay - low trigger
 *        - Normally Open (N/O) connected to garage opener
 *        - The 1-channel 'TongLing' relay requires 5 volts from VIN pin
 *  Buzzer - passive (piezo) buzzer [100 ohm resistor from OUT to +]
 *  
 *  Program flow:
 *    SETUP - sets the following timers:
 *      Continuous timers
 *        CheckGarageState - Checks what state the garage door is in
 *        CheckConnection - Simply checks to ensure we still have WiFi & Blynk connections
 *      Run Once timer
 *        GarageStateChanged - Initialize the garage state
 *    LOOP - Only runs Blynk and the Blynk Timer, which runs the above timers
 *    INTERRUPTS - Occur with any change in the garage state (activated by the Hall Sensors)
 *      They set an immediate timer to run GarageStateChanged (minimal code in the Interrupts)
 *      GarageStateChanged also calls CheckGarageState
 *      
 *    The Garage Door can be opened/closed (ActivateDoor) from the Blynk App
 *  
 * ----------------------------------------------------------------------------
 */

/*******************************************************************************
/* Debugging Macros (comment out #define DEBUG to remove debugging code)
/*******************************************************************************/
//#define DEBUG
#ifdef DEBUG
 #define BLYNK_PRINT        Serial                // Blynk printouts to serial port
 #define DEBUG_PRINT(x)     Serial.print (x)
 #define DEBUG_PRINTDEC(x)  Serial.print (x, DEC)
 #define DEBUG_PRINTLN(x)   Serial.println (x)
#else
 #define DEBUG_PRINT(x)
 #define DEBUG_PRINTDEC(x)
 #define DEBUG_PRINTLN(x)
#endif

/*******************************************************************************
/* Included Libraries
/*******************************************************************************/
#include "arduino_secrets.h"    // Passwords
#include <ESP8266WiFi.h>        // WiFi Library
#include <BlynkSimpleEsp8266.h> // Blynk ESP8266 Library
#include <TimeLib.h>            // Time Library (required by Blynk RTC)
#include <WidgetRTC.h>          // Blynk RealTimeClock (RTC) widget
#include <ArduinoOTA.h>         // For Over-The-Air Code Updates
#include <PubSubClient.h>       // MQTT Client

/*******************************************************************************
/* Global Variables
/*******************************************************************************/

//----------------------------------------
// Pins
const int WIFI_OFF_PIN    = D2; // RED led - when Wifi is not connected
const int WIFI_ON_PIN     = D3; // GREEN led - when Wifi is connected
const int RELAY1_PIN      = D4; // D4 is HIGH at boot (relay triggers low) - Garage Door Control
const int BUZZER_PIN      = D5; // Passive (piezo) buzzer pin (PWM pin)
const int HALL_CLOSED_PIN = D6; // Hall sensor at garage door CLOSED point
const int HALL_OPEN_PIN   = D7; // Hall sensor at garage door OPEN point

//----------------------------------------
// Hall Sensors
//    Initialize to door AJAR (sensors are checked in Setup function anyway)
//    Global variables in Interrupts must be declared 'volatile'
volatile byte hallClosedState = HIGH; // sensor to check if the door is down (CLOSED)
volatile byte hallOpenState   = HIGH; // sensor to check if the door is up (OPEN)

//----------------------------------------
// Garage Door States
const int GARAGE_CLOSED         = 0;  // CLOSED - Door completely shut
const int GARAGE_AJAR           = 1;  // AJAR - between closed and open, could be stopped or moving
const int GARAGE_OPEN           = 2;  // OPEN - Door completely open
const char *garageStateString[] = {"CLOSED", "AJAR", "OPEN"}; // String for each state coinciding with numbers above
volatile int garageState        = GARAGE_AJAR; // default to the door AJAR - V11 Blynk

// Keep track of the time in the current state & if Open/Ajar send a notification to Blynk
// Checked in DoorOpenTooLong() function if:   
//     (now()-stateStartTime) > (MAX_OPEN_TIME+openNotifyRepeat+(allowOpenMin * 60))
time_t stateStartTime               = 0;    // start time for the current garage door state - V13 Blynk
const unsigned int MAX_OPEN_TIME    = 300;  // If the door is open longer, send a notification (300 seconds=5min)
long openNotifyRepeat               = 0;    // time counter to re-send notifications to Blynk (seconds) about door open/ajar
volatile unsigned int allowOpenMin  = 0;    // Override allows the door to be open/ajar (minutes) longer - V12 Blynk controlled

//----------------------------------------
// Blynk WiFi and Variables
// V0 - OTA off(1) and on(2) switch [Blynk Segmented "slide" switch starts at 1]
// V5 - Alarm Status (only used in Blynk app)
// V6 - Alarm Status text           [Alarm SMS->Tasker->Blynk->NodeMCU sends MQTT to Home Assistant]
// V10 - Activate the garage door (button)
// V11 - Garage Door State
// V12 - Override for number of minutes the garage door is allowed to be open
// V13 - State start time
// V14 - Garage Door Image

WidgetRTC rtc;        // Blynk Real Time Clock widget
BlynkTimer myTimer;   // Blynk Timer - single timer object can schedule up to 16 timers
int idTimerWifiDay;   // ID for the timer to normally check WiFi & Blynk connections during day
int idTimerWifiNight; // ID for the timer if Not conntected to WiFi at night (WiFi is turned off)

// Blynk Widget color definitions
#define BLYNK_GREEN     "#23C48E"
#define BLYNK_YELLOW    "#ED9D00"
#define BLYNK_RED       "#D3435C"
#define BLYNK_BLUE      "#04C0F8"
#define BLYNK_DARK_BLUE "#5F7CD8"
const char *garageStateColor[] = { BLYNK_GREEN, BLYNK_YELLOW, BLYNK_RED };

// Your WiFi credentials
const char auth[] = SECRET_BLYNK;     // Blynk Project Token
const char ssid[] = SECRET_SSID;      // Wifi Network name
const char pass[] = SECRET_SSID_PWD;  // Wifi password
int cntWifiConnect = 0;               // Counter while waiting for Wifi to connect

//----------------------------------------
// OTA variables
#define CLIENT_NAME             "GarageMC"  // For MQTT topics, MQTT Client ID, and ArduinoOTA
const char ota_pass[]           = SECRET_OTA_PWD;
const unsigned long OTA_TIMEOUT = 600000;   // Turn off OTA after 10 minutes if no update
unsigned long otaTimeout        = 0;        // End time for OTA = millis()+OTA_TIMEOUT
bool otaOn                      = false;    // set to "true" to turn on OTA updates
const int OTA_BLINK_DELAY       = 300;      // blink the leds while OTA is ON
unsigned long otaBlinkTimer     = 0;        // next led blink time

//----------------------------------------
// MQTT
const char mqtt_server[]        = "192.168.1.91"; // homeassistant.local
const int mqtt_port             = 1883;
const char mqtt_user[]          = "mqttuser";
const char mqtt_client_name[]   = CLIENT_NAME;
const char mqtt_pass[]          = SECRET_MQTT_PWD;
// topics
const char topicAlarmState[]    = "alarm/state";
const char topicCheckIn[]       = CLIENT_NAME"/checkIn";
const char topicGarageControl[] = CLIENT_NAME"/control";
const char topicGarageState[]   = CLIENT_NAME"/state";
unsigned long nextReconnect     = 0;  // millis time for next reconnect attempt
int reconnectTries              = 0;  // count of tries to reconnect to MQTT
void callback(char* topic, byte* payload, unsigned int length); // Callback function header
WiFiClient espClient;
PubSubClient client(espClient);

//----------------------------------------
// Interrupt Declarations
// ESP board needs ICACHE_RAM_ATTR included
void ICACHE_RAM_ATTR InterruptDoorClosed();
void ICACHE_RAM_ATTR InterruptDoorOpen();

/*******************************************************************************
/* SETUP - run once initialization code
/*******************************************************************************/
void setup() {
  #ifdef DEBUG
  Serial.begin(115200);
  delay(500);             // give the Serial port time to initialize
  #endif
  
  // PINS setup first
  // Setup the relay. Ensure it is set HIGH, as it is active low (we don't want the door opening on reset)
  digitalWrite(RELAY1_PIN, HIGH); // Garage Door Relay
  pinMode(RELAY1_PIN, OUTPUT);    // Garage Door Relay
  pinMode(BUZZER_PIN, OUTPUT);    // Buzzer

  // Interrupts set for the two hall sensors
  // Both Interrupts are activated on a CHANGE of state
  pinMode(HALL_CLOSED_PIN,INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(HALL_CLOSED_PIN), InterruptDoorClosed, CHANGE);
  pinMode(HALL_OPEN_PIN,INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(HALL_OPEN_PIN), InterruptDoorOpen, CHANGE);

  // LED pins to show status of Wifi
  pinMode(WIFI_ON_PIN, OUTPUT);
  pinMode(WIFI_OFF_PIN, OUTPUT);
  SetWifiLeds(false);

  // Setup Timers
  myTimer.setInterval(9000L, CheckGarageState);                       // Check garage door state every 9 seconds
  idTimerWifiDay = myTimer.setInterval(120000L, CheckConnection);     // Check WiFi & Blynk connection every 2 min (120,000)
  idTimerWifiNight = myTimer.setInterval(1260000L, CheckConnection);  // Check for WiFi connection at night, every 21 min (1,260,000)
  myTimer.disable(idTimerWifiNight);                                  // Disable the night WiFi check (Default is Day)
  setSyncInterval(60 * 60 * 6);                                       // RTC Sync interval in seconds (60 sec * 60 min * 6 hours)

  InitConnection(); // Connect to WiFi & Blynk (must be after timers setup)
  InitOTA();        // Set OTA variables

  // MQTT Setup
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);

  // Get the Intial state of the garage door based on hall sensors
  hallClosedState = digitalRead(HALL_CLOSED_PIN);
  hallOpenState = digitalRead(HALL_OPEN_PIN);
  DEBUG_PRINTLN((String)"@Setup@ ClosedState: " + hallClosedState + " - OpenState: " + hallOpenState);
  
  // We are initially in an unknown garage state, so GarageStateChanged
  // First check of the states the time is likely not properly synchronized
  myTimer.setTimeout(1000, GarageStateChanged);
}

/*******************************************************************************
/* MAIN LOOP
 *  Run Blynk and the Blynk Timer which runs the code
 */
/*******************************************************************************/
void loop() {
  // OTA - Only use if switched on from Blynk
  // if OTA, don't run blynk, timers, or mqtt
  if (otaOn) {
    ArduinoOTA.handle();
    if (millis() > otaTimeout) {  // timeout if no upload, so restart to turn off OTA Service
      ESP.restart();
    }
    // blink the red and green leds while OTA is on
    if (millis() > otaBlinkTimer) {
      SetWifiLeds(!digitalRead(WIFI_ON_PIN));
      otaBlinkTimer = millis() + OTA_BLINK_DELAY;
    }
  } else {
    if (Blynk.connected()) {Blynk.run();} // run Blynk if it's connected
    myTimer.run();                        // Blynk Events Timer (run either way)
  
    // MQTT - loop or reconnect if needed
    mqttLoop();
  }
}

/*******************************************************************************
/*******************************************************************************
/* Interrupts
/*******************************************************************************/

// Interrupt for the Hall Sensor at the Door CLOSED point
// activated on CHANGE
// Sets a timer to immediately CheckGarageState
void InterruptDoorClosed() {
  hallClosedState = digitalRead(HALL_CLOSED_PIN); // get the sensor state
  myTimer.setTimeout(10, GarageStateChanged);     // set short timer to run function immediately
}

// Interrupt for the Hall Sensor at the Door OPEN point
// activated on CHANGE
// Sets a timer to immediately CheckGarageState
void InterruptDoorOpen() {
  hallOpenState = digitalRead(HALL_OPEN_PIN);   // get the sensor state
  myTimer.setTimeout(10, GarageStateChanged);   // set short timer to run function immediately
}

/*******************************************************************************
/*******************************************************************************
/* Functions
/*******************************************************************************/
/*----------------------------------------------------
 * GarageStateChanged
 * Change in the state of the garage door (ie. door has moved)
 * Interrupts change the garageStateChange
*/
void GarageStateChanged() {
  stateStartTime = now(); // new state start time
  openNotifyRepeat = 0;   // Reset the Door Open/Ajar repeat time for new state

  // figure out the new state of the door
  if ((hallClosedState == LOW) && (hallOpenState == HIGH)) {          // Garage door is CLOSED
    garageState = GARAGE_CLOSED;
  } else if ((hallClosedState == HIGH) && (hallOpenState == LOW)) {   // Garage door is FULLY OPEN
    garageState = GARAGE_OPEN;
  } else if ((hallClosedState == HIGH) && (hallOpenState == HIGH)) {  // Garage door is AJAR or MOVING
    garageState = GARAGE_AJAR;
  } else {  // can't be in any other state, so we have an error
    Blynk.notify("!ERROR! - GarageStateChanged()");
    DEBUG_PRINTLN("!ERROR! - GarageStateChanged()");
  }

  #ifdef DEBUG
  char buff[20];
  strftime(buff, 20, "%Y-%m-%d %H:%M:%S ", localtime(&stateStartTime));
  Serial.println((String)"@GarageStateChanged@ - State Start: " + buff + 
                " - hallClosedState=" + hallClosedState + " hallOpenState=" + hallOpenState + 
                " - garageState=" + garageStateString[garageState]);
  #endif
  
  CheckGarageState();   // Update Garage State
  UpdateBlynkWidgets(); // Update Blynk app with new state
  // publish the state change using MQTT
  mqttPublish(topicGarageState, garageStateString[garageState], true);
}

/*----------------------------------------------------
 * CheckGarageState
 * Main function to check the state of the garage door
 * Run by a Blynk Timer every 10 seconds
 * also called immediately from a timer after an Interrupt
*/
void CheckGarageState() {
  unsigned int secondsInState = 0; // used to notify time door open
  String strNotify = "Door " + (String)garageStateString[garageState] + " : ";
  
  // do things based on the current garage state
  switch (garageState) {
    case GARAGE_CLOSED:
      secondsInState = now() - stateStartTime;
      DEBUG_PRINTLN(strNotify + (String)secondsInState + " seconds.");
      break;
    case GARAGE_AJAR:
    case GARAGE_OPEN:
      if (secondsInState = DoorOpenTooLong()) {
        strNotify = strNotify + (String)(secondsInState/60) + " min " + (String)(secondsInState%60) + " sec!";
        Blynk.notify(strNotify);
        DEBUG_PRINTLN(strNotify);
      }
      break;
  }
}

//----------------------------------------
// Determine if the garage door has been open for too long
// Return: Number of seconds open if longer than maxOpenTime
//         Otherwise, return zero
unsigned int DoorOpenTooLong() {
  unsigned int secondsOpen = now() - stateStartTime;                                      // How long have we been in this State
  unsigned int maxOpenTime = MAX_OPEN_TIME + openNotifyRepeat + (allowOpenMin * 60);  // Get the Maximum Open Time
  DEBUG_PRINTLN((String)"@DoorOpenTooLong@ - MaxOpenTime: " + maxOpenTime + " - Time open: " + secondsOpen);

  if (secondsOpen > maxOpenTime) {
    openNotifyRepeat += MAX_OPEN_TIME; // ensure the Blynk notify is repeated every MAX_OPEN_TIME seconds
  } else {
    secondsOpen = 0;  // return 0 = Door hasn't been open too long
  }

  // Error Case - Time not properly synchronized if seconds open
  //              is greater than 11 hours (60 sec * 60 min * 11)
  // So set the stateStartTime to now()
  // It will still send a notification until next time checked
  if (secondsOpen > 39600) {
    stateStartTime = now();
    openNotifyRepeat = 0;
    secondsOpen = 0;
    DEBUG_PRINTLN("!ERROR! DoorOpenTooLong() - Time not properly Synchronized");
  }

  return secondsOpen;
}

/*******************************************************************************
/*******************************************************************************
/* WiFi, Blynk & OTA Connections
/*******************************************************************************/

// Used for the Initial Connection
// Should only be run once from Setup
void InitConnection() {
  Blynk.config(auth); // Only needs to be done once
  CheckConnection();
}

// Called by Timers to Check the WiFi & Blynk Connection
void CheckConnection() {
  DEBUG_PRINTLN("@CheckConnection@");

  // Check the Blynk Connection First
  // If it's connected, WiFi is Connected so don't have to do anything
  if (!Blynk.connected()) {
    if (WiFi.status() != WL_CONNECTED) {
      WiFi.begin(ssid, pass);
      DEBUG_PRINT("Connecting");
    }
    cntWifiConnect = 0;
    myTimer.setTimeout(500, ConnectionTimer); // Run the Connection Timer
  }

  // Ensure that the time is set (otherwise hour is zero which may not be correct)
  // If time hasn't been set just use default day timer from SETUP
  // Time is Set, then pick which timer to use
  if (timeStatus() == timeSet) {
    int theHour = hour();
    if ((theHour == 23) || (theHour < 6)) {   // Home WiFi is turned off at NIGHT
      myTimer.disable(idTimerWifiDay);
      myTimer.enable(idTimerWifiNight);
      DEBUG_PRINTLN((String)"Night - Hour=" + theHour);
    } else {                                  // DAY
      myTimer.enable(idTimerWifiDay);
      myTimer.disable(idTimerWifiNight);
      DEBUG_PRINTLN((String)"Day - Hour=" + theHour);
    }
  }
}

// ConnectionTimer
// None blocking timer which sets up the WiFi & Blynk connection
void ConnectionTimer() {
  if (WiFi.status() != WL_CONNECTED) {  // NOT connected to Wifi
    DEBUG_PRINT(".");
    if (cntWifiConnect < 16) {  // Only try for so long
      cntWifiConnect++;
      myTimer.setTimeout(500, ConnectionTimer);
    } else {
      DEBUG_PRINTLN("@ConnectionTimer@ WiFi NOT connected");
      SetWifiLeds(false); // Turn on the Not connect LED
      WiFi.disconnect();  // Stop trying to connect
    }
  } else {                              // CONNECTED to Wifi
    DEBUG_PRINT("IP address: ");
    DEBUG_PRINTLN(WiFi.localIP());

    if (!Blynk.connected()) {           // Blynk is not connected, so try to connect
      DEBUG_PRINTLN("@ConnectionTimer@ Blynk NOT Connected");
      Blynk.connect();
    }
  }
}

// Turn on the appropriate LED based on WiFi Status
void SetWifiLeds(bool wifiOn) {
  digitalWrite(WIFI_ON_PIN, wifiOn);
  digitalWrite(WIFI_OFF_PIN, !wifiOn);
}

// Setup OTA (but not listening)
void InitOTA() {
  ArduinoOTA.setHostname(CLIENT_NAME);
  ArduinoOTA.setPasswordHash(ota_pass);

  ArduinoOTA.onStart([]() {
    DEBUG_PRINTLN("OTA Starting update");
    digitalWrite(WIFI_ON_PIN, true);
    digitalWrite(WIFI_OFF_PIN, true);
  });
  ArduinoOTA.onEnd([]() {
    DEBUG_PRINTLN("OTA Finished");
  });
}

/*******************************************************************************
/*******************************************************************************
/* Activate the Garage Door
/*******************************************************************************/

// The door plays a song prior to activating (opening/closing) the door
// So, someone could Activate the door from Blynk, then someone else use the garage controller
// To prevent two activations, we check that the door state hasn't changed from button press to actual activation
int activateState = 0;

//-------------------------------------------
// send signal to relay (garage door) to open or close
// from the Blynk Widget
void ActivateDoor() {
  DEBUG_PRINTLN("Activate Door pressed");
  activateState = garageState;                        // get the garage state when the button was pressed
  long songLength = PlaySong();                       // Play a song to notify anyone in the garage that the door is about to move
  myTimer.setTimeout(songLength, ActivateDoorRelay);  // Wait for the song to finish, then activate the relay
}

//-------------------------------------------
void ActivateDoorRelay() {
  // only trigger the relay if the state hasn't changed since the button was pressed
  if (garageState == activateState) {
    digitalWrite(RELAY1_PIN, LOW);                    // trigger the relay to open/close the door
    myTimer.setTimeout(300, ActivateDoorRelayReset);  // Timer to wait 300ms
  }
}

//-------------------------------------------
void ActivateDoorRelayReset() {
  digitalWrite(RELAY1_PIN, HIGH);                   // Deactivate the door relay
  Blynk.virtualWrite(V10, 0);                       // Reset the Blynk switch after the door is activated
  //TestButton(); // for testing, sets hall sensor states
}

//-------------------------------------------
// Test Function
// manually set the states of the Hall Sensors for testing purposes
void TestButton() {
  switch (garageState) {
    case 0: // CLOSED so change to AJAR
      hallClosedState = HIGH;
      hallOpenState = HIGH;
      break;
    case 1: // AJAR so change to OPEN
      hallClosedState = HIGH;
      hallOpenState = LOW;
      break;
    case 2: // OPEN so change to CLOSED
      hallClosedState = LOW;
      hallOpenState = HIGH;
      break;
  }
  myTimer.setTimeout(10, GarageStateChanged);   // set short timer to run function immediately
}

/*******************************************************************************
/*******************************************************************************
/* PlaySound
/*******************************************************************************/

//----------------------------------------
// Melody variables (played by piezo buzzer)
// Play a short song to let anyone in the garage know that the door is about to move
const int numNotes      = 8;                                          // number of notes
const char noteNames[]  = { 'c', 'd', 'e', 'f', 'g', 'a', 'b', 'C' }; // musical notes used
const int frequencies[] = { 262, 294, 330, 349, 392, 440, 494, 523 }; // note frequencies
const int songLength    = 17;                                  // Length equals the total notes & spaces
const char notes[]      = "cdfda ag cdfdg gf";                 // Notes to play, a space represents a rest
const int beats[]       = {1,1,1,1,1,1,4,4,2,1,1,1,1,1,1,4,4}; // Notes & rests. 1=quarter-note, 2=half-note, etc.
const int tempo         = 113;                                 // Tempo is how fast to play the song. Lower value is faster.
int iSong               = 0;                                   // Counter for current note to play

//-------------------------------------------
// - Play a sound so that people know that the garage door is about to move
// - None blocking, therefore...
// - Returns: long of the duration of the song in milliseconds
long PlaySong() {
  iSong = 0;                              // reset to first note of song
  myTimer.setTimeout(10, PlaySongTimer);  // start playing the song (none blocking)

  // Calculate the length of the song in milliseconds to return
  long duration = 0;
  for (int i = 0; i < songLength; i++) {        // step through the song arrays
    duration += (beats[i] * tempo) + (tempo/10); // length of note/rest in ms + brief pause between notes
  }
  return duration;
}

//-------------------------------------------
// Only called by timer set in PlaySong
void PlaySongTimer() {
  int duration = 0;
  if (iSong < songLength) {           // step through the song array
    duration = beats[iSong] * tempo;  // length of note/rest in ms
    if (notes[iSong] != ' ') {        // if this is not a rest, play the note
      tone(BUZZER_PIN, Frequency(notes[iSong]), duration);
    }
    iSong++;                          // increment the counter to go to the next note
    // timer to wait for tone/rest to finish + brief pause between notes, then return to function
    myTimer.setTimeout(duration+(tempo/10), PlaySongTimer);
  }
}

//-------------------------------------------
// This function takes a note character (a-g), and returns the
// corresponding frequency in Hz for the tone() function.
int Frequency(char note) {
  // Now we'll search through the letters in the array, and if
  // we find it, we'll return the frequency for that note.
  for (int i = 0; i < numNotes; i++) {    // Step through the notes
    if (noteNames[i] == note) {           // Is this the one?
      return(frequencies[i]);             // Yes! Return the frequency
    }
  }
  return(0);
}

/*******************************************************************************
/*******************************************************************************
/* MQTT Functions
 *  boolean PubSubClient::subscribe(const char* topic)
 *  boolean PubSubClient::subscribe(const char* topic, uint8_t qos)
 *  boolean PubSubClient::publish(const char* topic, const char* payload)
 *  boolean PubSubClient::publish(const char* topic, const char* payload, boolean retained)
 *  
 *  // Possible values for client.state()
 *  #define MQTT_CONNECTION_TIMEOUT     -4
 *  #define MQTT_CONNECTION_LOST        -3
 *  #define MQTT_CONNECT_FAILED         -2
 *  #define MQTT_DISCONNECTED           -1
 *  #define MQTT_CONNECTED               0
 *  #define MQTT_CONNECT_BAD_PROTOCOL    1
 *  #define MQTT_CONNECT_BAD_CLIENT_ID   2
 *  #define MQTT_CONNECT_UNAVAILABLE     3
 *  #define MQTT_CONNECT_BAD_CREDENTIALS 4
 *  #define MQTT_CONNECT_UNAUTHORIZED    5
 *  
 */
/*******************************************************************************/

// MQTT Loop
// If connected, the MQTT client checks for connections
// If Not connected, reconnect to MQTT
void mqttLoop() {
  if (client.connected()) {
    client.loop();
  } 
  else { // MQTT non-blocking reconnect
    unsigned long nowMillis = millis();
    if (nowMillis > nextReconnect) {
      if (!mqttReconnect()) {                   // Unable to Reconnect to MQTT broker
        DEBUG_PRINT("MQTT Reconnect Failed=");
        DEBUG_PRINT(client.state());
        DEBUG_PRINTLN(" - Retry in 12 seconds");
        nextReconnect = nowMillis + 12000;
        reconnectTries++;
        if (reconnectTries >= 10) {             // try for 2 minutes
          nextReconnect = nowMillis + 3600000;  // try again in 1 hour, and every hour
          reconnectTries = 0;                   // reset attempts
          //ESP.restart();
        }
      } else {  // Successfull reconnect
        nextReconnect = 0;
        reconnectTries = 0;
      }
    }  
  }
}

// Reconnect - Non-blocking
boolean mqttReconnect() {
  DEBUG_PRINTLN((String)"Name: " + mqtt_client_name + " User: " + mqtt_user + " Pass: " + mqtt_pass);
  if (client.connect(mqtt_client_name, mqtt_user, mqtt_pass)) {
    DEBUG_PRINTLN(CLIENT_NAME" Connected to MQTT");
    client.publish(topicCheckIn, "Reconnected");    // Once connected, publish an announcement...
    client.subscribe(topicGarageControl);           // ... and resubscribe
  }
  return client.connected();
}

// callback - handle messages received from the broker
void callback(char* topic, byte* payload, unsigned int length) {
  DEBUG_PRINTLN((String)"Message arrived [" + topic + "] ");

  payload[length] = '\0';
  char *cstring = (char *) payload;
  DEBUG_PRINTLN((String)"Payload: " + cstring);

  // do actions based on the topic & payload
  String newTopic = topic;
  String newPayload = String((char *)payload);
  if (newTopic == topicGarageControl) {
    if (newPayload == "ACTIVATE") {
      // activate the garage door
      ActivateDoor();
      DEBUG_PRINTLN("MQTT activated the garage door");
    }
  }
  else {
    DEBUG_PRINTLN("Unknown Topic");
  }
}

bool mqttPublish(String topic, String payload, bool retain) {
  if (client.connected()) {
    DEBUG_PRINTLN("MQTT Publish: " + topic + " - " + payload);
    client.publish(topic.c_str(), payload.c_str(), retain);
  } else {
    DEBUG_PRINTLN("MQTT Publish - NO CONNECTION");
  }
}


/*******************************************************************************
/*******************************************************************************
/* Blynk Functions
/*******************************************************************************/

// BLYNK_CONNECTED is done when connected to the Blynk server
BLYNK_CONNECTED() {
  rtc.begin();                      // Synchronize RTC time on Blynk connection
  Blynk.virtualWrite(V0, otaOn+1);  // Initialize Blynk slide switch to "OTA Off"
  Blynk.virtualWrite(V12, 0);       // Initialize the open override to zero in Blynk app
  SetWifiLeds(true);                // Set LED to show WiFi & Blynk connected
  myTimer.setTimeout(1000, GarageStateChanged);
}

//----------------------------------------
// Blynk OTA switch changed
// Off=1; On=2
BLYNK_WRITE(V0) {
  otaOn = param.asInt() - 1;
  DEBUG_PRINTLN((String)"V0: " + otaOn);

  if (otaOn) {                            // OTA turned ON
    otaTimeout = millis() + OTA_TIMEOUT;  // Timeout end time
    ArduinoOTA.begin();                   // Start the OTA service
  } else {                                // OTA turned OFF
    ESP.restart();                        // Restart to turn stop ArduinoOTA service
  }
}

//----------------------------------------
// Receive Alarm Status
// Alarm sends SMS to phone -> Tasker sends HTTP Request to Blynk
// Status then sent to Home Assistant using MQTT
BLYNK_WRITE(V6) {
  mqttPublish(topicAlarmState, param.asStr(), true);
}

//----------------------------------------
// Blynk Switch Widget pressed
// Activate the garage door using the relay
//
// IFTTT sends a "1" to the Blynk button widget to trigger the door
// it has to be reset to "0", not sure how to do multiple actions through IFTTT
BLYNK_WRITE(V10) {
  if (param.asInt()) {  // only do it on button "rising=1"
    ActivateDoor();     // trigger the door
  }
}

//----------------------------------------
// Blynk slider widget changed
// Sets the number of minutes the door is allowed to be open
// until the app starts sending messages again
BLYNK_WRITE(V12) {
  allowOpenMin = param.asInt();
}

//----------------------------------------
// Update information in the Blynk widgets
void UpdateBlynkWidgets() {
  if (!Blynk.connected()) {   // Blynk isn't connected, so don't do anything
    return;
  }

  if (garageState == GARAGE_CLOSED) {
    Blynk.virtualWrite(V12, 0); // garage is closed so reset the open override to zero
  }

  // Update the Garage State Widget with text & color
  Blynk.virtualWrite(V11, garageStateString[garageState]);
  Blynk.setProperty(V11, "color", garageStateColor[garageState]);

  // Update the Garage Image
  Blynk.virtualWrite(V14, garageState+1);

  // Tell Blynk the State Start Time as a String
  char strStateTime[20];
  strftime(strStateTime, 10, "%a %H:%M ", localtime(&stateStartTime));
  Blynk.virtualWrite(V13, strStateTime);
}

Credits

Craig Gibson

Craig Gibson

2 projects • 1 follower

Comments

Add projectSign up / Login