Files
xiaomihome/xiaomi/migration/CLIENT_REFACTOR.md
T
4566704 a3d94c4b9f chore: 提交高层 xiaomi 封装库及示例
- 新增 xiaomi/examples/ 下 8 个示例程序(用户/家庭查询、设备列表、开关/灯光/空调控制、属性订阅、高级过滤分类、SPEC 解析)
- miot_client_sub.go: 重构 SubProp/SubEvent,使用 buildPropTopic/buildEventTopic 支持通配符订阅(siid/piid=0 → +),并修复锁顺序问题(将 RequestRefreshProp 移到 Lock 外)
- spec_parser.go: 新增 downloadSpecFile 方法,本地 SPEC 文件缺失时自动从 miot-spec.org 下载
- ARCH_PLAN.md: 架构设计从 Proposed 更新为 Accepted(v1.0→v1.1),补充设备分类/工厂/SPEC 映射等模块设计
- xiaomi/: 新增 miot 上层强类型封装模块,包含 Client 主入口、用户/家庭/设备 API、属性读写、动作调用、订阅通知,以及 devices/ 设备控制抽象(Switch/Light/AirConditioner/Fan/Cover/Humidifier/Vacuum/WaterHeater/Thermostat)和 specs/ SPEC 查询辅助
- 更多 xiaomi 示例(风扇/窗帘/传感器控制)
2026-06-29 08:51:21 +08:00

289 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# xiaomi 客户端重构方案
> 状态:Proposed | 版本:v3.0 | 日期:2026-06-29 | 参照:miot/examples/full_client
---
## 一、Config 字段说明
| 字段 | 含义 | 来源 |
|------|------|------|
| `ClientID` | OAuth Client ID,默认 `"2882303761520251711"`(小米官方 HA 客户端) | 小米开放平台,可留空走默认值 |
| `ClientSecret` | OAuth Client Secret,用于 token 交换 | 小米开放平台申请 |
| `AccessToken` | OAuth 授权后获得的 token,云端 API 鉴权 | OAuth 流程获取(AuthURL → ExchangeCode) |
---
## 二、目标形态
```go
// 零 miot import,1 个对象
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
ClientID: "", // 空则默认 "2882303761520251711"
ClientSecret: "your-secret",
AccessToken: "your-access-token",
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
user, _ := client.GetUserInfo(ctx)
```
---
## 三、参照源码
`miot/examples/full_client/main.go`(已验证可运行)的初始化流程:
```go
// 1. entryData 只放 access_token
entryData := cfg.EntryData() // → {"access_token": "xxx"}
// 2. 创建 MIoTClient
client := miot.NewMIoTClient("demo", entryData, cfg.UID, cfg.CloudServer, cfg.GetCtrlMode())
// 3. 设置组件(必须在 Init 之前)
client.SetStorage(miot.NewMIoTStorage("./data"))
client.SetHTTPClient(miot.NewMIoTHttpClient(cfg.CloudServer, miot.OAUTH2_CLIENT_ID, cfg.AuthInfo.AccessToken))
client.SetMipsCloud(miot.NewMipsCloudClient("ssl://cn-ha.mqtt.io.mi.com:8883", "ha."+cfg.UUID, ...))
// 4. Init(一次性完成:OAuth 客户端、证书、HTTP、MIPS 云、mDNS、LAN)
client.Init()
// 5. 业务操作
client.RefreshDevices()
client.DeviceList()
client.SubDeviceState(...)
client.SubProp(...)
// 6. 清理
client.Deinit()
```
**关键要点**:
- entryData 只需 `access_token` 一个字段
- `SetStorage` / `SetHTTPClient` / `SetMipsCloud` 必须在 Init 之前调用
- Init 内部会跳过已设置的组件(`if c.http == nil { ... }`)
- 不需要两次 Init,一次足够
---
## 四、逐文件修改
### 4.1 新建 `xiaomi/config.go`
```go
package xiaomi
// Config 客户端初始化配置
type Config struct {
// ClientID OAuth Client ID,空则默认 "2882303761520251711"(小米 HA 客户端)
ClientID string
// ClientSecret OAuth Client Secret
ClientSecret string
// AccessToken OAuth 授权后获取的 access token
AccessToken string
}
```
### 4.2 重写 `xiaomi/client.go`
**删除**:
```go
func NewClient(inner *miot.MIoTClient, httpClient *miot.MIoTHttpClient) *Client
func (c *Client) Inner() *miot.MIoTClient
func (c *Client) HTTPClient() *miot.MIoTHttpClient
```
**新增**(完全对齐 full_client 示例的初始化流程):
```go
import "context"
// NewClient 创建并初始化客户端。
//
// 内部流程(对齐 miot/examples/full_client):
// 1. 构建 entryData(仅 access_token)
// 2. 创建 MIoTClient
// 3. SetStorage → SetHTTPClient → SetMipsCloud(必须在 Init 前)
// 4. Init(一次性完成所有子组件初始化)
func NewClient(ctx context.Context, cfg Config) (*Client, error) {
clientID := cfg.ClientID
if clientID == "" {
clientID = miot.OAUTH2_CLIENT_ID // 默认小米 HA 客户端
}
// 构造 entryData(仅 access_token,对齐 full_client 的 EntryData())
entryData := map[string]interface{}{
"access_token": cfg.AccessToken,
}
// 创建 MIoTClient(uid 空、CloudServer=cn、CtrlMode=auto)
miotClient := miot.NewMIoTClient("xiaomi_sdk", entryData, "", "cn", miot.CtrlModeAuto)
// 设置存储(对齐 full_client line 36-37)
miotClient.SetStorage(miot.NewMIoTStorage("./data"))
// 设置 HTTP 客户端(对齐 full_client line 41-46)
httpClient, err := miot.NewMIoTHttpClient("cn", clientID, cfg.AccessToken)
if err != nil {
return nil, fmt.Errorf("xiaomi: http: %w", err)
}
miotClient.SetHTTPClient(httpClient)
// 设置 MIPS 云端客户端(对齐 full_client line 50-57)
mipsCloud := miot.NewMipsCloudClient(
"ssl://cn-ha.mqtt.io.mi.com:8883",
"ha."+uuid.New().String(),
clientID,
cfg.AccessToken,
)
miotClient.SetMipsCloud(mipsCloud)
// 一次性 Init(对齐 full_client line 60)
if err := miotClient.Init(); err != nil {
return nil, fmt.Errorf("xiaomi: init: %w", err)
}
return &Client{
cfg: cfg,
inner: miotClient,
http: httpClient,
devices: make(map[string]*miot.MIoTDevice),
propSubs: make(map[string]string),
eventSubs: make(map[string]string),
}, nil
}
// Close 关闭客户端(对齐 full_client line 125: client.Deinit())
func (c *Client) Close() error {
if c.inner != nil {
return c.inner.Deinit()
}
return nil
}
```
**Client struct 新增 cfg 字段**:
```go
type Client struct {
cfg Config // ← 新增
inner *miot.MIoTClient
http *miot.MIoTHttpClient
deviceMu sync.RWMutex
devices map[string]*miot.MIoTDevice
propSubs map[string]string
eventSubs map[string]string
mu sync.RWMutex
}
```
### 4.3 新建 `xiaomi/oauth.go`
```go
package xiaomi
import "context"
// AuthURL 生成小米 OAuth 授权页面 URL
func AuthURL(cfg Config) string
// ExchangeCode 用 authorization code 交换 access token
func ExchangeCode(ctx context.Context, cfg Config, code string) (*TokenResult, error)
// TokenResult OAuth token 交换结果
type TokenResult struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
UID string `json:"uid"`
}
```
### 4.4 重写 `xiaomi/examples/main.go`
```go
package main
import (
"context"
"log"
"time"
"xiaomihome/xiaomi"
"xiaomihome/xiaomi/examples/config"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cfg, _ := config.LoadFromEnv()
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
ClientSecret: cfg.ClientSecret,
AccessToken: cfg.AccessToken,
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
user, _ := client.GetUserInfo(ctx)
log.Printf("用户: %s (UID: %s)", user.NickName, user.UID)
}
```
---
## 五、不动的文件(15 个)
`properties.go` `actions.go` `subscribe.go` `user.go` `homes.go` `devices.go` `types.go` `errors.go` `convert.go` + `devices/` 下全部 9 个文件
---
## 六、附带清理
### 6.1 `xiaomi/examples/config/config.go` — 去掉无用的 CountryCode / CloudServer
```go
// 修改前
type Config struct {
CountryCode string // ← 无用
CloudServer string // ← 硬编码 "cn",无需暴露
ClientID string
ClientSecret string
AccessToken string
}
func (c *Config) LoadFromEnv() {
cfg.CountryCode = getEnv("XIAOMI_COUNTRY_CODE", "86") // ← 删
cfg.CloudServer = getEnv("XIAOMI_CLOUD_SERVER", "cn") // ← 删
}
// 修改后
type Config struct {
ClientID string
ClientSecret string
AccessToken string
}
```
### 6.2 `miot/miot_client.go` — `NewMIoTClient` 中去掉无用的 `country_code`
已验证 Go 代码和 Python 代码中均无任何地方读取 entryData 的 `country_code`,一并移除。
---
## 七、实施步骤
1. 新建 `xiaomi/config.go`
2. 重写 `xiaomi/client.go`(旧 NewClient 改名内部保留)
3. 新建 `xiaomi/oauth.go`
4. 清理 `xiaomi/examples/config/config.go`(去 CountryCode / CloudServer)
5. 重写 `xiaomi/examples/main.go`
6. 清理 `miot/miot_client.go` 中无用的 `country_code`
7. 更新 `xiaomi/migration/ARCH_PLAN.md` 4.2 节
8. `go vet ./...` 编译验证