The code that talks to the band is a port of Gadgetbridge XiaomiAuthService. Band 8 support, including the protobuf devices, is on their Xiaomi wearables page. Bleak is the Python Bluetooth library I used on the Mac. The local page is a file served by an Onion Omega serving that file.
The band is a Chinese Xiaomi Smart Band 8, model M2239B1, bought from a seller in China. The reset text on the band is in Chinese. Xiaomi uses the same model number, M2239B1, on the global band, so the model number did not tell me the region. This one shows up in a scan as Xiaomi Smart Band 8 40CA. Those last four characters are the end of its Bluetooth MAC address.
I wanted the steps, heart rate, and battery on a dashboard I made, instead of opening Mi Fitness every time. I set sync.py to run every day at 10pm.
Getting the key took a long time. I use an iPhone, and Mi Fitness on the iPhone would not pair this band, so I charged an old Google Pixel 6a and installed the Mi Fitness app there. It would not connect. I remembered this was the China version of the band, so I changed the app region to mainland China. That still did not work. The app said I had to install a separate China APK. I uninstalled the global app and installed that APK from the link Xiaomi gives. It still would not connect, and I thought I was not going to get a key at all. Then it connected. That one pairing is where the 16-byte key comes from. The app keeps it. The script does not create a new key.
The key is already on the phone after that pairing. Mi Fitness writes it into its own log, under Android/data/com.xiaomi.wearable/files/log/. On the Pixel 6a I could not open that folder. Android 11 and later blocks the file manager from another app's directory, so the folder looks empty even when the log is there. That is why the phone never showed me the 16 bytes.
What worked was Notify for Xiaomi, the notification app on the Play Store. I left the band paired in Mi Fitness and did not unpair it. In Notify I added a Mi Band 8. On the auth key screen I tapped GET and chose the automatic online login, not the option that reads a log file. Notify on this Pixel cannot read those logs. The login has to be the same Xiaomi email and password as Mi Fitness, and it has to be the email login, not Google. The key filled in by itself. It is 32 hex characters, which is 16 bytes. If it shows a 0x in front, that part is not part of the key. I copied the hex into keys.env as AUTH_KEY.
The Mac does not pick up the Bluetooth bond from the phone. Bleak on macOS does not have a pair() call. sync.py uses the copied key and speaks the Xiaomi protocol itself.
Talking to the band from the Mac
sync.py runs on the Mac. Bleak scans for 12 seconds. macOS does not give me the Bluetooth MAC address. It gives a UUID, so I match BAND_BLE_ID first, then the 40CA in the advertised name, then the full MAC address if the adapter actually shows one.
While Mi Fitness still held the connection, this Mac did not see the band. I force-stop Mi Fitness, turn the phone's Bluetooth off, and run the script with the band next to the laptop. BleakClient is called with timeout=30, which is 30 seconds. MiBand8Client sends phoneName="Mac" and region="CN", then reads battery, steps, and heart rate.
What gets saved
store.py opens store.db next to the script and inserts one row: steps, heart_rate, battery, synced_at in UTC. No account, no upload. latest() is just ORDER BY id DESC LIMIT 1, so the page is always the newest row.
The key stays in keys.env
The band already shows FE95 before you log in. The key is what gets you through the handshake after that. It sits in keys.env next to BAND_MAC and BAND_BLE_ID. keys.env is not committed. keys.env.example is the empty example.
Service 0000fe95-0000-1000-8000-00805f9b34fb. Writes go to 00000052-0000-1000-8000-00805f9b34fb. Notifications come back on 00000051-0000-1000-8000-00805f9b34fb. The payloads are protobuf (xiaomi_pb2). The handshake is HMAC-SHA256 over the nonces. After that, the command payloads are AES-CCM. Battery is a system command. Steps and heart rate come off the real-time health stream. The client starts that stream and stops it before it disconnects.
Local dashboard
The row also leaves store.db. export_json writes the latest row to onion/www/miband/data.json: steps, heart rate, battery, and synced_at. push-to-onion.sh copies that file to an Onion Omega on the house LAN, at /www/miband/data.json.
The page is onion/www/index.html, served by the Omega. It fetches /miband/data.json with cache: no-store and again every 15 seconds. Steps today, heart rate, battery percent, last sync. The page never calls Xiaomi. With the phone's Bluetooth off, the Mac is the one talking to the band. The Omega just serves the file the Mac pushed.