**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 示例
223 lines
7.0 KiB
Go
223 lines
7.0 KiB
Go
// 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
|
||
}
|