diff --git a/xiaomi/devices/air_conditioner.go b/xiaomi/devices/air_conditioner.go index b4b1cb7..d12bf0e 100644 --- a/xiaomi/devices/air_conditioner.go +++ b/xiaomi/devices/air_conditioner.go @@ -2,6 +2,7 @@ package devices import ( "context" + "sort" "strings" "sync" "time" @@ -17,6 +18,10 @@ type valMapper struct { val2name map[int]string name2val map[string]int names []string + + // levelFallback 允许 low/medium/high 映射到数字档位(仅 fan-level 启用, + // 用于枚举为 Level1~LevelN 的设备;mode/swing 枚举语义固定,禁用)。 + levelFallback bool } func newValMapper(vl miot.MIoTSpecValueList) *valMapper { @@ -38,15 +43,82 @@ func newValMapper(vl miot.MIoTSpecValueList) *valMapper { return vm } +// newValMapperLevel 创建支持 low/medium/high → LevelN 映射的 valMapper(仅 fan-level 使用)。 +func newValMapperLevel(vl miot.MIoTSpecValueList) *valMapper { + vm := newValMapper(vl) + vm.levelFallback = true + return vm +} + func (vm *valMapper) name(v int) string { - if vm == nil { return "" } - return vm.val2name[v] + if vm == nil { + return "" + } + if s, ok := vm.val2name[v]; ok { + if !vm.levelFallback { + return s + } + // levelFallback:数字档位反向映射到语义枚举(与 val() 正向对称)。 + // 0→auto,最低档→low,中间档→medium,最高档→high;其余返回 spec 描述。 + levels := vm.levelValues() + switch { + case v == 0: + return "auto" + case len(levels) > 0 && v == levels[0]: + return "low" + case len(levels) > 1 && v == levels[len(levels)-1]: + return "high" + case len(levels) > 2 && v == levels[len(levels)/2]: + return "medium" + } + return s + } + return "" } func (vm *valMapper) val(name string) (int, bool) { - if vm == nil { return 0, false } - v, ok := vm.name2val[strings.ToLower(name)] - return v, ok + if vm == nil { + return 0, false + } + key := strings.ToLower(name) + if v, ok := vm.name2val[key]; ok { + return v, true + } + // fallback:标准档位枚举(low/medium/high)映射到数字档位的相对位置。 + // 适用于 fan-level 枚举为 Level1~LevelN(或纯数字)的设备,如 klwdz1.airc.001。 + // 排除 0(通常为 Auto/Off),low→最低档,medium→中间档,high→最高档。 + // 仅 levelFallback 启用(fan-level)时生效,mode/swing 不受影响。 + if !vm.levelFallback { + return 0, false + } + levels := vm.levelValues() + if len(levels) == 0 { + return 0, false + } + switch key { + case "low": + return levels[0], true + case "high": + return levels[len(levels)-1], true + case "medium": + return levels[len(levels)/2], true + } + return 0, false +} + +// levelValues 返回非 0 档位的升序值列表(levelFallback 辅助)。 +func (vm *valMapper) levelValues() []int { + var levels []int + for v := range vm.val2name { + if v != 0 { + levels = append(levels, v) + } + } + if len(levels) == 0 { + return nil + } + sort.Ints(levels) + return levels } func (vm *valMapper) allNames() []string { @@ -282,14 +354,14 @@ func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirCondi if resolver.hasService("thermostat") { if s, p, ok := resolver.findInService("thermostat", "fan-level"); ok { d.propFanSpeed = siidPiid{s, p, true} - d.fanMap = newValMapper(findValListInService("thermostat", "fan-level")) + d.fanMap = newValMapperLevel(findValListInService("thermostat", "fan-level")) } else if s, p, ok := resolver.find("fan-level"); ok { d.propFanSpeed = siidPiid{s, p, true} - d.fanMap = newValMapper(findValList("fan-level")) + d.fanMap = newValMapperLevel(findValList("fan-level")) } } else if s, p, ok := resolver.find("fan-level"); ok { d.propFanSpeed = siidPiid{s, p, true} - d.fanMap = newValMapper(findValList("fan-level")) + d.fanMap = newValMapperLevel(findValList("fan-level")) } // Try multiple swing mode type names if s, p, ok := resolver.find("swing-mode"); ok { diff --git a/xiaomi/devices/devices_test.go b/xiaomi/devices/devices_test.go index c406fa3..61637f6 100644 --- a/xiaomi/devices/devices_test.go +++ b/xiaomi/devices/devices_test.go @@ -3,6 +3,7 @@ package devices import ( "testing" + "xiaomihome/miot" "xiaomihome/xiaomi" ) @@ -200,3 +201,43 @@ func TestServiceShortName(t *testing.T) { } } } + +func TestValMapperLevelFallback(t *testing.T) { + // klwdz1.airc.001 场景:Auto + Level1~Level5 + vl := miot.MIoTSpecValueList{ + {Value: 0, Desc: "Auto"}, {Value: 1, Desc: "Level1"}, {Value: 2, Desc: "Level2"}, + {Value: 3, Desc: "Level3"}, {Value: 4, Desc: "Level4"}, {Value: 5, Desc: "Level5"}, + } + vm := newValMapperLevel(vl) + cases := []struct{ name string; want int; ok bool }{ + {"auto", 0, true}, {"Auto", 0, true}, + {"low", 1, true}, {"medium", 3, true}, {"high", 5, true}, + {"level3", 3, true}, + } + for _, c := range cases { + got, ok := vm.val(c.name) + if ok != c.ok || (ok && got != c.want) { + t.Errorf("val(%q) = (%d,%v), want (%d,%v)", c.name, got, ok, c.want, c.ok) + } + } + // 反向解析(MQTT 上报值 → 语义枚举),与 val() 正向对称 + rname := []struct{ v int; want string }{ + {0, "auto"}, {1, "low"}, {3, "medium"}, {5, "high"}, {2, "Level2"}, + } + for _, c := range rname { + if got := vm.name(c.v); got != c.want { + t.Errorf("name(%d) = %q, want %q", c.v, got, c.want) + } + } + // mode 枚举不受影响(无 low/medium/high fallback 副作用) + mvl := miot.MIoTSpecValueList{ + {Value: 0, Desc: "Auto"}, {Value: 1, Desc: "Cool"}, {Value: 2, Desc: "Heat"}, + } + mvm := newValMapper(mvl) + if _, ok := mvm.val("low"); ok { + t.Error("mode mapper should not resolve 'low'") + } + if v, ok := mvm.val("cool"); !ok || v != 1 { + t.Errorf("mode val(cool) = (%d,%v), want (1,true)", v, ok) + } +}