tagalong

Three $2 BLE keyfinders, turned into a whole-home find-my network and room-aware light buttons for Home Assistant — on a self-healing presence trick with no coordinator, no cloud, no vendor app.

☁︎ no cloud 📵 no vendor app 🏠 Home Assistant 📦 HACS ⚖︎ AGPL-3.0

▶️The whole system, in one loop

Three $2 tags, four rooms, zero coordinator — contested claiming, room handoff, button → lights and whole-home find-my, animated end to end.

Animated walkthrough of the tagalong system across four rooms. Phase 1, contested claiming: Tag B advertises and three ESP32 nodes show their claim-delay countdowns — the nearest node (0.24 s) wins the GATT link and becomes Tag B's room. Phase 2, room handoff: Tag B moves toward the office, its link RSSI weakens past −85 dBm three times, the link releases, the tag re-advertises, and the closer office node re-claims it. Phase 3, button to lights: Tag A's button is pressed in the kitchen, an esphome.itag_button event flows to Home Assistant, and the kitchen lamp toggles on. Phase 4, find-my: a find command races from Home Assistant to every node in parallel, and Tag C beeps in the detached shed.
One ~30-second loop through all four behaviours. Honours your light/dark theme and prefers-reduced-motion (which freezes it to a labelled still).

The $2 keyfinder

The classic AliExpress "iTAG" anti-lost keyfinder is about as cheap as Bluetooth gets: a Telink chip, a CR2032 coin cell, one button, one beeper — two or three dollars shipped. This project has three, one per family member.

There's no vendor cloud to escape and barely any firmware to reverse. The whole tag speaks only over a GATT connection: the button reports on a notify characteristic, and a single write makes it beep. No connection, no button — so the interesting part isn't the tag. It's who's connected to it.

One button, one beeper, a CR2032 — and a GATT link that decides which room it's in.

📍The elegant core: rooms decide themselves

Every ESP32 node (running ESPHome) hears the tag advertise. Instead of a central brain picking a winner, the nodes race — and the race is rigged in favour of the closest one:

Each node delays its connect attempt in proportion to how weak it hears the tag: delay = (−55 − rssi) × 80 ms, clamped to 3 s. The nearest node has the shortest delay, grabs the GATT link first, and every other node — seeing the tag stop advertising — quietly backs off. The node holding the link is the room.
 tag advertises ─▶ Kitchen node   rssi −58  →  delay  240 ms  ┐
                ─▶ Hall node      rssi −72  →  delay 1360 ms  ├─▶  closest node wins the
                ─▶ Bedroom node   rssi −81  →  delay 2080 ms  ┘    link  =  "the room"

 link RSSI sags ─▶ 3 samples < −85 dBm ─▶ node disconnects ─▶ tag re-advertises ─▶ a closer node re-claims

🏁 Contested claiming

The RSSI-weighted head-start means the physically nearest node connects first, house-wide, with no coordinator and no shared state. A node only commits the connection if the tag is still advertising — proof nobody closer already grabbed it.

🔄 Proximity release

While connected, each node watches its link RSSI. Three consecutive samples below −85 dBm means the tag walked away: the node drops the link, the tag re-advertises, and whichever node now hears it best claims it. Room handoff, fully distributed.

🧭 Room sensors

Because "connected here" equals "in this room," each tag gets a plain-language location in Home Assistant — Tag A: Kitchen — with no trilateration, no calibration, and no fixed anchors to survey.

🪫 Frugal by design

Only one node ever holds a given tag, and only long enough to serve a button or a beep before proximity release frees it. A coin cell doesn't mind.

💡Button → the lights in this room

Press the tag's button and the lights in whatever room you're standing in toggle. There's no per-tag configuration of "which lights" — the room resolves itself:

  1. The connected node fires a Home Assistant event tagged with its own name — it already knows it's the one holding the link.
  2. HA resolves node → area, then toggles every entity in that area carrying the button_lights label. Any domain — lights, switches — opts in just by wearing the label.
  3. If the link looks weak at press time, a correction path releases the tag and lets the co-located node re-claim it before toggling — so a press never lights up the room you just left.

Adding a room is additive: point another node's ble_client at the tag and label that room's lights. Nothing else changes.

🔔Whole-home find-my

Lost a tag? Tap its tile on the dashboard and every node races to write the beep (1802/2a06 → 0x01). Whichever one currently holds — or wins — the link makes the tag ring, wherever in the house it happens to be. Tap again to silence it (0x00).

Paired with the per-tag room sensors, "find" stops being guesswork: the dashboard already says Tag A: Kitchen before you ever make it beep. A $2 tag with a whole-home locator built entirely on connection presence.

🧩Two ways to run it

🎯 The native integration · start here

A custom itag integration with a config flow and Bluetooth auto-discovery through Home Assistant's own stack. It works over your existing ESPHome BLE proxies — no local adapter required. You get a button event entity (plus an itag_button bus event), Find / Stop beep buttons, and connected / room / RSSI sensors. It reads battery opportunistically — for tags that expose it — and silences its own link-loss so a normal disconnect never triggers a phantom beep.

🛰️ The distributed ESPHome fleet · advanced

The multi-room setup that runs in the reference house: an ESPHome node per room, all doing the contested-claiming and proximity-release dance from above. This is where room-aware button → lights and instant house-wide find-my come from. Ships in esphome/ alongside the HA package that wires the button events to your rooms' lights.

🔬The tag, over GATT

Everything the tag does lives behind a connection. The three buttoned tags this project drives expose the button and the beeper; battery is present only on some variants of the family, so the integration treats it as a bonus.

ServiceCharDoesOn these tags
ffe0 vendor (Telink)ffe1 Button press — notify✔ the button
1802 Immediate Alert2a06 Beep — write 0x01 / 0x00✔ find-my
180f Battery2a19 Battery % — read/notify✘ not exposed (opportunistic elsewhere)

MACs shown in the repo are placeholders like AA:BB:CC:DD:EE:01; these tags advertise a static-random address that can rotate after a battery swap.

📂What's in the repo

custom_components/itag/The native Home Assistant integration — config flow, Bluetooth auto-discovery, button + beep + room/RSSI/battery entities.
esphome/The distributed node firmware: contested claiming, proximity release, per-tag button notify and find/stop beep.
ha-packages/itag_lights.yamlThe room-aware button→lights engine: node→area resolution, the button_lights label convention, weak-link correction, and find/stop scripts.
dashboards/Lovelace dashboards — per-tag room, find/stop tiles, fleet health.
tools/itag_fleet_check.pyA fleet-health CLI: per-node reachability, which tag each node holds, per-tag room, and drop/flap counts over a time window.

Quick start

Install the integration through HACS as a custom repository, then let HA discover the tag:

HACS → ⋮ → Custom repositories
  Repository:  https://github.com/jphein/tagalong
  Category:    Integration

# then, in Home Assistant:
Settings → Devices & Services → Add Integration → "itag"
#   → auto-discovers tags seen by your ESPHome BLE proxies

For the multi-room button→lights and house-wide find-my, add the esphome/ node config to each room and drop ha-packages/itag_lights.yaml into your packages — full instructions →

📣Share this project

On a phone, “Share…” opens your native share sheet — Instagram, WhatsApp, Messenger and friends live there.