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 示例(风扇/窗帘/传感器控制)
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
# 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 ./...` 编译验证
|
||||
Reference in New Issue
Block a user