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
+1
View File
@@ -49,3 +49,4 @@ ip.xdb
/xiaomi/examples/05_ac_control/data/miot_config/*.dict
*.dict
/xiaomi/examples/*/data/
/.codebuddy/memory
+2 -10
View File
@@ -143,7 +143,7 @@ func newBridge(ctx context.Context, id int64, uuid string, auth xiaomi.AuthInfo,
for _, info := range devList {
d, createErr := devices.Create(client, info)
if createErr != nil {
if createErr != nil || d == nil {
continue
}
b.dv[info.DID] = d
@@ -179,7 +179,6 @@ func deviceTypeName(d interface{}) string {
case devices.Humidifier: return devices.TypeHumidifier
case devices.Vacuum: return devices.TypeVacuum
case devices.WaterHeater: return devices.TypeWaterHeater
case devices.Thermostat: return devices.TypeThermostat
case devices.TempHumiditySensor: return devices.TypeTempHumidSensor
case devices.OccupancySensor: return devices.TypeOccupancySensor
case devices.Speaker: return devices.TypeSpeaker
@@ -195,7 +194,7 @@ func (b *Bridge) AC(did string) (devices.AirConditioner, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
ac, ok := d.(devices.AirConditioner)
if !ok { return nil, fmt.Errorf("bridge: device %s is not an AirConditioner", did) }
if !ok { return nil, fmt.Errorf("bridge: device %s is not an AirConditioner (got %T)", did, d) }
return ac, nil
}
func (b *Bridge) Light(did string) (devices.Light, error) {
@@ -247,13 +246,6 @@ func (b *Bridge) WaterHeater(did string) (devices.WaterHeater, error) {
if !ok { return nil, fmt.Errorf("bridge: device %s is not a WaterHeater", did) }
return wh, nil
}
func (b *Bridge) Thermostat(did string) (devices.Thermostat, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
t, ok := d.(devices.Thermostat)
if !ok { return nil, fmt.Errorf("bridge: device %s is not a Thermostat", did) }
return t, nil
}
func (b *Bridge) TempHumidity(did string) (devices.TempHumiditySensor, error) {
d, ok := b.dv[did]
if !ok { return nil, fmt.Errorf("bridge: device %s not found", did) }
+4 -12
View File
@@ -55,8 +55,8 @@ func (b *Bridge) subscribeDevice(did string, d interface{}) {
b.fire(did, devType, ch, props)
})
case devices.Cover:
ctrl.OnPropsChanged(func(pos int) {
b.fire(did, devType, devices.PropPosition, pos)
ctrl.OnPropsChanged(func(props devices.CoverProps) {
b.fire(did, devType, devices.PropPosition, props)
})
case devices.Humidifier:
ctrl.OnPropsChanged(func(props devices.HumidifierProps, ch string) {
@@ -70,10 +70,6 @@ func (b *Bridge) subscribeDevice(did string, d interface{}) {
ctrl.OnPropsChanged(func(props devices.HeaterProps, ch string) {
b.fire(did, devType, ch, props)
})
case devices.Thermostat:
ctrl.OnPropsChanged(func(props devices.HeaterProps, ch string) {
b.fire(did, devType, ch, props)
})
case devices.TempHumiditySensor:
ctrl.OnPropsChanged(func(temp, hum float64) {
b.fire(did, devType, "temperature_humidity", map[string]float64{"temperature": temp, "humidity": hum})
@@ -164,13 +160,9 @@ func (b *Bridge) FetchAllStates(ctx context.Context) {
if s, err := ctrl.FetchProps(ctx); err == nil {
b.lgr.Debugf("[bridge] %d Heater: on=%v target=%.1f", b.id, s.On, s.TargetTemp)
}
case devices.Thermostat:
if s, err := ctrl.FetchProps(ctx); err == nil {
b.lgr.Debugf("[bridge] %d Thermostat: target=%.1f current=%.1f", b.id, s.TargetTemp, s.CurrentTemp)
}
case devices.Cover:
if pos, err := ctrl.FetchProps(ctx); err == nil {
b.lgr.Debugf("[bridge] %d Cover: pos=%d", b.id, pos)
if s, err := ctrl.FetchProps(ctx); err == nil {
b.lgr.Debugf("[bridge] %d Cover: pos=%d status=%d fault=%d", b.id, s.Position, s.Status, s.Fault)
}
case devices.TempHumiditySensor:
if t, h, err := ctrl.FetchProps(ctx); err == nil {
+17
View File
@@ -0,0 +1,17 @@
{
"auth_info": {
"access_token": "V3_R0Je3kB-S6Mf1O7kKDPm3EDvgHiwsJMze4mVkPG0rRGHYv0KQEUVUyNRJ-ittoSSOmbNxso1syxYrKjOIAGck6EatzRIJsyULI5maduCo-GUyfHu4A6ZYa8zWD6EXp6pmnLim8KT8i1mDbczyOsFTQhNxWY1tq4CM54xlVhFqQwGS65SVV1DhrXvW9XU3nPa",
"refresh_token": "R3_WZYXokg6Nnmj2Ceib_bcPtI2fr548hNrjCnK5aG_Lm8-8X5PJeK5SHpicsOBy4Z5cWP4672pviFfWORSsSCpH3tF7UXY8Xuib8uyQP4ppxRiPssybXb67VLFfEHa7raQYtVH_2NHkp53flOZKiRtcFHOX4HAZoC7_AKuIdfWcIa4P3k03XmQF1WG0Ccp4BMH12P7eOJBHyU3LScJbFqVLQ",
"mac_key": "0K_213_6BrQPCpmmfolHVZpTR1Y",
"expires_in": 259200,
"expires_ts": 1783011905
},
"uid": "2897121707",
"cloud_server": "cn",
"uuid": "84259d29c352cad945d1f5e3dab3d89b",
"redirect_url": "http://homeassistant.local:8123",
"language": "en",
"ctrl_mode": "auto",
"storage_path": "",
"nick_name": "Xiaomi"
}
+90
View File
@@ -0,0 +1,90 @@
// Command diag_control 直接控制米家设备(纯 HTTP,不初始化 MQTT)。
//
// 控制序列:开空调 → 设温度 → 设模式 → 回读验证。
// 与生产 SDK 同一云端链路(miotspec/prop/set),不占 MQTT client id。
//
// 用法:token/did 改下面常量,在生产环境 go run .(Linux 下 dataDir 填生产路径)。
package main
import (
"fmt"
"os"
"xiaomihome/miot"
)
// ── 配置:按需修改 ──
const (
accessToken = "V3_1GfW88WpawoKACRqwdYBb86In_I5e8r8stZdRPXI1PR4zZgIEm4cbjNwn0wYa6-ZQtIliJJerf-iI0W9Lx8t6fm7Ln16w0RVAmYO3e9XubJ0dK7D3eM_zBlCcVmQCla6Sx0BKQv_YXI9E2zXXopKaRjIUWyx0lb0YcE7oEopLuvfA0sEFoTohVKt543KnHJH" // 米家 OAuth access_token
targetDID = "2188673403" // 目标设备 did
dataDir = "/home/ubuntu/server/zoSmartUser/data" // spec 缓存目录(生产 Linux)
// ──────────────────────
)
func main() {
if accessToken == "" {
fmt.Fprintln(os.Stderr, "请先填写 accessToken")
os.Exit(1)
}
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.Printf("HTTP 客户端 OK(未连接 MQTT),设备 %s\n", targetDID)
// 读取当前状态(控制前)
readAll("控制前状态", httpClient)
// ── 控制序列 ──
set("开空调 siid=4/piid=1=true", 4, 1, true, httpClient)
set("设温度 siid=4/piid=2=26", 4, 2, float64(26), httpClient)
set("设模式 siid=2/piid=2=1(Cool)", 2, 2, float64(1), httpClient)
set("关空调 siid=4/piid=1=false", 4, 1, false, httpClient)
// 回读验证(控制后)
readAll("控制后状态", httpClient)
}
// set 写属性并回读验证。
func set(label string, siid, piid int, value interface{}, c *miot.MIoTHttpClient) {
fmt.Printf("\n=== %s ===\n", label)
rs, err := c.SetProp([]miot.PropParam{{DID: targetDID, Siid: siid, Piid: piid, Value: value}})
if err != nil {
fmt.Printf(" 写失败: %v\n", err)
return
}
for _, r := range rs {
fmt.Printf(" 写返回 code=%d\n", r.Code)
}
// 回读
gr, err := c.GetProps([]miot.PropParam{{DID: targetDID, Siid: siid, Piid: piid}})
if err != nil {
fmt.Printf(" 回读失败: %v\n", err)
return
}
if len(gr) > 0 {
fmt.Printf(" 回读 siid=%d/piid=%d = %v\n", siid, piid, gr[0].Value)
}
}
// readAll 读取关键属性。
func readAll(label string, c *miot.MIoTHttpClient) {
fmt.Printf("\n--- %s ---\n", label)
read := func(siid, piid int, name string) {
rs, err := c.GetProps([]miot.PropParam{{DID: targetDID, Siid: siid, Piid: piid}})
if err != nil {
fmt.Printf(" %-28s 错误: %v\n", name, err)
return
}
if len(rs) > 0 {
fmt.Printf(" %-28s = %v\n", name, rs[0].Value)
}
}
read(4, 1, "空调开关 siid=4/piid=1")
read(4, 2, "空调温度 siid=4/piid=2")
read(2, 2, "模式 siid=2/piid=2")
read(2, 4, "地暖温度 siid=2/piid=4")
read(5, 2, "环境温度 siid=5/piid=2")
}
+222
View File
@@ -0,0 +1,222 @@
// 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
}
+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)
}
}
+30 -30
View File
@@ -156,7 +156,7 @@ func (c *MIoTClient) Init() error {
if et, ok := authInfo["expires_ts"].(float64); ok {
c.entryData["expires_ts"] = et
}
c.lgr.Infof("[MIoTClient] Loaded token from storage\n")
c.lgr.Debugf("[MIoTClient] Loaded token from storage\n")
} else if accessToken != "" {
// First run: write entryData token to storage
expiresTS := float64(time.Now().Unix()) + 3600*24*30 // default 30 days
@@ -168,7 +168,7 @@ func (c *MIoTClient) Init() error {
"refresh_token": refreshToken,
"expires_ts": expiresTS,
}, true)
c.lgr.Infof("[MIoTClient] Saved initial token to storage\n")
c.lgr.Debugf("[MIoTClient] Saved initial token to storage\n")
}
}
@@ -187,7 +187,7 @@ func (c *MIoTClient) Init() error {
oauthRedirectURL, _ := c.entryData["oauth_redirect_url"].(string)
uuid, _ := c.entryData["uuid"].(string)
c.oauth = NewMIoTOauthClient(OAUTH2_CLIENT_ID, oauthRedirectURL, c.cloudServer, uuid)
c.lgr.Infof("[MIoTClient] OAuth client initialized\n")
c.lgr.Debugf("[MIoTClient] OAuth client initialized\n")
}
// 3.2 Initialize Cert client
@@ -197,7 +197,7 @@ func (c *MIoTClient) Init() error {
} else {
c.cert = cert
c.cert.VerifyCACert()
c.lgr.Infof("[MIoTClient] Cert client initialized\n")
c.lgr.Debugf("[MIoTClient] Cert client initialized\n")
}
// 4. Initialize HTTP client (use token from storage if available)
@@ -207,7 +207,7 @@ func (c *MIoTClient) Init() error {
c.lgr.Warnf("[MIoTClient] Warning: failed to create HTTP client: %v\n", err)
} else {
c.http = httpClient
c.lgr.Infof("[MIoTClient] HTTP client created\n")
c.lgr.Debugf("[MIoTClient] HTTP client created\n")
}
}
@@ -225,7 +225,7 @@ func (c *MIoTClient) Init() error {
if err := mips.Connect(); err != nil {
c.lgr.Warnf("[MIoTClient] Warning: MIPS cloud connect failed: %v\n", err)
} else {
c.lgr.Infof("[MIoTClient] MIPS cloud connected\n")
c.lgr.Debugf("[MIoTClient] MIPS cloud connected\n")
}
c.mu.Lock()
}
@@ -239,7 +239,7 @@ func (c *MIoTClient) Init() error {
nw.SubNetworkStatus(key, c.onNetworkStatusChanged)
status := nw.GetNetworkStatus()
c.onNetworkStatusChanged(status)
c.lgr.Infof("[MIoTClient] Network status subscribed (online=%v)\n", status)
c.lgr.Debugf("[MIoTClient] Network status subscribed (online=%v)\n", status)
}
}
@@ -258,12 +258,12 @@ func (c *MIoTClient) Init() error {
groupID, _ := info["group_id"].(string)
if groupID != "" {
svc.SubServiceChange(key, groupID, c.onMipsServiceStateChange)
c.lgr.Infof("[MIoTClient] mDNS service subscribed: home=%s, group=%s\n", homeID, groupID)
c.lgr.Debugf("[MIoTClient] mDNS service subscribed: home=%s, group=%s\n", homeID, groupID)
// Check if service already discovered
serviceData := svc.GetServices(groupID)
if sd, exists := serviceData[groupID]; exists && sd.validService() {
c.lgr.Infof("[MIoTClient] Central mips service scanned: %s, %v\n", homeID, sd)
c.lgr.Debugf("[MIoTClient] Central mips service scanned: %s, %v\n", homeID, sd)
mips := NewMipsLocalClient(
getVirtualDID(c.entryData),
sd.Addresses[0],
@@ -294,7 +294,7 @@ func (c *MIoTClient) Init() error {
if lan.InitDone() {
c.onMiotLanStateChange(true)
}
c.lgr.Infof("[MIoTClient] LAN control initialized\n")
c.lgr.Debugf("[MIoTClient] LAN control initialized\n")
}
}
} else {
@@ -324,7 +324,7 @@ func (c *MIoTClient) Init() error {
}
c.i18n = NewMIoTI18n(lang)
c.i18n.Init()
c.lgr.Infof("[MIoTClient] i18n initialized (lang=%s)\n", lang)
c.lgr.Debugf("[MIoTClient] i18n initialized (lang=%s)\n", lang)
c.lgr.Infof("[MIoTClient] Initialized\n")
return nil
@@ -358,7 +358,7 @@ func (c *MIoTClient) Deinit() error {
if c.mipsCloud != nil {
c.mipsCloud.UnsubState(key)
c.mipsCloud.Disconnect()
c.lgr.Infof("[MIoTClient] MIPS cloud disconnected\n")
c.lgr.Debugf("[MIoTClient] MIPS cloud disconnected\n")
}
// 4. Cancel refresh cloud devices timer
@@ -493,7 +493,7 @@ func (c *MIoTClient) loadCacheDevice() error {
c.deviceListCache[did] = infoMap
}
}
c.lgr.Infof("[MIoTClient] Loaded %d devices from cache\n", len(deviceList))
c.lgr.Debugf("[MIoTClient] Loaded %d devices from cache\n", len(deviceList))
return nil
}
@@ -647,7 +647,7 @@ func (c *MIoTClient) RefreshAuthInfo() (map[string]interface{}, error) {
c.mipsCloud.UpdateAccessToken(ac)
}
c.lgr.Infof("[MIoTClient] RefreshAuthInfo: token refreshed\n")
c.lgr.Debugf("[MIoTClient] RefreshAuthInfo: token refreshed\n")
return result, nil
}
@@ -690,7 +690,7 @@ func (c *MIoTClient) refreshUserCert() {
nextDelay = 60
}
c.RequestRefreshUserCert(nextDelay)
c.lgr.Infof("[MIoTClient] Cert still valid for %ds, recheck in %ds\n", remainSec, nextDelay)
c.lgr.Debugf("[MIoTClient] Cert still valid for %ds, recheck in %ds\n", remainSec, nextDelay)
return
}
@@ -720,7 +720,7 @@ func (c *MIoTClient) refreshUserCert() {
}
// Save cert
if err := c.cert.SaveUserCert(certPEM); err != nil {
if err = c.cert.SaveUserCert(certPEM); err != nil {
c.lgr.Errorf("[MIoTClient] Cert refresh: save cert FAILED: %v\n", err)
return
}
@@ -783,13 +783,13 @@ func (c *MIoTClient) ShowCentralStateChangedNotify(connected bool) {
// Aligns with Python: __on_mips_cloud_state_changed.
func (c *MIoTClient) onMipsCloudStateChanged(key string, connected bool) {
if connected {
c.lgr.Infof("[MIoTClient] MIPS cloud connected\n")
c.lgr.Debugf("[MIoTClient] MIPS cloud connected\n")
// Refresh devices on reconnect
c.ScheduleRefreshDevices(RefreshCloudDevicesDelay * time.Millisecond)
return
}
c.lgr.Infof("[MIoTClient] MIPS cloud disconnected\n")
c.lgr.Debugf("[MIoTClient] MIPS cloud disconnected\n")
// Disconnect: mark all cloud devices offline
c.deviceListMu.Lock()
@@ -821,7 +821,7 @@ func (c *MIoTClient) onMipsCloudStateChanged(key string, connected bool) {
// onNetworkStatusChanged handles network connectivity changes.
// Aligns with Python: __on_network_status_changed.
func (c *MIoTClient) onNetworkStatusChanged(status bool) {
c.lgr.Infof("[MIoTClient] Network status changed: online=%v\n", status)
c.lgr.Debugf("[MIoTClient] Network status changed: online=%v\n", status)
if status {
// Network is back; trigger device refresh and reconnect MIPS
if c.mipsCloud != nil && !c.mipsCloud.IsConnected() {
@@ -834,15 +834,15 @@ func (c *MIoTClient) onNetworkStatusChanged(status bool) {
// onMipsServiceStateChange handles mDNS service discoveries for central gateways.
// Aligns with Python: __on_mips_service_state_change.
func (c *MIoTClient) onMipsServiceStateChange(groupID string, state MipsServiceState, data map[string]interface{}) {
c.lgr.Infof("[MIoTClient] MIPS service change: group=%s, state=%s\n", groupID, state)
c.lgr.Debugf("[MIoTClient] MIPS service change: group=%s, state=%s\n", groupID, state)
virtualDID := getVirtualDID(c.entryData)
if virtualDID == "" {
c.lgr.Infof("[MIoTClient] MIPS service: missing virtual_did, skip\n")
c.lgr.Debugf("[MIoTClient] MIPS service: missing virtual_did, skip\n")
return
}
if c.cert == nil {
c.lgr.Infof("[MIoTClient] MIPS service: cert not initialized, skip\n")
c.lgr.Debugf("[MIoTClient] MIPS service: cert not initialized, skip\n")
return
}
@@ -851,7 +851,7 @@ func (c *MIoTClient) onMipsServiceStateChange(groupID string, state MipsServiceS
// Create new MipsLocalClient
host, _ := data["addresses"].([]interface{})
if len(host) == 0 {
c.lgr.Infof("[MIoTClient] MIPS service: no addresses for %s\n", groupID)
c.lgr.Debugf("[MIoTClient] MIPS service: no addresses for %s\n", groupID)
return
}
hostStr, _ := host[0].(string)
@@ -872,7 +872,7 @@ func (c *MIoTClient) onMipsServiceStateChange(groupID string, state MipsServiceS
mips.SubState(groupID, c.onMipsLocalStateChanged)
mips.Connect()
c.mipsLocal[groupID] = mips
c.lgr.Infof("[MIoTClient] MIPS local client created: %s @ %s:%d\n", groupID, hostStr, int(port))
c.lgr.Debugf("[MIoTClient] MIPS local client created: %s @ %s:%d\n", groupID, hostStr, int(port))
case MipsServiceUpdated:
// Disconnect old, connect new
@@ -902,7 +902,7 @@ func (c *MIoTClient) onMipsServiceStateChange(groupID string, state MipsServiceS
mips.SubState(groupID, c.onMipsLocalStateChanged)
mips.Connect()
c.mipsLocal[groupID] = mips
c.lgr.Infof("[MIoTClient] MIPS local client updated: %s @ %s:%d\n", groupID, hostStr, int(port))
c.lgr.Debugf("[MIoTClient] MIPS local client updated: %s @ %s:%d\n", groupID, hostStr, int(port))
case MipsServiceRemoved:
if mips, exists := c.mipsLocal[groupID]; exists {
@@ -910,7 +910,7 @@ func (c *MIoTClient) onMipsServiceStateChange(groupID string, state MipsServiceS
mips.UnsubState(groupID)
mips.Disconnect()
delete(c.mipsLocal, groupID)
c.lgr.Infof("[MIoTClient] MIPS local client removed: %s\n", groupID)
c.lgr.Debugf("[MIoTClient] MIPS local client removed: %s\n", groupID)
}
}
}
@@ -919,7 +919,7 @@ func (c *MIoTClient) onMipsServiceStateChange(groupID string, state MipsServiceS
// Aligns with Python: __on_mips_local_state_changed.
// key is the groupID of the gateway.
func (c *MIoTClient) onMipsLocalStateChanged(key string, connected bool) {
c.lgr.Infof("[MIoTClient] MIPS local state changed: group=%s, connected=%v\n", key, connected)
c.lgr.Debugf("[MIoTClient] MIPS local state changed: group=%s, connected=%v\n", key, connected)
if connected {
// Reconnect: pull gateway device list
@@ -966,7 +966,7 @@ func (c *MIoTClient) onMipsLocalStateChanged(key string, connected bool) {
// onMiotLanStateChange handles LAN controller state transitions.
// Aligns with Python: __on_miot_lan_state_change.
func (c *MIoTClient) onMiotLanStateChange(initialized bool) {
c.lgr.Infof("[MIoTClient] LAN state changed: initialized=%v\n", initialized)
c.lgr.Debugf("[MIoTClient] LAN state changed: initialized=%v\n", initialized)
if initialized {
c.deviceListMu.Lock()
lan, ok := c.miotLan.(*MIoTLan)
@@ -1026,7 +1026,7 @@ func (c *MIoTClient) onLanDeviceStateChanged(did string, state map[string]interf
// Aligns with Python: __on_gw_device_list_changed.
// Called when a MipsLocalClient reports that its device list has changed.
func (c *MIoTClient) onGWDeviceListChanged() {
c.lgr.Infof("[MIoTClient] GW device list changed\n")
c.lgr.Debugf("[MIoTClient] GW device list changed\n")
for groupID, mips := range c.mipsLocal {
if mips == nil || !mips.IsConnected() {
@@ -1090,7 +1090,7 @@ func (c *MIoTClient) updateDevicesFromGW(groupID string, devices map[string]inte
}
if changed {
c.lgr.Infof("[MIoTClient] GW devices updated: group=%s, count=%d\n", groupID, len(devices))
c.lgr.Debugf("[MIoTClient] GW devices updated: group=%s, count=%d\n", groupID, len(devices))
}
}
+19 -1
View File
@@ -31,8 +31,26 @@ func (c *MIoTClient) SetProp(did string, siid, piid int, value interface{}) erro
return err
}
// Aligns with Python: 空 result 视为失败(Python 会 raise device_exec_error)
if len(results) == 0 {
return fmt.Errorf("set property failed: empty result (device may be offline)")
}
// code=0 或 code=1 均视为成功(0=读取成功,1=设置成功)
if len(results) > 0 && results[0].Code != 0 && results[0].Code != 1 {
if results[0].Code != 0 && results[0].Code != 1 {
// 设备移除或离线:刷新设备列表(Aligns with Python)
if results[0].Code == -704010000 || results[0].Code == -704042011 {
c.lgr.Errorf("[MIoTClient] device may be removed or offline, %s\n", did)
if devs, rerr := c.http.GetDevices(nil); rerr == nil {
c.deviceListMu.Lock()
for k := range c.deviceListCloud {
if _, ok := devs[k]; !ok {
delete(c.deviceListCloud, k)
}
}
c.deviceListMu.Unlock()
}
}
return fmt.Errorf("set property failed, code=%d", results[0].Code)
}
+1 -1
View File
@@ -155,7 +155,7 @@ func (c *MIoTClient) updateDeviceMsgSub(did string) {
// Update subscription source
c.subSourceList[did] = newSource
c.lgr.Infof("[MIoTClient] updateDeviceMsgSub: %s source %s → %s (gw=%v/%v, lan=%v/%v, cloud=%v)\n",
c.lgr.Debugf("[MIoTClient] updateDeviceMsgSub: %s source %s → %s (gw=%v/%v, lan=%v/%v, cloud=%v)\n",
did, oldSource, newSource, gwOnline, gwPushAvailable, lanOnline, lanPushAvailable, cloudOnline)
}
+28 -7
View File
@@ -477,7 +477,7 @@ func (c *MIoTHttpClient) getDevRoomPage(maxID string) (map[string]map[string]int
}
// Check for more pages
hasMore, _ := result["has_more"].(bool)
hasMore := parseBool(result["has_more"])
nextMaxID, _ := result["max_id"].(string)
if hasMore && nextMaxID != "" {
nextList, err := c.getDevRoomPage(nextMaxID)
@@ -582,11 +582,13 @@ func (c *MIoTHttpClient) GetHomeInfos() (map[string]interface{}, error) {
}
// Handle pagination
hasMore, _ := result["has_more"].(bool)
hasMore := parseBool(result["has_more"])
maxID, _ := result["max_id"].(string)
if hasMore && maxID != "" {
moreList, err := c.getDevRoomPage(maxID)
if err == nil {
if err != nil {
return nil, err
}
for _, deviceSource := range []string{"homelist", "share_home_list"} {
for hidStr, info := range moreList {
if _, exists := homeInfos[deviceSource][hidStr]; !exists {
@@ -621,7 +623,6 @@ func (c *MIoTHttpClient) GetHomeInfos() (map[string]interface{}, error) {
}
}
}
}
return map[string]interface{}{
"uid": uid,
@@ -725,15 +726,16 @@ func (c *MIoTHttpClient) getDeviceListPage(dids []string, startDID string) (map[
// Handle pagination
nextStartDID, _ := result["next_start_did"].(string)
hasMore, _ := result["has_more"].(bool)
hasMore := parseBool(result["has_more"])
if hasMore && nextStartDID != "" {
nextInfos, err := c.getDeviceListPage(dids, nextStartDID)
if err == nil {
if err != nil {
return nil, err
}
for k, v := range nextInfos {
deviceInfos[k] = v
}
}
}
return deviceInfos, nil
}
@@ -1196,3 +1198,22 @@ func containsString(slice []string, val string) bool {
}
return false
}
// parseBool 兼容云端多种布尔表达(bool / 0|1 / "true"|"false")。
func parseBool(v interface{}) bool {
switch n := v.(type) {
case bool:
return n
case float64:
return n != 0
case float32:
return n != 0
case int:
return n != 0
case int64:
return n != 0
case string:
return n == "true" || n == "1"
}
return false
}
+142
View File
@@ -0,0 +1,142 @@
package miot
// MIoT 云端错误码
// 来源: https://iot.mi.com/v2/new/doc/help-support/faq/spec/error-code
const (
ErrCodeOK = -702000000 // OK
ErrCodeAccept = -702010000 // accept
ErrCodeProcessing = -702022036 // 操作正在处理中
ErrCodeBadRequest = -704000000 // 错误的请求
ErrCodeBadBody = -704000001 // 错误的请求体
ErrCodeScheduleOnlyMi = -704001000 // 定时只支持小米设备
ErrCodeDeviceError = -704002000 // 设备错误(通用)
ErrCodeUnauthenticated = -704010000 // 未认证
ErrCodeTokenExpired = -704012901 // token 不存在或过期
ErrCodeTokenInvalid = -704012902 // token 非法
ErrCodeAuthExpired = -704012903 // 授权过期
ErrCodeNotAuthToXiaoAi = -704012904 // 设备未授权控制能力给小爱
ErrCodeDeviceUnbound = -704012905 // 设备未绑定
ErrCodeAuthFailed = -704012906 // 认证失败
ErrCodeIRNotSupported = -704013101 // 红外设备不支持此操作
ErrCodePropertyNotRead = -704030013 // 属性不可读
ErrCodePropertyNotWrite = -704030023 // 属性不可写
ErrCodePropertyNotNotify = -704030033 // 属性不可上报
ErrCodeTooFrequent = -704030992 // 请求过于频繁
ErrCodeServiceNotFound = -704040002 // 服务不存在
ErrCodePropertyNotFound = -704040003 // 属性不存在
ErrCodeEventNotFound = -704040004 // 事件不存在
ErrCodeMethodNotFound = -704040005 // 方法不存在
ErrCodeFeatureOffline = -704040999 // 功能未上线
ErrCodeCloudNotFound = -704041007 // cloud not found
ErrCodeDeviceNotFound2 = -704042001 // 未找到设备
ErrCodeSceneNotFound = -704042009 // 未找到场景
ErrCodeSceneError = -704042010 // 触发场景异常
ErrCodeDeviceOffline = -704042011 // 设备离线
ErrCodeSceneNoPermission = -704042012 // 场景无权限
ErrCodeSpecNotFound = -704044006 // 未找到功能定义
ErrCodeCannotExecute = -704053100 // 无法执行此操作
ErrCodeCameraSleeping = -704053101 // 摄像机休眠中
ErrCodeTimeout = -704083036 // 操作超时
ErrCodeDeviceNotFound = -704090001 // 未找到设备
ErrCodeInvalidID = -704220008 // 非法的 ID
ErrCodeInvalidUID = -704220009 // 非法的 uid
ErrCodeInvalidSubID = -704220010 // 非法的订阅 ID
ErrCodeActionParamCount = -704220025 // 方法输入参数数量不匹配
ErrCodeActionParamError = -704220035 // 方法输入参数错误
ErrCodeInvalidPropValue = -704220043 // 属性值不正确
ErrCodeEventParamCount = -704222034 // 事件参数数量不匹配
ErrCodeActionOutParamError = -704222035 // 方法输出参数错误
ErrCodeOpenPlatformError = -705001000 // 开放平台服务器内部错误
ErrCodeSpecServerError = -705004000 // MIoT Spec 服务器内部错误
ErrCodeSubIDEncodeError = -705005000 // 订阅 ID 编码错误
ErrCodeNotifyError = -705006000 // notify error
ErrCodeReadPropFailed = -705201013 // 读属性失败
ErrCodeActionFailed = -705201015 // 方法执行失败
ErrCodeWritePropFailed = -705201023 // 写属性失败
ErrCodeReportPropFailed = -705201033 // 上报属性失败
ErrCodeInvalidSpec = -705204006 // 非法的功能定义
ErrCodeInstanceNotLoaded = -705204007 // 实例未加载
ErrCodeRemoteServiceError = -706010002 // 远程服务异常
ErrCodeMIoTError = -706010004 // MIoT 错误
ErrCodePropCacheFailed = -706010005 // 属性缓存失败
ErrCodeThirdPartyError = -706012000 // 三方云服务器内部错误
ErrCodeThirdReadPropFailed = -706012013 // 三方云读属性失败
ErrCodeThirdActionFailed = -706012015 // 三方云方法执行失败
ErrCodeThirdWritePropFailed = -706012023 // 三方云写属性失败
ErrCodeThirdReportPropFailed = -706012033 // 三方云上报属性失败
ErrCodeThirdSubFailed = -706012043 // 三方云订阅失败
)
// errDescriptions 错误码说明
var errDescriptions = map[int]string{
ErrCodeOK: "OK",
ErrCodeAccept: "accept",
ErrCodeProcessing: "操作正在处理中",
ErrCodeBadRequest: "错误的请求",
ErrCodeBadBody: "错误的请求体",
ErrCodeScheduleOnlyMi: "定时只支持小米设备",
ErrCodeDeviceError: "设备错误(通用)",
ErrCodeUnauthenticated: "未认证",
ErrCodeTokenExpired: "token 不存在或过期",
ErrCodeTokenInvalid: "token 非法",
ErrCodeAuthExpired: "授权过期",
ErrCodeNotAuthToXiaoAi: "设备未授权控制能力给小爱",
ErrCodeDeviceUnbound: "设备未绑定",
ErrCodeAuthFailed: "认证失败",
ErrCodeIRNotSupported: "红外设备不支持此操作",
ErrCodePropertyNotRead: "属性不可读",
ErrCodePropertyNotWrite: "属性不可写",
ErrCodePropertyNotNotify: "属性不可上报",
ErrCodeTooFrequent: "请求过于频繁",
ErrCodeServiceNotFound: "服务不存在",
ErrCodePropertyNotFound: "属性不存在",
ErrCodeEventNotFound: "事件不存在",
ErrCodeMethodNotFound: "方法不存在",
ErrCodeFeatureOffline: "功能未上线",
ErrCodeCloudNotFound: "cloud not found",
ErrCodeDeviceNotFound2: "未找到设备",
ErrCodeSceneNotFound: "未找到场景",
ErrCodeSceneError: "触发场景异常",
ErrCodeDeviceOffline: "设备离线",
ErrCodeSceneNoPermission: "场景无权限",
ErrCodeSpecNotFound: "未找到功能定义",
ErrCodeCannotExecute: "无法执行此操作",
ErrCodeCameraSleeping: "摄像机休眠中",
ErrCodeTimeout: "操作超时",
ErrCodeDeviceNotFound: "未找到设备",
ErrCodeInvalidID: "非法的 ID",
ErrCodeInvalidUID: "非法的 uid",
ErrCodeInvalidSubID: "非法的订阅 ID",
ErrCodeActionParamCount: "方法输入参数数量不匹配",
ErrCodeActionParamError: "方法输入参数错误",
ErrCodeInvalidPropValue: "属性值不正确",
ErrCodeEventParamCount: "事件参数数量不匹配",
ErrCodeActionOutParamError: "方法输出参数错误",
ErrCodeOpenPlatformError: "开放平台服务器内部错误",
ErrCodeSpecServerError: "MIoT Spec 服务器内部错误",
ErrCodeSubIDEncodeError: "订阅 ID 编码错误",
ErrCodeNotifyError: "notify error",
ErrCodeReadPropFailed: "读属性失败",
ErrCodeActionFailed: "方法执行失败",
ErrCodeWritePropFailed: "写属性失败",
ErrCodeReportPropFailed: "上报属性失败",
ErrCodeInvalidSpec: "非法的功能定义",
ErrCodeInstanceNotLoaded: "实例未加载",
ErrCodeRemoteServiceError: "远程服务异常",
ErrCodeMIoTError: "MIoT 错误",
ErrCodePropCacheFailed: "属性缓存失败",
ErrCodeThirdPartyError: "三方云服务器内部错误",
ErrCodeThirdReadPropFailed: "三方云读属性失败",
ErrCodeThirdActionFailed: "三方云方法执行失败",
ErrCodeThirdWritePropFailed: "三方云写属性失败",
ErrCodeThirdReportPropFailed: "三方云上报属性失败",
ErrCodeThirdSubFailed: "三方云订阅失败",
}
// ErrDesc 返回错误码的中文说明。
func ErrDesc(code int) string {
if s, ok := errDescriptions[code]; ok {
return s
}
return "未知错误"
}
+42 -18
View File
@@ -25,7 +25,6 @@ const (
mipsQoS = 2
mipsReconnectIntervalMin = 10.0 // seconds
mipsReconnectIntervalMax = 600.0 // seconds
mipsSubPatch = 300
mipsSubInterval = 1 // second
mipsMQTTInterval = 1 // second
uint32max = 0xFFFFFFFF
@@ -269,6 +268,7 @@ type MQTTClient interface {
Disconnect()
IsConnected() bool
Subscribe(topic string, qos byte, handler func(topic string, payload []byte)) error
SubscribeMany(topics map[string]byte) error
Unsubscribe(topic string) error
Publish(topic string, qos byte, payload []byte) error
SetCredentials(username, password string)
@@ -528,37 +528,45 @@ func (c *mipsClient) flushPendingSubs() {
return
}
// 收集所有待订阅 topic,一次 SUBSCRIBE 包批量订阅(大幅减少网络往返)
c.pendingSubMu.Lock()
subbed := 0
batch := make(map[string]byte, len(c.pendingSubs))
for topic, count := range c.pendingSubs {
if subbed >= mipsSubPatch {
break
}
if count > 3 {
delete(c.pendingSubs, topic)
c.lgr.Debugf("[mips] retry sub exceeded: %s", topic)
continue
}
subbed++
if err := c.mqttConn.Subscribe(topic, byte(mipsQoS), func(t string, p []byte) {
c.onMQTTMessage(t, p)
}); err == nil {
delete(c.pendingSubs, topic)
c.lgr.Debugf("[mips] sub success: %s", topic)
} else {
c.pendingSubs[topic] = count + 1
c.lgr.Debugf("[mips] retry sub %d: %s %v", count, topic, err)
batch[topic] = byte(mipsQoS)
}
}
hasMore := len(c.pendingSubs) > 0
c.pendingSubMu.Unlock()
if len(batch) == 0 {
return
}
if err := c.mqttConn.SubscribeMany(batch); err == nil {
c.pendingSubMu.Lock()
for t := range batch {
delete(c.pendingSubs, t)
c.lgr.Debugf("[mips] sub success: %s", t)
}
hasMore := len(c.pendingSubs) > 0
c.pendingSubMu.Unlock()
if hasMore {
time.Sleep(time.Duration(mipsSubInterval) * time.Second)
c.flushPendingSubs()
}
} else {
c.lgr.Debugf("[mips] batch sub error: %v", err)
c.pendingSubMu.Lock()
for t := range batch {
c.pendingSubs[t] = c.pendingSubs[t] + 1
}
c.pendingSubMu.Unlock()
time.Sleep(time.Duration(mipsSubInterval) * time.Second)
c.flushPendingSubs()
}
}
// sendRequest sends a MIPS request and waits for a reply.
@@ -1042,6 +1050,22 @@ func (p *pahoMQTTClient) Subscribe(topic string, qos byte, handler func(topic st
return nil
}
// SubscribeMany 用单个 SUBSCRIBE 包批量订阅多个 topic,显著减少网络往返。
// 消息分发仍走 DefaultPublishHandler,与 Subscribe 行为一致。
func (p *pahoMQTTClient) SubscribeMany(topics map[string]byte) error {
if p.client == nil || !p.client.IsConnected() {
return fmt.Errorf("not connected")
}
if len(topics) == 0 {
return nil
}
token := p.client.SubscribeMultiple(topics, nil)
if token.Wait() && token.Error() != nil {
return token.Error()
}
return nil
}
func (p *pahoMQTTClient) Unsubscribe(topic string) error {
if p.client == nil || !p.client.IsConnected() {
return fmt.Errorf("not connected")
-3
View File
@@ -183,9 +183,6 @@ func TestMipsConstants(t *testing.T) {
if mipsQoS != 2 {
t.Errorf("mipsQoS = %d, want 2", mipsQoS)
}
if mipsSubPatch != 300 {
t.Errorf("mipsSubPatch = %d, want 300", mipsSubPatch)
}
if MipsRequestTimeoutDefault != 10000 {
t.Errorf("MipsRequestTimeoutDefault = %d, want 10000", MipsRequestTimeoutDefault)
}
+3 -3
View File
@@ -38,9 +38,9 @@ var deviceTypeToKind = map[string]Kind{
"water-heater": KindWaterHeater,
"water-cooler": KindWaterHeater,
"kettle": KindWaterHeater,
"heater": KindThermostat,
"electric-blanket": KindThermostat,
"thermostat": KindThermostat,
"heater": KindAirConditioner,
"electric-blanket": KindAirConditioner,
"thermostat": KindAirConditioner,
"speaker": KindSpeaker,
"bluetooth-speaker": KindSpeaker,
"alarm-clock": KindSpeaker,
+45 -3
View File
@@ -199,25 +199,67 @@ func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirCondi
return nil
}
// "on" is required
// findValListInService 从指定服务(短名)内查找属性的 ValueList。
findValListInService := func(svcName, propType string) miot.MIoTSpecValueList {
for _, svc := range spec.Services {
if serviceShortName(svc.Type) != svcName {
continue
}
for _, p := range svc.Properties {
if propertyShortName(p.Type) == propType {
return p.ValueList
}
}
}
return nil
}
// "on" is required。
// 三合一设备(thermostat+air-conditioner+air-fresh 多服务同名冲突)的空调实体在 thermostat 服务:
// 存在 thermostat 服务时优先从它取 on/target-temperature,否则取第一个同名属性(普通空调)。
if resolver.hasService("thermostat") {
if s, p, ok := resolver.findInService("thermostat", "on"); ok {
d.propOn = siidPiid{s, p, true}
} else if s, p, ok := resolver.find("on"); ok {
d.propOn = siidPiid{s, p, true}
}
} else {
siid, piid, ok := resolver.find("on")
if !ok {
return nil, xiaomi.ErrNotSupported
}
d.propOn = siidPiid{siid, piid, true}
}
// Optional properties — each independently resolved
if s, p, ok := resolver.find("mode"); ok {
d.propMode = siidPiid{s, p, true}
d.modeMap = newValMapper(findValList("mode"))
}
if s, p, ok := resolver.find("target-temperature"); ok {
if resolver.hasService("thermostat") {
// 三合一设备:target-temperature 优先 thermostat 服务(空调温度)
if s, p, ok := resolver.findInService("thermostat", "target-temperature"); ok {
d.propTargetTemp = siidPiid{s, p, true}
} else if s, p, ok := resolver.find("target-temperature"); ok {
d.propTargetTemp = siidPiid{s, p, true}
}
} else if s, p, ok := resolver.find("target-temperature"); ok {
d.propTargetTemp = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("temperature"); ok {
d.propCurrentTemp = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("fan-level"); ok {
// fan-level:三合一设备优先 thermostat 服务(空调风速 siid=4/piid=3),
// 否则取第一个同名属性(普通空调 air-conditioner 服务 / air-fresh 新风机)。
if resolver.hasService("thermostat") {
if s, p, ok := resolver.findInService("thermostat", "fan-level"); ok {
d.propFanSpeed = siidPiid{s, p, true}
d.fanMap = newValMapper(findValListInService("thermostat", "fan-level"))
} else if s, p, ok := resolver.find("fan-level"); ok {
d.propFanSpeed = siidPiid{s, p, true}
d.fanMap = newValMapper(findValList("fan-level"))
}
} else if s, p, ok := resolver.find("fan-level"); ok {
d.propFanSpeed = siidPiid{s, p, true}
d.fanMap = newValMapper(findValList("fan-level"))
}
+46 -1
View File
@@ -142,7 +142,8 @@ func (d *BaseDevice) getSpecResolver() *specResolver {
// specResolver is a thin wrapper to avoid importing specs package in every device file.
type specResolver struct {
byType map[string]*miot.MIoTSpecProperty
byType map[string]*miot.MIoTSpecProperty // property short name → first match
services []*miot.MIoTSpecService // 保留服务列表(支持按服务查找)
}
func newSpecResolver(spec *miot.MIoTSpecInstance) *specResolver {
@@ -151,6 +152,7 @@ func newSpecResolver(spec *miot.MIoTSpecInstance) *specResolver {
}
r := &specResolver{
byType: make(map[string]*miot.MIoTSpecProperty),
services: spec.Services,
}
for _, svc := range spec.Services {
for i := range svc.Properties {
@@ -176,6 +178,17 @@ func propertyShortName(urn string) string {
return urn
}
// serviceShortName extracts the short name from a SPEC service URN.
// E.g., "urn:miot-spec-v2:service:thermostat:0000784A:..." → "thermostat".
// 注意:不能用 propertyShortName 解析服务 URN(其只认 parts[2]=="property")。
func serviceShortName(urn string) string {
parts := strings.Split(urn, ":")
if len(parts) >= 4 && parts[2] == "service" {
return parts[3]
}
return urn
}
func (r *specResolver) find(propType string) (siid, piid int, found bool) {
if r == nil {
return 0, 0, false
@@ -186,6 +199,38 @@ func (r *specResolver) find(propType string) (siid, piid int, found bool) {
return 0, 0, false
}
// findInService 在指定服务(短名,如 "thermostat")内查找属性。
// 用于多服务同名属性冲突时按服务精确定位(如三合一设备空调实体在 thermostat 服务)。
func (r *specResolver) findInService(svcName, propType string) (siid, piid int, found bool) {
if r == nil {
return 0, 0, false
}
for _, svc := range r.services {
if svc == nil || serviceShortName(svc.Type) != svcName {
continue
}
for _, prop := range svc.Properties {
if prop != nil && propertyShortName(prop.Type) == propType {
return svc.IID, prop.PIID, true
}
}
}
return 0, 0, false
}
// hasService 判断设备是否包含指定服务(短名)。
func (r *specResolver) hasService(svcName string) bool {
if r == nil {
return false
}
for _, svc := range r.services {
if svc != nil && serviceShortName(svc.Type) == svcName {
return true
}
}
return false
}
// prop returns the full SPEC property for the given type name.
func (r *specResolver) prop(propType string) *miot.MIoTSpecProperty {
if r == nil {
+75 -19
View File
@@ -5,6 +5,13 @@ import (
"xiaomihome/xiaomi"
)
// CoverProps 窗帘完整状态。
type CoverProps struct {
Position int `json:"position"` // 当前位置 0-100
Status int `json:"status"` // 运动状态:0=停止 1=开启中 2=关闭中
Fault int `json:"fault"` // 故障码,0=无故障
}
// Cover 窗帘/晾衣架统一控制接口
type Cover interface {
Device
@@ -15,17 +22,20 @@ type Cover interface {
SetPosition(ctx context.Context, pct int) error
GetPosition(ctx context.Context) (int, error)
OnPropsChanged(handler func(position int)) (string, error)
GetProps() int
FetchProps(ctx context.Context) (int, error)
OnPropsChanged(handler func(props CoverProps)) (string, error)
GetProps() CoverProps
FetchProps(ctx context.Context) (CoverProps, error)
}
type coverDevice struct {
BaseDevice
propMotor siidPiid
propPosition siidPiid
propMotor siidPiid // motor-control(write)
propPosition siidPiid // current-position(read)
propTarget siidPiid // target-position(write,SetPosition 用)
propStatus siidPiid // status(read)
propFault siidPiid // fault(read)
propsCache int
propsCache CoverProps
}
const (
@@ -48,6 +58,15 @@ func NewCover(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Cover, error) {
if s, p, ok := resolver.find("current-position"); ok {
d.propPosition = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("target-position"); ok {
d.propTarget = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("status"); ok {
d.propStatus = siidPiid{s, p, true}
}
if s, p, ok := resolver.find("fault"); ok {
d.propFault = siidPiid{s, p, true}
}
return d, nil
}
@@ -63,10 +82,19 @@ func (d *coverDevice) Stop(ctx context.Context) error {
if !d.propMotor.valid { return xiaomi.ErrNotSupported }
return d.SetProp(ctx, d.propMotor.siid, d.propMotor.piid, coverMotorStop)
}
// SetPosition 设定目标位置,写 target-position(可写属性)。
// 老设备无 target-position 时回退写 current-position。
func (d *coverDevice) SetPosition(ctx context.Context, pct int) error {
if !d.propPosition.valid { return xiaomi.ErrNotSupported }
if d.propTarget.valid {
return d.SetProp(ctx, d.propTarget.siid, d.propTarget.piid, pct)
}
if d.propPosition.valid {
return d.SetProp(ctx, d.propPosition.siid, d.propPosition.piid, pct)
}
return xiaomi.ErrNotSupported
}
func (d *coverDevice) GetPosition(ctx context.Context) (int, error) {
if !d.propPosition.valid { return 0, xiaomi.ErrNotSupported }
prop, err := d.GetProp(ctx, d.propPosition.siid, d.propPosition.piid)
@@ -74,20 +102,48 @@ func (d *coverDevice) GetPosition(ctx context.Context) (int, error) {
return toInt(prop.Value), nil
}
func (d *coverDevice) OnPropsChanged(handler func(position int)) (string, error) {
if !d.propPosition.valid { return "", xiaomi.ErrNotSupported }
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
if prop.SIID == d.propPosition.siid && prop.PIID == d.propPosition.piid {
d.propsCache = toInt(prop.Value)
handler(d.propsCache)
func (d *coverDevice) OnPropsChanged(handler func(props CoverProps)) (string, error) {
if !d.propPosition.valid && !d.propStatus.valid && !d.propFault.valid {
return "", xiaomi.ErrNotSupported
}
return d.SubProp(0, 0, func(did string, prop *xiaomi.PropertyValue) {
switch {
case d.propPosition.valid && prop.SIID == d.propPosition.siid && prop.PIID == d.propPosition.piid:
d.propsCache.Position = toInt(prop.Value)
case d.propStatus.valid && prop.SIID == d.propStatus.siid && prop.PIID == d.propStatus.piid:
d.propsCache.Status = toInt(prop.Value)
case d.propFault.valid && prop.SIID == d.propFault.siid && prop.PIID == d.propFault.piid:
d.propsCache.Fault = toInt(prop.Value)
default:
return // 其他属性不触发回调
}
handler(d.propsCache)
})
}
func (d *coverDevice) GetProps() int { return d.propsCache }
func (d *coverDevice) FetchProps(ctx context.Context) (int, error) {
pos, err := d.GetPosition(ctx)
if err != nil { return d.propsCache, err }
d.propsCache = pos
return pos, nil
func (d *coverDevice) GetProps() CoverProps { return d.propsCache }
func (d *coverDevice) FetchProps(ctx context.Context) (CoverProps, error) {
keys := []xiaomi.PropKey{}
if d.propPosition.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propPosition.siid, PIID: d.propPosition.piid}) }
if d.propStatus.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propStatus.siid, PIID: d.propStatus.piid}) }
if d.propFault.valid { keys = append(keys, xiaomi.PropKey{DID: d.info.DID, SIID: d.propFault.siid, PIID: d.propFault.piid}) }
if len(keys) == 0 {
return d.propsCache, xiaomi.ErrNotSupported
}
props, err := d.client.GetProps(ctx, keys)
if err != nil {
return d.propsCache, err
}
for _, p := range props {
switch {
case d.propPosition.valid && p.SIID == d.propPosition.siid && p.PIID == d.propPosition.piid:
d.propsCache.Position = toInt(p.Value)
case d.propStatus.valid && p.SIID == d.propStatus.siid && p.PIID == d.propStatus.piid:
d.propsCache.Status = toInt(p.Value)
case d.propFault.valid && p.SIID == d.propFault.siid && p.PIID == d.propFault.piid:
d.propsCache.Fault = toInt(p.Value)
}
}
return d.propsCache, nil
}
+29
View File
@@ -131,6 +131,18 @@ func TestMatchURN(t *testing.T) {
[]string{"heater"},
true,
},
{
"tofan-wk01 not fan", // "tofan" 含 "fan" 子串,不得误匹配 fan 系列
"urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01:1:0000C822",
[]string{"fan", "ceiling-fan", "ventilator", "air-fresh", "air-purifier"},
false,
},
{
"tofan-wk01 thermostat",
"urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01:1:0000C822",
[]string{"heater", "electric-blanket", "thermostat"},
true,
},
{
"no match",
"urn:miot-spec-v2:device:unknown:0000A099:test:1",
@@ -171,3 +183,20 @@ func TestCreateNilInfo(t *testing.T) {
t.Error("expected error for nil info")
}
}
func TestServiceShortName(t *testing.T) {
cases := []struct {
urn string
want string
}{
{"urn:miot-spec-v2:service:thermostat:0000784A:tofan-wk01:1", "thermostat"},
{"urn:miot-spec-v2:service:air-conditioner:0000780F:tofan-wk01:1", "air-conditioner"},
{"urn:tofan-spec:service:adv-setting:00007801:tofan-wk01:1", "adv-setting"},
{"urn:miot-spec-v2:property:on:00000006:tofan-wk01:1", "urn:miot-spec-v2:property:on:00000006:tofan-wk01:1"},
}
for _, c := range cases {
if got := serviceShortName(c.urn); got != c.want {
t.Errorf("serviceShortName(%q) = %q, want %q", c.urn, got, c.want)
}
}
}
+41 -6
View File
@@ -1,6 +1,8 @@
package devices
import (
"strings"
"xiaomihome/xiaomi"
)
@@ -84,7 +86,7 @@ func createImpl(client *xiaomi.Client, info *xiaomi.DeviceInfo) (interface{}, er
case matchURN(urn, "water-heater", "water-cooler", "kettle"):
ctrl, err = NewWaterHeater(client, info)
case matchURN(urn, "heater", "electric-blanket", "thermostat"):
ctrl, err = NewThermostat(client, info)
ctrl, err = NewAirConditioner(client, info)
case matchURN(urn, "sensor-occupy", "sensor_occupy", "occupancy-sensor", "occupancy_sensor", "motion-sensor", "motion_sensor", "magnet"):
ctrl, err = NewOccupancySensor(client, info)
case matchURN(urn, "sensor-temp", "sensor_temp", "sensor-humidity", "sensor_humidity", "sensor-ht", "sensor_ht", "sensor-smoke", "sensor-gasm", "illumination"):
@@ -105,17 +107,50 @@ func createImpl(client *xiaomi.Client, info *xiaomi.DeviceInfo) (interface{}, er
return ctrl, nil
}
// matchURN checks if the device URN contains any of the given type strings.
// matchURN 匹配 URN 的 device 类型段。
// 标准格式 urn:miot-spec-v2:device:{TYPE}:... 做精确段匹配(避免 "tofan" 撞 "fan" 这类子串误判);
// 非标准 URN 回退到边界子串匹配。
func matchURN(urn string, types ...string) bool {
parts := strings.Split(urn, ":")
if len(parts) >= 4 && parts[2] == "device" {
devType := parts[3]
for _, t := range types {
if len(urn) >= len(t) {
// Simple substring match — URN format: urn:miot-spec-v2:device:TYPE:...
for i := 0; i <= len(urn)-len(t); i++ {
if urn[i:i+len(t)] == t {
if devType == t {
return true
}
}
return false
}
// 非标准 URN:按边界匹配,前后必须是分隔符,避免误撞
for _, t := range types {
if boundaryContains(urn, t) {
return true
}
}
return false
}
// boundaryContains 检查 sub 是否在 s 中出现且前后是分隔符或边界。
func boundaryContains(s, sub string) bool {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] != sub {
continue
}
beforeOK := i == 0
if !beforeOK {
beforeOK = isBoundary(s[i-1])
}
afterOK := i+len(sub) == len(s)
if !afterOK {
afterOK = isBoundary(s[i+len(sub)])
}
if beforeOK && afterOK {
return true
}
}
return false
}
func isBoundary(c byte) bool {
return c == ':' || c == '-' || c == '.' || c == '_' || c == '/'
}
+22
View File
@@ -0,0 +1,22 @@
package devices
import "xiaomihome/xiaomi"
// Gateway 网关设备(仅类型识别,不参与控制)。
type Gateway interface {
Device
}
type gatewayDevice struct {
BaseDevice
}
// NewGateway creates a Gateway device wrapper for type recognition.
func NewGateway(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Gateway, error) {
if info == nil {
return nil, xiaomi.ErrDeviceNotFound
}
return &gatewayDevice{
BaseDevice: NewBaseDevice(client, info),
}, nil
}
+102
View File
@@ -0,0 +1,102 @@
package devices
import (
"context"
"fmt"
"xiaomihome/xiaomi"
)
// Plug 插座/插头统一控制接口(电脑开机卡、智能插座等)。
type Plug interface {
Device
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
IsOn(ctx context.Context) (bool, error)
OnPropsChanged(handler func(on bool)) (string, error)
GetProps() PlugProps
FetchProps(ctx context.Context) (PlugProps, error)
}
// PlugProps 插座完整状态。
type PlugProps struct {
On bool `json:"on"`
}
type plugDevice struct {
BaseDevice
propOn siidPiid
propsCache PlugProps
}
// NewPlug creates a Plug control for the given device.
func NewPlug(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Plug, error) {
if info == nil {
return nil, xiaomi.ErrDeviceNotFound
}
miotDev := client.GetMIoTDevice(info.DID)
if miotDev == nil || miotDev.SpecInstance() == nil {
return nil, fmt.Errorf("%w: device has no SPEC", xiaomi.ErrNotSupported)
}
d := &plugDevice{
BaseDevice: NewBaseDevice(client, info),
}
for _, svc := range miotDev.SpecInstance().Services {
for _, prop := range svc.Properties {
if propertyShortName(prop.Type) == "on" && !d.propOn.valid {
d.propOn = siidPiid{prop.SIID, prop.PIID, true}
}
}
}
if !d.propOn.valid {
return nil, fmt.Errorf("%w: no on property", xiaomi.ErrNotSupported)
}
return d, nil
}
func (d *plugDevice) TurnOn(ctx context.Context) error {
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, true)
}
func (d *plugDevice) TurnOff(ctx context.Context) error {
return d.SetProp(ctx, d.propOn.siid, d.propOn.piid, false)
}
func (d *plugDevice) IsOn(ctx context.Context) (bool, error) {
prop, err := d.GetProp(ctx, d.propOn.siid, d.propOn.piid)
if err != nil {
return false, err
}
b, _ := prop.Value.(bool)
return b, nil
}
func (d *plugDevice) OnPropsChanged(handler func(on bool)) (string, error) {
return d.SubProp(d.propOn.siid, d.propOn.piid, func(did string, prop *xiaomi.PropertyValue) {
if b, ok := prop.Value.(bool); ok {
d.propsCache.On = b
handler(b)
}
})
}
func (d *plugDevice) GetProps() PlugProps { return d.propsCache }
func (d *plugDevice) FetchProps(ctx context.Context) (PlugProps, error) {
props, err := d.client.GetProps(ctx, []xiaomi.PropKey{
{DID: d.info.DID, SIID: d.propOn.siid, PIID: d.propOn.piid},
})
if err != nil {
return d.propsCache, err
}
for _, p := range props {
if p.SIID == d.propOn.siid && p.PIID == d.propOn.piid {
d.propsCache.On, _ = p.Value.(bool)
}
}
return d.propsCache, nil
}
-1
View File
@@ -12,7 +12,6 @@ const (
TypeHumidifier = "humidifier" // 加湿器
TypeVacuum = "vacuum" // 吸尘器/扫地机器人
TypeWaterHeater = "water_heater" // 热水器
TypeThermostat = "thermostat" // 温控器
TypeOccupancySensor = "sensor_occupy" // 人体传感器
TypeTempHumidSensor = "sensor" // 温湿度传感器(官方 URN type)
TypeSpeaker = "speaker" // 音箱
@@ -70,7 +70,6 @@ func main() {
xiaomi.KindHumidifier,
xiaomi.KindVacuum,
xiaomi.KindWaterHeater,
xiaomi.KindThermostat,
}
for _, kind := range allKinds {
-1
View File
@@ -154,7 +154,6 @@ const (
KindHumidifier Kind = "humidifier"
KindVacuum Kind = "vacuum"
KindWaterHeater Kind = "water_heater"
KindThermostat Kind = "thermostat"
KindSpeaker Kind = "speaker"
KindGateway Kind = "gateway"
KindPlug Kind = "plug"