bluetooth-autoconnect
Automatically reconnect paired Bluetooth devices — event-driven, systemd-native, zero-configuration.
Overview • Install • Usage • Hooks • Config • Architecture • Platform Status • Roadmap • Troubleshooting • Contributing
What is bluetooth-autoconnect?
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 Highlights
| 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 |
📦 Installation
Option 1 — One-command installer (recommended)
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.sh
The service starts immediately. Devices reconnect automatically from this point on.
Option 2 — PyPI
pip install bluetooth-autoconnect
Requires 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-autoconnect
Option 3 — Native distro packages
For 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 |
🖥️ Supported Platforms
Linux distributions
| 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.
Platform overview
See Project Status for the full cross-platform readiness matrix.
🚀 Usage
bluetooth-autoconnect [--daemon] [--debug] [--rescan-interval SECONDS]
[--max-attempts N] [--max-concurrency N]
[--backend NAME] [--config FILE] [--version]
[doctor]
Commands
| 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 |
Flags
| 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 |
Exit codes
| 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 |
Examples
# 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 doctor
🔄 Automatic Reconnect Behavior
bluetooth-autoconnect uses two complementary mechanisms to ensure devices reconnect as reliably as possible.
1 — Event-driven reconnect (instant)
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 |
2 — Periodic background scan (the silent-return fix)
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 ✓
Per-device backoff schedule
| 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.
Force an immediate rescan
sudo systemctl kill -s SIGHUP bluetooth-autoconnect
🔧 Device Selection
A 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:FF
🪝 Hooks
Run custom scripts automatically when a device connects or disconnects. Hooks execute asynchronously — a slow or failing script cannot stall the daemon.
Configuration
# /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.sh
Environment variables
Every 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 |
Example script
#!/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
fi
Make scripts executable:
chmod +x /usr/local/bin/bt-connected.sh
Hook failures are logged and swallowed — they will never crash the daemon.
⚙️ Configuration
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:FF
🏗️ Architecture
bluetooth-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
📊 Project Status
Platform support matrix
| 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 |
Test coverage
| 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 |
🛠️ Service Management
System-wide service (recommended)
# 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
Per-user service
# Enable at login
systemctl --user enable --now bluetooth-autoconnect
# Stream live logs
journalctl --user -u bluetooth-autoconnect -f
🔍 Diagnostics
bluetooth-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.
🔁 Update
cd bluetooth-autoconnect
sudo bash scripts/update.sh
Pulls the latest source, upgrades the package, refreshes systemd units, and restarts the service.
🗑️ Uninstall
cd bluetooth-autoconnect
sudo bash scripts/uninstall.sh
Removes the service, binary symlink, and virtualenv. Your Bluetooth pairing data in BlueZ is never touched.
❓ Troubleshooting
org.bluez is not available on the system bus
BlueZ is not running:
sudo systemctl enable --now bluetooth
sudo systemctl status bluetooth
Devices are found but never connect
- 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
Permission denied on Connect()
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 in
Daemon doesn't react when a device comes back into range
The periodic scanner handles this (enabled by default). If you've disabled it:
bluetooth-autoconnect --daemon --rescan-interval 30
Or in the config file:
daemon:
rescan_interval_seconds: 30
Notifications firing in a loop (connect/disconnect cycling)
Upgrade 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.
👩💻 Development
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 |
Project layout
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
Adding a new backend
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.
🗺️ Roadmap
v1.3.0 — Windows device discovery
- Windows adapter enumeration via
Windows.Devices.Radios - Windows device enumeration (Classic Bluetooth + BLE)
- Windows adapter detection in
doctor - Basic
--backend windowssupport
v1.4.0 — Windows reconnect
- Windows connection status event subscriptions via WinRT
- RFCOMM and GATT connection support
- Periodic scanner on Windows
- Windows
doctorhealth checks
v2.0.0 — Official multi-platform release
- 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.
📋 Release Highlights
v1.2.0 — Cross-platform backend architecture
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
v1.1.1 — Hook notification stability
- Fixed duplicate
on_connect/on_disconnectnotifications from repeated BlueZConnected=Falsesignals during profile negotiation - State tracker (
_DeviceStateTracker) ensures hooks fire only on genuine state transitions
v1.1.0 — Hook system
- 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
v1.0.0 — Initial release
- 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
🤝 Contributing
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.
📄 License
MIT — see LICENSE.
👤 Author
Bikram Kumar Das · github.com/Zero-day-Exploit-np · bikramkumardas@proton.me
If bluetooth-autoconnect saves you frustration, consider giving it a ⭐ on GitHub.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bluetooth_autoconnect-1.2.0.tar.gz.
File metadata
- Download URL: bluetooth_autoconnect-1.2.0.tar.gz
- Upload date:
- Size: 79.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4db1665cd932c0b9cb8212753fea1048eaf45504151caba9e9597852af0d65c8
|
|
| MD5 |
237e9267c971e45ede5c9d1c2b2923bf
|
|
| BLAKE2b-256 |
b1ff5933f736fa043cc40de44eb82f9c59c1e5d89774638299927a2b1cc23c53
|
File details
Details for the file bluetooth_autoconnect-1.2.0-py3-none-any.whl.
File metadata
- Download URL: bluetooth_autoconnect-1.2.0-py3-none-any.whl
- Upload date:
- Size: 48.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80557022b5ec8b55785e9ba6296ee27e5721b356e5adf3f8d37179bdd7d86f7d
|
|
| MD5 |
e85722e47e37c691facd0ef97103252b
|
|
| BLAKE2b-256 |
38614193ebd7f433cec3e83c78f58c394ba17ef04b6c1a2f2c49ba99ca64ee15
|