- README 新增有意差异一节:设备列表语义(剔除米家已删设备)与缓存持久化内容的两条有意偏离及其原因、剔除判定规则 - README 架构概览补齐 xiaomi/、bridge/ 两层目录说明 - xiaomi/README 更新 GetDevices/RefreshDevices 签名(ctx),补充设备列表语义、家庭范围刷新示例与数字档位风速映射说明
383 lines
9.8 KiB
Markdown
383 lines
9.8 KiB
Markdown
# xiaomi — Xiaomi 智能家居 Go SDK 高级封装层
|
||
|
||
`xiaomihome/xiaomi` 是 `xiaomihome/miot` 的强类型上层封装,将底层 `map[string]interface{}` API 转换为主流 Go 风格的 struct + interface,并提供设备控制抽象,隐藏 siid/piid 细节。
|
||
|
||
## 快速开始
|
||
|
||
```go
|
||
import (
|
||
"xiaomihome/miot"
|
||
"xiaomihome/xiaomi"
|
||
"xiaomihome/xiaomi/devices"
|
||
)
|
||
|
||
// 1. 创建底层 miot 客户端
|
||
miotClient := miot.NewMIoTClient("phone", entryData, "", "cn", miot.CtrlModeAuto)
|
||
miotClient.Init()
|
||
|
||
// 2. 创建 HTTP 客户端
|
||
httpClient, _ := miot.NewMIoTHttpClient(cloudServer, clientID, accessToken)
|
||
|
||
// 3. 包装为 xiaomi.Client
|
||
client := xiaomi.NewClient(miotClient, httpClient)
|
||
```
|
||
|
||
## API 参考
|
||
|
||
### Client 主入口 (`client.go`)
|
||
|
||
```go
|
||
client := xiaomi.NewClient(inner *miot.MIoTClient, httpClient *miot.MIoTHttpClient)
|
||
|
||
client.Inner() // 返回底层 MIoTClient
|
||
client.HTTPClient() // 返回底层 MIoTHttpClient
|
||
```
|
||
|
||
### 用户信息 (`user.go`)
|
||
|
||
```go
|
||
user, err := client.GetUserInfo()
|
||
// → *UserInfo{UID, NickName, AvatarURL}
|
||
```
|
||
|
||
### 家庭/房间 (`homes.go`)
|
||
|
||
```go
|
||
homes, err := client.GetHomeList() // → []*HomeInfo
|
||
home, err := client.GetHome(homeID) // → *HomeInfo (含房间列表)
|
||
rooms, err := client.GetRooms(homeID) // → []*RoomInfo
|
||
dids, err := client.GetRoomDevices(homeID, roomID) // → []string
|
||
```
|
||
|
||
**HomeInfo 结构:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| ID | `string` | 家庭 ID |
|
||
| Name | `string` | 家庭名称 |
|
||
| UID | `string` | 用户 ID |
|
||
| GroupID | `string` | MIPS 分组 ID |
|
||
| Rooms | `[]RoomInfo` | 房间列表 |
|
||
| DidList | `[]string` | 所有设备 DID |
|
||
| ShareState | `bool` | 是否为共享家庭 |
|
||
|
||
### 设备列表 (`devices.go`)
|
||
|
||
```go
|
||
devices, err := client.GetDevices(ctx, opts...) // → []*DeviceInfo
|
||
device, err := client.GetDevice(ctx, did) // → *DeviceInfo
|
||
err = client.RefreshDevices(ctx) // 强制刷新设备列表(全量)
|
||
```
|
||
|
||
**设备列表语义:`GetDevices()` 返回的即「米家当前设备」**
|
||
|
||
`GetDevices()` 读的是 SDK 的设备缓存,而 `RefreshDevices()` 会在刷新时**剔除米家在 App 里
|
||
已删除的设备**(底层 `miot.pruneRemovedDevicesLocked`),所以列表不会返回已删设备;
|
||
磁盘缓存 `miot_devices/*.dict` 也会同步清理,不会在重启后复活。
|
||
|
||
```go
|
||
// 全量刷新:所有家庭;云端返回空列表时不会清库(接口异常保护)
|
||
err = client.RefreshDevices(ctx)
|
||
|
||
// 只刷新指定家庭:剔除范围也限定在该家庭内,不影响其它家庭
|
||
err = client.RefreshDevices(ctx, "984001863127")
|
||
```
|
||
|
||
注意:离线设备仍在列表里(`DeviceInfo.Online == false`),只有**从米家删除**的设备才会消失。
|
||
|
||
**过滤器(组合使用):**
|
||
|
||
```go
|
||
// 只获取在线设备
|
||
client.GetDevices(xiaomi.FilterByOnline())
|
||
|
||
// 指定家庭 + 在线
|
||
client.GetDevices(
|
||
xiaomi.FilterByHome("home001"),
|
||
xiaomi.FilterByOnline(),
|
||
)
|
||
|
||
// 指定房间
|
||
client.GetDevices(xiaomi.FilterByRoom("room001"))
|
||
|
||
// 指定型号
|
||
client.GetDevices(xiaomi.FilterByModel("xiaomi.aircondition.c1"))
|
||
```
|
||
|
||
### 属性读写 (`properties.go`)
|
||
|
||
```go
|
||
prop, err := client.GetProp(did, siid, piid) // → *PropertyValue
|
||
err := client.SetProp(did, siid, piid, value)
|
||
|
||
props, err := client.GetProps([]PropKey{...}) // → []*PropertyValue
|
||
results, err := client.SetProps([]PropKeyValue{...}) // → []*PropResult
|
||
```
|
||
|
||
### Action 调用 (`actions.go`)
|
||
|
||
```go
|
||
result, err := client.Action(did, siid, aiid, params) // → *ActionResult
|
||
```
|
||
|
||
### 订阅通知 (`subscribe.go`)
|
||
|
||
```go
|
||
// 属性变化订阅
|
||
subID, err := client.SubProp(did, siid, piid, func(did string, prop *PropertyValue) {
|
||
fmt.Printf("属性变化: %v\n", prop.Value)
|
||
})
|
||
|
||
// 取消属性订阅
|
||
client.UnsubProp(did, subID)
|
||
|
||
// 事件订阅
|
||
subID, err := client.SubEvent(did, siid, eiid, func(did string, event *EventOccurred) {
|
||
fmt.Printf("事件: %v\n", event.Arguments)
|
||
})
|
||
client.UnsubEvent(did, subID)
|
||
|
||
// 设备在线状态订阅
|
||
client.SubDeviceState(did, func(did string, state xiaomi.DeviceState) {
|
||
if state == xiaomi.StateOnline {
|
||
fmt.Println("设备上线")
|
||
}
|
||
})
|
||
client.UnsubDeviceState(did)
|
||
```
|
||
|
||
### 设备控制抽象 (`devices/`)
|
||
|
||
#### 基础能力 (`BaseDevice`)
|
||
|
||
所有设备控制对象共享的基础方法:
|
||
|
||
```go
|
||
d.DID() // 设备 ID
|
||
d.Name() // 设备名称
|
||
d.Model() // 设备型号
|
||
d.Online() // 是否在线
|
||
d.Info() // 完整设备信息 *DeviceInfo
|
||
|
||
d.GetProp(siid, piid) // 读取属性
|
||
d.SetProp(siid, piid, value) // 写入属性
|
||
d.Action(siid, aiid, params) // 调用 Action
|
||
d.SubProp(siid, piid, handler) // 订阅属性变化
|
||
d.SubDeviceState(handler) // 订阅在线状态
|
||
```
|
||
|
||
#### 开关 (`Switch`)
|
||
|
||
```go
|
||
sw, _ := devices.NewSwitch(client, info)
|
||
|
||
sw.TurnOn()
|
||
sw.TurnOff()
|
||
sw.Toggle()
|
||
on, _ := sw.IsOn()
|
||
|
||
// 订阅状态变化
|
||
sw.OnPropsChanged(func(on bool) { ... })
|
||
```
|
||
|
||
#### 灯光 (`Light`)
|
||
|
||
```go
|
||
light, _ := devices.NewLight(client, info)
|
||
|
||
light.TurnOn() / TurnOff() / Toggle()
|
||
|
||
light.SetBrightness(80) // 1-100
|
||
b, _ := light.GetBrightness()
|
||
|
||
light.SetColorTemp(4000) // 2700-6500K
|
||
k, _ := light.GetColorTemp()
|
||
|
||
light.SetColor(255, 0, 0) // RGB 0-255
|
||
r, g, b, _ := light.GetColor()
|
||
|
||
light.OnPropsChanged(func(state LightState) { ... })
|
||
```
|
||
|
||
#### 空调 (`AirConditioner`)
|
||
|
||
```go
|
||
ac, _ := devices.NewAirConditioner(client, info)
|
||
|
||
// 基础
|
||
ac.TurnOn() / TurnOff()
|
||
on, _ := ac.IsOn()
|
||
|
||
// 模式
|
||
ac.SetMode(devices.ModeCool) // Cool/Heat/Fan/Dry/Auto
|
||
mode, _ := ac.GetMode()
|
||
|
||
// 温度
|
||
ac.SetTargetTemp(26.0)
|
||
target, _ := ac.GetTargetTemp()
|
||
current, _ := ac.GetCurrentTemp()
|
||
|
||
// 风速
|
||
ac.SetFanSpeed(devices.SpeedAuto) // Low/Medium/High/Auto
|
||
speed, _ := ac.GetFanSpeed()
|
||
|
||
// 摆风
|
||
ac.SetSwing(devices.SwingBoth) // Off/Vertical/Horizontal/Both
|
||
swing, _ := ac.GetSwing()
|
||
|
||
// 湿度(部分支持)
|
||
ac.SetTargetHumidity(50)
|
||
ac.GetTargetHumidity()
|
||
ac.GetCurrentHumidity()
|
||
|
||
// 电辅热 / 节能(部分支持)
|
||
ac.SetAuxHeat(true)
|
||
ac.SetEco(true)
|
||
|
||
ac.OnPropsChanged(func(state ACState) { ... })
|
||
```
|
||
|
||
> **风速档位映射(数字档位设备)**:部分设备的 `fan-level` 枚举是 `Auto + Level1~Level5`,
|
||
> 没有 `low/medium/high` 字面值(如 `klwdz1.airc.001`、`tofan.airrtc.wk01`)。此时按数字档位的
|
||
> **相对位置**映射:`low`→最低档、`medium`→中间档、`high`→最高档(`0`/Auto 不参与)。
|
||
> 设备上报的数字档位会**反向解析**回 `low/medium/high`,保证「控制写什么、通知就回报什么」;
|
||
> 落在映射之外的档位仍返回 SPEC 描述原文。枚举本身就是 `low/medium/high` 的设备走精确匹配,
|
||
> 行为不受影响。
|
||
|
||
#### 风扇 (`Fan`)
|
||
|
||
```go
|
||
fan, _ := devices.NewFan(client, info)
|
||
fan.TurnOn() / TurnOff()
|
||
fan.SetFanLevel(2) // 1-4 档位
|
||
fan.SetOscillation(true) // 摇头
|
||
fan.SetNaturalWind(true) // 自然风
|
||
```
|
||
|
||
#### 窗帘 (`Cover`)
|
||
|
||
```go
|
||
cover, _ := devices.NewCover(client, info)
|
||
cover.Open() / Close() / Stop()
|
||
cover.SetPosition(50) // 0=全关, 100=全开
|
||
pos, _ := cover.GetPosition()
|
||
```
|
||
|
||
#### 加湿器 (`Humidifier`)
|
||
|
||
```go
|
||
h, _ := devices.NewHumidifier(client, info)
|
||
h.TurnOn() / TurnOff()
|
||
h.SetTargetHumidity(60)
|
||
h.SetFanLevel(2)
|
||
```
|
||
|
||
#### 扫地机 (`Vacuum`)
|
||
|
||
```go
|
||
v, _ := devices.NewVacuum(client, info)
|
||
v.Start() / Stop()
|
||
v.ReturnToDock()
|
||
v.SetFanLevel(3) // 吸力档位
|
||
status, _ := v.GetStatus() // → VacuumStatus{On, Mode, FanLevel, Battery}
|
||
```
|
||
|
||
#### 热水器 (`WaterHeater`)
|
||
|
||
```go
|
||
wh, _ := devices.NewWaterHeater(client, info)
|
||
wh.TurnOn() / TurnOff()
|
||
wh.SetTargetTemp(45.0)
|
||
wh.GetCurrentTemp()
|
||
```
|
||
|
||
#### 温控器 (`Thermostat`)
|
||
|
||
```go
|
||
t, _ := devices.NewThermostat(client, info)
|
||
t.TurnOn() / TurnOff()
|
||
t.SetTargetTemp(22.0)
|
||
t.SetHeating(true)
|
||
```
|
||
|
||
#### 工厂函数 (`factory.go`)
|
||
|
||
自动根据设备 URN 创建对应的控制接口:
|
||
|
||
```go
|
||
for _, info := range deviceList {
|
||
ctrl, err := devices.Create(client, info)
|
||
if err != nil || ctrl == nil {
|
||
continue
|
||
}
|
||
switch d := ctrl.(type) {
|
||
case devices.Switch:
|
||
d.TurnOn()
|
||
case devices.Light:
|
||
d.SetBrightness(80)
|
||
case devices.AirConditioner:
|
||
d.SetTargetTemp(26.0)
|
||
case devices.Fan:
|
||
d.SetFanLevel(2)
|
||
case devices.Cover:
|
||
d.Open()
|
||
case devices.Humidifier:
|
||
d.SetTargetHumidity(60)
|
||
case devices.Vacuum:
|
||
d.Start()
|
||
case devices.WaterHeater:
|
||
d.SetTargetTemp(45.0)
|
||
case devices.Thermostat:
|
||
d.SetTargetTemp(22.0)
|
||
}
|
||
}
|
||
```
|
||
|
||
### SPEC 解析器 (`specs/`)
|
||
|
||
用于直接从设备 SPEC 中按功能语义查找 siid/piid:
|
||
|
||
```go
|
||
resolver := specs.NewPropertyResolver(spec)
|
||
|
||
siid, piid, ok := resolver.FindByType("on") // 按属性类型
|
||
piid, ok := resolver.FindByFormat("bool", siid) // 按数据格式
|
||
siid, piid, ok := resolver.FindByDescription("开关") // 按描述
|
||
aiid, ok := resolver.FindAction(siid, "toggle") // 按 Action 类型
|
||
props := resolver.GetAllProperties(siid) // 获取某服务所有属性
|
||
prop := resolver.GetProperty(siid, piid) // 获取单个属性
|
||
```
|
||
|
||
## 错误处理
|
||
|
||
所有错误都实现了 Go 标准 `error` 接口,同时可通过 `errors.Is` 判断:
|
||
|
||
```go
|
||
if errors.Is(err, xiaomi.ErrDeviceOffline) { ... }
|
||
if errors.Is(err, xiaomi.ErrNotSupported) { ... }
|
||
|
||
// ClientError 携带操作上下文
|
||
var ce *xiaomi.ClientError
|
||
if errors.As(err, &ce) {
|
||
fmt.Println("操作:", ce.Op, "设备:", ce.DID)
|
||
}
|
||
```
|
||
|
||
## 关于 siid/piid
|
||
|
||
如果你需要直接操作底层属性,xiaomi 封装层仍支持传统的 siid/piid 方式:
|
||
|
||
```go
|
||
// 读取空调开关 (常见 siid=2, piid=1)
|
||
prop, _ := client.GetProp("did", 2, 1)
|
||
|
||
// 写入目标温度
|
||
client.SetProp("did", 2, 3, 26.0)
|
||
```
|
||
|
||
但强烈推荐使用设备控制抽象,它会自动从 SPEC 解析正确的 siid/piid。
|
||
|
||
## 完整示例
|
||
|
||
参见 [`examples/main.go`](examples/main.go)。
|