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,240 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user