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:
@@ -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"`,删掉该行。
|
||||
@@ -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 ./...` 编译验证
|
||||
@@ -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`。
|
||||
|
||||
---
|
||||
|
||||
*本文档已全部实施完成。*
|
||||
@@ -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"` 字段,删掉该行。
|
||||
@@ -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 |
|
||||
Reference in New Issue
Block a user