Automating my BullyDog GTO Gate Opener

Gate openers are a fairly common one to stick a relay on the toggle input and say “automated!” Well I got tired of unknown states with mine which make automations tricky. I have a GTO opener that is basically a linear actuator with some logic built in. It has a few sensor inputs that are used for safety and exiting (for a loop in the driveway sensing a car waiting to leave).

So I attached a relay to the exit sensor which allowed me to at least be able to tell the gate to open (linear actuator all they way closed) but there is no close input, just the toggle input. It does have a timeout to auto close, which I contemplated using along with the safety input to hold the gate open, but this still led to some awkward state assumptions.

I started probing around on the sensor wires going into the motor area of the board and found that one of them is a limit switch that indicates a fully retracted arm. There was also an optical encoder wheel attached to the shaft.

Both of these were controlled by the existing microcontroller on the board and I wanted to keep all of the stock, so I attached them to optoisolators. They presented a significant challenge that the GTO opener pulses them in order to keep the battery usage low when the gate is not moving. I created a pulse counter sensor that watches for the closure with filters that keep it from taking over my logger and logic in a binary_sensor template to indicate open or close based on the pulse counter.

sensor:
  - platform: pulse_counter
    pin:
      number: 17
      inverted: true
      mode:
       input: true
       pullup: true
    name: "Pulse Counter"
    id: gate_open_pulses
    update_interval: 500ms
    internal: true
    filters:
      - or:
        - throttle: 15s
        - delta: 1000


binary_sensor:
  - platform: template
    device_class: door
    id: gate_open
    internal: true
    name: "Gate Open"
    lambda: |-
      if (id(gate_open_pulses).state > 100) {
        // Gate is open.
        return true;
      } else {
        // Gate is closed or closing.
        return false;
      }

In order to determine which direction the gate was going, I hooked up two optical isolators to the motor leads so one activated when the motor was counter clockwise and the other when the motor was spinning clockwise.

The optical encoder when it rests in an open slot of the wheel is constantly sending out pulses when the system is at rest and the onboard micro is looking for new movement. This made just keeping track of the pulses from the wheel quite difficult. So I soldered an ~80kohm resistor to another pin and turn that pin high when the motor is actually spinning. (if this was more easily programmatically controlled from ESPHOME that would have worked too, but I decided soldering a resistor was easier than more lambda functions) With that I was now only getting pulses when the gate was actually moving.

    on_state:
      then:
        - lambda: |-
            if (id(gate_motor_opening).state) {
              id(pullup_cycles).turn_on();
            } else if (id(gate_motor_closing).state) {
              id(pullup_cycles).turn_on();
            } else {
              id(gate_cover).current_operation = COVER_OPERATION_IDLE;
              id(pullup_cycles).turn_off();
            }

Then I wrote some logic to watch the motor direction and created some global variables to keep track of them. The pulse count is then incremented of decremented according to the direction.

  - platform: pulse_meter
    pin:
      number: 18
      inverted: true
      mode:
       input: true
#       pullup: true #A pullup is now provided with a ~80kohm resistor to the pullup_cycles switch
    name: 'Cycle Pulses'
    id: total_cycles
    internal: true
    total:
      name: "Total Pulses in cycle"
      internal: true
      filters:
        - lambda:  |-
            if (id(gate_motor_closing).state) {
            // Gate is moving, count the pulses.
              id(latest_enc_sum) = x - id(last_total_enc) + id(latest_enc_sum);
              id(last_total_enc) = x;
              return x;
            } else if (id(gate_motor_opening).state) {
            // Gate is moving, count the pulses.
              id(latest_enc_sum) = id(last_total_enc) - x + id(latest_enc_sum);
              id(last_total_enc) = x;
              return x;              
            } else {
            // Gate is idle, wheel is stuck in an open position
              return x;
            }
        - or:
          - throttle: 10s
          - delta: 5.0

This count now represents the gate location in its swing in the cover template.

cover:
  - platform: template
    device_class: gate
    id: gate_cover
    name: "Gate"
    lambda: |-
      if (id(latest_enc_sum) == 0) {
        return COVER_OPEN;
      } else if (id(latest_enc_sum) > 0  && id(latest_enc_sum) < 1130 ){
        return 1 - ( id(latest_enc_sum) / 1138 );
      } else {
        return COVER_CLOSED; //TODO: This could be improved to have an error state and not just assume the gate is closed
        //This typically happens after a power cycle and the gate has not hit the open limit switch since
      }

gaet

All put together it looks like this:

esphome:
  name: gate
  platform: ESP32
  board: esp32dev
  comment: "GTO Gate Controller"

<<: !include templates/common.yaml

globals:
   - id: latest_enc_sum
     type: float
     restore_value: no
     initial_value: '0'
   - id: last_total_enc
     type: float
     restore_value: no
     initial_value: '0'

switch:
  - platform: gpio
    pin:
      number: 13 #colocated with 34 (formerly 26)
      inverted: true
    name: "Gate Exit Switch"
    id: exit_switch
  - platform: gpio
    pin:
      number: 32 #(formerly 14)
      inverted: true
    name: "Gate Toggle Switch"
    id: gate_toggle_switch
    on_turn_on:
    - delay: 400ms
    - switch.turn_off: gate_toggle_switch
  - platform: gpio
    pin:
      number: 4  #colocated with 35 (formerly 27)
      inverted: true
    name: "Gate Safety Switch"
    id: gate_safety
  - platform: gpio
    pin:
      number: 16
    name: "Pullup for Cycles"
    id: pullup_cycles
    restore_mode: ALWAYS_OFF
    internal: true

cover:
  - platform: template
    device_class: gate
    id: gate_cover
    name: "Gate"
    lambda: |-
      if (id(latest_enc_sum) == 0) {
        return COVER_OPEN;
      } else if (id(latest_enc_sum) > 0  && id(latest_enc_sum) < 1130 ){
        return 1 - ( id(latest_enc_sum) / 1138 );
      } else {
        return COVER_CLOSED; //TODO: This could be improved to have an error state and not just assume the gate is closed
        //This typically happens after a power cycle and the gate has not hit the open limit switch since
      }
    open_action:
      # Cancel any previous action
      - switch.turn_off: gate_toggle_switch
      # Turn the OPEN switch on briefly
      - switch.turn_on: exit_switch
      - delay: 0.3s
      - switch.turn_off: exit_switch
    close_action:
      if:
        condition:
          - binary_sensor.is_on: gate_motor_closing
        then:
          - switch.turn_off: exit_switch
          - switch.turn_off: gate_toggle_switch
        else:
          - switch.turn_off: exit_switch
          - switch.turn_on: gate_toggle_switch
          - delay: 0.3s
          - switch.turn_off: gate_toggle_switch
    stop_action:
      if:
        condition:
          or:
            - binary_sensor.is_on: gate_motor_opening
            - binary_sensor.is_on: gate_motor_closing
        then:
          - switch.turn_off: exit_switch
          - switch.turn_on: gate_toggle_switch
          - delay: 0.3s
          - switch.turn_off: gate_toggle_switch
        else:
          - switch.turn_off: exit_switch
          - switch.turn_off: gate_toggle_switch
    has_position: true

sensor:
  - platform: pulse_counter
    pin:
      number: 17
      inverted: true
      mode:
       input: true
       pullup: true
    name: "Pulse Counter"
    id: gate_open_pulses
    update_interval: 500ms
    internal: true
    filters:
      - or:
        - throttle: 15s
        - delta: 1000

  - platform: pulse_meter
    pin:
      number: 18
      inverted: true
      mode:
       input: true
#       pullup: true #A pullup is now provided with a ~80kohm resistor to the pullup_cycles switch
    name: 'Cycle Pulses'
    id: total_cycles
    internal: true
    total:
      name: "Total Pulses in cycle"
      internal: true
      filters:
        - lambda:  |-
            if (id(gate_motor_closing).state) {
            // Gate is moving, count the pulses.
              id(latest_enc_sum) = x - id(last_total_enc) + id(latest_enc_sum);
              id(last_total_enc) = x;
              return x;
            } else if (id(gate_motor_opening).state) {
            // Gate is moving, count the pulses.
              id(latest_enc_sum) = id(last_total_enc) - x + id(latest_enc_sum);
              id(last_total_enc) = x;
              return x;              
            } else {
            // Gate is idle, wheel is stuck in an open position
              return x;
            }
        - or:
          - throttle: 10s
          - delta: 5.0

binary_sensor:
  - platform: template
    device_class: door
    id: gate_open
    internal: true
    name: "Gate Open"
    lambda: |-
      if (id(gate_open_pulses).state > 100) {
        // Gate is open.
        return true;
      } else {
        // Gate is closed or closing.
        return false;
      }
    on_state:
      if:
        condition:
          - binary_sensor.is_on: gate_open
        then:
          - pulse_meter.set_total_pulses:
              id: total_cycles
              value: 0
          - globals.set:
              id: latest_enc_sum
              value: '0'
          - globals.set:
              id: last_total_enc
              value: '0'

  - platform: gpio
    pin:
      number: 5
      inverted: true
      mode:
       input: true
       pullup: true
    name: "Gate Opening"
    id: gate_motor_opening
    internal: true
    on_state:
      then:
        - lambda: |-
            if (id(gate_motor_opening).state) {
              id(pullup_cycles).turn_on();
            } else if (id(gate_motor_closing).state) {
              id(pullup_cycles).turn_on();
            } else {
              id(gate_cover).current_operation = COVER_OPERATION_IDLE;
              id(pullup_cycles).turn_off();
            }

  - platform: gpio
    pin:
      number: 19
      inverted: true
      mode:
       input: true
       pullup: true
    name: "Gate Closing"
    internal: true
    id: gate_motor_closing
    on_state:
      then:
        - lambda: |-
            if (id(gate_motor_opening).state) {
              id(pullup_cycles).turn_on();
            } else if (id(gate_motor_closing).state) {
              id(pullup_cycles).turn_on();
            } else {
              id(gate_cover).current_operation = COVER_OPERATION_IDLE;
              id(pullup_cycles).turn_off();
            }
            
1 Like