- 新增 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 示例(风扇/窗帘/传感器控制)
289 lines
7.7 KiB
Markdown
289 lines
7.7 KiB
Markdown
# 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 ./...` 编译验证
|