Universal BLE Smart Scale bridge. Captures body composition from Renpho, Xiaomi & 20+ others, syncs to Garmin Connect, Strava, MQTT (Home Assistant), InfluxDB, Webhooks, Ntfy & local files (CSV/JSONL). Headless CLI for Raspberry Pi, Linux, macOS & Windows.
170
stars
994
commits
TypeScript
primary language
Sep 9, 2026
updated
A cross-platform CLI tool that reads body composition data from 25+ BLE smart scales and exports to Garmin Connect, Strava, Intervals.icu, Runalyze, Wger, MQTT (Home Assistant), InfluxDB, Webhooks, Ntfy, Telegram, and local files (CSV/JSONL). No phone app needed. Your data stays on your device.
Documentation · Getting Started · Supported Scales · Exporters · FAQ
Most BLE smart scales measure weight and body impedance over Bluetooth, but their companion apps have no way to sync data to Garmin Connect. The only workflow was: open the phone app, wait for it to sync, then manually type the numbers into Garmin. Every single time.
I didn't want to depend on a phone app. So I built this tool. A Raspberry Pi Zero 2W sits next to the scale, always on, always listening. Step on the scale, wait a few seconds, and the reading appears in Garmin Connect - no phone needed, no app, no manual entry. It just works.
If you can't have a Pi next to your scale, a cheap ESP32 proxy can sit nearby and relay BLE data over WiFi to a Docker server anywhere on your network. See the ESP32 BLE proxy guide.
# Configure
docker run --rm -it --network host --cap-add NET_ADMIN --cap-add NET_RAW \
--group-add "$(getent group bluetooth | cut -d: -f3)" -v /var/run/dbus:/var/run/dbus:ro \
-v ./config.yaml:/app/config.yaml \
-v ./garmin-tokens:/app/garmin-tokens \
ghcr.io/kristianp26/ble-scale-sync:latest setup
# Run (continuous mode, auto-restart)
docker run -d --restart unless-stopped --network host \
--cap-add NET_ADMIN --cap-add NET_RAW \
--group-add "$(getent group bluetooth | cut -d: -f3)" --device /dev/rfkill \
-v /var/run/dbus:/var/run/dbus:ro \
-v ./config.yaml:/app/config.yaml:ro \
-v ./garmin-tokens:/app/garmin-tokens:ro \
-e CONTINUOUS_MODE=true \
ghcr.io/kristianp26/ble-scale-sync:latest
Ideal for Raspberry Pi, NAS, and headless servers. Works alongside any Home Assistant install (Container, Core, OS) via MQTT auto-discovery.
If you run Home Assistant OS or Supervised, one click is all it takes:
The badge opens your Home Assistant instance, confirms the repository, and shows BLE Scale Sync in the Add-on Store ready to install.
https://github.com/KristianP26/ble-scale-sync and install BLE Scale SyncThe add-on handles config through the UI, auto-detects the Mosquitto broker for Home Assistant auto-discovery, and bootstraps Garmin tokens on first start. See the Home Assistant Add-on guide for the full option reference, MFA workaround, and custom config mode.
Note: Add-ons are not available on HA Container or HA Core installs (no Supervisor). Use the Docker method above instead. Sensors still appear in HA via MQTT auto-discovery.
Runs natively on all major desktop and server operating systems. No containers required.
Zero clone, if you already have Node.js v22+:
npx ble-scale-sync setup # interactive wizard, writes ./config.yaml
CONTINUOUS_MODE=true npx ble-scale-sync # always-on
config.yaml and .env are read from the directory you run the command in. Add npm install -g ble-scale-sync to keep the command on your PATH.
Or from a clone, which is what you want if you plan to contribute:
git clone https://github.com/KristianP26/ble-scale-sync.git
cd ble-scale-sync && npm install
npm run setup # interactive wizard
CONTINUOUS_MODE=true npm start # always-on
Requires Node.js v22+ and a BLE adapter. See the full install guide for prerequisites and systemd service setup.
ble-scale-sync diagnose (or npm run diagnose from a clone) for detailed BLE troubleshooting.ble.scale_mac for GATT-only scales (for example QN-Scale) so the ESP32 can connect autonomously the instant it sees the scale. It connects to both public-address and random-address GATT scales even when the controller misreports the address type, and connects with minimal delay so a briefly-connectable scale is reached before it sleeps.ble.adapter: hci1 for multi-adapter setups (Linux).Type=notify integration for whole-loop freezes. The watchdog is idle-aware, so a scale that only advertises while in use does not trigger needless restarts when nobody is weighing.update_check: false); see the auto update guide for Watchtower, systemd timer, and HA add-on recipes.For reverse-engineering a scale protocol (e.g. decoding an undecoded impedance frame), the native BlueZ handler can dump every notify frame as hex and hold the GATT connection open past the weight-stable point so the scale finishes sending its trailing frames before disconnect.
BLE_RAW_CAPTURE=true enables it (off by default; no effect on normal runs).BLE_RAW_CAPTURE_HOLD_SEC=20 sets how long to hold the link after the weight
stabilizes (default 20s, keep it under 120).# native
BLE_RAW_CAPTURE=true DEBUG=true CONTINUOUS_MODE=true npm start
# docker
docker run ... -e BLE_RAW_CAPTURE=true -e DEBUG=true ghcr.io/kristianp26/ble-scale-sync:dev
Step on the scale and look for [BLE] [RAW] <uuid> (<n>B): <hex> lines. The run
still exports a weight-only reading. Applies to every GATT reading path (native
BlueZ and noble, plus the ESP32 and ESPHome proxies).
KristianP26 | APIUM | marcelorodrigo | fromport | boildead | alexw23 | bondesen | junaidk |
hedoric | alasano | O-Palda | Bretos | albert-canfield | JamieSBenson |
See CONTRIBUTING.md for development setup, project structure, and how to add new scale adapters or exporters.
GPL-3.0. See LICENSE for details.
TypeScript
89.5%
Python
6.4%
C
2.5%
Shell
1.3%
Universal BLE Smart Scale bridge. Captures body composition from Renpho, Xiaomi & 20+ others, syncs to Garmin Connect, Strava, MQTT (Home Assistant), InfluxDB, Webhooks, Ntfy & local files (CSV/JSONL). Headless CLI for Raspberry Pi, Linux, macOS & Windows.
170
stars
994
commits
TypeScript
primary language
Sep 9, 2026
updated
A cross-platform CLI tool that reads body composition data from 25+ BLE smart scales and exports to Garmin Connect, Strava, Intervals.icu, Runalyze, Wger, MQTT (Home Assistant), InfluxDB, Webhooks, Ntfy, Telegram, and local files (CSV/JSONL). No phone app needed. Your data stays on your device.
Documentation · Getting Started · Supported Scales · Exporters · FAQ
Most BLE smart scales measure weight and body impedance over Bluetooth, but their companion apps have no way to sync data to Garmin Connect. The only workflow was: open the phone app, wait for it to sync, then manually type the numbers into Garmin. Every single time.
I didn't want to depend on a phone app. So I built this tool. A Raspberry Pi Zero 2W sits next to the scale, always on, always listening. Step on the scale, wait a few seconds, and the reading appears in Garmin Connect - no phone needed, no app, no manual entry. It just works.
If you can't have a Pi next to your scale, a cheap ESP32 proxy can sit nearby and relay BLE data over WiFi to a Docker server anywhere on your network. See the ESP32 BLE proxy guide.
# Configure
docker run --rm -it --network host --cap-add NET_ADMIN --cap-add NET_RAW \
--group-add "$(getent group bluetooth | cut -d: -f3)" -v /var/run/dbus:/var/run/dbus:ro \
-v ./config.yaml:/app/config.yaml \
-v ./garmin-tokens:/app/garmin-tokens \
ghcr.io/kristianp26/ble-scale-sync:latest setup
# Run (continuous mode, auto-restart)
docker run -d --restart unless-stopped --network host \
--cap-add NET_ADMIN --cap-add NET_RAW \
--group-add "$(getent group bluetooth | cut -d: -f3)" --device /dev/rfkill \
-v /var/run/dbus:/var/run/dbus:ro \
-v ./config.yaml:/app/config.yaml:ro \
-v ./garmin-tokens:/app/garmin-tokens:ro \
-e CONTINUOUS_MODE=true \
ghcr.io/kristianp26/ble-scale-sync:latest
Ideal for Raspberry Pi, NAS, and headless servers. Works alongside any Home Assistant install (Container, Core, OS) via MQTT auto-discovery.
If you run Home Assistant OS or Supervised, one click is all it takes:
The badge opens your Home Assistant instance, confirms the repository, and shows BLE Scale Sync in the Add-on Store ready to install.
https://github.com/KristianP26/ble-scale-sync and install BLE Scale SyncThe add-on handles config through the UI, auto-detects the Mosquitto broker for Home Assistant auto-discovery, and bootstraps Garmin tokens on first start. See the Home Assistant Add-on guide for the full option reference, MFA workaround, and custom config mode.
Note: Add-ons are not available on HA Container or HA Core installs (no Supervisor). Use the Docker method above instead. Sensors still appear in HA via MQTT auto-discovery.
Runs natively on all major desktop and server operating systems. No containers required.
Zero clone, if you already have Node.js v22+:
npx ble-scale-sync setup # interactive wizard, writes ./config.yaml
CONTINUOUS_MODE=true npx ble-scale-sync # always-on
config.yaml and .env are read from the directory you run the command in. Add npm install -g ble-scale-sync to keep the command on your PATH.
Or from a clone, which is what you want if you plan to contribute:
git clone https://github.com/KristianP26/ble-scale-sync.git
cd ble-scale-sync && npm install
npm run setup # interactive wizard
CONTINUOUS_MODE=true npm start # always-on
Requires Node.js v22+ and a BLE adapter. See the full install guide for prerequisites and systemd service setup.
ble-scale-sync diagnose (or npm run diagnose from a clone) for detailed BLE troubleshooting.ble.scale_mac for GATT-only scales (for example QN-Scale) so the ESP32 can connect autonomously the instant it sees the scale. It connects to both public-address and random-address GATT scales even when the controller misreports the address type, and connects with minimal delay so a briefly-connectable scale is reached before it sleeps.ble.adapter: hci1 for multi-adapter setups (Linux).Type=notify integration for whole-loop freezes. The watchdog is idle-aware, so a scale that only advertises while in use does not trigger needless restarts when nobody is weighing.update_check: false); see the auto update guide for Watchtower, systemd timer, and HA add-on recipes.For reverse-engineering a scale protocol (e.g. decoding an undecoded impedance frame), the native BlueZ handler can dump every notify frame as hex and hold the GATT connection open past the weight-stable point so the scale finishes sending its trailing frames before disconnect.
BLE_RAW_CAPTURE=true enables it (off by default; no effect on normal runs).BLE_RAW_CAPTURE_HOLD_SEC=20 sets how long to hold the link after the weight
stabilizes (default 20s, keep it under 120).# native
BLE_RAW_CAPTURE=true DEBUG=true CONTINUOUS_MODE=true npm start
# docker
docker run ... -e BLE_RAW_CAPTURE=true -e DEBUG=true ghcr.io/kristianp26/ble-scale-sync:dev
Step on the scale and look for [BLE] [RAW] <uuid> (<n>B): <hex> lines. The run
still exports a weight-only reading. Applies to every GATT reading path (native
BlueZ and noble, plus the ESP32 and ESPHome proxies).
KristianP26 | APIUM | marcelorodrigo | fromport | boildead | alexw23 | bondesen | junaidk |
hedoric | alasano | O-Palda | Bretos | albert-canfield | JamieSBenson |
See CONTRIBUTING.md for development setup, project structure, and how to add new scale adapters or exporters.
GPL-3.0. See LICENSE for details.
TypeScript
89.5%
Python
6.4%
C
2.5%
Shell
1.3%