- 新增 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 示例(风扇/窗帘/传感器控制)
306 lines
9.2 KiB
Markdown
306 lines
9.2 KiB
Markdown
# 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):
|
||
```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`**:
|
||
```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 中加方法**:
|
||
```go
|
||
func (d *DeviceInfo) DeviceKind() devices.Kind
|
||
```
|
||
|
||
**client.go 中加过滤器**:
|
||
```go
|
||
func FilterByKind(kind devices.Kind) DeviceFilter
|
||
```
|
||
|
||
---
|
||
|
||
### #4 缺少 `Device` 基础接口
|
||
|
||
**问题**:BaseDevice 目前是 struct 而非 interface。设备控制抽象需要一个通用 `Device` 接口让所有设备类型内嵌,工厂函数才能返回统一类型。
|
||
|
||
**当前代码**(devices/base.go):
|
||
```go
|
||
type BaseDevice struct {
|
||
client *xiaomi.Client
|
||
info *xiaomi.DeviceInfo
|
||
}
|
||
```
|
||
|
||
**修复方案**:在 base.go 中拆分接口和实现:
|
||
|
||
```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:
|
||
```go
|
||
type AirConditioner interface {
|
||
Device // ← 嵌入
|
||
TurnOn() error
|
||
// ...
|
||
}
|
||
type Light interface {
|
||
Device
|
||
TurnOn() error
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**新文件 `factory.go`**(通用工厂):
|
||
```go
|
||
// NewDevice 根据 DeviceInfo 自动创建对应的设备控制接口
|
||
// 返回 Device,调用方用 type switch 分流
|
||
func NewDevice(client *xiaomi.Client, info *xiaomi.DeviceInfo) (Device, error)
|
||
```
|
||
|
||
使用模式:
|
||
```go
|
||
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 结构体加字段:
|
||
```go
|
||
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 保持无状态:
|
||
```go
|
||
type Client struct {
|
||
inner *miot.MIoTClient
|
||
mu sync.RWMutex
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### #7 枚举值与 SPEC 原始值的映射关系未定义
|
||
|
||
**问题**:`ACMode` 值是 `"cool"`、`"heat"` 等字符串,但 SPEC 中 mode 是数字 0-4。SpecResolver 需要做双向值映射,文档未提。
|
||
|
||
**修复方案**:新增 `specs/mapper.go`:
|
||
|
||
```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 中执行。*
|