package devices import ( "context" "git.zeroonesoft.cn/golib/xiaomihome/miot" "git.zeroonesoft.cn/golib/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 }