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:
2026-06-30 12:16:49 +08:00
parent a3d94c4b9f
commit 8ffc9e4e44
33 changed files with 1019 additions and 593 deletions
+62 -19
View File
@@ -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
View File
@@ -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)
}
})
}
+45
View File
@@ -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
}
}
})
}
+53 -64
View File
@@ -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
View File
@@ -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)
})
}
+42 -57
View File
@@ -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)
})
}
+21 -8
View File
@@ -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
}
}
})
}
+44 -56
View File
@@ -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])
})
}
+55 -85
View File
@@ -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
View File
@@ -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
}
}
})
}
+56 -64
View File
@@ -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
}
}
})
}