Files
xiaomihome/xiaomi/client.go
T
4566704 2b7de69403 refactor(logger): 日志改用 logrus 全局包级调用,移除 Logger 注入接口
为什么:并入宿主项目后须与宿主日志体系一致——宿主统一用 pkg/logger 初始化
logrus 全局实例并直接 logrus.Xxx 包级调用;包一层 Logger 接口会让 logrus 的
caller 定位(报告调用文件:行号)失准。

改动:
- 删除 logger 包(Logger 接口 / Default / SetDefault,默认本就是 logrus.StandardLogger)
- 11 个结构体移除 lgr logger.Logger 字段、SetLogger 方法及构造中的 lgr 初始化
- 192 处 lgr.Xxxf 调用改为 logrus.Xxxf,日志消息文本保持不变
- xiaomi.Client 移除 Logger() 访问器,air_conditioner 回读日志去掉判空调用直连 logrus
- miot_i18n.go 两处标准库 log.Printf 改为 logrus.Errorf,统一日志出口
- go.mod:logrus 从 indirect 提升为直接依赖
- goimports/goformat 全树规范化:此前部分文件未 gofmt(单行 if、对齐),本次顺带
  格式化,纯空白/换行变化,无逻辑改动

验证:go build / go vet / go test ./... 全部通过(bridge、miot、xiaomi、devices、specs)
2026-09-15 20:16:26 +08:00

184 lines
4.9 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"
"github.com/sirupsen/logrus"
)
// 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
readbackModels map[string]struct{} // 需要 set 后回读的型号集合
}
// 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,
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),
readbackModels: make(map[string]struct{}, len(cfg.ReadbackModels)),
}
for _, m := range cfg.ReadbackModels {
if m != "" {
c.readbackModels[m] = struct{}{}
}
}
return c, nil
}
// NeedReadback 判断指定型号是否需要"set 成功后回读状态"。
func (c *Client) NeedReadback(model string) bool {
if model == "" || len(c.readbackModels) == 0 {
return false
}
_, ok := c.readbackModels[model]
return ok
}
// 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 {
logrus.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]
}