Skip to content

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.

Docker
Linux hosts such as a Raspberry Pi or a NAS. Reaches any Home Assistant install through MQTT.
Home Assistant add-on
Home Assistant OS or Supervised.
Standalone Node.js
Linux, macOS and Windows.

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.

bash
mkdir -p ~/ble-scale-sync/garmin-tokens ~/ble-scale-sync/strava-tokens
cd ~/ble-scale-sync
touch config.yaml

The 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:

bash
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 setup

The 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 ​

bash
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:latest

Check 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 host and -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's bluetooth group 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:

  1. Copy docker-compose.example.yml from the repository to docker-compose.yml in this directory.
  2. In a .env next to it, set BLUETOOTH_GID= to the output of getent group bluetooth | cut -d: -f3.
  3. Replace the volume garmin-tokens:/home/node/.garmin_tokens with ./garmin-tokens:/app/garmin-tokens, add ./strava-tokens:/app/strava-tokens, and set TZ to your time zone.
  4. Run docker compose up -d.

Other commands ​

The image takes the same subcommands as the standalone commands:

bash
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.yaml

Home 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.

Add BLE Scale Sync repository to your Home Assistant

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 the python3-venv package.

Install from npm (no clone) ​

Install the ble-scale-sync command and create a directory for its config:

bash
npm install -g ble-scale-sync       # with sudo if npm reports EACCES
mkdir -p ~/ble-scale-sync && cd ~/ble-scale-sync

config.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:

bash
git clone https://github.com/KristianP26/ble-scale-sync.git
cd ble-scale-sync
npm install

The 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:

bash
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.txt

The 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 ​

bash
ble-scale-sync setup   # from a clone: npm run setup

The 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:

bash
ble-scale-sync setup-garmin --all-users   # from a clone: npm run setup-garmin -- --all-users

It uses the accounts and token_dir from config.yaml; --user <name> signs in one user.

Run ​

bash
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 exports

From 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 installFrom a cloneWhat it does
ble-scale-syncnpm startRun the sync flow
ble-scale-sync setupnpm run setupInteractive setup wizard
ble-scale-sync setup-garmin --all-usersnpm run setup-garmin -- --all-usersGarmin Connect sign-in for the users in config.yaml
ble-scale-sync setup-stravanpm run setup-stravaStrava OAuth token setup
ble-scale-sync scannpm run scanDiscover nearby BLE devices
ble-scale-sync diagnose MACnpm run diagnose -- MACBLE diagnostic dump of this host's radio; on a proxy transport only with --native
ble-scale-sync validatenpm run validateValidate config.yaml and exit
ble-scale-sync --helpnpm start -- --helpCommand 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:

bash
npm install -g @stoprocent/noble    # next to a global ble-scale-sync install

Under 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) ​

  1. Find your user name with id -un. Raspberry Pi OS has no default pi user: it is the user you created in Raspberry Pi Imager or at first boot.

  2. For an npm install, find the command's path with command -v ble-scale-sync.

  3. Create /etc/systemd/system/ble-scale.service, with YOUR_USER replaced and ExecStart set 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.target
  4. From a clone, build the app with npm run build, and again after every git pull.

  5. Start the service and enable it at boot:

    bash
    sudo 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 ​

Released under the GPL-3.0 License.
Software made in Europe