Esphome control of CentSys D5-Evo

The following is a solution for controlling a CentSys Centurion D5-Evo gate motor using Esphome on a Shelly Uni. This code has:

  • Button for triggering normal open
  • Button for triggering hold open
  • Button for triggering pedestrian open
  • Cover entity which reports current gate status and provides discrete buttons for opening and closing

I chose a Shelly Uni because:

  • It can be powered directly from the D5’s aux output, no power conversion required
  • It has two built-in potential-free outputs which are perfect for TRG and PED, no relays required
  • The analog input can read the STATUS output safely, no logic level shifters or optoisolators required
  • It has an external antenna which works appreciably better than the on-board antenna of most typical devkit-style boards
  • I had a few lying around

I previously deployed this with a NodeMCU clone but it died after about 6 months. Since replacing it with the Shelly Uni, it’s been running for well over 12 months now and unlike before, the wifi has been rock solid.

The one quirk for supporting the CentSys D5-Evo is deciphering its status LED and translating that into gate status. The possible states are:

  • Closed: steady off
  • Opening: slow flash (100 pulses/min, or around 300ms high + 300ms low)
  • Open: steady on
  • Closing: fast flash (200 pulses/min, or around 150ms high + 150ms low)

It’s a tricky one to read because the flashing light isn’t always perfectly aligned with change of state. The first and last flashes can be truncated, so there’s a risk of misinterpretation. In order to avoid misreads, you need to verify at least two durations. This could be two contiguous high durations, or, as I have done here, measuring both the high and low duration.

After trying a few methods, I settled upon on_multi_click combined with a script as a reliable way of discriminating between states. Every time it flashes, it sets the state based on the flash duration, then calls the script. The script begins with a delay, so as long as keeps flashing, it won’t do anything. Only once the flashing stops can the script run to completion, which sets the gate as either fully open or closed.

Here’s my YAML (excluding boilerplate stuff like api/wifi/ota):

substitutions:
  pin_led:       GPIO0
  pin_relay_1:   GPIO15 # Labelled "Out 1", connected to TRG
  pin_relay_2:   GPIO4  # Labelled "Out 2", connected to PED
  pin_white:     A0     # White wire, connected to STATUS
  pin_blue:      GPIO05
  pin_orange:    GPIO12
  pin_brown:     GPIO13
  pin_adc_range: GPIO17

esp8266:
  board: esp01_1m

status_led:
  pin:
    number: ${pin_led}
    inverted: yes

esphome:
  on_boot:
    priority: 600
    then:
      - script.execute: completion

script:
  - id: completion
    mode: restart
    then:
      - if:
          condition:
            lambda: 'return id(gate_cover).current_operation == COVER_OPERATION_OPENING;'
          then:
            - delay: 3s
          else:
            - delay: 1s
      - lambda: |-
          if (id(gate_status).state) {
              id(gate_cover).position = COVER_OPEN;
              id(gate_cover).current_operation = COVER_OPERATION_IDLE;
              id(gate_cover).publish_state();
          } else {
              id(gate_cover).position = COVER_CLOSED;
              id(gate_cover).current_operation = COVER_OPERATION_IDLE;
              id(gate_cover).publish_state();
          }

sensor:
  - platform: adc
    pin: ${pin_white}
    id: analog_sensor
    update_interval: 0.02s
    filters:
      - or:
        - delta: 0.01
        - throttle: 5s

binary_sensor:
  - platform: analog_threshold
    id: gate_status
    name: "Status LED"
    sensor_id: analog_sensor
    threshold:
      upper: 0.11
      lower: 0.1
    filters:
       - delayed_on_off: 10ms
    on_multi_click:
      - timing: # OPENING
          - ON for 0.2s to 0.4s  # 300ms
          - OFF for 0.2s to 0.4s
        then:
          - logger.log: "on for 200-400ms"
          - lambda: |-
              if (id(gate_cover).current_operation != COVER_OPERATION_OPENING) {
                id(gate_cover).current_operation = COVER_OPERATION_OPENING;
                id(gate_cover).publish_state();
              }
          - script.execute: completion
      - timing: # CLOSING
          - OFF for 0.1s to 0.2s  # 150ms
          - ON for 0.1s to 0.2s
        then:
          - logger.log: "off for 100-200ms"
          - lambda: |-
              if (id(gate_cover).current_operation != COVER_OPERATION_CLOSING) {
                id(gate_cover).current_operation = COVER_OPERATION_CLOSING;
                id(gate_cover).publish_state();
              }
          - script.execute: completion

switch:
  - platform: gpio
    pin:
      number: ${pin_relay_1}
      inverted: false
    id: trg
    restore_mode: ALWAYS_OFF
    
  - platform: gpio
    pin:
      number: ${pin_relay_2}
      inverted: false
    id: ped
    restore_mode: ALWAYS_OFF

button:
  - platform: template
    id: trigger_short
    name: Trigger Short
    on_press:
      then:
        - switch.turn_on: trg
        - delay: 1 sec
        - switch.turn_off: trg
        
  - platform: template
    id: trigger_long
    name: Trigger Long
    on_press:
      then:
        - switch.turn_on: trg
        - delay: 5 sec
        - switch.turn_off: trg
        
  - platform: template
    id: trigger_ped
    name: Trigger Ped
    on_press:
      then:
        - switch.turn_on: ped
        - delay: 1 sec
        - switch.turn_off: ped
        
cover:
  - platform: template
    name: "Gate"
    id: gate_cover
    device_class: gate
    optimistic: false
    has_position: true
    assumed_state: false
    open_action:
      - button.press: trigger_long
    close_action:
      - button.press: trigger_short