Files
xiaomihome/xiaomi/client.go
T
4566704 b808e51b3e refactor: 重大架构升级 — HomeID/RoomID int64化 + 设备驱动重构 + 结构化日志
主要变更:

**类型系统重构**
- HomeInfo.ID/RoomInfo.ID/HomeID 从 string 改为 int64,json tag 使用 ,string
- GetHome/GetRooms/GetRoomDevices 接口同步修改
- 新增 int64FromStr 工具函数,更新 parseHomeInfo 解析逻辑

**新增 Speaker 设备**
- 新增 KindSpeaker 类型
- classify 映射覆盖 speaker/bluetooth-speaker/alarm-clock/media-player/playback/music

**设备驱动重构**
- AC 驱动彻底重写:从硬编码常量改为基于 SPEC ValueList 的动态 valMapper
- 所有设备(Fan/Humidifier/Light/OccupancySensor/Switch/TempHumiditySensor/Thermostat/Vacuum/WaterHeater/Cover)新增 GetState() 和 FetchState(ctx) 接口
- base.go 新增 specResolver.format() 方法

**MIoT 模块增强**
- MIoTClient 新增 homeIDs 字段,支持按家庭过滤设备
- RefreshDevices 支持 homeIDs 参数
- SpecStdLib 引入 logger.Logger,log.Printf 全部迁移为结构化日志

**其他**
- Config 新增 RedirectURL 和 HomeIDs 字段
- AuthInfo.ExpiresIn 类型从 int 改为 int32
- UUID 生成简化为 hex.EncodeToString
- 新增 github.com/sirupsen/logrus 依赖
- .gitignore 新增 examples/*/data/ 忽略规则
- 删除已提交的 config.json 敏感配置
2026-07-13 10:40:20 +08:00

171 lines
4.5 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/logger"
"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
lgr logger.Logger
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)
var ids []string
if len(cfg.HomeIDs) > 0 {
ids = make([]string, len(cfg.HomeIDs))
for i, v := range cfg.HomeIDs {
ids[i] = fmt.Sprintf("%d", v)
}
miotClient.SetHomeIDs(ids)
}
if err := miotClient.Init(); err != nil {
return nil, fmt.Errorf("xiaomi: init: %w", err)
}
// Use the same homeIDs slice from above (nil if not set)
_ = miotClient.RefreshDevices(ids)
if err := mipsCloud.Connect(); err != nil {
return nil, fmt.Errorf("xiaomi: mips connect: %w", err)
}
c := &Client{
cfg: cfg,
inner: miotClient,
http: httpClient,
mipsCloud: mipsCloud,
uuid: uuid,
lgr: logger.Default(),
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),
}
return c, nil
}
// SetLogger 设置日志记录器。
func (c *Client) SetLogger(l logger.Logger) { c.lgr = l }
// SetHomeIDs 动态设置 miot 层的家庭过滤,供后台定时 RefreshDevices 使用。
func (c *Client) SetHomeIDs(homeIDs []string) { c.inner.SetHomeIDs(homeIDs) }
// 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: int32(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 {
c.lgr.Warnf("[xiaomi] spec parse failed for %s (%s): %v", 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]
}