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 示例(风扇/窗帘/传感器控制)
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user