Skip to content

Home Assistant Add-on ​

BLE Scale Sync ships as a native Home Assistant add-on for Home Assistant OS and Supervised installations. The add-on generates a working config from the UI, auto-detects the Mosquitto broker, exposes every metric as an MQTT auto-discovery sensor, and can optionally upload to Garmin Connect.

Install ​

Add BLE Scale Sync repository to your Home Assistant

The badge above uses My Home Assistant to open your instance, confirm the repository, and drop you on the Add-on Store with BLE Scale Sync visible. Click Install, then head to the Configuration tab and fill in your scale MAC and user profile. Start the add-on from the Info tab. The Supervisor pulls the arm64 / armv7 / amd64 image to match your host.

Prefer manual steps?
  1. Settings > Add-ons > Add-on Store.

  2. Three-dot menu > Repositories and add:

    https://github.com/KristianP26/ble-scale-sync
  3. Refresh the store. BLE Scale Sync appears under the new repository.

  4. Click Install, then open the Configuration tab and fill in your scale MAC and user profile. Start the add-on from the Info tab.

TIP

The add-on requires host_network, host_dbus, and the NET_ADMIN / NET_RAW capabilities to access the host Bluetooth adapter through BlueZ. The Supervisor grants these automatically. The add-on also declares apparmor: false, because the Supervisor's default AppArmor profile blocks the D-Bus handshake that BlueZ needs. See #271.

Quick start ​

Minimal config for a single-user Renpho / Xiaomi / Eufy scale with MQTT and Home Assistant auto-discovery:

  1. Install the Mosquitto broker add-on (if you do not already run an MQTT broker) and start it.
  2. In the BLE Scale Sync config:
    • Leave Scale MAC address empty for auto-discovery, or paste the MAC you found with the scan command.
    • Fill User profile (name, height, birth date, gender).
    • Leave MQTT enabled and MQTT auto-detect on. The add-on reads the Mosquitto broker details from the Supervisor API, so no broker URL or credentials are needed.
  3. Start the add-on and step on the scale. Within a few minutes new sensors appear under Settings > Devices & Services > MQTT.

The exposed sensors cover weight, body fat, water, muscle mass, bone mass, BMI, BMR, visceral fat, metabolic age, and impedance.

Configuration reference ​

All options live under the Configuration tab. The add-on regenerates /data/config.yaml on every restart, so changes here take effect on the next start.

Scale and BLE ​

OptionDefaultNotes
scale_macemptyLeave empty for auto-discovery. Set to a specific MAC like AA:BB:CC:DD:EE:FF to skip scanning. Required for scales with non-unique advertised names.
ble_adapterempty (uses default)Set to hci0, hci1, ... on hosts with multiple Bluetooth adapters.
reset_bluetoothtrueRuns btmgmt power off/on at startup. Leave on unless you run other HA Bluetooth integrations that lose connectivity when the adapter is power-cycled.
force_scale_adapteremptyOverrides protocol auto-detection with the adapter name exactly as printed in the Adapters: line in the log. Requires scale_mac, because a forced adapter claims every device it is shown. Please also report the misdetection.
qn_protocol_byteemptyQN-family scales only. The protocol byte the handshake echoes back, 0 to 255. Set it only when a QN scale completes the whole handshake in the log and then never reports a weight.
qn_report_byteemptyQN-family scales only, worth trying after qn_protocol_byte did not help. Payload byte of the history-response frame, 0 to 255. The default depends on the dialect: 252 on the long-frame ones (es26m and extended) and 254 on the classic one. Try the other value if your scale completes the handshake and then reports nothing.
auto_clear_stale_bondfalseBonded scales only (Beurer BF7xx / BF9xx). Delete a pairing key the scale has forgotten and pair again, instead of failing every connect until bluetoothctl remove is run by hand.
preemptive_adapter_resettrueBuilt-in Bluetooth only. Power-cycles the adapter after every connection to the scale, to clear a stuck scanning state. Leave on; turning it off only stops the brief adapter drop other HA Bluetooth integrations see after each weigh-in. It was not the cause of the Beurer BF915 re-pairing in #417 (see adapter_privacy).
adapter_privacyfalseBuilt-in Bluetooth only. For Beurer scales that ask for SET and a new pairing before every weigh-in. Turns on LE privacy on the adapter with a key derived from its address, so the scale keeps the pairing. Affects the whole adapter, Home Assistant's own Bluetooth integration included, and other paired LE devices may need pairing again; a dedicated adapter under ble_adapter avoids that. Remove the old pairing once after turning it on. See the configuration notes.
qn_weight_ackunsetQN-family scales only. Sends your weight anchor right after the start command and acknowledges live weight frames, as the vendor apps do. On the 19-byte Arboleaf dialect it also sends your age and height as the app does, and one such scale streamed its weight with this and qn_time_sync_long both true (reading it is not yet confirmed on hardware, #331). Try true if your QN scale completes the handshake and then reports nothing.
qn_a4_preludeunsetQN-family scales only, and the last thing to try. Sends the two undecoded 0xA4 frames an Arboleaf vendor app sends between START and the first weight frame. Set true only if qn_weight_ack did not help.
qn_time_sync_longunsetQN-family scales only. Sends the 9-byte form of the clock-setting frame the same Arboleaf capture shows, instead of the 8-byte one. The extra byte is undecoded.
qn_config_longunsetQN-family scales only, and the last difference anyone has found between our start-up conversation and the vendor app's. Sends the 10-byte form of the settings frame instead of the 9-byte one. The extra bytes are undecoded.
display_unitweight_unitUnit requested on the physical display independently of exported values. Only QN-family scales are told which unit to show.
proxy_liveness_timeout_min30Proxy transports only, which the add-on uses only with custom_config. Minutes of total advertisement silence before the link is treated as wedged and the add-on restarts. 0 disables. A value in your custom config file takes precedence. No effect on the built-in Bluetooth adapter.

The QN options, auto_clear_stale_bond, preemptive_adapter_reset, adapter_privacy and display_unit are ignored when custom_config is enabled, since that mode skips config generation entirely; set them in the corresponding ble: or scale: section of your own file instead. The add-on logs a warning if you leave one of the ble: options set. proxy_liveness_timeout_min is the exception: it only matters with a proxy transport, so the add-on applies it on top of your file (the file itself is not changed) unless the file sets ble.proxy_liveness_timeout_min itself.

Unit preferences ​

OptionDefaultAllowed
weight_unitkgkg, lbs
height_unitcmcm, in
display_unitweight_unitweight_unit, kg, lbs, st

The CLI and exporters display weights and heights in your chosen unit; all internal math stays in kg / cm. display_unit controls the physical scale separately; today only QN-family scales support it. For example, weight_unit: kg with display_unit: st keeps Home Assistant values in kg while asking a compatible QN scale to display stones and pounds.

User profile ​

OptionDefaultNotes
user_nameDefaultDisplay name used in logs and HA entity names.
user_height170In the unit chosen above.
user_birth_date1990-01-01YYYY-MM-DD. Used for age-based BMR and physique rating.
user_gendermalemale or female.
user_is_athletefalseShifts body fat formulas for athletic body types.
user_weight_min / user_weight_max40 / 150The plausible weight range for this person, in kg. On its own it only warns; set out_of_range below to skip to have readings outside it discarded.
out_of_rangewarnWhat to do with a reading outside that range. warn logs it and exports anyway (the behaviour before this option existed). skip logs it and stops: no export, and the remembered weight is left alone.

MQTT ​

OptionDefaultNotes
mqtt_enabledtrueEnable the MQTT exporter.
mqtt_autotrueAuto-detect the Mosquitto add-on broker via the Supervisor API. Overrides manual URL / credentials when the Mosquitto add-on is installed.
mqtt_broker_urlemptyManual broker URL, e.g. mqtt://192.168.1.50:1883 or mqtts://.... Only used when mqtt_auto is off or auto-detection fails.
mqtt_username / mqtt_passwordemptyCredentials for the manual broker.
mqtt_topicscale/body-compositionBase topic. Payload is published to this topic; HA discovery configs go to homeassistant/sensor/<device id>/<metric>/config, independent of this topic.
mqtt_ha_discoverytruePublish auto-discovery entities under homeassistant/. Disable if you want raw MQTT only.
mqtt_ha_device_nameBLE ScaleDevice name grouping the entities in HA.

Garmin Connect ​

OptionDefaultNotes
garmin_enabledfalseEnable the Garmin Connect exporter.
garmin_email / garmin_passwordemptyGarmin credentials. On first start the add-on runs setup_garmin.py to authenticate and saves OAuth tokens to /data/garmin-tokens/.
garmin_weight_onlyfalseUpload the weight alone; BMI, body fat, water, bone, muscle, visceral fat, physique rating, metabolic age and BMR are left unset in Garmin.
garmin_upload_timeout_sec180Seconds one Garmin upload attempt may take before it is killed (10-900). Three attempts are made. Raise it if uploads time out for a measurement that uploads fine later.

If your account uses MFA, see MFA workaround below.

Runtime ​

OptionDefaultNotes
scan_cooldown30Seconds to wait after a successful reading before scanning again. Range: 5-3600.
idle_rescan_delay5Seconds to wait before scanning again after a cycle that found no scale while the adapter was healthy. Range: 0-3600. Real failures keep their own backoff.
retry_failed_exportstrueKeep a reading whose upload failed and retry it later, up to 72 hours. Only targets that can record a past measurement are retried; MQTT and notifications cannot. The queue lives in /data, which survives add-on restarts and updates.
debugfalseEnable verbose BLE logs. Useful when opening an issue.
update_checktrueCheck once a day whether a newer version exists (anonymous, see the FAQ). Set false to turn it off.
custom_configfalseIgnore UI options entirely and use /share/ble-scale-sync/config.yaml instead. See Custom config mode.

MQTT auto-detection ​

When mqtt_auto: true and the Mosquitto add-on is running on the same host, BLE Scale Sync pulls the broker URL, username, and password from the Supervisor API (GET /services/mqtt) and wires the MQTT exporter automatically. You will see a line like this in the logs:

[ble-scale-sync] MQTT auto-detected: mqtt://core-mosquitto:1883

If the Mosquitto add-on is not installed or the API call fails, the add-on falls back to whatever you set in mqtt_broker_url / mqtt_username / mqtt_password, and the log says why, with the HTTP status from the Supervisor. Before this was fixed the add-on did not declare the MQTT service, so the Supervisor refused every request and auto-detection never worked; if you set the broker by hand for that reason, you can switch back to auto-detection after updating.

Garmin Connect ​

To upload measurements to Garmin Connect:

  1. Enable Garmin Connect and enter your email and password in the Configuration tab.
  2. Start the add-on.
  3. On first start the add-on runs python3 garmin-scripts/setup_garmin.py --from-config --config-path /data/config.yaml. If authentication succeeds, OAuth tokens are saved under /data/garmin-tokens/ and subsequent runs reuse them without re-entering the password.

MFA workaround ​

Home Assistant add-ons run without an interactive terminal, so the add-on cannot prompt for a 2FA code. If your account has MFA enabled:

  1. On a laptop or desktop, clone the repo and run python3 garmin-scripts/setup_garmin.py. Enter email, password, and MFA code when prompted. This writes garmin_tokens.json to ~/.garmin_tokens/.
  2. Copy that file to /share/ble-scale-sync/garmin-tokens/ on the Home Assistant host. The Samba and File editor add-ons both expose /share/ for easy uploads.
  3. Restart BLE Scale Sync. On startup the add-on detects the pre-generated token and imports it into /data/garmin-tokens/.

The import only happens while the add-on has no Garmin token yet. A token placed in /share/ later is not used (the log says so), because other add-ons and Samba users can write there. To switch to another Garmin account, reinstall the add-on, which clears /data, and import again.

The same workflow applies if Garmin is blocking your HA host's IP as a data-centre / VPN address: authenticate from a trusted network and import the tokens.

Custom config mode ​

For multi-user setups or exporters the UI does not cover (InfluxDB, Webhook, Ntfy, Strava, File), flip custom_config: true and drop a full config.yaml at:

/share/ble-scale-sync/config.yaml

The add-on copies that file verbatim into the runtime location on each start. See config.yaml.example for the full schema.

Editing it needs a restart

The copy happens once, at startup. The config watcher that picks up live edits watches the runtime copy, not the file under /share/, so editing /share/ble-scale-sync/config.yaml while the add-on is running changes nothing until you restart it.

Custom config mode still benefits from last_known_weight persistence (see below) but the add-on does not auto-run Garmin authentication; you handle that yourself by pre-seeding /share/ble-scale-sync/garmin-tokens/. On start the add-on imports that token into /data/garmin-tokens, where every garmin exporter without its own token_dir looks. With several Garmin accounts each needs its own token_dir. Anything under /share/ can be read and changed by other add-ons with share access and by Samba users, including your custom config.yaml.

Testing a development build ​

Fixes land on the dev branch before they are released, and maintainers often ask reporters to retest there. What that means for the add-on is not obvious, so it is spelled out here.

The Supervisor accepts a branch in the repository URL:

https://github.com/KristianP26/ble-scale-sync#dev

That gives you the dev add-on wrapper, not dev application code

The add-on is a thin layer over a published application image, and build.yaml pins that image to the last release on both branches. So a #dev install gives you the dev branch's options UI and run.sh, running the released application underneath.

This is exactly the trap in #318: a new option appeared in the UI, was set, and did nothing, because the application inside the container predated it.

To actually run unreleased application code today, run the plain Docker image outside the Supervisor:

ghcr.io/kristianp26/ble-scale-sync:dev

Every push to dev publishes that tag, plus an immutable dev-<sha> tag if you need two machines on provably the same build. See Docker deployment for the flags. You lose Supervisor integration and MQTT auto-detection; you gain the code under test.

When reporting back, paste the Version line from the top of the log. On an image build it names the channel and the commit:

[Sync] Version 1.22.1 (image dev @ 9c67525)

Persistence ​

If the app stops on its own (for example after repeated Bluetooth failures, to start the adapter fresh), the add-on starts it again after a short, growing delay and says so in the log. You do not need to turn on the Supervisor's Watchdog switch for that.

Everything that should survive add-on restarts lives under /data/ inside the container, which the Supervisor maps to persistent storage:

PathPurpose
/data/config.yamlThe merged runtime config. Regenerated from UI options on every start, with last_known_weight preserved per user.
/data/garmin-tokens/Garmin OAuth tokens produced by first authentication.

User-supplied files live under /share/ble-scale-sync/:

PathPurpose
/share/ble-scale-sync/config.yamlCustom config (used when custom_config: true).
/share/ble-scale-sync/garmin-tokens/Optional pre-generated Garmin tokens imported on startup (MFA workaround).

Troubleshooting ​

No scale found ​

  1. Step on the scale to wake it up. Most scales go to sleep after a few seconds.

  2. Run the scan command from a terminal on the HA host:

    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 \
      --security-opt apparmor=unconfined \
      ghcr.io/kristianp26/ble-scale-sync:latest scan

    On hosts whose Docker applies a restrictive default AppArmor policy, dropping --security-opt apparmor=unconfined makes this command exit with DBusError and AccessDenied. See Troubleshooting.

  3. If the scale is visible, paste its MAC into the Scale MAC address field.

  4. If multiple Bluetooth adapters are attached, set BLE adapter to the one facing the scale (hci1, etc.).

MQTT entities do not appear ​

  1. Open the Mosquitto add-on logs and confirm it is running and accepting connections.
  2. Enable debug: true in the BLE Scale Sync add-on and restart. Look for [ble-scale-sync] MQTT auto-detected: ... at startup.
  3. Check that the scale actually produced a measurement; HA auto-discovery entities are published on the first successful reading.

Garmin "No such file or directory: oauth1_token.json" ​

Fixed in v1.7.5. Make sure the add-on is on that version or newer. If it still fails, your account likely uses MFA. See MFA workaround.

Garmin "'Garmin' object has no attribute 'garth'" ​

Fixed in v1.8.1. The garminconnect library released 0.3.0 on 2026-04-02 which removed the garth attribute. The add-on now uses the new native auth API and automatically strips incompatible legacy token files on startup, then re-authenticates from the credentials you entered. If you still see this error, upgrade to v1.8.1 or newer and restart the add-on. MFA users also need to regenerate the MFA token (single garmin_tokens.json file now, no more oauth1/oauth2_token.json).

BlueZ discovery gets stuck after hours ​

Known upstream BlueZ bug on Broadcom adapters (bluez/bluez#807). See the general troubleshooting page for the full recovery behaviour the app implements. The add-on already has /dev/rfkill and CAP_NET_ADMIN available for the recovery tiers.

Limitations and roadmap ​

  • The UI is scoped to a single user. For multi-user / per-user-exporter setups, use custom config mode.
  • The add-on has not yet been submitted to the HACS default repository. For now, add the GitHub URL as a custom repository.
  • A dedicated Home Assistant notification exporter (persistent notifications or Companion app push) is planned.

Source, issue tracker, and changelog live at github.com/KristianP26/ble-scale-sync.

Released under the GPL-3.0 License.