chore: 提交高层 xiaomi 封装库及示例

- 新增 xiaomi/examples/ 下 8 个示例程序(用户/家庭查询、设备列表、开关/灯光/空调控制、属性订阅、高级过滤分类、SPEC 解析)
- miot_client_sub.go: 重构 SubProp/SubEvent,使用 buildPropTopic/buildEventTopic 支持通配符订阅(siid/piid=0 → +),并修复锁顺序问题(将 RequestRefreshProp 移到 Lock 外)
- spec_parser.go: 新增 downloadSpecFile 方法,本地 SPEC 文件缺失时自动从 miot-spec.org 下载
- ARCH_PLAN.md: 架构设计从 Proposed 更新为 Accepted(v1.0→v1.1),补充设备分类/工厂/SPEC 映射等模块设计
- xiaomi/: 新增 miot 上层强类型封装模块,包含 Client 主入口、用户/家庭/设备 API、属性读写、动作调用、订阅通知,以及 devices/ 设备控制抽象(Switch/Light/AirConditioner/Fan/Cover/Humidifier/Vacuum/WaterHeater/Thermostat)和 specs/ SPEC 查询辅助
- 更多 xiaomi 示例(风扇/窗帘/传感器控制)
This commit is contained in:
2026-06-29 08:51:21 +08:00
parent 35efed7d15
commit a3d94c4b9f
93 changed files with 7582 additions and 157 deletions
+320 -135
View File
@@ -1,6 +1,6 @@
# go-xiaomihome 上层封装架构设计规划
> 状态:Proposed | 版本:v1.0 | 日期:2026-06-28
> 状态:Accepted | 版本:v1.1 | 日期:2026-06-28
---
@@ -48,6 +48,8 @@ go-xiaomihome/
│ │
│ ├── devices/ # [新] 设备控制抽象
│ │ ├── base.go # Device 基础接口 + BaseDevice 实现
│ │ ├── kind.go # Kind 枚举 + Classify 设备分类
│ │ ├── factory.go # NewDevice 通用工厂
│ │ ├── air_conditioner.go # 空调控制接口
│ │ ├── light.go # 灯光控制接口
│ │ ├── switch.go # 开关控制接口
@@ -59,7 +61,8 @@ go-xiaomihome/
│ │ └── thermostat.go # 温控器/电热毯控制接口
│ │
│ └── specs/ # [新] SPEC 辅助(设备能力查询)
│ └── resolver.go # 按功能语义解析 SPEC,自动找 siid/piid
│ ├── resolver.go # 按功能语义解析 SPEC,自动找 siid/piid
│ └── mapper.go # SPEC 值映射(原始值 ↔ 枚举字符串)
```
---
@@ -89,6 +92,10 @@ type HomeInfo struct {
Rooms []RoomInfo `json:"rooms"`
DidList []string `json:"did_list"`
ShareState bool `json:"share_state"`
CityID string `json:"city_id"` // 城市ID
Longitude float64 `json:"longitude"` // 经度
Latitude float64 `json:"latitude"` // 纬度
Address string `json:"address"` // 地址
}
// RoomInfo 房间信息
@@ -125,6 +132,9 @@ type DeviceInfo struct {
ConnectType int `json:"connect_type"` // 0=WiFi, 1=BLE, 2=ZigBee, ...
}
// DeviceKind 从 URN/Model 快速判断设备大类(不依赖 SPEC 加载)
func (d *DeviceInfo) DeviceKind() devices.Kind
// ========== 属性/事件 ==========
// PropertyValue 属性值(强类型)
@@ -133,8 +143,8 @@ type PropertyValue struct {
SIID int `json:"siid"`
PIID int `json:"piid"`
Code int `json:"code"`
Value interface{} `json:"value"` // 对上层暴露具体类型,不作转换
Type string `json:"type"` // SPEC format: "bool"/"uint8"/"float"/"string"
Value interface{} `json:"value"` // 原始值,上层自行类型断言
Type string `json:"type"` // 仅 GetProp 场景可用(从 SPEC 查询);SubProp 回调中为空
}
// PropertiesChanged 属性变化事件
@@ -176,13 +186,12 @@ type ActionResult struct {
```go
package xiaomi
import "context"
// Client 高级封装客户端(包装 miot.MIoTClient)
type Client struct {
inner *miot.MIoTClient // 内部 miot 实例(不对外暴露)
currentUser UserInfo
homes map[string]*HomeInfo
mu sync.RWMutex
mu sync.RWMutex
}
// NewClient 创建客户端
@@ -191,30 +200,30 @@ func NewClient(inner *miot.MIoTClient) *Client
// ========== 用户/家庭/房间 ==========
// GetUserInfo 获取用户信息(强类型返回)
func (c *Client) GetUserInfo() (*UserInfo, error)
func (c *Client) GetUserInfo(ctx context.Context) (*UserInfo, error)
// GetHomeList 获取所有家庭列表
func (c *Client) GetHomeList() ([]*HomeInfo, error)
func (c *Client) GetHomeList(ctx context.Context) ([]*HomeInfo, error)
// GetHome 获取指定家庭详情(含房间)
func (c *Client) GetHome(homeID string) (*HomeInfo, error)
func (c *Client) GetHome(ctx context.Context, homeID string) (*HomeInfo, error)
// GetRooms 获取指定家庭的所有房间
func (c *Client) GetRooms(homeID string) ([]*RoomInfo, error)
func (c *Client) GetRooms(ctx context.Context, homeID string) ([]*RoomInfo, error)
// GetRoomDevices 获取指定房间的设备 DID 列表
func (c *Client) GetRoomDevices(homeID, roomID string) ([]string, error)
func (c *Client) GetRoomDevices(ctx context.Context, homeID, roomID string) ([]string, error)
// ========== 设备列表 ==========
// GetDevices 获取所有设备信息(强类型返回)
func (c *Client) GetDevices(opts ...DeviceFilter) ([]*DeviceInfo, error)
func (c *Client) GetDevices(ctx context.Context, opts ...DeviceFilter) ([]*DeviceInfo, error)
// GetDevice 获取单个设备信息
func (c *Client) GetDevice(did string) (*DeviceInfo, error)
func (c *Client) GetDevice(ctx context.Context, did string) (*DeviceInfo, error)
// RefreshDevices 刷新设备列表
func (c *Client) RefreshDevices() error
func (c *Client) RefreshDevices(ctx context.Context) error
// DeviceFilter 设备过滤器函数类型
type DeviceFilter func(*DeviceInfo) bool
@@ -227,6 +236,8 @@ func FilterByRoom(roomID string) DeviceFilter
func FilterByOnline() DeviceFilter
// FilterByModel 按型号过滤
func FilterByModel(model string) DeviceFilter
// FilterByKind 按设备大类过滤
func FilterByKind(kind devices.Kind) DeviceFilter
```
### 4.3 属性读写封装
@@ -234,17 +245,19 @@ func FilterByModel(model string) DeviceFilter
```go
package xiaomi
import "context"
// GetProp 读取单个属性(与 miot.GetProp 对齐命名)
func (c *Client) GetProp(did string, siid, piid int) (*PropertyValue, error)
func (c *Client) GetProp(ctx context.Context, did string, siid, piid int) (*PropertyValue, error)
// SetProp 写入单个属性(与 miot.SetProp 对齐命名)
func (c *Client) SetProp(did string, siid, piid int, value interface{}) error
func (c *Client) SetProp(ctx context.Context, did string, siid, piid int, value interface{}) error
// GetProps 批量读取属性(与 miot.GetProps 对齐命名)
func (c *Client) GetProps(params []PropKey) ([]*PropertyValue, error)
func (c *Client) GetProps(ctx context.Context, params []PropKey) ([]*PropertyValue, error)
// SetProps 批量写入属性(与 miot.SetProps 对齐命名)
func (c *Client) SetProps(params []PropKeyValue) ([]*PropResult, error)
func (c *Client) SetProps(ctx context.Context, params []PropKeyValue) ([]*PropResult, error)
// PropKey 属性键对
type PropKey struct {
@@ -268,7 +281,7 @@ type PropResult struct {
// ========== Action 封装 ==========
// Action 调用设备 Action(与 miot.Action 对齐命名)
func (c *Client) Action(did string, siid, aiid int, params []interface{}) (*ActionResult, error)
func (c *Client) Action(ctx context.Context, did string, siid, aiid int, params []interface{}) (*ActionResult, error)
```
### 4.4 订阅通知接口
@@ -396,12 +409,24 @@ xiaomi/devices/
- 通过 `specs/resolver.go` 从 SPEC 中自动解析出对应的 siid/piid
- 用户不关心 `siid=2, piid=3` 是什么,只关心 `ac.SetTargetTemp(26)` / `ac.SetMode(ModeCool)`
### 5.2 BaseDevice
### 5.2 Device 基础接口 + BaseDevice 实现
```go
package devices
// BaseDevice 所有设备控制类的公共基础
import "context"
// Device 所有设备控制接口的公共基础
// AirConditioner / Light / Switch / ... 都内嵌此接口
type Device interface {
DID() string
Name() string
Model() string
Online() bool
Info() *xiaomi.DeviceInfo
}
// BaseDevice 所有设备控制类的公共实现(嵌入到具体类型中)
type BaseDevice struct {
client *xiaomi.Client
info *xiaomi.DeviceInfo
@@ -419,11 +444,11 @@ func (d *BaseDevice) Online() bool
func (d *BaseDevice) Info() *xiaomi.DeviceInfo
// GetProp 快捷属性读取(委托给 xiaomi.Client.GetProp)
func (d *BaseDevice) GetProp(siid, piid int) (*xiaomi.PropertyValue, error)
func (d *BaseDevice) GetProp(ctx context.Context, siid, piid int) (*xiaomi.PropertyValue, error)
// SetProp 快捷属性写入
func (d *BaseDevice) SetProp(siid, piid int, value interface{}) error
func (d *BaseDevice) SetProp(ctx context.Context, siid, piid int, value interface{}) error
// Action 快捷 Action 调用
func (d *BaseDevice) Action(siid, aiid int, params []interface{}) (*xiaomi.ActionResult, error)
func (d *BaseDevice) Action(ctx context.Context, siid, aiid int, params []interface{}) (*xiaomi.ActionResult, error)
// SubProp 订阅属性变化(委托给 xiaomi.Client.SubProp)
func (d *BaseDevice) SubProp(siid, piid int, handler xiaomi.PropHandler) (string, error)
@@ -436,45 +461,49 @@ func (d *BaseDevice) SubDeviceState(handler xiaomi.DeviceStateHandler) error
### 5.3 空调控制接口
```go
import "context"
// AirConditioner 空调统一控制接口
type AirConditioner interface {
Device // 嵌入公共接口
// 基础能力
TurnOn() error
TurnOff() error
IsOn() (bool, error)
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
IsOn(ctx context.Context) (bool, error)
// 模式控制
SetMode(mode ACMode) error
GetMode() (ACMode, error)
SetMode(ctx context.Context, mode ACMode) error
GetMode(ctx context.Context) (ACMode, error)
// 温度控制
SetTargetTemp(celsius float64) error
GetTargetTemp() (float64, error)
GetCurrentTemp() (float64, error) // 室内温度
SetTargetTemp(ctx context.Context, celsius float64) error
GetTargetTemp(ctx context.Context) (float64, error)
GetCurrentTemp(ctx context.Context) (float64, error) // 室内温度
// 风速控制
SetFanSpeed(speed FanSpeed) error
GetFanSpeed() (FanSpeed, error)
SetFanSpeed(ctx context.Context, speed FanSpeed) error
GetFanSpeed(ctx context.Context) (FanSpeed, error)
// 摆风控制
SetSwing(mode SwingMode) error
GetSwing() (SwingMode, error)
SetSwing(ctx context.Context, mode SwingMode) error
GetSwing(ctx context.Context) (SwingMode, error)
// 湿度(部分空调支持)
SetTargetHumidity(pct int) error
GetTargetHumidity() (int, error)
GetCurrentHumidity() (int, error)
SetTargetHumidity(ctx context.Context, pct int) error
GetTargetHumidity(ctx context.Context) (int, error)
GetCurrentHumidity(ctx context.Context) (int, error)
// 电辅热
SetAuxHeat(on bool) error
GetAuxHeat() (bool, error)
SetAuxHeat(ctx context.Context, on bool) error
GetAuxHeat(ctx context.Context) (bool, error)
// 节能
SetEco(on bool) error
GetEco() (bool, error)
SetEco(ctx context.Context, on bool) error
GetEco(ctx context.Context) (bool, error)
// 订阅
OnStateChanged(handler func(state ACState)) (string, error)
// 订阅(不需要 ctx,订阅是本地注册操作)
OnStateChanged(handler func(state ACState)) (subID string, err error)
}
// ACMode 空调模式
@@ -530,31 +559,35 @@ type ACState struct {
### 5.4 灯光控制接口
```go
import "context"
// Light 灯光统一控制接口
type Light interface {
TurnOn() error
TurnOff() error
Toggle() error
Device
// 亮度
SetBrightness(pct int) error // 1-100, -1 表示不支持
GetBrightness() (int, error)
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
Toggle(ctx context.Context) error
// 色温
SetColorTemp(kelvin int) error // 2700-6500, -1 表示不支持
GetColorTemp() (int, error)
// 亮度(若返回 -1 表示不支持)
SetBrightness(ctx context.Context, pct int) error // 1-100
GetBrightness(ctx context.Context) (int, error)
// 颜色
SetColor(r, g, b int) error // 0-255, -1 表示不支持
GetColor() (r, g, b int, error)
// 色温(若返回 -1 表示不支持)
SetColorTemp(ctx context.Context, kelvin int) error // 2700-6500
GetColorTemp(ctx context.Context) (int, error)
OnStateChanged(handler func(state LightState)) (string, error)
// 颜色(若 r/g/b 均返回 -1 表示不支持)
SetColor(ctx context.Context, r, g, b int) error // 0-255
GetColor(ctx context.Context) (r, g, b int, error)
OnStateChanged(handler func(state LightState)) (subID string, err error)
}
type LightState struct {
Power bool `json:"power"`
Brightness int `json:"brightness"`
ColorTemp int `json:"color_temp"`
Power bool `json:"power"`
Brightness int `json:"brightness"`
ColorTemp int `json:"color_temp"`
ColorRGB [3]int `json:"color_rgb"`
}
```
@@ -562,71 +595,179 @@ type LightState struct {
### 5.5 开关控制接口
```go
import "context"
// Switch 开关统一控制接口
type Switch interface {
TurnOn() error
TurnOff() error
Toggle() error
IsOn() (bool, error)
Device
OnStateChanged(handler func(on bool)) (string, error)
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
Toggle(ctx context.Context) error
IsOn(ctx context.Context) (bool, error)
OnStateChanged(handler func(on bool)) (subID string, err error)
}
```
### 5.6 其他设备接口(概要)
```go
import "context"
// Fan 风扇接口
type Fan interface {
TurnOn() error
TurnOff() error
SetSpeed(speed FanLevel) error // 0=off, 1-4 档位
SetOscillation(on bool) error
SetNaturalWind(on bool) error
Device
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
SetSpeed(ctx context.Context, speed FanLevel) error // 0=off, 1-4 档位
SetOscillation(ctx context.Context, on bool) error
SetNaturalWind(ctx context.Context, on bool) error
}
// Cover 窗帘/晾衣架
type Cover interface {
Open() error
Close() error
Stop() error
SetPosition(pct int) error // 0=关, 100=全开
GetPosition() (int, error)
Device
Open(ctx context.Context) error
Close(ctx context.Context) error
Stop(ctx context.Context) error
SetPosition(ctx context.Context, pct int) error // 0=关, 100=全开
GetPosition(ctx context.Context) (int, error)
}
// Humidifier 加湿器/除湿机
type Humidifier interface {
TurnOn() error
TurnOff() error
SetTargetHumidity(pct int) error
SetFanSpeed(speed FanLevel) error
Device
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
SetTargetHumidity(ctx context.Context, pct int) error
SetFanSpeed(ctx context.Context, speed FanLevel) error
}
// Vacuum 扫地机
type Vacuum interface {
Start() error
Stop() error
ReturnToDock() error
SetFanSpeed(speed FanLevel) error
GetStatus() (VacuumStatus, error)
Device
Start(ctx context.Context) error
Stop(ctx context.Context) error
ReturnToDock(ctx context.Context) error
SetFanSpeed(ctx context.Context, speed FanLevel) error
GetStatus(ctx context.Context) (VacuumStatus, error)
}
// WaterHeater 热水器
type WaterHeater interface {
TurnOn() error
TurnOff() error
SetTargetTemp(celsius float64) error
GetCurrentTemp() (float64, error)
Device
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
SetTargetTemp(ctx context.Context, celsius float64) error
GetCurrentTemp(ctx context.Context) (float64, error)
}
// Thermostat 温控器
type Thermostat interface {
SetTargetTemp(celsius float64) error
GetCurrentTemp() (float64, error)
SetHeating(on bool) error
Device
SetTargetTemp(ctx context.Context, celsius float64) error
GetCurrentTemp(ctx context.Context) (float64, error)
SetHeating(ctx context.Context, on bool) error
}
```
### 5.7 设备分类与工厂
```go
package devices
// Kind 设备大类(用于分类和过滤)
type Kind string
const (
KindAirConditioner Kind = "air_conditioner"
KindLight Kind = "light"
KindSwitch Kind = "switch"
KindFan Kind = "fan"
KindCover Kind = "cover"
KindHumidifier Kind = "humidifier"
KindVacuum Kind = "vacuum"
KindWaterHeater Kind = "water_heater"
KindThermostat Kind = "thermostat"
KindUnknown Kind = "unknown"
)
// Classify 从 DeviceInfo 快速判断设备种类
// 基于 URN/Model 前缀做字符串匹配,不依赖 SPEC 加载
func Classify(info *xiaomi.DeviceInfo) Kind
// NewDevice 通用工厂:根据 DeviceInfo 自动创建对应的设备控制实例
// 返回的是具体接口(AirConditioner / Light / Switch / ...),调用方用 type switch 使用
func NewDevice(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Device, error)
```
**工厂使用模式**:
```go
dev, err := devices.NewDevice(client, info)
if err != nil {
return err
}
switch d := dev.(type) {
case devices.AirConditioner:
d.TurnOn(ctx)
d.SetTargetTemp(ctx, 26)
case devices.Light:
d.SetBrightness(ctx, 80)
case devices.Switch:
d.Toggle(ctx)
default:
fmt.Printf("设备 %s 暂无专用控制接口\n", dev.Name())
}
```
```
### 值映射
SPEC 中的枚举属性(如空调模式)在 SPEC 中以整数值存储,但封装层用字符串枚举(如 `"cool"`)。SpecResolver 需要提供双向映射:
```go
package specs
// ValueMapping Spec 原始值 → 枚举字符串 的映射
type ValueMapping struct {
SpecValue interface{} // SPEC 中的原始值(通常是 int 或 string)
EnumValue string // 对应的语义枚举值(用于 ACMode / FanSpeed 等)
}
// PropertyMapper 属性值映射器
// 从 SPEC 的 value-list 中提取原始值到枚举值的对应关系
type PropertyMapper struct {
rawToEnum map[interface{}]string // 原始值 → 枚举字符串
enumToRaw map[string]interface{} // 枚举字符串 → 原始值
}
// NewPropertyMapper 从 SPEC property 的 value-list 构造映射器
// 需要外部提供 "原始值 → 枚举名" 的对照表(如 {0:"auto", 1:"cool", 2:"dry", 3:"fan", 4:"heat"})
func NewPropertyMapper(vlist []miot.MIoTSpecValueItem, valueMap map[interface{}]string) *PropertyMapper
// ToEnum 将原始值转为枚举字符串
func (m *PropertyMapper) ToEnum(raw interface{}) (string, bool)
// ToRaw 将枚举字符串转为原始值
func (m *PropertyMapper) ToRaw(enum string) (interface{}, bool)
```
**示例**:空调模式映射
```go
acModeMap := map[interface{}]string{
0: "auto",
1: "cool",
2: "dry",
3: "fan",
4: "heat",
}
mapper := specs.NewPropertyMapper(modeProp.ValueList, acModeMap)
rawValue, _ := mapper.ToRaw("cool") // → 1
enumValue, _ := mapper.ToEnum(4) // → "heat"
```
---
## 六、specs/resolver.go — SPEC 语义解析器
@@ -776,6 +917,39 @@ func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirCondi
---
### ADR-006:所有 IO 方法携带 context.Context
**状态**:Proposed
**背景**:封装层的 GetUserInfo / GetDevices / SetProp / Action 等涉及网络 IO,Go 惯例需要超时和取消控制。
**决定**:所有涉及 IO 的方法首参加 `ctx context.Context`。订阅方法(SubProp / SubEvent / SubDeviceState)因为是本地注册操作不涉及直接 IO,不携带 ctx。
**后果**:
- ✅ 调用方可控制超时和取消
- ✅ 符合 Go 标准库惯例
- ❌ 方法签名变长(但这是 Go 生态的标准代价)
---
### ADR-007:Device 基础接口 + Kind 分类体系
**状态**:Proposed
**背景**:用户拿到 DeviceInfo 需要判断设备类型才能创建对应的控制接口。需要一个快速分类机制(不依赖 SPEC 加载)和一个统一的工厂入口。
**决定**:
1. 定义 `Device` 基础接口,所有设备接口内嵌它
2. 定义 `Kind` 枚举,`Classify(DeviceInfo)` 基于 URN/Model 字符串匹配快速分类
3. 提供 `NewDevice(client, info)` 通用工厂,返回 `Device`,调用方 type switch
**后果**:
- ✅ 用户无需了解 URN 命名规则
- ✅ 一个工厂函数处理所有设备类型
- ❌ Classify 基于字符串匹配,极端情况下可能误判(非常见型号),需要可扩展的匹配表
---
## 八、实施计划
### Phase 1 — 基础封装(优先级 P0)
@@ -783,10 +957,11 @@ func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirCondi
| 任务 | 文件 | 工作量 |
|------|------|--------|
| 项目骨架搭建 | `xiaomi/go.mod`, module 初始化 | 小 |
| types.go 结构体定义 | `xiaomi/types.go` | 中 |
| Client 基础框架 | `xiaomi/client.go` | 中 |
| types.go 结构体定义(UserInfo/HomeInfo/RoomInfo/DeviceInfo 等) | `xiaomi/types.go` | 中 |
| DeviceKind 分类 + Classify 函数 | `xiaomi/types.go` (DeviceKind 方法) + `xiaomi/devices/kind.go` | 小 |
| Client 基础框架 + context 集成 | `xiaomi/client.go` | 中 |
| 用户/家庭/房间信息封装 | `xiaomi/user.go`, `homes.go` | 中 |
| 设备列表封装 | `xiaomi/devices.go` | 中 |
| 设备列表封装(含 FilterByKind) | `xiaomi/devices.go` | 中 |
| 属性读写封装 | `xiaomi/properties.go` | 中 |
| Action 封装 | `xiaomi/actions.go` | 小 |
| 错误定义 | `xiaomi/errors.go` | 小 |
@@ -795,30 +970,31 @@ func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirCondi
| 任务 | 文件 | 工作量 |
|------|------|--------|
| 订阅通知接口 | `xiaomi/subscribe.go` | 中 |
| 订阅通知接口(SubProp/SubEvent/SubDeviceState) | `xiaomi/subscribe.go` | 中 |
| 底层回调 → 强类型事件转换 | `xiaomi/subscribe.go` | 中 |
| 设备状态订阅 | `xiaomi/subscribe.go` | 小 |
| subID 映射管理 | `xiaomi/subscribe.go` | 小 |
### Phase 3 — 设备控制抽象(优先级 P1)
| 任务 | 文件 | 工作量 |
|------|------|--------|
| SPEC 语义解析器 | `xiaomi/specs/resolver.go` | 大 |
| BaseDevice 基础实现 | `xiaomi/devices/base.go` | 中 |
| SPEC 语义解析器(PropertyResolver) | `xiaomi/specs/resolver.go` | 大 |
| SPEC 值映射(PropertyMapper) | `xiaomi/specs/mapper.go` | 中 |
| Device 接口 + BaseDevice 实现 | `xiaomi/devices/base.go` | 中 |
| Switch 接口 + 实现 | `xiaomi/devices/switch.go` | 小 |
| Light 接口 + 实现 | `xiaomi/devices/light.go` | 中 |
| AirConditioner 接口 + 实现 | `xiaomi/devices/air_conditioner.go` | 大 |
| Fan 接口 + 实现 | `xiaomi/devices/fan.go` | 中 |
| Cover 接口 + 实现 | `xiaomi/devices/cover.go` | 中 |
| Humidifier/Vacuum/WaterHeater | 各文件 | 各小 |
| 工厂函数(NewXXX 从 DeviceInfo 创建) | `xiaomi/devices/factory.go` | 中 |
| Humidifier/Vacuum/WaterHeater/Thermostat | 各文件 | 各小 |
| 通用工厂 NewDevice | `xiaomi/devices/factory.go` | 中 |
### Phase 4 — 测试和文档(优先级 P2)
| 任务 | 文件 | 工作量 |
|------|------|--------|
| 单元测试 | `*_test.go` | 大 |
| 集成示例 | `xiaomi/examples/` | 中 |
| 集成示例(更新为 ctx + Device 接口 + factory) | `xiaomi/examples/` | 中 |
| API 文档 | `xiaomi/README.md` | 中 |
---
@@ -829,12 +1005,17 @@ func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirCondi
package main
import (
"context"
"fmt"
"time"
"xiaomihome/xiaomi"
"xiaomihome/xiaomi/devices"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. 创建底层 miot 客户端(照常)
// ... miot.NewMIoTClient(...) ...
@@ -842,56 +1023,60 @@ func main() {
client := xiaomi.NewClient(miotClient)
// 3. 获取用户信息(强类型!)
user, _ := client.GetUserInfo()
user, _ := client.GetUserInfo(ctx)
fmt.Printf("用户: %s (UID: %s)\n", user.NickName, user.UID)
// 4. 获取家庭和房间
homes, _ := client.GetHomeList()
homes, _ := client.GetHomeList(ctx)
for _, h := range homes {
fmt.Printf("家庭: %s\n", h.Name)
fmt.Printf("家庭: %s (%s)\n", h.Name, h.Address)
fmt.Printf(" 位置: %.4f, %.4f\n", h.Latitude, h.Longitude)
for _, r := range h.Rooms {
fmt.Printf(" 房间: %s (%d 设备)\n", r.Name, len(r.DidList))
}
}
// 5. 获取设备列表(可过滤)
devices_, _ := client.GetDevices(
devices_, _ := client.GetDevices(ctx,
xiaomi.FilterByHome(homes[0].ID),
xiaomi.FilterByOnline(),
)
for _, d := range devices_ {
fmt.Printf("设备: %s (%s) 在线:%v\n", d.Name, d.Model, d.Online)
fmt.Printf("设备: %s (%s) 在线:%v 种类:%s\n",
d.Name, d.Model, d.Online, d.DeviceKind())
}
// 6. 创建空调控制对象(自动解析 SPEC)
acInfo := devices_[0] // 假设是个空调
ac, err := devices.NewAirConditioner(client, acInfo)
if err != nil {
// 该设备不是空调或不支持
return
// 6. 通用工厂:一条命令创建对应控制接口
for _, info := range devices_ {
dev, err := devices.NewDevice(client, info)
if err != nil {
continue
}
switch d := dev.(type) {
case devices.AirConditioner:
d.TurnOn(ctx)
d.SetMode(ctx, devices.ModeCool)
d.SetTargetTemp(ctx, 26)
d.SetFanSpeed(ctx, devices.SpeedAuto)
// 订阅状态变化
d.OnStateChanged(func(state devices.ACState) {
fmt.Printf("空调 %s: 模式=%s 温度=%.1f°C\n",
dev.Name(), state.Mode, state.TargetTemp)
})
case devices.Light:
d.TurnOn(ctx)
d.SetBrightness(ctx, 80)
d.SetColorTemp(ctx, 4000)
case devices.Switch:
d.Toggle(ctx)
}
}
// 7. 像操作普通对象一样控制(隐藏了 siid/piid!)
ac.TurnOn()
ac.SetMode(devices.ModeCool)
ac.SetTargetTemp(26)
ac.SetFanSpeed(devices.SpeedAuto)
// 8. 订阅变化
ac.OnStateChanged(func(state devices.ACState) {
fmt.Printf("空调状态变化: 模式=%s 温度=%.1f°C\n",
state.Mode, state.TargetTemp)
// 7. 直接订阅原始属性变化(不用设备抽象)
client.SubProp("did_xxx", 0, 0, func(did string, prop *xiaomi.PropertyValue) {
fmt.Printf("属性变化: %s siid=%d piid=%d value=%v\n",
did, prop.SIID, prop.PIID, prop.Value)
})
// 9. 灯光控制
light, _ := devices.NewLight(client, lightInfo)
light.TurnOn()
light.SetBrightness(80)
light.SetColorTemp(4000)
// 10. 开关控制
sw, _ := devices.NewSwitch(client, switchInfo)
sw.Toggle()
}
```
+305
View File
@@ -0,0 +1,305 @@
# ARCH_PLAN.md 审查报告
> 审查时间:2026-06-28 | 目标版本:v1.0 → v1.1
---
## 审查结果总览
共发现 **11 个问题**,分级如下:
| 等级 | 数量 | 说明 |
|------|------|------|
| P0 阻塞 | 4 | 必须修复才能进入实施 |
| P1 重要 | 3 | 影响完整性和一致性 |
| P2 优化 | 4 | 锦上添花 |
---
## P0 — 阻塞项
### #1 缺少 `context.Context` 支持
**问题**:所有涉及 IO 的方法(GetUserInfo、GetHomeList、GetDevices、SetProp、Action 等)都没有 `context.Context` 参数。Go 惯例要求 IO 操作可超时和取消。
**影响范围**:
- `client.go`:GetUserInfo、GetHomeList、GetHome、GetRooms、GetRoomDevices、GetDevices、GetDevice、RefreshDevices
- `properties.go`:GetProp、SetProp、GetProps、SetProps
- `actions.go`:Action
- `devices/base.go`:GetProp、SetProp、Action
- `devices/air_conditioner.go`:TurnOn、TurnOff、IsOn、SetMode、GetMode、SetTargetTemp、GetTargetTemp、GetCurrentTemp 等全部 IO 方法
- `devices/light.go`、`switch.go`、`fan.go`、`cover.go` 等:同上
**不修改的方法**(纯本地操作):
- SubProp / UnsubProp / SubEvent / UnsubEvent / SubDeviceState / UnsubDeviceState
- OnStateChanged
- NewClient / NewDevice 等工厂方法
---
### #2 `PropertyValue.Type` 字段在回调场景下不可用
**问题**:`PropertyValue` 定义了 `Type string` 字段标注"SPEC format"。但 SubProp 回调中 miot 传入的 params 只有 `{did, siid, piid, value}`,没有 type 信息。该字段只在主动 GetProp 时才有可能从 SPEC 查到。
**当前代码**(types.go):
```go
type PropertyValue struct {
DID string `json:"did"`
SIID int `json:"siid"`
PIID int `json:"piid"`
Code int `json:"code"`
Value interface{} `json:"value"`
Type string `json:"type"` // SPEC format: "bool"/"uint8"/"float"/"string"
}
```
**修复方案**:改为注释说明只在 GetProp 场景可用。
---
### #3 缺少通用的设备类型判断/分类机制
**问题**:用户拿到 `[]*DeviceInfo` 后无法区分哪台是空调、哪台是灯。只能盲目试 `devices.NewAirConditioner(...)` 看返回 error。
**修复方案**:在 `xiaomi/devices/` 新增分类体系:
**新文件 `kind.go`**:
```go
package devices
// Kind 设备大类
type Kind string
const (
KindAirConditioner Kind = "air_conditioner"
KindLight Kind = "light"
KindSwitch Kind = "switch"
KindFan Kind = "fan"
KindCover Kind = "cover"
KindHumidifier Kind = "humidifier"
KindVacuum Kind = "vacuum"
KindWaterHeater Kind = "water_heater"
KindThermostat Kind = "thermostat"
KindUnknown Kind = "unknown"
)
// Classify 从 DeviceInfo 快速判断设备种类(基于 URN/Model 前缀匹配,不依赖 SPEC 加载)
func Classify(info *xiaomi.DeviceInfo) Kind
```
**types.go 中加方法**:
```go
func (d *DeviceInfo) DeviceKind() devices.Kind
```
**client.go 中加过滤器**:
```go
func FilterByKind(kind devices.Kind) DeviceFilter
```
---
### #4 缺少 `Device` 基础接口
**问题**:BaseDevice 目前是 struct 而非 interface。设备控制抽象需要一个通用 `Device` 接口让所有设备类型内嵌,工厂函数才能返回统一类型。
**当前代码**(devices/base.go):
```go
type BaseDevice struct {
client *xiaomi.Client
info *xiaomi.DeviceInfo
}
```
**修复方案**:在 base.go 中拆分接口和实现:
```go
// Device 所有设备控制接口的公共基础
type Device interface {
DID() string
Name() string
Model() string
Online() bool
Info() *xiaomi.DeviceInfo
}
// BaseDevice 公共实现(嵌入到具体类型中)
type BaseDevice struct {
client *xiaomi.Client
info *xiaomi.DeviceInfo
}
// 实现 Device 接口的所有方法...
```
各设备接口改为内嵌 Device:
```go
type AirConditioner interface {
Device // ← 嵌入
TurnOn() error
// ...
}
type Light interface {
Device
TurnOn() error
// ...
}
```
**新文件 `factory.go`**(通用工厂):
```go
// NewDevice 根据 DeviceInfo 自动创建对应的设备控制接口
// 返回 Device,调用方用 type switch 分流
func NewDevice(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Device, error)
```
使用模式:
```go
dev, err := devices.NewDevice(client, info)
switch d := dev.(type) {
case devices.AirConditioner:
d.TurnOn(ctx)
case devices.Light:
d.SetBrightness(ctx, 80)
}
```
---
## P1 — 重要项
### #5 HomeInfo 缺少地理位置字段
**问题**:Python 版 `get_homeinfos_async` 返回的 home 包含 `city_id`、`longitude`、`latitude`、`address`,对位置相关自动化有用。
**修复方案**:HomeInfo 结构体加字段:
```go
type HomeInfo struct {
// ... 现有字段 ...
CityID string `json:"city_id"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
Address string `json:"address"`
}
```
---
### #6 Client 的 homes 缓存语义不清
**问题**:Client struct 定义了 `homes map[string]*HomeInfo` 暗示缓存,但 GetHomeList 每次都调 HTTP API。缓存何时加载、何时失效均未说明。
**修复方案**:去掉 `homes` 缓存字段,Client 保持无状态:
```go
type Client struct {
inner *miot.MIoTClient
mu sync.RWMutex
}
```
---
### #7 枚举值与 SPEC 原始值的映射关系未定义
**问题**:`ACMode` 值是 `"cool"`、`"heat"` 等字符串,但 SPEC 中 mode 是数字 0-4。SpecResolver 需要做双向值映射,文档未提。
**修复方案**:新增 `specs/mapper.go`:
```go
package specs
// PropertyMapper SPEC 原始值 ↔ 枚举字符串 双向映射
type PropertyMapper struct {
rawToEnum map[interface{}]string
enumToRaw map[string]interface{}
}
// NewPropertyMapper 从 SPEC value-list + 外部映射表 构造
func NewPropertyMapper(vlist []miot.MIoTSpecValueItem, valueMap map[interface{}]string) *PropertyMapper
func (m *PropertyMapper) ToEnum(raw interface{}) (string, bool)
func (m *PropertyMapper) ToRaw(enum string) (interface{}, bool)
```
---
## P2 — 优化项
### #8 DeviceInfo 无直接可读的设备类型
**解决**:已通过 P0 #3 的 `DeviceKind()` 方法和 `Classify()` 函数解决。
---
### #9 各设备接口 OnStateChanged 返回值不统一
**问题**:OnStateChanged 返回 `(string, error)`,但 SubProp/SubEvent 返回 `(string, error)` — 这里它们是统一的。但缺少对 subID 语义的说明。
**修复方案**:注释统一说明 subID 是订阅标识符,可用于取消。
---
### #10 例中 ACMode 枚举值和 SPEC 原始值需对应
**解决**:已通过 P1 #7 的 PropertyMapper 解决。
---
### #11 使用示例未体现上下文控制和工厂模式
**修复方案**:使用示例改为展示 `context.WithTimeout`、`devices.NewDevice` 通用工厂、`type switch` 分流模式。
---
## 修复后需新增的 ADR
### ADR-006:所有 IO 方法携带 context.Context
- 决定:IO 方法首参加 ctx;订阅/工厂等本地方法不加
- 代价:方法签名变长
### ADR-007:Device 基础接口 + Kind 分类体系
- 决定:Device 接口为所有设备接口的公共基;Classify 基于 URN 字符串匹配快速分类;NewDevice 为通用工厂
- 代价:Classify 依赖字符串匹配,非常见型号可能误判,匹配表需可扩展
---
## 修复实施清单
| 序号 | 等级 | 修改文件 | 内容 |
|------|------|----------|------|
| 1 | P0 | client.go, properties.go, actions.go, devices/*.go | 所有 IO 方法加 ctx context.Context 首参 |
| 2 | P0 | types.go | PropertyValue.Type 注释改为"仅 GetProp 场景可用" |
| 3 | P0 | devices/kind.go(新) | Kind 枚举 + Classify 函数 |
| 3 | P0 | types.go | DeviceInfo.DeviceKind() 方法 |
| 3 | P0 | client.go | 新增 FilterByKind |
| 4 | P0 | devices/base.go | 拆分 Device 接口 + BaseDevice 实现 |
| 4 | P0 | devices/factory.go(新) | NewDevice 通用工厂 |
| 4 | P0 | devices/*.go | 各接口内嵌 Device |
| 5 | P1 | types.go | HomeInfo 加 CityID/Longitude/Latitude/Address |
| 6 | P1 | client.go | Client 去掉 homes 缓存字段 |
| 7 | P1 | specs/mapper.go(新) | PropertyMapper 双向值映射 |
| 9 | P2 | devices/*.go | OnStateChanged 统一命名返回值 subID |
| 11 | P2 | ARCH_PLAN.md 使用示例 | 更新为 ctx + factory + type switch 模式 |
---
## 修复后的包结构变化
```
xiaomi/
├── devices/
│ ├── base.go # 修改:拆分 Device 接口 + BaseDevice 实现
│ ├── kind.go # 新增:Kind 枚举 + Classify 分类
│ ├── factory.go # 新增:NewDevice 通用工厂
│ └── ... # 修改:各接口内嵌 Device,IO 方法加 ctx
├── specs/
│ ├── resolver.go # 不变
│ └── mapper.go # 新增:PropertyMapper 值映射
├── client.go # 修改:加 ctx、去 homes 缓存、加 FilterByKind
├── types.go # 修改:HomeInfo 加字段、PropertyValue.Type 注释、DeviceKind 方法
├── properties.go # 修改:加 ctx
├── actions.go # 修改:加 ctx
└── ... # 其他 IO 方法加 ctx
```
---
*此报告为独立审查文档,详细修复已在 ARCH_PLAN.md 中执行。*