My Abrobot Bluetooth Proxy for Home Assistant

Abrobat-based ESP32-C3 BLE Proxy and scanner with WiFi jumping

Just a few days ago this little project was the poor brother of the two projects that succeed it. But hey – things change and this is now a VERY useful tool – I hope you enjoy.

The Abrobot is a cheap ESP23 board with tiny display, widely available on Aliexpress. It is described in the code as SSD1306 but only has a 72*40px display.

My project now called abrobot-based-ble-proxy uses this board in ESPHOME and is a normal ESPHOME Bluetooth proxy… and far more… This means it can relay Bluetooth devices that would normally be out of range of Home Assistant.

bluetooth_proxy:
  active: true
  connection_slots: 3

It is also a BLE activity counter

The OLED shows the number of devices heard during the preceding 120 seconds.

An MQTT BLE event reporter

It publishes: abrobot/ble with JSON output such as:

{
  "event": "new",
  "mac": "5C:53:10:AB:60:99",
  "name": "IoTorero Sensor",
  "rssi": -83
}

or:

{
  "event": "rejoined",
  "mac": "AA:BB:CC:DD:EE:FF",
  "name": "",
  "rssi": -57
}

Note: rejoined doesn’t mean the physical device was switched off and back on. It means that the Abrobot had previously seen the MAC, subsequently heard nothing from it for 120 seconds, removed it from the active cache, and then saw it again

There’s a WebUI display and also a LED. The WebUI gives you: Display Brightness, Display toggle (to show the nearest WiFi, current time, number of BLE devices seen in the last 120 seconds and date or OFF – and product version. The WebUI also has some diagnostic info such as which SSID the board is hooked into, the IP address of the Abrobot, up time and more.

I’ve crammed a lot into the little display on this cheap ESP32-C3-based board. The board is able to jump from WIFI to WiFi thanks to ESPHome and keeps track of the current WiFi SSID at the top of the screen. There’s a blue LED bottom right of the board which shows solid blue unless you turn both it and the screen off to save power. When search between WiFi access points – the blue light flashes. For maximum power saving you can physically (carefullly) cut that RED LED on the left – it is, sadly, not programmable, it’s just a power indicator.

If you want to see new and rejoined BLE devices in detail – an MQTT subscription to abrobat/ble will do the job.

Here is a sample version 1.4.3 output from the WebUI…

Abrobot-based BLE Proxy tool by Peter Scargill

Below is the complete ESPHOME code. Just a point here – I used to put directly passwords in my code – I still could as no-one else in my home would know what to do with them – but honestly – just having a secrets file in ESPHOME keeps the lot in one place.

Abrobot board, typically from AliExpress

If using ESPHOME within Home Assistant, don’t confuse that secrets file with the one in Home Assistant itself. If anyone has already tackled this project, note that today (August 30, 2026) I’ve updated the code and version – as while being utterly compatible with the original, it is WAY better.

You could likely make this with an ESP32-S3 and one of those common mini-OLED displays with only tiny changes – but this is a wiring-free version – straight out-of-the-box hardware needing only a USB-C supply:-)

Abrobot board, typically from AliExpress

If you can’t get an Abrobot board – use an ESP32-S3 mini board and one of these cheap SSD1306 display boards – respect the I2c pins I’m using in the code. As the SSD1306 boards have a different number of pixels to work with – you’ll need some minor changes. HEY, actually that’s not a bad idea, I have some of those – WHEEE.

Oh yes, the board controls also appear within Home Assistant – but I digress.

Caviat here, this is NOT like a Zigbee router (several of which can mesh together-. The Abrobot’s own BLE radio can only hear devices that are within its Bluetooth range. A second proxy doesn’t relay BLE advertisements backwards into another ESP32’s scanner. Each ESP32 sees what its own radio can hear. Home assistant hears the lot.

Of course you could miss off the little display altogether but it’s awfully handy for making sure everything is working.

Common SSD1306 board

Before you start

The code below is incredible for such a small device – and I went on to develop larger BLE trackers detailed elsewhere. However this SHOULD work on your router and did work on my GL-iNET Flint (WiFi 6) router until a few days ago after I made some changes to the router… the end of a long story is — I ended up factory resetting the router as this code would not connect (or rather I saw an error in the logs “Could not reconnect”)… despite every other board and project connecting – including another ESP32-C3. It turns out that the antenna on this little board is not that great and I realised it was failing due to the immense amount of unshielded USB 3 cabling in my office. In the end (long story) I hooked up a cheap router I had lying around, actually in the office – and that made all the difference. Most of my problems (though I didn’t realise it at the time) were down to a duff router.

A couple of things anyone looking at this project code may notice – the code contains references for 5 different web connections (SSIDs) in the WiFi section below. I live in a “cave-home” in rural Spain and it is notoriously difficult to get WiFi to work from room to room, given the thick stone walls not common to modern thin breezeblock (or wooden) housing, so I currently have 5 access points. You can put fewer or more as needed into ESPHOME – unlike Tasmota which I seem to recall will accept up to 2 SSIDs, ESPHOME doesn’t have that limit.

# ========================================================
# Abrobot-based BLE Proxy with display
# August 2026 Peter Scargill and credit to ChatGPT
# v1.4.2 - BLE proxy + WebUI + Multi-WiFi, SSID display
# and more
# ========================================================

substitutions:
  product_version: "1.4.3"

esphome:
  name: abrobot-based-ble-proxy
  friendly_name: Abrobot-based BLE Proxy
  includes:
    - <esp_sntp.h>  
  on_boot:
    priority: 600
    then:
      - delay: 5s
      - globals.set:
          id: ble_startup
          value: 'false'
      - component.update:
          id: ssd1306_display

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf

debug:
  
logger:
  level: INFO

api:

ota:
  - platform: esphome
    password: !secret abrobot_ota_password

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

    - ssid: !secret wifi_ssid2
      password: !secret wifi_password

    - ssid: !secret wifi_ssid3
      password: !secret wifi_password

    - ssid: !secret wifi_ssid4
      password: !secret wifi_password
      
    - ssid: !secret wifi_ssid5
      password: !secret wifi_password
      
  power_save_mode: none
  post_connect_roaming: true

  on_connect:
    then:
      - globals.set:
          id: wifi_connected
          value: 'true'

      - if:
          condition:
            lambda: 'return id(display_on);'
          then:
            - light.turn_on:
                id: onboard_led

  on_disconnect:
    then:
      - globals.set:
          id: wifi_connected
          value: 'false'

  ap:
    ssid: "Abrobot Fallback Hotspot"
    password: "ih2TLODxSA25"

captive_portal:

sensor:
  - platform: wifi_signal
    name: "WiFi Signal"
    id: wifi_signal_sensor
    update_interval: 30s 
    web_server:
          sorting_weight: 3

  - platform: uptime
    name: "Uptime (updated every 60 seconds)"
    update_interval: 60s 
    web_server:
      sorting_weight: 10

  - platform: debug
    free:
      name: "Free Heap"
      web_server:
        sorting_weight: 9
      
text_sensor:
  - platform: template
    name: "Abrobat Time"
    id: ttgo_time
    icon: "mdi:clock"
    lambda: |-
      auto now = id(current_time).now();
      if (!now.is_valid())
        return {"Not synchronised"};
      return now.strftime("%H:%M:%S");
    update_interval: 1s
    web_server:
      sorting_weight: 1

  - platform: template
    name: "Version"
    lambda: |-
      return {"${product_version}"};
        
  - platform: wifi_info
    ssid:
      name: "WiFi SSID"
      id: wifi_ssid
      web_server:
            sorting_weight: 3
    ip_address:
      name: "IP Address"
      id: wifi_ip
      web_server:
            sorting_weight: 2

    bssid:
      name: "WiFi AP MAC"
      web_server:
            sorting_weight: 5

  - platform: template
    name: "Gateway"
    id: wifi_gateway
    icon: "mdi:router-network"
    entity_category: diagnostic
    update_interval: 10s
    web_server:
      sorting_weight: 6
    lambda: |-
      esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");

      if (netif == nullptr) {
        return {"----"};
      }

      esp_netif_ip_info_t ip_info;

      if (esp_netif_get_ip_info(netif, &ip_info) != ESP_OK) {
        return {"----"};
      }

      char gateway[16];
      snprintf(gateway, sizeof(gateway),
               IPSTR, IP2STR(&ip_info.gw));

      return {gateway}; 
  
web_server:
  port: 80
  version: 3
  local: true

time:
  - platform: sntp
    id: current_time
    timezone: Europe/Brussels
    update_interval: 15min    
  
i2c:
  id: bus_i2c
  sda: GPIO5
  scl: GPIO6
  scan: true
  frequency: 800kHz

globals:
  # OLED on/off
  - id: display_on
    type: bool
    restore_value: yes
    initial_value: 'true'

  # Number of BLE devices currently heard
  - id: ble_device_count
    type: int
    restore_value: no
    initial_value: '0'

  # BLE startup screen
  - id: ble_startup
    type: bool
    restore_value: no
    initial_value: 'true'

  # BLE / normal display mode
  - id: ble_mode
    type: bool
    restore_value: no
    initial_value: 'false'

  # Full / half OLED contrast
  - id: display_full_brightness
    type: bool
    restore_value: yes
    initial_value: 'true'

  # Current WiFi connection state
  - id: wifi_connected
    type: bool
    restore_value: no
    initial_value: 'false'

binary_sensor:
  # ------------------------------------------------
  # PHYSICAL BOOT BUTTON
  # ------------------------------------------------

  - platform: gpio
    pin:
      number: GPIO9
      mode:
        input: true
        pullup: true
      inverted: true

    name: "Display Button"
    id: display_button
    internal: true

    on_press:
      - switch.toggle: display_toggle

display:
  - platform: ssd1306_i2c
    id: ssd1306_display
    model: "SSD1306 72x40"
    rotation: 0
    update_interval: 1s
    contrast: 100%

    lambda: |-

      // ------------------------------------------------
      // APPLY SELECTED CONTRAST
      // ------------------------------------------------

      if (id(display_full_brightness)) {
        id(ssd1306_display).set_contrast(1.0f);
      } else {
        id(ssd1306_display).set_contrast(0.20f);
      }

      // ------------------------------------------------
      // DISPLAY OFF
      // ------------------------------------------------

      if (!id(display_on)) {
        return;
      }

      // ------------------------------------------------
      // BLE STARTUP / BLE MODE
      // ------------------------------------------------

      if (id(ble_startup)) {
      
        // Startup screen
      
        it.printf(
          36, 0,
          id(font3),
          TextAlign::TOP_CENTER,
          "BLE"
        );
      
        it.printf(
          36, 19,
          id(font3),
          TextAlign::TOP_CENTER,
          "Proxy"
        );
      
      } else if (id(ble_mode)) {
      
        // Product version screen
      
        it.printf(
          36, 0,
          id(font2),
          TextAlign::TOP_CENTER,
          "Firmware"
        );
      
        it.printf(
          36, 19,
          id(font3),
          TextAlign::TOP_CENTER,
          "v%s",
          "${product_version}"
        );
      
      } else {

        // ------------------------------------------------
        // NORMAL SCREEN
        //
        //       SSID
        // TIME       COUNT
        //       DATE
        // ------------------------------------------------

        char ssid_buf[33];
        
        const char *ssid =
          wifi::global_wifi_component->wifi_ssid_to(ssid_buf);
        
        if (ssid[0] == '\0') {
          ssid = "------";
        }
        
        // Centre SSID
        it.printf(
          36, 0,
          id(font4),
          TextAlign::TOP_CENTER,
          "%.14s",
          ssid
        );

        // Time - left
        //it.strftime(
        //  0, 12,
        //  id(font1),
        //  TextAlign::TOP_LEFT,
        //  "%H:%M",
        //  id(current_time).now()
        //);

        if (id(current_time).now().is_valid()) {
        
          it.strftime(0, 12,
                      id(font1),
                      TextAlign::TOP_LEFT,
                      "%H:%M",
                      id(current_time).now());
        
        } else {
        
          it.print(0, 12,
                   id(font1),
                   TextAlign::TOP_LEFT,
                   "--:--");
        }        

        // BLE count - right
        it.printf(
          72, 12,
          id(font1),
          TextAlign::TOP_RIGHT,
          "%d",
          id(ble_device_count)
        );

        // Date - centred
        it.strftime(
          36, 27,
          id(font2),
          TextAlign::TOP_CENTER,
          "%d/%m/%y",
          id(current_time).now()
        );
      }


      // ------------------------------------------------
      // FLASHING UNDERLINE
      // ------------------------------------------------

      if (id(current_time).now().second % 2 == 0) {
        it.horizontal_line(0, 39, 72);
      }

font:
  # Time / BLE count
  - id: font1
    file: "fonts/Roboto-Black.ttf"
    size: 16

  # Date and word Firmware
  - id: font2
    file: "fonts/Roboto-Black.ttf"
    size: 10

  # BLE Proxy startup
  - id: font3
    file: "fonts/Roboto-Black.ttf"
    size: 18

  # SSID
  - id: font4
    file: "fonts/Roboto-Black.ttf"
    size: 12

output:
  - platform: gpio
    pin:
      number: GPIO8
      inverted: true
    id: gpio_8

light:
  # Internal only - no separate WebUI control
  - platform: binary
    id: onboard_led
    internal: true
    output: gpio_8
    restore_mode: RESTORE_DEFAULT_ON

switch:
  # ==================================================
  # DISPLAY + BLUE LED TOGETHER
  # ==================================================

  - platform: template
    name: "Display Toggle"
    id: display_toggle
    restore_mode: RESTORE_DEFAULT_ON

    lambda: |-
      return id(display_on);

    turn_on_action:
      - globals.set:
          id: display_on
          value: 'true'

      - if:
          condition:
            lambda: 'return id(wifi_connected);'
          then:
            - light.turn_on:
                id: onboard_led

      - component.update:
          id: ssd1306_display

    turn_off_action:
      - globals.set:
          id: display_on
          value: 'false'

      - light.turn_off:
          id: onboard_led

      - component.update:
          id: ssd1306_display

  # ==================================================
  # BLE / NORMAL DISPLAY MODE
  #
  # ON  = BLE screen
  # OFF = SSID / TIME / DATE
  # ==================================================

  - platform: template
    name: "Product Version Display"
    id: display_mode
    optimistic: true
    restore_mode: ALWAYS_OFF

    turn_on_action:
      - globals.set:
          id: ble_mode
          value: 'true'

      - component.update:
          id: ssd1306_display

    turn_off_action:
      - globals.set:
          id: ble_mode
          value: 'false'

      - component.update:
          id: ssd1306_display

  # ==================================================
  # OLED FULL / HALF CONTRAST
  #
  # ON  = FULL
  # OFF = DIM
  # ==================================================

  - platform: template
    name: "Display Brightness"
    id: display_brightness
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON

    turn_on_action:
      - globals.set:
          id: display_full_brightness
          value: 'true'

      - lambda: |-
          id(ssd1306_display).set_contrast(1.0f);

      - component.update:
          id: ssd1306_display

    turn_off_action:
      - globals.set:
          id: display_full_brightness
          value: 'false'

      - lambda: |-
          id(ssd1306_display).set_contrast(0.20f);

      - component.update:
          id: ssd1306_display

# ==================================================
# BLUE LED WIFI STATUS
#
# WiFi connected    = solid ON
# WiFi disconnected = flashing
# Display OFF       = OFF
# ==================================================

interval:
  - interval: 500ms
    then:
      - if:
          condition:
            lambda: 'return id(display_on);'
          then:
            - if:
                condition:
                  lambda: 'return id(wifi_connected);'
                then:
                  - light.turn_on:
                      id: onboard_led
                else:
                  - light.toggle:
                      id: onboard_led
          else:
            - light.turn_off:
                id: onboard_led

esp32_ble_tracker:
  
  scan_parameters:
    interval: 1100ms
    window: 1100ms
    active: true

  on_ble_advertise:
    then:

      - lambda: |-

          // ------------------------------------------------
          // ACTIVE BLE DEVICES
          // Devices heard within the last 120 seconds
          // ------------------------------------------------

          static std::map<std::string, unsigned long> devices_seen;

          unsigned long now = millis();


          // ------------------------------------------------
          // CURRENT BLE MAC ADDRESS
          // ------------------------------------------------

          char mac_buf[18];

          const char *mac =
            x.address_str_to(mac_buf);


          // ------------------------------------------------
          // UPDATE ACTIVE DEVICE CACHE
          // ------------------------------------------------

          devices_seen[mac] = now;


          // ------------------------------------------------
          // REMOVE DEVICES NOT HEARD FOR 120 SECONDS
          // ------------------------------------------------

          for (auto it = devices_seen.begin();
               it != devices_seen.end(); ) {

            if (now - it->second >= 120000) {
              it = devices_seen.erase(it);
            } else {
              ++it;
            }
          }

          // ------------------------------------------------
          // UPDATE BLE DEVICE COUNT
          // ------------------------------------------------

          int new_count =
            devices_seen.size();

          if (new_count != id(ble_device_count)) {

            id(ble_device_count) =
              new_count;

            ESP_LOGI(
              "ble_count",
              "BLE devices in last 120 seconds: %d",
              new_count
            );
          }

bluetooth_proxy:
  active: true
  connection_slots: 3

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave the field below empty!

Are you human? Please solve:Captcha


The maximum upload file size: 512 MB. You can upload: image, audio, video, document, spreadsheet, interactive, text, archive, code, other. Links to YouTube, Facebook, Twitter and other services inserted in the comment text will be automatically embedded. Drop file here