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] }