Files
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

7.7 KiB
Raw Permalink Blame History

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)

二、目标形态

// 零 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(已验证可运行)的初始化流程:

// 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

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

删除:

func NewClient(inner *miot.MIoTClient, httpClient *miot.MIoTHttpClient) *Client
func (c *Client) Inner() *miot.MIoTClient
func (c *Client) HTTPClient() *miot.MIoTHttpClient

新增(完全对齐 full_client 示例的初始化流程):

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 字段:

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

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

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

// 修改前
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 ./... 编译验证