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

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 different export targets (e.g., separate Garmin accounts), define exporters on that user; it completely replaces global_exporters for them:

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

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

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

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
GarminOne account per user via per-user exporter config

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.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, switching between single-user (1 user) and multi-user (>1).

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

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")