Files
xiaomihome/xiaomi/devices/air_conditioner.go
T
4566704 a3d94c4b9f chore: 提交高层 xiaomi 封装库及示例
- 新增 xiaomi/examples/ 下 8 个示例程序(用户/家庭查询、设备列表、开关/灯光/空调控制、属性订阅、高级过滤分类、SPEC 解析)
- miot_client_sub.go: 重构 SubProp/SubEvent,使用 buildPropTopic/buildEventTopic 支持通配符订阅(siid/piid=0 → +),并修复锁顺序问题(将 RequestRefreshProp 移到 Lock 外)
- spec_parser.go: 新增 downloadSpecFile 方法,本地 SPEC 文件缺失时自动从 miot-spec.org 下载
- ARCH_PLAN.md: 架构设计从 Proposed 更新为 Accepted(v1.0→v1.1),补充设备分类/工厂/SPEC 映射等模块设计
- xiaomi/: 新增 miot 上层强类型封装模块,包含 Client 主入口、用户/家庭/设备 API、属性读写、动作调用、订阅通知,以及 devices/ 设备控制抽象(Switch/Light/AirConditioner/Fan/Cover/Humidifier/Vacuum/WaterHeater/Thermostat)和 specs/ SPEC 查询辅助
- 更多 xiaomi 示例(风扇/窗帘/传感器控制)
2026-06-29 08:51:21 +08:00

460 lines
11 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/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
}