Files
4566704 7ae32bd012 docs: 补充「与 Python 原版的有意差异」说明与设备剔除/风速映射文档
- README 新增有意差异一节:设备列表语义(剔除米家已删设备)与缓存持久化内容的两条有意偏离及其原因、剔除判定规则
- README 架构概览补齐 xiaomi/、bridge/ 两层目录说明
- xiaomi/README 更新 GetDevices/RefreshDevices 签名(ctx),补充设备列表语义、家庭范围刷新示例与数字档位风速映射说明
2026-09-15 19:51:38 +08:00

383 lines
9.8 KiB
Markdown
Raw Permalink 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.
# 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)。