Files
xiaomihome/miot/migration/ARCH_PLAN_REVIEW.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

9.2 KiB
Raw Blame History

ARCH_PLAN.md 审查报告

审查时间:2026-06-28 | 目标版本:v1.0 → v1.1


审查结果总览

共发现 11 个问题,分级如下:

等级 数量 说明
P0 阻塞 4 必须修复才能进入实施
P1 重要 3 影响完整性和一致性
P2 优化 4 锦上添花

P0 — 阻塞项

#1 缺少 context.Context 支持

问题:所有涉及 IO 的方法(GetUserInfo、GetHomeList、GetDevices、SetProp、Action 等)都没有 context.Context 参数。Go 惯例要求 IO 操作可超时和取消。

影响范围:

  • client.go:GetUserInfo、GetHomeList、GetHome、GetRooms、GetRoomDevices、GetDevices、GetDevice、RefreshDevices
  • properties.go:GetProp、SetProp、GetProps、SetProps
  • actions.go:Action
  • devices/base.go:GetProp、SetProp、Action
  • devices/air_conditioner.go:TurnOn、TurnOff、IsOn、SetMode、GetMode、SetTargetTemp、GetTargetTemp、GetCurrentTemp 等全部 IO 方法
  • devices/light.go、switch.go、fan.go、cover.go 等:同上

不修改的方法(纯本地操作):

  • SubProp / UnsubProp / SubEvent / UnsubEvent / SubDeviceState / UnsubDeviceState
  • OnStateChanged
  • NewClient / NewDevice 等工厂方法

#2 PropertyValue.Type 字段在回调场景下不可用

问题:PropertyValue 定义了 Type string 字段标注"SPEC format"。但 SubProp 回调中 miot 传入的 params 只有 {did, siid, piid, value},没有 type 信息。该字段只在主动 GetProp 时才有可能从 SPEC 查到。

当前代码(types.go):

type PropertyValue struct {
    DID   string      `json:"did"`
    SIID  int         `json:"siid"`
    PIID  int         `json:"piid"`
    Code  int         `json:"code"`
    Value interface{} `json:"value"`
    Type  string      `json:"type"`   // SPEC format: "bool"/"uint8"/"float"/"string"
}

修复方案:改为注释说明只在 GetProp 场景可用。


#3 缺少通用的设备类型判断/分类机制

问题:用户拿到 []*DeviceInfo 后无法区分哪台是空调、哪台是灯。只能盲目试 devices.NewAirConditioner(...) 看返回 error。

修复方案:在 xiaomi/devices/ 新增分类体系:

新文件 kind.go:

package devices

// Kind 设备大类
type Kind string
const (
    KindAirConditioner Kind = "air_conditioner"
    KindLight          Kind = "light"
    KindSwitch         Kind = "switch"
    KindFan            Kind = "fan"
    KindCover          Kind = "cover"
    KindHumidifier     Kind = "humidifier"
    KindVacuum         Kind = "vacuum"
    KindWaterHeater    Kind = "water_heater"
    KindThermostat     Kind = "thermostat"
    KindUnknown        Kind = "unknown"
)

// Classify 从 DeviceInfo 快速判断设备种类(基于 URN/Model 前缀匹配,不依赖 SPEC 加载)
func Classify(info *xiaomi.DeviceInfo) Kind

types.go 中加方法:

func (d *DeviceInfo) DeviceKind() devices.Kind

client.go 中加过滤器:

func FilterByKind(kind devices.Kind) DeviceFilter

#4 缺少 Device 基础接口

问题:BaseDevice 目前是 struct 而非 interface。设备控制抽象需要一个通用 Device 接口让所有设备类型内嵌,工厂函数才能返回统一类型。

当前代码(devices/base.go):

type BaseDevice struct {
    client *xiaomi.Client
    info   *xiaomi.DeviceInfo
}

修复方案:在 base.go 中拆分接口和实现:

// Device 所有设备控制接口的公共基础
type Device interface {
    DID() string
    Name() string
    Model() string
    Online() bool
    Info() *xiaomi.DeviceInfo
}

// BaseDevice 公共实现(嵌入到具体类型中)
type BaseDevice struct {
    client *xiaomi.Client
    info   *xiaomi.DeviceInfo
}
// 实现 Device 接口的所有方法...

各设备接口改为内嵌 Device:

type AirConditioner interface {
    Device  // ← 嵌入
    TurnOn() error
    // ...
}
type Light interface {
    Device
    TurnOn() error
    // ...
}

新文件 factory.go(通用工厂):

// NewDevice 根据 DeviceInfo 自动创建对应的设备控制接口
// 返回 Device,调用方用 type switch 分流
func NewDevice(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Device, error)

使用模式:

dev, err := devices.NewDevice(client, info)
switch d := dev.(type) {
case devices.AirConditioner:
    d.TurnOn(ctx)
case devices.Light:
    d.SetBrightness(ctx, 80)
}

P1 — 重要项

#5 HomeInfo 缺少地理位置字段

问题:Python 版 get_homeinfos_async 返回的 home 包含 city_id、longitude、latitude、address,对位置相关自动化有用。

修复方案:HomeInfo 结构体加字段:

type HomeInfo struct {
    // ... 现有字段 ...
    CityID    string  `json:"city_id"`
    Longitude float64 `json:"longitude"`
    Latitude  float64 `json:"latitude"`
    Address   string  `json:"address"`
}

#6 Client 的 homes 缓存语义不清

问题:Client struct 定义了 homes map[string]*HomeInfo 暗示缓存,但 GetHomeList 每次都调 HTTP API。缓存何时加载、何时失效均未说明。

修复方案:去掉 homes 缓存字段,Client 保持无状态:

type Client struct {
    inner *miot.MIoTClient
    mu    sync.RWMutex
}

#7 枚举值与 SPEC 原始值的映射关系未定义

问题:ACMode 值是 "cool"、"heat" 等字符串,但 SPEC 中 mode 是数字 0-4。SpecResolver 需要做双向值映射,文档未提。

修复方案:新增 specs/mapper.go:

package specs

// PropertyMapper SPEC 原始值 ↔ 枚举字符串 双向映射
type PropertyMapper struct {
    rawToEnum map[interface{}]string
    enumToRaw map[string]interface{}
}

// NewPropertyMapper 从 SPEC value-list + 外部映射表 构造
func NewPropertyMapper(vlist []miot.MIoTSpecValueItem, valueMap map[interface{}]string) *PropertyMapper

func (m *PropertyMapper) ToEnum(raw interface{}) (string, bool)
func (m *PropertyMapper) ToRaw(enum string) (interface{}, bool)

P2 — 优化项

#8 DeviceInfo 无直接可读的设备类型

解决:已通过 P0 #3 的 DeviceKind() 方法和 Classify() 函数解决。


#9 各设备接口 OnStateChanged 返回值不统一

问题:OnStateChanged 返回 (string, error),但 SubProp/SubEvent 返回 (string, error) — 这里它们是统一的。但缺少对 subID 语义的说明。

修复方案:注释统一说明 subID 是订阅标识符,可用于取消。


#10 例中 ACMode 枚举值和 SPEC 原始值需对应

解决:已通过 P1 #7 的 PropertyMapper 解决。


#11 使用示例未体现上下文控制和工厂模式

修复方案:使用示例改为展示 context.WithTimeout、devices.NewDevice 通用工厂、type switch 分流模式。


修复后需新增的 ADR

ADR-006:所有 IO 方法携带 context.Context

  • 决定:IO 方法首参加 ctx;订阅/工厂等本地方法不加
  • 代价:方法签名变长

ADR-007:Device 基础接口 + Kind 分类体系

  • 决定:Device 接口为所有设备接口的公共基;Classify 基于 URN 字符串匹配快速分类;NewDevice 为通用工厂
  • 代价:Classify 依赖字符串匹配,非常见型号可能误判,匹配表需可扩展

修复实施清单

序号 等级 修改文件 内容
1 P0 client.go, properties.go, actions.go, devices/*.go 所有 IO 方法加 ctx context.Context 首参
2 P0 types.go PropertyValue.Type 注释改为"仅 GetProp 场景可用"
3 P0 devices/kind.go(新) Kind 枚举 + Classify 函数
3 P0 types.go DeviceInfo.DeviceKind() 方法
3 P0 client.go 新增 FilterByKind
4 P0 devices/base.go 拆分 Device 接口 + BaseDevice 实现
4 P0 devices/factory.go(新) NewDevice 通用工厂
4 P0 devices/*.go 各接口内嵌 Device
5 P1 types.go HomeInfo 加 CityID/Longitude/Latitude/Address
6 P1 client.go Client 去掉 homes 缓存字段
7 P1 specs/mapper.go(新) PropertyMapper 双向值映射
9 P2 devices/*.go OnStateChanged 统一命名返回值 subID
11 P2 ARCH_PLAN.md 使用示例 更新为 ctx + factory + type switch 模式

修复后的包结构变化

xiaomi/
├── devices/
│   ├── base.go      # 修改:拆分 Device 接口 + BaseDevice 实现
│   ├── kind.go      # 新增:Kind 枚举 + Classify 分类
│   ├── factory.go   # 新增:NewDevice 通用工厂
│   └── ...          # 修改:各接口内嵌 Device,IO 方法加 ctx
├── specs/
│   ├── resolver.go  # 不变
│   └── mapper.go    # 新增:PropertyMapper 值映射
├── client.go        # 修改:加 ctx、去 homes 缓存、加 FilterByKind
├── types.go         # 修改:HomeInfo 加字段、PropertyValue.Type 注释、DeviceKind 方法
├── properties.go    # 修改:加 ctx
├── actions.go       # 修改:加 ctx
└── ...              # 其他 IO 方法加 ctx

此报告为独立审查文档,详细修复已在 ARCH_PLAN.md 中执行。