Hörmann Garage door opener (RECEIVE ONLY)

There’s a long thread on the forums for how to control this garage door. I was feeling a little lazy so I’ve not done that yet, but the Bluetooth app that Hörmann provide was too tempting, so I have an ESPhome snippet to do decode the BLE adverts.

Now I can have my HA automation tell me I’ve left the garage door open, which is the critical part for me.


sensor:
  - platform: template
    name: Garage door percentage
    entity_category: diagnostic
    id: door_percent
    accuracy_decimals: 0
    unit_of_measurement: '%'
    filters:
      - or:
        - throttle: 60s
        - delta: 1.0

binary_sensor:
  - platform: template
    id: door_moving
    name: Garage door moving
    publish_initial_state: true
    device_class: moving

esp32_ble_tracker:
  on_ble_advertise:
    - mac_address: E5:64:xx:xx:xx:xx
      then:
        - lambda: |-
            auto mfrs = x.get_manufacturer_datas();
            if (mfrs.size() == 2) {
              bool moving = mfrs[0].data[2] & 0x40;
              int percent = mfrs[1].data[1] / 2;

              id(door_percent).publish_state(percent);
              id(door_moving).publish_state(moving);
            }

This is fantastic. I tried several times to get https://community.home-assistant.io/t/hormann-garage-door-opener-receive-only/871821 working, but after my wife couldn’t open the gate for the third time because something went wrong again, I gave up on this approach. What I mainly wanted to know was whether my gates were open or closed. For that, this solution is ideal and very simple. I further adapted the script using ChatGPT so that I can use my two gates and also get the statuses open, opening, closing, and closed.

substitutions:
  garage_right_mac: "XX:XX:XX:XX:XX:XX"
  garage_left_mac:  "XX:XX:XX:XX:XX:XX"

esphome:
  name: garage_doors
  friendly_name: Garage Doors

logger:

esp32:
  board: esp32dev
  framework:
    type: esp-idf

ota:
  - platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  
  ap:
    ssid: "BLE-Proxy-Garage"
    password: "password"

captive_portal:
improv_serial:

api:
  reboot_timeout: 1h

# ==================================================
# Globals (store previous position)
# ==================================================
globals:
  - id: right_last_percent
    type: int
    restore_value: no
    initial_value: '0'

  - id: left_last_percent
    type: int
    restore_value: no
    initial_value: '0'

# ==================================================
# Sensors
# ==================================================
sensor:
  # Wi-Fi RSSI
  - platform: wifi_signal
    name: "Garage Doors WiFi RSSI"
    id: wifi_rssi
    unit_of_measurement: "dBm"
    update_interval: 60s

  # Right door position
  - platform: template
    name: "Garage Door Right Position"
    id: door_right_percent
    unit_of_measurement: "%"
    accuracy_decimals: 0
    filters:
      - or:
        - throttle: 60s
        - delta: 1.0

  # Left door position
  - platform: template
    name: "Garage Door Left Position"
    id: door_left_percent
    unit_of_measurement: "%"
    accuracy_decimals: 0
    filters:
      - or:
        - throttle: 60s
        - delta: 1.0

  # BLE RSSI right
  - platform: template
    name: "Garage Door Right BLE RSSI"
    id: door_right_rssi
    unit_of_measurement: "dBm"
    accuracy_decimals: 0
    filters:
      - throttle: 60s
      - median:
          window_size: 3

  # BLE RSSI left
  - platform: template
    name: "Garage Door Left BLE RSSI"
    id: door_left_rssi
    unit_of_measurement: "dBm"
    accuracy_decimals: 0
    filters:
      - throttle: 60s
      - median:
          window_size: 3

# ==================================================
# Text sensors (door status)
# ==================================================
text_sensor:
  - platform: template
    name: "Garage Door Right Status"
    id: door_right_status

  - platform: template
    name: "Garage Door Left Status"
    id: door_left_status

# ==================================================
# BLE tracker (read-only)
# ==================================================
esp32_ble_tracker:
  on_ble_advertise:

    # ===== Garage Door Right =====
    - mac_address: ${garage_right_mac}
      then:
        - lambda: |-
            auto mfrs = x.get_manufacturer_datas();
            if (mfrs.size() != 2) return;

            int percent = mfrs[1].data[1] / 2;
            int rssi = x.get_rssi();

            if (percent < 0) percent = 0;
            if (percent > 100) percent = 100;

            id(door_right_percent).publish_state(percent);
            id(door_right_rssi).publish_state(rssi);

            int last = id(right_last_percent);
            std::string new_status;

            if (percent == 0) {
              new_status = "Closed";
            } else if (percent == 100) {
              new_status = "Open";
            } else if (percent > last) {
              new_status = "Opening";
            } else if (percent < last) {
              new_status = "Closing";
            }

            if (!new_status.empty() && id(door_right_status).state != new_status) {
              id(door_right_status).publish_state(new_status);
            }

            id(right_last_percent) = percent;

    # ===== Garage Door Left =====
    - mac_address: ${garage_left_mac}
      then:
        - lambda: |-
            auto mfrs = x.get_manufacturer_datas();
            if (mfrs.size() != 2) return;

            int percent = mfrs[1].data[1] / 2;
            int rssi = x.get_rssi();

            if (percent < 0) percent = 0;
            if (percent > 100) percent = 100;

            id(door_left_percent).publish_state(percent);
            id(door_left_rssi).publish_state(rssi);

            int last = id(left_last_percent);
            std::string new_status;

            if (percent == 0) {
              new_status = "Closed";
            } else if (percent == 100) {
              new_status = "Open";
            } else if (percent > last) {
              new_status = "Opening";
            } else if (percent < last) {
              new_status = "Closing";
            }

            if (!new_status.empty() && id(door_left_status).state != new_status) {
              id(door_left_status).publish_state(new_status);
            }

            id(left_last_percent) = percent;