A small Python scanner that uses Bluetooth Low Energy advertisements to estimate which devices are nearby. It includes a command-line interface, a Python API, and a local web dashboard.
It can:
- estimate distance from RSSI;
- classify common phones, audio devices, wearables, and beacons;
- report devices appearing, leaving, or crossing the configured radius;
- track named devices and emit
presentorabsentevents; - show live results in a browser.
Distance and classification are estimates. BLE addresses can rotate, and the radar angle is synthetic. This is a proximity tool, not a people counter.
Python 3.11 or newer is required.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e .On macOS, grant Bluetooth access to the terminal or IDE. Linux and Raspberry Pi need BlueZ and a working Bluetooth adapter.
Start the scanner and dashboard:
radio-presence-scanner --serverOpen http://127.0.0.1:8787.
The dashboard has a live radar, filters, device details, exports, and RSSI calibration. It receives updates through Server-Sent Events and falls back to polling when necessary.
The supplied configuration starts the dashboard in phone-only mode. Use all BLE devices for the current run with:
radio-presence-scanner --server --all-devicesRun one scan in the terminal:
radio-presence-scannerOr scan continuously:
radio-presence-scanner --loopUseful options:
--config PATH
--phones-only
--all-devices
--scan-seconds 8
--log-limit 50
--server-host 127.0.0.1
--server-port 8787
Edit config.yaml. The main settings are:
ble_scan_seconds: 4.0
max_missed_scans: 0
radius_m: 2.0
ema_alpha: 0.55
tx_power_default: -59
path_loss_n: 3.0
server:
phones_only_default: trueradius_m defines the nearby boundary. max_missed_scans controls how many missed scans are tolerated before a device disappears. The RSSI calibration tool in the dashboard can suggest values for tx_power_default, path_loss_n, and ema_alpha.
The full file also contains commented examples for named targets, notifications, storage, and identifier hashing.
Named targets match a BLE address or device-name regular expression. Target transitions are available in the Python result and can be sent to callbacks, a webhook, or MQTT.
MQTT is disabled unless notifications.mqtt.host is set. Install its optional dependency with:
python -m pip install -e '.[mqtt]'SQLite history is also disabled by default. It uses Python's built-in SQLite support and needs no database server:
storage:
enabled: true
path: radio-presence-scanner.sqlite3
retention_hours: 168When enabled, SQLite stores scan snapshots and target events for history, daily statistics, and export.
Device keys can be hashed before they reach the dashboard or database:
privacy:
hash_identifiers: true
identifier_salt: "replace-with-a-private-random-value"Hashing does not remove device names or make BLE observations anonymous. The HTTP server has no authentication or TLS, so keep it on localhost or place it behind a trusted reverse proxy.
Each call to sample() performs one BLE scan. The library does not start a background process.
import asyncio
from radio_presence_scanner import PresenceDetector
async def main() -> None:
detector = PresenceDetector.from_config_file("config.yaml")
result = await detector.sample()
print(result.snapshot.nearby_devices_count)
print(result.snapshot.devices)
asyncio.run(main())Synchronous code can use sample_sync(). Pass phones_only=True to either method to return the phone-only snapshot.
Target callbacks can be synchronous or asynchronous:
detector.add_event_listener(lambda event: print(event.target_id, event.event))Server mode exposes:
GET /api/state
GET /api/events
GET /api/health
GET /api/history
GET /api/stats/daily
GET /api/export?format=csv|json
GET /api/calibration
POST /api/settings
POST /api/calibration/sample
POST /api/calibration/reset
POST /api/history/reset
History and daily statistics are empty when SQLite is disabled. Export still returns the current snapshot.
Install and enable Bluetooth if the adapter is not ready:
sudo apt update
sudo apt install -y bluez bluetooth
sudo systemctl enable --now bluetooth
sudo rfkill unblock bluetooth
sudo bluetoothctl power onCheck it with:
bluetoothctl show
bluetoothctl --timeout 12 scan onpython -m pip install -e '.[dev]'
ruff check src tests
PYTHONPATH=src python -m unittest discover -s tests -vMIT. See LICENSE.