Skip to content

Exporters

BLE Scale Sync exports body composition data to 11 targets. The setup wizard walks you through exporter selection, configuration, and connectivity testing.

Exporters are configured in global_exporters (shared by all users). For multi-user setups with separate accounts, see Per-User Exporters. All enabled exporters run in parallel; the process reports an error only if every exporter fails.

TargetDescription
Garmin ConnectAutomatic body composition upload, no phone app needed
MQTTHome Assistant auto-discovery with 10 sensors, LWT
InfluxDBTime-series database (v2 write API)
WebhookAny HTTP endpoint (n8n, Make, Zapier, custom APIs)
NtfyPush notifications to phone/desktop
TelegramSend measurement notifications to a Telegram chat
File (CSV/JSONL)Append readings to a local file
StravaUpdate weight in your Strava athlete profile
Intervals.icuPush weight + body fat to Intervals.icu wellness
RunalyzePush weight + body composition to Runalyze metrics
WgerPush weight + body composition to a Wger instance

Garmin Connect

Automatic body composition upload to Garmin Connect, no phone app needed. Uses a Python subprocess with cached authentication tokens.

FieldRequiredDefaultDescription
emailYes(none)Garmin account email
passwordYes(none)Garmin account password
token_dirNo~/.garmin_tokensDirectory for cached auth tokens
yaml
global_exporters:
  - type: garmin
    email: '${GARMIN_EMAIL}'
    password: '${GARMIN_PASSWORD}'

Authentication

The setup wizard handles Garmin authentication automatically. You only need to authenticate once; tokens are cached and reused. To re-authenticate manually:

Standalone (Node.js):

bash
npm run setup-garmin

Docker (single user with env vars):

bash
docker run --rm -it \
  -v ./config.yaml:/app/config.yaml \
  -v ./garmin-tokens:/app/garmin-tokens \
  -e GARMIN_EMAIL \
  -e GARMIN_PASSWORD \
  ghcr.io/kristianp26/ble-scale-sync:latest setup-garmin

Docker (specific user from config.yaml):

bash
docker run --rm -it \
  -v ./config.yaml:/app/config.yaml \
  -v ./garmin-tokens-alice:/app/garmin-tokens-alice \
  -e GARMIN_EMAIL -e GARMIN_PASSWORD \
  ghcr.io/kristianp26/ble-scale-sync:latest setup-garmin --user Alice

Docker (all users from config.yaml):

bash
docker run --rm -it \
  -v ./config.yaml:/app/config.yaml \
  -v ./garmin-tokens-alice:/app/garmin-tokens-alice \
  -v ./garmin-tokens-bob:/app/garmin-tokens-bob \
  -e GARMIN_EMAIL -e GARMIN_PASSWORD \
  ghcr.io/kristianp26/ble-scale-sync:latest setup-garmin --all-users

IP blocking

Garmin may block requests from cloud/VPN IPs. If authentication fails, try from a different network, then copy the token directory to your target machine.

Upgrading from v1.8.0 or earlier

v1.8.1 bumps garminconnect to 0.3.x, which replaced the old garth-based OAuth files (oauth1_token.json, oauth2_token.json) with a single garmin_tokens.json. Existing tokens are incompatible. Re-run npm run setup-garmin; the script auto-removes the legacy files before writing the new format.

MQTT

Publishes body composition as JSON to an MQTT broker. Home Assistant auto-discovery is enabled by default; all 10 metrics appear as sensors grouped under a single device, with availability tracking (LWT) and display precision per metric.

Home Assistant users

If you run Home Assistant OS or Supervised, the Home Assistant Add-on auto-detects the Mosquitto broker through the Supervisor API, so you do not need to wire MQTT manually.

FieldRequiredDefaultDescription
broker_urlYes(none)mqtt://host:1883 or mqtts:// for TLS
topicNoscale/body-compositionPublish topic
qosNo1QoS level (0, 1, or 2)
retainNotrueRetain last message
usernameNo(none)Broker auth username
passwordNo(none)Broker auth password
client_idNoble-scale-syncMQTT client identifier
ha_discoveryNotrueHome Assistant auto-discovery
ha_device_nameNoBLE ScaleDevice name in Home Assistant
yaml
global_exporters:
  - type: mqtt
    broker_url: 'mqtts://broker.example.com:8883'
    username: myuser
    password: '${MQTT_PASSWORD}'

Webhook

Sends body composition as JSON to any HTTP endpoint. Works with n8n, Make, Zapier, or custom APIs.

FieldRequiredDefaultDescription
urlYes(none)Target URL
methodNoPOSTHTTP method
headersNo(none)Custom headers (YAML object)
timeoutNo10000Request timeout in ms
yaml
global_exporters:
  - type: webhook
    url: 'https://example.com/hook'
    headers:
      X-Api-Key: '${WEBHOOK_API_KEY}'

InfluxDB

Writes metrics to InfluxDB v2 using line protocol. Float fields use 2 decimal places, integer fields use i suffix.

FieldRequiredDefaultDescription
urlYes(none)InfluxDB server URL
tokenYes(none)API token with write access
orgYes(none)Organization name
bucketYes(none)Destination bucket
measurementNobody_compositionMeasurement name
yaml
global_exporters:
  - type: influxdb
    url: 'http://localhost:8086'
    token: '${INFLUXDB_TOKEN}'
    org: my-org
    bucket: my-bucket

Ntfy

Push notifications to phone/desktop via ntfy. Works with ntfy.sh or self-hosted instances.

FieldRequiredDefaultDescription
urlNohttps://ntfy.shNtfy server URL
topicYes(none)Topic name
titleNoScale MeasurementNotification title
priorityNo3Priority (1 to 5)
tokenNo(none)Bearer token auth
usernameNo(none)Basic auth username
passwordNo(none)Basic auth password
yaml
global_exporters:
  - type: ntfy
    topic: my-scale
    priority: 4

Telegram

Send a measurement notification to a Telegram chat via a bot. Create a bot with @BotFather to get a bot token, then start a chat with your bot (or add it to a group/channel) so it can message you.

FieldRequiredDefaultDescription
bot_tokenYes(none)Bot token from @BotFather
chat_idYes(none)Target chat ID (numeric) or @channelusername
titleNoScale MeasurementFirst line of the message
silentNofalseDeliver without a notification sound
yaml
global_exporters:
  - type: telegram
    bot_token: '${TELEGRAM_BOT_TOKEN}'
    chat_id: '987654321'
    title: Scale Measurement
    silent: false

The message is sent as plain text. In multi-user setups the user's name is prepended as [Name]. Historical readings replayed from a scale's offline cache are skipped (a notification for an old measurement is not meaningful).

Finding your chat ID

Message your bot once, then open https://api.telegram.org/bot<token>/getUpdates in a browser — the chat.id field holds your chat ID. For groups, add the bot to the group first.

File (CSV/JSONL)

Append each reading to a local CSV or JSONL file. Useful for simple logging without external services.

FieldRequiredDefaultDescription
file_pathYesPath to the output file
formatNocsvcsv or jsonl
yaml
global_exporters:
  - type: file
    file_path: './measurements.csv'
    format: csv

CSV files get an automatic header row on first write. JSONL files append one JSON object per line.

Docker

Mount a volume so the file persists across container restarts:

yaml
volumes:
  - scale-data:/app/data
# config.yaml: file_path: './data/measurements.csv'

Strava

Update your weight in the Strava athlete profile. Requires a Strava API application.

FieldRequiredDefaultDescription
client_idYesStrava API application client ID
client_secretYesStrava API application client secret
token_dirNo./strava-tokensDirectory for cached OAuth tokens
yaml
users:
  - name: Alice
    exporters:
      - type: strava
        client_id: '${STRAVA_CLIENT_ID}'
        client_secret: '${STRAVA_CLIENT_SECRET}'

Creating a Strava API Application

  1. Go to strava.com/settings/api
  2. Upload an Application Icon (required before you can save the form)
  3. Fill in the application details:
    • Application Name: anything you like (e.g. BLE Scale Sync)
    • Category: choose any
    • Website: can be anything (e.g. https://github.com/KristianP26/ble-scale-sync)
    • Authorization Callback Domain: set to localhost (the OAuth flow redirects here, but the page does not need to load)
  4. Save and copy the Client ID and Client Secret

Callback Domain

The Authorization Callback Domain must be set to localhost. During the OAuth flow, Strava redirects to http://localhost?code=XXXX. The page will not load (nothing is listening), but you only need to copy the code parameter from the URL bar.

Authentication

After adding the Strava exporter to your config, run the setup script to authorize:

Standalone (Node.js):

bash
npm run setup-strava

Docker:

bash
docker run --rm -it \
  -v ./config.yaml:/app/config.yaml \
  -v strava-tokens:/app/strava-tokens \
  ghcr.io/kristianp26/ble-scale-sync:latest setup-strava

The script prints a browser URL for Strava authorization. After authorizing, copy the code parameter from the redirect URL and paste it back. Tokens are cached and automatically refreshed.

Intervals.icu

Push weight and body fat to your Intervals.icu wellness data. Intervals.icu is a free training-analytics platform — a natural fit alongside the Garmin and Strava exporters.

FieldRequiredDefaultDescription
athlete_idYes(none)Intervals.icu athlete ID (e.g. i123456)
api_keyYes(none)API key from Intervals.icu Settings → Developer
yaml
users:
  - name: Alice
    exporters:
      - type: intervals
        athlete_id: i123456
        api_key: '${INTERVALS_API_KEY}'

Authentication uses HTTP Basic with the API key — no OAuth flow. Find both values on the Intervals.icu Settings → Developer page. The reading updates the wellness record for its day (weight + bodyFat); historical readings replayed from a scale's offline cache land on their original date.

Runalyze

Push weight and body composition to Runalyze, an endurance-training analytics platform, as health metrics.

FieldRequiredDefaultDescription
tokenYes(none)Personal API token from Runalyze Settings → Personal API
yaml
users:
  - name: Alice
    exporters:
      - type: runalyze
        token: '${RUNALYZE_TOKEN}'

Authentication uses the Runalyze Personal API token (sent in the token header), no OAuth flow. Generate it at runalyze.com/settings/personal-api; note that Runalyze tokens require an expiry date, so the token has to be regenerated when it lapses.

The reading is sent to the bodyComposition metric with an exact timestamp, so historical readings replayed from a scale's offline cache land on their original date and time. Weight, body fat and body water map directly; muscle and bone are converted from mass to a percentage of body weight (Runalyze stores those as percentages). Metrics that an adapter could not measure are omitted.

Wger

Push weight and body composition to Wger, the open-source self-hosted workout and weight manager. A natural fit for the self-hosting audience, and it matches what openScale-sync already supports.

FieldRequiredDefaultDescription
base_urlYes(none)Wger instance URL, e.g. https://wger.de or your self-hosted host
tokenYes(none)Permanent API key from <base_url>/en/user/api-key
sync_measurementsNotrueAlso push body fat, water, muscle, bone as custom measurements
yaml
users:
  - name: Alice
    exporters:
      - type: wger
        base_url: https://wger.de
        token: '${WGER_TOKEN}'
        sync_measurements: true

Authentication uses a permanent API key (sent as Authorization: Token <key>), no OAuth flow. Generate it on the Wger account settings API page. Weight is written to a weight entry on the reading's calendar day, so historical readings replayed from a scale's offline cache land on their original date. With sync_measurements enabled, body fat and water (percent) and muscle and bone (kg) are written as Wger custom measurements; the matching categories are created automatically on first use and reused afterwards. Measurement failures are logged but do not block the weight sync.

Secrets

Use ${ENV_VAR} references in YAML for passwords and tokens. The variable must be defined in the environment or in a .env file:

yaml
global_exporters:
  - type: garmin
    email: '${GARMIN_EMAIL}'
    password: '${GARMIN_PASSWORD}'

See Configuration: Environment Variables for details.

Healthchecks

At startup, exporters are tested for connectivity. Failures are logged as warnings but don't block the scan.

ExporterMethod
MQTTConnect + disconnect
WebhookHEAD request
InfluxDB/health endpoint
Ntfy/v1/health endpoint
TelegramgetChat endpoint
Intervals.icuGET wellness record
RunalyzeGET bodyComposition metric
WgerGET userprofile record
GarminNone (Python subprocess)
FileDirectory writable check
StravaNone (avoid API rate limits)

Released under the GPL-3.0 License.