Files
xiaomihome/xiaomi/specs/resolver_test.go
T
4566704 a3d94c4b9f 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 示例(风扇/窗帘/传感器控制)
2026-06-29 08:51:21 +08:00

241 lines
6.3 KiB
Go

package specs
import (
"testing"
"xiaomihome/miot"
)
// buildTestSpec creates a mock SPEC instance for testing.
func buildTestSpec() *miot.MIoTSpecInstance {
return &miot.MIoTSpecInstance{
URN: "urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c1:1",
Type: "air-conditioner",
Description: "Xiaomi Air Conditioner",
DescriptionTrans: "小米空调",
Services: []*miot.MIoTSpecService{
{
IID: 1,
Type: "device-information",
Description: "Device Information",
Properties: []*miot.MIoTSpecProperty{
{SIID: 1, PIID: 1, Type: "", Format: "string", Access: []string{"read"}, Description: "manufacturer", DescriptionTrans: "制造商"},
},
},
{
IID: 2,
Type: "air-conditioner",
Description: "Air Conditioner",
Properties: []*miot.MIoTSpecProperty{
{SIID: 2, PIID: 1, Type: "on", Format: "bool", Access: []string{"read", "write", "notify"}, Description: "Power", DescriptionTrans: "开关"},
{SIID: 2, PIID: 2, Type: "mode", Format: "uint8", Access: []string{"read", "write", "notify"}, Description: "Mode", DescriptionTrans: "模式"},
{SIID: 2, PIID: 3, Type: "target-temperature", Format: "float", Access: []string{"read", "write", "notify"}, Description: "Target Temp", DescriptionTrans: "目标温度"},
{SIID: 2, PIID: 4, Type: "temperature", Format: "float", Access: []string{"read"}, Description: "Current Temp", DescriptionTrans: "当前温度"},
{SIID: 2, PIID: 5, Type: "fan-level", Format: "uint8", Access: []string{"read", "write", "notify"}, Description: "Fan Speed", DescriptionTrans: "风速"},
{SIID: 2, PIID: 6, Type: "swing-mode", Format: "uint8", Access: []string{"read", "write", "notify"}, Description: "Swing", DescriptionTrans: "摆风"},
},
Actions: []*miot.MIoTSpecAction{
{SIID: 2, AIID: 1, Type: "toggle", Description: "Toggle Power"},
},
},
},
}
}
func TestNewPropertyResolver(t *testing.T) {
t.Run("valid spec", func(t *testing.T) {
spec := buildTestSpec()
r := NewPropertyResolver(spec)
if r == nil {
t.Fatal("resolver should not be nil")
}
if r.spec != spec {
t.Error("spec reference mismatch")
}
})
t.Run("nil spec", func(t *testing.T) {
r := NewPropertyResolver(nil)
if r != nil {
t.Error("resolver should be nil for nil spec")
}
})
}
func TestFindByType(t *testing.T) {
r := NewPropertyResolver(buildTestSpec())
tests := []struct {
name string
propType string
wantSIID int
wantPIID int
wantOK bool
}{
{"on", "on", 2, 1, true},
{"mode", "mode", 2, 2, true},
{"target-temperature", "target-temperature", 2, 3, true},
{"temperature", "temperature", 2, 4, true},
{"fan-level", "fan-level", 2, 5, true},
{"swing-mode", "swing-mode", 2, 6, true},
{"nonexistent", "brightness", 0, 0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
siid, piid, ok := r.FindByType(tt.propType)
if ok != tt.wantOK {
t.Errorf("ok = %v, want %v", ok, tt.wantOK)
}
if siid != tt.wantSIID {
t.Errorf("siid = %d, want %d", siid, tt.wantSIID)
}
if piid != tt.wantPIID {
t.Errorf("piid = %d, want %d", piid, tt.wantPIID)
}
})
}
}
func TestFindByFormat(t *testing.T) {
r := NewPropertyResolver(buildTestSpec())
tests := []struct {
name string
format string
siid int
wantPIID int
wantOK bool
}{
{"bool in siid 2", "bool", 2, 1, true},
{"float in siid 2 (first match)", "float", 2, 3, true},
{"string in siid 1", "string", 1, 1, true},
{"nonexistent format", "int16", 2, 0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
piid, ok := r.FindByFormat(tt.format, tt.siid)
if ok != tt.wantOK {
t.Errorf("ok = %v, want %v", ok, tt.wantOK)
}
if piid != tt.wantPIID {
t.Errorf("piid = %d, want %d", piid, tt.wantPIID)
}
})
}
}
func TestFindByDescription(t *testing.T) {
r := NewPropertyResolver(buildTestSpec())
tests := []struct {
name string
desc string
wantSIID int
wantPIID int
wantOK bool
}{
{"by translated desc", "开关", 2, 1, true},
{"by raw desc", "Power", 2, 1, true},
{"nonexistent desc", "亮度", 0, 0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
siid, piid, ok := r.FindByDescription(tt.desc)
if ok != tt.wantOK {
t.Errorf("ok = %v, want %v", ok, tt.wantOK)
}
if siid != tt.wantSIID {
t.Errorf("siid = %d, want %d", siid, tt.wantSIID)
}
if piid != tt.wantPIID {
t.Errorf("piid = %d, want %d", piid, tt.wantPIID)
}
})
}
}
func TestFindAction(t *testing.T) {
r := NewPropertyResolver(buildTestSpec())
t.Run("existing action", func(t *testing.T) {
aiid, ok := r.FindAction(2, "toggle")
if !ok {
t.Error("toggle action should be found")
}
if aiid != 1 {
t.Errorf("aiid = %d, want 1", aiid)
}
})
t.Run("nonexistent action", func(t *testing.T) {
_, ok := r.FindAction(2, "nonexistent")
if ok {
t.Error("nonexistent action should not be found")
}
})
t.Run("nonexistent service", func(t *testing.T) {
_, ok := r.FindAction(99, "toggle")
if ok {
t.Error("action in nonexistent service should not be found")
}
})
}
func TestGetAllProperties(t *testing.T) {
r := NewPropertyResolver(buildTestSpec())
t.Run("siid 2 has 6 properties", func(t *testing.T) {
props := r.GetAllProperties(2)
if len(props) != 6 {
t.Errorf("len = %d, want 6", len(props))
}
})
t.Run("siid 1 has 1 property", func(t *testing.T) {
props := r.GetAllProperties(1)
if len(props) != 1 {
t.Errorf("len = %d, want 1", len(props))
}
})
t.Run("nonexistent siid", func(t *testing.T) {
props := r.GetAllProperties(99)
if len(props) != 0 {
t.Errorf("len = %d, want 0", len(props))
}
})
}
func TestGetProperty(t *testing.T) {
r := NewPropertyResolver(buildTestSpec())
t.Run("existing property", func(t *testing.T) {
prop := r.GetProperty(2, 3)
if prop == nil {
t.Fatal("property should be found")
}
if prop.Type != "target-temperature" {
t.Errorf("Type = %q", prop.Type)
}
})
t.Run("nonexistent property", func(t *testing.T) {
prop := r.GetProperty(2, 99)
if prop != nil {
t.Error("property should be nil")
}
})
}
func TestSpec(t *testing.T) {
spec := buildTestSpec()
r := NewPropertyResolver(spec)
if r.Spec() != spec {
t.Error("Spec() should return the original spec")
}
}