Step on. Get a notification. Keep your data.
A small Android app and Python toolkit for the Mi Smart Scale 2 (XMTZC04HM). Read live weights, recover stored measurements, and keep everything local.
Android app · Python CLI · BLE protocol
Weighing should take one step. The goal is to capture a reading while your phone stays in your pocket, then put the result in a notification you can actually use. The Android app uses system-managed Bluetooth events to wake its capture code, with a short foreground session while weighing or syncing history.
- Watch it settle. Changing weight appears in an ongoing notification; the final result becomes a regular New reading notification.
- Catch up from memory. History sync can recover a final result missed over BLE and deduplicates readings already received.
- Keep the readings you care about. Mark someone else's measurement Not interested, then Restore it whenever you want. Both actions work in the app and notifications.
- Take your data with you. Export every saved reading to JSON, including original
packets, timestamps, units, and the reversible
deletedflag. - Keep it local. The Android app has no Internet permission. It uses no account, analytics, battery-exemption request, or permanent idle foreground service.
Experimental PoC. The Python tools have been tested against real XMTZC04HM hardware. Android capture flows have been exercised with a virtual BLE scale; the app is installed on a Galaxy S23 running Android 16, with physical background capture validation still pending. Live delivery timing depends on Android and the phone's Bluetooth implementation. See current limits.
flowchart LR
scale[Mi Smart Scale 2] -->|BLE advertisement| wake[Android scan intent]
wake --> live[Live weight notification]
wake -->|New reading or sync due| history[Read scale memory]
live -->|Settled result| db[(Local readings)]
history -->|Unseen records| db
db --> result[New reading: share or mark]
db --> json[Export JSON]
The app registers reusable scan intents and companion-device presence observation. A new settled reading requests history sync; availability events sync when the last successful sync was at least ten minutes ago. There is no periodic polling job. The Android notes explain the triggers, batching, recovery, and process lifecycle.
Requires Android 12+, Bluetooth LE, and Android's companion-device setup support.
To build, install JDK 17 and Android SDK 36, and set ANDROID_HOME.
git clone https://github.com/HugoJF/mi-scale.git
cd mi-scale/android
./gradlew :app:assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk- Open Mi Scale and tap Set up scale.
- Allow Nearby devices and notifications.
- Wake the scale and select MI SCALE2 in Android's device chooser.
- Wait for Ready, close the app, and try a weighing.
The initial memory snapshot establishes a silent baseline; existing scale history is marked as seen rather than imported. Subsequent unseen readings are saved. The app shows all saved readings, including those marked Not interested. Export JSON opens Android's file picker so you can choose where to save them.
The app is a native Kotlin prototype using platform UI, SQLite, BLE, and companion APIs. It does not require Google Play services or an application backend.
Requires Python 3.11+, a BLE adapter, and a running Bluetooth service. Linux uses BlueZ through Bleak. Close Zepp Life / Mi Fit while connecting to the scale.
cd mi-scale
uv venv
uv pip install -r requirements.txt
# Wake the scale and find its address.
.venv/bin/python mi_scale.py scan --duration 15
# Replace the address with the one found above.
.venv/bin/python mi_scale.py watch --address AA:BB:CC:DD:EE:FF| Command | What you get |
|---|---|
scan |
Scale discovery, service data, and address |
watch |
Live and settled readings as JSON Lines; optional GATT indications |
history |
Stored ten-byte measurements with original timestamps and raw bytes |
inspect |
GATT services, characteristic properties, device information, and clock |
set-time |
Local clock update, with readback verification when supported |
decode |
Offline decoding of a captured packet |
Clock updates are available in the Python CLI. The Android app leaves the scale's clock unchanged. History transfer enables recording and requests all available records without sending the separate received-history acknowledgement or erase command. CLI usage and troubleshooting cover the complete options.
Android exports a JSON object containing schema_version: 1 and a readings array.
Each reading retains its ID, scale address, original packet, receipt time, scale
clock, native weight/unit, kilogram conversion, BLE status flags, and deleted
boolean. Marking a reading changes that boolean; export includes both states.
The scale clock is local wall time without a time zone. An unset or invalid clock
stays null; receipt time is recorded separately in UTC. The app offers export
only. Data details document the fields.
| Area | Evidence |
|---|---|
| Real scale | Discovery, live/stable/removed broadcasts, complete history transfer, and clock write/readback on firmware V1.0.0.12 |
| Python | 25 offline tests for packets, history framing, deduplication, and clock behavior |
| Android logic | 22 JVM tests and 10 emulator instrumentation tests |
| Virtual BLE radio | Screen-off live/final capture, wake after process death, missed-final history recovery, sync gating, and runtime Bluetooth restart |
| Reading controls | App and notification mark/restore, persistence, and actual file-picker JSON export/cancel |
| Physical Android phone | Installed on Galaxy S23 / Android 16; background behavior and battery consumption await physical validation |
Cold-process recovery after Bluetooth is toggled still needs a successful physical test. The app cannot receive while force-stopped, without permissions, or out of range. A virtual radio test does not establish Samsung power-policy behavior.
This project targets the weight-only XMTZC04HM, with ten-byte packets under
service 0x181D. The Body Composition Scale 2 (XMTZC05HM) uses a different
protocol. Check the model printed on the underside of your scale.
# Python tests run offline and require no scale.
python3 -m unittest -v
# Android unit tests and lint.
cd android
./gradlew :app:testDebugUnitTest :app:lintDebugFor Android integration tests, use a disposable emulator. The
emulator guide includes the virtual
scale's weigh, memory, and removed commands. The
visibility logger measures host-received
BLE reports from a real scale without connecting or writing to it.
| Path | Purpose |
|---|---|
android/ |
Native Android app, unit tests, and instrumentation tests |
mi_scale.py |
Python BLE commands and decoder |
test_mi_scale.py |
Offline Python tests |
tools/ |
Virtual BLE scale and visibility logger |
PROTOCOL.md |
Packet layout, history commands, clock semantics, and research sources |
ANDROID_APP.md |
App behavior, background strategy, building, and limitations |
PYTHON_CLI.md |
Python command reference and troubleshooting |
Build output, APKs, logs, personal captures, databases, editor settings, and test screenshots are excluded from Git. The Gradle wrapper and build configuration are included so the app can be built from source.
Try another firmware or Android phone and open an issue with the model, software version, Android version, whether the screen was off, and whether the app process had been killed. Use synthetic or redacted examples when sharing packets; weighings and Bluetooth addresses can be personal data.
The protocol research builds on community documentation from openScale, Gadgetbridge, ESPHome, BLE Scale Sync, and ScaleLink. Exact source links and the observations they support are in PROTOCOL.md.