Files
xiaomihome/miot/examples/diag_device/main.go
T
4566704 86c21ce91c 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 示例
2026-08-23 19:42:55 +08:00

223 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Command diag_device 诊断指定米家设备的云端属性(纯 HTTP,不初始化 MQTT)。
//
// 用途:对比 SDK 读到属性值 vs 米家 App 显示,验证 siid/piid 是否正确、
// 控制是否生效、设备连接类型等。可在生产环境旁路运行(不与生产 MQTT 冲突)。
//
// 用法:token/did 直接改下面常量,然后 go run . 即可。
package main
import (
"fmt"
"os"
"sort"
"strconv"
"strings"
"xiaomihome/miot"
)
// ── 配置:按需修改 ──
const (
accessToken = "V3_1GfW88WpawoKACRqwdYBb86In_I5e8r8stZdRPXI1PR4zZgIEm4cbjNwn0wYa6-ZQtIliJJerf-iI0W9Lx8t6fm7Ln16w0RVAmYO3e9XubJ0dK7D3eM_zBlCcVmQCla6Sx0BKQv_YXI9E2zXXopKaRjIUWyx0lb0YcE7oEopLuvfA0sEFoTohVKt543KnHJH" // 米家 OAuth access_token
targetDID = "2188673403" // 目标设备 did
homeID = "619001787902" // 家庭 ID(留空=全部家庭;先跑一遍看家庭列表再填)
dataDir = "/home/ubuntu/server/zoSmartUser/data" // spec 缓存目录(生产 Linux 路径)
setTest = "4,1,false|4,2,26" // 写属性测试:"siid,piid,value",多组用 | 分隔;留空跳过
// ──────────────────────
)
func main() {
if accessToken == "" {
fmt.Fprintln(os.Stderr, "请先在 main.go 顶部 const 里填写 accessToken")
os.Exit(1)
}
// 纯 HTTP 客户端(不连 MQTT,不占 mqtt client id)
httpClient, err := miot.NewMIoTHttpClient("cn", miot.OAUTH2_CLIENT_ID, accessToken)
if err != nil {
fmt.Fprintf(os.Stderr, "创建 HTTP 客户端失败: %v\n", err)
os.Exit(1)
}
fmt.Println("HTTP 客户端 OK(未连接 MQTT)")
// 1. 按 did 精确拉设备信息(不依赖家庭列表)
devMap, err := httpClient.GetDevicesWithDIDs([]string{targetDID})
if err != nil {
fmt.Fprintf(os.Stderr, "获取设备信息失败: %v\n", err)
os.Exit(1)
}
info, ok := devMap[targetDID]
if !ok {
fmt.Fprintf(os.Stderr, "设备 %s 未返回(共 %d 台)\n", targetDID, len(devMap))
os.Exit(1)
}
infoMap := info
fmt.Printf("\n=== 设备信息 %s ===\n", targetDID)
printDeviceInfo(infoMap)
urn, _ := infoMap["urn"].(string)
model, _ := infoMap["model"].(string)
fmt.Printf("URN: %s\nMODEL: %s\n", urn, model)
// 2. 解析 SPEC
parser := miot.NewMIoTSpecParser("en", miot.NewMIoTStorage(dataDir), dataDir+"/specs")
spec, err := parser.Parse(urn, model)
if err != nil {
fmt.Fprintf(os.Stderr, "SPEC 解析失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("\n=== SPEC 服务/属性(siid 布局) ===\n")
for _, svc := range spec.Services {
if svc == nil {
continue
}
fmt.Printf("svc[%d] %s\n", svc.IID, short(svc.Type))
names := make([]string, 0, len(svc.Properties))
for _, p := range svc.Properties {
if p == nil {
continue
}
names = append(names, fmt.Sprintf("%s(%d)", short(p.Type), p.PIID))
}
sort.Strings(names)
fmt.Printf(" %s\n", strings.Join(names, " "))
}
// 3. 批量读取全部可读属性
fmt.Printf("\n=== 读取全部可读属性 ===\n")
var params []miot.PropParam
keys := make([]string, 0)
for _, svc := range spec.Services {
if svc == nil {
continue
}
for _, p := range svc.Properties {
if p == nil {
continue
}
params = append(params, miot.PropParam{DID: targetDID, Siid: svc.IID, Piid: p.PIID})
keys = append(keys, fmt.Sprintf("%d.%d %s", svc.IID, p.PIID, short(p.Type)))
}
}
results, err := httpClient.GetProps(params)
if err != nil {
fmt.Fprintf(os.Stderr, "读取属性失败: %v\n", err)
} else {
for i, r := range results {
name := ""
if i < len(keys) {
name = keys[i]
}
fmt.Printf(" %-24s code=%d value=%v\n", name, r.Code, r.Value)
}
}
// 4. 三合一设备对比读取(空调/地暖双温度)
fmt.Printf("\n=== 温度属性对比(三合一设备) ===\n")
read := func(siid, piid int, label string) {
rs, err := httpClient.GetProps([]miot.PropParam{{DID: targetDID, Siid: siid, Piid: piid}})
if err != nil {
fmt.Printf(" %s: 错误 %v\n", label, err)
return
}
if len(rs) > 0 {
fmt.Printf(" %-28s code=%d value=%v\n", label, rs[0].Code, rs[0].Value)
}
}
read(2, 1, "siid=2/piid=1 on(空调)")
read(2, 2, "siid=2/piid=2 mode")
read(2, 4, "siid=2/piid=4 target-temperature")
read(4, 2, "siid=4/piid=2 target-temperature")
read(5, 2, "siid=5/piid=2 temperature(环境)")
// 5. 写属性测试(可选,多组用 | 分隔,如 "2,4,26|4,2,26")
if setTest != "" {
for _, spec := range strings.Split(setTest, "|") {
doSetAndVerify(httpClient, strings.TrimSpace(spec))
}
}
}
// doSetAndVerify 执行一次写属性并回读验证。
func doSetAndVerify(httpClient *miot.MIoTHttpClient, spec string) {
parts := strings.Split(spec, ",")
if len(parts) < 3 {
fmt.Fprintf(os.Stderr, "setTest 格式错误: %s (应为 siid,piid,value)\n", spec)
return
}
siid, _ := strconv.Atoi(strings.TrimSpace(parts[0]))
piid, _ := strconv.Atoi(strings.TrimSpace(parts[1]))
var val interface{}
if b, err := strconv.ParseBool(parts[2]); err == nil {
val = b
} else if f, err := strconv.ParseFloat(strings.TrimSpace(parts[2]), 64); err == nil {
val = f
} else {
val = strings.TrimSpace(parts[2])
}
fmt.Printf("\n=== 写属性 siid=%d piid=%d value=%v ===\n", siid, piid, val)
rs, err := httpClient.SetProp([]miot.PropParam{{DID: targetDID, Siid: siid, Piid: piid, Value: val}})
if err != nil {
fmt.Printf("写属性失败: %v\n", err)
return
}
for _, r := range rs {
fmt.Printf(" code=%d (0/1=成功,非0=失败)\n", r.Code)
}
// 写后立即回读验证(区分"写错服务" vs "云端假成功")
fmt.Println("--- 写后回读 ---")
verify := func(vsiid, vpiid int, label string) {
vrs, err := httpClient.GetProps([]miot.PropParam{{DID: targetDID, Siid: vsiid, Piid: vpiid}})
if err != nil {
fmt.Printf(" %s: 错误 %v\n", label, err)
return
}
if len(vrs) > 0 {
fmt.Printf(" %-30s code=%d value=%v\n", label, vrs[0].Code, vrs[0].Value)
}
}
verify(siid, piid, "回读 siid=目标")
verify(2, 1, "siid=2/piid=1 on(地暖)")
verify(2, 2, "siid=2/piid=2 mode(地暖)")
verify(2, 4, "siid=2/piid=4 target-temperature(地暖)")
verify(4, 1, "siid=4/piid=1 on(空调)")
verify(4, 2, "siid=4/piid=2 target-temperature(空调)")
}
func printDeviceInfo(m map[string]interface{}) {
fields := []struct{ k, label string }{
{"name", "名称"}, {"model", "型号"}, {"connect_type", "connect_type"},
{"pid", "pid"}, {"online", "online"}, {"isOnline", "isOnline"},
{"rssi", "rssi"}, {"local_ip", "local_ip"}, {"parent_id", "parent_id"},
{"urn", "urn"},
}
for _, f := range fields {
if v, ok := m[f.k]; ok {
fmt.Printf(" %-12s = %v\n", f.label, v)
}
}
}
func short(urn string) string {
parts := strings.Split(urn, ":")
if len(parts) >= 4 {
return parts[3]
}
return urn
}
// toStrSlice 将 interface{}([]interface{})转为 []string。
func toStrSlice(v interface{}) []string {
arr, ok := v.([]interface{})
if !ok {
return nil
}
out := make([]string, 0, len(arr))
for _, item := range arr {
if s, ok := item.(string); ok {
out = append(out, s)
}
}
return out
}