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:
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user