Metadata-Version: 2.4
Name: xiaomi-mimo
Version: 0.2.0
Summary: Unofficial Python client for the Xiaomi MiMo platform with automatic session refresh
Author: Eight Labs
License: MIT License
        
        Copyright (c) 2026 Eight Labs
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: api,client,mimo,xiaomi
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.27
Provides-Extra: test
Requires-Dist: build>=1.2; extra == 'test'
Requires-Dist: mypy>=1.11; extra == 'test'
Requires-Dist: pytest>=8; extra == 'test'
Requires-Dist: ruff>=0.7; extra == 'test'
Description-Content-Type: text/markdown

# xiaomi-mimo

An unofficial, typed Python client for the Xiaomi MiMo platform. It packages the browser's Mi Account SSO exchange, persists the resulting cookies securely, and refreshes the short-lived MiMo session automatically.

> This project is not affiliated with Xiaomi. The MiMo web API is undocumented and can change without notice. Use it only with accounts and data you are authorized to access, and review Xiaomi's applicable terms.

## Authentication model

The MiMo console uses four required cookies:

| Cookie | Purpose observed in the web client |
| --- | --- |
| `api-platform_serviceToken` | MiMo service session |
| `api-platform_slh` | MiMo service session companion value |
| `api-platform_ph` | MiMo session value and POST query parameter |
| `userId` | MiMo user identity |

The console describes these as 24-hour cookies. They are not refresh tokens. The apparently automatic browser refresh works because the browser also has a longer-lived Xiaomi Account session. The minimal reusable seed for that session is normally the `userId` and `passToken` cookies from **`account.xiaomi.com`**.

When a MiMo request is unauthorized, the library:

1. asks `/api/v1/genLoginUrl` for a signed login URL with a safe GET follow-up;
2. exchanges the saved Xiaomi Account `userId` and `passToken` at `/pass/serviceLogin?_json=true`;
3. computes Xiaomi's `clientSign` when required;
4. visits the signed MiMo `/sts` callback, which replaces the four platform cookies;
5. verifies `/api/v1/userProfile`; and
6. retries the original API request once.

No browser or JavaScript engine is needed while the Xiaomi Account session remains valid. Xiaomi can still expire or revoke `passToken`, or require CAPTCHA/MFA. No client can guarantee refresh forever in those cases; this library raises `ReauthenticationRequired` instead of attempting to bypass interactive verification.

## Install

```bash
pip install xiaomi-mimo
```

## One-time setup

1. Sign in normally at `https://account.xiaomi.com` in your browser.
2. Open Developer Tools → Application/Storage → Cookies → `https://account.xiaomi.com`.
3. Copy only `userId` and `passToken`.
4. Provide them once and choose a cookie file.

```python
import os
from xiaomi_mimo import MimoClient

with MimoClient(
    account_cookie={
        "userId": os.environ["XIAOMI_USER_ID"],
        "passToken": os.environ["XIAOMI_PASS_TOKEN"],
    },
    cookie_file="~/.config/xiaomi-mimo/cookies.json",
) as mimo:
    print(mimo.token_plan_usage())
```

The cookie file is created atomically with mode `0600` on POSIX systems. On later runs, the seed does not need to be supplied again:

```python
from xiaomi_mimo import MimoClient

with MimoClient(cookie_file="~/.config/xiaomi-mimo/cookies.json") as mimo:
    profile = mimo.user_profile()
    usage = mimo.token_plan_usage()
```

Treat both the environment variables and the cookie file like passwords. Never commit either one.

A raw Cookie header is also accepted:

```python
mimo = MimoClient(
    account_cookie="userId=...; passToken=...",
    cookie_file="~/.config/xiaomi-mimo/cookies.json",
)
```

### Finding `passToken` when it is not visible

`passToken` is a Xiaomi Account cookie, not a MiMo platform cookie. It will not appear in a copied request to `platform.xiaomimimo.com`.

1. Open `https://account.xiaomi.com` in the same browser profile that opens MiMo without another sign-in.
2. Open Developer Tools on that tab.
3. Select Application/Storage → Cookies.
4. Inspect `https://account.xiaomi.com` and rows whose Domain is `.account.xiaomi.com` or `.xiaomi.com`.
5. Filter for `userId` and `passToken`.

The cookie is normally HttpOnly. It is therefore absent from `document.cookie`, but remains visible in Developer Tools. If the Application panel does not show it:

1. Enable Preserve log in the Network panel.
2. Open an expired MiMo `loginUrl` or revisit a protected MiMo console page.
3. Select the first request to `https://account.xiaomi.com/pass/serviceLogin`.
4. Open Cookies → Request Cookies and find `userId` and `passToken`.
5. Alternatively, use Copy → Copy as cURL and inspect the Cookie header locally.

Never paste either value into an issue, log, source file, or chat. A posted `passToken` should be revoked by signing out the corresponding Xiaomi Account session. The persisted cookie file contains the same sensitive capability and should not be synchronized or committed.

## Start from an existing MiMo session

The four cookies from a copied MiMo `curl` request can be loaded as `platform_cookie`. This permits immediate API use, but those cookies alone cannot renew themselves. Supply the Xiaomi Account seed as well if automatic refresh is required.

```python
with MimoClient(
    account_cookie="userId=...; passToken=...",
    platform_cookie=(
        "userId=...; api-platform_serviceToken=...; "
        "api-platform_slh=...; api-platform_ph=..."
    ),
    cookie_file="~/.config/xiaomi-mimo/cookies.json",
) as mimo:
    print(mimo.token_plan_usage())
```

Analytics cookies, browser client-hint headers, `cookie-preferences`, and a browser user agent are not required.

## Captured endpoint helpers

Every MiMo API endpoint observed in `ref.har` and `ref2.har` has a convenience method:

| Client method | HTTP endpoint |
| --- | --- |
| `user_profile()` | `GET /userProfile` |
| `balance_alert_config()` | `GET /balanceAlertConfig` |
| `ab_test_experiments()` | `GET /abtest/experiments` |
| `invitation_eligible()` | `GET /invitation/eligible` |
| `token_plans()` | `GET /tokenPlan/list` |
| `token_plan_detail()` | `GET /tokenPlan/detail` |
| `token_plan_usage(user_id=...)` | `GET /tokenPlan/usage` |
| `token_plan_api_key()` | `GET /tokenPlan/apiKey` |
| `token_plan_api_key_raw()` | `GET /tokenPlan/apiKey/raw` |
| `token_plan_management_url()` | `GET /tokenPlan/managementUrl` |
| `usage()` | `GET /usage` |
| `token_plan_usage_records(year=..., month=...)` | `POST /usage/token-plan/list` |
| `usage_details(year=..., month=...)` | `POST /usage/detail/list` |
| `plugin_usage(year=..., month=...)` | `POST /usage/plugin/list` |
| `api_keys(with_deleted=...)` | `GET /apiKeys` |
| `create_api_key(name)` | `POST /apiKeys` |
| `delete_api_key(id)` | `DELETE /apiKeys/{id}` |
| `refundable_amount()` | `GET /refund/refundableAmount` |
| `balance()` | `GET /balance` |
| `recharge_or_refund_records()` | `GET /rechargeOrRefund` |
| `accumulated_recharge_amount()` | `GET /accumulatedRechargeAmount` |
| `plugins()` | `GET /plugins` |
| `email_bind_info()` | `GET /email/bindInfo` |

The API-key methods handle secrets. `token_plan_api_key_raw()` and `create_api_key()` can return a complete API key; do not log their results. `delete_api_key()` permanently deletes the selected key.

```python
summary = mimo.usage()
records = mimo.usage_details(year=2026, month=8)
keys = mimo.api_keys(with_deleted=False)
```

## Generic requests

Paths are relative to `/api/v1`:

```python
result = mimo.request_json("GET", "/tokenPlan/usage")
result = mimo.request_json(
    "POST",
    "/usage/detail/list",
    json={"pageNum": 1, "pageSize": 20},
)
```

For POST requests, `api-platform_ph` is copied from the cookie jar into the query string to match the MiMo web client. API envelopes with a `data` field are unwrapped by default:

```python
full_envelope = mimo.request_json("GET", "/userProfile", unwrap=False)
response = mimo.request("GET", "/userProfile")
```

Automatic retry supports replayable JSON, form mappings, and `str` or `bytes` content. Streaming upload bodies are intentionally not accepted by the high-level method because replay after a 401 would be unsafe.

## Explicit refresh and status

```python
state = mimo.auth_state
print(state.authenticated, state.renewable, state.expires_at)

state = mimo.refresh()
```

Refresh is serialized across threads. If another thread already replaced the session, waiting requests reuse it instead of running a second SSO exchange.

## Errors

```python
from xiaomi_mimo import APIError, ReauthenticationRequired

try:
    print(mimo.token_plan_usage())
except ReauthenticationRequired as exc:
    print("Sign in to Xiaomi Account again and replace userId/passToken")
    print(exc.login_url)
except APIError as exc:
    print(exc.code, str(exc))
```

`ReauthenticationRequired.login_url` can be opened for normal interactive sign-in. After signing in, copy the new Xiaomi Account `userId` and `passToken` into a new client invocation so the saved store is updated.

## Development

```bash
uv sync --extra test
uv run ruff check .
uv run mypy src
uv run pytest
uv build
```
