Getting started
BLE Scale Sync runs on Linux (including Raspberry Pi), macOS and Windows. The host needs a Bluetooth adapter in range of the scale, or a proxy near it.
Recommended hardware
- A Raspberry Pi Zero 2 W (about 15 EUR, built-in Bluetooth) with Raspberry Pi OS Lite, or any Raspberry Pi 3, 4 or 5.
- Any supported scale.
The first-generation Raspberry Pi Zero W (ARMv6) is not supported: npm install fails with SIGILL. See install fails on Raspberry Pi Zero W.
Hosts without Bluetooth
If the host has no Bluetooth or is out of range of the scale, read the scale through an ESP32 or ESPHome proxy near it, or through the Bluetooth proxies Home Assistant already has.
Bluetooth transports compares them.
Docker (Linux only)
The image reads Bluetooth through the host's BlueZ over D-Bus, so it needs a Linux host. On macOS and Windows use the standalone install.
1. Configure
Create the file and folders the container mounts first. Docker would create a missing mount source as a root-owned directory that the container cannot write to.
mkdir -p ~/ble-scale-sync/garmin-tokens ~/ble-scale-sync/strava-tokens
cd ~/ble-scale-sync
touch config.yamlThe container runs as UID 1000, which is the first user on Raspberry Pi OS. If id -u prints another number, run sudo chown -R 1000:1000 ~/ble-scale-sync.
Run the setup wizard from that directory. It finds the scale, asks for your profile and exporters, signs in to Garmin and Strava, and writes config.yaml:
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 \
-v ./strava-tokens:/app/strava-tokens \
ghcr.io/kristianp26/ble-scale-sync:latest setupThe wizard reports that the empty config.yaml cannot be edited and sets up a new one. Token directories default to ./garmin-tokens and ./strava-tokens next to config.yaml, which is /app/garmin-tokens and /app/strava-tokens in the container. If you enter another token_dir, mount that path instead.
2. Run
docker run -d --name ble-scale-sync --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 \
-v ./garmin-tokens:/app/garmin-tokens \
-v ./strava-tokens:/app/strava-tokens \
-e CONTINUOUS_MODE=true \
-e TZ="$(timedatectl show -p Timezone --value)" \
ghcr.io/kristianp26/ble-scale-sync:latestCheck the log with docker logs -f ble-scale-sync. It shows BLE Scale Sync (continuous) at startup, then a Measurement: line when you step on the scale.
The flags:
--network hostand-v /var/run/dbus:/var/run/dbus:ro: access to BlueZ through the host's system D-Bus.--cap-add NET_ADMIN --cap-add NET_RAW: raw Bluetooth operations.--group-add "$(getent group bluetooth | cut -d: -f3)": the host'sbluetoothgroup ID. Keep the quotes, so a missing group is reported by Docker and not by an unclear D-Bus error later.--device /dev/rfkill: lets the app power-cycle the adapter when BlueZ gets stuck. Optional.-e TZ=...: the host's time zone. Without it the container runs in UTC, and the Intervals.icu and wger exporters file a weigh-in made shortly after midnight under the previous day.
Because config.yaml is mounted as a single file, the files the app keeps next to it stay inside the container. A re-created container, for example after an update, loses them: the export retry queue, the replay check state, the stored weigh-in marks and the update check date. Tokens are safe in their mounted directories.
For Docker Compose instead:
- Copy
docker-compose.example.ymlfrom the repository todocker-compose.ymlin this directory. - In a
.envnext to it, setBLUETOOTH_GID=to the output ofgetent group bluetooth | cut -d: -f3. - Replace the volume
garmin-tokens:/home/node/.garmin_tokenswith./garmin-tokens:/app/garmin-tokens, add./strava-tokens:/app/strava-tokens, and setTZto your time zone. - Run
docker compose up -d.
Other commands
The image takes the same subcommands as the standalone commands:
docker run --rm --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 \
ghcr.io/kristianp26/ble-scale-sync:latest scan # list nearby BLE devices
docker run --rm -v ./config.yaml:/app/config.yaml:ro \
ghcr.io/kristianp26/ble-scale-sync:latest validate # check config.yamlHome Assistant add-on
The add-on needs Home Assistant OS or Supervised; Settings > About shows your install type. On Home Assistant Container or Core, use Docker with the MQTT exporter.
Install steps, options and Garmin setup are in the Home Assistant add-on guide.
Standalone (Node.js)
Prerequisites
- Node.js 22.23.4 or later 22.x, 24.22.1 or later 24.x, or 26.12.1 or later.
- A Bluetooth adapter. On Linux, install BlueZ:
sudo apt-get install bluetooth bluez. - On Windows, Visual Studio Build Tools with the C++ workload, or the prebuilt driver
ble.noble_driver: stoprocent. See Noble drivers. - For Garmin Connect only: Python 3.12 or newer (
python3 --version), and on Debian or Raspberry Pi OS thepython3-venvpackage.
Install from npm (no clone)
Install the ble-scale-sync command and create a directory for its config:
npm install -g ble-scale-sync # with sudo if npm reports EACCES
mkdir -p ~/ble-scale-sync && cd ~/ble-scale-syncconfig.yaml and .env are read from the directory you run the command in. Keep both in this directory and always start the app from there.
To try the app without installing it, run npx ble-scale-sync setup and then npx ble-scale-sync in that directory. The Garmin and service steps below need the global install.
Install from a clone
For contributing, or to run code that is not released yet:
git clone https://github.com/KristianP26/ble-scale-sync.git
cd ble-scale-sync
npm installThe clone directory holds config.yaml, and the commands are npm scripts, see Commands.
Install the Garmin dependencies
Skip this step without Garmin Connect. The wizard signs in to Garmin with a Python script, so it needs the Python packages. Raspberry Pi OS and Debian refuse a system-wide pip install, so use a virtual environment in the app directory:
cd ~/ble-scale-sync # from a clone: the clone directory
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r "$(npm root -g)/ble-scale-sync/requirements.txt" # from a clone: pip install -r requirements.txtThe app finds Python as python3 or python on PATH. Activate the virtual environment before you run the app by hand. The service puts it on PATH itself.
Configure
ble-scale-sync setup # from a clone: npm run setupThe wizard finds the scale, asks for your profile and exporters, signs in to Garmin and Strava, and writes config.yaml in the current directory. To write it by hand, see Configuration.
To sign in to Garmin again later, for example after a password change:
ble-scale-sync setup-garmin --all-users # from a clone: npm run setup-garmin -- --all-usersIt uses the accounts and token_dir from config.yaml; --user <name> signs in one user.
Run
ble-scale-sync # one reading, then exit
CONTINUOUS_MODE=true ble-scale-sync # keep running
DRY_RUN=true ble-scale-sync # read the scale, skip exportsFrom a clone the same three are npm start, CONTINUOUS_MODE=true npm start and DRY_RUN=true npm start. Step on the scale: a reading logs a Measurement: line with the weight. Press Ctrl+C to stop continuous mode.
Commands
From an install the commands are subcommands, from a clone they are npm scripts:
| From an install | From a clone | What it does |
|---|---|---|
ble-scale-sync | npm start | Run the sync flow |
ble-scale-sync setup | npm run setup | Interactive setup wizard |
ble-scale-sync setup-garmin --all-users | npm run setup-garmin -- --all-users | Garmin Connect sign-in for the users in config.yaml |
ble-scale-sync setup-strava | npm run setup-strava | Strava OAuth token setup |
ble-scale-sync scan | npm run scan | Discover nearby BLE devices |
ble-scale-sync diagnose MAC | npm run diagnose -- MAC | BLE diagnostic dump of this host's radio; on a proxy transport only with --native |
ble-scale-sync validate | npm run validate | Validate config.yaml and exit |
ble-scale-sync --help | npm start -- --help | Command list and environment overrides |
ble-scale-sync --version | - | Print the version |
--config <path> points the run command, validate, setup, scan and diagnose at another config.yaml, see where config.yaml is read from.
Install a Bluetooth stack later
If the app says a Bluetooth stack package is missing, install it next to the app:
npm install -g @stoprocent/noble # next to a global ble-scale-sync installUnder npx the cache directory is discarded between runs, so install the app with the stack instead: npm install -g ble-scale-sync @stoprocent/noble.
Run as a service (Linux)
Find your user name with
id -un. Raspberry Pi OS has no defaultpiuser: it is the user you created in Raspberry Pi Imager or at first boot.For an npm install, find the command's path with
command -v ble-scale-sync.Create
/etc/systemd/system/ble-scale.service, withYOUR_USERreplaced andExecStartset to the path from step 2:ini[Unit] Description=BLE Scale Sync After=network.target bluetooth.target # Keep restarting after repeated BLE or network failures instead of giving up # after the default 5 starts in 10 s. StartLimitIntervalSec=0 [Service] Type=simple User=YOUR_USER # The directory that holds config.yaml (for a clone: the clone directory) WorkingDirectory=/home/YOUR_USER/ble-scale-sync # The leading "-" lets the service start when there is no .env EnvironmentFile=-/home/YOUR_USER/ble-scale-sync/.env Environment="CONTINUOUS_MODE=true" # venv/bin first, so the Garmin exporter finds its Python packages Environment="PATH=/home/YOUR_USER/ble-scale-sync/venv/bin:/usr/local/bin:/usr/bin:/bin" # npm install: the path from step 2 ExecStart=/usr/bin/ble-scale-sync # Clone: the compiled build instead # ExecStart=/usr/bin/node dist/index.js Restart=always RestartSec=5 [Install] WantedBy=multi-user.targetFrom a clone, build the app with
npm run build, and again after everygit pull.Start the service and enable it at boot:
bashsudo systemctl daemon-reload sudo systemctl enable --now ble-scale.service
Check it with journalctl -u ble-scale.service -f: it logs BLE Scale Sync (continuous) at startup.
Next steps
- Configuration: every
config.yamloption. - Exporters: Garmin Connect, MQTT and the other targets.
- Multi-user: several people on one scale.
- Troubleshooting: when the scale is not found or no reading arrives.