package devices import ( "context" "xiaomihome/xiaomi" ) // ========== 空调模式/风速/摆风常量 ========== // ACMode 空调模式 type ACMode string const ( ModeCool ACMode = "cool" ModeHeat ACMode = "heat" ModeFan ACMode = "fan" ModeDry ACMode = "dry" ModeAuto ACMode = "auto" ) // modeValueMap 模式值映射(uint8 → ACMode) var modeValueMap = map[int]ACMode{ 0: ModeCool, 1: ModeHeat, 2: ModeFan, 3: ModeDry, 4: ModeAuto, } var modeReverseMap = map[ACMode]int{ ModeCool: 0, ModeHeat: 1, ModeFan: 2, ModeDry: 3, ModeAuto: 4, } // FanSpeed 风速 type FanSpeed string const ( SpeedLow FanSpeed = "low" SpeedMedium FanSpeed = "medium" SpeedHigh FanSpeed = "high" SpeedAuto FanSpeed = "auto" ) var fanSpeedValueMap = map[int]FanSpeed{ 0: SpeedLow, 1: SpeedMedium, 2: SpeedHigh, 3: SpeedAuto, } var fanSpeedReverseMap = map[FanSpeed]int{ SpeedLow: 0, SpeedMedium: 1, SpeedHigh: 2, SpeedAuto: 3, } // SwingMode 摆风模式 type SwingMode string const ( SwingOff SwingMode = "off" SwingVertical SwingMode = "vertical" SwingHorizontal SwingMode = "horizontal" SwingBoth SwingMode = "both" ) var swingValueMap = map[int]SwingMode{ 0: SwingOff, 1: SwingVertical, 2: SwingHorizontal, 3: SwingBoth, } var swingReverseMap = map[SwingMode]int{ SwingOff: 0, SwingVertical: 1, SwingHorizontal: 2, SwingBoth: 3, } // ========== AirConditioner 接口 ========== // AirConditioner 空调统一控制接口 type AirConditioner interface { Device TurnOn(ctx context.Context) error TurnOff(ctx context.Context) error IsOn(ctx context.Context) (bool, error) // 能力查询 HasFanSpeed() bool HasSwing() bool HasHumidity() bool HasAuxHeat() bool HasEco() bool // 模式控制 SetMode(ctx context.Context, mode ACMode) error GetMode(ctx context.Context) (ACMode, error) // 温度控制 SetTargetTemp(ctx context.Context, celsius float64) error GetTargetTemp(ctx context.Context) (float64, error) GetCurrentTemp(ctx context.Context) (float64, error) // 室内温度 // 风速控制 SetFanSpeed(ctx context.Context, speed FanSpeed) error GetFanSpeed(ctx context.Context) (FanSpeed, error) // 摆风控制 SetSwing(ctx context.Context, mode SwingMode) error GetSwing(ctx context.Context) (SwingMode, error) // 湿度 SetTargetHumidity(ctx context.Context, pct int) error GetTargetHumidity(ctx context.Context) (int, error) GetCurrentHumidity(ctx context.Context) (int, error) // 电辅热 SetAuxHeat(ctx context.Context, on bool) error GetAuxHeat(ctx context.Context) (bool, error) // 节能 SetEco(ctx context.Context, on bool) error GetEco(ctx context.Context) (bool, error) // 订阅 OnStateChanged(handler func(state ACState)) (string, error) } // ACState 空调完整状态 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"` } 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" } // NewAirConditioner creates an AirConditioner control for the given device. func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirConditioner, error) { d := &acDevice{ BaseDevice: NewBaseDevice(client, info), } resolver := d.getSpecResolver() if resolver == nil { return nil, xiaomi.ErrNoSPEC } // "on" is required siid, piid, ok := resolver.find("on") if !ok { return nil, xiaomi.ErrNotSupported } d.propOn = siidPiid{siid, piid, true} // Optional properties — each independently resolved if s, p, ok := resolver.find("mode"); ok { d.propMode = 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("fan-level"); ok { d.propFanSpeed = siidPiid{s, p, true} } // Try multiple swing mode type names if s, p, ok := resolver.find("swing-mode"); ok { d.propSwing = siidPiid{s, p, true} } else if s, p, ok := resolver.find("horizontal-swing"); ok { d.propSwing = siidPiid{s, p, true} } else if s, p, ok := resolver.find("vertical-swing"); ok { d.propSwing = 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("relative-humidity"); ok { d.propCurrentHumidity = siidPiid{s, p, true} } if s, p, ok := resolver.find("heater"); ok { d.propAuxHeat = siidPiid{s, p, true} } else if s, p, ok := resolver.find("aux-heat"); ok { d.propAuxHeat = siidPiid{s, p, true} } if s, p, ok := resolver.find("eco"); ok { d.propEco = siidPiid{s, p, true} } return d, nil } func (d *acDevice) 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 *acDevice) 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 *acDevice) 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 *acDevice) HasFanSpeed() bool { return d.propFanSpeed.valid } func (d *acDevice) HasSwing() bool { return d.propSwing.valid } func (d *acDevice) HasHumidity() bool { return d.propTargetHumidity.valid } func (d *acDevice) HasAuxHeat() bool { return d.propAuxHeat.valid } func (d *acDevice) HasEco() bool { return d.propEco.valid } func (d *acDevice) SetMode(ctx context.Context, mode ACMode) error { if !d.propMode.valid { return xiaomi.ErrNotSupported } val, ok := modeReverseMap[mode] if !ok { val = 4 // default auto } return d.SetProp(ctx, d.propMode.siid, d.propMode.piid, val) } func (d *acDevice) GetMode(ctx context.Context) (ACMode, error) { if !d.propMode.valid { return ModeAuto, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propMode.siid, d.propMode.piid) if err != nil { return ModeAuto, err } v := toInt(prop.Value) if m, ok := modeValueMap[v]; ok { return m, nil } return ModeAuto, nil } func (d *acDevice) 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 *acDevice) GetTargetTemp(ctx context.Context) (float64, error) { 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 } return toFloat(prop.Value), nil } func (d *acDevice) 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 *acDevice) SetFanSpeed(ctx context.Context, speed FanSpeed) error { if !d.propFanSpeed.valid { return xiaomi.ErrNotSupported } val, ok := fanSpeedReverseMap[speed] if !ok { val = 3 // default auto } return d.SetProp(ctx, d.propFanSpeed.siid, d.propFanSpeed.piid, val) } func (d *acDevice) GetFanSpeed(ctx context.Context) (FanSpeed, error) { if !d.propFanSpeed.valid { return SpeedAuto, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propFanSpeed.siid, d.propFanSpeed.piid) if err != nil { return SpeedAuto, err } v := toInt(prop.Value) if s, ok := fanSpeedValueMap[v]; ok { return s, nil } return SpeedAuto, nil } func (d *acDevice) SetSwing(ctx context.Context, mode SwingMode) error { if !d.propSwing.valid { return xiaomi.ErrNotSupported } val, ok := swingReverseMap[mode] if !ok { val = 0 } return d.SetProp(ctx, d.propSwing.siid, d.propSwing.piid, val) } func (d *acDevice) GetSwing(ctx context.Context) (SwingMode, error) { if !d.propSwing.valid { return SwingOff, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propSwing.siid, d.propSwing.piid) if err != nil { return SwingOff, err } v := toInt(prop.Value) if s, ok := swingValueMap[v]; ok { return s, nil } return SwingOff, nil } func (d *acDevice) SetTargetHumidity(ctx context.Context, pct int) error { if !d.propTargetHumidity.valid { return xiaomi.ErrNotSupported } return d.SetProp(ctx, d.propTargetHumidity.siid, d.propTargetHumidity.piid, pct) } func (d *acDevice) GetTargetHumidity(ctx context.Context) (int, error) { 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 } return toInt(prop.Value), nil } func (d *acDevice) GetCurrentHumidity(ctx context.Context) (int, error) { if !d.propCurrentHumidity.valid { return 0, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propCurrentHumidity.siid, d.propCurrentHumidity.piid) if err != nil { return 0, err } return toInt(prop.Value), nil } func (d *acDevice) SetAuxHeat(ctx context.Context, on bool) error { if !d.propAuxHeat.valid { return xiaomi.ErrNotSupported } return d.SetProp(ctx, d.propAuxHeat.siid, d.propAuxHeat.piid, on) } func (d *acDevice) GetAuxHeat(ctx context.Context) (bool, error) { if !d.propAuxHeat.valid { return false, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propAuxHeat.siid, d.propAuxHeat.piid) if err != nil { return false, err } if b, ok := prop.Value.(bool); ok { return b, nil } return false, nil } func (d *acDevice) SetEco(ctx context.Context, on bool) error { if !d.propEco.valid { return xiaomi.ErrNotSupported } return d.SetProp(ctx, d.propEco.siid, d.propEco.piid, on) } func (d *acDevice) GetEco(ctx context.Context) (bool, error) { if !d.propEco.valid { return false, xiaomi.ErrNotSupported } prop, err := d.GetProp(ctx, d.propEco.siid, d.propEco.piid) if err != nil { return false, err } if b, ok := prop.Value.(bool); ok { return b, nil } return false, nil } func (d *acDevice) OnStateChanged(handler func(state ACState)) (string, error) { if !d.propOn.valid { 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 } handler(state) }) } // toFloat converts an interface{} to float64. func toFloat(v interface{}) float64 { switch n := v.(type) { case float64: return n case float32: return float64(n) case int: return float64(n) case int64: return float64(n) } return 0 }