**回读目标聚合(recordReadback)** - 所有 set 方法(TurnOn/TurnOff/SetMode/SetTargetTemp/SetFanSpeed/ SetSwing/SetTargetHumidity/SetAuxHeat/SetEco)成功后记录目标值 rbTarget - 同属性目标覆盖、不同属性追加,连续 set 自动聚合 **防抖 + 重试验证(readbackLoop)** - 静默窗口 readbackIdle=3s:等待最后一次 set 无新操作才开始验证 - 未命中目标时按 readbackRetryInterval=3s 重试,上限 readbackMaxRetries=5 - 整体超时 readbackTimeout=30s,防云端卡住导致回读永久挂起 - 命中后逐字段回调 OnPropsChanged 注册的 handler(与 MQTT 推送同语义) - rbValueEqual 数值比较用 epsilon=0.01,bool/string 直接相等 **白名单配置透传** - xiaomi.Config 新增 ReadbackModels 白名单(留空 = 全部不回读,默认) - Client 新增 NeedReadback(model) 判定 + Logger() 访问器 - bridge.Pool.NewPool(readbackModels...) 透传;newBridge 传入 Client 配置 **其他** - bridge 设备创建失败(Warn)与不支持跳过(Debug)分别记录日志
191 lines
5.1 KiB
Go
191 lines
5.1 KiB
Go
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
|
||
|
||
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,
|
||
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),
|
||
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
|
||
}
|
||
|
||
// SetLogger 设置日志记录器。
|
||
func (c *Client) SetLogger(l logger.Logger) { c.lgr = l }
|
||
|
||
// Logger 返回日志记录器。
|
||
func (c *Client) Logger() logger.Logger { return c.lgr }
|
||
|
||
// 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]
|
||
}
|