Wi-Fi Remote Garage Opener

Status Operational
Platform Arduino ESP8266
Budget $25
Date Completed December 2019

Summary of operation

Ever wanted to remotely open your garage door from your phone or computer?

Want to interface your garage door controller with a home control solution like Home Assistant/Hass.io?

Want to know if you've accidentally left the garage open for longer than 15 minutes?

Most garage door controllers have the ability to accept an external hard-wired push button.

The idea here is to use these terminals on the door controller to connect a WiFi-enabled microcontroller via a small relay for electrical isolation.

Parts used

  • Garage door operator/controller with terminals for a hard-wired remote push button. In my case, the door is a Marantec Comfort 270.
  • 3D printed bracket to fit the roller door channel to mount both the position switch and the controller board.
  • I've used the "LC Technology" 1CH ESP-8266 based single-channel relay. Available from eBay, Amazon and similar. Otherwise you can go for a WiFi-enabled microcontroller such as an ESP8266 "Wemos D1 Mini" or similar. Such as https://www.jaycar.com.au/wifi-mini-esp8266-main-board/p/XC3802 with a small relay board to electrically separate the ESP8266's output from the garage door operator's interface terminals. Such as https://www.jaycar.com.au/arduino-compatible-5v-relay-board/p/XC4419 which will work with a 3.3V digital output signal from the ESP.
  • Power supply for the microcontroller such as USB iPhone charger or DC-DC to harness power from the garage controller such as https://www.jaycar.com.au/twin-usb-panel-mount-outlet-5v-3-1a/p/MP3618 or a basic eBay-special DC-DC step-down converter.

Some engineering detail

Basically the wiring is as follows:

  • 24VDC power supply comes from Pin 1 (GND) and Pin 3 (+24VDC) of the XB03 terminals on the garage door motor unit. This is converted to USB by the adapter above to power the D1 Mini. Lucky it's DC - an older garage door controller I had was AC... that would have required more messing about to rectify etc.
  • The D1 output of the D1 Mini is connected to the signal input of the small relay board. The relay board 5V and GND pins are connected to the corresponding 5V and GND pins on the D1 Mini. Note that although the D1 output pin is 3.3V only, this is enough to drive the NPN transistor to switch the relay.
  • Be careful not to use an output on the D1 Mini that goes high during boot. You don't want to come home to find your garage door open due to a power outage! See https://randomnerdtutorials.com/esp8266-pinout-reference-gpios/ for more info.
  • The relay board "normally open" output is wired to GND and Pin 1 (impulse button input) pins on the XB03 terminals on the garage door motor to remotely trigger it.

My first attempt was setup to work with Tasmota:

  • Flashed with Tasmota using Tasmotizer.
  • Set Relay 1 to be connected to D1 output in the setup.
  • Use command "PulseTime1 10" in the Tasmota console to make relay 1 momentary for 1 second when it's activated.
  • The code on the D1 Mini acts as a web server. Once connected to the Wi-Fi, visiting "http://<ip address>/" from an iPhone or similar to use a "toggle" button to open/close the door.
  • Use the Tasmota integration for Home Assistant to make it available remotely!

I've later migrated to ESPHome with the  "LC Technology" 1CH ESP-8266 based single-channel relay which I just find easier and works better with Home Assistant out of the box. The benefit of ESPHome is I've been able to make it use a "cover" control in Home Assistant based on the work done here but modified for only a single reed switch with the ESPHome yaml code:

# thanks to https://github.com/juaigl/esphome-garage-cover-single-control
esphome:
  name: esp-garage-door
  on_boot:
    priority: -200
    then:
      - wait_until:
          wifi.connected:
      - delay: 2s
      - lambda: |-
          id(new_door_state) = id(garage_closed).state ? 2 : 1;
      - script.execute: set_door_state
      - script.execute: publish_door_state

esp8266:
  board: esp01_1m

# Enable logging
logger:
  baud_rate: 0 #need this to free up UART pins

# Enable Home Assistant API
api:

ota:
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esp-Garage-Door Fallback Hotspot"
    password: !secret fallback_password

captive_portal:

# Example configuration entry
web_server:
  port: 80

uart:
  baud_rate: 9600 # speed to STC15L101EW
  tx_pin: GPIO1
  rx_pin: GPIO3

globals:
  - id: door_state
    type: int
    restore_value: no
    # STOPED = 0, OPENED = 1, CLOSED = 2, OPENING = 3, CLOSING = 4
    initial_value: "0"
  - id: last_door_state
    type: int
    restore_value: no
    # STOPED = 0, OPENED = 1, CLOSED = 2, OPENING = 3, CLOSING = 4
    initial_value: "-1"
  - id: new_door_state
    type: int
    restore_value: no
    # STOPED = 0, OPENED = 1, CLOSED = 2, OPENING = 3, CLOSING = 4
    initial_value: "-1"
  - id: door_duration
    type: float
    restore_value: no
    initial_value: "20000.0"
    
binary_sensor:
  - platform: gpio
    pin:
      number: GPIO2
      inverted: False
      mode:
        input: True
        pullup: True
    id: garage_closed
#    internal: True
    name: "Garage Door Closed Switch"
    # filters:
    #   - delayed_on_off: 20ms
    on_release: # door externally closed
      then:
        - script.stop: garage_door_timer
        - lambda: !lambda |-
            id(new_door_state) = 3; // OPENING
        - script.execute: set_door_state
        - script.execute: publish_door_state
        - script.execute: garage_door_timer
    on_press: # door externally opened
      then:
        - script.stop: garage_door_timer
        - lambda: !lambda |-
            id(new_door_state) = 2; // CLOSED
        - script.execute: set_door_state
        - script.execute: publish_door_state
  - platform: template
    name: "Garage Door"
    device_class: garage_door
    lambda: !lambda |-
      return !id(garage_closed).state;
      
switch:
  - platform: uart
    #name: "Garage Door Toggle"
    internal: true
    id: A1on
    data: [0xA0, 0x01, 0x01, 0xA2]
    on_turn_on:
    - delay: 500ms
    - switch.turn_on: A1off

  - platform: uart
    internal: true
    #name: "A1off"
    id: A1off
    data: [0xA0, 0x01, 0x00, 0xA1]
    
  - platform: template
    icon: "mdi:arrow-up-down-bold-outline"
    name: "Garage Control"
    id: garage_control
    lambda: 'return id(A1on).state;'
    turn_on_action:
      - lambda: !lambda |-
          if (id(door_state) == 0) // when door stopped
          {
            if (id(last_door_state) == 3) // when was opening
            {
              id(new_door_state) = 4; // CLOSING
            }
            else if (id(last_door_state) == 4) // when was closing
            {
              id(new_door_state) = 3; // OPENING
            }
          }
          else if (id(door_state) == 1) // when door opened
          {
            id(new_door_state) = 4; // CLOSING
          }
          else if (id(door_state) == 2) // when door closed
          {
            id(new_door_state) = 3; // OPENING
          }
          else // when opening or closing
          {
            id(new_door_state) = 0; // STOPPED
          }
      - switch.turn_on: A1on
      - script.execute: set_door_state
      - script.execute: publish_door_state
      - script.execute: garage_door_timer

text_sensor:
  - platform: template
    name: "Garage Door State"
    id: door_state_text

cover:
  - platform: template
    name: "Garage Door Cover"
    id: garage_door_cover
    device_class: garage
    optimistic: False
    has_position: True
    assumed_state: False
    open_action:
      - switch.turn_on: garage_control
    close_action:
      - switch.turn_on: garage_control
    stop_action:
      - switch.turn_on: garage_control
    lambda: |-
      static uint32_t last_recompute_time = 0;
      static uint32_t last_publish_time = 0;
      static uint8_t calculated_current_operation = -1;
      
      // Guard that the door is closed or in idle state then do not calculate position
      // Tree hazard checks
      if (id(garage_closed).state == 1 // Door closed
          || id(garage_door_cover).current_operation == COVER_OPERATION_IDLE
          || id(garage_door_timer).is_running() == false) 
      {
        calculated_current_operation = -1;
        return {};
      }
      // Safety check do distinguish direction change
      if (calculated_current_operation != id(garage_door_cover).current_operation)
      {
        last_recompute_time = millis();
        last_publish_time = millis();
        calculated_current_operation = id(garage_door_cover).current_operation;
      }
      // set dir and duration depending on current movement
      float dir = (id(garage_door_cover).current_operation == COVER_OPERATION_CLOSING) ? -1.0f : 1.0f; 
      // calculate position
      float position = id(garage_door_cover).position;
      position += dir * (millis() - last_recompute_time) / id(door_duration);
      id(garage_door_cover).position = clamp(position, 0.0f, 1.0f);
      // publish position every second
      if (millis() - last_publish_time > 1000)
      {
        id(garage_door_cover).publish_state();
        last_publish_time = millis();
      }
      last_recompute_time = millis();
      return {};
script:
  - id: set_door_state
    mode: "single"
    then:
      - lambda: !lambda |-
          if (id(new_door_state) < 0) 
          {
            return;
          }
          id(last_door_state) = id(door_state);
          id(door_state) = id(new_door_state);
          id(new_door_state) = -1;
  - id: garage_door_timer
    mode: "single"
    then:
      - delay: 20s
      - lambda: !lambda |-
          // when was opening
          // simulated opened state based on door duration
          if (id(door_state) == 3)
          {
            id(new_door_state) = 1; // OPENED
          }
          if (id(door_state) == 4)
          {
            id(new_door_state) = 0; // STOPPED
          }
          if (id(garage_closed).state)
          {
            // reed switch has the biggest priority
            // when the door really closed then set the state
            id(new_door_state) = 2; // CLOSED
          }
      - script.execute: set_door_state
      - script.execute: publish_door_state
  - id: publish_door_state
    mode: "single"
    then:
      - lambda: !lambda |-
          switch (id(door_state))
          {
            case 0: // STOPPED
              id(door_state_text).publish_state("Stopped");
              id(garage_door_cover).current_operation = COVER_OPERATION_IDLE;
              id(garage_door_cover).position = 0.5;
              id(garage_door_cover).publish_state();
              break;
            case 1: // OPENED
              id(door_state_text).publish_state("Opened");
              id(garage_door_cover).current_operation = COVER_OPERATION_IDLE;
              id(garage_door_cover).position = COVER_OPEN;
              id(garage_door_cover).publish_state();
              break;
            case 2: // CLOSED
              id(door_state_text).publish_state("Closed");
              id(garage_door_cover).current_operation = COVER_OPERATION_IDLE;
              id(garage_door_cover).position = COVER_CLOSED;
              id(garage_door_cover).publish_state();
              break;
            case 3: // OPENING
              id(door_state_text).publish_state("Opening");
              id(garage_door_cover).current_operation = COVER_OPERATION_OPENING;
              id(garage_door_cover).position = 0.0;
              id(garage_door_cover).publish_state();
              break;
            case 4: // CLOSING
              id(door_state_text).publish_state("Closing");
              id(garage_door_cover).current_operation = COVER_OPERATION_CLOSING;
              id(garage_door_cover).position = 1.0;
              id(garage_door_cover).publish_state();
              break;
          }

Future improvements

I've used a microswitch to give basic position - i.e. it only knows when the door is properly closed (important!)

Further enhancement could be to use a hall effect sensor or an additional reed switch to count motor/cog revolutions to determine (via dead reckoning) the current door position. That would allow you to send it opening commands with a percentage - e.g. open 10% or open 50%.

The above would allow the position to be returned to your home automation platform e.g. Home Assistant/hass.io.