Files
xiaomihome/miot/migration/ARCH_PLAN.md
T
4566704 35efed7d15 chore: 修复 import 路径为 xiaomihome/miot,补充 README 项目文档
go.mod 模块名为 xiaomihome,将所有内部 import 从 "miot" 更新为
"xiaomihome/miot",涉及所有测试文件和示例文件。新增 README.md 项目
介绍文档(架构概览、模块说明、移植进度等)。新增 ARCH_PLAN.md 架构
设计文档。
2026-06-28 23:21:18 +08:00

913 lines
29 KiB
Markdown
Raw 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.
# go-xiaomihome 上层封装架构设计规划
> 状态:Proposed | 版本:v1.0 | 日期:2026-06-28
---
## 一、问题分析
当前 `miot` 包的三个核心痛点:
| 痛点 | 现状 | 影响 |
|------|------|------|
| **大量 map[string]interface{}** | 22+ 个 API 返回 map,10+ 个结构体字段使用 map,回调全部用 map | 无类型安全、无 IDE 补全、运行时才发现错误 |
| **高层信息分散在裸 map** | GetUserInfo/GetHomeInfos/GetDevices 全部返回 map | 用户/家庭/房间/设备信息需要用 `["key"]` 取值 |
| **设备控制无抽象** | 直接走 SetProp(siid, piid, value) | 不知道空调当前模式/温度具体对应哪个 siid/piid |
---
## 二、设计目标
1. **强类型封装**:所有 API 返回值从 `map[string]interface{}` 转为明确的结构体
2. **设备控制抽象**:空调、灯光、开关等提供统一控制接口,隐藏 siid/piid
3. **订阅回调强类型**:PropsChanged / EventOccurred / DeviceStateChanged 回调携带强类型数据
4. **miot 底层隔离**:上层使用者不直接接触 miot 包的任何类型(可选零依赖)
5. **渐进迁移**:miot 包自行不变,封装层纯粹是一层薄壳
---
## 三、包结构设计
```
go-xiaomihome/
├── miot/ # 现有底层包(不动)
│ └── ... # 与 Python miot/ 严格对齐
│
├── xiaomi/ # [新] 高级封装层
│ ├── go.mod # module xiaomihome/xiaomi
│ │
│ ├── types.go # 所有强类型结构体定义
│ ├── client.go # Client 主入口(包装 miot.MIoTClient)
│ ├── devices.go # 设备列表 + 设备信息
│ ├── homes.go # 家庭/房间信息
│ ├── user.go # 用户信息
│ ├── properties.go # 属性读写封装(强类型)
│ ├── actions.go # Action 调用封装
│ ├── subscribe.go # 订阅通知 API(强类型回调)
│ ├── errors.go # 封装层错误定义
│ │
│ ├── devices/ # [新] 设备控制抽象
│ │ ├── base.go # Device 基础接口 + BaseDevice 实现
│ │ ├── air_conditioner.go # 空调控制接口
│ │ ├── light.go # 灯光控制接口
│ │ ├── switch.go # 开关控制接口
│ │ ├── fan.go # 风扇控制接口
│ │ ├── cover.go # 窗帘/晾衣架控制接口
│ │ ├── humidifier.go # 加湿器控制接口
│ │ ├── vacuum.go # 扫地机控制接口
│ │ ├── water_heater.go # 热水器控制接口
│ │ └── thermostat.go # 温控器/电热毯控制接口
│ │
│ └── specs/ # [新] SPEC 辅助(设备能力查询)
│ └── resolver.go # 按功能语义解析 SPEC,自动找 siid/piid
```
---
## 四、核心类型设计
### 4.1 types.go — 强类型结构体
```go
package xiaomi
// ========== 基础信息 ==========
// UserInfo 用户信息
type UserInfo struct {
UID string `json:"uid"`
NickName string `json:"miliao_nick"`
AvatarURL string `json:"avatar_url"`
}
// HomeInfo 家庭信息
type HomeInfo struct {
ID string `json:"id"`
Name string `json:"name"`
UID string `json:"uid"`
GroupID string `json:"group_id"`
Rooms []RoomInfo `json:"rooms"`
DidList []string `json:"did_list"`
ShareState bool `json:"share_state"`
}
// RoomInfo 房间信息
type RoomInfo struct {
ID string `json:"id"`
Name string `json:"name"`
HomeID string `json:"home_id"`
DidList []string `json:"did_list"` // 房间内设备 DID
}
// DeviceInfo 设备信息(替代 map[string]interface{})
type DeviceInfo struct {
DID string `json:"did"`
Name string `json:"name"`
Model string `json:"model"`
Manufacturer string `json:"manufacturer"`
FWVersion string `json:"fw_version"`
Icon string `json:"icon"`
URN string `json:"urn"` // 设备 SPEC 类型
Token string `json:"token"`
IP string `json:"local_ip"`
RSSI int `json:"rssi"`
SSID string `json:"ssid"`
BSSID string `json:"bssid"`
Online bool `json:"is_online"`
HomeID string `json:"home_id"`
HomeName string `json:"home_name"`
RoomID string `json:"room_id"`
RoomName string `json:"room_name"`
OwnerID string `json:"owner_id"`
ParentID string `json:"parent_id"`
ParentModel string `json:"parent_model"`
GroupID string `json:"group_id"`
ConnectType int `json:"connect_type"` // 0=WiFi, 1=BLE, 2=ZigBee, ...
}
// ========== 属性/事件 ==========
// PropertyValue 属性值(强类型)
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"
}
// PropertiesChanged 属性变化事件
type PropertiesChanged struct {
DID string `json:"did"`
Properties []PropertyValue `json:"properties"`
}
// EventOccurred 事件发生通知
type EventOccurred struct {
DID string `json:"did"`
SIID int `json:"siid"`
EIID int `json:"eiid"`
Arguments map[string]interface{} `json:"arguments"`
Timestamp int64 `json:"timestamp"`
}
// DeviceState 设备在线状态
type DeviceState string
const (
StateOnline DeviceState = "online"
StateOffline DeviceState = "offline"
)
// ========== Action ==========
// ActionResult Action 执行结果
type ActionResult struct {
DID string `json:"did"`
SIID int `json:"siid"`
AIID int `json:"aiid"`
Code int `json:"code"`
Out []map[string]interface{} `json:"out"`
}
```
### 4.2 Client — 主入口
```go
package xiaomi
// Client 高级封装客户端(包装 miot.MIoTClient)
type Client struct {
inner *miot.MIoTClient // 内部 miot 实例(不对外暴露)
currentUser UserInfo
homes map[string]*HomeInfo
mu sync.RWMutex
}
// NewClient 创建客户端
func NewClient(inner *miot.MIoTClient) *Client
// ========== 用户/家庭/房间 ==========
// GetUserInfo 获取用户信息(强类型返回)
func (c *Client) GetUserInfo() (*UserInfo, error)
// GetHomeList 获取所有家庭列表
func (c *Client) GetHomeList() ([]*HomeInfo, error)
// GetHome 获取指定家庭详情(含房间)
func (c *Client) GetHome(homeID string) (*HomeInfo, error)
// GetRooms 获取指定家庭的所有房间
func (c *Client) GetRooms(homeID string) ([]*RoomInfo, error)
// GetRoomDevices 获取指定房间的设备 DID 列表
func (c *Client) GetRoomDevices(homeID, roomID string) ([]string, error)
// ========== 设备列表 ==========
// GetDevices 获取所有设备信息(强类型返回)
func (c *Client) GetDevices(opts ...DeviceFilter) ([]*DeviceInfo, error)
// GetDevice 获取单个设备信息
func (c *Client) GetDevice(did string) (*DeviceInfo, error)
// RefreshDevices 刷新设备列表
func (c *Client) RefreshDevices() error
// DeviceFilter 设备过滤器函数类型
type DeviceFilter func(*DeviceInfo) bool
// FilterByHome 按家庭过滤
func FilterByHome(homeID string) DeviceFilter
// FilterByRoom 按房间过滤
func FilterByRoom(roomID string) DeviceFilter
// FilterByOnline 只返回在线设备
func FilterByOnline() DeviceFilter
// FilterByModel 按型号过滤
func FilterByModel(model string) DeviceFilter
```
### 4.3 属性读写封装
```go
package xiaomi
// GetProp 读取单个属性(与 miot.GetProp 对齐命名)
func (c *Client) GetProp(did string, siid, piid int) (*PropertyValue, error)
// SetProp 写入单个属性(与 miot.SetProp 对齐命名)
func (c *Client) SetProp(did string, siid, piid int, value interface{}) error
// GetProps 批量读取属性(与 miot.GetProps 对齐命名)
func (c *Client) GetProps(params []PropKey) ([]*PropertyValue, error)
// SetProps 批量写入属性(与 miot.SetProps 对齐命名)
func (c *Client) SetProps(params []PropKeyValue) ([]*PropResult, error)
// PropKey 属性键对
type PropKey struct {
DID string
SIID int
PIID int
}
// PropKeyValue 属性键值对
type PropKeyValue struct {
PropKey
Value interface{}
}
// PropResult 写入结果
type PropResult struct {
PropKey
Code int
}
// ========== Action 封装 ==========
// Action 调用设备 Action(与 miot.Action 对齐命名)
func (c *Client) Action(did string, siid, aiid int, params []interface{}) (*ActionResult, error)
```
### 4.4 订阅通知接口
**底层机制回顾**:miot 包通过 `subTree`(topic 匹配树)管理订阅。设备消息从 MQTT/LAN 推送到达后,走 `OnPropMsg` / `OnEventMsg` → `subTree.Match(topic)` → 回调原始 `func(map[string]interface{}, interface{})` handler。封装层**不直接碰 MQTT**,而是复用 miot 的 SubProp/SubEvent/SubDeviceState,做 handler 签名转换。
数据流:
```
MQTT/LAN 推送
→ miot.OnPropMsg(params) / miot.OnEventMsg(params)
→ subTree.Match(did/p/siid/piid)
→ miot handler: func(map[string]interface{}, interface{})
│
▼ 封装层转换(本文范围)
→ xiaomi handler: func(did string, prop *PropertyValue)
```
```go
package xiaomi
// ========== 强类型回调定义 ==========
// PropHandler 属性变化回调(强类型,替代 miot 的 map handler)
type PropHandler func(did string, prop *PropertyValue)
// EventHandler 事件回调(强类型)
type EventHandler func(did string, event *EventOccurred)
// DeviceStateHandler 设备在线状态变化回调
type DeviceStateHandler func(did string, state DeviceState)
// ========== 订阅方法(与 miot.SubProp/SubEvent/SubDeviceState 对齐命名)==========
// SubProp 订阅设备属性变化通知
// 内部调用 miot.MIoTClient.SubProp,在 adapter handler 中完成 map→PropertyValue 转换
// siid/piid 为 0 表示订阅该设备所有属性
// 返回 subscription ID(与 miot.SubProp 一致)
func (c *Client) SubProp(did string, siid, piid int, handler PropHandler) (string, error)
// UnsubProp 取消属性订阅(与 miot.UnsubProp 对齐命名)
func (c *Client) UnsubProp(did string, subID string) error
// SubEvent 订阅设备事件通知
// 内部调用 miot.MIoTClient.SubEvent,完成 map→EventOccurred 转换
func (c *Client) SubEvent(did string, siid, eiid int, handler EventHandler) (string, error)
// UnsubEvent 取消事件订阅
func (c *Client) UnsubEvent(did string, subID string) error
// SubDeviceState 订阅设备在线状态变化
// 内部调用 miot.MIoTClient.SubDeviceState
func (c *Client) SubDeviceState(did string, handler DeviceStateHandler) error
// UnsubDeviceState 取消设备状态订阅
func (c *Client) UnsubDeviceState(did string) error
```
**实现要点**:
```go
// SubProp 实现示意:包装 miot handler,转换 map → PropertyValue
func (c *Client) SubProp(did string, siid, piid int, handler PropHandler) (string, error) {
// 1. 包装 handler:map → PropertyValue
wrappedHandler := func(params map[string]interface{}, ctx interface{}) {
prop := &PropertyValue{
DID: params["did"].(string),
SIID: toInt(params["siid"]),
PIID: toInt(params["piid"]),
Value: params["value"],
}
handler(did, prop)
}
// 2. 委托给 miot 底层(触发 subTree 注册 + MQTT/LAN 底层订阅)
subID := c.inner.SubProp(did, wrappedHandler, siid, piid)
// 3. 记录 subID→did 映射(用于 UnsubProp 时清理)
c.propSubs[subID] = did
return subID, nil
}
```
**subID 映射管理**(miot.SubProp 返回 string subID,miot.UnsubProp 需要 subID):
```go
type Client struct {
// ...
propSubs map[string]string // subID → did(用于 UnsubProp 清理)
eventSubs map[string]string // subID → did
}
```
**与 miot 底层签名的对应关系**:
| 封装层 API | 内部调用 miot 方法 | miot handler 签名 | 转换后 handler 签名 |
|---|---|---|---|
| SubProp | SubProp(did, wrapped, siid, piid) | `func(map[string]interface{}, interface{})` | `func(string, *PropertyValue)` |
| SubEvent | SubEvent(did, wrapped, siid, eiid) | `func(map[string]interface{}, interface{})` | `func(string, *EventOccurred)` |
| SubDeviceState | SubDeviceState(did, wrapped) | `func(string, MIoTDeviceState, interface{})` | `func(string, DeviceState)` |
---
## 五、设备控制抽象设计
### 5.1 架构思路
```
xiaomi/devices/
├── base.go # Device 基础接口
├── air_conditioner.go # AirConditioner 接口
├── light.go # Light 接口
├── switch.go # Switch 接口
├── fan.go # Fan 接口
├── cover.go # Cover 接口
├── humidifier.go # Humidifier 接口
├── vacuum.go # Vacuum 接口
├── water_heater.go # WaterHeater 接口
└── thermostat.go # Thermostat 接口
```
**设计原则**:
- 每种设备类型定义一个**接口**(Interface)和**具体实现**(Struct)
- 具体实现内部持有 `*xiaomi.Client` 引用,将高层语义操作翻译为 `siid/piid` 调用
- 通过 `specs/resolver.go` 从 SPEC 中自动解析出对应的 siid/piid
- 用户不关心 `siid=2, piid=3` 是什么,只关心 `ac.SetTargetTemp(26)` / `ac.SetMode(ModeCool)`
### 5.2 BaseDevice
```go
package devices
// BaseDevice 所有设备控制类的公共基础
type BaseDevice struct {
client *xiaomi.Client
info *xiaomi.DeviceInfo
}
// DID 返回设备 DID
func (d *BaseDevice) DID() string
// Name 返回设备名称
func (d *BaseDevice) Name() string
// Model 返回设备型号
func (d *BaseDevice) Model() string
// Online 返回设备在线状态
func (d *BaseDevice) Online() bool
// Info 返回完整设备信息
func (d *BaseDevice) Info() *xiaomi.DeviceInfo
// GetProp 快捷属性读取(委托给 xiaomi.Client.GetProp)
func (d *BaseDevice) GetProp(siid, piid int) (*xiaomi.PropertyValue, error)
// SetProp 快捷属性写入
func (d *BaseDevice) SetProp(siid, piid int, value interface{}) error
// Action 快捷 Action 调用
func (d *BaseDevice) Action(siid, aiid int, params []interface{}) (*xiaomi.ActionResult, error)
// SubProp 订阅属性变化(委托给 xiaomi.Client.SubProp)
func (d *BaseDevice) SubProp(siid, piid int, handler xiaomi.PropHandler) (string, error)
// SubEvent 订阅事件
func (d *BaseDevice) SubEvent(siid, eiid int, handler xiaomi.EventHandler) (string, error)
// SubDeviceState 订阅在线状态
func (d *BaseDevice) SubDeviceState(handler xiaomi.DeviceStateHandler) error
```
### 5.3 空调控制接口
```go
// AirConditioner 空调统一控制接口
type AirConditioner interface {
// 基础能力
TurnOn() error
TurnOff() error
IsOn() (bool, error)
// 模式控制
SetMode(mode ACMode) error
GetMode() (ACMode, error)
// 温度控制
SetTargetTemp(celsius float64) error
GetTargetTemp() (float64, error)
GetCurrentTemp() (float64, error) // 室内温度
// 风速控制
SetFanSpeed(speed FanSpeed) error
GetFanSpeed() (FanSpeed, error)
// 摆风控制
SetSwing(mode SwingMode) error
GetSwing() (SwingMode, error)
// 湿度(部分空调支持)
SetTargetHumidity(pct int) error
GetTargetHumidity() (int, error)
GetCurrentHumidity() (int, error)
// 电辅热
SetAuxHeat(on bool) error
GetAuxHeat() (bool, error)
// 节能
SetEco(on bool) error
GetEco() (bool, error)
// 订阅
OnStateChanged(handler func(state ACState)) (string, error)
}
// ACMode 空调模式
type ACMode string
const (
ModeCool ACMode = "cool"
ModeHeat ACMode = "heat"
ModeFan ACMode = "fan"
ModeDry ACMode = "dry"
ModeAuto ACMode = "auto"
)
// FanSpeed 风速
type FanSpeed string
const (
SpeedLow FanSpeed = "low"
SpeedMedium FanSpeed = "medium"
SpeedHigh FanSpeed = "high"
SpeedAuto FanSpeed = "auto"
)
// SwingMode 摆风
type SwingMode string
const (
SwingOff SwingMode = "off"
SwingVertical SwingMode = "vertical"
SwingHorizontal SwingMode = "horizontal"
SwingBoth SwingMode = "both"
)
// ACState 空调完整状态
type ACState struct {
Power bool `json:"power"`
Mode ACMode `json:"mode"`
TargetTemp float64 `json:"target_temp"`
CurrentTemp float64 `json:"current_temp"`
TargetHumidity int `json:"target_humidity"`
CurrentHumidity int `json:"current_humidity"`
FanSpeed FanSpeed `json:"fan_speed"`
Swing SwingMode `json:"swing"`
AuxHeat bool `json:"aux_heat"`
Eco bool `json:"eco"`
}
```
**实现原理**:`acDevice` 结构体内部维护一个 `specMap`,通过 `specs/resolver.go` 从 SPEC 中找出:
- `power` → `siid=2, piid=1`
- `mode` → `siid=2, piid=2`
- `target_temp` → `siid=2, piid=3`
- `fan_speed` → `siid=2, piid=4`
- ...
### 5.4 灯光控制接口
```go
// Light 灯光统一控制接口
type Light interface {
TurnOn() error
TurnOff() error
Toggle() error
// 亮度
SetBrightness(pct int) error // 1-100, -1 表示不支持
GetBrightness() (int, error)
// 色温
SetColorTemp(kelvin int) error // 2700-6500, -1 表示不支持
GetColorTemp() (int, error)
// 颜色
SetColor(r, g, b int) error // 0-255, -1 表示不支持
GetColor() (r, g, b int, error)
OnStateChanged(handler func(state LightState)) (string, error)
}
type LightState struct {
Power bool `json:"power"`
Brightness int `json:"brightness"`
ColorTemp int `json:"color_temp"`
ColorRGB [3]int `json:"color_rgb"`
}
```
### 5.5 开关控制接口
```go
// Switch 开关统一控制接口
type Switch interface {
TurnOn() error
TurnOff() error
Toggle() error
IsOn() (bool, error)
OnStateChanged(handler func(on bool)) (string, error)
}
```
### 5.6 其他设备接口(概要)
```go
// Fan 风扇接口
type Fan interface {
TurnOn() error
TurnOff() error
SetSpeed(speed FanLevel) error // 0=off, 1-4 档位
SetOscillation(on bool) error
SetNaturalWind(on bool) error
}
// Cover 窗帘/晾衣架
type Cover interface {
Open() error
Close() error
Stop() error
SetPosition(pct int) error // 0=关, 100=全开
GetPosition() (int, error)
}
// Humidifier 加湿器/除湿机
type Humidifier interface {
TurnOn() error
TurnOff() error
SetTargetHumidity(pct int) error
SetFanSpeed(speed FanLevel) error
}
// Vacuum 扫地机
type Vacuum interface {
Start() error
Stop() error
ReturnToDock() error
SetFanSpeed(speed FanLevel) error
GetStatus() (VacuumStatus, error)
}
// WaterHeater 热水器
type WaterHeater interface {
TurnOn() error
TurnOff() error
SetTargetTemp(celsius float64) error
GetCurrentTemp() (float64, error)
}
// Thermostat 温控器
type Thermostat interface {
SetTargetTemp(celsius float64) error
GetCurrentTemp() (float64, error)
SetHeating(on bool) error
}
```
---
## 六、specs/resolver.go — SPEC 语义解析器
为了让设备控制类能自动找到 `power` / `mode` / `target_temp` 等语义属性对应的 `siid/piid`,需要一个规约解析器:
```go
package specs
// PropertyResolver 属性语义解析器
// 输入:设备的 SPEC Instance
// 输出:语义 → (siid, piid) 映射
type PropertyResolver struct {
spec *miot.MIoTSpecInstance
// cache
byType map[string][]*miot.MIoTSpecProperty // "power" → [{siid:2, piid:1}]
byDesc map[string]*miot.MIoTSpecProperty // "开关" → {siid:2, piid:1}
byFormat map[string][]*miot.MIoTSpecProperty // "bool" → [...]
}
// NewPropertyResolver 创建解析器
func NewPropertyResolver(spec *miot.MIoTSpecInstance) *PropertyResolver
// FindByType 按 SPEC property type 查找
// type 如: "on", "mode", "target-temperature", "fan-level", "brightness"
func (r *PropertyResolver) FindByType(propType string) (siid, piid int, found bool)
// FindByFormat 按数据格式查找
// format 如: "bool", "uint8", "float", "string"
// 返回第一个匹配的
func (r *PropertyResolver) FindByFormat(format string, siid int) (piid int, found bool)
// FindAction 查找 Action
func (r *PropertyResolver) FindAction(siid int, actionType string) (aiid int, found bool)
```
### 解析策略
每种设备控制接口的构造流程:
```go
// acDevice 构造函数
func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirConditioner, error) {
// 1. 获取设备 SPEC
device := client.GetMIoTDevice(info.DID) // 内部可能获取
spec := device.SpecInstance()
// 2. 创建解析器
resolver := specs.NewPropertyResolver(spec)
// 3. 解析语义映射
powerSIID, powerPIID, _ := resolver.FindByType("on")
modeSIID, modePIID, _ := resolver.FindByType("mode")
tempSIID, tempPIID, _ := resolver.FindByType("target-temperature")
fanSIID, fanPIID, _ := resolver.FindByType("fan-level")
// 4. 如果某个语义映射找不到,该功能标记为不支持
return &acDevice{
BaseDevice: NewBaseDevice(client, info),
propMap: propMap{
power: {powerSIID, powerPIID},
mode: {modeSIID, modePIID},
temp: {tempSIID, tempPIID},
fan: {fanSIID, fanPIID},
},
}, nil
}
```
---
## 七、关键设计决策
### ADR-001:封装层不修改 miot 包
**状态**:Proposed
**背景**:miot 包需要与 Python 严格对齐,API 签名和行为不能轻易改变。
**决定**:上层封装 `xiaomi/` 作为独立 Go module(`module xiaomihome/xiaomi`),通过 import miot 使用,不做任何修改。封装层专注于转换和简化。
**后果**:
- ✅ miot 对齐性不受影响
- ✅ 封装层可独立测试
- ❌ 封装层需要处理 miot 类型转换(额外开销但可控)
---
### ADR-002:设备控制接口按功能语义而非 SPEC 编号
**状态**:Proposed
**背景**:不同型号空调的 `power` 属性可能是 `siid=2,piid=1` 或 `siid=3,piid=1`,必须抽象。
**决定**:接口方法使用功能语义(`SetTargetTemp`、`SetMode`),由 `specs.PropertyResolver` 在构造时从 SPEC 自动解析映射关系。解析失败的功能标记为不支持。
**后果**:
- ✅ 用户无需关心 siid/piid
- ✅ 跨型号兼容
- ❌ 需要在 SPEC 解析阶段做额外工作
- ❌ 部分非常规设备可能解析不完全,需要降级方案
---
### ADR-003:Interface 而非 Struct 嵌入
**状态**:Proposed
**背景**:有两种设计方式:纯 interface 或 struct 嵌入 + 方法重写。
**决定**:每种设备类型定义 interface + 私有 struct 实现。工厂函数返回 interface。这样用户代码只依赖接口,便于 mock 测试。
**后果**:
- ✅ 可测试性强(mock 接口即可)
- ✅ 未来可替换实现
- ❌ 增加了一点 boilerplate(接口 + 实现双定义)
---
### ADR-004:回调使用强类型事件对象
**状态**:Proposed
**背景**:底层回调全是 `func(map[string]interface{}, interface{})`。
**决定**:封装层在内部转换底层 map 回调为强类型事件对象(`*PropertyValue`、`*EventOccurred`),对上层暴露强类型回调签名。
**后果**:
- ✅ 类型安全
- ✅ IDE 补全
- ❌ 封包/解包开销(每个回调一次类型断言,影响极小)
---
### ADR-005:Filter 模式用于设备过滤
**状态**:Proposed
**背景**:用户需要按家庭/房间/在线状态筛选设备。
**决定**:使用函数式 `DeviceFilter` 模式(`func(*DeviceInfo) bool`),提供预设 Filter 组合。
**后果**:
- ✅ 灵活可组合
- ✅ 无额外依赖
- ❌ 性能对比预建索引略差(但对家庭设备量级无影响)
---
## 八、实施计划
### Phase 1 — 基础封装(优先级 P0)
| 任务 | 文件 | 工作量 |
|------|------|--------|
| 项目骨架搭建 | `xiaomi/go.mod`, module 初始化 | 小 |
| types.go 结构体定义 | `xiaomi/types.go` | 中 |
| Client 基础框架 | `xiaomi/client.go` | 中 |
| 用户/家庭/房间信息封装 | `xiaomi/user.go`, `homes.go` | 中 |
| 设备列表封装 | `xiaomi/devices.go` | 中 |
| 属性读写封装 | `xiaomi/properties.go` | 中 |
| Action 封装 | `xiaomi/actions.go` | 小 |
| 错误定义 | `xiaomi/errors.go` | 小 |
### Phase 2 — 订阅与通知(优先级 P0)
| 任务 | 文件 | 工作量 |
|------|------|--------|
| 订阅通知接口 | `xiaomi/subscribe.go` | 中 |
| 底层回调 → 强类型事件转换 | `xiaomi/subscribe.go` | 中 |
| 设备状态订阅 | `xiaomi/subscribe.go` | 小 |
### Phase 3 — 设备控制抽象(优先级 P1)
| 任务 | 文件 | 工作量 |
|------|------|--------|
| SPEC 语义解析器 | `xiaomi/specs/resolver.go` | 大 |
| 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` | 中 |
### Phase 4 — 测试和文档(优先级 P2)
| 任务 | 文件 | 工作量 |
|------|------|--------|
| 单元测试 | `*_test.go` | 大 |
| 集成示例 | `xiaomi/examples/` | 中 |
| API 文档 | `xiaomi/README.md` | 中 |
---
## 九、使用示例(目标形态)
```go
package main
import (
"fmt"
"xiaomihome/xiaomi"
"xiaomihome/xiaomi/devices"
)
func main() {
// 1. 创建底层 miot 客户端(照常)
// ... miot.NewMIoTClient(...) ...
// 2. 用上层封装包装
client := xiaomi.NewClient(miotClient)
// 3. 获取用户信息(强类型!)
user, _ := client.GetUserInfo()
fmt.Printf("用户: %s (UID: %s)\n", user.NickName, user.UID)
// 4. 获取家庭和房间
homes, _ := client.GetHomeList()
for _, h := range homes {
fmt.Printf("家庭: %s\n", h.Name)
for _, r := range h.Rooms {
fmt.Printf(" 房间: %s (%d 设备)\n", r.Name, len(r.DidList))
}
}
// 5. 获取设备列表(可过滤)
devices_, _ := client.GetDevices(
xiaomi.FilterByHome(homes[0].ID),
xiaomi.FilterByOnline(),
)
for _, d := range devices_ {
fmt.Printf("设备: %s (%s) 在线:%v\n", d.Name, d.Model, d.Online)
}
// 6. 创建空调控制对象(自动解析 SPEC)
acInfo := devices_[0] // 假设是个空调
ac, err := devices.NewAirConditioner(client, acInfo)
if err != nil {
// 该设备不是空调或不支持
return
}
// 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)
})
// 9. 灯光控制
light, _ := devices.NewLight(client, lightInfo)
light.TurnOn()
light.SetBrightness(80)
light.SetColorTemp(4000)
// 10. 开关控制
sw, _ := devices.NewSwitch(client, switchInfo)
sw.Toggle()
}
```
---
## 十、待讨论问题
1. **工厂模式**:`devices.NewAirConditioner(client, info)` 还是 `client.NewAirConditioner(info)`?前者职责分离更清晰,后者使用稍方便。
2. **SPEC 解析失败降级**:当某个功能语义找不到时,是返回 error(构造失败)还是返回一个部分功能的对象(不支持的方法返回 `ErrNotSupported`)?建议后者——空调不能调摆风总比整个空调不能用好。
3. **go-xiaomihome 主模块 go.mod**:目前 `go-xiaomihome/go.mod` 是 `module xiaomihome`,miot 是子包 `xiaomihome/miot`。新封装包是否作为 `xiaomihome/xiaomi` 子包,还是独立 repo?建议子包,降低引用复杂度。
4. **是否需要 Cloud 直连简化**:`xiaomi.Client` 的构造函数是否应支持直接传配置,内部自建 miot 客户端(完整封装),这样用户连 miot 都不需要 import。建议 Phase 4 再做。
---
*本文档为架构规划,尚未执行。*