// 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 }