Wifi-enabling a gate the complete way

Photo by Zan on Unsplash

Wifi-enabling a gate the complete way

The why

There are products on the market to automate gates. I especially like Remootio. I have however been yearning for some DIY electronics action so I took it upon myself to DIY something similar that could not only open and close my sliding gate, but what would tell me if the gate is open or closed.

The how

I have a Centurion gate motor; they are a popular gate motor supplier in South Africa and they also have their own lineup of home automation products.

The content :)

Initial concept

I've had a simple eWeLink relay in the gate box for a while now, which can help me open and close it remotely. This is very useful when a courier decides to stop by as soon as I go out to the shops. The problem with this is that I have no way of verifying that the gate is actually closed after they leave, other than asking them. Two weeks ago, I noticed that my gate control board has a status LED output, which mirrors the Status LED on the control board itself. Upon some nerdy doc reading, I discovered that this light (and thus output), except for the obvious rudimentary error checking, also indicates the gate status as follows:

  • Open: Solid on

  • Opening: flashing

  • Closing: flashing

  • Closed: off

I immediately started looking for solutions as to how to use this somehow.

And action

I recently flashed my first Sonoff board with ESPHome and I fell in love instantly. First off, it integrates into Home Assistant (I have it running in a cloud instance) with a single click and the YAML OTA is extremely convenient for updating the configuration.

When dealing with the status light, one of my primary concerns was regarding how to check if the light is just flashing or whether it's staying on or off. It's not ideal to watch a gate icon flash between open and closed for 30 seconds, wondering whether the gate is actually closing or not. I trawled ESPHome's documentation and eventually saw a line that made my heart skip a beat.

For example, you can measure if a status LED of a pool controller is permanently active (indicating that the pump is on) or blinking.

It raised a couple of questions for me though, mostly regarding the time frame of the state checks. I decided to power through anyway and headed out to buy a NodeMCU dev board.

The proof of concept worked flawlessly. I built a simple voltage divider with some resistors I had lying around and it worked. The final voltage was actually around 1V (3.3V logic) but it still seems to trigger it just fine.

The installation

I headed out to my gate, armed with a NodeMCU hanging onto a breadboard for dear life, and started connecting it up. The dreaded moment of truth finally arrived, but instead of confirming my genius by spluttering to life, nothing happened. After a lot of troubleshooting, I finally figured out that my gate controller outputs about 16V on the 12V port! Not ideal for the cheap YwRobot board I had repurposed to deliver safe 3.3V to my NodeMCU.

Back to the drawing board

Not to be outdone by a simple voltage spike, I headed back to the interwebs in search of a better solution and there it was! A Sonoff SV.

Sonoff SV

The Sonoff SV is a simple board which is designed to handle 5-24V; perfect for my rebellious gate controller. As a bonus, it exposes 3 GPIO pins right on the board!

The installation v2

It turns out that the Sonoff SV is intended to be used to power DC devices, but luckily, this specific board is extremely hacker-friendly.

Pop out two relays and pretend to remove the jumper that wasn't included with my specific board and you now have a relay that is unpowered, perfect for triggering something. Of course, there is a but though; there is always a but. The relay is unpowered but one of the two sides needs to be connected to complete the gate trigger circuit.

Also, because I like doing things the right way, I busted out my soldering iron. Now, even calling my soldering skills decent is a pretty long shot, but I managed to bridge the gap and while I was at it I soldered a jumper wire onto the board (the relay inputs do not have headers.

The aftermath

HASS view of my gate device

At the time of writing this, ESPHome does not have the "gate" device class as per Home Assistant's documentation, but I have just submitted a PR to fix it :) https://github.com/esphome/esphome/pull/1175 (also my first contribution to ESPHome).

Depending on how well this post does, I will show off my horrible soldering skills and I'll add some more information regarding how I installed it in the motor enclosure.

Thanks for reading :)

The esphome YAML file

esphome:
  name: gate
  platform: ESP8266
  board: esp8285

wifi:
  ssid: "*** IoT"
  password: "****"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Gate Fallback Hotspot"
    password: "supersecure"

captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

web_server:
  port: 80

switch:
  - platform: gpio
    pin: GPIO12
    id: relay1
    internal: true
  - platform: template
    name: "Gate Trigger"
    id: rTrg
    internal: true
    lambda: |-
      return false;
    turn_on_action:
      - switch.turn_on: relay1
      - delay: 100ms
      - switch.turn_off: relay1

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO0
      mode: INPUT_PULLUP
      inverted: True
    name: "On-PCB Trigger"
    internal: true
    on_press:
      - switch.turn_on: rTrg

sensor:
  - platform: duty_cycle
    pin: 14 #same as 4
    name: "Status Duty Cycle"
    id: status_duty
    update_interval: 1s
    filters:
      - delta: 2.0
  - platform: pulse_counter
    pin: 4 # same as 14
    name: "Status Pulse Counter"
    update_interval: 2s
    id: status_pulse
    filters:
      - delta: 2.0
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
    update_interval: 60s
    filters:
      - sliding_window_moving_average:
          window_size: 10
          send_every: 15

cover:
  - platform: template
    device_class: gate
    id: gateCover
    name: "Gate"
    lambda: |-

      if (id(status_duty).state > 80) {
        id(gateCover).current_operation = COVER_OPERATION_IDLE;
        return COVER_OPEN;
      } else if (id(status_duty).state < 5) {
        id(gateCover).current_operation = COVER_OPERATION_IDLE;
        return COVER_CLOSED;
      } else {
        if (id(status_pulse).state > 60 && id(status_pulse).state < 90) {
          id(gateCover).current_operation = COVER_OPERATION_OPENING;
        } else if (id(status_pulse).state > 230 && id(status_pulse).state < 290) {
          id(gateCover).current_operation = COVER_OPERATION_CLOSING;
        }
        return {};
      }
    open_action:
      - if:
          condition:
            lambda: |-
              return id(gateCover).state != COVER_OPEN;
          then:
            - switch.turn_on: rTrg
            - cover.template.publish:
                id: gateCover
                current_operation: OPENING
          else:
            - logger.log: "Gate is already open!"
    close_action:
      - if:
          condition:
            lambda: |-
              return id(gateCover).state != COVER_CLOSED;
          then:
            - switch.turn_on: rTrg
            - cover.template.publish:
                id: gateCover
                current_operation: CLOSING
          else:
            - logger.log:
                "Gate is in secure state. Cannot perform unsecure
                op(close_action)"
    stop_action:
      - if:
          condition:
            lambda: |-
              return id(gateCover).current_operation != COVER_OPERATION_IDLE;
          then:
            - switch.turn_on: rTrg
            - cover.template.publish:
                id: gateCover
                current_operation: IDLE
          else:
            - logger.log:
                "Gate is in secure state. Cannot perform unsecure
                op(stop_action)"

text_sensor:
  - platform: template
    id: gateText
    name: "Gate State"
    lambda: |-
      if (id(status_duty).state > 80) {
        if (id(gateText).state == "Open!")
          return {};
        return {"Open!"};
      } else if (id(status_duty).state < 5) {
        if (id(gateText).state == "Closed")
          return {};
        return {"Closed"};
      } else if (id(status_pulse).state > 60 && id(status_pulse).state < 90) {
        return {"Opening..."};
      } else if (id(status_pulse).state > 230 && id(status_pulse).state < 290) {
        return {"Closing..."};
      }
      return {"Unknown"};
    update_interval: 1s

status_led:
  pin:
    number: GPIO13
    inverted: yes