# 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 再做。 --- *本文档为架构规划,尚未执行。*