Files
xiaomihome/miot/migration/ARCH_PLAN.md
T
4566704 a3d94c4b9f 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 示例(风扇/窗帘/传感器控制)
2026-06-29 08:51:21 +08:00

37 KiB
Raw Blame History

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 — 强类型结构体

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 — 主入口

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 属性读写封装

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)
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

实现要点:

// 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):

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 实现

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 空调控制接口

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,订阅是本地注册操作)
    OnStateChanged(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 灯光控制接口

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)

    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"`
    ColorRGB   [3]int `json:"color_rgb"`
}

5.5 开关控制接口

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)

    OnStateChanged(handler func(on bool)) (subID string, err error)
}

5.6 其他设备接口(概要)

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 设备分类与工厂

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)

工厂使用模式:

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)

示例:空调模式映射

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,需要一个规约解析器:

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)

解析策略

每种设备控制接口的构造流程:

// 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 中

九、使用示例(目标形态)

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.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. 直接订阅原始属性变化(不用设备抽象)
    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 再做。


本文档为架构规划,尚未执行。