Files
4566704 04333750ab feat: State→Props 命名统一 + 新增 Plug/Gateway 设备 + 分类系统重构
**命名统一(State → Props)**
- 所有设备类型:XxxState → XxxProps,GetState() → GetProps(),FetchState() → FetchProps()
- 回调接口:OnStateChanged → OnPropsChanged
- Bridge 层类型别名、示例代码、文档同步更名

**新增设备类型**
- KindPlug + Plug 驱动:智能插座/插头(TurnOn/TurnOff/IsOn/OnPropsChanged)
- KindGateway + Gateway 驱动:网关设备类型识别
- KindOccupancySensor 从 KindSensor 独立,添加 OccupancyProps

**设备分类系统改进**
- classifyByModel 改为按 key 长度降序匹配(防止 sensor 先于 sensor-occupy)
- 新增下划线变体识别(sensor_occupy、sensor_temp 等)
- outlet 重新归类为 Plug(非 Switch)
- 新增 Or() 组合过滤器

**连接桥增强**
- 设备创建成功后自动订阅属性变更
- 新增 nil 防护检查
- 支持 Plug/Speaker/Gateway/Occupancy 类型的 FetchProps 刷新
- 日志级别 Info → Debug

**AC 驱动增强**
- 新增 HasPower()、GetElectricPower()、GetPowerConsumption() 功率接口
- ACProps 新增 ElectricPower、PowerConsumption 字段
- valMapper:Desc 为空时 fallback 用 Name,key 统一小写

**MIoT 核心**
- 设备列表刷新改用 reflect.DeepEqual 全字段变更检测
- 移除 BLE/代理设备默认在线 hack
- RefreshDeviceAllProps 增加离线检查
- 新增 miot_errors.go:MIoT 错误码定义
- 全局日志级别 Info→Debug/Warn(降低噪音)

**示例与文档**
- 所有示例代码同步 API 变更
- 传感器示例改用 Or 组合过滤器
- ARCH_PLAN 等迁移文档同步更新
2026-08-05 17:54:08 +08:00

1098 lines
37 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.
# go-xiaomihome 上层封装架构设计规划
> 状态:Accepted | 版本:v1.1 | 日期: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 实现
│ │ ├── kind.go # Kind 枚举 + Classify 设备分类
│ │ ├── factory.go # NewDevice 通用工厂
│ │ ├── 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
│ └── mapper.go # SPEC 值映射(原始值 ↔ 枚举字符串)
```
---
## 四、核心类型设计
### 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"`
CityID string `json:"city_id"` // 城市ID
Longitude float64 `json:"longitude"` // 经度
Latitude float64 `json:"latitude"` // 纬度
Address string `json:"address"` // 地址
}
// 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, ...
}
// DeviceKind 从 URN/Model 快速判断设备大类(不依赖 SPEC 加载)
func (d *DeviceInfo) DeviceKind() devices.Kind
// ========== 属性/事件 ==========
// 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"` // 仅 GetProp 场景可用(从 SPEC 查询);SubProp 回调中为空
}
// 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
import "context"
// Client 高级封装客户端(包装 miot.MIoTClient)
type Client struct {
inner *miot.MIoTClient // 内部 miot 实例(不对外暴露)
mu sync.RWMutex
}
// NewClient 创建客户端
func NewClient(inner *miot.MIoTClient) *Client
// ========== 用户/家庭/房间 ==========
// GetUserInfo 获取用户信息(强类型返回)
func (c *Client) GetUserInfo(ctx context.Context) (*UserInfo, error)
// GetHomeList 获取所有家庭列表
func (c *Client) GetHomeList(ctx context.Context) ([]*HomeInfo, error)
// GetHome 获取指定家庭详情(含房间)
func (c *Client) GetHome(ctx context.Context, homeID string) (*HomeInfo, error)
// GetRooms 获取指定家庭的所有房间
func (c *Client) GetRooms(ctx context.Context, homeID string) ([]*RoomInfo, error)
// GetRoomDevices 获取指定房间的设备 DID 列表
func (c *Client) GetRoomDevices(ctx context.Context, homeID, roomID string) ([]string, error)
// ========== 设备列表 ==========
// GetDevices 获取所有设备信息(强类型返回)
func (c *Client) GetDevices(ctx context.Context, opts ...DeviceFilter) ([]*DeviceInfo, error)
// GetDevice 获取单个设备信息
func (c *Client) GetDevice(ctx context.Context, did string) (*DeviceInfo, error)
// RefreshDevices 刷新设备列表
func (c *Client) RefreshDevices(ctx context.Context) 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
// FilterByKind 按设备大类过滤
func FilterByKind(kind devices.Kind) DeviceFilter
```
### 4.3 属性读写封装
```go
package xiaomi
import "context"
// GetProp 读取单个属性(与 miot.GetProp 对齐命名)
func (c *Client) GetProp(ctx context.Context, did string, siid, piid int) (*PropertyValue, error)
// SetProp 写入单个属性(与 miot.SetProp 对齐命名)
func (c *Client) SetProp(ctx context.Context, did string, siid, piid int, value interface{}) error
// GetProps 批量读取属性(与 miot.GetProps 对齐命名)
func (c *Client) GetProps(ctx context.Context, params []PropKey) ([]*PropertyValue, error)
// SetProps 批量写入属性(与 miot.SetProps 对齐命名)
func (c *Client) SetProps(ctx context.Context, 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(ctx context.Context, 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 Device 基础接口 + BaseDevice 实现
```go
package devices
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
}
// 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(ctx context.Context, siid, piid int) (*xiaomi.PropertyValue, error)
// SetProp 快捷属性写入
func (d *BaseDevice) SetProp(ctx context.Context, siid, piid int, value interface{}) error
// Action 快捷 Action 调用
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)
// 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
import "context"
// AirConditioner 空调统一控制接口
type AirConditioner interface {
Device // 嵌入公共接口
// 基础能力
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
IsOn(ctx context.Context) (bool, error)
// 模式控制
SetMode(ctx context.Context, mode ACMode) error
GetMode(ctx context.Context) (ACMode, error)
// 温度控制
SetTargetTemp(ctx context.Context, celsius float64) error
GetTargetTemp(ctx context.Context) (float64, error)
GetCurrentTemp(ctx context.Context) (float64, error) // 室内温度
// 风速控制
SetFanSpeed(ctx context.Context, speed FanSpeed) error
GetFanSpeed(ctx context.Context) (FanSpeed, error)
// 摆风控制
SetSwing(ctx context.Context, mode SwingMode) error
GetSwing(ctx context.Context) (SwingMode, error)
// 湿度(部分空调支持)
SetTargetHumidity(ctx context.Context, pct int) error
GetTargetHumidity(ctx context.Context) (int, error)
GetCurrentHumidity(ctx context.Context) (int, error)
// 电辅热
SetAuxHeat(ctx context.Context, on bool) error
GetAuxHeat(ctx context.Context) (bool, error)
// 节能
SetEco(ctx context.Context, on bool) error
GetEco(ctx context.Context) (bool, error)
// 订阅(不需要 ctx,订阅是本地注册操作)
OnPropsChanged(handler func(state ACState)) (subID string, err 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
import "context"
// Light 灯光统一控制接口
type Light interface {
Device
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
Toggle(ctx context.Context) error
// 亮度(若返回 -1 表示不支持)
SetBrightness(ctx context.Context, pct int) error // 1-100
GetBrightness(ctx context.Context) (int, error)
// 色温(若返回 -1 表示不支持)
SetColorTemp(ctx context.Context, kelvin int) error // 2700-6500
GetColorTemp(ctx context.Context) (int, 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)
OnPropsChanged(handler func(state LightState)) (subID string, err 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
import "context"
// Switch 开关统一控制接口
type Switch interface {
Device
TurnOn(ctx context.Context) error
TurnOff(ctx context.Context) error
Toggle(ctx context.Context) error
IsOn(ctx context.Context) (bool, error)
OnPropsChanged(handler func(on bool)) (subID string, err error)
}
```
### 5.6 其他设备接口(概要)
```go
import "context"
// Fan 风扇接口
type Fan interface {
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 {
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 {
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 {
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 {
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 {
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 语义解析器
为了让设备控制类能自动找到 `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 组合。
**后果**:
- ✅ 灵活可组合
- ✅ 无额外依赖
- ❌ 性能对比预建索引略差(但对家庭设备量级无影响)
---
### 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)
| 任务 | 文件 | 工作量 |
|------|------|--------|
| 项目骨架搭建 | `xiaomi/go.mod`, module 初始化 | 小 |
| 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` | 中 |
| 设备列表封装(含 FilterByKind) | `xiaomi/devices.go` | 中 |
| 属性读写封装 | `xiaomi/properties.go` | 中 |
| Action 封装 | `xiaomi/actions.go` | 小 |
| 错误定义 | `xiaomi/errors.go` | 小 |
### Phase 2 — 订阅与通知(优先级 P0)
| 任务 | 文件 | 工作量 |
|------|------|--------|
| 订阅通知接口(SubProp/SubEvent/SubDeviceState) | `xiaomi/subscribe.go` | 中 |
| 底层回调 → 强类型事件转换 | `xiaomi/subscribe.go` | 中 |
| subID 映射管理 | `xiaomi/subscribe.go` | 小 |
### Phase 3 — 设备控制抽象(优先级 P1)
| 任务 | 文件 | 工作量 |
|------|------|--------|
| 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/Thermostat | 各文件 | 各小 |
| 通用工厂 NewDevice | `xiaomi/devices/factory.go` | 中 |
### Phase 4 — 测试和文档(优先级 P2)
| 任务 | 文件 | 工作量 |
|------|------|--------|
| 单元测试 | `*_test.go` | 大 |
| 集成示例(更新为 ctx + Device 接口 + factory) | `xiaomi/examples/` | 中 |
| API 文档 | `xiaomi/README.md` | 中 |
---
## 九、使用示例(目标形态)
```go
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(...) ...
// 2. 用上层封装包装
client := xiaomi.NewClient(miotClient)
// 3. 获取用户信息(强类型!)
user, _ := client.GetUserInfo(ctx)
fmt.Printf("用户: %s (UID: %s)\n", user.NickName, user.UID)
// 4. 获取家庭和房间
homes, _ := client.GetHomeList(ctx)
for _, h := range homes {
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(ctx,
xiaomi.FilterByHome(homes[0].ID),
xiaomi.FilterByOnline(),
)
for _, d := range devices_ {
fmt.Printf("设备: %s (%s) 在线:%v 种类:%s\n",
d.Name, d.Model, d.Online, d.DeviceKind())
}
// 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.OnPropsChanged(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. 直接订阅原始属性变化(不用设备抽象)
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)
})
}
```
---
## 十、待讨论问题
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 再做。
---
*本文档为架构规划,尚未执行。*