Automatically reconnect paired Bluetooth devices β event-driven, systemd-native, zero-configuration.
Overview β’ Install β’ Usage β’ Hooks β’ Config β’ Architecture β’ Platform Status β’ Roadmap β’ Troubleshooting β’ Contributing
Most Linux Bluetooth tools reconnect devices only when a desktop session is running. bluetooth-autoconnect is different β it works at the system level, before any user logs in, with no desktop environment required.
- Runs as a systemd service β reconnects headphones, mice, keyboards, and speakers at boot
- Talks to BlueZ directly over D-Bus β no
bluetoothctlshelling, no polling - Fixes the "silent return" gap β catches devices that wake up without firing a D-Bus event via periodic background scanning
- Backs off per device, independently β a flaky headset does not delay your keyboard from reconnecting
- Fires custom scripts on connect/disconnect events via a hook system
- Built on a cross-platform backend architecture β Linux is fully supported today; Windows support is in active development
| Feature | Details |
|---|---|
| Instant event-driven reconnect | Subscribes to BlueZ D-Bus signals; reacts within milliseconds of adapter power-on, device appearance, or disconnect |
| Periodic background scan | Wakes every 30 s (configurable) to catch devices that return silently |
| Per-device exponential backoff | 1 min β 2 β 4 β 8 β 16 min cap; each MAC address is tracked independently |
| Multi-adapter support | Scans all powered adapters simultaneously |
| Hook system | Run custom scripts automatically on connect or disconnect events |
| Safe by default | Only connects Paired: yes + Trusted: yes devices; never pairs automatically |
| systemd-native | System-wide and per-user service units; structured journal logging |
| Doctor command | bluetooth-autoconnect doctor β instant PASS/FAIL system diagnostics |
| Cross-platform architecture | BluetoothBackend protocol ready for Linux, Windows, and future platforms |
| Zero native extensions | Pure Python; no C extensions or compiled dependencies |
Clones the repo, installs system dependencies, creates an isolated virtualenv, configures systemd, and starts the service:
git clone https://github.com/Zero-day-Exploit-np/bluetooth-autoconnect.git
cd bluetooth-autoconnect
sudo bash scripts/install.shThe service starts immediately. Devices reconnect automatically from this point on.
pip install bluetooth-autoconnectRequires Python 3.10+.
For structured journal logging, add the optional extra:pip install "bluetooth-autoconnect[journal]"
After installing via PyPI, enable the systemd service manually:
sudo install -Dm644 systemd/bluetooth-autoconnect.service \
/usr/lib/systemd/system/bluetooth-autoconnect.service
sudo systemctl daemon-reload
sudo systemctl enable --now bluetooth-autoconnectFor package-manager-managed installs with proper dependency tracking:
| Distro family | Build guide |
|---|---|
| Debian / Ubuntu / Kali / Mint | packaging/debian/README.md |
| Arch Linux / Manjaro | packaging/arch/README.md |
| Fedora / openSUSE | packaging/fedora/README.md |
| Distribution | Status | Package manager |
|---|---|---|
| Ubuntu 22.04 / 24.04 | β Tested | apt |
| Debian 12 (Bookworm) | β Tested | apt |
| Kali Linux (rolling) | β Tested | apt |
| Linux Mint 21+ | β Tested | apt |
| Fedora 39 / 40 | β Tested | dnf |
| Arch Linux | β Tested | pacman |
| Manjaro | β Tested | pacman |
| openSUSE Tumbleweed | β Tested | zypper |
Requirements: Python 3.10+, BlueZ β₯ 5.x, systemd, D-Bus.
See Project Status for the full cross-platform readiness matrix.
bluetooth-autoconnect [--daemon] [--debug] [--rescan-interval SECONDS]
[--max-attempts N] [--max-concurrency N]
[--backend NAME] [--config FILE] [--version]
[doctor]
| Command | Behaviour |
|---|---|
bluetooth-autoconnect |
One-shot scan β connect all paired+trusted devices once, then exit |
bluetooth-autoconnect --daemon |
Run continuously; reconnects via backend events and periodic scans |
bluetooth-autoconnect doctor |
Health-check diagnostics; prints PASS/FAIL for every system component |
bluetooth-autoconnect --version |
Print the installed version and exit |
| Flag | Default | Description |
|---|---|---|
--debug |
off | Structured DEBUG-level logging with per-device fields |
--max-attempts N |
5 |
Connect attempts per device before giving up |
--max-concurrency N |
5 |
Maximum simultaneous connect attempts |
--rescan-interval SECONDS |
30 |
Periodic background scan interval (0 = disable) |
--backend NAME |
auto | Force a specific backend: linux or windows |
--config FILE |
/etc/bluetooth-autoconnect/config.yaml |
Path to configuration file |
| Code | Meaning |
|---|---|
0 |
All eligible devices connected (or none were needed) |
1 |
At least one eligible device failed to connect |
2 |
Fatal backend / startup error |
130 |
Interrupted by Ctrl-C |
# Connect everything right now (one-shot)
bluetooth-autoconnect
# Run as a daemon (the same mode the systemd service uses)
bluetooth-autoconnect --daemon
# Debug a device that isn't reconnecting β structured per-attempt logs
bluetooth-autoconnect --daemon --debug
# Scan every 10 s instead of every 30 s
bluetooth-autoconnect --daemon --rescan-interval 10
# Disable periodic scanning; rely only on D-Bus events
bluetooth-autoconnect --daemon --rescan-interval 0
# Allow more retries for a flaky headset
bluetooth-autoconnect --max-attempts 10
# Run system health checks before troubleshooting
bluetooth-autoconnect doctorbluetooth-autoconnect uses two complementary mechanisms to ensure devices reconnect as reliably as possible.
The daemon subscribes to BlueZ D-Bus signals and fires an immediate reconnect attempt on any of these events:
| Signal | Trigger |
|---|---|
Adapter.Powered = true |
Bluetooth adapter switched on |
InterfacesAdded (Device) |
Known device object appeared on the bus |
Device.Connected = false |
A connected device dropped off |
Device.RSSI updated |
Device advertisement received β it's back in range |
Device.Trusted = true |
Device was just marked trusted |
Device.Paired = true |
Device was just paired |
Some devices return to range without firing any D-Bus event β slowly waking headphones, congested RF environments, or BLE devices with long advertisement intervals. The periodic scanner catches these.
t=0s Device disconnects β immediate attempt (fails: page-timeout)
t=1s Backoff: wait 60 s
t=61s Periodic scan β still unreachable β fail (backoff: 120 s)
Β·Β·Β·
t=Xm Device silently returns to range
t=Xm+30s Periodic scan β reconnect succeeds β backoff cleared β
| Consecutive failures | Wait before next attempt |
|---|---|
| 1 | 1 minute |
| 2 | 2 minutes |
| 3 | 4 minutes |
| 4 | 8 minutes |
| 5+ | 16 minutes (hard cap: 30 minutes) |
Backoff resets immediately on successful reconnect, an RSSI signal, or Device.Connected = true.
sudo systemctl kill -s SIGHUP bluetooth-autoconnectA device is auto-connected only when BlueZ reports both:
Paired: yesβ pairing handshake completed, andTrusted: yesβ device marked trusted
Everything else is skipped silently. bluetooth-autoconnect never pairs or trusts devices automatically.
To trust an already-paired device:
bluetoothctl trust AA:BB:CC:DD:EE:FFRun custom scripts automatically when a device connects or disconnects. Hooks execute asynchronously β a slow or failing script cannot stall the daemon.
# /etc/bluetooth-autoconnect/config.yaml
hooks:
timeout_seconds: 30 # kill script after this many seconds (0 = no limit)
on_connect:
- /usr/local/bin/bt-connected.sh
on_disconnect:
- /usr/local/bin/bt-disconnected.shEvery hook script receives the triggering event through environment variables:
| Variable | Example | Description |
|---|---|---|
BT_EVENT |
connected |
connected or disconnected |
BT_DEVICE_MAC |
AA:BB:CC:DD:EE:FF |
MAC address of the device |
BT_DEVICE_NAME |
JBL Speaker |
Human-readable device name |
BT_DEVICE_PATH |
/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF |
Backend device path |
BT_ADAPTER_PATH |
/org/bluez/hci0 |
Backend adapter path |
#!/usr/bin/env bash
# /usr/local/bin/bt-connected.sh
set -euo pipefail
echo "$(date): $BT_DEVICE_NAME connected ($BT_DEVICE_MAC)" >> /var/log/bt-events.log
# Route audio to the Bluetooth device
if command -v pactl &>/dev/null; then
pactl set-default-sink bluez_sink.${BT_DEVICE_MAC//:/_}.a2dp_sink 2>/dev/null || true
fiMake scripts executable:
chmod +x /usr/local/bin/bt-connected.shHook failures are logged and swallowed β they will never crash the daemon.
The config file lives at /etc/bluetooth-autoconnect/config.yaml and is never overwritten by updates.
retry:
max_attempts: 5 # connect attempts per device before giving up
base_delay: 1.0 # seconds before the first retry
max_delay: 60.0 # cap on per-attempt delay
multiplier: 2.0 # exponential backoff multiplier
daemon:
rescan_interval_seconds: 30 # periodic background scan interval (0 = disabled)
max_concurrency: 5 # maximum simultaneous connect attempts
logging:
level: INFO # set to DEBUG for verbose per-device logs
hooks:
timeout_seconds: 30
on_connect:
- /usr/local/bin/bt-connected.sh
on_disconnect:
- /usr/local/bin/bt-disconnected.sh
# Per-device connection priority (higher = connect first)
# device_priorities:
# AA:BB:CC:DD:EE:FF: 250
# Prevent specific devices from ever auto-connecting
# blacklist:
# - AA:BB:CC:DD:EE:FFbluetooth-autoconnect v1.2.0 introduces a clean platform abstraction layer. All reconnection logic, backoff tracking, and hook execution live in the platform-agnostic core. Only the backend layer touches OS-specific APIs.
βββββββββββββββββββββββββββββββββββββββββββββββ
β CLI (cli.py) β
β AutoConnectDaemon (daemon.py) β
β Connector Β· Hooks Β· Config Β· Doctor β
β ββ Core Logic ββ β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β BluetoothBackend β β Protocol (backends/__init__.py)
β (Protocol) β
ββββββββββββ¬ββββββββββββ
β
βββββββββββ΄βββββββββββ
β β
βΌ βΌ
βββββββββββββββββββ βββββββββββββββββββββββ
β LinuxBackend β β WindowsBackend β
β (backends/ β β (backends/ β
β linux.py) β β windows.py) β
β β β β
β BlueZ D-Bus β β WinRT APIs β
β dbus-next β β (architecture β
β β
Production β β ready, WinRT β
β β β impl in progress) β
βββββββββββββββββββ βββββββββββββββββββββββ
The BluetoothBackend protocol defines six methods every backend must implement:
class BluetoothBackend(Protocol):
async def connect(self) -> None: ...
async def close(self) -> None: ...
async def get_adapters(self) -> list[Adapter]: ...
async def get_devices(self, adapter_path=None) -> list[Device]: ...
async def connect_device(self, device_path: str) -> None: ...
async def subscribe(self, callback: EventCallback) -> None: ...The daemon receives a backend instance at startup via dependency injection:
daemon = AutoConnectDaemon(backend=create_backend())create_backend() auto-detects the current platform. You can override it:
bluetooth-autoconnect --daemon --backend linux| Platform | Status | Backend | Notes |
|---|---|---|---|
| Linux (BlueZ) | β Fully Supported | LinuxBackend |
Production-ready since v1.0.0 |
| Windows | π§ Architecture Ready | WindowsBackend |
Backend skeleton in place; WinRT implementation in progress (v1.3.0+) |
| macOS | β Not Supported | β | No CoreBluetooth backend yet; contributions welcome |
| Metric | Value |
|---|---|
| Total automated tests | 262 |
| Code coverage | β₯ 90% |
| Platforms tested in CI | Linux (Ubuntu, Fedora, Arch) |
| Type checking | mypy strict β zero errors |
| Linting | ruff β zero warnings |
# Status
sudo systemctl status bluetooth-autoconnect
# Start / stop / restart
sudo systemctl start bluetooth-autoconnect
sudo systemctl stop bluetooth-autoconnect
sudo systemctl restart bluetooth-autoconnect
# Enable / disable at boot
sudo systemctl enable bluetooth-autoconnect
sudo systemctl disable bluetooth-autoconnect
# Stream live logs
journalctl -u bluetooth-autoconnect -f
# Trigger immediate full rescan without restarting
sudo systemctl kill -s SIGHUP bluetooth-autoconnect# Enable at login
systemctl --user enable --now bluetooth-autoconnect
# Stream live logs
journalctl --user -u bluetooth-autoconnect -fbluetooth-autoconnect doctor checks every prerequisite and prints a clear PASS/FAIL/WARN report:
bluetooth-autoconnect doctor
[PASS] bluetooth.service β active
[PASS] D-Bus system bus β socket reachable at /run/dbus/system_bus_socket
[PASS] Bluetooth backend β backend available
[PASS] Adapter hci0 β powered β address=AA:BB:CC:DD:EE:FF
[PASS] Paired devices β 3 paired device(s) found
[PASS] Trusted device: JBL Speaker β connected β mac=AA:BB:CC:DD:EE:FF
[WARN] Trusted device: Sony WH-1000XM5 β not connected β mac=11:22:33:44:55:66
All checks passed.
Exit code 0 = all checks passed. Exit code 1 = at least one hard failure.
cd bluetooth-autoconnect
sudo bash scripts/update.shPulls the latest source, upgrades the package, refreshes systemd units, and restarts the service.
cd bluetooth-autoconnect
sudo bash scripts/uninstall.shRemoves the service, binary symlink, and virtualenv. Your Bluetooth pairing data in BlueZ is never touched.
BlueZ is not running:
sudo systemctl enable --now bluetooth
sudo systemctl status bluetooth- Check the device is paired and trusted:
bluetoothctl info AA:BB:CC:DD:EE:FF # Must show: Paired: yes Trusted: yes - If
Trusted: no:bluetoothctl trust AA:BB:CC:DD:EE:FF - Run with
--debugto see per-attempt structured logs
The system-wide service has full BlueZ access. The per-user service may need group membership:
sudo usermod -aG bluetooth "$USER"
# Log out and back inThe periodic scanner handles this (enabled by default). If you've disabled it:
bluetooth-autoconnect --daemon --rescan-interval 30Or in the config file:
daemon:
rescan_interval_seconds: 30Upgrade to v1.1.1 or later. Earlier versions had a bug where reconnect attempts fired false-positive on_connect hooks before profile negotiation completed.
For the full troubleshooting guide, see docs/TROUBLESHOOTING.md.
git clone https://github.com/Zero-day-Exploit-np/bluetooth-autoconnect.git
cd bluetooth-autoconnect
make venv
source .venv/bin/activate| Task | Command |
|---|---|
| Run test suite (coverage β₯ 90%) | make test |
| Lint | make lint |
| Auto-format | make format |
| Type-check | make typecheck |
| Build wheel | make build |
src/bluetooth_autoconnect/
βββ backends/
β βββ __init__.py BluetoothBackend protocol + create_backend() factory
β βββ linux.py LinuxBackend β BlueZ D-Bus via dbus-next
β βββ windows.py WindowsBackend β WinRT skeleton (in progress)
βββ cli.py Argument parsing and main entry point
βββ connector.py Retry / backoff / concurrency logic
βββ daemon.py Event loop, periodic scan, signal handling
βββ dbus_client.py Backward-compat shim β backends/linux.py
βββ doctor.py Health-check diagnostics
βββ exceptions.py Exception hierarchy
βββ hooks.py Hook execution engine
βββ logging_setup.py stdout + journal logging
βββ models.py Adapter / Device dataclasses
tests/ 262 pytest tests β no real D-Bus required
systemd/ System + user service units
scripts/ install.sh uninstall.sh update.sh
packaging/ debian/ arch/ fedora/
docs/ INSTALL.md TROUBLESHOOTING.md FAQ.md
Implement the BluetoothBackend protocol from backends/__init__.py:
from bluetooth_autoconnect.backends import BluetoothBackend
class MyBackend:
async def connect(self) -> None: ...
async def close(self) -> None: ...
async def get_adapters(self) -> list[Adapter]: ...
async def get_devices(self, adapter_path=None) -> list[Device]: ...
async def connect_device(self, device_path: str) -> None: ...
async def subscribe(self, callback: EventCallback) -> None: ...Register it in create_backend() in backends/__init__.py and it will be available as --backend myplatform.
- Windows adapter enumeration via
Windows.Devices.Radios - Windows device enumeration (Classic Bluetooth + BLE)
- Windows adapter detection in
doctor - Basic
--backend windowssupport
- Windows connection status event subscriptions via WinRT
- RFCOMM and GATT connection support
- Periodic scanner on Windows
- Windows
doctorhealth checks
- Fully supported Linux + Windows
- Windows native packaging (
.exe,.msi) - Windows service integration (Windows Service API)
- CI testing on Windows runners
- macOS backend investigation
Note: Linux support is not affected by any of this work. The Linux backend is stable, production-ready, and changes to the backend architecture are fully backward-compatible.
BluetoothBackendProtocol β typed interface all backends must satisfyLinuxBackendβ the existing BlueZ/D-Bus implementation, extracted tobackends/linux.pyWindowsBackendskeleton β architecture in place; WinRT implementation follows in v1.3.0+create_backend()factory β auto-detects the current platform; accepts--backendoverride- Backend-agnostic daemon β
AutoConnectDaemonaccepts anyBluetoothBackendvia dependency injection - Backend-agnostic doctor β platform pre-checks (D-Bus on Linux, WinRT on Windows) selected at runtime
dbus_client.pyshim β backward-compatible re-export; existing code and tests unchanged- 262 automated tests β all passing; 90%+ coverage
- Fixed duplicate
on_connect/on_disconnectnotifications from repeated BlueZConnected=Falsesignals during profile negotiation - State tracker (
_DeviceStateTracker) ensures hooks fire only on genuine state transitions
- Execute custom scripts on device connect and disconnect
- Scripts receive device context via
BT_*environment variables - Configurable per-hook timeout with automatic process kill
- Full stdout/stderr capture and structured logging
- Event-driven reconnect via BlueZ D-Bus
- Periodic background scanner for silent device returns
- Per-device exponential backoff
- systemd system-wide and per-user service units
bluetooth-autoconnect doctordiagnostics- 90%+ test coverage from day one
Contributions are welcome. Please:
- Fork the repository
- Create a feature branch β
git checkout -b feat/my-feature - Add tests for new behaviour
- Verify
make test lint typecheckall pass - Open a pull request with a clear description
Bug reports: use the issue tracker and include the output of bluetooth-autoconnect doctor and journalctl -u bluetooth-autoconnect --since "1 hour ago".
Windows contributors: the WindowsBackend skeleton in backends/windows.py is ready for WinRT implementation. See the architecture section for the interface contract and the roadmap for the planned scope. Contributions for v1.3.0 are especially welcome.
MIT β see LICENSE.
Bikram Kumar Das Β· github.com/Zero-day-Exploit-np Β· bikramkumardas@proton.me
If bluetooth-autoconnect saves you frustration, consider giving it a β on GitHub.
