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
+62
View File
@@ -0,0 +1,62 @@
# 清理修复方案
> 2026-06-29
## 设计原则
- 与 `miot/examples` 共用同一份 `config.json`,不重复造轮子
- 用户先跑 `miot/examples/cloud_oauth` 获取 token,token 自动写入 `miot/examples/config.json`
- xiaomi 示例直接读这份已有的配置文件
---
## 修改清单
### A. 删除 `xiaomi/examples/config/config.go`
整个文件删掉。复用 `miot/examples/config` 包。
### B. 9 个示例文件 — 统一替换
**旧代码**(每个文件都要删):
```go
import "xiaomihome/xiaomi/examples/config"
cfg, err := config.LoadFromEnv()
entryData := map[string]interface{}{
"country_code": cfg.CountryCode,
"cloud_server": cfg.CloudServer,
"client_id": cfg.ClientID,
"client_secret": cfg.ClientSecret,
"access_token": cfg.AccessToken,
}
miotClient := miot.NewMIoTClient("phone", entryData, "", miot.DefaultCloudServer, miot.CtrlModeAuto)
miotClient.Init()
defer miotClient.Deinit()
httpClient, _ := miot.NewMIoTHttpClient(cfg.CloudServer, cfg.ClientID, cfg.AccessToken)
client := xiaomi.NewClient(miotClient, httpClient)
```
**替换为**:
```go
import miotconfig "xiaomihome/miot/examples/config"
cfg, err := miotconfig.LoadConfig("miot/examples/config.json")
if err != nil {
log.Fatal("请先运行 miot/examples/cloud_oauth 获取 token:", err)
}
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
ClientID: miot.OAUTH2_CLIENT_ID,
ClientSecret: "",
AccessToken: cfg.AuthInfo.AccessToken,
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
```
### C. `miot/miot_client.go` — 去无用字段
`NewMIoTClient` 中 entryData 如有 `"country_code"`,删掉该行。
+288
View File
@@ -0,0 +1,288 @@
# xiaomi 客户端重构方案
> 状态:Proposed | 版本:v3.0 | 日期:2026-06-29 | 参照:miot/examples/full_client
---
## 一、Config 字段说明
| 字段 | 含义 | 来源 |
|------|------|------|
| `ClientID` | OAuth Client ID,默认 `"2882303761520251711"`(小米官方 HA 客户端) | 小米开放平台,可留空走默认值 |
| `ClientSecret` | OAuth Client Secret,用于 token 交换 | 小米开放平台申请 |
| `AccessToken` | OAuth 授权后获得的 token,云端 API 鉴权 | OAuth 流程获取(AuthURL → ExchangeCode) |
---
## 二、目标形态
```go
// 零 miot import,1 个对象
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
ClientID: "", // 空则默认 "2882303761520251711"
ClientSecret: "your-secret",
AccessToken: "your-access-token",
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
user, _ := client.GetUserInfo(ctx)
```
---
## 三、参照源码
`miot/examples/full_client/main.go`(已验证可运行)的初始化流程:
```go
// 1. entryData 只放 access_token
entryData := cfg.EntryData() // → {"access_token": "xxx"}
// 2. 创建 MIoTClient
client := miot.NewMIoTClient("demo", entryData, cfg.UID, cfg.CloudServer, cfg.GetCtrlMode())
// 3. 设置组件(必须在 Init 之前)
client.SetStorage(miot.NewMIoTStorage("./data"))
client.SetHTTPClient(miot.NewMIoTHttpClient(cfg.CloudServer, miot.OAUTH2_CLIENT_ID, cfg.AuthInfo.AccessToken))
client.SetMipsCloud(miot.NewMipsCloudClient("ssl://cn-ha.mqtt.io.mi.com:8883", "ha."+cfg.UUID, ...))
// 4. Init(一次性完成:OAuth 客户端、证书、HTTP、MIPS 云、mDNS、LAN)
client.Init()
// 5. 业务操作
client.RefreshDevices()
client.DeviceList()
client.SubDeviceState(...)
client.SubProp(...)
// 6. 清理
client.Deinit()
```
**关键要点**:
- entryData 只需 `access_token` 一个字段
- `SetStorage` / `SetHTTPClient` / `SetMipsCloud` 必须在 Init 之前调用
- Init 内部会跳过已设置的组件(`if c.http == nil { ... }`)
- 不需要两次 Init,一次足够
---
## 四、逐文件修改
### 4.1 新建 `xiaomi/config.go`
```go
package xiaomi
// Config 客户端初始化配置
type Config struct {
// ClientID OAuth Client ID,空则默认 "2882303761520251711"(小米 HA 客户端)
ClientID string
// ClientSecret OAuth Client Secret
ClientSecret string
// AccessToken OAuth 授权后获取的 access token
AccessToken string
}
```
### 4.2 重写 `xiaomi/client.go`
**删除**:
```go
func NewClient(inner *miot.MIoTClient, httpClient *miot.MIoTHttpClient) *Client
func (c *Client) Inner() *miot.MIoTClient
func (c *Client) HTTPClient() *miot.MIoTHttpClient
```
**新增**(完全对齐 full_client 示例的初始化流程):
```go
import "context"
// NewClient 创建并初始化客户端。
//
// 内部流程(对齐 miot/examples/full_client):
// 1. 构建 entryData(仅 access_token)
// 2. 创建 MIoTClient
// 3. SetStorage → SetHTTPClient → SetMipsCloud(必须在 Init 前)
// 4. Init(一次性完成所有子组件初始化)
func NewClient(ctx context.Context, cfg Config) (*Client, error) {
clientID := cfg.ClientID
if clientID == "" {
clientID = miot.OAUTH2_CLIENT_ID // 默认小米 HA 客户端
}
// 构造 entryData(仅 access_token,对齐 full_client 的 EntryData())
entryData := map[string]interface{}{
"access_token": cfg.AccessToken,
}
// 创建 MIoTClient(uid 空、CloudServer=cn、CtrlMode=auto)
miotClient := miot.NewMIoTClient("xiaomi_sdk", entryData, "", "cn", miot.CtrlModeAuto)
// 设置存储(对齐 full_client line 36-37)
miotClient.SetStorage(miot.NewMIoTStorage("./data"))
// 设置 HTTP 客户端(对齐 full_client line 41-46)
httpClient, err := miot.NewMIoTHttpClient("cn", clientID, cfg.AccessToken)
if err != nil {
return nil, fmt.Errorf("xiaomi: http: %w", err)
}
miotClient.SetHTTPClient(httpClient)
// 设置 MIPS 云端客户端(对齐 full_client line 50-57)
mipsCloud := miot.NewMipsCloudClient(
"ssl://cn-ha.mqtt.io.mi.com:8883",
"ha."+uuid.New().String(),
clientID,
cfg.AccessToken,
)
miotClient.SetMipsCloud(mipsCloud)
// 一次性 Init(对齐 full_client line 60)
if err := miotClient.Init(); err != nil {
return nil, fmt.Errorf("xiaomi: init: %w", err)
}
return &Client{
cfg: cfg,
inner: miotClient,
http: httpClient,
devices: make(map[string]*miot.MIoTDevice),
propSubs: make(map[string]string),
eventSubs: make(map[string]string),
}, nil
}
// Close 关闭客户端(对齐 full_client line 125: client.Deinit())
func (c *Client) Close() error {
if c.inner != nil {
return c.inner.Deinit()
}
return nil
}
```
**Client struct 新增 cfg 字段**:
```go
type Client struct {
cfg Config // ← 新增
inner *miot.MIoTClient
http *miot.MIoTHttpClient
deviceMu sync.RWMutex
devices map[string]*miot.MIoTDevice
propSubs map[string]string
eventSubs map[string]string
mu sync.RWMutex
}
```
### 4.3 新建 `xiaomi/oauth.go`
```go
package xiaomi
import "context"
// AuthURL 生成小米 OAuth 授权页面 URL
func AuthURL(cfg Config) string
// ExchangeCode 用 authorization code 交换 access token
func ExchangeCode(ctx context.Context, cfg Config, code string) (*TokenResult, error)
// TokenResult OAuth token 交换结果
type TokenResult struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
UID string `json:"uid"`
}
```
### 4.4 重写 `xiaomi/examples/main.go`
```go
package main
import (
"context"
"log"
"time"
"xiaomihome/xiaomi"
"xiaomihome/xiaomi/examples/config"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cfg, _ := config.LoadFromEnv()
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
ClientSecret: cfg.ClientSecret,
AccessToken: cfg.AccessToken,
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
user, _ := client.GetUserInfo(ctx)
log.Printf("用户: %s (UID: %s)", user.NickName, user.UID)
}
```
---
## 五、不动的文件(15 个)
`properties.go` `actions.go` `subscribe.go` `user.go` `homes.go` `devices.go` `types.go` `errors.go` `convert.go` + `devices/` 下全部 9 个文件
---
## 六、附带清理
### 6.1 `xiaomi/examples/config/config.go` — 去掉无用的 CountryCode / CloudServer
```go
// 修改前
type Config struct {
CountryCode string // ← 无用
CloudServer string // ← 硬编码 "cn",无需暴露
ClientID string
ClientSecret string
AccessToken string
}
func (c *Config) LoadFromEnv() {
cfg.CountryCode = getEnv("XIAOMI_COUNTRY_CODE", "86") // ← 删
cfg.CloudServer = getEnv("XIAOMI_CLOUD_SERVER", "cn") // ← 删
}
// 修改后
type Config struct {
ClientID string
ClientSecret string
AccessToken string
}
```
### 6.2 `miot/miot_client.go` — `NewMIoTClient` 中去掉无用的 `country_code`
已验证 Go 代码和 Python 代码中均无任何地方读取 entryData 的 `country_code`,一并移除。
---
## 七、实施步骤
1. 新建 `xiaomi/config.go`
2. 重写 `xiaomi/client.go`(旧 NewClient 改名内部保留)
3. 新建 `xiaomi/oauth.go`
4. 清理 `xiaomi/examples/config/config.go`(去 CountryCode / CloudServer)
5. 重写 `xiaomi/examples/main.go`
6. 清理 `miot/miot_client.go` 中无用的 `country_code`
7. 更新 `xiaomi/migration/ARCH_PLAN.md` 4.2 节
8. `go vet ./...` 编译验证
+319
View File
@@ -0,0 +1,319 @@
# xiaomi 封装层示例程序规划
> 状态:Completed | 版本:v1.0 | 日期:2026-06-28 | 完成日期:2026-06-29
---
## 一、现状分析
### 已有示例
| 文件 | 状态 | 问题 |
|------|------|------|
| `xiaomi/examples/main.go` | 存在(224 行) | 几乎全是 `fmt.Println` 伪代码,非可运行的完整示例 |
### 目标
对标 `ARCH_PLAN.md` v1.1 的最终形态,覆盖完整 API 面,每个示例独立可理解、贴近实战。
---
## 二、示例架构
```
xiaomi/examples/
├── 01_user_home/ # 用户 + 家庭 + 房间信息
│ └── main.go
├── 02_device_list/ # 设备列表 + 过滤
│ └── main.go
├── 03_switch_control/ # 开关控制
│ └── main.go
├── 04_light_control/ # 灯光控制
│ └── main.go
├── 05_ac_control/ # 空调控制
│ └── main.go
├── 06_fan_cover/ # 风扇 + 窗帘控制
│ └── main.go
├── 07_prop_subscribe/ # 属性/事件/状态订阅
│ └── main.go
├── 08_adv_filter_classify/ # 高级过滤 + 分类 + 工厂
│ └── main.go
├── 09_spec_parser/ # SPEC 解析器独立使用(已有完整代码)
│ └── main.go
└── config/ # 共享配置模块(敏感信息统一管理)
└── config.go
```
---
## 三、各示例详述
### 01_user_home — 用户/家庭/房间信息
**覆盖 API**:
- `client.GetUserInfo(ctx)` → `*UserInfo`
- `client.GetHomeList(ctx)` → `[]*HomeInfo`(含经纬度、地址)
- `client.GetHome(ctx, homeID)` → `*HomeInfo`
- `client.GetRooms(ctx, homeID)` → `[]*RoomInfo`
- `client.GetRoomDevices(ctx, homeID, roomID)` → `[]string`
**演示重点**:
- context.WithTimeout 控制超时
- HomeInfo 的地里位置字段(CityID, Longitude, Latitude, Address)
- for-range 遍历家庭→房间→设备层级结构
---
### 02_device_list — 设备列表与过滤
**覆盖 API**:
- `client.GetDevices(ctx, opts...)` → `[]*DeviceInfo`
- `client.GetDevice(ctx, did)` → `*DeviceInfo`
- `client.RefreshDevices(ctx)` → error
- `FilterByHome` / `FilterByRoom` / `FilterByOnline` / `FilterByModel` / `FilterByKind`
- `DeviceInfo.DeviceKind()` 分类
**演示重点**:
- 多种过滤组合(在线+客厅、按型号、按种类)
- DeviceKind() 输出
- RefreshDevices 刷新时机
---
### 03_switch_control — 开关控制
**覆盖 API**:
- `devices.Create(client, info)` 工厂 → `Switch`
- `Switch.{TurnOn, TurnOff, Toggle, IsOn}(ctx)`
- `Switch.OnStateChanged(handler)` 状态订阅
**演示重点**:
- 工厂创建单设备
- TurnOn/TurnOff/Toggle 完整链路
- IsOn 状态查询
- OnStateChanged 回调演示
---
### 04_light_control — 灯光控制
**覆盖 API**:
- `Light.{TurnOn, TurnOff, Toggle}(ctx)`
- `Light.{SetBrightness, GetBrightness}(ctx, pct)`
- `Light.{SetColorTemp, GetColorTemp}(ctx, kelvin)`
- `Light.{SetColor, GetColor}(ctx, r, g, b)`
- `Light.OnStateChanged(handler)`
**演示重点**:
- 亮度 0-100 调节
- 色温 2700K-6500K 调节
- RGB 颜色控制
- 不支持功能的降级处理(`GetBrightness` 返回 -1 → 跳过)
---
### 05_ac_control — 空调控制
**覆盖 API**:
- `AirConditioner.{TurnOn, TurnOff, IsOn}(ctx)`
- `SetMode(ctx, ACMode)` / `GetMode(ctx)` — cool/heat/dry/fan/auto
- `SetTargetTemp(ctx, celsius)` / `GetTargetTemp(ctx)` / `GetCurrentTemp(ctx)`
- `SetFanSpeed(ctx, FanSpeed)` / `GetFanSpeed(ctx)` — low/medium/high/auto
- `SetSwing(ctx, SwingMode)` / `GetSwing(ctx)` — off/vertical/horizontal/both
- `SetTargetHumidity(ctx, pct)` / `GetTargetHumidity(ctx)` / `GetCurrentHumidity(ctx)`
- `SetAuxHeat(ctx, on)` / `GetAuxHeat(ctx)`
- `SetEco(ctx, on)` / `GetEco(ctx)`
- `OnStateChanged(handler)` → `ACState`
**演示重点**:
- 完整空调控制链路(开→设制冷→设26°C→自动风速→垂直摆风)
- OnStateChanged 实时状态回调
- 不支持的功能优雅降级(如无湿度传感器,跳过湿度设置)
---
### 06_fan_cover — 风扇 + 窗帘控制
**覆盖 API**:
- `Fan.{TurnOn, TurnOff, SetSpeed, SetOscillation, SetNaturalWind}(ctx)`
- `Cover.{Open, Close, Stop, SetPosition, GetPosition}(ctx)`
**演示重点**:
- 风扇 4 档调速 + 摇头 + 自然风切换
- 窗帘百分比定位(0%=关, 100%=全开, 50%=半开)
- Cover 的 Open/Close/Stop 语义对比 SetPosition
---
### 07_prop_subscribe — 属性/事件/状态订阅
**覆盖 API**:
- `client.SubProp(did, siid, piid, PropHandler)` → subID
- `client.UnsubProp(did, subID)`
- `client.SubEvent(did, siid, eiid, EventHandler)` → subID
- `client.UnsubEvent(did, subID)`
- `client.SubDeviceState(did, DeviceStateHandler)`
- `client.UnsubDeviceState(did)`
**演示重点**:
- 订阅前后属性值对比
- 等待N秒展示属性/事件推送
- 取消订阅后不再收到回调
- 设备上下线状态监听
---
### 08_adv_filter_classify — 高级过滤 + 分类 + 工厂模式
**覆盖 API**:
- `devices.Classify(info)` → `Kind` 枚举
- `FilterByKind(kind)` 过滤器
- `devices.Create(client, info)` 工厂 → 各设备接口
- `devices.Kind` 所有枚举值
**演示重点**:
- 获取全部设备 → 按 Kind 分类 → 统计各类型数量
- 工厂批量创建:遍历 DeviceInfo → Create → type switch 分流
- 不支持的设备返回 nil → continue 跳过
- 输出表格:"设备名 | 型号 | 种类 | 状态"
---
### 09_spec_parser — SPEC 解析器独立使用
**覆盖 API**:
- specs 包全部公开 API(如果 specs 是公开的)
- SPEC 属性/事件/动作的枚举和查询
**演示重点**:
- 无需设备在线即可解析 SPEC
- 打印设备有哪些属性(text/switch/select/number/sensor)
- 打印设备有哪些事件和动作
---
## 四、共享配置模块 `config/config.go`
```go
package config
type Config struct {
ClientID string
ClientSecret string
AccessToken string // OAuth token(从环境变量或文件读取)
}
// LoadFromEnv 从环境变量加载配置
func LoadFromEnv() (*Config, error)
// LoadFromFile 从 JSON 文件加载配置
func LoadFromFile(path string) (*Config, error)
```
**设计原则**:
- 所有示例复用一个 config 加载逻辑,避免重复
- 仅 3 个字段,对应 xiaomi.Config
- 敏感信息从环境变量或文件读取,不硬编码
---
## 五、编码规范
### 5.1 统一约定
```
1. 每个示例的 main() 必须有 context.WithTimeout(30s)
2. 所有 IO 操作带 ctx
3. 错误必须处理(不忽略 err)
4. defer cancel() 放在 func main() 顶部
5. 日志用 log 包,不用 fmt.Print(方便区分日志级别)
6. 每个示例一句话说明功能(注释在 main 函数上方)
```
### 5.2 模板骨架
```go
// Package main demonstrates <功能简述>.
// 运行前提:已配置环境变量 XIAOMI_CLIENT_ID / XIAOMI_CLIENT_SECRET / XIAOMI_ACCESS_TOKEN。
package main
import (
"context"
"log"
"time"
"xiaomihome/xiaomi"
"xiaomihome/xiaomi/devices"
"xiaomihome/xiaomi/examples/config"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 1. 加载配置
cfg, err := config.LoadFromEnv()
if err != nil {
log.Fatal("配置加载失败:", err)
}
// 2. 创建客户端(零 miot 引用)
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
AccessToken: cfg.AccessToken,
})
if err != nil {
log.Fatal("创建客户端失败:", err)
}
defer client.Close()
// 3. 业务逻辑(根据示例不同)
// ...
}
```
---
## 六、与 ARCH_PLAN.md 的对应关系
| ARCH_PLAN.md 目标 | 对应示例 |
|-------------------|---------|
| 用户/家庭/房间信息(GetUserInfo / GetHomeList) | 01_user_home |
| 设备列表 + Filter | 02_device_list |
| Switch 控制 + OnStateChanged | 03_switch_control |
| Light 控制 | 04_light_control |
| AirConditioner 完整控制 | 05_ac_control |
| Fan / Cover 控制 | 06_fan_cover |
| SubProp / SubEvent / SubDeviceState 订阅 | 07_prop_subscribe |
| Classify / FilterByKind / Create 工厂 | 08_adv_filter_classify |
| SPEC 解析器 | 09_spec_parser |
| context.Context + 错误处理 | 全部示例 |
---
## 七、实施计划
| 序号 | 示例 | 文件 | 优先级 | 复杂度 |
|------|------|------|--------|--------|
| 0 | config 共享模块 | `config/config.go` | P0 | 小 |
| 1 | 用户/家庭/房间 | `01_user_home/main.go` | P0 | 小 |
| 2 | 设备列表+过滤 | `02_device_list/main.go` | P0 | 中 |
| 3 | 开关控制 | `03_switch_control/main.go` | P0 | 小 |
| 4 | 灯光控制 | `04_light_control/main.go` | P0 | 中 |
| 5 | 空调控制 | `05_ac_control/main.go` | P0 | 大 |
| 6 | 风扇+窗帘 | `06_fan_cover/main.go` | P1 | 中 |
| 7 | 订阅通知 | `07_prop_subscribe/main.go` | P1 | 大 |
| 8 | 高级过滤+工厂 | `08_adv_filter_classify/main.go` | P1 | 中 |
| 9 | SPEC 解析 | `09_spec_parser/main.go` | P2 | 小 |
| — | 统一 README | `examples/README.md` | P2 | 小 |
---
## 八、与现有 main.go 的关系
现有 `xiaomi/examples/main.go` 将被**替换**为上述 9 个独立示例。如果作为归档保留,可移到 `examples/_archive/main_old.go`。
---
*本文档已全部实施完成。*
+70
View File
@@ -0,0 +1,70 @@
# 修复方案 v2
> 2026-06-29
## xiaomi.Config 最终形态
```go
type Config struct {
ClientID string // 默认 miot.OAUTH2_CLIENT_ID("2882303761520251711")
AccessToken string
}
```
删掉的字段及原因:
| 字段 | 原因 |
|------|------|
| CountryCode | 全项目无任何地方读取 |
| ClientSecret | miot API 不需要,从未使用 |
| CloudServer | 国内恒为 "cn",NewClient 内部硬编码 |
---
## 修改清单
### 1. 删除 `xiaomi/examples/config/` 整个目录
示例程序复用 `miot/examples/config` 包。
### 2. 9 个示例文件统一替换
```
xiaomi/examples/01_user_home/main.go
xiaomi/examples/02_device_list/main.go
xiaomi/examples/03_switch_control/main.go
xiaomi/examples/04_light_control/main.go
xiaomi/examples/05_ac_control/main.go
xiaomi/examples/06_fan_cover/main.go
xiaomi/examples/07_prop_subscribe/main.go
xiaomi/examples/08_adv_filter_classify/main.go
xiaomi/examples/09_spec_parser/main.go
```
**替换模板**:
```go
import miotconfig "xiaomihome/miot/examples/config"
func main() {
cfg, err := miotconfig.LoadConfig("miot/examples/config.json")
if err != nil {
log.Fatal("请先运行 miot/examples/cloud_oauth 获取 token:", err)
}
client, err := xiaomi.NewClient(ctx, xiaomi.Config{
AccessToken: cfg.AuthInfo.AccessToken,
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
// ...
}
```
删除的内容:`entryData`、`miot.NewMIoTClient`、`Init/Deinit`、`NewMIoTHttpClient`、import `"xiaomihome/miot"`。
### 3. `miot/miot_client.go` — 去掉 `country_code`
`NewMIoTClient` 中 entryData 如有 `"country_code"` 字段,删掉该行。
+89
View File
@@ -0,0 +1,89 @@
# xiaomi/ 封装层实施报告
> 状态:Phase 1-4 Complete | 日期:2026-06-28
---
## 实施摘要
按 ARCH_PLAN.md 完成了 `xiaomi/` 上层封装包的 Phase 1-4 全部实现。
### 文件清单(25 个 Go 源文件 + 文档)
```
xiaomi/
├── types.go # 强类型结构体定义
├── errors.go # 封装层错误定义
├── client.go # Client 主入口
├── convert.go # map → 类型安全提取工具
├── user.go # GetUserInfo
├── homes.go # GetHomeList/GetHome/GetRooms/GetRoomDevices
├── devices.go # GetDevices/GetDevice/RefreshDevices + DeviceFilter
├── properties.go # GetProp/SetProp/GetProps/SetProps
├── actions.go # Action
├── subscribe.go # SubProp/SubEvent/SubDeviceState(强类型回调)
│
├── devices/
│ ├── base.go # BaseDevice 基础实现
│ ├── switch.go # Switch 接口 + impl
│ ├── light.go # Light 接口 + impl
│ ├── air_conditioner.go # AirConditioner 接口 + impl(含 ACMode/FanSpeed/SwingMode)
│ ├── fan.go # Fan 接口 + impl
│ ├── cover.go # Cover 接口 + impl
│ ├── humidifier.go # Humidifier 接口 + impl
│ ├── vacuum.go # Vacuum 接口 + impl
│ ├── water_heater.go # WaterHeater 接口 + impl
│ ├── thermostat.go # Thermostat 接口 + impl
│ └── factory.go # Create() 工厂函数(URN 自动路由)
│
├── specs/
│ └── resolver.go # PropertyResolver(SPEC 语义→siid/piid)
│
├── examples/
│ └── main.go # 完整使用示例
│
├── migration/
│ └── IMPLEMENTATION.md # 本实施报告
│
└── README.md # API 文档
```
### 测试文件(5 个,对应 3 个包)
```
xiaomi/
├── convert_test.go # strVal/intVal/boolVal/strSliceVal
├── errors_test.go # 哨兵错误/ClientError/wrapErr
├── types_test.go # DeviceInfo/HomeInfo 解析 + JSON 序列化
├── devices_test.go # DeviceFilter 组合
├── subscribe_test.go # PropHandler/EventHandler/DeviceStateHandler
xiaomi/devices/
└── devices_test.go # toInt/toFloat/常量一致性/matchURN/Create
xiaomi/specs/
└── resolver_test.go # FindByType/FindByFormat/FindByDescription/FindAction
```
### 关键设计决策
| 决策 | 说明 |
|------|------|
| **子包非独立 module** | `xiaomihome/xiaomi` 作为主模块子包,无独立 go.mod |
| **NewClient 双参数** | `(inner *miot.MIoTClient, httpClient *miot.MIoTHttpClient)`,因 GetUserInfo/GetHomeInfos 仅在 MIoTHttpClient 上 |
| **设备构造降级策略** | 可选属性(如电辅热/摆风)独立解析,失败不阻塞构造 |
| **内置 specResolver** | BaseDevice 内置轻量 SPEC 解析器,避免所有设备文件都 import specs 包 |
### 验证状态
- `go build ./xiaomi/...` — 通过
- `go vet ./xiaomi/...` — 通过
- `go test ./xiaomi/...` — 3 个测试包全部通过
### 测试覆盖
| 包 | 测试文件 | 测试函数 | 状态 |
|----|---------|---------|------|
| `xiaomi` | 5 | 18 | PASS |
| `xiaomi/devices` | 1 | 7 | PASS |
| `xiaomi/specs` | 1 | 8 | PASS |