Skip to content

Add support for Xiaomi Body Composition Scale S400#1297

Merged
oliexdev merged 11 commits into
oliexdev:masterfrom
JPFrancoia:master
Feb 18, 2026
Merged

Add support for Xiaomi Body Composition Scale S400#1297
oliexdev merged 11 commits into
oliexdev:masterfrom
JPFrancoia:master

Conversation

@JPFrancoia
Copy link
Copy Markdown
Contributor

@JPFrancoia JPFrancoia commented Jan 29, 2026

Hi,

Thank you for maintaining openScale. I just bought the Xiaomi Body Composition Scale S400 and I noticed openScale didn't support it, so here we are.

I didn't reverse the bluetooth protocol myself, I leveraged the work done in this repo, so that sped things up.

This scale uses encryption in its bluetooth protocol, so the BLE key must be extracted. Here is how:


1. Initial Scale Setup (if not done)

  1. Install the Xiaomi Home app on your phone
  2. Add your S400 scale as a new device
  3. Complete at least one weighing to ensure the scale is registered with Xiaomi Cloud

2. Extract Tokens Using Xiaomi Cloud Tokens Extractor

# Clone the repository
git clone https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor.git
cd Xiaomi-cloud-tokens-extractor

# Install dependencies
pip install -r requirements.txt

# Run the extractor
python token_extractor.py

When prompted:

  1. Enter your Xiaomi account email/username
  2. Enter your password
  3. Select your region (e.g., de for Germany, us for USA, cn for China)

3. Find Your Scale's Information

The extractor will output a list of all your Xiaomi devices. Look for your scale:

Device: Xiaomi Body Composition Scale S400
Model: xmtzc14hm (or similar)
MAC: AA:BB:CC:DD:EE:FF
BLE KEY: 0123456789abcdef0123456789abcdef

Save these two values:

  • MAC: The Bluetooth address (format XX:XX:XX:XX:XX:XX)
  • BLE KEY: A 32-character hexadecimal string

4. Kill the Xiaomi Home App

Kill the app or uninstall it.


Once you have the key, this is pretty straightforward.

This is a measurement done with the Xiaomi Home App:

Screenshot_20260129-075926 Xiaomi Home

This is a measurement done with openScale (there is a difference because I did the measurement in the Xiaomi Home app the day after, while making this PR, to prove that it works):
Screenshot_20260129-074517 openScale

Here are a few other screenshots:

Screenshot_20260129-074325 openScale Screenshot_20260129-074334 openScale Screenshot_20260129-074420 openScale

To be clear: I built the app locally in debug mode, sideloaded it onto my phone and performed a real measurement.

I had to add a little bit of UI to add the BLE bind key in the setting. I couldn't find an existing example in openScale where this was done already, please let me know if I missed it.

Disclaimer: I did use AI to write most of the code. However I reviewed the code to the best of my abilities and I carefully tested the app. I compared all the metrics one by one with the official app to make sure that the values make sense in openScale.

@oliexdev
Copy link
Copy Markdown
Owner

oliexdev commented Feb 1, 2026

Currently I am on holiday but a quick feedback.
First of all awesome PR! before I will merge that, it need some adjustments. The scale config read and write should be done in Handler class not in the SettingsFacade there are public read / write function available. Secondly (which are currently not possible) is that the specific ui config for the scale should also be in the scale handler. I think we need some kind of method which a scale handler could be overload to show them in the bluetooth detail screen settings. Maybe you can try to implement it but I will look into it when I am back.

@JPFrancoia
Copy link
Copy Markdown
Contributor Author

JPFrancoia commented Feb 1, 2026

Thanks so much for the feedback! I'll get right on that. Enjoy your holidays!

@JPFrancoia
Copy link
Copy Markdown
Contributor Author

👋 Ok I made an attempt at shoving scale-specific UI settings in their respective handler. The trick is declaring a ScaleConfigField attribute in ScaleDeviceHandler, and that attribute can be overloaded by each handler. Right now the S400 is the only scale using this mechanism because it's the only scale that needs UI-specific tweaks.

…wing `ScaleDeviceHandler` and `ScaleCommunicator` to define custom Compose-based settings interfaces.
@oliexdev
Copy link
Copy Markdown
Owner

I'm back from holiday and have just pushed some adjustments to make the ui device configuration more generic.

Every scale device handler can now expose its own Composable UI. This provides much more freedom to implement custom controls like switches, sliders, or hex inputs (as seen in the S400 implementation) directly within the handler settings flow.

Testing is still pending —please feel free to try it out and let me know if you encounter any issues!

@henri-now
Copy link
Copy Markdown

henri-now commented Feb 15, 2026

Hi, first of all thanks to both of you for your awesome work.

Initially I was able to get measurements from my S400 scale, however I noticed that it doesn't work for me most of the time. When I try to connect to the scale while standing on it or after weighing, when the scale is still active, it says that it is trying to connect to the scale but after that nothing seems to happen. I attached a log file of such a situation.

Please let me know if I accidentally left in sensitive information such as BLE keys. Also the BLE key changes if you reset the scale via the reset button hidden underneath the battery cover.

Log file

@JPFrancoia
Copy link
Copy Markdown
Contributor Author

Hi, first of all thanks to both of you for your awesome work.

Initially I was able to get measurements from my S400 scale, however I noticed that it doesn't work for me most of the time. When I try to connect to the scale while standing on it or after weighing, when the scale is still active, it says that it is trying to connect to the scale but after that nothing seems to happen. I attached a log file of such a situation.

Please let me know if I accidentally left in sensitive information such as BLE keys. Also the BLE key changes if you reset the scale via the reset button hidden underneath the battery cover.

Log file

What bluetooth tuning profile are you using?
When I was testing, I realised I needed to use the "Conservative" profile. I think this is because each profile sets a given amount of time before timing out during a measurement. IIRC the "Balanced" one sets it at like 20s. But the S400 measures the BPM (heart rate) on top of the other metrics like weight and body fat, and because of this most of the time the measurement takes more than 20s, and it times out. But not always, a few times I managed to get the full measurement on time. Switching to "Conservative" made it fully reliable.

I don't think this is a problem per se, but this scale is the first one that measures the BPM among all the scales handled by Openscale (AFAIK). Measuring the BPM understandably takes some extra time (you probably need 10-20s of measurement for a stable value), so maybe the tuning profiles need adjusting? But I didn't want to change the timeouts in this PR, switching to "Conservative" should do the trick. I think you can also disable the BPM metric in the Xiaomi home app, and that should solve your problem too.

I also have a branch to add the BPM metric to Openscale 😄 I was waiting for this PR to get merged before opening a new one.

@oliexdev I'll test asap, probably today

@oliexdev
Copy link
Copy Markdown
Owner

You can just set the tuning profile in the s400 handler, see d316648 You can also customized the tuning profile specifically for the s400 scale if it's needed.

…leDeviceHandler` to display a message when no special configuration is available.
@henri-now
Copy link
Copy Markdown

henri-now commented Feb 15, 2026

Hi, first of all thanks to both of you for your awesome work.
Initially I was able to get measurements from my S400 scale, however I noticed that it doesn't work for me most of the time. When I try to connect to the scale while standing on it or after weighing, when the scale is still active, it says that it is trying to connect to the scale but after that nothing seems to happen. I attached a log file of such a situation.
Please let me know if I accidentally left in sensitive information such as BLE keys. Also the BLE key changes if you reset the scale via the reset button hidden underneath the battery cover.
Log file

What bluetooth tuning profile are you using? When I was testing, I realised I needed to use the "Conservative" profile. I think this is because each profile sets a given amount of time before timing out during a measurement. IIRC the "Balanced" one sets it at like 20s. But the S400 measures the BPM (heart rate) on top of the other metrics like weight and body fat, and because of this most of the time the measurement takes more than 20s, and it times out. But not always, a few times I managed to get the full measurement on time. Switching to "Conservative" made it fully reliable.

I don't think this is a problem per se, but this scale is the first one that measures the BPM among all the scales handled by Openscale (AFAIK). Measuring the BPM understandably takes some extra time (you probably need 10-20s of measurement for a stable value), so maybe the tuning profiles need adjusting? But I didn't want to change the timeouts in this PR, switching to "Conservative" should do the trick. I think you can also disable the BPM metric in the Xiaomi home app, and that should solve your problem too.

I also have a branch to add the BPM metric to Openscale 😄 I was waiting for this PR to get merged before opening a new one.

@oliexdev I'll test asap, probably today

Okay, I got around to testing it, thanks for the input.
It started working for me most of the time when I switched to the Conservative profile and deactivated BPM measuring via the Xiaomi Home app.

At first I wasn't able to find the correct way of getting it to work, so I'll document my steps if other people face the same problems:

  1. Step on the scale
  2. Click the crossed out Bluetooth symbol in openScale (a notification opens and says "Listening for Xiaomi Scale S400...", however, this only showed up once I changed the language from German to English, so it might be useful to display the notification always in English if there is no localization)
  3. Wait for the measurement to progress
  4. The app should be able to read the measurements now

Personally I found this to be a bit confusing, however I don't know how the app usually works, as I don't own another scale. I would find it helpful if there were some other notification when a timeout occurs. Also I don't really understand on how to initiate a second measurement shortly after you just finished one, as once the crossed Bluetooth symbol is pressed, the earlier mentioned notification doesn't show up again. Another thing that confused me was that I got the notification several times to check the Bluetooth settings, which I'm not sure on how this would help me.

What I've incorrectly assumed is that the scale also stays active after a measurement was finished ("After weighing, Mi Body Composition Scale 2 is active for 15 minutes on bluetooth transmission" Export 2 to Garmin Connect), but this does not seem to be the case.

Again, thanks for the work and these are just a few of the things I noticed. Also let me know if I can contribute any work.

@JPFrancoia
Copy link
Copy Markdown
Contributor Author

@oliexdev I was getting this error:

kotlin.UninitializedPropertyAccessException: lateinit property settings has not been initialized

So I had to make settings nullable. Then it works

@oliexdev
Copy link
Copy Markdown
Owner

Hi @JPFrancoia, thanks for the effort! Unfortunately, the your fix doesn't quite work because the settings aren't being initialized correctly before use, which prevents them from being saved.
I've done some refactoring to ensure the settings lifecycle is handled properly. I also took the opportunity to remove the manual "Save" button in favor of an auto-save feature to make the UI a bit smoother. Could you please give it another try?

@JPFrancoia
Copy link
Copy Markdown
Contributor Author

No worries at all, and thank you for helping me push this PR over the line.
I just tried your changes and I can confirm they work. No crash this time. I cleaned all the data from the app and started fresh after sideloading the app. I paired with the scale, added my bluetooth key, and performed a couple of measurements. Everything worked smoothly.

Thank you!

@oliexdev oliexdev merged commit 4c8d536 into oliexdev:master Feb 18, 2026
@oliexdev
Copy link
Copy Markdown
Owner

Thanks for your awesome PR 🥇 I have merged your PR and have updated the wiki.

@watchingdogs
Copy link
Copy Markdown

Thank you all for the contribution, I will literally just buy this scale now after stumbling on this merge! 😅

@nerisal
Copy link
Copy Markdown

nerisal commented Feb 18, 2026

Thank you all for the contribution, I will literally just buy this scale now after stumbling on this merge! 😅

Yes, me too. Thank you all for your work! 😎

@pappasadrian
Copy link
Copy Markdown

pappasadrian commented Mar 4, 2026

Hello!

First of all, thank you so much for this PR, i've had this scale for a while and i've been looking for a humane way of recording its measurements.
I tried this method out and was able to connect just fine, and everything works well, including heart rate. However, I noticed a bit of a discrepancy in the body fat measurements.

The xiaomi app, as well as the scale itself in its screen shows a specific value 37.7%, but the openscale app records this as 41.3%. Both measurements were done consecutively, a few minutes apart.

image image image

I notice that there is a similar discrepancy in the measurements in @JPFrancoia 's original post. 23.6% in the xiaomi app and and 24.7% in openscale (I believe this is too much to be justified as a variance between days).

Since I have the scale, I'm available if you need me for any testing etc.

@watchingdogs
Copy link
Copy Markdown

I am having the same issue.

@JPFrancoia
Copy link
Copy Markdown
Contributor Author

Yeah I noticed this discrepancy too. I didn't pay too much attention to it since the weight was the same with both apps. My guess is that the scale sends the impedance to the apps (Xiaomi Home App or Open scales) and the apps compute the body fat from the impedance measurements. The algorithms used in each apps are probably different.

This isn't a big deal for me since I know the body fat percentages are way off anyway (it's actually ~10% lower than what the apps tells me, measured by my doctor with more electrodes than the scale, which only has 2). The impedance measurements from cheap consumer scales are always a bit so-so anyway, because they only have 2 electrodes. That's also why I tried implementing the body fat measurement with the US navy method (merged recently by @oliexdev ).
What really matters is the trend over time, not the absolute number.

@pappasadrian
Copy link
Copy Markdown

Hey @JPFrancoia, thanks for the input. I definitely agree that it is a matter of trend over time rather than absolute measurement values.

My issue with this discrepancy is that I already have previous measurements on the xiaomi app, which I tried to migrate over to openscale (via csv import). This leads to a very sudden spike/step in the measurements. I guess it could be ignored if my overall goal is to check the trend over time, but still it is somewhat annoying.

I really don't know anything about the methods of calculation of body fat, so it might be that the calculation as performed by openscale (us navy) is better. However, if there is a function that can help me (and others looking to migrate) to transform the xiaomi measurements to how openscale calculates it, it would be great.

(e.g. [Openscale value] = [xiaomi value] * a + b)

@Mushoz
Copy link
Copy Markdown

Mushoz commented Apr 20, 2026

The discrepancy is probably caused by the fact that the current s400 support only reads a single impedance value from the scale and uses that single impedance value (together with height) to calculate all the metrics. The s400 is a dual frequency impedance scale and will provide both an "impedance (high)" as well as an "impedance low" value.

Supporting it would require:

  1. Reading out both impedance values from the scale
  2. Update the logic that calculates all the metrics (bone-mass, muscle, etc) to support both impedance values

There are some other projects which do read out and use both values:

Maybe those repo's can be used for inspiration? @JPFrancoia were you aware of the fact this scale uses two impedance value, and if so, was there a reason you only ended up using a single one? It would be helpful to have as much information as possible before someone tries to have a go at this I think.

I really don't know anything about the methods of calculation of body fat, so it might be that the calculation as performed by openscale (us navy) is better. However, if there is a function that can help me (and others looking to migrate) to transform the xiaomi measurements to how openscale calculates it, it would be great.

Probably not possible. A single impedance value simply doesn't contain enough information to reconstruct the metrics you would get by using both impedance values. If it was that simple there would be no point in making dual frequency scales :)

@kobazauros
Copy link
Copy Markdown

kobazauros commented May 12, 2026

Guys thank you very much for this awesome work.
Reddit threads show that people have been trying to connect this scale to some apps for years.
I am a proud user of OpenScale app myself now, since I can finally read data from the scale.
However, I think there should be some confidence in measurements. For example the weight, heart rate measurements, and simple derivations can be collected reliably. The other measurements cannot. I think a simple asterisc with warning is enough for this case. I think this is important because the openscale app is publicly deployed in Google Play.

@JPFrancoia
Copy link
Copy Markdown
Contributor Author

I don't pay much attention to the body fat measurement because even the original Xiaomi Home App (probably using the 2 impedance values, and nope, I wasn't aware there were 2 values) is already way off (by about 9%) compared to a measurement obtained clinically. Based on the old measurements I did:

23.6% in the xiaomi app and and 24.7% in openscale

So a difference that is likely due to openscale using one impedance value instead of 2. Or maybe a different algorithm, but now that I know that there are 2 impedance values, I'd say it's that. In reality, I'm at 15% though.

Let's be honest here, this is a semi-decent measurement. Most consumer scales use one impedance value anyway. Nothing here that requires a warning.

I would fix this if I had more time, but unfortunately I'm time-constrained at the moment and it's just an extra 1.1% of fake accuracy on the body fat measurement.

If you have the time, Openscale could use your contributions. Hell, you might even get away with pointing Claude at openscale's codebase and the other codebase that uses the 2 impedance values!

@kobazauros
Copy link
Copy Markdown

I would like to thank the author for active participation in discussion.
Truly everybody's here is time-constrained, yet somebody's still making contributions.
I am not asking to fine-tune the app... I am asking to mark unreliable measurements so the user wouldn't be confused by the wierd difference in some values... Because they make this truly remarkable contribution look like a toy.
Screenshot 2026-05-14 123649

@DanyPM
Copy link
Copy Markdown
Contributor

DanyPM commented May 14, 2026

Gave a try a dual impedance measurements in #1367

About to test it on my own scale: would love the feedback on the approach

@JPFrancoia
Copy link
Copy Markdown
Contributor Author

Thanks, that's the spirit. One makes it possible, one perfects it. Happy to test in the next couple of days too

@JPFrancoia
Copy link
Copy Markdown
Contributor Author

I tested your branch. I think there is an improvement on the Body fat % (albeit the value in openscale is different from the xiaomi app) and on the bone mass.

@DanyPM
Copy link
Copy Markdown
Contributor

DanyPM commented May 17, 2026

Thanks ! Open to any ideas to improve

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.

9 participants