</>CoderDIY
← Back to list
Article

Build a Bluetooth Presence Detector for Smart Homes with ESP32

Author: CoderDIY··Đọc bằng tiếng Việt
📶

Smart home automation based on phone GPS drains battery and needs a stable internet connection; automation based on a motion sensor can’t tell who’s actually home. A cheaper and more reliable approach: most phones constantly broadcast Bluetooth Low Energy (BLE) signals even when not paired to anything — an ESP32 can scan for these signals within a few meters’ range and infer “someone’s home” or “everyone left” with zero app or phone-side configuration.

How it works

  1. The ESP32 periodically runs a BLE scan for nearby broadcasting devices, collecting each device’s MAC address and signal strength (RSSI).
  2. It compares each scanned MAC address against a pre-known list (the household members’ phones) — modern iOS/Android randomize their broadcast MAC over time, which is a real limitation covered in the pitfalls section below.
  3. If a known MAC shows up consistently across several scans, the ESP32 marks that device “present”; if it’s absent across many consecutive scans (beyond a timeout window), it’s marked as having left.
  4. Presence state is published over MQTT to Home Assistant or a self-hosted broker, which then drives automations: turn on lights/AC when someone arrives, shut everything off when nobody’s left.
<rect x="10" y="20" width="140" height="60" rx="10" class="box" />
<text x="80" y="45" text-anchor="middle" class="label">BLE scan</text>
<text x="80" y="62" text-anchor="middle" class="label">(every 10-15s)</text>

<rect x="170" y="20" width="140" height="60" rx="10" class="box" />
<text x="240" y="45" text-anchor="middle" class="label">Match MAC</text>
<text x="240" y="62" text-anchor="middle" class="label">against known list</text>

<rect x="330" y="20" width="140" height="60" rx="10" class="box" />
<text x="400" y="45" text-anchor="middle" class="label">Update state</text>
<text x="400" y="62" text-anchor="middle" class="label">(present/away)</text>

<rect x="490" y="20" width="140" height="60" rx="10" class="box" />
<text x="560" y="45" text-anchor="middle" class="label">Publish MQTT</text>
<text x="560" y="62" text-anchor="middle" class="label">→ Home Assistant</text>

<line x1="150" y1="50" x2="170" y2="50" class="arrow" />
<line x1="310" y1="50" x2="330" y2="50" class="arrow" />
<line x1="470" y1="50" x2="490" y2="50" class="arrow" />

<path d="M 560 80 L 560 190 L 80 190 L 80 80" class="arrow" fill="none" />
<text x="320" y="210" text-anchor="middle" class="sublabel">Repeats continuously to keep the state up to date</text>
Presence detection loop: BLE scan, match MAC, update state, publish over MQTT to the smart home system.

Parts list

  • An ESP32 (any variant with BLE support, including ESP32-C3/S3)
  • A stable 5V power supply (USB adapter), since the device runs continuously 24/7
  • An MQTT broker (self-hosted Mosquitto, or the one bundled with Home Assistant)
  • The household members’ phones, to collect their BLE MAC addresses (use a BLE scanner app like nRF Connect to view the MAC while Bluetooth is on)
<rect x="20" y="80" width="150" height="70" rx="10" class="box" />
<text x="95" y="120" text-anchor="middle" class="label">Phone (BLE)</text>

<rect x="245" y="80" width="150" height="70" rx="10" class="box" />
<text x="320" y="120" text-anchor="middle" class="label">ESP32 (scanning)</text>

<rect x="470" y="80" width="150" height="70" rx="10" class="box" />
<text x="545" y="110" text-anchor="middle" class="label">MQTT Broker /</text>
<text x="545" y="128" text-anchor="middle" class="label">Home Assistant</text>

<line x1="170" y1="115" x2="245" y2="115" class="wire" />
<text x="207" y="105" text-anchor="middle" class="sublabel">BLE</text>

<line x1="395" y1="115" x2="470" y2="115" class="wire" />
<text x="432" y="105" text-anchor="middle" class="sublabel">WiFi/MQTT</text>
The ESP32 passively scans for nearby BLE broadcasts, then publishes presence state over WiFi to the MQTT broker.

Sample code

#include <WiFi.h>
#include <PubSubClient.h>
#include <BLEDevice.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

const char* WIFI_SSID = "your-wifi";
const char* WIFI_PASS = "your-wifi-password";
const char* MQTT_HOST = "192.168.1.10";

WiFiClient espClient;
PubSubClient mqtt(espClient);
BLEScan* bleScan;

// Phone MAC address to track, replace with the real one
const char* KNOWN_MAC = "aa:bb:cc:dd:ee:ff";
bool present = false;
unsigned long lastSeen = 0;
const unsigned long TIMEOUT_MS = 60000; // 1 minute unseen -> treat as away

void connectWifiMqtt() {
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  mqtt.setServer(MQTT_HOST, 1883);
  while (!mqtt.connected()) {
    mqtt.connect("esp32-presence");
    delay(500);
  }
}

void setup() {
  Serial.begin(115200);
  connectWifiMqtt();

  BLEDevice::init("");
  bleScan = BLEDevice::getScan();
  bleScan->setActiveScan(false); // passive scan, easier on the phone's battery
}

void loop() {
  if (!mqtt.connected()) connectWifiMqtt();
  mqtt.loop();

  BLEScanResults* results = bleScan->start(5, false); // scan 5 seconds per cycle
  bool foundNow = false;

  for (int i = 0; i < results->getCount(); i++) {
    BLEAdvertisedDevice device = results->getDevice(i);
    if (device.getAddress().toString() == KNOWN_MAC) {
      foundNow = true;
      lastSeen = millis();
      break;
    }
  }

  bleScan->clearResults();

  bool nowPresent = (millis() - lastSeen) < TIMEOUT_MS;
  if (nowPresent != present) {
    present = nowPresent;
    mqtt.publish("home/presence/phone1", present ? "home" : "away", true);
    Serial.println(present ? "Arrived home" : "Left home");
  }

  delay(10000); // pause between scan cycles
}

Common pitfalls

  • MAC address randomization: modern iOS and Android periodically rotate their advertised MAC address for privacy, which makes tracking by a fixed MAC unreliable. A more practical fix: use a dedicated BLE device with a fixed MAC (a wristband, a BLE tag like a Nordic Thingy, or an iBeacon) instead of the phone directly, or accept the imperfect accuracy.
  • A timeout that’s too short causes false “away” alerts when the phone temporarily stops advertising BLE (screen locked, battery saver) — keep the timeout at least 1-2 minutes.
  • Scanning too frequently heats up the ESP32 and wastes power unnecessarily — scanning 5 seconds, resting 5-10 seconds, is plenty for most home use.
  • Not handling WiFi/MQTT disconnects leaves the device silently stuck — always check and reconnect, as shown in the sample code.

Extensions

Track multiple devices at once (one MAC per household member) to distinguish “someone’s home” from “who exactly is home,” combine it with a PIR motion sensor for extra confidence, or integrate directly into Home Assistant via ESPHome instead of hand-rolling MQTT firmware.

Tags:#esp32#bluetooth#home-automation

Comments