Skip to content

Fix: body composition computed for the wrong user profile#1411

Open
DanyPM wants to merge 1 commit into
oliexdev:masterfrom
DanyPM:fix/bodycomp-wrong-user-profile
Open

Fix: body composition computed for the wrong user profile#1411
DanyPM wants to merge 1 commit into
oliexdev:masterfrom
DanyPM:fix/bodycomp-wrong-user-profile

Conversation

@DanyPM

@DanyPM DanyPM commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Fix: body composition computed for the wrong user profile

Problem

When you weigh in while a different user profile (B) is active on the app (or the scale auto-routes the reading by weight), the measurement is correctly stored under the right user (A) — but its body-composition values were computed from the wrong user's (B) biodata (sex, height, age).

Because BIA (bioelectrical impedance) results are driven by sex and height, the error can be large. Concrete case from a backup: a male user (172 cm) showed 30.1 % body fat. Re-deriving that row with the other profile on the device (female / 160 cm / age 31) reproduces every stored field exactly (fat 30.08, water 51.18, muscle 35.20, ecw 23.62, icw 27.56, bmr 1471, vfi 8.02); re-deriving with his real profile (male / 172 cm / 28) gives the correct ~17.8 %. The formula is fine — it was fed the wrong user.

Root cause — body composition is computed before the user is finalized

The user a measurement belongs to is decided in three places, but body composition is calculated up front against the wrong one and never recomputed:

  1. The scale handler computes body composition against the selected profile snapshot (whoever was active when the scale broadcast) and tags the measurement with that user.
  2. BleConnector.saveMeasurementFromEvent discards that and rebuilds the row with the current app user.
  3. MeasurementTransformationUseCase.applySmartUserAssignment reassigns the row to the closest-weight user.

So the persisted body composition belongs to a different profile than the row it lands on. This affects every BIA scale, not just one model, and recurs on every weigh-in until the reading is re-derived for the final user.

Fix

1. Live fix — compute with the final user (all BIA scales)

Body composition is now derived after the user is finalized, then computed once for that user:

  • Extracted resolveAssignedUserId(weightKg, fallbackUserId) from applySmartUserAssignment (pure decision, no mutation).
  • Added ScaleDeviceHandler.recomputeBodyComposition(raw, user) and threaded it through ScaleCommunicatorModernScaleAdapter so BleConnector can re-derive for the assigned user via a recompute lambda passed to MeasurementFacade.saveMeasurementFromBleDevice.
  • Shared per-lib derivations live in one place — core/bluetooth/libs/BodyComposition.kt (fromMiScale, fromTrisa, standardImpedanceLib) — so each calculator lib is bound to (user, raw) once. Same-lib handlers reduce to one-liners.

Handlers fixed (13): S400, MiScale, EufyC20, OneByone, OneByoneNew, Trisa, Etekcity, Cult, QN, HuaweiCH100S, DrTrust (VFI excepted — needs segmental z3 that isn't persisted), ES-CS20M, Realme.

Not fixable: Yunmai, Soehnle never persist impedance → raw input is gone (default no-op). HuaweiAhCh100 sends a final fat %, which is user-independent (no override needed).

2. Retroactive fix — MIGRATION_15_16

Re-derives body composition for every stored dual-band S400 row using each row's own user via S400BodyComposition.compute (S400 defaults: MI_LEGACY bone, Cunningham-1991 BMR, foot-to-foot 1.10). Rows whose inputs fall outside the validated range (NOT_AVAILABLE) are left untouched. Raw weight/impedance are never modified.

The migration is S400-only by necessity: it is the only scale that persists enough raw input and has a centralised pure formula to reconstruct from. The Measurement table stores no device id, so an arbitrary historic row can't be matched to the right per-scale formula.

Tests

  • MigrationTest.migration15to16_reDerivesS400BodyCompositionForRowUser — seeds a male row carrying female-derived body fat + raw impedance; asserts the migration rewrites it to the male re-derivation (~17.8 %), leaves weight/impedance untouched, and skips an out-of-range row.
  • Full suite green: 288 tests, the only 2 failures (BackupRestoreUseCasesTest) are pre-existing Windows file-swap flakes — identical on a clean master checkout.

Notes / follow-ups

  • DrTrust VFI and Yunmai/Soehnle are documented limitations above.
  • The migration uses S400 default bone/BMR formulas (SharedPreferences aren't reachable from a Room Migration); this affects only BONE/BMR magnitude, never fat/water/muscle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant