The band is the Chinese version of the Xiaomi Smart Band 8, model M2239B1, bought from a CN seller. Reset text on the band is Chinese. The BLE name is Xiaomi Smart Band 8 40CA. That name does not say CN or global. The About screen model string is the same on both variants.

A CN band will not finish pairing in Mi Fitness unless the app region is 中国大陆. Global region comes back as "device not supported." I paired it once in the China build of Mi Fitness, then left that pairing in place. The Mac does not create the pairing. It reuses the key from that one phone session, stored in keys.env as AUTH_KEY.

Laptop link

sync.py runs on the Mac. Bleak scans for 12 seconds. macOS gives the peripheral a UUID, not the radio MAC, so the match is BAND_BLE_ID first, then the last four MAC digits inside the advertised name (40CA), then the full MAC on adapters that still expose it.

The phone has to let go. If Mi Fitness is holding the BLE link, the Mac scan is empty. Force-stop Mi Fitness and turn the phone radio off, then run the script with the band near the laptop. BleakClient connects with a 30-second timeout. MiBand8Client tells the band phoneName="Mac" and region="CN" on the FE95 service, then reads battery, steps, and heart rate.

What a sync writes

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 ORDER BY id DESC LIMIT 1.

The key stays in keys.env

The band will not answer the Xiaomi service 0000fe95-0000-1000-8000-00805f9b34fb without the auth key from the original pairing. That key is 32 hex characters in keys.env, next to BAND_MAC and BAND_BLE_ID. keys.env is not committed. keys.env.example is the placeholder file.

MiBand8Client loads that key and speaks the FE95 command characteristic. Writes go to 00000052-0000-1000-8000-00805f9b34fb, notifications come back on 00000051-0000-1000-8000-00805f9b34fb. Payloads are protobuf (xiaomi_pb2). The session encrypts with AES-CCM and checks HMAC-SHA256. Battery is a system command. Steps and heart rate come from the real-time health stream, which the client starts and then stops before it disconnects. BleakClient timeout is 30 seconds.

Local dashboard

The row does not stay only in 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, and fills four fields: steps today, heart rate, battery percent, last sync. Nothing in that page calls Xiaomi. The Mac is the only thing that talks to the band. The Omega only serves the JSON the Mac just pushed.