mifit 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. mifit-0.2.0/LICENSE +21 -0
  2. mifit-0.2.0/MANIFEST.in +3 -0
  3. mifit-0.2.0/PKG-INFO +397 -0
  4. mifit-0.2.0/README.md +362 -0
  5. mifit-0.2.0/THIRD_PARTY_NOTICES +50 -0
  6. mifit-0.2.0/pyproject.toml +62 -0
  7. mifit-0.2.0/setup.cfg +4 -0
  8. mifit-0.2.0/src/mifit/__init__.py +67 -0
  9. mifit-0.2.0/src/mifit/auth/__init__.py +0 -0
  10. mifit-0.2.0/src/mifit/auth/lifetime.py +140 -0
  11. mifit-0.2.0/src/mifit/auth/qr.py +558 -0
  12. mifit-0.2.0/src/mifit/auth/renew.py +110 -0
  13. mifit-0.2.0/src/mifit/auth/session.py +155 -0
  14. mifit-0.2.0/src/mifit/chunks.py +239 -0
  15. mifit-0.2.0/src/mifit/cli/__init__.py +755 -0
  16. mifit-0.2.0/src/mifit/cli/__main__.py +5 -0
  17. mifit-0.2.0/src/mifit/cli/partial.py +241 -0
  18. mifit-0.2.0/src/mifit/client.py +540 -0
  19. mifit-0.2.0/src/mifit/crypto.py +138 -0
  20. mifit-0.2.0/src/mifit/domains/__init__.py +0 -0
  21. mifit-0.2.0/src/mifit/domains/activity_daily.py +127 -0
  22. mifit-0.2.0/src/mifit/domains/base.py +160 -0
  23. mifit-0.2.0/src/mifit/domains/consumption.py +102 -0
  24. mifit-0.2.0/src/mifit/domains/daily_report.py +141 -0
  25. mifit-0.2.0/src/mifit/domains/days.py +69 -0
  26. mifit-0.2.0/src/mifit/domains/heart_rate.py +138 -0
  27. mifit-0.2.0/src/mifit/domains/heart_rate_daily.py +69 -0
  28. mifit-0.2.0/src/mifit/domains/passthrough.py +136 -0
  29. mifit-0.2.0/src/mifit/domains/profile.py +81 -0
  30. mifit-0.2.0/src/mifit/domains/raw_window.py +81 -0
  31. mifit-0.2.0/src/mifit/domains/sleep.py +332 -0
  32. mifit-0.2.0/src/mifit/domains/spo2.py +75 -0
  33. mifit-0.2.0/src/mifit/domains/spo2_daily.py +69 -0
  34. mifit-0.2.0/src/mifit/domains/steps.py +102 -0
  35. mifit-0.2.0/src/mifit/domains/stress_daily.py +84 -0
  36. mifit-0.2.0/src/mifit/domains/weight.py +79 -0
  37. mifit-0.2.0/src/mifit/domains/workout_detail.py +472 -0
  38. mifit-0.2.0/src/mifit/domains/workouts.py +212 -0
  39. mifit-0.2.0/src/mifit/errors.py +72 -0
  40. mifit-0.2.0/src/mifit/fds/__init__.py +36 -0
  41. mifit-0.2.0/src/mifit/fds/binary.py +137 -0
  42. mifit-0.2.0/src/mifit/fds/cipher.py +74 -0
  43. mifit-0.2.0/src/mifit/fds/download.py +224 -0
  44. mifit-0.2.0/src/mifit/fds/ids.py +76 -0
  45. mifit-0.2.0/src/mifit/fds/layouts.py +273 -0
  46. mifit-0.2.0/src/mifit/fds/urls.py +94 -0
  47. mifit-0.2.0/src/mifit/models/__init__.py +0 -0
  48. mifit-0.2.0/src/mifit/models/activity.py +33 -0
  49. mifit-0.2.0/src/mifit/models/consumption.py +30 -0
  50. mifit-0.2.0/src/mifit/models/heart_rate.py +40 -0
  51. mifit-0.2.0/src/mifit/models/profile.py +32 -0
  52. mifit-0.2.0/src/mifit/models/sleep.py +74 -0
  53. mifit-0.2.0/src/mifit/models/spo2.py +38 -0
  54. mifit-0.2.0/src/mifit/models/steps.py +29 -0
  55. mifit-0.2.0/src/mifit/models/stress.py +35 -0
  56. mifit-0.2.0/src/mifit/models/weight.py +24 -0
  57. mifit-0.2.0/src/mifit/models/workout_detail.py +62 -0
  58. mifit-0.2.0/src/mifit/models/workouts.py +46 -0
  59. mifit-0.2.0/src/mifit/paths.py +30 -0
  60. mifit-0.2.0/src/mifit/py.typed +0 -0
  61. mifit-0.2.0/src/mifit/region.py +280 -0
  62. mifit-0.2.0/src/mifit/transport.py +369 -0
  63. mifit-0.2.0/src/mifit.egg-info/PKG-INFO +397 -0
  64. mifit-0.2.0/src/mifit.egg-info/SOURCES.txt +66 -0
  65. mifit-0.2.0/src/mifit.egg-info/dependency_links.txt +1 -0
  66. mifit-0.2.0/src/mifit.egg-info/entry_points.txt +2 -0
  67. mifit-0.2.0/src/mifit.egg-info/requires.txt +8 -0
  68. mifit-0.2.0/src/mifit.egg-info/top_level.txt +1 -0
mifit-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jenwein
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ # The sdist is the package and its metadata. setuptools would otherwise add
2
+ # tests/test*.py by default, without the conftest, helpers and fixtures they need.
3
+ prune tests
mifit-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,397 @@
1
+ Metadata-Version: 2.4
2
+ Name: mifit
3
+ Version: 0.2.0
4
+ Summary: Read your own Mi Fitness data from the Xiaomi cloud.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/Jenwein/mifit
7
+ Project-URL: Source, https://github.com/Jenwein/mifit
8
+ Project-URL: Issues, https://github.com/Jenwein/mifit/issues
9
+ Project-URL: Changelog, https://github.com/Jenwein/mifit/blob/main/CHANGELOG.md
10
+ Keywords: mi-fitness,xiaomi,health,fitness,wearable,sleep,heart-rate
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Utilities
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ License-File: THIRD_PARTY_NOTICES
27
+ Requires-Dist: requests>=2.31
28
+ Requires-Dist: qrcode>=7.4
29
+ Requires-Dist: cryptography>=42
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest>=8; extra == "test"
32
+ Requires-Dist: tzdata; extra == "test"
33
+ Requires-Dist: pyyaml; extra == "test"
34
+ Dynamic: license-file
35
+
36
+ # mifit
37
+
38
+ [![CI](https://github.com/Jenwein/mifit/actions/workflows/ci.yml/badge.svg)](https://github.com/Jenwein/mifit/actions/workflows/ci.yml)
39
+ [![PyPI](https://img.shields.io/pypi/v/mifit)](https://pypi.org/project/mifit/)
40
+
41
+ Read your own Mi Fitness (小米运动健康) data from the Xiaomi cloud: a small
42
+ Python library and a command-line tool.
43
+
44
+ The Mi Fitness app syncs your steps, heart rate, sleep and more to the Xiaomi
45
+ cloud, but Xiaomi offers no public API for individuals. mifit signs in to your
46
+ own account by QR code, reads that synced data and returns it as JSON or as
47
+ Python objects, so you can build on it.
48
+
49
+ > **Unofficial.** Not affiliated with, endorsed by or supported by Xiaomi.
50
+ > See [Disclaimer](https://github.com/Jenwein/mifit#disclaimer).
51
+
52
+ ## What it reads today
53
+
54
+ | Domain | What you get | `records` type | Checked against the app |
55
+ |---|---|---|---|
56
+ | `steps` | steps and distance per day | `StepRecord` | yes |
57
+ | `heart_rate` | raw heart-rate samples | `HeartRatePoint` | yes |
58
+ | `heart_rate_daily` | heart rate per day: average, maximum, minimum, resting | `HeartRateDailySummary` | yes |
59
+ | `consumption` | calories per day: resting, active, intake, total | `ConsumptionRecord` | yes |
60
+ | `sleep` | sleep per day: totals, stage minutes, sessions with their stage timeline | `SleepRecord` → `SleepSession` → `SleepStage` | yes; the sleep score could not be checked and is in `raw` only |
61
+ | `workouts` | one entry per workout: start and end, sport, duration, distance, active and total calories, average and maximum heart rate, steps | `Workout` | yes |
62
+ | `workout_detail` | per workout (outdoor runs and rides): per-second samples (heart rate, cumulative distance and steps, cadence, pace or speed) and the GPS track | `WorkoutDetail` → `WorkoutSample` / `TrackPoint` | yes (one outdoor run, one ride); altitude is in `raw` only: the run's was a few metres off the app's |
63
+ | `spo2_daily` | blood oxygen (SpO2) per day: average, maximum, minimum | `Spo2DailySummary` | yes |
64
+ | `spo2` | raw SpO2 readings | `Spo2Point` | yes |
65
+ | `stress_daily` | stress level per day: average, highest, lowest, and minutes in each stress band (relaxed, mild, moderate, severe) | `StressDailySummary` | yes |
66
+ | `activity_daily` | activity per day, as on the app's 活力指标 page: activity count (活动次数), activity minutes (活动时长), active calories (卡路里) | `ActivityDailySummary` | yes |
67
+ | `weight` | weight entries: time, weight, BMI (body composition in `raw` only) | `WeightRecord` | yes; body composition could not be checked and is in `raw` only |
68
+ | `profile` (its own command: `mifit profile`) | height, weight, daily step, calorie and activity-time goals | `UserProfile` | yes |
69
+ | `raw` (CLI: `mifit raw`; Python: `client.raw`, `raw_window`, `daily_report`) | the rows of any fitness-data or daily-report key, as the service sends them | none: the service's rows, uninterpreted | not applicable: nothing is interpreted |
70
+
71
+ "Checked against the app" means the values were compared with what the Mi
72
+ Fitness app shows, on one real account (China region, UTC+8). More is
73
+ planned. Meanwhile, `mifit raw` (in Python `raw_window` and `daily_report`)
74
+ returns the rows of any other key of the fitness-data or (with `--daily`)
75
+ daily-report endpoint, uninterpreted.
76
+
77
+ What changed in each version, breaking changes included, is in
78
+ [CHANGELOG.md](https://github.com/Jenwein/mifit/blob/main/CHANGELOG.md).
79
+
80
+ ## Install
81
+
82
+ Requires Python 3.12 or later; `uv` installs one for you if needed.
83
+
84
+ ```bash
85
+ uv tool install mifit # recommended: its own environment, `mifit` on your PATH
86
+ pipx install mifit # the same, with pipx
87
+ pip install mifit # into the current environment, to use it as a library
88
+ ```
89
+
90
+ Upgrade: `uv tool upgrade mifit` (or `pipx upgrade mifit`, `pip install -U mifit`).
91
+
92
+ ## Log in
93
+
94
+ ```bash
95
+ mifit login
96
+ ```
97
+
98
+ A QR code is drawn in the terminal, with the login URL below it. Scan it with
99
+ the Xiaomi / Mi Fitness app on your phone (or open the URL) and approve the
100
+ sign-in. Your password never passes through mifit.
101
+
102
+ The session is saved to `~/.mifit/session.json` (on Windows
103
+ `%USERPROFILE%\.mifit\session.json`). It grants access to your Mi Fitness
104
+ account's health data (not only reading). It also holds your account's
105
+ passToken, which mifit uses to renew the session when it expires. The passToken
106
+ is account-level: it can obtain tokens for any Xiaomi service, so the file
107
+ grants access to your whole Xiaomi account, not just your Mi Fitness health
108
+ data, and it stays useful long after the session token itself has expired.
109
+ Treat it like a password, and do not share it or commit it. On Linux and macOS
110
+ the file is readable by you only; on Windows it relies on your user profile's
111
+ permissions.
112
+
113
+ If Xiaomi asks for an extra security confirmation, `mifit login` prints a URL:
114
+ open it, confirm, then run `mifit login` again.
115
+
116
+ ## Export
117
+
118
+ ```bash
119
+ mifit export <domain> --from YYYY-MM-DD --to YYYY-MM-DD [--json PATH] [--full] [--no-track] [--chunk day|week|month] [--long] [--resume]
120
+ ```
121
+
122
+ - `<domain>`: `activity_daily`, `consumption`, `heart_rate`, `heart_rate_daily`, `sleep`,
123
+ `spo2`, `spo2_daily`, `steps`, `stress_daily`, `weight`, `workouts` or `workout_detail`
124
+ (per-second samples and GPS track of outdoor runs and rides).
125
+ - `--from` / `--to`: first and last day, inclusive, as local calendar days.
126
+ - `--json PATH`: write to a file instead of standard output.
127
+ - `--full`: `heart_rate` only; page backwards through the history instead of
128
+ querying one time window (see [Known limitations](https://github.com/Jenwein/mifit#known-limitations)).
129
+ This is a single read: it ignores `--from`, so it needs no `--long` however
130
+ long the range, and it cannot be combined with `--chunk`, `--long` or
131
+ `--resume`.
132
+ - `--no-track`: `workout_detail` only; skip the GPS file, so no track is
133
+ requested or written.
134
+ - `--chunk day|week|month`: the chunk size (1, 7 or 31 days) for a range
135
+ longer than one chunk; the default depends on the domain.
136
+ - `--long`: allow a range over 92 days.
137
+ - `--resume`: continue an interrupted export from `PATH.partial` (needs
138
+ `--json`).
139
+
140
+ ```bash
141
+ mifit export sleep --from 2026-10-01 --to 2026-10-07 --json sleep.json
142
+ ```
143
+
144
+ The output is one JSON object with three lists:
145
+
146
+ ```json
147
+ { "records": [], "raw": [], "notes": [] }
148
+ ```
149
+
150
+ ### Long ranges
151
+
152
+ A range longer than one chunk (7 days for raw heart rate, SpO2 and workout
153
+ detail, 31 days for everything else, counted from `--from`) is read chunk by
154
+ chunk, one request at a time, with progress on standard error. A shorter range
155
+ is a single read, as before. The chunks are merged into one output of the same
156
+ shape; with more than one chunk, each note starts with the chunk it came
157
+ from. Ranges over 92 days need `--long`: they take many requests and can run
158
+ for many minutes. The one exception is `heart_rate --full`: it ignores
159
+ `--from` and is a single read, never chunked, so it needs no `--long`.
160
+
161
+ ```bash
162
+ mifit export heart_rate --from 2026-01-01 --to 2026-06-30 --long --json heart_rate.json
163
+ ```
164
+
165
+ With `--json PATH`, progress is kept in `PATH.partial`: your exported data
166
+ plus the export's arguments (domain, dates, chunk size), no credentials.
167
+ Treat it like the output itself. If the export stops (a network error, an
168
+ expired session, Ctrl+C), the finished chunks stay in that file; rerun the
169
+ same command with `--resume` and only the missing chunks are read. The output
170
+ is the same as an uninterrupted run's, and the partial file is deleted once
171
+ it is written. An export to standard output is chunked the same way but
172
+ cannot be resumed.
173
+
174
+ A failed chunk stops the export; nothing is retried automatically. If one
175
+ chunk keeps failing, you can skip it by exporting the ranges before and after
176
+ it separately.
177
+
178
+ ### Any key: `mifit raw`
179
+
180
+ ```bash
181
+ mifit raw <key> --from YYYY-MM-DD --to YYYY-MM-DD [--daily [--tag TAG]] [--json PATH] [--long]
182
+ ```
183
+
184
+ Reads any key of the fitness-data or (with `--daily`) daily-report endpoint as
185
+ the service sends it, whether or not mifit also has a domain for it
186
+ (`heart_rate` has one):
187
+
188
+ - It writes `{records: [], raw, notes}`. `raw` holds the service's rows, with
189
+ their own fields and `value` decoded from its JSON string; `records` is
190
+ always empty.
191
+ - For a raw key it asks the service a day early and keeps the rows inside your
192
+ window, so the first hours of the window are complete.
193
+ - `--daily` reads the key's daily report instead, one row per local day that
194
+ has data; `--tag TAG` picks another aggregated tag (default `daily_report`).
195
+ - It is one read, paged: a range over 92 days needs `--long`, and there is no
196
+ `--resume`.
197
+ - The keys known so far are listed in [notes/domain-catalog.md](https://github.com/Jenwein/mifit/blob/main/notes/domain-catalog.md).
198
+
199
+ ```bash
200
+ mifit raw stress --from 2026-10-01 --to 2026-10-07 --json stress.json
201
+ ```
202
+
203
+ ## Python API
204
+
205
+ ```python
206
+ from mifit import MiFit
207
+
208
+ client = MiFit() # uses the session saved by `mifit login`
209
+
210
+ steps = client.steps("2026-10-01", "2026-10-07")
211
+ for day in steps.records:
212
+ print(day.date, day.steps, day.distance_m)
213
+
214
+ sleep = client.sleep("2026-10-01", "2026-10-07")
215
+ energy = client.consumption("2026-10-01", "2026-10-07")
216
+ heart = client.heart_rate("2026-10-07", "2026-10-07") # one time window
217
+ heart_full = client.heart_rate("2026-10-07", "2026-10-07", full=True)
218
+
219
+ detail = client.workout_detail("2026-10-12", "2026-10-12") # samples and GPS track
220
+ no_gps = client.workout_detail("2026-10-12", "2026-10-12", track=False)
221
+
222
+ rows = client.raw("sleep", "2026-10-07", "2026-10-07") # any fitness-data key, as served
223
+ window = client.raw_window("stress", "2026-10-07", "2026-10-07") # a complete window
224
+ daily = client.daily_report("valid_stand", "2026-10-01", "2026-10-07") # daily rows
225
+ ```
226
+
227
+ `raw_window(key, ...)` is `raw` made complete: it asks a day early and keeps
228
+ the rows of your window, with `value` decoded. `daily_report(key, ...)` returns
229
+ the key's daily rows, one per local day that has data, with `tag=` to pick a
230
+ tag other than `daily_report`. Both give `records=[]` and the rows in `raw`.
231
+
232
+ Which keys exist and what is verified: [notes/domain-catalog.md](https://github.com/Jenwein/mifit/blob/main/notes/domain-catalog.md).
233
+
234
+ `workout_detail` gives one `WorkoutDetail` per workout in `records`: its
235
+ `workout` summary (the `Workout` of `workouts`), its `samples` (`WorkoutSample`,
236
+ one per recorded second) and its `track` (`TrackPoint`); `track=False` asks
237
+ for no GPS file.
238
+
239
+ Dates are `"YYYY-MM-DD"` strings (local calendar days, inclusive) or ints of
240
+ Unix seconds. Each domain method returns a result with `records`, `raw` and
241
+ `notes`; `raw(key, ...)` returns the service's rows as they came, for any key
242
+ of the fitness-data endpoint (`data/get_fitness_data_by_time`), not the daily
243
+ reports (see `raw_window` for a complete window). Errors specific to mifit
244
+ derive from `mifit.MiFitError`: `AuthError` (no usable session, or HTTP 401),
245
+ its subclass `RenewalError` (an HTTP 401 the automatic renewal could not fix;
246
+ log in again), `ApiError` (the service refused the request or sent something
247
+ unreadable) and `NotificationRequiredError` (raised by the login when Xiaomi
248
+ asks for a security confirmation). Network failures surface as `requests`
249
+ exceptions, and a bad date argument raises `ValueError` or `TypeError`.
250
+
251
+ For a long range, `read_chunks` reads one chunk at a time (the CLI's chunk
252
+ sizes; `size="day"`, `"week"` or `"month"` overrides them) and `merge_chunks`
253
+ joins the parts. Its dates are `"YYYY-MM-DD"` strings only.
254
+
255
+ ```python
256
+ from mifit.chunks import merge_chunks
257
+
258
+ parts = []
259
+ for chunk, part in client.read_chunks("heart_rate", "2026-01-01", "2026-06-30"):
260
+ print(chunk.label, len(part.records)) # each chunk is read when the loop asks for it
261
+ parts.append((chunk, part))
262
+ heart = merge_chunks(parts) # its records are those of one read of the whole range
263
+ ```
264
+
265
+ `read_chunks(..., skip=labels)` passes over the chunks whose labels you
266
+ already have, to resume, and `options=` gives keyword arguments to every
267
+ chunk's call, such as `{"track": False}` for `workout_detail`.
268
+ `options={"full": True}` is refused with `ValueError`: a full read pages back
269
+ from each chunk's end, so every chunk would read the same samples again; call
270
+ `heart_rate(..., full=True)` once instead. The library has no 92-day limit: a
271
+ long range is many requests, so ask for what you need.
272
+
273
+ When a request is refused with HTTP 401, the client renews the session once
274
+ from its passToken and sends the request again. A client that uses the
275
+ session saved by `mifit login` saves the renewed session back to
276
+ `~/.mifit`. Explicit sessions (`MiFit(session)`) are renewed in memory; read
277
+ `client.session` to keep the renewed one. `MiFit(auto_renew=False)` turns
278
+ renewal off, so a 401 raises `AuthError` straight away.
279
+
280
+ `MiFit(http=...)` injects the HTTP client, to keep a client offline. With
281
+ renewal on, the renewal runs on that client too, so its `get` must accept the
282
+ `cookies=` and `allow_redirects=` keywords as well as the usual `params=`,
283
+ `headers=` and `timeout=` (a `requests.Session` does). Without an injected
284
+ client, the renewal uses a fresh `requests.Session` of its own.
285
+
286
+ ## Two layers: `raw` and `records`
287
+
288
+ - **`raw`** is the service's decoded payloads (the inner `value` of each row,
289
+ or for consumption the response's `result` object; for `workout_detail`,
290
+ each workout's decoded summary and files as stored values), with Xiaomi's
291
+ own field names. It is not a complete dump of every row the service sent.
292
+ The generic reads are the exception: for `raw_window`, `daily_report` and
293
+ `mifit raw`, `raw` holds each whole row the service sent for your window,
294
+ its outer fields included and its `value` decoded.
295
+ - **`records`** are typed dataclasses with neutral names (`asleep_min`,
296
+ `avg_hr_bpm`, ...). A field's meaning is established from what the app
297
+ displays or from the app's own code before it enters this layer. One, the
298
+ REM stage, rests on the app's code alone, because the author's device
299
+ reports no REM. The sleep score is not in this layer: the app showed only a
300
+ rating, so the number could not be checked; it is in `raw` as `sleep_score`.
301
+ A value the service did not report is `None`, never a made-up `0`.
302
+ - **`notes`** are caveats about that particular result: skipped rows, data
303
+ that may be incomplete.
304
+
305
+ ## Known limitations
306
+
307
+ - **Cloud only, and the cloud lags.** mifit reads what the app has synced to
308
+ the Xiaomi cloud. If cloud sync is off or has not run yet, data is missing
309
+ or late; the latest day may be partial.
310
+ - **The calorie ring is synthesized by the app.** The app combines cloud
311
+ activity with phone-side activity, so `consumption` for the latest day can
312
+ read lower than the ring. `step_calories` in `steps` is the step report's
313
+ walking-calorie figure, not the ring.
314
+ - **Raw windows are corrected by the SDK.** The service drops the samples at
315
+ the start of a raw-data window (the first bucket); every dense raw key
316
+ checked does this: `heart_rate`, `spo2`, `stress` and `calories`
317
+ ([notes/08](https://github.com/Jenwein/mifit/blob/main/notes/08-heart-rate-keys-and-completeness.md),
318
+ [notes/11](https://github.com/Jenwein/mifit/blob/main/notes/11-spo2-weight-profile.md), [notes/domain-catalog.md](https://github.com/Jenwein/mifit/blob/main/notes/domain-catalog.md)). The `heart_rate`, `spo2` and `weight`
319
+ reads, `raw_window` and `mifit raw` ask the service for a day before the
320
+ start and keep only the requested window. `raw(key, ...)` returns the
321
+ service's answer as it came, for any key, without this correction: use
322
+ `raw_window` for a complete window. `--full` (`full=True`, `heart_rate` only)
323
+ pages backwards from the end date instead; it ignores the start date and
324
+ stops after 10 pages, saying so in `notes` when older data remains.
325
+ - **NaN and Infinity become `null`.** JSON has no such numbers, so where the
326
+ service sends one, the CLI writes `null` and a note says how many it
327
+ replaced.
328
+ - **A read the service breaks off says so.** When the service reports more
329
+ pages but sends no usable cursor to fetch them, the read stops there and
330
+ `notes` says that rows in the range may be missing.
331
+ - **Workout detail reads outdoor runs and rides only.** It covers the file
332
+ formats seen so far (record file versions 8 and 5, GPS file version 2).
333
+ Walks, hikes, swims, indoor runs and other sports, and other file versions,
334
+ get a note and no samples: each needs a real file and a hand-written layout
335
+ table first. Altitude is in `raw` only. One call reads the detail of at most
336
+ 10 workouts (long ranges are read a week per chunk). A GPS track shows where
337
+ you were, including where you live: `--no-track` (`track=False`) leaves it
338
+ out. The download links and keys never appear in the output.
339
+ - **Workout files outside the China region are untested.** Files download
340
+ only from hosts under `.fds.api.xiaomi.com`, the host seen on a China-region
341
+ account. A link to another host is refused with a note, and that workout
342
+ keeps its summary without samples or track.
343
+ - **Read-only.** mifit never writes, changes or deletes anything in your
344
+ account.
345
+ - **Sessions expire.** On an HTTP 401, mifit renews the session once from the
346
+ stored passToken and retries the request. When that fails, or the session
347
+ was saved before renewal existed (it has no passToken), run `mifit login`
348
+ again. Renewal was checked live on 2026-10-15. The lifetime without renewal
349
+ is still being observed and is at least 22 hours so far: logins, 401s and
350
+ renewals are logged (event, time and ages only) to
351
+ `~/.mifit/session-log.jsonl`. (An HTTP 401 can also mean the request
352
+ signature was refused; a 401 right after a renewal is taken to mean that and
353
+ is not retried.)
354
+ - **Narrowly tested.** One account in the China region. Other regions are
355
+ routed but unverified, and Xiaomi can change the protocol at any time.
356
+ - **Be gentle with the service.** Requests are sent one at a time with about a
357
+ second's pause plus random jitter. Long ranges are read in chunks, and a
358
+ range over 92 days needs `--long`. This lowers the risk of being
359
+ rate-limited, but does not remove it.
360
+
361
+ ## Development
362
+
363
+ ```bash
364
+ git clone https://github.com/Jenwein/mifit.git
365
+ cd mifit
366
+ pip install -e ".[test]"
367
+ python -m pytest -q
368
+ ```
369
+
370
+ The tests are offline: a fake transport stands in for the Xiaomi cloud, and
371
+ any attempt to open a network connection fails the test. The `test` extra
372
+ includes `tzdata`, so the daylight-saving tests also run on Windows, which has
373
+ no time-zone database of its own. CI runs the suite on Linux with Python 3.12,
374
+ 3.13 and 3.14.
375
+
376
+ ## Disclaimer
377
+
378
+ mifit is an independent, unofficial project. It is not affiliated with,
379
+ endorsed by or supported by Xiaomi. "Xiaomi" and "Mi Fitness" are trademarks
380
+ of their owners and are used here only to say what the software works with.
381
+
382
+ It is meant for reading **your own** data with **your own** account.
383
+ Automated access may conflict with Xiaomi's terms of service and could get
384
+ your account rate-limited or restricted; use it at your own risk. Nothing it
385
+ outputs is medical advice. The software is provided "as is", without warranty
386
+ of any kind (see [LICENSE](https://github.com/Jenwein/mifit/blob/main/LICENSE)).
387
+
388
+ ## Credits
389
+
390
+ Request signing and encryption, the transport, the region table and the STS
391
+ token exchange are adapted from
392
+ [Mi-Fitness-Sync](https://github.com/kevinkwee/Mi-Fitness-Sync) by kevinkwee
393
+ (MIT License). See [THIRD_PARTY_NOTICES](https://github.com/Jenwein/mifit/blob/main/THIRD_PARTY_NOTICES).
394
+
395
+ ## License
396
+
397
+ MIT. See [LICENSE](https://github.com/Jenwein/mifit/blob/main/LICENSE).