Files
xiaomihome/xiaomi/client.go
T
4566704 8ffc9e4e44 refactor: 重构 token 管理,新增 NATS adapter 和多示例
按照 TOKEN_REFACTOR.md / FIX_CONFIG.md 方案重构核心层:
- miot: Init() 优先读 storage token,新增 RefreshAuthInfo() 和刷新回调
- xiaomi: Config 改用 AuthInfo 结构体(含 UUID/UID/DataDir/OnTokenRefreshed)
- xiaomi: NewClient(ctx, Config) 新签名,新增 RefreshToken(),后台自动刷新
- xiaomi/devices: 设备控制接口重构
- 新增示例 12_ac_subscribe,其余示例适配新 Config
2026-06-30 12:16:49 +08:00

157 lines
4.1 KiB
Go
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.
package xiaomi
import (
"context"
"fmt"
"sync"
"time"
"xiaomihome/miot"
)
// Client 高级封装客户端(包装 miot.MIoTClient + miot.MIoTHttpClient)
type Client struct {
cfg Config
inner *miot.MIoTClient
http *miot.MIoTHttpClient
mipsCloud *miot.MipsCloudClient
specParser *miot.MIoTSpecParser
uuid string
deviceMu sync.RWMutex
devices map[string]*miot.MIoTDevice
propSubs map[string]string
eventSubs map[string]string
mu sync.RWMutex
}
// NewClient 创建并初始化客户端。
func NewClient(ctx context.Context, cfg Config) (*Client, error) {
if cfg.AuthInfo.AccessToken == "" {
return nil, fmt.Errorf("xiaomi: Config.AuthInfo.AccessToken is required")
}
if cfg.UUID == "" {
return nil, fmt.Errorf("xiaomi: Config.UUID is required")
}
if cfg.DataDir == "" {
cfg.DataDir = "./data"
}
// 计算 expires_ts(如果未提供,从 expires_in 推算)
expiresTS := float64(cfg.AuthInfo.ExpiresTS)
if expiresTS == 0 && cfg.AuthInfo.ExpiresIn > 0 {
expiresTS = float64(time.Now().Unix() + int64(cfg.AuthInfo.ExpiresIn))
}
uuid := cfg.UUID
entryData := map[string]interface{}{
"access_token": cfg.AuthInfo.AccessToken,
"refresh_token": cfg.AuthInfo.RefreshToken,
"expires_ts": expiresTS,
"uuid": uuid,
}
miotClient := miot.NewMIoTClient("xiaomi_sdk", entryData, cfg.UID, "cn", miot.CtrlModeAuto)
miotClient.SetStorage(miot.NewMIoTStorage(cfg.DataDir))
miotClient.SetSpecParser(miot.NewMIoTSpecParser("en", miot.NewMIoTStorage(cfg.DataDir), cfg.DataDir+"/specs"))
httpClient, err := miot.NewMIoTHttpClient("cn", miot.OAUTH2_CLIENT_ID, cfg.AuthInfo.AccessToken)
if err != nil {
return nil, fmt.Errorf("xiaomi: http: %w", err)
}
miotClient.SetHTTPClient(httpClient)
mipsCloud := miot.NewMipsCloudClient(
"ssl://cn-ha.mqtt.io.mi.com:8883",
"ha."+uuid,
miot.OAUTH2_CLIENT_ID,
cfg.AuthInfo.AccessToken,
)
miotClient.SetMipsCloud(mipsCloud)
if err := miotClient.Init(); err != nil {
return nil, fmt.Errorf("xiaomi: init: %w", err)
}
_ = miotClient.RefreshDevices()
if err := mipsCloud.Connect(); err != nil {
return nil, fmt.Errorf("xiaomi: mips connect: %w", err)
}
// 启动后台 token 自动刷新
if cfg.OnTokenRefreshed != nil {
miotClient.SetTokenRefreshCallback(cfg.OnTokenRefreshed)
}
miotClient.RequestRefreshAuthInfo(10)
return &Client{
cfg: cfg,
inner: miotClient,
http: httpClient,
mipsCloud: mipsCloud,
uuid: uuid,
specParser: miot.NewMIoTSpecParser("en", miot.NewMIoTStorage(cfg.DataDir), cfg.DataDir+"/specs"),
devices: make(map[string]*miot.MIoTDevice),
propSubs: make(map[string]string),
eventSubs: make(map[string]string),
}, nil
}
// Close 关闭客户端。
func (c *Client) Close() error {
if c.mipsCloud != nil {
c.mipsCloud.Disconnect()
}
if c.inner != nil {
return c.inner.Deinit()
}
return nil
}
// RefreshToken 手动刷新 token,返回新值供调用方持久化。
func (c *Client) RefreshToken(ctx context.Context) (*TokenResult, error) {
result, err := c.inner.RefreshAuthInfo()
if err != nil {
return nil, fmt.Errorf("xiaomi: refresh token: %w", err)
}
return &TokenResult{
AccessToken: strVal(result, "access_token"),
RefreshToken: strVal(result, "refresh_token"),
ExpiresIn: intVal(result, "expires_in"),
ExpiresTS: int64(intVal(result, "expires_ts")),
UID: strVal(result, "uid"),
}, nil
}
func (c *Client) getOrCreateDevice(did string, info map[string]interface{}) *miot.MIoTDevice {
c.deviceMu.Lock()
defer c.deviceMu.Unlock()
if dev, ok := c.devices[did]; ok {
return dev
}
urn, _ := info["urn"].(string)
model, _ := info["model"].(string)
var spec *miot.MIoTSpecInstance
if c.specParser != nil && urn != "" {
s, err := c.specParser.Parse(urn, model)
if err == nil {
spec = s
} else {
fmt.Printf("[xiaomi] spec parse failed for %s (%s): %v\n", did, urn, err)
}
}
dev := miot.NewMIoTDevice(c.inner, info, spec)
c.devices[did] = dev
return dev
}
func (c *Client) getDevice(did string) *miot.MIoTDevice {
c.deviceMu.RLock()
defer c.deviceMu.RUnlock()
return c.devices[did]
}