feat: add Go API library
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# mijia-api Go Refactor Implementation Plan
|
||||
|
||||
> [!NOTE]
|
||||
> This document may not reflect the current implementation.
|
||||
> See the final report for up-to-date state:
|
||||
> [Final Report](../reports/mijia-go-api.md)
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use compose:subagent (recommended) or compose:execute to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build a pure Go library equivalent to the Python mijia-api core and high-level device API, excluding CLI, skills, MCP, and packet-decryption tools.
|
||||
|
||||
**Architecture:** A single `mijia` package separates deterministic cryptography, authenticated HTTP transport, public API methods, and MIoT device metadata. Public methods accept `context.Context`; flexible Xiaomi payloads use typed request structures plus `json.RawMessage` results where schemas vary.
|
||||
|
||||
**Tech Stack:** Go 1.22+, standard library, `github.com/mdp/qrterminal/v3` only for terminal QR rendering.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Module path is `git.misaka.ren/m1saka/mijia-go-api` and package name is `mijia`.
|
||||
- Preserve Python v4.1.2 endpoints, compact JSON encoding, ordered signing parameters, and RC4-drop-1024 behavior.
|
||||
- Include login/token refresh, homes, devices, shared devices, scenes, consumables, properties, actions, statistics, and high-level device access.
|
||||
- Exclude CLI, MCP, skills, documentation site, and HAR/decrypt utilities.
|
||||
- Keep network dependencies injectable through `*http.Client`; tests must not require Xiaomi services.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Module, errors, and encryption
|
||||
|
||||
**Covers:** [S2, S3, S4, S7, S8]
|
||||
|
||||
**Files:**
|
||||
- Create: `go.mod`
|
||||
- Create: `errors.go`
|
||||
- Create: `crypto.go`
|
||||
- Test: `crypto_test.go`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `APIError`, device error types, `generateNonce`, `signedNonce`, `encryptRC4`, `decryptPayload`, `generateEncryptedParams`.
|
||||
|
||||
- [ ] Write table-driven tests using the fixed vector `ssecurity=MDEyMzQ1Njc4OWFiY2RlZg==`, `nonce=AAECAwQFBgcICQoL`, signed nonce `16/CeTzC9IqVVbiZ01Hy/Qd8rtVo5ybLo+ph/Vvh52k=`, and RC4 ciphertext `9ve6riTrkW1oJUE=`.
|
||||
- [ ] Run `go test ./...`; expect failure because encryption functions are undefined.
|
||||
- [ ] Implement SHA-256 signed nonce, RC4-drop-1024, SHA-1 ordered signatures, encrypted form parameters, plain/gzip response decryption, and the Xiaomi error-code map.
|
||||
- [ ] Run `gofmt -w errors.go crypto.go crypto_test.go && go test ./...`; expect PASS.
|
||||
|
||||
### Task 2: Client transport and QR authentication
|
||||
|
||||
**Covers:** [S3, S4, S7]
|
||||
|
||||
**Files:**
|
||||
- Create: `client.go`
|
||||
- Create: `auth.go`
|
||||
- Test: `client_test.go`
|
||||
- Test: `auth_test.go`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `generateEncryptedParams`, `decryptPayload`, `APIError`.
|
||||
- Produces: `Client`, `AuthData`, `NewClient(authPath string, options ...Option)`, `WithHTTPClient`, `Login(context.Context)`, `Available(context.Context)`, and internal `request`.
|
||||
|
||||
- [ ] Add `httptest.Server` tests for encrypted POST form fields, decrypted JSON responses, API errors, `&&&START&&&` parsing, auth persistence, and cookie/header construction.
|
||||
- [ ] Run `go test ./...`; expect failures for missing client/auth symbols.
|
||||
- [ ] Implement auth-file loading, generated UA/device ID/pass_o, API session headers, compact request JSON, response decoding, and 60-second availability caching.
|
||||
- [ ] Implement service-login discovery, passToken refresh, QR login data retrieval, terminal QR rendering, 120-second long polling, callback cookies, and atomic auth-file persistence.
|
||||
- [ ] Run `gofmt -w client.go auth.go client_test.go auth_test.go && go test ./...`; expect PASS.
|
||||
|
||||
### Task 3: Public Xiaomi API methods
|
||||
|
||||
**Covers:** [S3, S5, S7]
|
||||
|
||||
**Files:**
|
||||
- Create: `types.go`
|
||||
- Create: `api.go`
|
||||
- Test: `api_test.go`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Client.request`.
|
||||
- Produces: `GetHomes`, `GetDevices`, `GetSharedDevices`, `GetScenes`, `RunScene`, `GetConsumables`, `GetProperties`, `SetProperties`, `RunActions`, `GetStatistics`, and `CheckNewMessages` with typed parameters/results.
|
||||
|
||||
- [ ] Add HTTP fixture tests asserting every URI and exact decoded request body, including device pagination and one-request-per-action/statistic behavior.
|
||||
- [ ] Run `go test ./...`; expect failures for missing API methods.
|
||||
- [ ] Define stable public structs for homes/devices/property/action/statistic calls while preserving unknown response fields in `json.RawMessage` where needed.
|
||||
- [ ] Implement all endpoint methods, owner lookup, all-home aggregation, home ID annotation, pagination, and Xiaomi result-code messages.
|
||||
- [ ] Run `gofmt -w types.go api.go api_test.go && go test ./...`; expect PASS.
|
||||
|
||||
### Task 4: High-level MIoT device API
|
||||
|
||||
**Covers:** [S3, S6, S7]
|
||||
|
||||
**Files:**
|
||||
- Create: `device.go`
|
||||
- Test: `device_test.go`
|
||||
- Create: `testdata/miot-spec.html`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Client.GetDevices`, `Client.GetProperties`, `Client.SetProperties`, `Client.RunActions`.
|
||||
- Produces: `Device`, `DeviceInfo`, `PropertySpec`, `ActionSpec`, `NewDevice`, `GetDeviceInfo`, `Device.Get`, `Device.Set`, and `Device.RunAction`.
|
||||
|
||||
- [ ] Add local HTML fixture tests for MIoT embedded JSON parsing, duplicate-name qualification, underscore aliases, cache read/write, device selection, value conversion, range/step checks, and API error propagation.
|
||||
- [ ] Run `go test ./...`; expect failures for missing device symbols.
|
||||
- [ ] Implement spec download/parsing/cache and high-level device construction by DID or unique name.
|
||||
- [ ] Implement readable/writable checks, bool/number/string conversion, range/value-list validation, property calls, action calls, and configurable post-command delay.
|
||||
- [ ] Run `gofmt -w device.go device_test.go && go test ./...`; expect PASS.
|
||||
|
||||
### Task 5: Documentation and final verification
|
||||
|
||||
**Covers:** [S1, S2, S7, S8]
|
||||
|
||||
**Files:**
|
||||
- Create: `README.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: all exported package APIs.
|
||||
- Produces: install, login, low-level property/action, and high-level device usage examples.
|
||||
|
||||
- [ ] Write concise Go examples and explicitly document unsupported CLI/MCP/skills features and auth-file security.
|
||||
- [ ] Run `gofmt -w *.go && go vet ./... && go test -race ./...`; expect all commands to pass.
|
||||
- [ ] Review exported API names with `go doc ./...` and remove any unused or speculative surface.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Initialize Git Repository Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use compose:subagent (recommended) or compose:execute to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Initialize the Go library as a Git repository and publish its first commit to git.misaka.ren.
|
||||
|
||||
**Architecture:** Track the root Go module as the repository. Exclude local MiMoCode state, authentication data, generated MIoT cache data, and the nested Python reference repository.
|
||||
|
||||
**Tech Stack:** Git, Go 1.22+
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Use `https://git.misaka.ren/m1saka/mijia-go-api.git` as `origin`.
|
||||
- Keep `mijia-api/` untracked.
|
||||
- Use `feat: add Go API library` as the initial commit message.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Initialize And Publish Repository
|
||||
|
||||
**Files:**
|
||||
- Create: `.gitignore`
|
||||
|
||||
- [ ] **Step 1: Verify the Go library**
|
||||
|
||||
Run: `go test ./...`
|
||||
Expected: all packages pass.
|
||||
|
||||
- [ ] **Step 2: Initialize the repository**
|
||||
|
||||
Run: `git init -b main`
|
||||
Expected: an empty Git repository on branch `main`.
|
||||
|
||||
- [ ] **Step 3: Configure the remote**
|
||||
|
||||
Run: `git remote add origin https://git.misaka.ren/m1saka/mijia-go-api.git`
|
||||
Expected: `origin` points to the target repository.
|
||||
|
||||
- [ ] **Step 4: Create the initial commit**
|
||||
|
||||
Run: `git add . && git commit -m "feat: add Go API library"`
|
||||
Expected: one root commit containing the Go library and documentation, excluding ignored local data.
|
||||
|
||||
- [ ] **Step 5: Publish and verify**
|
||||
|
||||
Run: `git push -u origin main`
|
||||
Expected: `main` tracks `origin/main` and the worktree is clean.
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
feature: mijia-go-api
|
||||
status: delivered
|
||||
specs:
|
||||
- docs/compose/specs/2026-07-16-mijia-go-api-design.md
|
||||
plans:
|
||||
- docs/compose/plans/2026-07-16-mijia-go-api.md
|
||||
branch: none
|
||||
commits: none
|
||||
---
|
||||
|
||||
# mijia-api Go 重构 — Final Report
|
||||
|
||||
## What Was Built
|
||||
|
||||
项目提供纯 Go `mijia` 库,等价实现 Python mijia-api v4.1.2 的核心能力:小米账号二维码登录、passToken 静默刷新、RC4 加密 API 请求、家庭和设备查询、场景、耗材、属性读写、动作执行、统计数据,以及基于 MIoT spec 的高级设备封装。项目不包含 CLI、MCP、skills 或抓包解密工具。
|
||||
|
||||
客户端支持认证文件安全持久化、请求前在线 token 有效性缓存、每实例 CookieJar 隔离、并发认证快照和有界 gzip 响应读取。动态 API 字段通过 `Extra` 保留,属性大整数通过 `json.Number` 保持精度。
|
||||
|
||||
## Architecture
|
||||
|
||||
- `crypto.go` 实现 signed nonce、RC4-drop-1024、有序双重签名和响应解密。
|
||||
- `auth.go` 与 `client.go` 实现认证文件、二维码流程、token 刷新、Cookie 和加密 HTTP 传输;`response.go` 统一限制原始与解压响应大小。
|
||||
- `api.go` 与 `types.go` 提供家庭、设备、共享设备、场景、耗材、property、action 和 statistics API。
|
||||
- `device.go` 获取、解析并缓存 MIoT spec,提供 `NewDevice`、`Get`、`Set` 和 `RunAction`。
|
||||
|
||||
### Design Decisions
|
||||
|
||||
- 保持 Python v4.1.2 的实际网络契约,因为目标是等价重构;包括共享设备的 `owner=true` 筛选、action 的 `value` 字段和耗材首分组行为。
|
||||
- 单次请求使用同一认证快照,因为签名、Cookie 和响应解密不能混用不同代 token。
|
||||
- 设备 spec 缓存同时兼容 Go 顶层标识和 Python `method` 格式,因为两个实现默认共享认证目录和缓存文件名。
|
||||
- 整数规格校验使用精确十进制/有理数比较,因为 `float64` 无法安全表达 `2^53` 以上整数。
|
||||
|
||||
## Usage
|
||||
|
||||
安装:
|
||||
|
||||
```bash
|
||||
go get git.misaka.ren/m1saka/mijia-go-api
|
||||
```
|
||||
|
||||
创建客户端并登录:
|
||||
|
||||
```go
|
||||
client, err := mijia.NewClient("")
|
||||
if err != nil { /* handle */ }
|
||||
auth, err := client.Login(ctx)
|
||||
```
|
||||
|
||||
底层控制使用 `GetDevices`、`GetProperties`、`SetProperties` 和 `RunActions`。高级控制使用 `NewDevice` 按 DID 或唯一名称选择设备,再调用 `device.Get`、`device.Set` 和 `device.RunAction`。完整示例和 option 名称见根目录 `README.md`。
|
||||
|
||||
认证文件默认位于 `~/.config/mijia-api/auth.json`,包含敏感 token,库以 `0600` 权限原子写入,不应提交到版本控制。
|
||||
|
||||
## Verification
|
||||
|
||||
- `gofmt -w *.go`
|
||||
- `go test -count=1 ./...`:通过
|
||||
- `go vet ./...`:通过
|
||||
- 独立规格审查和逐阶段代码质量审查均通过。
|
||||
- 测试覆盖固定加密向量、gzip/大小限制、QR 与静默刷新、本地加密 HTTP 端点、分页异常、精确大整数、MIoT HTML 变体、Python 缓存迁移和 context 取消。
|
||||
|
||||
真实小米服务仍存在账号区域、Cookie 策略、限流和设备型号页面变化等集成风险;本地测试不使用真实账号或外网。
|
||||
|
||||
## Journey Log
|
||||
|
||||
> Brief notes on what informed the final design. Not required reading.
|
||||
|
||||
- [lesson] 手动设置 `Accept-Encoding: gzip` 会关闭 Go Transport 自动解压,因此登录和 API 响应必须显式、有界解压。
|
||||
- [pivot] 认证状态从公开可变字段改为深拷贝快照,避免并发请求混用 token 和外部 map 竞态。
|
||||
- [lesson] IANA 时区存在负 DST(例如 Dublin),必须使用 `time.Time.IsDST()`,不能从 UTC offset 大小推断。
|
||||
- [pivot] MIoT 数值校验改为精确有理数,避免大整数通过 `float64` 静默失真。
|
||||
- [lesson] Python 与 Go 共用 spec 缓存路径时,格式迁移和语义校验属于实际兼容需求。
|
||||
|
||||
## Source Materials
|
||||
|
||||
| File | Role | Notes |
|
||||
|------|------|-------|
|
||||
| `docs/compose/specs/2026-07-16-mijia-go-api-design.md` | Initial design | 功能范围与协议约束 |
|
||||
| `docs/compose/plans/2026-07-16-mijia-go-api.md` | Implementation plan | 五阶段实施与验证计划 |
|
||||
@@ -0,0 +1,68 @@
|
||||
# mijia-api Go 重构设计(2026-07-16)
|
||||
|
||||
> [!NOTE]
|
||||
> This document may not reflect the current implementation.
|
||||
> See the final report for up-to-date state:
|
||||
> [Final Report](../reports/mijia-go-api.md)
|
||||
|
||||
## [S1] 问题
|
||||
|
||||
将 Python 版 mijia-api(v4.1.2)重构为纯 Go 库,只保留 API 核心功能,排除 skills、MCP server、CLI、decrypt 调试脚本、文档站。
|
||||
|
||||
## [S2] 方案概览
|
||||
|
||||
- 项目形态:纯 Go 库,module 路径 `git.misaka.ren/m1saka/mijia-go-api`,包名 `mijia`,代码放仓库根目录。
|
||||
- 功能范围:完整底层 API + 高级设备封装(mijiaDevice 等价物)。
|
||||
- Go 版本:1.22+,尽量只用标准库;二维码终端输出用 `github.com/mdp/qrterminal/v3`。
|
||||
|
||||
## [S3] 包结构
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
/ (package mijia)
|
||||
crypto.go ← miutils.py: GenNonce / SignedNonce / RC4-drop1024 加解密 / 签名 / GenerateEncParams(有序 KV)
|
||||
auth.go ← 登录: QRLogin、passToken 静默刷新、auth.json 读写、UA/deviceId 生成
|
||||
client.go ← Client 结构、session headers/cookies、统一加密请求 request()
|
||||
api.go ← GetHomesList / GetDevicesList(分页) / GetSharedDevicesList / GetScenesList / RunScene /
|
||||
GetConsumableItems / GetDevicesProp / SetDevicesProp / RunAction / GetStatistics / CheckNewMsg
|
||||
device.go ← Device 高级封装: miot-spec 抓取解析+缓存、Get/Set(类型与 range 校验)/RunAction
|
||||
errors.go ← ERROR_CODE 表 + 错误类型(LoginError/APIError/DeviceGetError/DeviceSetError 等)
|
||||
types.go ← 请求/响应结构体
|
||||
```
|
||||
|
||||
## [S4] 认证与加密关键点
|
||||
|
||||
1. 二维码扫码登录:`account.xiaomi.com/pass/serviceLogin` → `longPolling/loginUrl` → 终端二维码 → 长轮询(120s)→ 回调取 serviceToken;响应需去 `&&&START&&&` 前缀。
|
||||
2. passToken 有效时静默刷新 token;auth.json 保存 ua/deviceId/pass_o/ssecurity/serviceToken/passToken/userId/cUserId/expireTime(30 天)等。
|
||||
3. 加密请求(base `https://api.mijia.tech/app`,全部 POST form):
|
||||
- nonce = base64(8 字节随机 + 分钟时间戳字节);signedNonce = base64(SHA256(ssecurity||nonce))。
|
||||
- RC4 必须先丢弃 1024 字节 keystream(标准库 crypto/rc4 手动 drop)。
|
||||
- 双重签名:先对明文 params 签 `rc4_hash__`,RC4 加密后再签 `signature`;签名串顺序固定 `POST&uri&data=..&rc4_hash__=..&signedNonce`,Go 用有序 KV 切片而非 map。
|
||||
- data JSON 必须紧凑无空格。
|
||||
- 响应可能是 gzip 压缩后的 RC4 密文:先直接 JSON 解析,失败则解密,解密后 UTF-8 无效再 gzip 解压。
|
||||
4. `Available()`:检查 auth 字段完备 + 调 check_new_msg 验证,结果缓存 60 秒。
|
||||
|
||||
## [S5] API 方法映射
|
||||
|
||||
与 Python 版一一对应(URI、请求体 JSON 完全一致),注意:
|
||||
- GetDevicesList 分页(start_did/max_did,直到 has_more=false),home_id 为空时遍历所有家庭。
|
||||
- RunAction 逐条请求,prop get/set 批量。
|
||||
- 多数接口需 home 的 owner uid(从 GetHomesList 查)。
|
||||
- SetDevicesProp code==1 表示网关已接收但结果未知;非 0/1 查 ERROR_CODE 附中文消息。
|
||||
|
||||
## [S6] 高级设备封装
|
||||
|
||||
`NewDevice(api, did 或 name)`:
|
||||
- 从设备列表解析 model → GET `https://home.miot-spec.com/spec/{model}` 正则提取内嵌 JSON → 解析 services/properties/actions(siid/piid/aiid、类型、rw、range、value-list)→ 本地 JSON 缓存(auth.json 同目录)。
|
||||
- `Get(name)` / `Set(name, value)`(bool/int/float 类型转换、range 与枚举校验)/ `RunAction(name, args...)`;属性名 `-` 与 `_` 互为别名。不做 Python 的动态属性语法糖。
|
||||
|
||||
## [S7] 错误处理与测试
|
||||
|
||||
- 错误:哨兵/自定义错误类型 + ERROR_CODE map(码→中文)。
|
||||
- 测试:crypto.go 的纯函数(nonce 格式、signedNonce、RC4-drop1024、签名串)用与 Python 实现对照生成的固定向量做单测;spec HTML 解析用本地样本。网络调用不做集成测试。
|
||||
|
||||
## [S8] 排除项
|
||||
|
||||
mcp_server.py、skills/、docs/、decrypt/、`__main__.py` CLI、qrcode 图片生成(仅终端二维码)。
|
||||
Reference in New Issue
Block a user