I built the Garage MC because I wanted to learn more about electronics. As a bonus, I figured it would save my butt the next time I went out and left the garage door open.
*I added an update on 2021-01-06 with OTA updates & MQTT to Home Assistant (detailed below)*
My Goals- The ability to Monitor the state of the garage door (closed/ajar/open).
- The ability to Control (open/close) the garage door with my Android cell phone (and my wife with her Apple cell phone).
- Learn something new.
- Not spend a lot of money.
Prior to this project I purchased an Arduino compatible Starter Kit from Amazon. So I was a beginner with electronics and just learning the ropes. But I did have the benefit of doing some computer programming 15 years prior, so I was able to quickly pick up coding C++ on the Arduino IDE.
Garage Door Opener: First, I had to determine if I could easily interface a microcontroller with my Liftmaster garage door opener. Luckily, the Liftmaster is operated with two wires in a Normally-Open (think an open switch) state connected back to the wall remote. When the button is pressed on the wall remote, it briefly completes the circuit by "shorting" the wires and triggering the door to open or close. Ohhh, don't forget to check the voltage on those wires...the Liftmaster manual says the lines are approximately 20 volts (not 120 volts), but your unit may be different.
Microcontroller: Next, I had to decide how to connect the Garage MC to the internet. I initially thought about using an ENC28J60 Ethernet Board, and although I prefer hard-wired connections, running the line to my router would be more trouble than it's worth. I researched wifi boards and decided on a NodeMCU ESP8266 due to their low cost and having multiple GPIO pins.
Phone App: There are multiple ways to connect to the NodeMCU with your phone including through SMS messages, using the NodeMCU as a web server, etc. For me, using the Blynk app was the simplest and quickest way to get up and running.
Monitoring of the Garage Door: I decided to use two Hall Effect Sensors to monitor the position of the garage door. It may have been possible to get the door position and even direction of travel from the Liftmaster unit. But, with the off chance that someone pulls the emergency release cord and manually opens the door, the sensors will still notify me that the door is open.
Step 2 : DevelopmentNodeMCU: I started by plugging the NodeMCU into a breadboard and powering it through a USB connection to my computer. The link below has a good tutorial for the initial setup of the NodeMCU on the Arduino IDE: https://www.instructables.com/How-to-Program-NodeMCU-on-Arduino-IDE/
Blynk: Next, I connected the NodeMCU to the internet and the Blynk app on my phone. I used the Blynk tutorial to get started: https://blynk.io/en/getting-started
Breadboarding: Time to add the components. Although I made the Fritzing circuit diagram after putting on all the components, I added it here so it could be more easily referenced...
Project Code: I've attached the complete code for my project. I tried to comment it well, but if there's anything you don't understand, or I can improve on, let me know. I've added small code snippets in the tutorial to emphasize some things, such as:
- I made my code non-blocking. Meaning it has no "delay()" statements (okay there's one in the setup to wait for the serial port). I instead relied on Blynk timers, which are based on the "SimpleTimer" library.
First the Relay (Controller): The relay I used is a 1-channel relay that is triggered "LOW" to activate. The relay requires 5 volts, so I used the VIN pin on the NodeMCU to power it. The relay serves two purposes.
- The first is to Activate the garage door opener. This is done by connecting one wire from the garage door opener to the common (COM) pin on the relay. And the second wire to the N/O (Normally Open) pin on the relay. When the relay is triggered (for 300 milliseconds), the circuit is closed.
- The second purpose is to protect the NodeMCU from the 20 volt wires of the garage door opener, since the digital pins on the NodeMCU are tolerant to only 3.3 volts.
I used two functions to trigger my relay, making it non-blocking. When a button is pressed in my Blynk app, it calls a function to activate the door. Note that it's important to "ActivateDoor()" only when the button is pressed (when rising=1), not when the button is released or you'll activate the door twice. The ActivateDoorRelay function sets the relay pin "LOW", then sets a timer for 300 milliseconds, instead of using "delay()". The timer then triggers the second function to reset the pin to "HIGH" to turn off the relay. The reason I'm checking that the state hasn't changed between when the button is pressed and the door is activated is because a song is played to warn anyone in the garage that the door is about to move (someone could press the Liftmaster wall remote button during the song activating the door).
BLYNK_WRITE(V10) {
if (param.asInt()) { // only do it on button "rising=1"
ActivateDoor(); // trigger the door
}
}
void ActivateDoor() {
activateState = garageState; // get garage state when the button was pressed
long songLength = PlaySong(); // Play a song
// Wait for the song to finish, then activate the relay
myTimer.setTimeout(songLength, ActivateDoorRelay);
}
void ActivateDoorRelay() {
// only trigger the relay if the state hasn't changed since the button press
if (garageState == activateState) {
digitalWrite(RELAY1_PIN, LOW); // trigger the relay to activate the door
myTimer.setTimeout(300, ActivateDoorRelayReset); // Wait 300ms
}
}
void ActivateDoorRelayReset() {
digitalWrite(RELAY1_PIN, HIGH); // Deactivate the door relay
Blynk.virtualWrite(V10, 0); // Reset the Blynk app switch
}
Hall Effect Sensors (Monitors): The Hall Effect Sensors monitor the position of the garage door. The sensors are non-latching and are digital (it either does or doesn't sense a magnet; an analog output sensor would measure the magnet's strength). I used two neodymium magnets (together to make them stronger) on the garage door to trigger the sensors. I had some CAT6 cable, so I ran it from the NodeMCU on the garage ceiling to each Hall Sensor. The garage door can be in one of three states (since I don't care which direction the door is moving):
- Closed: The Hall Effect Sensor at the door closed position is activated by the magnets.
- Open: The Hall Effect Sensor at the door open position is activated by the magnets.
- Ajar: Neither Hall Effect Sensor is activated. So the door could be moving or stopped in an open position.
I don't like polling for events to happen. So, the Hall Sensors use "Interrupts" to indicate a change of state (ex. door closed to moving). An Interrupt is exactly that, when there is a state change with a Hall Sensor, it interrupts the program code to immediately run a short function.
// Interrupt Declarations
// ESP boards need ICACHE_RAM_ATTR included
void ICACHE_RAM_ATTR InterruptDoorClosed();
void ICACHE_RAM_ATTR InterruptDoorOpen();
// this is only a snippet from the "setup" function
void setup() {
// Interrupts set for the two hall sensors activated on a CHANGE of state
// the Hall sensors require a pullup resistor, I used the internal pullup
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);
}
// Interrupt for the Hall Sensor at the Door CLOSED point
// activated on CHANGE and Sets a timer to immediately CheckGarageState
void InterruptDoorClosed() {
hallClosedState = digitalRead(HALL_CLOSED_PIN); // get the sensor state
myTimer.setTimeout(10, GarageStateChanged); // run the function in 10ms
}
// Interrupt for the Hall Sensor at the Door OPEN point
// activated on CHANGE and Sets a timer to immediately CheckGarageState
void InterruptDoorOpen() {
hallOpenState = digitalRead(HALL_OPEN_PIN); // get the sensor state
myTimer.setTimeout(10, GarageStateChanged); // run the function in 10ms
}
Passive Piezo Buzzer: When the door is remotely activated (using the Blynk app), I wanted to alert anyone in the garage that the door was about to move. So I added a buzzer to play a short song. I added a 100 ohm resister to the circuit, although I'm not sure it's necessary. I also made the song non-blocking. Even though this requires me to get the length of the song so I don't activate the door until it completes.
LED's: Finally, I added two LED's, one green and one red. The LED's each require a 220 ohm resistor in their circuit. The green LED indicates that the Garage MC is connected to the Blynk server. And the red LED indicates that the Garage MC has lost it's connection.
My Programming Flow: If you normally don't use timers in your code, the flow of my program may be difficult to grasp. It basically works like this:
- The setup() function sets timers to:1. Check the internet connection.2. Check the state of the garage door (CheckGarageState), which updates the Blynk app and sends Notifications to Blynk if the door is open for too long.
- The loop() function keeps the timers running.
- When there is a change in the state of the garage door (closed to open), an Interrupt is triggered. The interrupts flag a state change then set an immediate timer (10 milliseconds) to check the state of the garage door (CheckGarageState), which updates the Blynk app.
I wanted the Garage MC to look semi-professional, so I decided to mount all the components on a Perfboard (individual copper pads with pre-drilled holes). With that I had to learn how to solder. Which isn't necessarily difficult, but can be difficult to do well.
The Case: A 3D printer is on my son's Christmas list. Hopefully Santa comes through because I'd like to use it as well. As it turned out I had an old Chamberlain myQ (now "Merlin") that I took apart, cut and drilled holes in, and turned it into a project case. I think it turned out okay, even with the relay sticking out the bottom.
Perfboard: Based on the size of the case, I selected a perfboard that would fit in the case, and almost fit the NodeMCU. As you can see in my diagram, the top pin on each side wouldn't fit (D0 and A0), but I wasn't using them anyways. I didn't remove them, they just aren't connected to anything.
I soldered two header pins on the Perfboard (16 pins long, but the NodeMCU has 15 pins on each side). So, the NodeMCU goes on the bottom 15 pins. I mainly soldered connection wires on the bottom of the Perfboard (with a couple of exceptions), and components on top. And to shorten a long story...
And mounted it in my garage above the garage door opener (the yellow CAT6 cable runs to the Hall Sensors by the door track)...
Power: There is an electrical outlet in the ceiling for the Liftmaster opener. So I decided to keep things simple. I plugged in an old Blackberry cell phone charger and used a old USB cable to power the Garage MC.
What would I do different?I'm actually pretty happy with my final result. But there are a couple of changes I'd make:
- I placed the screw connectors for the Hall Effects Sensors facing each other, thinking I was smart and saving space. This made it difficult to perfectly bend the wires of the CAT6 cable into position. I'd definitely take the straight ahead approach next time, and put the connectors side-by-side.
- DONE - I'm considering adding the ability to update my code Over The Air (OTA) so I don't have to get my ladder out to make changes.
Over the Covid stay at home Christmas break I started learning and setting up Home Assistant. As a result, I decided that I wanted the Garage MC to report back and be controlled by Home Assistant. Because of that, I decided to make the following changes to my code: adding OTA updates, and adding MQTT to communicate with Home Assistant.
OTA Updates
This was the first update to the Garage MC that I've made since October because frankly, it didn't need one. But, climbing the ladder to do updates was a pain so I implemented OTA updates.
I added a switch to my Blynk App in order to turn on and off OTA updates because I didn't want it always running. Next, I added the #include <ArduinoOTA.h> library.
I added a MD5 hashed password, a timeout (to turn off the update process in case I forgot), and made the LED's blink so I'd know the GarageMC was in "update mode". Global variables as follows:
// 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 min. 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
In setup(), I initialize OTA as follows:
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");
});
My main LOOP was updated so that if OTA updates was turned on, everything else would stop (I found the update would be slow or fail if Blynk was still running).
if (otaOn) {
ArduinoOTA.handle();
if (millis() > otaTimeout) { // timeout if no upload, then restart
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();
}
Finally, when I flip the switch in the Blynk app it sends a message to a virtual pin, V0 (off=1 and on =2, but I wanted it to be a boolean):
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
}
}
That all that's needed to handle OTA updates from the Arduino IDE. I like that fact that it's not running unless I take action by flipping the switch in Blynk.
MQTT to Home Assistant
I won't go into Home Assistant, because I'm just learning it. But I'd suggest searching YouTube for JuanMTech. And I just barely understand MQTT, so won't be much help there either. I used the popular #include <PubSubClient.h> library. The details can be found in my full code. But basically, as a client you connect to the MQTT broker on Home Assistant. You "publish" updates to the MQTT broker, and "subscribe" to topics that you want to receive updates on through a "callback" function.
In it's simplest, connect to the MQTT broker with:
// subscribe to the control topic which can "activate" the garage door (open/close)
const char topicGarageControl[] = CLIENT_NAME"/control";
// publish to the state topics, the state of the door
const char topicGarageState[] = CLIENT_NAME"/state";
WiFiClient espClient;
PubSubClient client(espClient); // client for connection to MQTT broker
client.setServer(mqtt_server, mqtt_port); // server to connect to
client.setCallback(callback); // the "callback" function for subscribed topics
client.connect(mqtt_client_name, mqtt_user, mqtt_pass); // connect to MQTT broker
// after connecting, subscribe to topics you want
client.subscribe(topicGarageControl);
When the garage door state changes (open->ajar->closed), I publish the change to Home Assistant:
// topic = CLIENT_NAME/state
// payload = door state (open/ajar/closed)
client.publish(topic.c_str(), payload.c_str(), retain);
So far, so good. Now I just have to figure out Home Assistant to do something with the data!
Final CommentsI intentionally didn't go into specific details on setting up the Blynk app and controls. And although I added schematics, I also didn't go into specific details on my production wiring, etc. I believe we learn more by trying to it ourselves. With that being said, if your stuck or need help to just get the project working (or wondering why I've done something), send me a message and I'll be glad to help.
I appreciate any feedback and hope you liked my project.
Comments