fix(xiaomi): 空调风速按数字档位映射并增加 MQTT 反向语义解析

klwdz1.airc.001 等设备 fan-level 枚举为 Auto + Level1~Level5,SDK 侧
low/medium/high 在枚举里查不到,导致只有 Auto 能设置成功。

- valMapper 增加 levelFallback 开关(仅 fan-level 启用):精确匹配失败时
  low/medium/high 按数字档位相对位置映射(排除 Auto=0)
- name() 增加对称的反向映射:设备上报 1/3/5 → low/medium/high,
  控制与 MQTT 通知使用同一套语义枚举
- newValMapper 默认关闭 fallback,mode/swing 行为不变
- 新增 TestValMapperLevelFallback 覆盖正反向映射与 mode 无副作用
This commit is contained in:
2026-09-12 19:29:32 +08:00
parent a0ef854726
commit c18c0adaae
2 changed files with 121 additions and 8 deletions
+80 -8
View File
@@ -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 {