- 新增 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 示例(风扇/窗帘/传感器控制)
188 lines
4.0 KiB
Go
188 lines
4.0 KiB
Go
package devices
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"xiaomihome/xiaomi"
|
|
)
|
|
|
|
func TestToInt(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value interface{}
|
|
want int
|
|
}{
|
|
{"float64", float64(42), 42},
|
|
{"int", int(42), 42},
|
|
{"int64", int64(42), 42},
|
|
{"uint8", uint8(100), 100},
|
|
{"uint16", uint16(1000), 1000},
|
|
{"nil", nil, 0},
|
|
{"string", "hello", 0},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := toInt(tt.value)
|
|
if got != tt.want {
|
|
t.Errorf("toInt(%v) = %d, want %d", tt.value, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestToFloat(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value interface{}
|
|
want float64
|
|
}{
|
|
{"float64", float64(26.5), 26.5},
|
|
{"float32", float32(30.0), 30.0},
|
|
{"int", int(25), 25.0},
|
|
{"int64", int64(20), 20.0},
|
|
{"string", "26.5", 0.0},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := toFloat(tt.value)
|
|
if got != tt.want {
|
|
t.Errorf("toFloat(%v) = %f, want %f", tt.value, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAirConditionerModeConstants(t *testing.T) {
|
|
// Verify all mode values have reverse mappings
|
|
for _, m := range []ACMode{ModeCool, ModeHeat, ModeFan, ModeDry, ModeAuto} {
|
|
if _, ok := modeReverseMap[m]; !ok {
|
|
t.Errorf("missing reverse mapping for %s", m)
|
|
}
|
|
}
|
|
|
|
// Verify all value entries map back correctly
|
|
for v, m := range modeValueMap {
|
|
if r, ok := modeReverseMap[m]; !ok || r != v {
|
|
t.Errorf("modeReverseMap[modeValueMap[%d]] = %v, want %d", v, r, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFanSpeedConstants(t *testing.T) {
|
|
for _, s := range []FanSpeed{SpeedLow, SpeedMedium, SpeedHigh, SpeedAuto} {
|
|
if _, ok := fanSpeedReverseMap[s]; !ok {
|
|
t.Errorf("missing reverse mapping for %s", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSwingModeConstants(t *testing.T) {
|
|
for _, s := range []SwingMode{SwingOff, SwingVertical, SwingHorizontal, SwingBoth} {
|
|
if _, ok := swingReverseMap[s]; !ok {
|
|
t.Errorf("missing reverse mapping for %s", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMatchURN(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
urn string
|
|
types []string
|
|
want bool
|
|
}{
|
|
{
|
|
"air-conditioner",
|
|
"urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c1:1",
|
|
[]string{"air-conditioner"},
|
|
true,
|
|
},
|
|
{
|
|
"light",
|
|
"urn:miot-spec-v2:device:light:0000A001:philips-bulb:1",
|
|
[]string{"light"},
|
|
true,
|
|
},
|
|
{
|
|
"switch outlet",
|
|
"urn:miot-spec-v2:device:outlet:0000A002:xiaomi-plug:1",
|
|
[]string{"switch", "outlet"},
|
|
true,
|
|
},
|
|
{
|
|
"fan",
|
|
"urn:miot-spec-v2:device:fan:0000A003:xiaomi-fan:1",
|
|
[]string{"fan"},
|
|
true,
|
|
},
|
|
{
|
|
"curtain",
|
|
"urn:miot-spec-v2:device:curtain:0000A005:aqara-c1:1",
|
|
[]string{"curtain"},
|
|
true,
|
|
},
|
|
{
|
|
"humidifier",
|
|
"urn:miot-spec-v2:device:humidifier:0000A006:xiaomi-h1:1",
|
|
[]string{"humidifier"},
|
|
true,
|
|
},
|
|
{
|
|
"vacuum",
|
|
"urn:miot-spec-v2:device:vacuum:0000A007:roborock-s5:1",
|
|
[]string{"vacuum"},
|
|
true,
|
|
},
|
|
{
|
|
"water-heater",
|
|
"urn:miot-spec-v2:device:water-heater:0000A008:xiaomi-wh1:1",
|
|
[]string{"water-heater"},
|
|
true,
|
|
},
|
|
{
|
|
"heater thermostat",
|
|
"urn:miot-spec-v2:device:heater:0000A009:xiaomi-h1:1",
|
|
[]string{"heater"},
|
|
true,
|
|
},
|
|
{
|
|
"no match",
|
|
"urn:miot-spec-v2:device:unknown:0000A099:test:1",
|
|
[]string{"air-conditioner", "light", "switch"},
|
|
false,
|
|
},
|
|
{
|
|
"empty URN",
|
|
"",
|
|
[]string{"light"},
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := matchURN(tt.urn, tt.types...)
|
|
if got != tt.want {
|
|
t.Errorf("matchURN(%q, %v) = %v, want %v", tt.urn, tt.types, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFactoryNilClientPanic(t *testing.T) {
|
|
// Create with a nil client should panic (calling methods on nil client)
|
|
// This is a documentation test noting expected behavior
|
|
t.Skip("requires real or mock client")
|
|
}
|
|
|
|
func TestCreateNilInfo(t *testing.T) {
|
|
client := &xiaomi.Client{}
|
|
result, err := Create(client, nil)
|
|
if result != nil {
|
|
t.Errorf("expected nil result, got %v", result)
|
|
}
|
|
if err == nil {
|
|
t.Error("expected error for nil info")
|
|
}
|
|
}
|