feat: AC 三合一设备支持 + 新增 Plug/Gateway 驱动 + Cover 结构化重构

**AC 驱动增强**
- 支持 thermostat + air-conditioner + air-fresh 多服务三合一设备,属性跨服务合并解析
- 新增 specResolver.findValListInService/findInService/hasService/serviceShortName 方法
- 移除 KindThermostat,heater/electric-blanket/thermostat 重新归类为 KindAirConditioner

**Cover 窗帘重构**
- CoverProps 结构化:Position/Status/Fault 分离
- SetPosition 优先写 target-position,回退 current-position
- OnPropsChanged/FetchProps 支持多属性同步刷新

**新增设备类型**
- Plug 驱动:插座/插头,支持开合、在线状态、属性变更订阅
- Gateway 驱动:网关设备类型识别
- miot/miot_errors.go:新增 MIoT 云端错误码定义

**MIoT 客户端健壮性**
- SetProp 空结果视为失败;设备被移除或离线时自动刷新设备列表
- GetDevices 的 has_more 改用 parseBool 严格解析
- MIPS SubscribeMany 批量订阅减少往返
- matchURN 改为按段精确匹配,避免 tofan 误匹配 fan

**Bridge**
- 移除 Thermostat case,Cover 改用结构化 CoverProps
- 新增设备创建后自动订阅属性变更

**其他**
- 日志整体 Info → Debug 降噪
- .gitignore 忽略 /.codebuddy/memory
- 更新 devices_test 与 08_adv_filter_classify 示例
This commit is contained in:
2026-08-23 19:42:55 +08:00
parent 04333750ab
commit 86c21ce91c
25 changed files with 1129 additions and 155 deletions
+130
View File
@@ -0,0 +1,130 @@
// Command diag_sdk 用完整 SDK 封装(xiaomi.NewClient + devices.Create)重现生产控制路径。
//
// 目的:验证 SDK 封装层解析出的属性绑定(on/target-temperature 绑到哪个 siid)。
// 与生产同链路:ac.TurnOn / ac.SetTargetTemp → SetProp(绑定siid) → 云端。
//
// 注意:会初始化 MQTT,但用自定义 uuid(client id 唯一,不和生产进程冲突)。
// 在开发机运行;dataDir 指向与生产相同的 spec 缓存(拷一份或指定生产目录)。
package main
import (
"context"
"fmt"
"os"
"time"
"xiaomihome/xiaomi"
"xiaomihome/xiaomi/devices"
)
// ── 配置:按需修改 ──
const (
accessToken = "V3_1GfW88WpawoKACRqwdYBb86In_I5e8r8stZdRPXI1PR4zZgIEm4cbjNwn0wYa6-ZQtIliJJerf-iI0W9Lx8t6fm7Ln16w0RVAmYO3e9XubJ0dK7D3eM_zBlCcVmQCla6Sx0BKQv_YXI9E2zXXopKaRjIUWyx0lb0YcE7oEopLuvfA0sEFoTohVKt543KnHJH" // 米家 OAuth access_token
uid = "2897121707" // 小米帐号 UID
homeID = int64(619001787902) // 家庭 ID
targetDID = "2188673403" // 目标设备 did
dataDir = "D:/xiaomihome/go-xiaomihome/miot/examples/diag_sdk/data" // 本 demo 自己的 spec 缓存目录
// ──────────────────────
)
func main() {
ctx := context.Background()
cfg := xiaomi.Config{
AuthInfo: xiaomi.AuthInfo{AccessToken: accessToken},
UUID: "diag-sdk-" + time.Now().Format("150405"), // 唯一 uuid,不占生产 client id
UID: uid,
DataDir: dataDir,
HomeIDs: []int64{homeID},
}
client, err := xiaomi.NewClient(ctx, cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "NewClient 失败: %v\n", err)
os.Exit(1)
}
defer client.Close()
fmt.Println("SDK 客户端 OK")
info, err := client.GetDevice(ctx, targetDID)
if err != nil || info == nil {
fmt.Fprintf(os.Stderr, "获取设备 %s 失败: %v\n", targetDID, err)
os.Exit(1)
}
fmt.Printf("设备: %s (%s)\nURN: %s\n", info.Name, info.Model, info.URN)
dev, err := devices.NewDevice(client, info)
if err != nil {
fmt.Fprintf(os.Stderr, "创建设备控制失败: %v\n", err)
os.Exit(1)
}
if dev == nil {
fmt.Fprintf(os.Stderr, "设备类型不支持\n")
os.Exit(1)
}
ac, ok := dev.(devices.AirConditioner)
if !ok {
fmt.Fprintf(os.Stderr, "设备不是 AirConditioner(got %T)\n", dev)
os.Exit(1)
}
fmt.Println("AirConditioner 封装 OK")
// 读取当前状态(封装层)
if props, err := ac.FetchProps(ctx); err != nil {
fmt.Printf("FetchProps 失败: %v\n", err)
} else {
fmt.Printf("封装层状态: on=%v mode=%s target=%.0f current=%.0f fan=%s hum=%d\n",
props.On, props.Mode, props.TargetTemperature, props.Temperature,
props.FanLevel, props.CurrentHumidity)
}
// 控制(观察日志里 SetProp 写的 siid)
fmt.Println("\n=== 控制序列(看 SetProp 日志的 siid) ===")
if err := ac.TurnOn(ctx); err != nil {
fmt.Printf("TurnOn 失败: %v\n", err)
} else {
fmt.Println("TurnOn OK")
}
time.Sleep(500 * time.Millisecond)
if err := ac.SetTargetTemp(ctx, 26); err != nil {
fmt.Printf("SetTargetTemp 失败: %v\n", err)
} else {
fmt.Println("SetTargetTemp(26) OK")
}
time.Sleep(500 * time.Millisecond)
if ac.HasFanSpeed() {
if err := ac.SetFanSpeed(ctx, "medium"); err != nil {
fmt.Printf("SetFanSpeed 失败: %v\n", err)
} else {
fmt.Println("SetFanSpeed(medium) OK")
}
} else {
fmt.Println("HasFanSpeed = false")
}
time.Sleep(500 * time.Millisecond)
if err := ac.SetMode(ctx, "cool"); err != nil {
fmt.Printf("SetMode 失败: %v\n", err)
} else {
fmt.Println("SetMode(cool) OK")
}
time.Sleep(500 * time.Millisecond)
if err := ac.TurnOff(ctx); err != nil {
fmt.Printf("TurnOff 失败: %v\n", err)
} else {
fmt.Println("TurnOff OK")
}
time.Sleep(500 * time.Millisecond)
// 控制后回读
if props, err := ac.FetchProps(ctx); err != nil {
fmt.Printf("FetchProps 失败: %v\n", err)
} else {
fmt.Printf("控制后状态: on=%v mode=%s target=%.0f current=%.0f fan=%s hum=%d\n",
props.On, props.Mode, props.TargetTemperature, props.Temperature,
props.FanLevel, props.CurrentHumidity)
}
}