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:
@@ -2,6 +2,7 @@ package devices
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,6 +18,10 @@ type valMapper struct {
|
|||||||
val2name map[int]string
|
val2name map[int]string
|
||||||
name2val map[string]int
|
name2val map[string]int
|
||||||
names []string
|
names []string
|
||||||
|
|
||||||
|
// levelFallback 允许 low/medium/high 映射到数字档位(仅 fan-level 启用,
|
||||||
|
// 用于枚举为 Level1~LevelN 的设备;mode/swing 枚举语义固定,禁用)。
|
||||||
|
levelFallback bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newValMapper(vl miot.MIoTSpecValueList) *valMapper {
|
func newValMapper(vl miot.MIoTSpecValueList) *valMapper {
|
||||||
@@ -38,15 +43,82 @@ func newValMapper(vl miot.MIoTSpecValueList) *valMapper {
|
|||||||
return vm
|
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 {
|
func (vm *valMapper) name(v int) string {
|
||||||
if vm == nil { return "" }
|
if vm == nil {
|
||||||
return vm.val2name[v]
|
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) {
|
func (vm *valMapper) val(name string) (int, bool) {
|
||||||
if vm == nil { return 0, false }
|
if vm == nil {
|
||||||
v, ok := vm.name2val[strings.ToLower(name)]
|
return 0, false
|
||||||
return v, ok
|
}
|
||||||
|
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 {
|
func (vm *valMapper) allNames() []string {
|
||||||
@@ -282,14 +354,14 @@ func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirCondi
|
|||||||
if resolver.hasService("thermostat") {
|
if resolver.hasService("thermostat") {
|
||||||
if s, p, ok := resolver.findInService("thermostat", "fan-level"); ok {
|
if s, p, ok := resolver.findInService("thermostat", "fan-level"); ok {
|
||||||
d.propFanSpeed = siidPiid{s, p, true}
|
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 {
|
} else if s, p, ok := resolver.find("fan-level"); ok {
|
||||||
d.propFanSpeed = siidPiid{s, p, true}
|
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 {
|
} else if s, p, ok := resolver.find("fan-level"); ok {
|
||||||
d.propFanSpeed = siidPiid{s, p, true}
|
d.propFanSpeed = siidPiid{s, p, true}
|
||||||
d.fanMap = newValMapper(findValList("fan-level"))
|
d.fanMap = newValMapperLevel(findValList("fan-level"))
|
||||||
}
|
}
|
||||||
// Try multiple swing mode type names
|
// Try multiple swing mode type names
|
||||||
if s, p, ok := resolver.find("swing-mode"); ok {
|
if s, p, ok := resolver.find("swing-mode"); ok {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package devices
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"xiaomihome/miot"
|
||||||
"xiaomihome/xiaomi"
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user