refactor(logger): 日志改用 logrus 全局包级调用,移除 Logger 注入接口

为什么:并入宿主项目后须与宿主日志体系一致——宿主统一用 pkg/logger 初始化
logrus 全局实例并直接 logrus.Xxx 包级调用;包一层 Logger 接口会让 logrus 的
caller 定位(报告调用文件:行号)失准。

改动:
- 删除 logger 包(Logger 接口 / Default / SetDefault,默认本就是 logrus.StandardLogger)
- 11 个结构体移除 lgr logger.Logger 字段、SetLogger 方法及构造中的 lgr 初始化
- 192 处 lgr.Xxxf 调用改为 logrus.Xxxf,日志消息文本保持不变
- xiaomi.Client 移除 Logger() 访问器,air_conditioner 回读日志去掉判空调用直连 logrus
- miot_i18n.go 两处标准库 log.Printf 改为 logrus.Errorf,统一日志出口
- go.mod:logrus 从 indirect 提升为直接依赖
- goimports/goformat 全树规范化:此前部分文件未 gofmt(单行 if、对齐),本次顺带
  格式化,纯空白/换行变化,无逻辑改动

验证:go build / go vet / go test ./... 全部通过(bridge、miot、xiaomi、devices、specs)
This commit is contained in:
2026-09-15 20:16:26 +08:00
parent 7ae32bd012
commit 2b7de69403
46 changed files with 1166 additions and 903 deletions
+43 -51
View File
@@ -9,6 +9,8 @@ import (
"xiaomihome/miot"
"xiaomihome/xiaomi"
"github.com/sirupsen/logrus"
)
// valMapper maps between device raw values and semantic names. Built from SPEC ValueList.
@@ -36,8 +38,8 @@ func newValMapper(vl miot.MIoTSpecValueList) *valMapper {
display = item.Name
}
key := strings.ToLower(display)
vm.val2name[v] = display // 原始描述做展示
vm.name2val[key] = v // 小写 key 做匹配
vm.val2name[v] = display // 原始描述做展示
vm.name2val[key] = v // 小写 key 做匹配
vm.names = append(vm.names, display)
}
return vm
@@ -122,7 +124,9 @@ func (vm *valMapper) levelValues() []int {
}
func (vm *valMapper) allNames() []string {
if vm == nil { return nil }
if vm == nil {
return nil
}
return vm.names
}
@@ -207,7 +211,7 @@ type AirConditioner interface {
// 功率
HasPower() bool
GetElectricPower(ctx context.Context) (float64, error) // 当前电功率(W)
GetElectricPower(ctx context.Context) (float64, error) // 当前电功率(W)
GetPowerConsumption(ctx context.Context) (float64, error) // 累计耗电量(kWh)
// 订阅
@@ -218,47 +222,47 @@ type AirConditioner interface {
// ACProps 空调完整状态(字段名对齐 SPEC type)
type ACProps struct {
On bool `json:"on"`
Mode string `json:"mode"` // SPEC ValueList 解析后的名称:cool/heat/fan/dry/auto
On bool `json:"on"`
Mode string `json:"mode"` // SPEC ValueList 解析后的名称:cool/heat/fan/dry/auto
TargetTemperature float64 `json:"targetTemperature"`
Temperature float64 `json:"temperature"`
TargetHumidity int `json:"targetHumidity"`
CurrentHumidity int `json:"currentHumidity"`
FanLevel string `json:"fanLevel"` // SPEC ValueList 解析后的名称:low/medium/high/auto
Swing string `json:"swing"` // SPEC ValueList 解析后的名称:off/vertical/horizontal/both
AuxHeat bool `json:"auxHeat"`
Eco bool `json:"eco"`
ElectricPower float64 `json:"electricPower"` // 当前电功率(W),来自 electric-power
PowerConsumption float64 `json:"powerConsumption"` // 累计耗电量(kWh),来自 power-consumption
TargetHumidity int `json:"targetHumidity"`
CurrentHumidity int `json:"currentHumidity"`
FanLevel string `json:"fanLevel"` // SPEC ValueList 解析后的名称:low/medium/high/auto
Swing string `json:"swing"` // SPEC ValueList 解析后的名称:off/vertical/horizontal/both
AuxHeat bool `json:"auxHeat"`
Eco bool `json:"eco"`
ElectricPower float64 `json:"electricPower"` // 当前电功率(W),来自 electric-power
PowerConsumption float64 `json:"powerConsumption"` // 累计耗电量(kWh),来自 power-consumption
}
type acDevice struct {
BaseDevice
propOn siidPiid // "on"
propMode siidPiid // "mode"
propTargetTemp siidPiid // "target-temperature"
propCurrentTemp siidPiid // "temperature"
propFanSpeed siidPiid // "fan-level"
propSwing siidPiid // "swing-mode" or "horizontal-swing" or "vertical-swing"
propTargetHumidity siidPiid // "target-humidity"
propCurrentHumidity siidPiid // "relative-humidity"
propAuxHeat siidPiid // "aux-heat" or "heater"
propEco siidPiid // "eco"
propElectricPower siidPiid // "electric-power"
propOn siidPiid // "on"
propMode siidPiid // "mode"
propTargetTemp siidPiid // "target-temperature"
propCurrentTemp siidPiid // "temperature"
propFanSpeed siidPiid // "fan-level"
propSwing siidPiid // "swing-mode" or "horizontal-swing" or "vertical-swing"
propTargetHumidity siidPiid // "target-humidity"
propCurrentHumidity siidPiid // "relative-humidity"
propAuxHeat siidPiid // "aux-heat" or "heater"
propEco siidPiid // "eco"
propElectricPower siidPiid // "electric-power"
propPowerConsumption siidPiid // "power-consumption"
modeMap *valMapper // SPEC value→name for mode
fanMap *valMapper // SPEC value→name for fan-level
modeMap *valMapper // SPEC value→name for mode
fanMap *valMapper // SPEC value→name for fan-level
swingMap *valMapper // SPEC value→name for swing
propsCache ACProps // MQTT 推送维护的本地缓存
// set 后回读(验证设置是否成功):记录目标值,防抖 + 重试比较
rbMu sync.Mutex
rbPending bool // 是否有回读调度在跑
rbLastSet time.Time // 最近一次 set 时间
rbTargets []rbTarget // 本次待验证的目标(聚合连续 set)
rbHandler func(props ACProps, changed string) // OnPropsChanged 注册的 handler
rbPending bool // 是否有回读调度在跑
rbLastSet time.Time // 最近一次 set 时间
rbTargets []rbTarget // 本次待验证的目标(聚合连续 set)
rbHandler func(props ACProps, changed string) // OnPropsChanged 注册的 handler
}
// rbTarget 回读验证目标:某属性本次 set 的目标值。
@@ -729,9 +733,7 @@ func (d *acDevice) recordReadback(name string, want interface{}) {
}
d.rbPending = true
d.rbMu.Unlock()
if d.client.Logger() != nil {
d.client.Logger().Debugf("[acDevice] readback scheduled: %s (%s) targets=%v\n", d.info.DID, d.info.Model, d.rbTargetNames())
}
logrus.Debugf("[acDevice] readback scheduled: %s (%s) targets=%v\n", d.info.DID, d.info.Model, d.rbTargetNames())
go d.readbackLoop()
}
@@ -768,9 +770,7 @@ func (d *acDevice) readbackLoop() {
}
props, err := d.FetchProps(ctx)
if err != nil {
if d.client.Logger() != nil {
d.client.Logger().Warnf("[acDevice] readback %s attempt %d failed: %v\n", d.info.DID, attempt, err)
}
logrus.Warnf("[acDevice] readback %s attempt %d failed: %v\n", d.info.DID, attempt, err)
if ctx.Err() != nil {
d.finishReadback("timeout")
return
@@ -782,9 +782,7 @@ func (d *acDevice) readbackLoop() {
d.finishReadbackOK(props, targets)
return
}
if d.client.Logger() != nil {
d.client.Logger().Debugf("[acDevice] readback %s attempt %d not applied yet: missed=%v\n", d.info.DID, attempt, missed)
}
logrus.Debugf("[acDevice] readback %s attempt %d not applied yet: missed=%v\n", d.info.DID, attempt, missed)
if attempt >= readbackMaxRetries {
d.finishReadback("not applied after retries")
return
@@ -810,9 +808,7 @@ func (d *acDevice) finishReadbackOK(props ACProps, targets []rbTarget) {
handler := d.rbHandler
d.rbMu.Unlock()
if handler == nil {
if d.client.Logger() != nil {
d.client.Logger().Warnf("[acDevice] readback %s done but handler not registered (OnPropsChanged never called)\n", d.info.DID)
}
logrus.Warnf("[acDevice] readback %s done but handler not registered (OnPropsChanged never called)\n", d.info.DID)
d.rbMu.Lock()
d.rbPending = false
d.rbMu.Unlock()
@@ -821,11 +817,9 @@ func (d *acDevice) finishReadbackOK(props ACProps, targets []rbTarget) {
for _, t := range targets {
handler(props, t.name)
}
if d.client.Logger() != nil {
d.client.Logger().Debugf("[acDevice] readback %s success: %v on=%v mode=%s target=%.0f temp=%.0f fan=%s hum=%d\n",
d.info.DID, d.rbTargetNamesLocked(), props.On, props.Mode, props.TargetTemperature,
props.Temperature, props.FanLevel, props.CurrentHumidity)
}
logrus.Debugf("[acDevice] readback %s success: %v on=%v mode=%s target=%.0f temp=%.0f fan=%s hum=%d\n",
d.info.DID, d.rbTargetNamesLocked(), props.On, props.Mode, props.TargetTemperature,
props.Temperature, props.FanLevel, props.CurrentHumidity)
d.rbMu.Lock()
d.rbPending = false
d.rbMu.Unlock()
@@ -838,9 +832,7 @@ func (d *acDevice) finishReadback(reason string) {
d.rbTargets = nil
d.rbPending = false
d.rbMu.Unlock()
if d.client.Logger() != nil {
d.client.Logger().Warnf("[acDevice] readback %s finished: %s targets=%v\n", d.info.DID, reason, names)
}
logrus.Warnf("[acDevice] readback %s finished: %s targets=%v\n", d.info.DID, reason, names)
}
func (d *acDevice) rbTargetNames() []string {
+24 -8
View File
@@ -71,15 +71,21 @@ func NewCover(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Cover, error) {
}
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)
}
@@ -96,9 +102,13 @@ func (d *coverDevice) SetPosition(ctx context.Context, pct int) error {
}
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
}
@@ -125,9 +135,15 @@ func (d *coverDevice) GetProps() CoverProps { return d.propsCache }
func (d *coverDevice) FetchProps(ctx context.Context) (CoverProps, error) {
keys := []xiaomi.PropKey{}
if d.propPosition.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propPosition.siid, PIID: d.propPosition.piid}) }
if d.propStatus.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propStatus.siid, PIID: d.propStatus.piid}) }
if d.propFault.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propFault.siid, PIID: d.propFault.piid}) }
if d.propPosition.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propPosition.siid, PIID: d.propPosition.piid})
}
if d.propStatus.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propStatus.siid, PIID: d.propStatus.piid})
}
if d.propFault.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propFault.siid, PIID: d.propFault.piid})
}
if len(keys) == 0 {
return d.propsCache, xiaomi.ErrNotSupported
}
+9 -2
View File
@@ -209,7 +209,11 @@ func TestValMapperLevelFallback(t *testing.T) {
{Value: 3, Desc: "Level3"}, {Value: 4, Desc: "Level4"}, {Value: 5, Desc: "Level5"},
}
vm := newValMapperLevel(vl)
cases := []struct{ name string; want int; ok bool }{
cases := []struct {
name string
want int
ok bool
}{
{"auto", 0, true}, {"Auto", 0, true},
{"low", 1, true}, {"medium", 3, true}, {"high", 5, true},
{"level3", 3, true},
@@ -221,7 +225,10 @@ func TestValMapperLevelFallback(t *testing.T) {
}
}
// 反向解析(MQTT 上报值 → 语义枚举),与 val() 正向对称
rname := []struct{ v int; want string }{
rname := []struct {
v int
want string
}{
{0, "auto"}, {1, "low"}, {3, "medium"}, {5, "high"}, {2, "Level2"},
}
for _, c := range rname {
+2 -2
View File
@@ -55,8 +55,8 @@ type fanDevice struct {
propNaturalWind siidPiid // "mode" — 无 ValueList 时为自然风开关
propOscillation siidPiid // "horizontal-swing" or "oscillation"
modeValues []int // 可用模式值列表(来自 SPEC value-list)
fanLevelValues []int // 可用风速档位值列表
modeValues []int // 可用模式值列表(来自 SPEC value-list)
fanLevelValues []int // 可用风速档位值列表
propsCache FanProps
}
+71 -24
View File
@@ -39,53 +39,87 @@ type humidifierDevice struct {
func NewHumidifier(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Humidifier, error) {
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) OnPropsChanged(handler func(props HumidifierProps, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*HumidifierProps, interface{}) }
type field struct {
siid, piid int
name string
fill func(*HumidifierProps, interface{})
}
var fields []field
if d.propOn.valid {
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *HumidifierProps, v interface{}) { s.On, _ = v.(bool) }})
@@ -96,7 +130,9 @@ func (d *humidifierDevice) OnPropsChanged(handler func(props HumidifierProps, ch
if d.propFanLevel.valid {
fields = append(fields, field{d.propFanLevel.siid, d.propFanLevel.piid, "fan-level", func(s *HumidifierProps, v interface{}) { s.FanLevel = toInt(v) }})
}
if len(fields) == 0 { return "", xiaomi.ErrNotSupported }
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 {
@@ -111,17 +147,28 @@ func (d *humidifierDevice) OnPropsChanged(handler func(props HumidifierProps, ch
func (d *humidifierDevice) GetProps() HumidifierProps { return d.propsCache }
func (d *humidifierDevice) FetchProps(ctx context.Context) (HumidifierProps, error) {
keys := []xiaomi.PropKey{}
if d.propOn.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid}) }
if d.propTargetHumidity.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propTargetHumidity.siid, PIID: d.propTargetHumidity.piid}) }
if d.propFanLevel.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propFanLevel.siid, PIID: d.propFanLevel.piid}) }
if d.propOn.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid})
}
if d.propTargetHumidity.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propTargetHumidity.siid, PIID: d.propTargetHumidity.piid})
}
if d.propFanLevel.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propFanLevel.siid, PIID: d.propFanLevel.piid})
}
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.propsCache, err }
if err != nil {
return d.propsCache, err
}
result := HumidifierProps{}
for _, p := range props {
switch {
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: result.On, _ = p.Value.(bool)
case d.propTargetHumidity.valid && p.SIID == d.propTargetHumidity.siid && p.PIID == d.propTargetHumidity.piid: result.TargetHumidity = toInt(p.Value)
case d.propFanLevel.valid && p.SIID == d.propFanLevel.siid && p.PIID == d.propFanLevel.piid: result.FanLevel = toInt(p.Value)
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid:
result.On, _ = p.Value.(bool)
case d.propTargetHumidity.valid && p.SIID == d.propTargetHumidity.siid && p.PIID == d.propTargetHumidity.piid:
result.TargetHumidity = toInt(p.Value)
case d.propFanLevel.valid && p.SIID == d.propFanLevel.siid && p.PIID == d.propFanLevel.piid:
result.FanLevel = toInt(p.Value)
}
}
d.propsCache = result
+29 -13
View File
@@ -38,10 +38,10 @@ type Light interface {
// LightProps 灯光完整状态(字段名对齐 SPEC type)
type LightProps struct {
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"
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 {
@@ -252,20 +252,36 @@ func toInt(v interface{}) int {
func (d *lightDevice) GetProps() LightProps { return d.propsCache }
func (d *lightDevice) FetchProps(ctx context.Context) (LightProps, error) {
keys := []xiaomi.PropKey{}
if d.propOn.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid}) }
if d.propBrightness.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propBrightness.siid, PIID: d.propBrightness.piid}) }
if d.propColorTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propColorTemp.siid, PIID: d.propColorTemp.piid}) }
if d.propColor.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propColor.siid, PIID: d.propColor.piid}) }
if d.propOn.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid})
}
if d.propBrightness.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propBrightness.siid, PIID: d.propBrightness.piid})
}
if d.propColorTemp.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propColorTemp.siid, PIID: d.propColorTemp.piid})
}
if d.propColor.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propColor.siid, PIID: d.propColor.piid})
}
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.propsCache, err }
if err != nil {
return d.propsCache, err
}
result := LightProps{}
for _, p := range props {
switch {
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: result.On, _ = p.Value.(bool)
case d.propBrightness.valid && p.SIID == d.propBrightness.siid && p.PIID == d.propBrightness.piid: result.Brightness = toInt(p.Value)
case d.propColorTemp.valid && p.SIID == d.propColorTemp.siid && p.PIID == d.propColorTemp.piid: result.ColorTemperature = toInt(p.Value)
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid:
result.On, _ = p.Value.(bool)
case d.propBrightness.valid && p.SIID == d.propBrightness.siid && p.PIID == d.propBrightness.piid:
result.Brightness = toInt(p.Value)
case d.propColorTemp.valid && p.SIID == d.propColorTemp.siid && p.PIID == d.propColorTemp.piid:
result.ColorTemperature = toInt(p.Value)
case d.propColor.valid && p.SIID == d.propColor.siid && p.PIID == d.propColor.piid:
if v, ok := p.Value.(float64); ok { c := int(v); result.ColorRGB = [3]int{(c>>16)&0xFF, (c>>8)&0xFF, c&0xFF} }
if v, ok := p.Value.(float64); ok {
c := int(v)
result.ColorRGB = [3]int{(c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF}
}
}
}
d.propsCache = result
+40 -16
View File
@@ -20,9 +20,9 @@ type OccupancySensor interface {
type occupancyDevice struct {
BaseDevice
propOccupancy siidPiid // "occupancy" / "occupancy-status"(bool/uint8)
propNoMotion siidPiid // "no-motion-duration"(uint16,linp.motion.* 等无 occupancy 属性的设备)
propIlluminance siidPiid // "illumination"
propOccupancy siidPiid // "occupancy" / "occupancy-status"(bool/uint8)
propNoMotion siidPiid // "no-motion-duration"(uint16,linp.motion.* 等无 occupancy 属性的设备)
propIlluminance siidPiid // "illumination"
propsCache [2]interface{} // [0]=bool occupied, [1]=float64 lux
}
@@ -35,21 +35,33 @@ func (d *occupancyDevice) occupied(v interface{}) bool {
}
func NewOccupancySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (OccupancySensor, error) {
if info == nil { return nil, xiaomi.ErrDeviceNotFound }
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) }
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) }
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", "occupancy-status":
if !d.propOccupancy.valid { d.propOccupancy = siidPiid{svc.IID, prop.PIID, true} }
if !d.propOccupancy.valid {
d.propOccupancy = siidPiid{svc.IID, prop.PIID, true}
}
case "no-motion-duration":
if !d.propNoMotion.valid { d.propNoMotion = siidPiid{svc.IID, prop.PIID, true} }
if !d.propNoMotion.valid {
d.propNoMotion = siidPiid{svc.IID, prop.PIID, true}
}
case "illumination":
if !d.propIlluminance.valid { d.propIlluminance = siidPiid{svc.IID, prop.PIID, true} }
if !d.propIlluminance.valid {
d.propIlluminance = siidPiid{svc.IID, prop.PIID, true}
}
}
}
}
@@ -70,18 +82,26 @@ func (d *occupancyDevice) occupiedProp() siidPiid {
func (d *occupancyDevice) IsOccupied(ctx context.Context) (bool, error) {
p := d.occupiedProp()
prop, err := d.GetProp(ctx, p.siid, p.piid)
if err != nil { return false, err }
if err != nil {
return false, err
}
return d.occupied(prop.Value), 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) OnPropsChanged(handler func(occupied bool, illuminance float64)) (string, error) {
if !d.propOccupancy.valid && !d.propNoMotion.valid && !d.propIlluminance.valid { return "", xiaomi.ErrNotSupported }
if !d.propOccupancy.valid && !d.propNoMotion.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:
@@ -97,7 +117,7 @@ func (d *occupancyDevice) OnPropsChanged(handler func(occupied bool, illuminance
})
}
func (d *occupancyDevice) GetProps() (bool, float64) {
func (d *occupancyDevice) GetProps() (bool, float64) {
b, _ := d.propsCache[0].(bool)
f, _ := d.propsCache[1].(float64)
return b, f
@@ -105,8 +125,12 @@ func (d *occupancyDevice) GetProps() (bool, float64) {
func (d *occupancyDevice) FetchProps(ctx context.Context) (bool, float64, error) {
keys := []xiaomi.PropKey{}
p := d.occupiedProp()
if p.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: p.siid, PIID: p.piid}) }
if d.propIlluminance.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propIlluminance.siid, PIID: d.propIlluminance.piid}) }
if p.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: p.siid, PIID: p.piid})
}
if d.propIlluminance.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propIlluminance.siid, PIID: d.propIlluminance.piid})
}
props, err := d.client.GetProps(ctx, keys)
if err != nil {
b, _ := d.propsCache[0].(bool)
+13 -13
View File
@@ -4,19 +4,19 @@ package devices
// 用于 bridge.OnPropsChanged 的 deviceType 字段 / factory 分类
const (
TypeAirConditioner = "air_conditioner" // 空调
TypeLight = "light" // 灯
TypeSwitch = "switch" // 开关
TypeFan = "fan" // 风扇
TypeCover = "cover" // 窗帘/遮阳帘
TypeHumidifier = "humidifier" // 加湿器
TypeVacuum = "vacuum" // 吸尘器/扫地机器人
TypeWaterHeater = "water_heater" // 热水器
TypeOccupancySensor = "sensor_occupy" // 人体传感器
TypeTempHumidSensor = "sensor" // 温湿度传感器(官方 URN type)
TypeSpeaker = "speaker" // 音箱
TypeGateway = "gateway" // 网关
TypePlug = "plug" // 插座
TypeAirConditioner = "air_conditioner" // 空调
TypeLight = "light" // 灯
TypeSwitch = "switch" // 开关
TypeFan = "fan" // 风扇
TypeCover = "cover" // 窗帘/遮阳帘
TypeHumidifier = "humidifier" // 加湿器
TypeVacuum = "vacuum" // 吸尘器/扫地机器人
TypeWaterHeater = "water_heater" // 热水器
TypeOccupancySensor = "sensor_occupy" // 人体传感器
TypeTempHumidSensor = "sensor" // 温湿度传感器(官方 URN type)
TypeSpeaker = "speaker" // 音箱
TypeGateway = "gateway" // 网关
TypePlug = "plug" // 插座
)
+4 -4
View File
@@ -80,11 +80,11 @@ type speakerDevice struct {
isSiid int
// 动作
actWakeUp isAction
actPlayRadio isAction
actPlayText isAction
actWakeUp isAction
actPlayRadio isAction
actPlayText isAction
actExecuteTextDirective isAction
actPlayMusic isAction
actPlayMusic isAction
propsCache SpeakerProps
}
+43 -15
View File
@@ -26,19 +26,29 @@ type tempHumidityDevice struct {
}
func NewTempHumiditySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (TempHumiditySensor, error) {
if info == nil { return nil, xiaomi.ErrDeviceNotFound }
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) }
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) }
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} }
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.propHumidity.valid {
d.propHumidity = siidPiid{svc.IID, prop.PIID, true}
}
}
}
}
@@ -49,20 +59,30 @@ func NewTempHumiditySensor(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Temp
}
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 }
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 }
if err != nil {
return 0, err
}
return toFloat(prop.Value), nil
}
func (d *tempHumidityDevice) OnPropsChanged(handler func(temp, humidity float64)) (string, error) {
if !d.propTemp.valid && !d.propHumidity.valid { return "", xiaomi.ErrNotSupported }
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:
@@ -77,15 +97,23 @@ func (d *tempHumidityDevice) OnPropsChanged(handler func(temp, humidity float64)
func (d *tempHumidityDevice) GetProps() (float64, float64) { return d.propsCache[0], d.propsCache[1] }
func (d *tempHumidityDevice) FetchProps(ctx context.Context) (float64, float64, error) {
keys := []xiaomi.PropKey{}
if d.propTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propTemp.siid, PIID: d.propTemp.piid}) }
if d.propHumidity.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propHumidity.siid, PIID: d.propHumidity.piid}) }
if d.propTemp.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propTemp.siid, PIID: d.propTemp.piid})
}
if d.propHumidity.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propHumidity.siid, PIID: d.propHumidity.piid})
}
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.propsCache[0], d.propsCache[1], err }
if err != nil {
return d.propsCache[0], d.propsCache[1], err
}
state := [2]float64{d.propsCache[0], d.propsCache[1]}
for _, p := range props {
switch {
case d.propTemp.valid && p.SIID == d.propTemp.siid && p.PIID == d.propTemp.piid: state[0] = toFloat(p.Value)
case d.propHumidity.valid && p.SIID == d.propHumidity.siid && p.PIID == d.propHumidity.piid: state[1] = toFloat(p.Value)
case d.propTemp.valid && p.SIID == d.propTemp.siid && p.PIID == d.propTemp.piid:
state[0] = toFloat(p.Value)
case d.propHumidity.valid && p.SIID == d.propHumidity.siid && p.PIID == d.propHumidity.piid:
state[1] = toFloat(p.Value)
}
}
d.propsCache = state
+83 -28
View File
@@ -34,61 +34,103 @@ type thermostatDevice struct {
func NewThermostat(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Thermostat, error) {
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("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, temp float64) error {
if !d.propTargetTemp.valid { return xiaomi.ErrNotSupported }
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) OnPropsChanged(handler func(props HeaterProps, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*HeaterProps, interface{}) }
type field struct {
siid, piid int
name string
fill func(*HeaterProps, interface{})
}
var fields []field
if d.propOn.valid {
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *HeaterProps, v interface{}) { s.On, _ = v.(bool) }})
@@ -99,7 +141,9 @@ func (d *thermostatDevice) OnPropsChanged(handler func(props HeaterProps, change
if d.propCurrentTemp.valid {
fields = append(fields, field{d.propCurrentTemp.siid, d.propCurrentTemp.piid, "temperature", func(s *HeaterProps, v interface{}) { s.CurrentTemp = toFloat(v) }})
}
if len(fields) == 0 { return "", xiaomi.ErrNotSupported }
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 {
@@ -114,17 +158,28 @@ func (d *thermostatDevice) OnPropsChanged(handler func(props HeaterProps, change
func (d *thermostatDevice) GetProps() HeaterProps { return d.propsCache }
func (d *thermostatDevice) FetchProps(ctx context.Context) (HeaterProps, error) {
keys := []xiaomi.PropKey{}
if d.propOn.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid}) }
if d.propTargetTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propTargetTemp.siid, PIID: d.propTargetTemp.piid}) }
if d.propCurrentTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propCurrentTemp.siid, PIID: d.propCurrentTemp.piid}) }
if d.propOn.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid})
}
if d.propTargetTemp.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propTargetTemp.siid, PIID: d.propTargetTemp.piid})
}
if d.propCurrentTemp.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propCurrentTemp.siid, PIID: d.propCurrentTemp.piid})
}
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.propsCache, err }
if err != nil {
return d.propsCache, err
}
result := HeaterProps{}
for _, p := range props {
switch {
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: result.On, _ = p.Value.(bool)
case d.propTargetTemp.valid && p.SIID == d.propTargetTemp.siid && p.PIID == d.propTargetTemp.piid: result.TargetTemp = toFloat(p.Value)
case d.propCurrentTemp.valid && p.SIID == d.propCurrentTemp.siid && p.PIID == d.propCurrentTemp.piid: result.CurrentTemp = toFloat(p.Value)
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid:
result.On, _ = p.Value.(bool)
case d.propTargetTemp.valid && p.SIID == d.propTargetTemp.siid && p.PIID == d.propTargetTemp.piid:
result.TargetTemp = toFloat(p.Value)
case d.propCurrentTemp.valid && p.SIID == d.propCurrentTemp.siid && p.PIID == d.propCurrentTemp.piid:
result.CurrentTemp = toFloat(p.Value)
}
}
d.propsCache = result
+45 -14
View File
@@ -40,13 +40,23 @@ type vacuumDevice struct {
func NewVacuum(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Vacuum, error) {
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
}
@@ -63,24 +73,37 @@ func (d *vacuumDevice) ReturnToDock(ctx context.Context) error {
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) (VacuumProps, 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}) }
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)
add(d.propOn)
add(d.propMode)
add(d.propFanLevel)
add(d.propBattery)
props, err := d.client.GetProps(ctx, keys)
if err != nil { return VacuumProps{}, err }
if err != nil {
return VacuumProps{}, err
}
status := VacuumProps{}
for _, p := range props {
switch {
@@ -98,7 +121,11 @@ func (d *vacuumDevice) GetStatus(ctx context.Context) (VacuumProps, error) {
}
func (d *vacuumDevice) OnPropsChanged(handler func(props VacuumProps, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*VacuumProps, interface{}) }
type field struct {
siid, piid int
name string
fill func(*VacuumProps, interface{})
}
var fields []field
if d.propOn.valid {
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *VacuumProps, v interface{}) { s.On, _ = v.(bool) }})
@@ -112,7 +139,9 @@ func (d *vacuumDevice) OnPropsChanged(handler func(props VacuumProps, changed st
if d.propBattery.valid {
fields = append(fields, field{d.propBattery.siid, d.propBattery.piid, "battery", func(s *VacuumProps, v interface{}) { s.Battery = toInt(v) }})
}
if len(fields) == 0 { return "", xiaomi.ErrNotSupported }
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 {
@@ -127,7 +156,9 @@ func (d *vacuumDevice) OnPropsChanged(handler func(props VacuumProps, changed st
func (d *vacuumDevice) GetProps() VacuumProps { return d.propsCache }
func (d *vacuumDevice) FetchProps(ctx context.Context) (VacuumProps, error) {
status, err := d.GetStatus(ctx)
if err != nil { return d.propsCache, err }
if err != nil {
return d.propsCache, err
}
d.propsCache = status
return status, nil
}
+68 -23
View File
@@ -38,49 +38,81 @@ type waterHeaterDevice struct {
func NewWaterHeater(client *xiaomi.Client, info *xiaomi.DeviceInfo) (WaterHeater, error) {
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, temp float64) error {
if !d.propTargetTemp.valid { return xiaomi.ErrNotSupported }
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 }
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 *waterHeaterDevice) OnPropsChanged(handler func(props HeaterProps, changed string)) (string, error) {
type field struct{ siid, piid int; name string; fill func(*HeaterProps, interface{}) }
type field struct {
siid, piid int
name string
fill func(*HeaterProps, interface{})
}
var fields []field
if d.propOn.valid {
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on", func(s *HeaterProps, v interface{}) { s.On, _ = v.(bool) }})
@@ -91,7 +123,9 @@ func (d *waterHeaterDevice) OnPropsChanged(handler func(props HeaterProps, chang
if d.propCurrentTemp.valid {
fields = append(fields, field{d.propCurrentTemp.siid, d.propCurrentTemp.piid, "temperature", func(s *HeaterProps, v interface{}) { s.CurrentTemp = toFloat(v) }})
}
if len(fields) == 0 { return "", xiaomi.ErrNotSupported }
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 {
@@ -106,17 +140,28 @@ func (d *waterHeaterDevice) OnPropsChanged(handler func(props HeaterProps, chang
func (d *waterHeaterDevice) GetProps() HeaterProps { return d.propsCache }
func (d *waterHeaterDevice) FetchProps(ctx context.Context) (HeaterProps, error) {
keys := []xiaomi.PropKey{}
if d.propOn.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid}) }
if d.propTargetTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propTargetTemp.siid, PIID: d.propTargetTemp.piid}) }
if d.propCurrentTemp.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propCurrentTemp.siid, PIID: d.propCurrentTemp.piid}) }
if d.propOn.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid})
}
if d.propTargetTemp.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propTargetTemp.siid, PIID: d.propTargetTemp.piid})
}
if d.propCurrentTemp.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propCurrentTemp.siid, PIID: d.propCurrentTemp.piid})
}
props, err := d.client.GetProps(ctx, keys)
if err != nil { return d.propsCache, err }
if err != nil {
return d.propsCache, err
}
result := HeaterProps{}
for _, p := range props {
switch {
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: result.On, _ = p.Value.(bool)
case d.propTargetTemp.valid && p.SIID == d.propTargetTemp.siid && p.PIID == d.propTargetTemp.piid: result.TargetTemp = toFloat(p.Value)
case d.propCurrentTemp.valid && p.SIID == d.propCurrentTemp.siid && p.PIID == d.propCurrentTemp.piid: result.CurrentTemp = toFloat(p.Value)
case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid:
result.On, _ = p.Value.(bool)
case d.propTargetTemp.valid && p.SIID == d.propTargetTemp.siid && p.PIID == d.propTargetTemp.piid:
result.TargetTemp = toFloat(p.Value)
case d.propCurrentTemp.valid && p.SIID == d.propCurrentTemp.siid && p.PIID == d.propCurrentTemp.piid:
result.CurrentTemp = toFloat(p.Value)
}
}
d.propsCache = result