Issue №26 · 2026July 22, 2026iameberhard.com
Back to the journal

Project

The ESP32 on my desk that learned to open my front door

I had a spare ESP32 and an idea — make the house notice me. A few hours later the door unlocks when I walk in, the bedroom light comes on at dusk, and the deadbolt locks itself at bedtime — all without ever turning on Location Services.

EEberhard6 min read

I had a generic ESP32 dev board sitting on my desk for months and an idea: I wanted my house to notice me. Phone in pocket, no apps to open, no GPS quietly draining the battery — just the door, light, and lock behaving like they know I just got home.

A few hours of tinkering later, that's exactly what happens. The deadbolt unlocks when I walk into the living room. The bedroom light turns on if I open the door between 5 PM and 9:30 PM. The deadbolt locks itself at bedtime when I switch off the bedroom rocker, and locks again if the phones in the house have been away for ten minutes. None of it uses location services. The path to get there was instructive — and full of small, satisfying surprises.

ESPresense, not the project I started with

Four firmware segments stacked along a baseline, two in cream and two in deep teal

I started by trying to flash a more ambitious project called RuView, a Wi-Fi CSI ("through-walls") presence platform. Its firmware specifically targets the ESP32-S3 with PSRAM, though — and my board is a classic ESP32. ESPresense is a much better fit. It's a small piece of firmware that turns any ESP32 into a node that listens for nearby Bluetooth advertisements and publishes what it sees over MQTT.

Flashing it took a small detour. The official release ships a single esp32.bin that turns out to be only the app — not a merged boot image. So instead of one flat write to offset zero, you need to grab the bootloader, partition table, and OTA selector from ESPresense's web-flasher manifest and flash all four at the right offsets. Skip a step and the chip drops into a charming infinite loop where the ROM bootloader is reading the firmware's text data as a header:

invalid header: 0x73736572 — that hex is ASCII "ress", the middle of "espressif" appearing where an image magic byte should be.

Once flashed correctly, the node shows up as a Wi-Fi access point, lets you configure your home network and MQTT broker through a captive portal, and starts publishing presence data on its own.

The locks were broken before I started

A small antenna and a padlock on opposite ends of the frame, joined by a single thin teal line

My SwitchBot deadbolts were already integrated into Home Assistant. Or so I thought. Both were unavailable and had been since the previous night. The HA logs were repeating one error every minute:

passive scanning on Linux requires BlueZ ≥ 5.56 with --experimental enabled

A two-line systemd drop-in for bluetooth.service adds the flag, and the underlying Bluetooth stack starts behaving. The SwitchBot integration's config entry had also vanished — I think Home Assistant garbage-collected it after enough consecutive failed retries. Fastest path back was the SwitchBot Cloud integration instead, which talks to the SwitchBot HTTP API and doesn't depend on the Pi's Bluetooth being healthy at all. Both deadbolts reappeared in HA within seconds.

The phone-tracking problem

Four stylised phones radiating BLE ripples; one is highlighted with a soft teal glow

Now the actually interesting question: how does ESPresense know my iPhone is my iPhone? iPhones rotate their Bluetooth address every ~15 minutes for privacy. A static MAC filter is useless.

The trick is an IRK — Identity Resolving Key. It's a 128-bit secret the phone hands to paired devices, and the maths of BLE address rotation means anyone with the IRK can resolve a "random resolvable" address back to the originating device. So: pair the phone with a Linux box once, copy the IRK out, hand it to ESPresense.

A few sharp edges that cost me an hour to walk around:

  • iOS pairing from the phone side does Bluetooth Classic first. The IRK doesn't materialise until a connection completes and triggers Cross-Transport Key Derivation. The IRK appears in BlueZ's info file under [IdentityResolvingKey] a few seconds after pair "looks" done.
  • Apple Watches can't pair with anything other than their companion iPhone over BLE. You can't extract the watch's IRK directly. In practice the phone and watch live in the same pocket, so tracking the phone tracks the person.
  • The IRK byte order ESPresense wants is the reverse of what BlueZ stores. This one was painful. The setting was accepted, the topic echoed back as expected, but ESPresense matched nothing. Reversing the bytes (bytes.fromhex(...)[::-1]) and re-publishing was the fix.
  • The settings topic is per-room (espresense/rooms/<room>/known_irks/set), not the global espresense/settings/... you might guess from another section of the docs.

Once those four bumps were behind me, the node started publishing espresense/devices/irk:<hex>/<room> messages with distance estimates. I could literally watch the number drop from 12 m to 2 m on the MQTT stream as I walked back toward the node — like seeing the house's eyes track me across a room.

Four automations

A door, a table lamp, and a deadbolt arranged in a triangle, connected by thin teal arrowed lines

With trustworthy presence in place, the rest is plain Home Assistant YAML. Four automations now run the front door and the bedroom light:

  1. Arrive home → unlock the deadbolt. Triggered by either iPhone's irk:* MQTT topic falling below 5 m distance. Fires only if the lock is currently locked, which means the automation re-arms itself every time the door gets manually locked again — no separate cooldown logic needed.
  2. Open the door in the evening → master light on. Triggered by the SwitchBot lock's contact sensor going open, between 5 PM and 9:30 PM, only if the light is currently off. Welcomes me home without me touching anything.
  3. Light off after 9:30 PM → lock the door. It doesn't matter whether the light goes off via the wall rocker, the Home Assistant app, or the Hue Tap remote — they all funnel through the same switch entity, so a single trigger covers every control path. Click the light off at bedtime, the deadbolt clicks shut.
  4. If the phones are absent from the node for 10 minutes → lock the door. This one needs a pair of mqtt_room sensors (one per IRK) that flip to not_home after the timeout. The lock condition then checks that they are away, so it doesn't fire while we are still home.

The trade-off in (4) is real and worth being honest about: with only one ESPresense node, "away from the living room" isn't the same as "away from the house". If everyone is in the bedroom for more than ten minutes, the deadbolt will lock itself behind us. Easy to solve later — a second node in the bedroom, or a longer timeout — but it's at ten for now.

What works, what's next

The thing that keeps surprising me is how invisible it feels. There's no app to open, no button to press, no announcement when something happens. I walk in, the door unlocks. I switch the bedroom light off, the door locks. The phone never knows it's being tracked — it's just doing the same BLE chatter it already does 24/7, and a small chip on a shelf is quietly listening.

Obvious next steps: a second ESPresense node in the bedroom for proper room-level positioning, and maybe a third near the entryway so the house can distinguish arrived at the door from walked past it. If I get bored I'll wire one of the unused Hue Tap buttons to a "good night, lock everything" macro. For now this is the smallest useful version, and it's been delightful to live with.

Filed under

esp32espresensehome-assistantbluetoothpresenceraspberry-pi
The ESP32 on my desk that learned to open my front door — iameberhard