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 示例(风扇/窗帘/传感器控制)
This commit is contained in:
2026-06-29 08:51:21 +08:00
parent 35efed7d15
commit a3d94c4b9f
93 changed files with 7582 additions and 157 deletions
+305
View File
@@ -0,0 +1,305 @@
# 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 中执行。*