Skip to content

Multi-User Support ​

When multiple users are configured, the app automatically identifies who stepped on the scale based on the measured weight. The setup wizard walks you through adding users and setting weight ranges, no manual YAML editing needed.

How It Works ​

  1. Someone steps on the scale
  2. The app reads the weight and identifies the user by their weight range
  3. Body composition is calculated using that user's profile (height, age, gender, athlete mode)
  4. Data is exported to that user's configured exporters
  5. last_known_weight is updated in config.yaml for better future matching

Each user defines a weight_range so the app knows who's who:

yaml
users:
  - name: Alice
    weight_range: { min: 50, max: 70 }
    last_known_weight: null
  - name: Bob
    weight_range: { min: 75, max: 100 }
    last_known_weight: 85.5

Only the matching-relevant fields are shown. Every user also needs slug, height, birth_date, gender and is_athlete, and the file needs version: 1 -> see the full reference.

Weight Matching ​

The app uses a 4-tier priority system to identify users:

PriorityConditionBehavior
1Single userAlways matches (warns if weight is outside range)
2Exact range matchOne user's range contains the weight
3Overlapping rangesMultiple matches; tiebreak by last_known_weight proximity, then config order
4No range matchClosest last_known_weight

If no match is found, the unknown_user strategy decides what happens:

StrategyBehavior
nearest (default)Picks the closest range midpoint (with a warning)
logLogs a warning and skips
ignoreSilently skips

Note what tiers 1 and 4 mean in practice: a reading outside every configured range is not rejected, it is assigned anyway. unknown_user is never consulted, because tier 4 already produced a match. If you want such a reading dropped instead, set out_of_range: skip; see Out-of-range readings. The setup wizard writes out_of_range: skip whenever it saves a config with more than one user.

Some scales also send weigh-ins stored in their memory while the app was not running. Each stored weigh-in is assigned by its own weight (or by the scale's user slot, when the scale sends one), not to whoever is standing on the scale right now. A stored weigh-in that matches nobody, or more than one user equally, is dropped and logged.

Drift Detection ​

After matching, the app checks if the weight falls in the outer 10% of the user's range. If it does, a warning is logged so you can adjust the range before mismatches start happening.

For example, if Alice's range is 50–70 kg and she weighs 68.5 kg, the app warns that she's near the upper boundary.

Automatic Weight Tracking ​

After each measurement, the matched user's last_known_weight is automatically updated in config.yaml. This improves matching accuracy over time, especially when ranges overlap. Updates are debounced (5s) and skipped for changes under 0.5 kg.

Per-User Exporters ​

By default, all users share global_exporters. If a user needs their own export targets (e.g., separate Garmin accounts), define exporters on that user. Their own exporters are added to global_exporters; when a user has their own exporter of a type that is also global (for example garmin), only theirs is used for that type. A list may hold several exporters of one type (two webhooks), and each of them gets the reading:

yaml
users:
  - name: Alice
    # ...
    exporters:
      - type: garmin
        email: '[email protected]'
        password: '${ALICE_GARMIN_PASSWORD}'
        token_dir: './garmin-tokens/alice'

  - name: Bob
    # ...
    exporters:
      - type: garmin
        email: '[email protected]'
        password: '${BOB_GARMIN_PASSWORD}'
        token_dir: './garmin-tokens/bob'

global_exporters:
  - type: influxdb
    # ... shared by users without their own exporters list

One token directory per account

Garmin and Strava keep each account's login tokens in token_dir. Give every user their own token_dir, as above. If two accounts share one directory, whichever account authenticates last owns the tokens and receives everyone's readings, so the config is rejected at startup with an error naming both users. Two Garmin entries with the same email may share a directory, since they are one account. The setup wizard offers a separate directory per user.

Exporter behavior in multi-user mode ​

ExporterWhat changes
MQTTPublishes to {topic}/{slug}, per-user HA device + LWT
InfluxDBAdds user={slug} tag to line protocol
WebhookAdds user_name + user_slug fields to JSON
NtfyPrepends [{name}] to notification (a single user gets it too)
GarminOne account per user, each with its own token_dir
StravaOne account per user, each with its own token_dir

Live Config Reload ​

In continuous mode, edits to config.yaml are detected automatically and applied before the next scan cycle. No restart, no manual signal. Works on Linux, macOS, and Windows. The config is re-validated before applying; if validation fails, the previous config is kept and an error is logged.

Hot-swappable on edit:

  • Exporter list (per-user and global_exporters)
  • User profiles (name, slug, height, birth_date, gender, is_athlete, weight_range, last_known_weight)
  • scale.weight_unit, scale.height_unit
  • unknown_user strategy
  • out_of_range strategy
  • runtime.dry_run, runtime.debug, runtime.scan_cooldown, runtime.idle_rescan_delay
  • ble.scale_mac
  • update_check

Restart-required (the change is detected and logged with a warning, but only takes effect after restart): runtime.retry_failed_exports (read once at startup), ble.handler, ble.adapter, ble.adapter_privacy, ble.noble_driver, ble.force_scale_adapter, every ble.mqtt_proxy.* field including embedded_broker_port and embedded_broker_bind, every ble.esphome_proxy.* field including client_info, additional_proxies and advertisement_timeout, ble.ha_bluetooth.url, ble.ha_bluetooth.token, ble.ha_bluetooth.source, runtime.continuous_mode, runtime.watchdog_max_consecutive_failures. Adding or removing users, including switching between one user and several, applies on reload.

Everything not in that list is hot-swapped, including the keys people most often tune while a scale is misbehaving: ble.session_timeout_sec, ble.auto_clear_stale_bond, ble.preemptive_adapter_reset, ble.bind_key, every ble.qn_* option and ble.proxy_liveness_timeout_min. On the proxy transports the liveness timeout is re-read when the next advertisement wait begins, so a change to it lands on the next cycle rather than the same instant.

In Docker with config.yaml mounted as a single file, edits made in place are picked up. An editor that saves by writing a new file and renaming it over the old one replaces the file on the host, and the container keeps seeing the old one; after such an edit send SIGHUP or restart the container, or mount the directory instead of the file.

To opt out (e.g. on a flaky network filesystem) and rely solely on the SIGHUP flow:

yaml
runtime:
  watch_config: false

SIGHUP still works as a manual fallback on Linux/macOS:

bash
kill -HUP $(pgrep -f "ble-scale-sync")

Released under the GPL-3.0 License.