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
This commit is contained in:
+1
-1
@@ -256,7 +256,7 @@ v, _ := devices.NewVacuum(client, info)
|
||||
v.Start() / Stop()
|
||||
v.ReturnToDock()
|
||||
v.SetFanLevel(3) // 吸力档位
|
||||
status, _ := v.GetStatus() // → VacuumStatus{Power, Mode, FanLevel, Battery}
|
||||
status, _ := v.GetStatus() // → VacuumStatus{On, Mode, FanLevel, Battery}
|
||||
```
|
||||
|
||||
#### 热水器 (`WaterHeater`)
|
||||
|
||||
+53
-16
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"xiaomihome/miot"
|
||||
)
|
||||
@@ -15,6 +16,7 @@ type Client struct {
|
||||
http *miot.MIoTHttpClient
|
||||
mipsCloud *miot.MipsCloudClient
|
||||
specParser *miot.MIoTSpecParser
|
||||
uuid string
|
||||
deviceMu sync.RWMutex
|
||||
devices map[string]*miot.MIoTDevice
|
||||
|
||||
@@ -23,32 +25,48 @@ type Client struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewClient 创建并初始化客户端(零 miot import,一个 Config 搞定)。
|
||||
// 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.AccessToken,
|
||||
"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("./data"))
|
||||
miotClient.SetSpecParser(miot.NewMIoTSpecParser("en", miot.NewMIoTStorage("./data"), "./data/specs"))
|
||||
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.AccessToken)
|
||||
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)
|
||||
|
||||
// MIPS client ID 使用 config 中的 UUID,空则自动生成
|
||||
mipsUUID := cfg.UUID
|
||||
if mipsUUID == "" {
|
||||
mipsUUID = generateUUID()
|
||||
}
|
||||
mipsCloud := miot.NewMipsCloudClient(
|
||||
"ssl://cn-ha.mqtt.io.mi.com:8883",
|
||||
"ha."+mipsUUID,
|
||||
"ha."+uuid,
|
||||
miot.OAUTH2_CLIENT_ID,
|
||||
cfg.AccessToken,
|
||||
cfg.AuthInfo.AccessToken,
|
||||
)
|
||||
miotClient.SetMipsCloud(mipsCloud)
|
||||
|
||||
@@ -56,20 +74,25 @@ func NewClient(ctx context.Context, cfg Config) (*Client, error) {
|
||||
return nil, fmt.Errorf("xiaomi: init: %w", err)
|
||||
}
|
||||
|
||||
// 初始刷新设备列表(对齐 full_client 示例)
|
||||
_ = miotClient.RefreshDevices()
|
||||
|
||||
// 连接 MIPS(对齐 full_client 示例 line 83)
|
||||
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,
|
||||
specParser: miot.NewMIoTSpecParser("en", miot.NewMIoTStorage("./data"), "./data/specs"),
|
||||
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),
|
||||
@@ -87,6 +110,21 @@ func (c *Client) Close() error {
|
||||
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()
|
||||
@@ -94,7 +132,6 @@ func (c *Client) getOrCreateDevice(did string, info map[string]interface{}) *mio
|
||||
return dev
|
||||
}
|
||||
|
||||
// 从 device info 获取 URN 和 model 用于 SPEC 加载
|
||||
urn, _ := info["urn"].(string)
|
||||
model, _ := info["model"].(string)
|
||||
var spec *miot.MIoTSpecInstance
|
||||
|
||||
+17
-7
@@ -1,10 +1,20 @@
|
||||
package xiaomi
|
||||
|
||||
// Config 客户端初始化配置。
|
||||
// AccessToken 必填,UUID/UID 对 MIPS 客户端和 cert 初始化有帮助。
|
||||
type Config struct {
|
||||
ClientSecret string // OAuth Client Secret
|
||||
AccessToken string // OAuth Access Token
|
||||
UID string // 小米账号 UID(选填,用于 cert 初始化)
|
||||
UUID string // MQTT 客户端标识(选填,空则自动生成)
|
||||
// AuthInfo OAuth token 信息。
|
||||
type AuthInfo struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"` // 有效期(秒)
|
||||
ExpiresTS int64 `json:"expires_ts"` // 过期时间戳(unix),0 则内部按 expires_in 计算
|
||||
}
|
||||
|
||||
// Config 客户端初始化配置。
|
||||
type Config struct {
|
||||
AuthInfo AuthInfo // OAuth token 信息
|
||||
UUID string // MIPS 设备标识,OAuth 前生成,永久不可变
|
||||
UID string // 小米帐号 UID,OAuth 后从 API 获取
|
||||
DataDir string // 存储根路径(token/设备/SPEC 缓存),多用户场景每个用户用不同目录,如 ./data/{uid}
|
||||
|
||||
// OnTokenRefreshed 后台自动刷新成功时回调,供调用方将新 token 持久化到数据库。
|
||||
OnTokenRefreshed func(accessToken, refreshToken string)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package devices
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"xiaomihome/xiaomi"
|
||||
)
|
||||
|
||||
@@ -130,21 +132,21 @@ type AirConditioner interface {
|
||||
GetEco(ctx context.Context) (bool, error)
|
||||
|
||||
// 订阅
|
||||
OnStateChanged(handler func(state ACState)) (string, error)
|
||||
OnStateChanged(handler func(state ACState, changed string)) (string, error)
|
||||
}
|
||||
|
||||
// ACState 空调完整状态
|
||||
// ACState 空调完整状态(字段名对齐 SPEC type)
|
||||
type ACState struct {
|
||||
Power bool `json:"power"`
|
||||
Mode ACMode `json:"mode"`
|
||||
TargetTemp float64 `json:"target_temp"`
|
||||
CurrentTemp float64 `json:"current_temp"`
|
||||
TargetHumidity int `json:"target_humidity"`
|
||||
CurrentHumidity int `json:"current_humidity"`
|
||||
FanSpeed FanSpeed `json:"fan_speed"`
|
||||
Swing SwingMode `json:"swing"`
|
||||
AuxHeat bool `json:"aux_heat"`
|
||||
Eco bool `json:"eco"`
|
||||
On bool `json:"on"` // SPEC: "on"
|
||||
Mode ACMode `json:"mode"` // SPEC: "mode"
|
||||
TargetTemperature float64 `json:"targetTemperature"` // SPEC: "target-temperature"
|
||||
Temperature float64 `json:"temperature"` // SPEC: "temperature" (当前温度)
|
||||
TargetHumidity int `json:"targetHumidity"` // SPEC: "target-humidity"
|
||||
CurrentHumidity int `json:"currentHumidity"` // SPEC: "relative-humidity"
|
||||
FanLevel FanSpeed `json:"fanLevel"` // SPEC: "fan-level"
|
||||
Swing SwingMode `json:"swing"` // SPEC: "swing"
|
||||
AuxHeat bool `json:"auxHeat"` // SPEC: "heater"
|
||||
Eco bool `json:"eco"` // SPEC: "eco"
|
||||
}
|
||||
|
||||
type acDevice struct {
|
||||
@@ -159,6 +161,8 @@ type acDevice struct {
|
||||
propCurrentHumidity siidPiid // "relative-humidity"
|
||||
propAuxHeat siidPiid // "aux-heat" or "heater"
|
||||
propEco siidPiid // "eco"
|
||||
|
||||
stateCache ACState // MQTT 推送维护的本地缓存
|
||||
}
|
||||
|
||||
// NewAirConditioner creates an AirConditioner control for the given device.
|
||||
@@ -430,19 +434,58 @@ func (d *acDevice) GetEco(ctx context.Context) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (d *acDevice) OnStateChanged(handler func(state ACState)) (string, error) {
|
||||
if !d.propOn.valid {
|
||||
// propField maps a SPEC short name to a siid/piid pair and a cache-update function.
|
||||
type propField struct {
|
||||
siid, piid int
|
||||
name string
|
||||
fill func(*ACState, interface{})
|
||||
}
|
||||
|
||||
func (d *acDevice) OnStateChanged(handler func(state ACState, changed string)) (string, error) {
|
||||
fields := d.propFields()
|
||||
if len(fields) == 0 {
|
||||
return "", xiaomi.ErrNotSupported
|
||||
}
|
||||
return d.SubProp(d.propOn.siid, 0, func(did string, prop *xiaomi.PropertyValue) {
|
||||
state := ACState{}
|
||||
if b, ok := prop.Value.(bool); ok {
|
||||
state.Power = b
|
||||
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
|
||||
for _, f := range fields {
|
||||
if f.siid == prop.SIID && f.piid == prop.PIID {
|
||||
f.fill(&d.stateCache, prop.Value)
|
||||
handler(d.stateCache, f.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
handler(state)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *acDevice) propFields() []propField {
|
||||
var fields []propField
|
||||
add := func(sp siidPiid, name string, fill func(*ACState, interface{})) {
|
||||
if sp.valid {
|
||||
fields = append(fields, propField{sp.siid, sp.piid, name, fill})
|
||||
}
|
||||
}
|
||||
add(d.propOn, "on", func(s *ACState, v interface{}) { s.On, _ = v.(bool) })
|
||||
add(d.propMode, "mode", func(s *ACState, v interface{}) {
|
||||
switch m := v.(type) {
|
||||
case string: s.Mode = ACMode(m)
|
||||
case float64: s.Mode = ACMode(fmt.Sprintf("%.0f", m))
|
||||
}
|
||||
})
|
||||
add(d.propTargetTemp, "target-temperature", func(s *ACState, v interface{}) { s.TargetTemperature = toFloat(v) })
|
||||
add(d.propCurrentTemp, "temperature", func(s *ACState, v interface{}) { s.Temperature = toFloat(v) })
|
||||
add(d.propFanSpeed, "fan-level", func(s *ACState, v interface{}) {
|
||||
if v, ok := v.(float64); ok { s.FanLevel = FanSpeed(fmt.Sprintf("%.0f", v)) }
|
||||
})
|
||||
add(d.propSwing, "swing", func(s *ACState, v interface{}) {
|
||||
if v, ok := v.(float64); ok { s.Swing = SwingMode(fmt.Sprintf("%.0f", v)) }
|
||||
})
|
||||
add(d.propTargetHumidity, "target-humidity", func(s *ACState, v interface{}) { s.TargetHumidity = int(toFloat(v)) })
|
||||
add(d.propCurrentHumidity, "relative-humidity", func(s *ACState, v interface{}) { s.CurrentHumidity = int(toFloat(v)) })
|
||||
add(d.propAuxHeat, "heater", func(s *ACState, v interface{}) { s.AuxHeat, _ = v.(bool) })
|
||||
add(d.propEco, "eco", func(s *ACState, v interface{}) { s.Eco, _ = v.(bool) })
|
||||
return fields
|
||||
}
|
||||
|
||||
// toFloat converts an interface{} to float64.
|
||||
func toFloat(v interface{}) float64 {
|
||||
switch n := v.(type) {
|
||||
|
||||
+24
-33
@@ -12,14 +12,18 @@ type Cover interface {
|
||||
Close(ctx context.Context) error
|
||||
Stop(ctx context.Context) error
|
||||
|
||||
SetPosition(ctx context.Context, pct int) error // 0=关, 100=全开
|
||||
SetPosition(ctx context.Context, pct int) error
|
||||
GetPosition(ctx context.Context) (int, error)
|
||||
|
||||
OnStateChanged(handler func(position int)) (string, error)
|
||||
}
|
||||
|
||||
type coverDevice struct {
|
||||
BaseDevice
|
||||
propMotor siidPiid // "motor-control" — open/close/stop
|
||||
propPosition siidPiid // "current-position"
|
||||
propMotor siidPiid
|
||||
propPosition siidPiid
|
||||
|
||||
stateCache int
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -28,65 +32,52 @@ const (
|
||||
coverMotorStop = 2
|
||||
)
|
||||
|
||||
// NewCover creates a Cover control for the given device.
|
||||
func NewCover(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Cover, error) {
|
||||
d := &coverDevice{
|
||||
BaseDevice: NewBaseDevice(client, info),
|
||||
}
|
||||
|
||||
d := &coverDevice{BaseDevice: NewBaseDevice(client, info)}
|
||||
resolver := d.getSpecResolver()
|
||||
if resolver == nil {
|
||||
return nil, xiaomi.ErrNoSPEC
|
||||
}
|
||||
|
||||
siid, piid, ok := resolver.find("motor-control")
|
||||
if !ok {
|
||||
return nil, xiaomi.ErrNotSupported
|
||||
}
|
||||
d.propMotor = siidPiid{siid, piid, true}
|
||||
|
||||
if s, p, ok := resolver.find("current-position"); ok {
|
||||
d.propPosition = siidPiid{s, p, true}
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *coverDevice) Open(ctx context.Context) error {
|
||||
if !d.propMotor.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propMotor.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propMotor.siid, d.propMotor.piid, coverMotorOpen)
|
||||
}
|
||||
|
||||
func (d *coverDevice) Close(ctx context.Context) error {
|
||||
if !d.propMotor.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propMotor.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propMotor.siid, d.propMotor.piid, coverMotorClose)
|
||||
}
|
||||
|
||||
func (d *coverDevice) Stop(ctx context.Context) error {
|
||||
if !d.propMotor.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propMotor.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propMotor.siid, d.propMotor.piid, coverMotorStop)
|
||||
}
|
||||
|
||||
func (d *coverDevice) SetPosition(ctx context.Context, pct int) error {
|
||||
if !d.propPosition.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propPosition.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propPosition.siid, d.propPosition.piid, pct)
|
||||
}
|
||||
|
||||
func (d *coverDevice) GetPosition(ctx context.Context) (int, error) {
|
||||
if !d.propPosition.valid {
|
||||
return 0, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propPosition.valid { return 0, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propPosition.siid, d.propPosition.piid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err != nil { return 0, err }
|
||||
return toInt(prop.Value), nil
|
||||
}
|
||||
|
||||
func (d *coverDevice) OnStateChanged(handler func(position int)) (string, error) {
|
||||
if !d.propPosition.valid { return "", xiaomi.ErrNotSupported }
|
||||
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
|
||||
if prop.SIID == d.propPosition.siid && prop.PIID == d.propPosition.piid {
|
||||
d.stateCache = toInt(prop.Value)
|
||||
handler(d.stateCache)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,6 +20,16 @@ type Fan interface {
|
||||
|
||||
SetNaturalWind(ctx context.Context, on bool) error
|
||||
IsNaturalWind(ctx context.Context) (bool, error)
|
||||
|
||||
OnStateChanged(handler func(state FanState, changed string)) (string, error)
|
||||
}
|
||||
|
||||
// FanState 风扇完整状态(MQTT 缓存)
|
||||
type FanState struct {
|
||||
On bool `json:"on"`
|
||||
FanLevel int `json:"fanLevel"`
|
||||
Oscillation bool `json:"oscillation"`
|
||||
NaturalWind bool `json:"naturalWind"`
|
||||
}
|
||||
|
||||
type fanDevice struct {
|
||||
@@ -28,6 +38,8 @@ type fanDevice struct {
|
||||
propFanLevel siidPiid // "fan-level"
|
||||
propOscillation siidPiid // "horizontal-swing" or "oscillation"
|
||||
propNaturalWind siidPiid // "mode" (natural wind mode)
|
||||
|
||||
stateCache FanState
|
||||
}
|
||||
|
||||
// NewFan creates a Fan control for the given device.
|
||||
@@ -148,3 +160,36 @@ func (d *fanDevice) IsNaturalWind(ctx context.Context) (bool, error) {
|
||||
}
|
||||
return toInt(prop.Value) == 1, nil
|
||||
}
|
||||
|
||||
func (d *fanDevice) OnStateChanged(handler func(state FanState, changed string)) (string, error) {
|
||||
type field struct{ siid, piid int; name string; fill func(*FanState, interface{}) }
|
||||
var fields []field
|
||||
if d.propOn.valid {
|
||||
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on",
|
||||
func(s *FanState, v interface{}) { s.On, _ = v.(bool) }})
|
||||
}
|
||||
if d.propFanLevel.valid {
|
||||
fields = append(fields, field{d.propFanLevel.siid, d.propFanLevel.piid, "fan-level",
|
||||
func(s *FanState, v interface{}) { s.FanLevel = toInt(v) }})
|
||||
}
|
||||
if d.propOscillation.valid {
|
||||
fields = append(fields, field{d.propOscillation.siid, d.propOscillation.piid, "swing",
|
||||
func(s *FanState, v interface{}) { s.Oscillation, _ = v.(bool) }})
|
||||
}
|
||||
if d.propNaturalWind.valid {
|
||||
fields = append(fields, field{d.propNaturalWind.siid, d.propNaturalWind.piid, "mode",
|
||||
func(s *FanState, v interface{}) { s.NaturalWind = toInt(v) == 1 }})
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return "", xiaomi.ErrNotSupported
|
||||
}
|
||||
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
|
||||
for _, f := range fields {
|
||||
if f.siid == prop.SIID && f.piid == prop.PIID {
|
||||
f.fill(&d.stateCache, prop.Value)
|
||||
handler(d.stateCache, f.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,108 +11,97 @@ type Humidifier interface {
|
||||
TurnOn(ctx context.Context) error
|
||||
TurnOff(ctx context.Context) error
|
||||
IsOn(ctx context.Context) (bool, error)
|
||||
|
||||
SetTargetHumidity(ctx context.Context, pct int) error
|
||||
GetTargetHumidity(ctx context.Context) (int, error)
|
||||
|
||||
SetFanLevel(ctx context.Context, level int) error
|
||||
GetFanLevel(ctx context.Context) (int, error)
|
||||
|
||||
OnStateChanged(handler func(state HumidifierState, changed string)) (string, error)
|
||||
}
|
||||
|
||||
// HumidifierState 加湿器状态(MQTT 缓存)
|
||||
type HumidifierState struct {
|
||||
On bool `json:"on"`
|
||||
TargetHumidity int `json:"targetHumidity"`
|
||||
FanLevel int `json:"fanLevel"`
|
||||
}
|
||||
|
||||
type humidifierDevice struct {
|
||||
BaseDevice
|
||||
propOn siidPiid // "on"
|
||||
propTargetHumidity siidPiid // "target-humidity"
|
||||
propFanLevel siidPiid // "fan-level"
|
||||
propOn siidPiid
|
||||
propTargetHumidity siidPiid
|
||||
propFanLevel siidPiid
|
||||
stateCache HumidifierState
|
||||
}
|
||||
|
||||
// NewHumidifier creates a Humidifier control for the given device.
|
||||
func NewHumidifier(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Humidifier, error) {
|
||||
d := &humidifierDevice{
|
||||
BaseDevice: NewBaseDevice(client, info),
|
||||
}
|
||||
|
||||
d := &humidifierDevice{BaseDevice: NewBaseDevice(client, info)}
|
||||
resolver := d.getSpecResolver()
|
||||
if resolver == nil {
|
||||
return nil, xiaomi.ErrNoSPEC
|
||||
}
|
||||
|
||||
if resolver == nil { return nil, xiaomi.ErrNoSPEC }
|
||||
siid, piid, ok := resolver.find("on")
|
||||
if !ok {
|
||||
return nil, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !ok { return nil, xiaomi.ErrNotSupported }
|
||||
d.propOn = siidPiid{siid, piid, true}
|
||||
|
||||
if s, p, ok := resolver.find("target-humidity"); ok {
|
||||
d.propTargetHumidity = siidPiid{s, p, true}
|
||||
}
|
||||
if s, p, ok := resolver.find("fan-level"); ok {
|
||||
d.propFanLevel = siidPiid{s, p, true}
|
||||
}
|
||||
|
||||
if s, p, ok := resolver.find("target-humidity"); ok { d.propTargetHumidity = siidPiid{s, p, true} }
|
||||
if s, p, ok := resolver.find("fan-level"); ok { d.propFanLevel = siidPiid{s, p, true} }
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *humidifierDevice) TurnOn(ctx context.Context) error {
|
||||
if !d.propOn.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propOn.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, true)
|
||||
}
|
||||
|
||||
func (d *humidifierDevice) TurnOff(ctx context.Context) error {
|
||||
if !d.propOn.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propOn.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, false)
|
||||
}
|
||||
|
||||
func (d *humidifierDevice) IsOn(ctx context.Context) (bool, error) {
|
||||
if !d.propOn.valid {
|
||||
return false, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propOn.valid { return false, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propOn.siid, d.propOn.piid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b, ok := prop.Value.(bool); ok {
|
||||
return b, nil
|
||||
}
|
||||
if err != nil { return false, err }
|
||||
if b, ok := prop.Value.(bool); ok { return b, nil }
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (d *humidifierDevice) SetTargetHumidity(ctx context.Context, pct int) error {
|
||||
if !d.propTargetHumidity.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propTargetHumidity.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propTargetHumidity.siid, d.propTargetHumidity.piid, pct)
|
||||
}
|
||||
|
||||
func (d *humidifierDevice) GetTargetHumidity(ctx context.Context) (int, error) {
|
||||
if !d.propTargetHumidity.valid {
|
||||
return 0, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propTargetHumidity.valid { return 0, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propTargetHumidity.siid, d.propTargetHumidity.piid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err != nil { return 0, err }
|
||||
return toInt(prop.Value), nil
|
||||
}
|
||||
|
||||
func (d *humidifierDevice) SetFanLevel(ctx context.Context, level int) error {
|
||||
if !d.propFanLevel.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propFanLevel.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propFanLevel.siid, d.propFanLevel.piid, level)
|
||||
}
|
||||
|
||||
func (d *humidifierDevice) GetFanLevel(ctx context.Context) (int, error) {
|
||||
if !d.propFanLevel.valid {
|
||||
return 0, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propFanLevel.valid { return 0, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propFanLevel.siid, d.propFanLevel.piid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err != nil { return 0, err }
|
||||
return toInt(prop.Value), nil
|
||||
}
|
||||
|
||||
func (d *humidifierDevice) OnStateChanged(handler func(state HumidifierState, changed string)) (string, error) {
|
||||
type field struct{ siid, piid int; name string; fill func(*HumidifierState, interface{}) }
|
||||
var fields []field
|
||||
if d.propOn.valid {
|
||||
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *HumidifierState, v interface{}) { s.On, _ = v.(bool) }})
|
||||
}
|
||||
if d.propTargetHumidity.valid {
|
||||
fields = append(fields, field{d.propTargetHumidity.siid, d.propTargetHumidity.piid, "target-humidity", func(s *HumidifierState, v interface{}) { s.TargetHumidity = toInt(v) }})
|
||||
}
|
||||
if d.propFanLevel.valid {
|
||||
fields = append(fields, field{d.propFanLevel.siid, d.propFanLevel.piid, "fan-level", func(s *HumidifierState, v interface{}) { s.FanLevel = toInt(v) }})
|
||||
}
|
||||
if len(fields) == 0 { return "", xiaomi.ErrNotSupported }
|
||||
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
|
||||
for _, f := range fields {
|
||||
if f.siid == prop.SIID && f.piid == prop.PIID {
|
||||
f.fill(&d.stateCache, prop.Value)
|
||||
handler(d.stateCache, f.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+44
-13
@@ -29,15 +29,15 @@ type Light interface {
|
||||
SetColor(ctx context.Context, r, g, b int) error
|
||||
GetColor(ctx context.Context) (r, g, b int, err error)
|
||||
|
||||
OnStateChanged(handler func(state LightState)) (string, error)
|
||||
OnStateChanged(handler func(state LightState, changed string)) (string, error)
|
||||
}
|
||||
|
||||
// LightState 灯光完整状态
|
||||
// LightState 灯光完整状态(字段名对齐 SPEC type)
|
||||
type LightState struct {
|
||||
Power bool `json:"power"`
|
||||
Brightness int `json:"brightness"`
|
||||
ColorTemp int `json:"color_temp"`
|
||||
ColorRGB [3]int `json:"color_rgb"`
|
||||
On bool `json:"on"` // SPEC: "on"
|
||||
Brightness int `json:"brightness"` // SPEC: "brightness"
|
||||
ColorTemperature int `json:"colorTemperature"` // SPEC: "color-temperature"
|
||||
ColorRGB [3]int `json:"colorRgb"` // SPEC: "color"
|
||||
}
|
||||
|
||||
type lightDevice struct {
|
||||
@@ -46,6 +46,8 @@ type lightDevice struct {
|
||||
propBrightness siidPiid // "brightness"
|
||||
propColorTemp siidPiid // "color-temperature"
|
||||
propColor siidPiid // "color"
|
||||
|
||||
stateCache LightState // MQTT 推送维护的本地缓存
|
||||
}
|
||||
|
||||
// NewLight creates a Light control for the given device.
|
||||
@@ -179,16 +181,45 @@ func (d *lightDevice) GetColor(ctx context.Context) (r, g, b int, err error) {
|
||||
return r, g, b, nil
|
||||
}
|
||||
|
||||
func (d *lightDevice) OnStateChanged(handler func(state LightState)) (string, error) {
|
||||
if !d.propOn.valid {
|
||||
func (d *lightDevice) OnStateChanged(handler func(state LightState, changed string)) (string, error) {
|
||||
type field struct {
|
||||
siid, piid int
|
||||
name string
|
||||
fill func(*LightState, interface{})
|
||||
}
|
||||
var fields []field
|
||||
if d.propOn.valid {
|
||||
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on",
|
||||
func(s *LightState, v interface{}) { s.On, _ = v.(bool) }})
|
||||
}
|
||||
if d.propBrightness.valid {
|
||||
fields = append(fields, field{d.propBrightness.siid, d.propBrightness.piid, "brightness",
|
||||
func(s *LightState, v interface{}) { s.Brightness = toInt(v) }})
|
||||
}
|
||||
if d.propColorTemp.valid {
|
||||
fields = append(fields, field{d.propColorTemp.siid, d.propColorTemp.piid, "color-temperature",
|
||||
func(s *LightState, v interface{}) { s.ColorTemperature = toInt(v) }})
|
||||
}
|
||||
if d.propColor.valid {
|
||||
fields = append(fields, field{d.propColor.siid, d.propColor.piid, "color",
|
||||
func(s *LightState, v interface{}) {
|
||||
if v, ok := v.(float64); ok {
|
||||
c := int(v)
|
||||
s.ColorRGB = [3]int{(c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF}
|
||||
}
|
||||
}})
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return "", xiaomi.ErrNotSupported
|
||||
}
|
||||
return d.SubProp(d.propOn.siid, d.propOn.piid, func(did string, prop *xiaomi.PropertyValue) {
|
||||
state := LightState{}
|
||||
if b, ok := prop.Value.(bool); ok {
|
||||
state.Power = b
|
||||
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
|
||||
for _, f := range fields {
|
||||
if f.siid == prop.SIID && f.piid == prop.PIID {
|
||||
f.fill(&d.stateCache, prop.Value)
|
||||
handler(d.stateCache, f.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
handler(state)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,79 +10,64 @@ import (
|
||||
// OccupancySensor 人在传感器读取接口(只读)。
|
||||
type OccupancySensor interface {
|
||||
Device
|
||||
|
||||
// IsOccupied 是否有人。
|
||||
IsOccupied(ctx context.Context) (bool, error)
|
||||
|
||||
// GetIlluminance 环境光照度(lux),部分传感器支持。
|
||||
GetIlluminance(ctx context.Context) (float64, error)
|
||||
}
|
||||
|
||||
// NewOccupancySensor 创建人在传感器。失败返回错误(含无 SPEC / 无相关属性)。
|
||||
func NewOccupancySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (OccupancySensor, error) {
|
||||
if info == nil {
|
||||
return nil, xiaomi.ErrDeviceNotFound
|
||||
}
|
||||
|
||||
miotDev := client.GetMIoTDevice(info.DID)
|
||||
if miotDev == nil {
|
||||
return nil, fmt.Errorf("%w: device not initialized", xiaomi.ErrDeviceNotFound)
|
||||
}
|
||||
spec := miotDev.SpecInstance()
|
||||
if spec == nil {
|
||||
return nil, fmt.Errorf("%w: device has no SPEC information", xiaomi.ErrNotSupported)
|
||||
}
|
||||
|
||||
d := &occupancyDevice{
|
||||
BaseDevice: BaseDevice{client: client, info: info},
|
||||
}
|
||||
|
||||
for _, svc := range spec.Services {
|
||||
for _, prop := range svc.Properties {
|
||||
switch propertyShortName(prop.Type) {
|
||||
case "occupancy":
|
||||
if !d.propOccupancy.valid {
|
||||
d.propOccupancy = siidPiid{svc.IID, prop.PIID, true}
|
||||
}
|
||||
case "illumination":
|
||||
if !d.propIlluminance.valid {
|
||||
d.propIlluminance = siidPiid{svc.IID, prop.PIID, true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !d.propOccupancy.valid {
|
||||
return nil, fmt.Errorf("%w: no occupancy property in SPEC", xiaomi.ErrNotSupported)
|
||||
}
|
||||
|
||||
return d, nil
|
||||
OnStateChanged(handler func(occupied bool, illuminance float64)) (string, error)
|
||||
}
|
||||
|
||||
type occupancyDevice struct {
|
||||
BaseDevice
|
||||
propOccupancy siidPiid
|
||||
propIlluminance siidPiid
|
||||
stateCache [2]interface{} // [0]=bool occupied, [1]=float64 lux
|
||||
}
|
||||
|
||||
func NewOccupancySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (OccupancySensor, error) {
|
||||
if info == nil { return nil, xiaomi.ErrDeviceNotFound }
|
||||
miotDev := client.GetMIoTDevice(info.DID)
|
||||
if miotDev == nil { return nil, fmt.Errorf("%w: device not initialized", xiaomi.ErrDeviceNotFound) }
|
||||
spec := miotDev.SpecInstance()
|
||||
if spec == nil { return nil, fmt.Errorf("%w: device has no SPEC information", xiaomi.ErrNotSupported) }
|
||||
d := &occupancyDevice{BaseDevice: BaseDevice{client: client, info: info}}
|
||||
for _, svc := range spec.Services {
|
||||
for _, prop := range svc.Properties {
|
||||
switch propertyShortName(prop.Type) {
|
||||
case "occupancy":
|
||||
if !d.propOccupancy.valid { d.propOccupancy = siidPiid{svc.IID, prop.PIID, true} }
|
||||
case "illumination":
|
||||
if !d.propIlluminance.valid { d.propIlluminance = siidPiid{svc.IID, prop.PIID, true} }
|
||||
}
|
||||
}
|
||||
}
|
||||
if !d.propOccupancy.valid { return nil, fmt.Errorf("%w: no occupancy property in SPEC", xiaomi.ErrNotSupported) }
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *occupancyDevice) IsOccupied(ctx context.Context) (bool, error) {
|
||||
prop, err := d.GetProp(ctx, d.propOccupancy.siid, d.propOccupancy.piid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b, ok := prop.Value.(bool); ok {
|
||||
return b, nil
|
||||
}
|
||||
if err != nil { return false, err }
|
||||
if b, ok := prop.Value.(bool); ok { return b, nil }
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (d *occupancyDevice) GetIlluminance(ctx context.Context) (float64, error) {
|
||||
if !d.propIlluminance.valid {
|
||||
return 0, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propIlluminance.valid { return 0, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propIlluminance.siid, d.propIlluminance.piid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err != nil { return 0, err }
|
||||
return toFloat(prop.Value), nil
|
||||
}
|
||||
|
||||
func (d *occupancyDevice) OnStateChanged(handler func(occupied bool, illuminance float64)) (string, error) {
|
||||
if !d.propOccupancy.valid && !d.propIlluminance.valid { return "", xiaomi.ErrNotSupported }
|
||||
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
|
||||
switch {
|
||||
case d.propOccupancy.valid && prop.SIID == d.propOccupancy.siid && prop.PIID == d.propOccupancy.piid:
|
||||
if b, ok := prop.Value.(bool); ok { d.stateCache[0] = b }
|
||||
case d.propIlluminance.valid && prop.SIID == d.propIlluminance.siid && prop.PIID == d.propIlluminance.piid:
|
||||
d.stateCache[1] = toFloat(prop.Value)
|
||||
}
|
||||
occ, _ := d.stateCache[0].(bool)
|
||||
lux, _ := d.stateCache[1].(float64)
|
||||
handler(occ, lux)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,13 +23,14 @@ type Switch interface {
|
||||
ToggleChannel(ctx context.Context, ch int) error
|
||||
IsChannelOn(ctx context.Context, ch int) (bool, error)
|
||||
|
||||
OnStateChanged(handler func(on bool)) (string, error)
|
||||
OnStateChanged(handler func(ch int, on bool)) (string, error)
|
||||
}
|
||||
|
||||
// switchDevice is the concrete Switch implementation.
|
||||
type switchDevice struct {
|
||||
BaseDevice
|
||||
channels []siidPiid // "on" property per channel (index 0 = channel 1)
|
||||
channels []siidPiid // "on" property per channel (index 0 = channel 1)
|
||||
stateCache []bool // MQTT 推送维护的本地缓存,索引 0 = 通道 1
|
||||
}
|
||||
|
||||
type siidPiid struct {
|
||||
@@ -63,6 +64,7 @@ func NewSwitch(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Switch, error) {
|
||||
return nil, xiaomi.ErrNotSupported
|
||||
}
|
||||
d.channels = channels
|
||||
d.stateCache = make([]bool, len(channels))
|
||||
|
||||
return d, nil
|
||||
}
|
||||
@@ -129,14 +131,25 @@ func (d *switchDevice) IsChannelOn(ctx context.Context, ch int) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (d *switchDevice) OnStateChanged(handler func(on bool)) (string, error) {
|
||||
p, ok := d.channel(1)
|
||||
if !ok {
|
||||
func (d *switchDevice) OnStateChanged(handler func(ch int, on bool)) (string, error) {
|
||||
if len(d.channels) == 0 {
|
||||
return "", xiaomi.ErrNotSupported
|
||||
}
|
||||
return d.SubProp(p.siid, p.piid, func(did string, prop *xiaomi.PropertyValue) {
|
||||
if b, ok := prop.Value.(bool); ok {
|
||||
handler(b)
|
||||
// 构建 siid/piid → channel index 的快速查找表
|
||||
type chField struct{ siid, piid int }
|
||||
lookup := make(map[chField]int) // key → channel index (0-based)
|
||||
for i, c := range d.channels {
|
||||
lookup[chField{c.siid, c.piid}] = i
|
||||
}
|
||||
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
|
||||
for k, idx := range lookup {
|
||||
if k.siid == prop.SIID && k.piid == prop.PIID {
|
||||
if b, ok := prop.Value.(bool); ok {
|
||||
d.stateCache[idx] = b
|
||||
handler(idx+1, b) // ch = idx+1
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,76 +10,64 @@ import (
|
||||
// TempHumiditySensor 温湿度传感器读取接口(只读)。
|
||||
type TempHumiditySensor interface {
|
||||
Device
|
||||
|
||||
GetTemperature(ctx context.Context) (float64, error)
|
||||
GetHumidity(ctx context.Context) (float64, error)
|
||||
}
|
||||
|
||||
// NewTempHumiditySensor 创建温湿度传感器。失败返回错误(含无 SPEC / 无相关属性)。
|
||||
func NewTempHumiditySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (TempHumiditySensor, error) {
|
||||
if info == nil {
|
||||
return nil, xiaomi.ErrDeviceNotFound
|
||||
}
|
||||
|
||||
miotDev := client.GetMIoTDevice(info.DID)
|
||||
if miotDev == nil {
|
||||
return nil, fmt.Errorf("%w: device not initialized", xiaomi.ErrDeviceNotFound)
|
||||
}
|
||||
spec := miotDev.SpecInstance()
|
||||
if spec == nil {
|
||||
return nil, fmt.Errorf("%w: device has no SPEC information", xiaomi.ErrNotSupported)
|
||||
}
|
||||
|
||||
d := &tempHumidityDevice{
|
||||
BaseDevice: BaseDevice{client: client, info: info},
|
||||
}
|
||||
|
||||
for _, svc := range spec.Services {
|
||||
for _, prop := range svc.Properties {
|
||||
switch propertyShortName(prop.Type) {
|
||||
case "temperature":
|
||||
if !d.propTemp.valid {
|
||||
d.propTemp = siidPiid{svc.IID, prop.PIID, true}
|
||||
}
|
||||
case "relative-humidity":
|
||||
if !d.propHumidity.valid {
|
||||
d.propHumidity = siidPiid{svc.IID, prop.PIID, true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !d.propTemp.valid && !d.propHumidity.valid {
|
||||
return nil, fmt.Errorf("%w: no temperature or humidity property in SPEC", xiaomi.ErrNotSupported)
|
||||
}
|
||||
|
||||
return d, nil
|
||||
OnStateChanged(handler func(temp, humidity float64)) (string, error)
|
||||
}
|
||||
|
||||
type tempHumidityDevice struct {
|
||||
BaseDevice
|
||||
propTemp siidPiid
|
||||
propHumidity siidPiid
|
||||
stateCache [2]float64 // [0]=temperature, [1]=humidity
|
||||
}
|
||||
|
||||
func NewTempHumiditySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (TempHumiditySensor, error) {
|
||||
if info == nil { return nil, xiaomi.ErrDeviceNotFound }
|
||||
miotDev := client.GetMIoTDevice(info.DID)
|
||||
if miotDev == nil { return nil, fmt.Errorf("%w: device not initialized", xiaomi.ErrDeviceNotFound) }
|
||||
spec := miotDev.SpecInstance()
|
||||
if spec == nil { return nil, fmt.Errorf("%w: device has no SPEC information", xiaomi.ErrNotSupported) }
|
||||
d := &tempHumidityDevice{BaseDevice: BaseDevice{client: client, info: info}}
|
||||
for _, svc := range spec.Services {
|
||||
for _, prop := range svc.Properties {
|
||||
switch propertyShortName(prop.Type) {
|
||||
case "temperature":
|
||||
if !d.propTemp.valid { d.propTemp = siidPiid{svc.IID, prop.PIID, true} }
|
||||
case "relative-humidity":
|
||||
if !d.propHumidity.valid { d.propHumidity = siidPiid{svc.IID, prop.PIID, true} }
|
||||
}
|
||||
}
|
||||
}
|
||||
if !d.propTemp.valid && !d.propHumidity.valid {
|
||||
return nil, fmt.Errorf("%w: no temperature or humidity property in SPEC", xiaomi.ErrNotSupported)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *tempHumidityDevice) GetTemperature(ctx context.Context) (float64, error) {
|
||||
if !d.propTemp.valid {
|
||||
return 0, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propTemp.valid { return 0, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propTemp.siid, d.propTemp.piid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err != nil { return 0, err }
|
||||
return toFloat(prop.Value), nil
|
||||
}
|
||||
func (d *tempHumidityDevice) GetHumidity(ctx context.Context) (float64, error) {
|
||||
if !d.propHumidity.valid { return 0, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propHumidity.siid, d.propHumidity.piid)
|
||||
if err != nil { return 0, err }
|
||||
return toFloat(prop.Value), nil
|
||||
}
|
||||
|
||||
func (d *tempHumidityDevice) GetHumidity(ctx context.Context) (float64, error) {
|
||||
if !d.propHumidity.valid {
|
||||
return 0, xiaomi.ErrNotSupported
|
||||
}
|
||||
prop, err := d.GetProp(ctx, d.propHumidity.siid, d.propHumidity.piid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return toFloat(prop.Value), nil
|
||||
func (d *tempHumidityDevice) OnStateChanged(handler func(temp, humidity float64)) (string, error) {
|
||||
if !d.propTemp.valid && !d.propHumidity.valid { return "", xiaomi.ErrNotSupported }
|
||||
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
|
||||
switch {
|
||||
case d.propTemp.valid && prop.SIID == d.propTemp.siid && prop.PIID == d.propTemp.piid:
|
||||
d.stateCache[0] = toFloat(prop.Value)
|
||||
case d.propHumidity.valid && prop.SIID == d.propHumidity.siid && prop.PIID == d.propHumidity.piid:
|
||||
d.stateCache[1] = toFloat(prop.Value)
|
||||
}
|
||||
handler(d.stateCache[0], d.stateCache[1])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,136 +5,106 @@ import (
|
||||
"xiaomihome/xiaomi"
|
||||
)
|
||||
|
||||
// Thermostat 温控器/电热毯统一控制接口
|
||||
// Thermostat 温控器统一控制接口
|
||||
type Thermostat interface {
|
||||
Device
|
||||
TurnOn(ctx context.Context) error
|
||||
TurnOff(ctx context.Context) error
|
||||
IsOn(ctx context.Context) (bool, error)
|
||||
|
||||
SetTargetTemp(ctx context.Context, celsius float64) error
|
||||
SetTargetTemp(ctx context.Context, temp float64) error
|
||||
GetTargetTemp(ctx context.Context) (float64, error)
|
||||
|
||||
GetCurrentTemp(ctx context.Context) (float64, error)
|
||||
|
||||
SetHeating(ctx context.Context, on bool) error
|
||||
IsHeating(ctx context.Context) (bool, error)
|
||||
|
||||
OnStateChanged(handler func(state HeaterState, changed string)) (string, error)
|
||||
}
|
||||
|
||||
type thermostatDevice struct {
|
||||
BaseDevice
|
||||
propOn siidPiid // "on"
|
||||
propTargetTemp siidPiid // "target-temperature"
|
||||
propCurrentTemp siidPiid // "temperature"
|
||||
propHeating siidPiid // "heating" or "mode"
|
||||
propOn siidPiid
|
||||
propTargetTemp siidPiid
|
||||
propCurrentTemp siidPiid
|
||||
propHeating siidPiid
|
||||
stateCache HeaterState
|
||||
}
|
||||
|
||||
// NewThermostat creates a Thermostat control for the given device.
|
||||
func NewThermostat(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Thermostat, error) {
|
||||
d := &thermostatDevice{
|
||||
BaseDevice: NewBaseDevice(client, info),
|
||||
}
|
||||
|
||||
d := &thermostatDevice{BaseDevice: NewBaseDevice(client, info)}
|
||||
resolver := d.getSpecResolver()
|
||||
if resolver == nil {
|
||||
return nil, xiaomi.ErrNoSPEC
|
||||
}
|
||||
|
||||
if resolver == nil { return nil, xiaomi.ErrNoSPEC }
|
||||
siid, piid, ok := resolver.find("on")
|
||||
if !ok {
|
||||
return nil, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !ok { return nil, xiaomi.ErrNotSupported }
|
||||
d.propOn = siidPiid{siid, piid, true}
|
||||
|
||||
if s, p, ok := resolver.find("target-temperature"); ok {
|
||||
d.propTargetTemp = siidPiid{s, p, true}
|
||||
}
|
||||
if s, p, ok := resolver.find("temperature"); ok {
|
||||
d.propCurrentTemp = siidPiid{s, p, true}
|
||||
}
|
||||
if s, p, ok := resolver.find("heating"); ok {
|
||||
d.propHeating = siidPiid{s, p, true}
|
||||
} else if s, p, ok := resolver.find("heater"); ok {
|
||||
d.propHeating = siidPiid{s, p, true}
|
||||
}
|
||||
|
||||
if s, p, ok := resolver.find("target-temperature"); ok { d.propTargetTemp = siidPiid{s, p, true} }
|
||||
if s, p, ok := resolver.find("temperature"); ok { d.propCurrentTemp = siidPiid{s, p, true} }
|
||||
if s, p, ok := resolver.find("heater"); ok { d.propHeating = siidPiid{s, p, true} }
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *thermostatDevice) TurnOn(ctx context.Context) error {
|
||||
if !d.propOn.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propOn.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, true)
|
||||
}
|
||||
|
||||
func (d *thermostatDevice) TurnOff(ctx context.Context) error {
|
||||
if !d.propOn.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propOn.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, false)
|
||||
}
|
||||
|
||||
func (d *thermostatDevice) IsOn(ctx context.Context) (bool, error) {
|
||||
if !d.propOn.valid {
|
||||
return false, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propOn.valid { return false, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propOn.siid, d.propOn.piid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b, ok := prop.Value.(bool); ok {
|
||||
return b, nil
|
||||
}
|
||||
if err != nil { return false, err }
|
||||
if b, ok := prop.Value.(bool); ok { return b, nil }
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (d *thermostatDevice) SetTargetTemp(ctx context.Context, celsius float64) error {
|
||||
if !d.propTargetTemp.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
return d.SetProp(ctx, d.propTargetTemp.siid, d.propTargetTemp.piid, celsius)
|
||||
func (d *thermostatDevice) SetTargetTemp(ctx context.Context, temp float64) error {
|
||||
if !d.propTargetTemp.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propTargetTemp.siid, d.propTargetTemp.piid, temp)
|
||||
}
|
||||
|
||||
func (d *thermostatDevice) GetTargetTemp(ctx context.Context) (float64, error) {
|
||||
if !d.propTargetTemp.valid {
|
||||
return 0, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propTargetTemp.valid { return 0, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propTargetTemp.siid, d.propTargetTemp.piid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err != nil { return 0, err }
|
||||
return toFloat(prop.Value), nil
|
||||
}
|
||||
|
||||
func (d *thermostatDevice) GetCurrentTemp(ctx context.Context) (float64, error) {
|
||||
if !d.propCurrentTemp.valid {
|
||||
return 0, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propCurrentTemp.valid { return 0, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propCurrentTemp.siid, d.propCurrentTemp.piid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err != nil { return 0, err }
|
||||
return toFloat(prop.Value), nil
|
||||
}
|
||||
|
||||
func (d *thermostatDevice) SetHeating(ctx context.Context, on bool) error {
|
||||
if !d.propHeating.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propHeating.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propHeating.siid, d.propHeating.piid, on)
|
||||
}
|
||||
|
||||
func (d *thermostatDevice) IsHeating(ctx context.Context) (bool, error) {
|
||||
if !d.propHeating.valid {
|
||||
return false, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propHeating.valid { return false, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propHeating.siid, d.propHeating.piid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b, ok := prop.Value.(bool); ok {
|
||||
return b, nil
|
||||
}
|
||||
if err != nil { return false, err }
|
||||
if b, ok := prop.Value.(bool); ok { return b, nil }
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (d *thermostatDevice) OnStateChanged(handler func(state HeaterState, changed string)) (string, error) {
|
||||
type field struct{ siid, piid int; name string; fill func(*HeaterState, interface{}) }
|
||||
var fields []field
|
||||
if d.propOn.valid {
|
||||
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *HeaterState, v interface{}) { s.On, _ = v.(bool) }})
|
||||
}
|
||||
if d.propTargetTemp.valid {
|
||||
fields = append(fields, field{d.propTargetTemp.siid, d.propTargetTemp.piid, "target-temperature", func(s *HeaterState, v interface{}) { s.TargetTemp = toFloat(v) }})
|
||||
}
|
||||
if d.propCurrentTemp.valid {
|
||||
fields = append(fields, field{d.propCurrentTemp.siid, d.propCurrentTemp.piid, "temperature", func(s *HeaterState, v interface{}) { s.CurrentTemp = toFloat(v) }})
|
||||
}
|
||||
if len(fields) == 0 { return "", xiaomi.ErrNotSupported }
|
||||
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
|
||||
for _, f := range fields {
|
||||
if f.siid == prop.SIID && f.piid == prop.PIID {
|
||||
f.fill(&d.stateCache, prop.Value)
|
||||
handler(d.stateCache, f.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+72
-90
@@ -11,131 +11,113 @@ type Vacuum interface {
|
||||
Start(ctx context.Context) error
|
||||
Stop(ctx context.Context) error
|
||||
ReturnToDock(ctx context.Context) error
|
||||
|
||||
SetFanLevel(ctx context.Context, level int) error
|
||||
GetFanLevel(ctx context.Context) (int, error)
|
||||
|
||||
GetStatus(ctx context.Context) (VacuumStatus, error)
|
||||
|
||||
OnStateChanged(handler func(state VacuumStatus, changed string)) (string, error)
|
||||
}
|
||||
|
||||
// VacuumStatus 扫地机运行状态
|
||||
// VacuumStatus 扫地机运行状态(字段名对齐 SPEC type)
|
||||
type VacuumStatus struct {
|
||||
Power bool `json:"power"`
|
||||
Mode int `json:"mode"` // 0=idle, 1=sweep, 2=mop, 3=sweep+mop, 4=charging
|
||||
FanLevel int `json:"fan_level"`
|
||||
Battery int `json:"battery"` // 0-100
|
||||
On bool `json:"on"`
|
||||
Mode int `json:"mode"`
|
||||
FanLevel int `json:"fanLevel"`
|
||||
Battery int `json:"battery"`
|
||||
}
|
||||
|
||||
type vacuumDevice struct {
|
||||
BaseDevice
|
||||
propOn siidPiid // "on" — start/stop control
|
||||
propMode siidPiid // "mode" — 0=charge(dock), 1=sweep, etc.
|
||||
propFanLevel siidPiid // "fan-level" (suction power)
|
||||
propBattery siidPiid // "battery-level"
|
||||
propOn siidPiid
|
||||
propMode siidPiid
|
||||
propFanLevel siidPiid
|
||||
propBattery siidPiid
|
||||
stateCache VacuumStatus
|
||||
}
|
||||
|
||||
// NewVacuum creates a Vacuum control for the given device.
|
||||
func NewVacuum(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Vacuum, error) {
|
||||
d := &vacuumDevice{
|
||||
BaseDevice: NewBaseDevice(client, info),
|
||||
}
|
||||
|
||||
d := &vacuumDevice{BaseDevice: NewBaseDevice(client, info)}
|
||||
resolver := d.getSpecResolver()
|
||||
if resolver == nil {
|
||||
return nil, xiaomi.ErrNoSPEC
|
||||
}
|
||||
|
||||
if resolver == nil { return nil, xiaomi.ErrNoSPEC }
|
||||
siid, piid, ok := resolver.find("on")
|
||||
if !ok {
|
||||
return nil, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !ok { return nil, xiaomi.ErrNotSupported }
|
||||
d.propOn = siidPiid{siid, piid, true}
|
||||
|
||||
if s, p, ok := resolver.find("mode"); ok {
|
||||
d.propMode = siidPiid{s, p, true}
|
||||
}
|
||||
if s, p, ok := resolver.find("fan-level"); ok {
|
||||
d.propFanLevel = siidPiid{s, p, true}
|
||||
}
|
||||
if s, p, ok := resolver.find("battery-level"); ok {
|
||||
d.propBattery = siidPiid{s, p, true}
|
||||
}
|
||||
|
||||
if s, p, ok := resolver.find("mode"); ok { d.propMode = siidPiid{s, p, true} }
|
||||
if s, p, ok := resolver.find("fan-level"); ok { d.propFanLevel = siidPiid{s, p, true} }
|
||||
if s, p, ok := resolver.find("battery-level"); ok { d.propBattery = siidPiid{s, p, true} }
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *vacuumDevice) Start(ctx context.Context) error {
|
||||
if !d.propOn.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, true)
|
||||
_, err := d.Action(ctx, d.propOn.siid, 1, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *vacuumDevice) Stop(ctx context.Context) error {
|
||||
if !d.propOn.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, false)
|
||||
_, err := d.Action(ctx, d.propOn.siid, 0, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *vacuumDevice) ReturnToDock(ctx context.Context) error {
|
||||
if !d.propMode.valid {
|
||||
// Try using on=false as fallback
|
||||
return d.Stop(ctx)
|
||||
}
|
||||
return d.SetProp(ctx, d.propMode.siid, d.propMode.piid, 0)
|
||||
_, err := d.Action(ctx, d.propOn.siid, 2, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *vacuumDevice) SetFanLevel(ctx context.Context, level int) error {
|
||||
if !d.propFanLevel.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propFanLevel.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propFanLevel.siid, d.propFanLevel.piid, level)
|
||||
}
|
||||
|
||||
func (d *vacuumDevice) GetFanLevel(ctx context.Context) (int, error) {
|
||||
if !d.propFanLevel.valid {
|
||||
return 0, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propFanLevel.valid { return 0, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propFanLevel.siid, d.propFanLevel.piid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err != nil { return 0, err }
|
||||
return toInt(prop.Value), nil
|
||||
}
|
||||
|
||||
func (d *vacuumDevice) GetStatus(ctx context.Context) (VacuumStatus, error) {
|
||||
// 云端实时查询
|
||||
keys := []xiaomi.PropKey{}
|
||||
add := func(sp siidPiid) {
|
||||
if sp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: sp.siid, PIID: sp.piid}) }
|
||||
}
|
||||
add(d.propOn); add(d.propMode); add(d.propFanLevel); add(d.propBattery)
|
||||
props, err := d.client.GetProps(ctx, keys)
|
||||
if err != nil { return VacuumStatus{}, err }
|
||||
status := VacuumStatus{}
|
||||
|
||||
if d.propOn.valid {
|
||||
prop, err := d.GetProp(ctx, d.propOn.siid, d.propOn.piid)
|
||||
if err == nil {
|
||||
if b, ok := prop.Value.(bool); ok {
|
||||
status.Power = b
|
||||
}
|
||||
for _, p := range props {
|
||||
switch {
|
||||
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid:
|
||||
status.On, _ = p.Value.(bool)
|
||||
case d.propMode.valid && p.SIID == d.propMode.siid && p.PIID == d.propMode.piid:
|
||||
status.Mode = toInt(p.Value)
|
||||
case d.propFanLevel.valid && p.SIID == d.propFanLevel.siid && p.PIID == d.propFanLevel.piid:
|
||||
status.FanLevel = toInt(p.Value)
|
||||
case d.propBattery.valid && p.SIID == d.propBattery.siid && p.PIID == d.propBattery.piid:
|
||||
status.Battery = toInt(p.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if d.propMode.valid {
|
||||
prop, err := d.GetProp(ctx, d.propMode.siid, d.propMode.piid)
|
||||
if err == nil {
|
||||
status.Mode = toInt(prop.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if d.propFanLevel.valid {
|
||||
prop, err := d.GetProp(ctx, d.propFanLevel.siid, d.propFanLevel.piid)
|
||||
if err == nil {
|
||||
status.FanLevel = toInt(prop.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if d.propBattery.valid {
|
||||
prop, err := d.GetProp(ctx, d.propBattery.siid, d.propBattery.piid)
|
||||
if err == nil {
|
||||
status.Battery = toInt(prop.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (d *vacuumDevice) OnStateChanged(handler func(state VacuumStatus, changed string)) (string, error) {
|
||||
type field struct{ siid, piid int; name string; fill func(*VacuumStatus, interface{}) }
|
||||
var fields []field
|
||||
if d.propOn.valid {
|
||||
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *VacuumStatus, v interface{}) { s.On, _ = v.(bool) }})
|
||||
}
|
||||
if d.propMode.valid {
|
||||
fields = append(fields, field{d.propMode.siid, d.propMode.piid, "mode", func(s *VacuumStatus, v interface{}) { s.Mode = toInt(v) }})
|
||||
}
|
||||
if d.propFanLevel.valid {
|
||||
fields = append(fields, field{d.propFanLevel.siid, d.propFanLevel.piid, "fan-level", func(s *VacuumStatus, v interface{}) { s.FanLevel = toInt(v) }})
|
||||
}
|
||||
if d.propBattery.valid {
|
||||
fields = append(fields, field{d.propBattery.siid, d.propBattery.piid, "battery", func(s *VacuumStatus, v interface{}) { s.Battery = toInt(v) }})
|
||||
}
|
||||
if len(fields) == 0 { return "", xiaomi.ErrNotSupported }
|
||||
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
|
||||
for _, f := range fields {
|
||||
if f.siid == prop.SIID && f.piid == prop.PIID {
|
||||
f.fill(&d.stateCache, prop.Value)
|
||||
handler(d.stateCache, f.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,106 +5,98 @@ import (
|
||||
"xiaomihome/xiaomi"
|
||||
)
|
||||
|
||||
// WaterHeater 热水器统一控制接口
|
||||
// WaterHeater 热水器/饮水机统一控制接口
|
||||
type WaterHeater interface {
|
||||
Device
|
||||
TurnOn(ctx context.Context) error
|
||||
TurnOff(ctx context.Context) error
|
||||
IsOn(ctx context.Context) (bool, error)
|
||||
|
||||
SetTargetTemp(ctx context.Context, celsius float64) error
|
||||
SetTargetTemp(ctx context.Context, temp float64) error
|
||||
GetTargetTemp(ctx context.Context) (float64, error)
|
||||
|
||||
GetCurrentTemp(ctx context.Context) (float64, error)
|
||||
|
||||
OnStateChanged(handler func(state HeaterState, changed string)) (string, error)
|
||||
}
|
||||
|
||||
// HeaterState 热水器状态(MQTT 缓存)
|
||||
type HeaterState struct {
|
||||
On bool `json:"on"`
|
||||
TargetTemp float64 `json:"targetTemperature"`
|
||||
CurrentTemp float64 `json:"temperature"`
|
||||
}
|
||||
|
||||
type waterHeaterDevice struct {
|
||||
BaseDevice
|
||||
propOn siidPiid // "on"
|
||||
propTargetTemp siidPiid // "target-temperature"
|
||||
propCurrentTemp siidPiid // "temperature"
|
||||
propOn siidPiid
|
||||
propTargetTemp siidPiid
|
||||
propCurrentTemp siidPiid
|
||||
stateCache HeaterState
|
||||
}
|
||||
|
||||
// NewWaterHeater creates a WaterHeater control for the given device.
|
||||
func NewWaterHeater(client *xiaomi.Client, info *xiaomi.DeviceInfo) (WaterHeater, error) {
|
||||
d := &waterHeaterDevice{
|
||||
BaseDevice: NewBaseDevice(client, info),
|
||||
}
|
||||
|
||||
d := &waterHeaterDevice{BaseDevice: NewBaseDevice(client, info)}
|
||||
resolver := d.getSpecResolver()
|
||||
if resolver == nil {
|
||||
return nil, xiaomi.ErrNoSPEC
|
||||
}
|
||||
|
||||
if resolver == nil { return nil, xiaomi.ErrNoSPEC }
|
||||
siid, piid, ok := resolver.find("on")
|
||||
if !ok {
|
||||
return nil, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !ok { return nil, xiaomi.ErrNotSupported }
|
||||
d.propOn = siidPiid{siid, piid, true}
|
||||
|
||||
if s, p, ok := resolver.find("target-temperature"); ok {
|
||||
d.propTargetTemp = siidPiid{s, p, true}
|
||||
}
|
||||
if s, p, ok := resolver.find("temperature"); ok {
|
||||
d.propCurrentTemp = siidPiid{s, p, true}
|
||||
}
|
||||
|
||||
if s, p, ok := resolver.find("target-temperature"); ok { d.propTargetTemp = siidPiid{s, p, true} }
|
||||
if s, p, ok := resolver.find("temperature"); ok { d.propCurrentTemp = siidPiid{s, p, true} }
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *waterHeaterDevice) TurnOn(ctx context.Context) error {
|
||||
if !d.propOn.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propOn.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, true)
|
||||
}
|
||||
|
||||
func (d *waterHeaterDevice) TurnOff(ctx context.Context) error {
|
||||
if !d.propOn.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propOn.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, false)
|
||||
}
|
||||
|
||||
func (d *waterHeaterDevice) IsOn(ctx context.Context) (bool, error) {
|
||||
if !d.propOn.valid {
|
||||
return false, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propOn.valid { return false, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propOn.siid, d.propOn.piid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if b, ok := prop.Value.(bool); ok {
|
||||
return b, nil
|
||||
}
|
||||
if err != nil { return false, err }
|
||||
if b, ok := prop.Value.(bool); ok { return b, nil }
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (d *waterHeaterDevice) SetTargetTemp(ctx context.Context, celsius float64) error {
|
||||
if !d.propTargetTemp.valid {
|
||||
return xiaomi.ErrNotSupported
|
||||
}
|
||||
return d.SetProp(ctx, d.propTargetTemp.siid, d.propTargetTemp.piid, celsius)
|
||||
func (d *waterHeaterDevice) SetTargetTemp(ctx context.Context, temp float64) error {
|
||||
if !d.propTargetTemp.valid { return xiaomi.ErrNotSupported }
|
||||
return d.SetProp(ctx, d.propTargetTemp.siid, d.propTargetTemp.piid, temp)
|
||||
}
|
||||
|
||||
func (d *waterHeaterDevice) GetTargetTemp(ctx context.Context) (float64, error) {
|
||||
if !d.propTargetTemp.valid {
|
||||
return 0, xiaomi.ErrNotSupported
|
||||
}
|
||||
if !d.propTargetTemp.valid { return 0, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propTargetTemp.siid, d.propTargetTemp.piid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err != nil { return 0, err }
|
||||
return toFloat(prop.Value), nil
|
||||
}
|
||||
func (d *waterHeaterDevice) GetCurrentTemp(ctx context.Context) (float64, error) {
|
||||
if !d.propCurrentTemp.valid { return 0, xiaomi.ErrNotSupported }
|
||||
prop, err := d.GetProp(ctx, d.propCurrentTemp.siid, d.propCurrentTemp.piid)
|
||||
if err != nil { return 0, err }
|
||||
return toFloat(prop.Value), nil
|
||||
}
|
||||
|
||||
func (d *waterHeaterDevice) GetCurrentTemp(ctx context.Context) (float64, error) {
|
||||
if !d.propCurrentTemp.valid {
|
||||
return 0, xiaomi.ErrNotSupported
|
||||
func (d *waterHeaterDevice) OnStateChanged(handler func(state HeaterState, changed string)) (string, error) {
|
||||
type field struct{ siid, piid int; name string; fill func(*HeaterState, interface{}) }
|
||||
var fields []field
|
||||
if d.propOn.valid {
|
||||
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *HeaterState, v interface{}) { s.On, _ = v.(bool) }})
|
||||
}
|
||||
prop, err := d.GetProp(ctx, d.propCurrentTemp.siid, d.propCurrentTemp.piid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
if d.propTargetTemp.valid {
|
||||
fields = append(fields, field{d.propTargetTemp.siid, d.propTargetTemp.piid, "target-temperature", func(s *HeaterState, v interface{}) { s.TargetTemp = toFloat(v) }})
|
||||
}
|
||||
return toFloat(prop.Value), nil
|
||||
if d.propCurrentTemp.valid {
|
||||
fields = append(fields, field{d.propCurrentTemp.siid, d.propCurrentTemp.piid, "temperature", func(s *HeaterState, v interface{}) { s.CurrentTemp = toFloat(v) }})
|
||||
}
|
||||
if len(fields) == 0 { return "", xiaomi.ErrNotSupported }
|
||||
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
|
||||
for _, f := range fields {
|
||||
if f.siid == prop.SIID && f.piid == prop.PIID {
|
||||
f.fill(&d.stateCache, prop.Value)
|
||||
handler(d.stateCache, f.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,10 +21,15 @@ func main() {
|
||||
}
|
||||
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
ClientSecret: "",
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
UID: cfg.UID,
|
||||
UUID: cfg.UUID,
|
||||
AuthInfo: xiaomi.AuthInfo{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
ExpiresIn: cfg.AuthInfo.ExpiresIn,
|
||||
ExpiresTS: cfg.AuthInfo.ExpiresTS,
|
||||
},
|
||||
DataDir: "./data",
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"xiaomihome/xiaomi"
|
||||
miotconfig "xiaomihome/miot/examples/config"
|
||||
"xiaomihome/xiaomi"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -21,10 +21,15 @@ func main() {
|
||||
}
|
||||
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
ClientSecret: "",
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
UID: cfg.UID,
|
||||
UUID: cfg.UUID,
|
||||
AuthInfo: xiaomi.AuthInfo{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
ExpiresIn: cfg.AuthInfo.ExpiresIn,
|
||||
ExpiresTS: cfg.AuthInfo.ExpiresTS,
|
||||
},
|
||||
DataDir: "./data",
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -22,10 +22,15 @@ func main() {
|
||||
}
|
||||
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
ClientSecret: "",
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
UID: cfg.UID,
|
||||
UUID: cfg.UUID,
|
||||
AuthInfo: xiaomi.AuthInfo{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
ExpiresIn: cfg.AuthInfo.ExpiresIn,
|
||||
ExpiresTS: cfg.AuthInfo.ExpiresTS,
|
||||
},
|
||||
DataDir: "./data",
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -22,10 +22,15 @@ func main() {
|
||||
}
|
||||
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
ClientSecret: "",
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
UID: cfg.UID,
|
||||
UUID: cfg.UUID,
|
||||
AuthInfo: xiaomi.AuthInfo{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
ExpiresIn: cfg.AuthInfo.ExpiresIn,
|
||||
ExpiresTS: cfg.AuthInfo.ExpiresTS,
|
||||
},
|
||||
DataDir: "./data",
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -88,9 +93,10 @@ func main() {
|
||||
log.Printf("当前颜色: R=%d G=%d B=%d", r, g, b)
|
||||
}
|
||||
|
||||
subID, err := lt.OnStateChanged(func(state devices.LightState) {
|
||||
log.Printf("灯光状态变化: power=%v brightness=%d colorTemp=%dK rgb=[%d,%d,%d]",
|
||||
state.Power, state.Brightness, state.ColorTemp,
|
||||
subID, err := lt.OnStateChanged(func(state devices.LightState, changed string) {
|
||||
log.Printf("灯光状态变化: %s → on=%v brightness=%d colorTemp=%dK rgb=[%d,%d,%d]",
|
||||
changed,
|
||||
state.On, state.Brightness, state.ColorTemperature,
|
||||
state.ColorRGB[0], state.ColorRGB[1], state.ColorRGB[2])
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -22,10 +22,15 @@ func main() {
|
||||
}
|
||||
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
ClientSecret: "",
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
UID: cfg.UID,
|
||||
UUID: cfg.UUID,
|
||||
AuthInfo: xiaomi.AuthInfo{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
ExpiresIn: cfg.AuthInfo.ExpiresIn,
|
||||
ExpiresTS: cfg.AuthInfo.ExpiresTS,
|
||||
},
|
||||
DataDir: "./data",
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -96,10 +101,10 @@ func main() {
|
||||
log.Println("开启节能模式...")
|
||||
ac.SetEco(ctx, true)
|
||||
|
||||
subID, err := ac.OnStateChanged(func(state devices.ACState) {
|
||||
log.Printf("空调状态变化: power=%v mode=%s target=%.1f current=%.1f fan=%s swing=%s",
|
||||
state.Power, state.Mode, state.TargetTemp, state.CurrentTemp,
|
||||
state.FanSpeed, state.Swing)
|
||||
subID, err := ac.OnStateChanged(func(state devices.ACState, changed string) {
|
||||
log.Printf("空调状态变化: %s → on=%v mode=%s target=%.1f current=%.1f fan=%s swing=%s",
|
||||
changed, state.On, state.Mode, state.TargetTemperature, state.Temperature,
|
||||
state.FanLevel, state.Swing)
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("订阅状态失败: %v", err)
|
||||
|
||||
@@ -22,10 +22,15 @@ func main() {
|
||||
}
|
||||
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
ClientSecret: "",
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
UID: cfg.UID,
|
||||
UUID: cfg.UUID,
|
||||
AuthInfo: xiaomi.AuthInfo{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
ExpiresIn: cfg.AuthInfo.ExpiresIn,
|
||||
ExpiresTS: cfg.AuthInfo.ExpiresTS,
|
||||
},
|
||||
DataDir: "./data",
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -21,10 +21,15 @@ func main() {
|
||||
}
|
||||
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
ClientSecret: "",
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
UID: cfg.UID,
|
||||
UUID: cfg.UUID,
|
||||
AuthInfo: xiaomi.AuthInfo{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
ExpiresIn: cfg.AuthInfo.ExpiresIn,
|
||||
ExpiresTS: cfg.AuthInfo.ExpiresTS,
|
||||
},
|
||||
DataDir: "./data",
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -26,10 +26,15 @@ func main() {
|
||||
}
|
||||
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
ClientSecret: "",
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
UID: cfg.UID,
|
||||
UUID: cfg.UUID,
|
||||
AuthInfo: xiaomi.AuthInfo{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
ExpiresIn: cfg.AuthInfo.ExpiresIn,
|
||||
ExpiresTS: cfg.AuthInfo.ExpiresTS,
|
||||
},
|
||||
DataDir: "./data",
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -27,10 +27,15 @@ func main() {
|
||||
}
|
||||
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
ClientSecret: "",
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
UID: cfg.UID,
|
||||
UUID: cfg.UUID,
|
||||
AuthInfo: xiaomi.AuthInfo{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
ExpiresIn: cfg.AuthInfo.ExpiresIn,
|
||||
ExpiresTS: cfg.AuthInfo.ExpiresTS,
|
||||
},
|
||||
DataDir: "./data",
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -22,10 +22,15 @@ func main() {
|
||||
}
|
||||
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
ClientSecret: "",
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
UID: cfg.UID,
|
||||
UUID: cfg.UUID,
|
||||
AuthInfo: xiaomi.AuthInfo{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
ExpiresIn: cfg.AuthInfo.ExpiresIn,
|
||||
ExpiresTS: cfg.AuthInfo.ExpiresTS,
|
||||
},
|
||||
DataDir: "./data",
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -21,10 +21,15 @@ func main() {
|
||||
}
|
||||
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
ClientSecret: "",
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
UID: cfg.UID,
|
||||
UUID: cfg.UUID,
|
||||
AuthInfo: xiaomi.AuthInfo{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
ExpiresIn: cfg.AuthInfo.ExpiresIn,
|
||||
ExpiresTS: cfg.AuthInfo.ExpiresTS,
|
||||
},
|
||||
DataDir: "./data",
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBazCCAQ+gAwIBAgIEA/UKYDAMBggqhkjOPQQDAgUAMCIxEzARBgNVBAoTCk1p
|
||||
amlhIFJvb3QxCzAJBgNVBAYTAkNOMCAXDTE2MTEyMzAxMzk0NVoYDzIwNjYxMTEx
|
||||
MDEzOTQ1WjAiMRMwEQYDVQQKEwpNaWppYSBSb290MQswCQYDVQQGEwJDTjBZMBMG
|
||||
ByqGSM49AgEGCCqGSM49AwEHA0IABL71iwLa4//4VBqgRI+6xE23xpovqPCxtv96
|
||||
2VHbZij61/Ag6jmi7oZ/3Xg/3C+whglcwoUEE6KALGJ9vccV9PmjLzAtMAwGA1Ud
|
||||
EwQFMAMBAf8wHQYDVR0OBBYEFJa3onw5sblmM6n40QmyAGDI5sURMAwGCCqGSM49
|
||||
BAMCBQADSAAwRQIgchciK9h6tZmfrP8Ka6KziQ4Lv3hKfrHtAZXMHPda4IYCIQCG
|
||||
az93ggFcbrG9u2wixjx1HKW4DUA5NXZG0wWQTpJTbQ==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBjzCCATWgAwIBAgIBATAKBggqhkjOPQQDAjAiMRMwEQYDVQQKEwpNaWppYSBS
|
||||
b290MQswCQYDVQQGEwJDTjAgFw0yMjA2MDkxNDE0MThaGA8yMDcyMDUyNzE0MTQx
|
||||
OFowLDELMAkGA1UEBhMCQ04xHTAbBgNVBAoMFE1JT1QgQ0VOVFJBTCBHQVRFV0FZ
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdYrzbnp/0x/cZLZnuEDXTFf8mhj4
|
||||
CVpZPwgj9e9Ve5r3K7zvu8Jjj7JF1JjQYvEC6yhp1SzBgglnK4L8xQzdiqNQME4w
|
||||
HQYDVR0OBBYEFCf9+YBU7pXDs6K6CAQPRhlGJ+cuMB8GA1UdIwQYMBaAFJa3onw5
|
||||
sblmM6n40QmyAGDI5sURMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIh
|
||||
AKUv+c8v98vypkGMTzMwckGjjVqTef8xodsy6PhcSCq+AiA/n9mDs62hAo5zXyJy
|
||||
Bs1s7mqXPf1XgieoxIvs1MqyiA==
|
||||
-----END CERTIFICATE-----
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,71 @@
|
||||
// Package main demonstrates AirConditioner.OnStateChanged subscription.
|
||||
// 运行后等待状态变化,Ctrl+C 退出。
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"xiaomihome/xiaomi"
|
||||
"xiaomihome/xiaomi/devices"
|
||||
miotconfig "xiaomihome/miot/examples/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := miotconfig.LoadConfig("../../../miot/examples/config.json")
|
||||
if err != nil {
|
||||
log.Fatal("请先运行 miot/examples/cloud_oauth 获取 token:", err)
|
||||
}
|
||||
|
||||
client, err := xiaomi.NewClient(context.Background(), xiaomi.Config{
|
||||
AuthInfo: xiaomi.AuthInfo{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
ExpiresIn: cfg.AuthInfo.ExpiresIn,
|
||||
ExpiresTS: cfg.AuthInfo.ExpiresTS,
|
||||
},
|
||||
DataDir: "./data",
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
const targetDID = "2053225728"
|
||||
info, err := client.GetDevice(context.Background(), targetDID)
|
||||
if err != nil {
|
||||
log.Fatalf("获取设备 %s 失败: %v", targetDID, err)
|
||||
}
|
||||
log.Printf("设备: %s (DID: %s, 型号: %s)", info.Name, info.DID, info.Model)
|
||||
|
||||
ac, err := devices.NewAirConditioner(client, info)
|
||||
if err != nil {
|
||||
log.Fatal("创建 AirConditioner 失败:", err)
|
||||
}
|
||||
|
||||
// 先查一次当前状态
|
||||
on, _ := ac.IsOn(context.Background())
|
||||
log.Printf("初始状态: on=%v", on)
|
||||
|
||||
// 订阅状态变化
|
||||
subID, err := ac.OnStateChanged(func(state devices.ACState, changed string) {
|
||||
log.Printf("[%s] on=%v mode=%s target=%.1f current=%.1f fan=%s swing=%s",
|
||||
changed, state.On, state.Mode, state.TargetTemperature, state.Temperature,
|
||||
state.FanLevel, state.Swing)
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("订阅失败: %v", err)
|
||||
} else {
|
||||
log.Printf("已订阅: subID=%s", subID)
|
||||
}
|
||||
|
||||
log.Println("\n等待状态变化,Ctrl+C 退出...")
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, os.Interrupt)
|
||||
<-ch
|
||||
log.Println("退出")
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
# Config 补回 UUID / UID
|
||||
|
||||
> 2026-06-30
|
||||
|
||||
## UUID 和 UID 的作用
|
||||
|
||||
| 字段 | 作用 | 约束 |
|
||||
|------|------|------|
|
||||
| UUID | MIPS MQTT 客户端标识,`"ha.{uuid}"` 格式连接小米云端 MQTT | OAuth 前生成,永久不变,变更则 token 失效 |
|
||||
| UID | 小米帐号唯一 ID,证书生成、storage 文件命名、OAuth 流程用到 | OAuth 后固定的,从 `GetHomeInfos` API 取 `uid` 字段 |
|
||||
|
||||
---
|
||||
|
||||
## 修改清单
|
||||
|
||||
### 1. `xiaomi/config.go`
|
||||
|
||||
```diff
|
||||
type Config struct {
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
+ UUID string // MIPS 设备标识,OAuth 前生成,永久不可变
|
||||
+ UID string // 小米帐号 UID,OAuth 后从 API 获取
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `xiaomi/client.go` — 第 33 行、第 40 行
|
||||
|
||||
```diff
|
||||
- uuid := generateUUID()
|
||||
+ if cfg.UUID == "" {
|
||||
+ return nil, fmt.Errorf("xiaomi: Config.UUID is required")
|
||||
+ }
|
||||
+ uuid := cfg.UUID
|
||||
|
||||
- miot.NewMIoTClient("xiaomi_sdk", entryData, "", "cn", miot.CtrlModeAuto)
|
||||
+ miot.NewMIoTClient("xiaomi_sdk", entryData, cfg.UID, "cn", miot.CtrlModeAuto)
|
||||
```
|
||||
|
||||
### 3. 11 个示例文件 — 每个补两行
|
||||
|
||||
```
|
||||
xiaomi/examples/01_user_home/main.go
|
||||
xiaomi/examples/02_device_list/main.go
|
||||
xiaomi/examples/03_switch_control/main.go
|
||||
xiaomi/examples/04_light_control/main.go
|
||||
xiaomi/examples/05_ac_control/main.go
|
||||
xiaomi/examples/06_fan_control/main.go
|
||||
xiaomi/examples/07_prop_subscribe/main.go
|
||||
xiaomi/examples/08_adv_filter_classify/main.go
|
||||
xiaomi/examples/09_spec_parser/main.go
|
||||
xiaomi/examples/10_cover_control/main.go
|
||||
xiaomi/examples/11_sensor_control/main.go
|
||||
```
|
||||
|
||||
每个文件当前是:
|
||||
|
||||
```go
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
})
|
||||
```
|
||||
|
||||
改为:
|
||||
|
||||
```go
|
||||
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
|
||||
AccessToken: cfg.AuthInfo.AccessToken,
|
||||
RefreshToken: cfg.AuthInfo.RefreshToken,
|
||||
UUID: cfg.UUID,
|
||||
UID: cfg.UID,
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 调用方使用流程
|
||||
|
||||
```
|
||||
1. OAuth 前:生成 UUID(crypto/rand 16字节 hex),存库
|
||||
2. OAuth:获取 AccessToken + RefreshToken
|
||||
3. OAuth 后:调 GetHomeInfos 获取 UID,存库
|
||||
4. 每次创建 Client:从库读四个值传入 Config
|
||||
5. Token 刷新:调 RefreshToken(),新 AccessToken/RefreshToken 写回库
|
||||
UID 和 UUID 永不更新
|
||||
```
|
||||
+6
-2
@@ -5,15 +5,17 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"xiaomihome/miot"
|
||||
)
|
||||
|
||||
// TokenResult OAuth token 交换结果
|
||||
// TokenResult OAuth token 交换/刷新结果
|
||||
type TokenResult struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
ExpiresTS int64 `json:"expires_ts"`
|
||||
UID string `json:"uid"`
|
||||
}
|
||||
|
||||
@@ -30,10 +32,12 @@ func ExchangeCode(ctx context.Context, cfg Config, code string) (*TokenResult, e
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xiaomi: exchange token: %w", err)
|
||||
}
|
||||
expiresIn := intVal(result, "expires_in")
|
||||
return &TokenResult{
|
||||
AccessToken: strVal(result, "access_token"),
|
||||
RefreshToken: strVal(result, "refresh_token"),
|
||||
ExpiresIn: intVal(result, "expires_in"),
|
||||
ExpiresIn: expiresIn,
|
||||
ExpiresTS: time.Now().Unix() + int64(expiresIn),
|
||||
UID: strVal(result, "uid"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user