We’re currently fostering a cat, and she’s very slowly getting more confident about leaving the room she’s in. We’re particularly curious about whether and where she roams while we’re out of the house.
![]()
There’s various commercial solutions to this that either use bluetooth or GPS - either animal specific ones like chipolo, pitpat or tractive, or using standard trackers like Apple AirTags or Tile Trackers.
Of course, it’s always more interesting to put something together out of open-source components, and tracking items via BLE is a well-established field. I have a Home Assistant Green running various automations, and I already have a bunch of Raspberry Pis around the house running snapcast, so in theory I should be able to leverage those instead.
Let’s give it a try!
The idea
The concept behind BLE tracking is pretty simple: a BLE beacon broadcasts an identifier periodically, which other devices can pick up. By measuring the signal strength of the message, you can approximate the distance between the transmitter and receiver. Combine a ‘roaming’ element (carried by a person, or a cat) with various ‘fixed’ elements (set up at each location), and you can work out the location of the roaming element.
There’s two ways you can set this up:
- A roaming transmitter, and several fixed receivers
- Or a roaming receiver, and several fixed transmitters
The first option makes use of a simple Bluetooth beacon that a user can carry around. The fixed receivers at each location perform Bluetooth scans, and report their results back. This requires the fixed receivers to have a power supply and a network connection of some kind.
The second option has Bluetooth beacons at each location, and a receiver that the user can carry around. The receiver periodically scans for the closest beacon, and uses that to work out its location. This can either be processed on-device, or sent back to whichever server needs to know.
The choice between the two setups depends on the available hardware, how many rooms you need to cover, and how many objects you’re trying to track. Bluetooth beacons are cheaper than receivers, but receivers require more power, while beacons can be left running for a long time with a simple cell battery.
If you’re using a smart phone as your ‘roaming’ element, it slightly limits your options. For privacy reasons, both iPhones and Android phones randomise their BLE addresses (precisely to avoid being tracked), which makes them harder to integrate into the first option.
In my case, since I already had Raspberry Pis set up around the house, I opted to use a small BLE beacon and set up the Raspberry Pis as receivers, feeding information back to my Home Assisstant server.
The beacon
So, first things first we need an actual beacon.
If we’re going to put it on the cat, it probably needs to be compact and not too heavy. It also needs to be attach-able somehow.
Various devices at various price points and feature sets exist, but I’m looking for something cheap and simple. I saw a few references to the NRF51822 SoC; searching that on AliExpress yielded various beacons including this iBeacon Bluetooth nRF51822 module 4.0 BLE beacon Beacon indoor positioning low power consumption Waterproof with case.
At around £4 per unit and free shipping over £8, it’s by far the cheapest option I saw. I’m dubious of the claim that it’s waterproof, but I’m not planning on dunking it in water. I ordered 2 units and waited the week for them to arrive.
While I waited, I also ordered a pack of CR2477 batteries, since they’re not common in shops around me.
Setting up the beacon
Once it arrived, it was short work to set up the beacon. It comes with a plastic shell and 3 screws - definitely doesn’t seem waterproof, although potentially it could be made so with a bit of silicone in the seal.
![]()
The aliexpress listing has instructions pointing to the ‘Holyiot-beacon’ app. No links are provided, but that seems to refer to this app on Android and this app on iOS.
Scanning yields the beacon, and tapping ‘connect’ requests a password. The listing suggests AA1406111200 is the default (the screenshot shows aa14061112). This worked and gave access to the beacon properties.
From there I named one ‘cat’ and one ‘dog’ for testing, and made a note of the MAC addresses. To change the properties you need to tap the text, provide the updated value, then tap ‘modify’ to make sure it gets saved to the beacon.
I then wanted to make sure it worked with a generic BLE scanner as well. I downloaded BLE radar from the F-Droid Android store, and could see the devices appearing in that app as well.
With the beacons working, let’s more on to tracking them!
The brains
To track the beacons, we’ll need some kind of co-ordinator that will take the data from the receivers and decide where the beacons are.
A quick search brings up a couple of suggestions with Home Assistant integration:
room-assistant looks interesting, but from an initial glance at the website there’s a lot of information on how to deploy it, but not much in terms of the architecture and how it goes about its work.
It seems like it’s a layer between multiple types of sensors (BLE, heat sensor, motion sensors) on one side and different destinations (Home Assistant, MQTT, etc.). In terms of the deployment model it then looks like multiple instances work in a cluster, discovered over mDNS.
If I wasn’t already running Home Assistant I’d be interested in this for its MQTT integration and multiple sensor types.
mqtt_room is architected differently: it runs as a single service, and relies on an MQTT broker to receive updates from scanners, which it uses to decide what locations beacons are in and publish that informationt to Home Assistant.
This means that anything that can publish messages via MQTT can be used as a source of information, without any changes to mqtt_room. mqtt_room is baked into Home Assistant - there’s no way to run it without HA.
The choice between the two is largely down to preference and the existing equipment you have available.
In my case, mqtt_room seems like a better fit: my Pi Zeros are already running snapcast, and I want to reuse them for BLE scanning with as little CPU impact as possible. Scanning for devices and publishing the results to MQTT seems to fit that bill, and I can leave it up to the Home Assistant server to do the remaining computational work.
You’ll just need an MQTT broker - I run mosquitto on Home Assistant via the mosquitto addon.
Anyway, enough analysis - let’s move on to the receivers.
Setting up BLE scanners
As mentioned above, mqtt_room requires various receivers to publish updates via MQTT. This page describes the format it needs, although that’s not especially important (we’ll come on to why in a second) as long as the information is there.
mqtt_room suggests a few receiver options here. Most are based on ESP32 chips, Happy Bubbles is on hiatus, and interestingly room-assistant is mentioned.
None precisely fit my usecase. Fortunately, Ivan Belokobylskiy (devbis) has created ble2mqtt, which is exactly what I need!
Ivan’s other projects include z03mmc, a custom firmware for a Xiaomi Bluetooth temperature and humidity sensor to add Zigbee support which I’ve used before.
Setting up ble2mqtt is very easy if you already have Python installed. I set up a virtual environment in /opt/ble2mqtt to store it, but use whichever Python package install method you prefer.
sudo -i
python -m venv /opt/ble2mqtt
source /opt/ble2mqtt/bin/activate
pip3 install ble2mqtt
# Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
# Collecting ble2mqtt
# Downloading https://archive1.piwheels.org/simple/ble2mqtt/ble2mqtt-0.2.5-py3-none-any.whl (83 kB)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 83.0/83.0 kB 561.7 kB/s eta 0:00:00
# Collecting aio-mqtt-mod>=0.3.0
# Downloading https://archive1.piwheels.org/simple/aio-mqtt-mod/aio_mqtt_mod-0.3.4-py3-none-any.whl (18 kB)
# Collecting bleak>=0.12.0
# Downloading https://www.piwheels.org/simple/bleak/bleak-1.1.1-py3-none-any.whl (136 kB)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 136.5/136.5 kB 421.6 kB/s eta 0:00:00
# Collecting dbus-fast>=1.83.0
# Downloading dbus_fast-2.44.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (909 kB)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 909.8/909.8 kB 839.6 kB/s eta 0:00:00
# Collecting typing-extensions>=4.7.0
# Downloading https://www.piwheels.org/simple/typing-extensions/typing_extensions-4.15.0-py3-none-any.whl (44 kB)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 44.6/44.6 kB 221.0 kB/s eta 0:00:00
# Installing collected packages: typing-extensions, dbus-fast, aio-mqtt-mod, bleak, ble2mqtt
# Successfully installed aio-mqtt-mod-0.3.4 ble2mqtt-0.2.5 bleak-1.1.1 dbus-fast-2.44.5 typing-extensions-4.15.0
# deactivate
ln -s /usr/local/bin/ble2mqtt /opt/ble2mqtt/bin/ble2mqtt
deactivate
exit
First, let’s check the Bluetooth chip works on the Raspberry Pi. We can run the bluetoothctl to interact with the Bluetooth device directly:
bluetoothctl
# Agent registered
# [CHG] Controller B8:27:EB:B6:75:B7 Pairable: yes
[bluetooth]# power on
# Changing power on succeeded
[bluetooth]# scan on
# Discovery started
# [CHG] Controller B8:27:EB:B6:75:B7 Discovering: yes
# [NEW] Device DF:7B:9F:4E:05:85 Cat
# [NEW] Device F6:DB:F4:36:68:90 Dog
[bluetooth]# exit
Excellent, let’s create a config file using the MAC addresses we’ve seen. I’ve set up a dedicated user/password on the MQTT broker for ble2mqtt to use, and instruct ble2mqtt to publish to ble2mqtt/rooms/kitchen.
{
"mqtt_host": "192.168.1.171",
"mqtt_port": 1883,
"mqtt_user": "ble2mqtt",
"mqtt_password": "$mysupersecretpassword",
"base_topic": "ble2mqtt/rooms/kitchen",
"legacy_color_mode": false,
"devices": [
{
"address": "df:7b:9f:4e:05:85",
"type": "presence",
"friendly_name": "cat"
},
{
"address": "f6:db:f4:36:68:90",
"type": "presence",
"friendly_name": "dog"
}
]
}
And try running it:
BLE2MQTT_CONFIG=./ble2mqtt.json ble2mqtt
# 2025-10-22 09:43:14 INFO: Starting BLE2MQTT version 0.2.5, bleak 1.1.1, adapter hci0
# 2025-10-22 09:43:14 INFO: Connected to 192.168.1.171
# 2025-10-22 09:43:19 INFO: [Generic_Cat_cat] send state={'linkquality': 69, 'presence': 'ON'}
# 2025-10-22 09:43:20 INFO: [Generic_Dog_dog] send state={'linkquality': 74, 'presence': 'ON'}
Let’s have a look at what is being published. We can use the # topic matcher to request all subpaths under ble2mqtt/rooms/kitchen:
mosquitto_sub -v -h 192.168.1.171 -p 1883 -u heos --pw "$mypw" --topic 'ble2mqtt/rooms/kitchen/#'
# ble2mqtt/rooms/kitchen/bridge/state online
# ble2mqtt/rooms/kitchen/bridge/state online
# ble2mqtt/rooms/kitchen/0xdf7b9f4e0585 {"presence": "ON", "linkquality": 64}
# ble2mqtt/rooms/kitchen/0xf6dbf4366890 {"presence": "ON", "linkquality": 13}
# ble2mqtt/rooms/kitchen/0xdf7b9f4e0585/availability offline
# ble2mqtt/rooms/kitchen/0xf6dbf4366890/availability offline
Once we’re happy with that, we can create a service file in /etc/systemd/system/ble2mqtt.service, adapting the suggestion from the README:
[Unit]
Description=ble2mqtt bridge
Wants=bluetooth.target
[Service]
Type=simple
ENVIRONMENT=BLE2MQTT_CONFIG=/etc/ble2mqtt.json
ExecStart=/usr/local/bin/ble2mqtt
[Install]
WantedBy=multi-user.target
And enable/start the service:
sudo mv ./ble2mqtt.json /etc/ble2mqtt.json
sudo systemctl daemon-reload
sudo systemctl enable --now ble2mqtt
More scanners
One scanner isn’t very useful - we need more than one to see which one is closer!
We can repeat the previous step, tweaking only the base_topic value in /etc/ble2mqtt.json to reflect the different room names.
Feeding the output to mqtt_room
Great, we’ve got messages being published with the following format:
Topic: ble2mqtt/rooms/kitchen/0xdf7b9f4e0585
Payload: {"presence": "ON", "linkquality": 64}
Looking at the mqtt_room docs, for each device we we’re tracking we need to set the following configuration:
sensor:
- platform: mqtt_room
device_id: <device-id>
state_topic: <topic-prefix>
and then need to make sure the following sort of message is published:
Topic: <topic-prefix>/<room>
Payload: { "id": "<device-id>", "distance": <distance> }
mqtt_room will use the topic-prefix to subscribe to messages, then extract the room value from the topics it sees. It will look for messages with the right id value, then record the distance between the beacon and that room.
By comparing the distances for each device ID across different room values, it can then work out what room it’s in!
So we need to do a couple of things:
- Transform the topics and messages into the right ‘shape’ that
mqtt_roomwill understand - Convert
linkqualityinto adistanceestimate
linkquality is an integer between 1 and 255. A better link quality indicates a closer distance. As a first approximation we could just take the inverse value, by calculating 255 / linkquality, so that a linkquality of 255 approximates 1m, 122 approximates 2m, etc. This is a very rough initial approximation, but if all the sensors are identical and have the same line of sight, it gives a good indication of which one is closest.
Because <topic-prefix> is set per-beacon, we have a lot of flexibility in how we arrange the messages. Let’s set up the following scheme:
- The sensor for room
$roompublishes toble2mqtt/rooms/$room/$device_idwith values{ linkquality } - A process subscribes to topics of form
ble2mqtt/rooms/$room/$device_id, then for each message- Extracts the
$linkqualityvalue from the payload - Re-publishes to
ble2mqtt_processed/rooms/$roomwith value{ id: $device_id, distance: (255 / $linkquality }
- Extracts the
My preferred option for this sort of subscribe-and-republish is running node-red as an addon on Home Assistant. Since Home Assistant is also running my MQTT broker, that means node-red is colocated with mosquitto.
An alternative would be to write a shell script that runs somewhere and does this processing.
Let’s set up the node-red flow. We need:
- An mqtt-in node that subscribes to MQTT messages
- A function node that transforms the messages
- An mqtt-out node that publishes the output again
The function body is in Javascript sits as follows:
// Split topic: ble2mqtt/rooms/$room/$device_id
const [,,room,id] = msg.topic.split("/");
// Round to two decimal places
const distance = Math.round(255 * 100 / msg.payload.linkquality) / 100;
return {
topic: `ble2mqtt_processed/rooms/${room}`,
payload: { id, distance }
};
We can check again what is being produced:
mosquitto_sub -v -h 192.168.1.171 -p 1883 -u heos --pw "$mypw" --topic 'ble2mqtt_processed/#'
# ble2mqtt_processed/rooms/bedroom {"id":"0xdf7b9f4e0585","distance":25.5}
# ble2mqtt_processed/rooms/bedroom {"id":"0xf6dbf4366890","distance":null}
# ble2mqtt_processed/rooms/kitchen {"id":"0xdf7b9f4e0585","distance":3.7}
# ble2mqtt_processed/rooms/kitchen {"id":"0xf6dbf4366890","distance":4.55}
Finally, we can configure mqtt_room to pick up these messages. Put the following in your Home Assistant’s configuration.yaml (according to your Home Assistant installation):
sensor:
- platform: mqtt_room
device_id: '0xdf7b9f4e0585'
name: beacon_cat
state_topic: ble2mqtt_processed/rooms
timeout: 120
away_timeout: 120
- platform: mqtt_room
device_id: '0xf6dbf4366890'
name: beacon_dog
state_topic: ble2mqtt_processed/rooms
timeout: 120
away_timeout: 120
And restart Home Assistant.
Did it work?
Seems so! Here’s what my new entity looks like in Home Assistant:
![]()
Was it useful?
About halfway through getting this running, my partner pointed out that the cat doens’t actually have a collar, and so we haven’t got anywhere to attach the beacon.
As a result, I’ve been keeping the beacon in my pocket as I move around the house to see how well it works. Perhaps there’s some clever automations I can come up with, like having music follow me as I wander around.
Otherwise, hopefully this will come in handy with a future cat or dog adoption!