Files
xiaomihome/xiaomi/devices/fan.go
T
4566704 2b7de69403 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)
2026-09-15 20:16:26 +08:00

335 lines
9.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package devices
import (
"context"
"xiaomihome/miot"
"xiaomihome/xiaomi"
)
// Fan 风扇/新风机统一控制接口。
// 风扇和新风都有 mode 和 fan-level,但部分新风机没有 fan-level,此时用 mode 控制。
type Fan interface {
Device
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
IsOn(ctx context.Context) (bool, error)
// 风速控制(有 fan-level 时可用)
HasFanLevel() bool
FanLevelValues() []int
SetFanLevel(ctx context.Context, level int) error
GetFanLevel(ctx context.Context) (int, error)
// 模式控制(有 mode+ValueList 时可用,返回原始 uint8 值)
HasMode() bool
ModeValues() []int
SetMode(ctx context.Context, mode int) error
GetMode(ctx context.Context) (int, error)
SetOscillation(ctx context.Context, on bool) error
IsOscillating(ctx context.Context) (bool, error)
SetNaturalWind(ctx context.Context, on bool) error
IsNaturalWind(ctx context.Context) (bool, error)
OnPropsChanged(handler func(props FanProps, changed string)) (string, error)
GetProps() FanProps
FetchProps(ctx context.Context) (FanProps, error)
}
// FanProps 风扇/新风机完整状态(MQTT 缓存)
type FanProps struct {
On bool `json:"on"`
FanLevel int `json:"fanLevel"`
Mode int `json:"mode"`
Oscillation bool `json:"oscillation"`
NaturalWind bool `json:"naturalWind"`
}
type fanDevice struct {
BaseDevice
propOn siidPiid // "on"
propFanLevel siidPiid // "fan-level"
propMode siidPiid // "mode" — 有 ValueList 时为模式选择器
propNaturalWind siidPiid // "mode" — 无 ValueList 时为自然风开关
propOscillation siidPiid // "horizontal-swing" or "oscillation"
modeValues []int // 可用模式值列表(来自 SPEC value-list)
fanLevelValues []int // 可用风速档位值列表
propsCache FanProps
}
// NewFan creates a Fan control for the given device.
func NewFan(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Fan, error) {
d := &fanDevice{
BaseDevice: NewBaseDevice(client, info),
}
resolver := d.getSpecResolver()
if resolver == nil {
return nil, xiaomi.ErrNoSPEC
}
siid, piid, ok := resolver.find("on")
if !ok {
return nil, xiaomi.ErrNotSupported
}
d.propOn = siidPiid{siid, piid, true}
if s, p, ok := resolver.find("fan-level"); ok {
d.propFanLevel = siidPiid{s, p, true}
if prop := resolver.prop("fan-level"); prop != nil {
if len(prop.ValueList) > 0 {
for _, item := range prop.ValueList {
d.fanLevelValues = append(d.fanLevelValues, toInt(item.Value))
}
} else if prop.ValueRange != nil {
d.fanLevelValues = rangeValues(prop.ValueRange)
}
}
}
if s, p, ok := resolver.find("horizontal-swing"); ok {
d.propOscillation = siidPiid{s, p, true}
}
// mode 属性:有 ValueList → 模式选择器,有 ValueRange → 连续值,否则 → 自然风开关
if s, p, ok := resolver.find("mode"); ok {
if prop := resolver.prop("mode"); prop != nil {
if len(prop.ValueList) > 0 {
d.propMode = siidPiid{s, p, true}
for _, item := range prop.ValueList {
d.modeValues = append(d.modeValues, toInt(item.Value))
}
} else if prop.ValueRange != nil {
d.propMode = siidPiid{s, p, true}
d.modeValues = rangeValues(prop.ValueRange)
}
}
if !d.propMode.valid {
d.propNaturalWind = siidPiid{s, p, true}
}
}
return d, nil
}
func (d *fanDevice) TurnOn(ctx context.Context) error {
if !d.propOn.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, true)
}
func (d *fanDevice) TurnOff(ctx context.Context) error {
if !d.propOn.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, false)
}
func (d *fanDevice) IsOn(ctx context.Context) (bool, error) {
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
}
return false, nil
}
func (d *fanDevice) HasFanLevel() bool { return d.propFanLevel.valid }
func (d *fanDevice) FanLevelValues() []int { return d.fanLevelValues }
func (d *fanDevice) SetFanLevel(ctx context.Context, level int) error {
if !d.propFanLevel.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propFanLevel.siid, d.propFanLevel.piid, level)
}
func (d *fanDevice) GetFanLevel(ctx context.Context) (int, error) {
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
}
return toInt(prop.Value), nil
}
func (d *fanDevice) HasMode() bool { return d.propMode.valid }
func (d *fanDevice) ModeValues() []int { return d.modeValues }
func (d *fanDevice) SetMode(ctx context.Context, mode int) error {
if !d.propMode.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propMode.siid, d.propMode.piid, mode)
}
func (d *fanDevice) GetMode(ctx context.Context) (int, error) {
if !d.propMode.valid {
return 0, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propMode.siid, d.propMode.piid)
if err != nil {
return 0, err
}
return toInt(prop.Value), nil
}
func (d *fanDevice) SetOscillation(ctx context.Context, on bool) error {
if !d.propOscillation.valid {
return xiaomi.ErrNotSupported
}
return d.SetProp(ctx, d.propOscillation.siid, d.propOscillation.piid, on)
}
func (d *fanDevice) IsOscillating(ctx context.Context) (bool, error) {
if !d.propOscillation.valid {
return false, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propOscillation.siid, d.propOscillation.piid)
if err != nil {
return false, err
}
if b, ok := prop.Value.(bool); ok {
return b, nil
}
return false, nil
}
func (d *fanDevice) SetNaturalWind(ctx context.Context, on bool) error {
if !d.propNaturalWind.valid {
return xiaomi.ErrNotSupported
}
v := 0
if on {
v = 1
}
return d.SetProp(ctx, d.propNaturalWind.siid, d.propNaturalWind.piid, v)
}
func (d *fanDevice) IsNaturalWind(ctx context.Context) (bool, error) {
if !d.propNaturalWind.valid {
return false, xiaomi.ErrNotSupported
}
prop, err := d.GetProp(ctx, d.propNaturalWind.siid, d.propNaturalWind.piid)
if err != nil {
return false, err
}
return toInt(prop.Value) == 1, nil
}
func (d *fanDevice) OnPropsChanged(handler func(props FanProps, changed string)) (string, error) {
type field struct {
siid, piid int
name string
fill func(*FanProps, interface{})
}
var fields []field
if d.propOn.valid {
fields = append(fields, field{d.propOn.siid, d.propOn.piid, "on",
func(s *FanProps, v interface{}) { s.On, _ = v.(bool) }})
}
if d.propFanLevel.valid {
fields = append(fields, field{d.propFanLevel.siid, d.propFanLevel.piid, "fan-level",
func(s *FanProps, v interface{}) { s.FanLevel = toInt(v) }})
}
if d.propMode.valid {
fields = append(fields, field{d.propMode.siid, d.propMode.piid, "mode",
func(s *FanProps, v interface{}) { s.Mode = toInt(v) }})
}
if d.propOscillation.valid {
fields = append(fields, field{d.propOscillation.siid, d.propOscillation.piid, "oscillation",
func(s *FanProps, v interface{}) { s.Oscillation, _ = v.(bool) }})
}
if d.propNaturalWind.valid {
fields = append(fields, field{d.propNaturalWind.siid, d.propNaturalWind.piid, "natural-wind",
func(s *FanProps, 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.propsCache, prop.Value)
handler(d.propsCache, f.name)
return
}
}
})
}
func (d *fanDevice) GetProps() FanProps { return d.propsCache }
func (d *fanDevice) FetchProps(ctx context.Context) (FanProps, 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.propFanLevel.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propFanLevel.siid, PIID: d.propFanLevel.piid})
}
if d.propMode.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propMode.siid, PIID: d.propMode.piid})
}
if d.propOscillation.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propOscillation.siid, PIID: d.propOscillation.piid})
}
if d.propNaturalWind.valid {
keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propNaturalWind.siid, PIID: d.propNaturalWind.piid})
}
props, err := d.client.GetProps(ctx, keys)
if err != nil {
return d.propsCache, err
}
result := FanProps{}
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.propFanLevel.valid && p.SIID == d.propFanLevel.siid && p.PIID == d.propFanLevel.piid:
result.FanLevel = toInt(p.Value)
case d.propMode.valid && p.SIID == d.propMode.siid && p.PIID == d.propMode.piid:
result.Mode = toInt(p.Value)
case d.propOscillation.valid && p.SIID == d.propOscillation.siid && p.PIID == d.propOscillation.piid:
result.Oscillation, _ = p.Value.(bool)
case d.propNaturalWind.valid && p.SIID == d.propNaturalWind.siid && p.PIID == d.propNaturalWind.piid:
result.NaturalWind = toInt(p.Value) == 1
}
}
d.propsCache = result
return result, nil
}
// rangeValues 从 SPEC value-range 生成离散值列表,如 [1,8,1] → [1,2,3,4,5,6,7,8]
func rangeValues(r *miot.MIoTSpecValueRange) []int {
if r == nil {
return nil
}
min, max, step := toInt(r.Min), toInt(r.Max), toInt(r.Step)
if step <= 0 {
step = 1
}
if max > 100 {
return nil // 太大会爆内存,返回 nil 让调用方自行处理
}
var vals []int
for i := min; i <= max; i += step {
vals = append(vals, i)
}
return vals
}