My keys now have a room

My Apple AirTags, room by room in Home Assistant: local Bluetooth trilateration with 21 proxies, without the Find My cloud and without Bermuda.

13 min read
My keys now have a room

TL;DR: Home Assistant now knows which room my keys, the backpack, the suitcase and even the car are in. They’re 11 Apple AirTags, and the same Bluetooth trilateration engine that locates us locates them too, all of it locally.

  • The previous post ended with “more things to locate: watches, keys with a Bluetooth tag…”. Done.
  • An AirTag can’t be recognized the way an iPhone can, and Bermuda can’t handle it: it changes its Bluetooth address every quarter of an hour. But with its keys you can work out which address it will use at any moment.
  • The keys are pulled out once, with a Mac, and never leave my server: Home Assistant only gets the addresses.
  • From there on, an AirTag is just another device: floor plan, history, training, Home Assistant entities and even the 3D house.
  • An AirTag broadcasts far less than a phone, and that forced two changes: training them separately and listening to them for longer.
  • Everything runs locally, without going through Apple’s cloud or the Find My network. I built it with AI in an afternoon and a morning.

I open the Position tab of the engine’s web app, and the ground floor plan has more squares than circles. The circles are Ruth and me, in the living room. The squares are things: the house keys, in the entrance hallway; the Mazda keys, in the handbag, in the living room; two more AirTags, in the kitchen. If I switch the plan to the basement, the car is in the garage, so is the suitcase, and the orange Volvo key is in the hall, where it always is.

An AirTag isn’t an iPhone (and Bermuda can’t handle it)

To follow our iPhones, Home Assistant uses the Private BLE Device integration. An iPhone changes its Bluetooth address every so often so nobody can follow it, but with one of its keys, the IRK, you can still recognize it. The trilateration engine relies on that.

An AirTag has no IRK. It changes its address every 15 minutes while it’s near its owner, and once a day when it has been away for a while. And that address isn’t random: it comes from keys that only the AirTag and its owner’s Apple account know. That’s how Apple’s Find My network works: any iPhone passing by hears it and sends its location, encrypted, so only its owner can read it.

That’s also why Bermuda, the most popular Bluetooth trilateration integration for Home Assistant, is no use here: it recognizes phones by their IRK, iBeacons and devices that always keep the same address. An AirTag is none of the three.

The Find My app tells me the keys are “at home”. Sure, but in which room?

I looked for anyone who had already solved it. The closest is hass-FindMy, a Home Assistant integration by the author of FindMy.py: it recognizes your AirTags with their keys and tells you which proxy hears them best. That fell short for me: the proxy that hears something best isn’t always in the same room. That’s what happened to me with Bermuda and the phones, and why I built my own trilateration engine.

The AirTag keys: a Mac and FindMy.py

An AirTag’s keys are only held by the devices on your Apple account. The easiest way to get them is a Mac signed into your account and the FindMy.py library, which decrypts them from the Find My app’s files:

python3.13 -m venv findmy-env
source findmy-env/bin/activate
pip install findmy
python3 -m findmy decrypt --out-dir airtags

The Mac asks for the keychain password for an entry called “BeaconStore”, which holds the key to those files. You have to be sitting in front of it: over SSH, the prompt never shows up.

A Mac exporting the AirTag keys with FindMy.py in the Terminal, and the macOS prompt asking for the keychain password for BeaconStore
The Mac asking for the keychain password for "BeaconStore".

Watch your macOS version. This works first time up to macOS 14. On macOS 15 it sometimes does too, and if not, there’s a much more complicated method that requires temporarily disabling one of the Mac’s security protections. On macOS 26, the latest, Apple protects the key to those files better, and so far nobody has managed to get it out, not even with that protection disabled: FindMy.py’s documentation explains it. So if you have a Mac on macOS 14 or earlier, don’t update it: it’s your key to adding new AirTags.

The result is the keys of every device on the account, as JSON: iPhones, Macs, AirPods… and the 11 AirTags. Those keys are like a password: with them, anyone could track my AirTags. They live on the server, in a folder that stays out of git, and the engine’s container can only read them.

First, check that the Bluetooth proxies hear them

That was the first thing we did: work out each AirTag’s address at that moment and look for it among what the proxies were hearing. It worked: 10 of the 11 could be heard, by up to 20 of the 21 proxies, Shellys included. Near their owner they send a short message, and away from it a long one, but both carry the address you can work out.

How it fits into the trilateration engine

The idea is simple: if I know which address each AirTag is going to use, from there on it’s just like a phone.

1Preparingonce per AirTag
The MacPulls each AirTag's keys out of the Find My appmacOSFindMy.py
The keysA secret JSON on the server, out of git and read-onlyJSON
The engineWorks out which addresses each AirTag will use in the coming hoursPython
only the addresses
2Listening and locatingnonstop
The 21 proxiesHear the AirTags just like they hear the phonesESPHomeShelly
The integrationSpots their addresses and passes the readings on under a pseudonymjcho_trilateration
The engineLocates it like a phone: area, floor and spotMQTT
shown in
Home AssistantOne device per AirTag, with its entities
The engine's web appThe floor plan, history and training
At the top, what the AirTags need. At the bottom, the same path the phones follow.
  • The engine reads the keys and works out the addresses each AirTag may use in the coming hours.
  • It passes them to my Home Assistant integration. Only the addresses, which the AirTag already broadcasts over the air: the keys never leave the engine, just as the iPhones’ IRK never leaves Home Assistant.
  • The integration looks for those addresses among everything the proxies hear and sends the readings to the engine under a pseudonym, as it does with the phones.
  • From there, everything is the same: floor plan, history, training and Home Assistant entities.
An AirTag as a device in Home Assistant, published over MQTT by the Bluetooth trilateration engine: area, confidence, floor, key proxies and X, Y, Z position, plus its floor history
An AirTag in Home Assistant, the garage Mazda key: it arrives over MQTT as just another device, with the same entities as a person. My Home Assistant speaks Spanish: Área is the area, Confianza the confidence, Planta the floor and Proxies clave the key proxies.

The engine remembers which key each AirTag is on, so after a restart it doesn’t have to look for it again. If an AirTag goes unheard for weeks, the engine may lose track of it; then you just pull its keys out with the Mac again.

To work out the addresses I didn’t want to depend on the whole of FindMy.py: I copied and adapted just the part I needed, which is MIT-licensed.

For the techies: how the address is derived
  • The keys: each AirTag has a public master key and two secrets, one for when it’s near its owner and another for when it’s away. Each secret yields a chain of keys, each one derived from the previous one (ANSI X9.63 KDF), and from it that moment’s public key, on the P-224 curve. The Bluetooth address is the first 6 bytes of that public key.
  • The pace: the “near” key moves on every 15 minutes; the “away” one, once a day.
  • The window: around the key it reckons is current, the engine sends the integration the addresses for the previous 36 hours and the next 12, and refreshes them every minute. Every time it hears an AirTag with a specific key, it uses that key to re-center the window.
  • The protocol: the engine first checks that the integration speaks its version and only then sends it the addresses, with a separate WebSocket command.
  • The state: which key each AirTag is on is saved every 5 minutes to a file, with no keys, just a fingerprint to know which AirTag it belongs to.

Built with AI, again

Like the engine, I built this with AI agents in GitHub Copilot, but this time with two models: Claude Opus 5.5 writes and GPT-6-Astra reviews. Astra reviewed the plan before I started and then the code, over four rounds.

11AirTags
~1 hfrom my go-ahead to seeing them in Home Assistant
4rounds of review by Astra
1442new lines in the first change, tests included

Training an AirTag without spoiling the phones

In the previous post I explained that you can teach the engine the house: you stand still at a spot with your phone and it learns what that spot sounds like. My first question was: if I train with an AirTag, do the phones get worse? They do.

Almost everything it learns is shared by all devices: how much to correct each proxy, how much the walls take away and how the signal drops with distance. An AirTag broadcasts more weakly and usually sits in a bag or hangs on a wall. The engine would have taken those differences as if they came from the proxies, and that would have moved the phones.

Now learning happens on two levels:

  • What’s shared, learned only from the phones’ sessions. An automated test checks that what’s learned for the phones comes out identical, with or without AirTag sessions.
  • What’s specific to each AirTag: how strongly it broadcasts and what it sounds like at the spots where I train it. It’s only used to locate that AirTag. If I delete its session, it goes back to how it was.

And you can train several at once: I left each one in its place and started 8 sessions in under half an hour.

List of training sessions in the engine web: the AirTag ones are labelled 'this AirTag only', and most of them show 0% accuracy
The training sessions. The AirTag ones say "this AirTag only". And look at the Accuracy column: almost all of them say 0%.

That 0% doesn’t mean the training went wrong. It’s how often the engine got it right during the session: while the key sat still in the hall, the engine kept putting it somewhere else. That led me to the next problem.

The key that lived in the garage

The orange Volvo key lives in the basement hall, a meter from the garage. I trained it for ten minutes in its spot and the engine wouldn’t budge: garage.

The orange Volvo key with its AirTag hanging from a key rack in the basement hall, about one meter from the garage door
Its spot: the basement hall, a meter from the garage door.

The training had gone fine: with the data from those ten minutes, the point that fit best was exactly where the key was. The problem was something else: an AirTag barely talks.

Ruth's iPhone329
My iPhone266
The loudest AirTag102
A typical AirTag81
The backpack27
Readings per minute, adding up all the proxies, over one hour. "A typical AirTag" is the median of the 11.

The engine decides based on what the proxies heard in the last 3 seconds. With a phone, that’s more than ten readings. With an AirTag, just a few, from about three proxies, with one or two readings each: thin clues that change all the time.

And with thin clues, the big area wins. The engine spreads the probability over points 50 cm apart, and the area with more points gets more probability. The garage has 151; the hall, 22.

Basement floor plan with the engine's grid: 22 points in the hall, 151 in the garage, and the orange key one meter from the garage
The basement as the engine sees it: one point every 50 cm. The key is a meter from the garage, which has almost seven times as many points as the hall.

To test it, I replayed the training session with the recorded data, but looking at the last 15 seconds instead of 3:

0%of the time in the hall, looking at 3 seconds
96%looking at 15 seconds

So now, for AirTags only, the engine looks at 15 seconds. The phones stay the same. The downside: if I grab the keys and take them away, it takes a few more seconds to notice.

In the first 27 hours after the change, the orange key was in the hall 91% of the time.

A web app for many devices

The Position tab was designed for two people. With eleven more things, names got cut off and the floor plan was too small. Now:

  • The floor plan takes up almost the whole screen, zooms with the wheel or two fingers and pans by dragging. On wide screens it rotates to make the most of the width.
  • People are circles, and things, squares.
  • Below it there’s a card for each one, split into people and objects. Tap one and the plan moves to it and shows its details.
Position tab of the Bluetooth trilateration engine web: ground floor plan with two people and several Apple AirTags, and below it one card per person and per object with its area, floor and confidence
The ground floor: Ruth and me in the living room, the house keys in the entrance hallway, the Mazda keys in the handbag and two more AirTags in the kitchen. Below, a card for each one.

In the 3D house too

The 3D house draws them too. Each AirTag is a pin with its own icon in the room where it is, with its lines to the key proxies. Hover over one and a tooltip tells you where it is, since when, and how confident the engine is.

The 3D digital twin of the house driven by Home Assistant, with the AirTags as map pins with icons; the house keys are selected in the hallway, with a tooltip showing their area, how long they have been there and the position confidence
The ground floor of the 3D house, with the AirTags as pins. The house keys, selected, in the hallway.

Q&A

Does it drain the AirTag’s battery?

No. The proxies only listen to what the AirTag already broadcasts for the Find My network.

Does Find My still work?

Yes. I don’t touch anything on the AirTag or the Apple account: I only listen.

Does it work locally, without internet?

Yes. The proxies, Home Assistant and the engine all run at home, without Apple’s cloud. You only need the Mac once, to get the keys out.

Do I need a Mac?

Yes, one on macOS 14 or earlier, to get the keys out once, and again if you re-pair the AirTag. On macOS 26 it’s no longer possible.

Does it work with other Find My tags?

In theory, yes: all Find My network accessories use the same system. I’ve only tested it with AirTags.

Is it safe?

The keys only live on the server, out of git, and the engine can only read them. They never show up in the logs or the web app. Home Assistant only gets the addresses for the coming hours, which the AirTag already broadcasts over the air.

Where’s the code?

Like the engine, it’s not public for now.

Comments

via GitHub Discussions

Sign in with GitHub to join the conversation. Comments are public and stored in GitHub Discussions.

All posts