package devices import ( "context" "xiaomihome/xiaomi" ) // Fan 风扇统一控制接口 type Fan interface { Device TurnOn(ctx context.Context) error TurnOff(ctx context.Context) error IsOn(ctx context.Context) (bool, error) SetFanLevel(ctx context.Context, level int) error // 0=off, 1-4 档位 GetFanLevel(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) OnStateChanged(handler func(state FanState, changed string)) (string, error) GetState() FanState FetchState(ctx context.Context) (FanState, 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 { BaseDevice propOn siidPiid // "on" 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. 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 s, p, ok := resolver.find("horizontal-swing"); ok { d.propOscillation = siidPiid{s, p, true} } if s, p, ok := resolver.find("mode"); ok { 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) 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) 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 } mode := 0 // normal if on { mode = 1 // natural wind } return d.SetProp(ctx, d.propNaturalWind.siid, d.propNaturalWind.piid, mode) } 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) 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 } } }) } func (d *fanDevice) GetState() FanState { return d.stateCache } func (d *fanDevice) FetchState(ctx context.Context) (FanState, 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.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.stateCache, err } state := FanState{} for _, p := range props { switch { case d.propOn.valid && p.SIID == d.propOn.siid && p.PIID == d.propOn.piid: state.On, _ = p.Value.(bool) case d.propFanLevel.valid && p.SIID == d.propFanLevel.siid && p.PIID == d.propFanLevel.piid: state.FanLevel = toInt(p.Value) case d.propOscillation.valid && p.SIID == d.propOscillation.siid && p.PIID == d.propOscillation.piid: state.Oscillation, _ = p.Value.(bool) case d.propNaturalWind.valid && p.SIID == d.propNaturalWind.siid && p.PIID == d.propNaturalWind.piid: state.NaturalWind = toInt(p.Value) == 1 } } d.stateCache = state return state, nil }