From 35efed7d154c5e292ae175a6435d6e93e288ff31 Mon Sep 17 00:00:00 2001 From: 4566704 <4566704@qq.com> Date: Sun, 28 Jun 2026 23:21:18 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E4=BF=AE=E5=A4=8D=20import=20?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E4=B8=BA=20xiaomihome/miot=EF=BC=8C=E8=A1=A5?= =?UTF-8?q?=E5=85=85=20README=20=E9=A1=B9=E7=9B=AE=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit go.mod 模块名为 xiaomihome,将所有内部 import 从 "miot" 更新为 "xiaomihome/miot",涉及所有测试文件和示例文件。新增 README.md 项目 介绍文档(架构概览、模块说明、移植进度等)。新增 ARCH_PLAN.md 架构 设计文档。 --- README.md | 271 ++++++++ miot/common_test.go | 4 +- miot/const_test.go | 2 +- miot/examples/cloud_devices/main.go | 4 +- miot/examples/cloud_oauth/main.go | 4 +- miot/examples/config/config.go | 10 +- miot/examples/full_client/main.go | 4 +- miot/examples/lan_control/main.go | 2 +- miot/examples/mdns_discovery/main.go | 2 +- miot/examples/mips_subscribe/main.go | 4 +- miot/examples/network_monitor/main.go | 2 +- miot/examples/spec_parser/main.go | 2 +- miot/examples/storage_manage/main.go | 4 +- miot/migration/ARCH_PLAN.md | 912 ++++++++++++++++++++++++++ miot/miot_device_test.go | 106 +-- miot/miot_error_test.go | 2 +- miot/miot_i18n_test.go | 2 +- miot/miot_matcher_test.go | 2 +- miot/miot_storage_test.go | 2 +- miot/spec_parser_test.go | 2 +- miot/web_pages_test.go | 2 +- 21 files changed, 1264 insertions(+), 81 deletions(-) create mode 100644 miot/migration/ARCH_PLAN.md diff --git a/README.md b/README.md index e69de29..1ba9c88 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,271 @@ +# go-xiaomihome 项目介绍 + +## 项目概述 + +**go-xiaomihome** 是小米智能家居 Home Assistant 集成(`ha_xiaomi_home`)的 **Golang 移植版**。 + +将原 Python 版核心 `miot` 包完整移植为 Go 版本,保持 API 语义与行为严格对齐,目标是提供一个高性能、可独立使用的小米 IoT(MIoT)核心 SDK。 + +> Python 原版:`ha_xiaomi_home/custom_components/xiaomi_home/miot/` +> Go 移植版:`go-xiaomihome/miot/` + +--- + +## 核心目标 + +- **严格对齐**:Go 版 `miot` 包与 Python 版功能严格对齐,行为一致 +- **独立可用**:移植后的 `miot` 包可作为独立 SDK 被任何 Go 项目引用 +- **高性能**:利用 Go 的并发优势,提升设备发现、消息订阅等场景的性能 +- **易维护**:保留 Python 版的文件/模块结构,对应关系清晰 + +--- + +## 架构概览 + +``` +go-xiaomihome/ +├── go.mod # Go 模块定义 (module: xiaomihome) +├── go.sum +├── README.md # (待完善) +└── miot/ # 核心 MIoT SDK (与 Python miot/ 严格对齐) + ├── miot_client.go # 主客户端 (设备管理和协调核心) + ├── miot_client_api.go # 外部 API 接口 + ├── miot_client_device.go # 设备管理方法 + ├── miot_client_prop.go # 属性刷新管理 + ├── miot_client_sub.go # 订阅管理 (MQTT/mDNS/LAN) + ├── miot_cloud.go # 云服务 HTTP API 客户端 + OAuth + ├── miot_device.go # 设备模型 (MIoTDevice) + ├── miot_error.go # 错误码定义 + ├── miot_storage.go # 本地存储 (cert/dict/device data) + ├── miot_lan.go # LAN 局域网控制 (UDP 9588) + ├── miot_mdns.go # mDNS 服务发现 (MIPS) + ├── miot_network.go # 网络状态监控 + ├── mips_client.go # MIPS MQTT 客户端 (云端+本地) + ├── spec_parser.go # MIoT SPEC 解析器 (设备能力描述) + ├── miot_i18n.go # 国际化 (i18n/) + ├── miot_matcher.go # 订阅匹配器 (Matcher) + ├── common.go # 工具函数 + ├── const.go # 常量定义 + ├── web_pages.go # OAuth 重定向页面 + ├── examples/ # 使用示例 (9个) + ├── i18n/ # 多语言 JSON 文件 (12种语言) + ├── specs/ # SPEC 解析配套文件 + ├── lan/ # LAN 控制配套文件 + ├── migration/ # 移植文档 (13个模块详细对照) + └── *_test.go # 单元测试 +``` + +--- + +## 核心模块说明 + +### 1. MIoTClient (`miot_client.go`) + +主客户端,管理所有子模块,是整个 SDK 的入口。 + +**核心职责**: +- 设备管理(云端/网关/本地三路合并) +- 定时器管理(Token 刷新、证书刷新、属性刷新) +- 订阅协调(云端 MQTT / 本地 MIPS / LAN) +- 回调通知(`persistenceNotify`) + +**对齐 Python**:`miot_client.py` `MIoTClient` 类 + +--- + +### 2. MIoTHttpClient + MIoTOauthClient (`miot_cloud.go`) + +小米云服务的 HTTP API 客户端,包含 OAuth2 授权流程。 + +**核心功能**: +- `MIoTHttpClient`:封装所有 REST API 调用(设备列表、属性读写、SSEC 加密等) +- `MIoTOauthClient`:OAuth2 授权码流程(生成授权 URL、轮询 token、刷新 token) +- SSEC 服务端加密(AES-128-CBC) + +**对齐 Python**:`miot_cloud.py` `MIoTHttpClient` + `MIoTOauthClient` + +--- + +### 3. MIoTDevice (`miot_device.go`) + +设备模型,描述单个小米设备的完整状态。 + +**核心字段**: +- 设备元数据(did/model/name/online 等) +- SPEC 实例(MIoTSpecService 树) +- 属性/事件/Action 的读写缓存 +- LAN 控制相关(token/io 端口/宏定义) + +**对齐 Python**:`miot_device.py` `MIoTDevice` 类 + +--- + +### 4. MIoTSpecParser (`spec_parser.go`) + +解析小米 IoT SPEC 描述文件,构建设备能力模型。 + +**核心功能**: +- 从云端或本地加载 SPEC 标准库(14 天缓存) +- 解析 JSON 格式 SPEC,生成 `MIoTSpecService` / `MIoTSpecProperty` / `MIoTSpecEvent` / `MIoTSpecAction` 树 +- 支持多语言(从 `i18n/` 加载) + +**对齐 Python**:`miot_spec.py` `MIoTSpecParser` + +--- + +### 5. MIoTLan (`miot_lan.go`) + +局域网控制,通过 UDP 9588 端口与设备直接通信。 + +**核心功能**: +- 设备发现(广播 `{"dev": "1234"}`) +- 属性读取(`get_prop`) +- 属性写入(`set_prop` / `enable_prop`) +- Action 调用(`action`) +- 事件订阅(通过 `MIoTMatcher`) + +**对齐 Python**:`miot_lan.py` `MIoTLan` + +--- + +### 6. MipsClient (`mips_client.go`) + +MIPS(小米智能平台服务)的 MQTT 客户端,支持云端和本地两种模式。 + +**核心功能**: +- `MipsCloudClient`:连接小米云端 MQTT Broker +- `MipsLocalClient`:连接本地网关 MQTT Broker +- 设备状态订阅/发布 +- 自动重连 + +**对齐 Python**:`miot_mips.py` `MipsCloudClient` + `MipsLocalClient` + +--- + +### 7. MIoTStorage (`miot_storage.go`) + +本地存储管理,处理证书、设备字典、SPEC 等文件的读写。 + +**存储内容**: +- `cert/`:MQTT 证书(PEM 格式) +- `miot_devices/`:设备字典(`.dict` 文件) +- `miot_specs/`:SPEC 标准库(`.dict` 文件) +- `logs/`:日志文件 + +**对齐 Python**:`miot_storage.py` `MIoTStorage` + +--- + +### 8. MIoTMdns (`miot_mdns.go`) + +mDNS 服务发现,用于发现本地网络中的 MIPS 网关。 + +**核心功能**: +- 监听 `_miot_mips._tcp.local.` 服务记录 +- 解析 SRV/TXT 记录,提取 group_id/IP/port +- 服务状态变化回调 + +**对齐 Python**:`miot_mdns.py` `MipsService` + +--- + +### 9. 其他模块 + +| 模块 | 文件 | 功能 | +|------|------|------| +| 错误码 | `miot_error.go` | 定义 `MIoTErrorCode` 枚举和 `MIoTError` 结构体 | +| 常量 | `const.go` | OAuth URL、平台列表、不支持型号等常量 | +| 网络监控 | `miot_network.go` | 网卡状态监控、ping 检测 | +| 国际化 | `miot_i18n.go` | 多语言翻译,加载 `i18n/*.json` | +| Matcher | `miot_matcher.go` | 属性/事件订阅的快速匹配器 | +| 工具函数 | `common.go` | `CalcGroupID`、`LoadJSONFile`、`Slugify` 等 | + +--- + +## 移植进度 + +根据 `MIGRATION_PLAN.md`,移植工作分四批完成: + +| 批次 | 优先级 | 模块数 | 状态 | +|------|--------|--------|------| +| 第一批 | P0 基础架构 | 7 | ✅ 完成 | +| 第二批 | P1 高级功能 | 3 | ✅ 完成 | +| 第三批 | P2 LAN 控制 | 2 | ✅ 完成 | +| 第四批 | P3 附加功能 | 2 | ✅ 完成 | + +**总计:14/14 全部完成** + +详细审核结果见各批次 `BATCH*_REVIEW.md` 和 `ROUND*_REVIEW.md`。 + +--- + +## 依赖项 + +```go +// go.mod +require ( + github.com/eclipse/paho.mqtt.golang v1.5.1 // MQTT 客户端 + github.com/hashicorp/mdns v1.0.7 // mDNS 服务发现 + gopkg.in/yaml.v3 v3.0.1 // YAML 解析 +) +``` + +--- + +## 使用示例 + +`miot/examples/` 下提供了 9 个独立可运行示例: + +| 示例 | 路径 | 功能 | +|------|------|------| +| 完整客户端 | `examples/full_client/` | 演示完整 MIoTClient 使用流程 | +| 云端设备 | `examples/cloud_devices/` | 从云端获取设备列表 | +| OAuth 授权 | `examples/cloud_oauth/` | OAuth2 授权码流程 | +| LAN 控制 | `examples/lan_control/` | 局域网设备控制 | +| mDNS 发现 | `examples/mdns_discovery/` | 发现本地 MIPS 网关 | +| MIPS 订阅 | `examples/mips_subscribe/` | 订阅设备状态变化 | +| 网络监控 | `examples/network_monitor/` | 网络状态监控 | +| SPEC 解析 | `examples/spec_parser/` | 解析设备 SPEC | +| 存储管理 | `examples/storage_manage/` | 证书/设备管理 | + +--- + +## 文件对照表 + +| Python 文件 | Go 文件 | 状态 | +|-------------|---------|------| +| `miot_error.py` | `miot_error.go` | ✅ | +| `const.py` | `const.go` | ✅ | +| `common.py` | `common.go` | ✅ | +| `miot_storage.py` | `miot_storage.go` | ✅ | +| `miot_cloud.py` | `miot_cloud.go` | ✅ | +| `miot_client.py` | `miot_client.go` + `api/device/prop/sub.go` | ✅ | +| `miot_device.py` | `miot_device.go` | ✅ | +| `miot_spec.py` | `spec_parser.go` | ✅ | +| `miot_mips.py` | `mips_client.go` | ✅ | +| `miot_network.py` | `miot_network.go` | ✅ | +| `miot_mdns.py` | `miot_mdns.go` | ✅ | +| `miot_lan.py` | `miot_lan.go` | ✅ | +| `miot_i18n.py` | `miot_i18n.go` | ✅ | +| `web_pages.py` | `web_pages.go` | ✅ | + +--- + +## 开发文档 + +移植过程中的详细文档均保存在 `miot/migration/` 目录: + +- `01-miot-client.md` ~ `13-miot-mdns.md`:各模块第一轮移植详细设计 +- `R2-01-*.md` ~ `R2-14-*.md`:第二轮方法级对齐审核记录 +- `MIGRATION_PLAN.md`:总索引和进度跟踪 +- `ROUND2_PLAN.md` / `ROUND3_PLAN.md`:后续优化轮次计划 + +--- + +## 许可证 + +与 Python 原版一致,遵循 Xiaomi proprietary license(非商业用途 Home Assistant 使用授权)。 + +--- + +*最后更新:2026-06-28* diff --git a/miot/common_test.go b/miot/common_test.go index e56a377..ed5b5d5 100644 --- a/miot/common_test.go +++ b/miot/common_test.go @@ -7,7 +7,7 @@ import ( "path/filepath" "testing" - miot "miot" + miot "xiaomihome/miot" ) // ============================================================================ @@ -259,7 +259,7 @@ func TestSlugifyName(t *testing.T) { {"", ""}, {" spaces ", "spaces"}, {"special!@#chars", "special_chars"}, - {"混合中文English", "english"}, // non-alpha chars become underscores, trim + {"混合中文English", "english"}, // non-alpha chars become underscores, trim {"dash-separated", "dash_separated"}, // dash is non-alphanumeric {"123numbers456", "123numbers456"}, {"__leading_trailing__", "leading_trailing"}, diff --git a/miot/const_test.go b/miot/const_test.go index f57b353..804bbe2 100644 --- a/miot/const_test.go +++ b/miot/const_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - miot "miot" + miot "xiaomihome/miot" ) // ============================================================================ diff --git a/miot/examples/cloud_devices/main.go b/miot/examples/cloud_devices/main.go index 657429e..96c9836 100644 --- a/miot/examples/cloud_devices/main.go +++ b/miot/examples/cloud_devices/main.go @@ -6,8 +6,8 @@ import ( "fmt" "os" - "miot" - "miot/examples/config" + "xiaomihome/miot" + "xiaomihome/miot/examples/config" ) func main() { diff --git a/miot/examples/cloud_oauth/main.go b/miot/examples/cloud_oauth/main.go index 538b3ff..0b9f42e 100644 --- a/miot/examples/cloud_oauth/main.go +++ b/miot/examples/cloud_oauth/main.go @@ -6,8 +6,8 @@ import ( "encoding/json" "fmt" - "miot" - "miot/examples/config" + "xiaomihome/miot" + "xiaomihome/miot/examples/config" ) func main() { diff --git a/miot/examples/config/config.go b/miot/examples/config/config.go index 1b07252..eebf85b 100644 --- a/miot/examples/config/config.go +++ b/miot/examples/config/config.go @@ -9,7 +9,7 @@ import ( "os" "time" - "miot" + "xiaomihome/miot" ) // AuthInfo mirrors the OAuth token stored in Python miot_config/{uid}_{cloud_server}.dict. @@ -36,10 +36,10 @@ type Config struct { RedirectURL string `json:"redirect_url"` // OAuth callback URL // === entry_data optional fields === - Language string `json:"language"` // integration_language: en/zh-Hans/de etc. - CtrlMode string `json:"ctrl_mode"` // auto/cloud/lan - StoragePath string `json:"storage_path"` // data directory for miot_storage - NickName string `json:"nick_name"` // display name in Xiaomi Home + Language string `json:"language"` // integration_language: en/zh-Hans/de etc. + CtrlMode string `json:"ctrl_mode"` // auto/cloud/lan + StoragePath string `json:"storage_path"` // data directory for miot_storage + NickName string `json:"nick_name"` // display name in Xiaomi Home // internal configPath string // path to this config file diff --git a/miot/examples/full_client/main.go b/miot/examples/full_client/main.go index cf81f2d..90de2bb 100644 --- a/miot/examples/full_client/main.go +++ b/miot/examples/full_client/main.go @@ -8,8 +8,8 @@ import ( "os/signal" "time" - "miot" - "miot/examples/config" + "xiaomihome/miot" + "xiaomihome/miot/examples/config" ) func main() { diff --git a/miot/examples/lan_control/main.go b/miot/examples/lan_control/main.go index c2b93bc..ce27bf7 100644 --- a/miot/examples/lan_control/main.go +++ b/miot/examples/lan_control/main.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - "miot" + "xiaomihome/miot" ) func main() { diff --git a/miot/examples/mdns_discovery/main.go b/miot/examples/mdns_discovery/main.go index 7e091ea..647be36 100644 --- a/miot/examples/mdns_discovery/main.go +++ b/miot/examples/mdns_discovery/main.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - "miot" + "xiaomihome/miot" ) func main() { diff --git a/miot/examples/mips_subscribe/main.go b/miot/examples/mips_subscribe/main.go index 292b142..3ee817d 100644 --- a/miot/examples/mips_subscribe/main.go +++ b/miot/examples/mips_subscribe/main.go @@ -7,8 +7,8 @@ import ( "os" "time" - "miot" - "miot/examples/config" + "xiaomihome/miot" + "xiaomihome/miot/examples/config" ) func main() { diff --git a/miot/examples/network_monitor/main.go b/miot/examples/network_monitor/main.go index a71e807..9169cce 100644 --- a/miot/examples/network_monitor/main.go +++ b/miot/examples/network_monitor/main.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - "miot" + "xiaomihome/miot" ) func main() { diff --git a/miot/examples/spec_parser/main.go b/miot/examples/spec_parser/main.go index 2aa0eb5..ec256cd 100644 --- a/miot/examples/spec_parser/main.go +++ b/miot/examples/spec_parser/main.go @@ -5,7 +5,7 @@ package main import ( "fmt" - "miot" + "xiaomihome/miot" ) func main() { diff --git a/miot/examples/storage_manage/main.go b/miot/examples/storage_manage/main.go index 1090e8e..fdaf4b1 100644 --- a/miot/examples/storage_manage/main.go +++ b/miot/examples/storage_manage/main.go @@ -5,8 +5,8 @@ package main import ( "fmt" - "miot" - "miot/examples/config" + "xiaomihome/miot" + "xiaomihome/miot/examples/config" ) func main() { diff --git a/miot/migration/ARCH_PLAN.md b/miot/migration/ARCH_PLAN.md new file mode 100644 index 0000000..dfcbc3d --- /dev/null +++ b/miot/migration/ARCH_PLAN.md @@ -0,0 +1,912 @@ +# go-xiaomihome 上层封装架构设计规划 + +> 状态:Proposed | 版本:v1.0 | 日期:2026-06-28 + +--- + +## 一、问题分析 + +当前 `miot` 包的三个核心痛点: + +| 痛点 | 现状 | 影响 | +|------|------|------| +| **大量 map[string]interface{}** | 22+ 个 API 返回 map,10+ 个结构体字段使用 map,回调全部用 map | 无类型安全、无 IDE 补全、运行时才发现错误 | +| **高层信息分散在裸 map** | GetUserInfo/GetHomeInfos/GetDevices 全部返回 map | 用户/家庭/房间/设备信息需要用 `["key"]` 取值 | +| **设备控制无抽象** | 直接走 SetProp(siid, piid, value) | 不知道空调当前模式/温度具体对应哪个 siid/piid | + +--- + +## 二、设计目标 + +1. **强类型封装**:所有 API 返回值从 `map[string]interface{}` 转为明确的结构体 +2. **设备控制抽象**:空调、灯光、开关等提供统一控制接口,隐藏 siid/piid +3. **订阅回调强类型**:PropsChanged / EventOccurred / DeviceStateChanged 回调携带强类型数据 +4. **miot 底层隔离**:上层使用者不直接接触 miot 包的任何类型(可选零依赖) +5. **渐进迁移**:miot 包自行不变,封装层纯粹是一层薄壳 + +--- + +## 三、包结构设计 + +``` +go-xiaomihome/ +├── miot/ # 现有底层包(不动) +│ └── ... # 与 Python miot/ 严格对齐 +│ +├── xiaomi/ # [新] 高级封装层 +│ ├── go.mod # module xiaomihome/xiaomi +│ │ +│ ├── types.go # 所有强类型结构体定义 +│ ├── client.go # Client 主入口(包装 miot.MIoTClient) +│ ├── devices.go # 设备列表 + 设备信息 +│ ├── homes.go # 家庭/房间信息 +│ ├── user.go # 用户信息 +│ ├── properties.go # 属性读写封装(强类型) +│ ├── actions.go # Action 调用封装 +│ ├── subscribe.go # 订阅通知 API(强类型回调) +│ ├── errors.go # 封装层错误定义 +│ │ +│ ├── devices/ # [新] 设备控制抽象 +│ │ ├── base.go # Device 基础接口 + BaseDevice 实现 +│ │ ├── air_conditioner.go # 空调控制接口 +│ │ ├── light.go # 灯光控制接口 +│ │ ├── switch.go # 开关控制接口 +│ │ ├── fan.go # 风扇控制接口 +│ │ ├── cover.go # 窗帘/晾衣架控制接口 +│ │ ├── humidifier.go # 加湿器控制接口 +│ │ ├── vacuum.go # 扫地机控制接口 +│ │ ├── water_heater.go # 热水器控制接口 +│ │ └── thermostat.go # 温控器/电热毯控制接口 +│ │ +│ └── specs/ # [新] SPEC 辅助(设备能力查询) +│ └── resolver.go # 按功能语义解析 SPEC,自动找 siid/piid +``` + +--- + +## 四、核心类型设计 + +### 4.1 types.go — 强类型结构体 + +```go +package xiaomi + +// ========== 基础信息 ========== + +// UserInfo 用户信息 +type UserInfo struct { + UID string `json:"uid"` + NickName string `json:"miliao_nick"` + AvatarURL string `json:"avatar_url"` +} + +// HomeInfo 家庭信息 +type HomeInfo struct { + ID string `json:"id"` + Name string `json:"name"` + UID string `json:"uid"` + GroupID string `json:"group_id"` + Rooms []RoomInfo `json:"rooms"` + DidList []string `json:"did_list"` + ShareState bool `json:"share_state"` +} + +// RoomInfo 房间信息 +type RoomInfo struct { + ID string `json:"id"` + Name string `json:"name"` + HomeID string `json:"home_id"` + DidList []string `json:"did_list"` // 房间内设备 DID +} + +// DeviceInfo 设备信息(替代 map[string]interface{}) +type DeviceInfo struct { + DID string `json:"did"` + Name string `json:"name"` + Model string `json:"model"` + Manufacturer string `json:"manufacturer"` + FWVersion string `json:"fw_version"` + Icon string `json:"icon"` + URN string `json:"urn"` // 设备 SPEC 类型 + Token string `json:"token"` + IP string `json:"local_ip"` + RSSI int `json:"rssi"` + SSID string `json:"ssid"` + BSSID string `json:"bssid"` + Online bool `json:"is_online"` + HomeID string `json:"home_id"` + HomeName string `json:"home_name"` + RoomID string `json:"room_id"` + RoomName string `json:"room_name"` + OwnerID string `json:"owner_id"` + ParentID string `json:"parent_id"` + ParentModel string `json:"parent_model"` + GroupID string `json:"group_id"` + ConnectType int `json:"connect_type"` // 0=WiFi, 1=BLE, 2=ZigBee, ... +} + +// ========== 属性/事件 ========== + +// PropertyValue 属性值(强类型) +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" +} + +// PropertiesChanged 属性变化事件 +type PropertiesChanged struct { + DID string `json:"did"` + Properties []PropertyValue `json:"properties"` +} + +// EventOccurred 事件发生通知 +type EventOccurred struct { + DID string `json:"did"` + SIID int `json:"siid"` + EIID int `json:"eiid"` + Arguments map[string]interface{} `json:"arguments"` + Timestamp int64 `json:"timestamp"` +} + +// DeviceState 设备在线状态 +type DeviceState string +const ( + StateOnline DeviceState = "online" + StateOffline DeviceState = "offline" +) + +// ========== Action ========== + +// ActionResult Action 执行结果 +type ActionResult struct { + DID string `json:"did"` + SIID int `json:"siid"` + AIID int `json:"aiid"` + Code int `json:"code"` + Out []map[string]interface{} `json:"out"` +} +``` + +### 4.2 Client — 主入口 + +```go +package xiaomi + +// Client 高级封装客户端(包装 miot.MIoTClient) +type Client struct { + inner *miot.MIoTClient // 内部 miot 实例(不对外暴露) + + currentUser UserInfo + homes map[string]*HomeInfo + mu sync.RWMutex +} + +// NewClient 创建客户端 +func NewClient(inner *miot.MIoTClient) *Client + +// ========== 用户/家庭/房间 ========== + +// GetUserInfo 获取用户信息(强类型返回) +func (c *Client) GetUserInfo() (*UserInfo, error) + +// GetHomeList 获取所有家庭列表 +func (c *Client) GetHomeList() ([]*HomeInfo, error) + +// GetHome 获取指定家庭详情(含房间) +func (c *Client) GetHome(homeID string) (*HomeInfo, error) + +// GetRooms 获取指定家庭的所有房间 +func (c *Client) GetRooms(homeID string) ([]*RoomInfo, error) + +// GetRoomDevices 获取指定房间的设备 DID 列表 +func (c *Client) GetRoomDevices(homeID, roomID string) ([]string, error) + +// ========== 设备列表 ========== + +// GetDevices 获取所有设备信息(强类型返回) +func (c *Client) GetDevices(opts ...DeviceFilter) ([]*DeviceInfo, error) + +// GetDevice 获取单个设备信息 +func (c *Client) GetDevice(did string) (*DeviceInfo, error) + +// RefreshDevices 刷新设备列表 +func (c *Client) RefreshDevices() error + +// DeviceFilter 设备过滤器函数类型 +type DeviceFilter func(*DeviceInfo) bool + +// FilterByHome 按家庭过滤 +func FilterByHome(homeID string) DeviceFilter +// FilterByRoom 按房间过滤 +func FilterByRoom(roomID string) DeviceFilter +// FilterByOnline 只返回在线设备 +func FilterByOnline() DeviceFilter +// FilterByModel 按型号过滤 +func FilterByModel(model string) DeviceFilter +``` + +### 4.3 属性读写封装 + +```go +package xiaomi + +// GetProp 读取单个属性(与 miot.GetProp 对齐命名) +func (c *Client) GetProp(did string, siid, piid int) (*PropertyValue, error) + +// SetProp 写入单个属性(与 miot.SetProp 对齐命名) +func (c *Client) SetProp(did string, siid, piid int, value interface{}) error + +// GetProps 批量读取属性(与 miot.GetProps 对齐命名) +func (c *Client) GetProps(params []PropKey) ([]*PropertyValue, error) + +// SetProps 批量写入属性(与 miot.SetProps 对齐命名) +func (c *Client) SetProps(params []PropKeyValue) ([]*PropResult, error) + +// PropKey 属性键对 +type PropKey struct { + DID string + SIID int + PIID int +} + +// PropKeyValue 属性键值对 +type PropKeyValue struct { + PropKey + Value interface{} +} + +// PropResult 写入结果 +type PropResult struct { + PropKey + Code int +} + +// ========== Action 封装 ========== + +// Action 调用设备 Action(与 miot.Action 对齐命名) +func (c *Client) Action(did string, siid, aiid int, params []interface{}) (*ActionResult, error) +``` + +### 4.4 订阅通知接口 + +**底层机制回顾**:miot 包通过 `subTree`(topic 匹配树)管理订阅。设备消息从 MQTT/LAN 推送到达后,走 `OnPropMsg` / `OnEventMsg` → `subTree.Match(topic)` → 回调原始 `func(map[string]interface{}, interface{})` handler。封装层**不直接碰 MQTT**,而是复用 miot 的 SubProp/SubEvent/SubDeviceState,做 handler 签名转换。 + +数据流: + +``` +MQTT/LAN 推送 + → miot.OnPropMsg(params) / miot.OnEventMsg(params) + → subTree.Match(did/p/siid/piid) + → miot handler: func(map[string]interface{}, interface{}) + │ + ▼ 封装层转换(本文范围) + → xiaomi handler: func(did string, prop *PropertyValue) +``` + +```go +package xiaomi + +// ========== 强类型回调定义 ========== + +// PropHandler 属性变化回调(强类型,替代 miot 的 map handler) +type PropHandler func(did string, prop *PropertyValue) + +// EventHandler 事件回调(强类型) +type EventHandler func(did string, event *EventOccurred) + +// DeviceStateHandler 设备在线状态变化回调 +type DeviceStateHandler func(did string, state DeviceState) + +// ========== 订阅方法(与 miot.SubProp/SubEvent/SubDeviceState 对齐命名)========== + +// SubProp 订阅设备属性变化通知 +// 内部调用 miot.MIoTClient.SubProp,在 adapter handler 中完成 map→PropertyValue 转换 +// siid/piid 为 0 表示订阅该设备所有属性 +// 返回 subscription ID(与 miot.SubProp 一致) +func (c *Client) SubProp(did string, siid, piid int, handler PropHandler) (string, error) + +// UnsubProp 取消属性订阅(与 miot.UnsubProp 对齐命名) +func (c *Client) UnsubProp(did string, subID string) error + +// SubEvent 订阅设备事件通知 +// 内部调用 miot.MIoTClient.SubEvent,完成 map→EventOccurred 转换 +func (c *Client) SubEvent(did string, siid, eiid int, handler EventHandler) (string, error) + +// UnsubEvent 取消事件订阅 +func (c *Client) UnsubEvent(did string, subID string) error + +// SubDeviceState 订阅设备在线状态变化 +// 内部调用 miot.MIoTClient.SubDeviceState +func (c *Client) SubDeviceState(did string, handler DeviceStateHandler) error + +// UnsubDeviceState 取消设备状态订阅 +func (c *Client) UnsubDeviceState(did string) error +``` + +**实现要点**: + +```go +// SubProp 实现示意:包装 miot handler,转换 map → PropertyValue +func (c *Client) SubProp(did string, siid, piid int, handler PropHandler) (string, error) { + // 1. 包装 handler:map → PropertyValue + wrappedHandler := func(params map[string]interface{}, ctx interface{}) { + prop := &PropertyValue{ + DID: params["did"].(string), + SIID: toInt(params["siid"]), + PIID: toInt(params["piid"]), + Value: params["value"], + } + handler(did, prop) + } + + // 2. 委托给 miot 底层(触发 subTree 注册 + MQTT/LAN 底层订阅) + subID := c.inner.SubProp(did, wrappedHandler, siid, piid) + + // 3. 记录 subID→did 映射(用于 UnsubProp 时清理) + c.propSubs[subID] = did + return subID, nil +} +``` + +**subID 映射管理**(miot.SubProp 返回 string subID,miot.UnsubProp 需要 subID): + +```go +type Client struct { + // ... + propSubs map[string]string // subID → did(用于 UnsubProp 清理) + eventSubs map[string]string // subID → did +} +``` + +**与 miot 底层签名的对应关系**: + +| 封装层 API | 内部调用 miot 方法 | miot handler 签名 | 转换后 handler 签名 | +|---|---|---|---| +| SubProp | SubProp(did, wrapped, siid, piid) | `func(map[string]interface{}, interface{})` | `func(string, *PropertyValue)` | +| SubEvent | SubEvent(did, wrapped, siid, eiid) | `func(map[string]interface{}, interface{})` | `func(string, *EventOccurred)` | +| SubDeviceState | SubDeviceState(did, wrapped) | `func(string, MIoTDeviceState, interface{})` | `func(string, DeviceState)` | + +--- + +## 五、设备控制抽象设计 + +### 5.1 架构思路 + +``` +xiaomi/devices/ +├── base.go # Device 基础接口 +├── air_conditioner.go # AirConditioner 接口 +├── light.go # Light 接口 +├── switch.go # Switch 接口 +├── fan.go # Fan 接口 +├── cover.go # Cover 接口 +├── humidifier.go # Humidifier 接口 +├── vacuum.go # Vacuum 接口 +├── water_heater.go # WaterHeater 接口 +└── thermostat.go # Thermostat 接口 +``` + +**设计原则**: +- 每种设备类型定义一个**接口**(Interface)和**具体实现**(Struct) +- 具体实现内部持有 `*xiaomi.Client` 引用,将高层语义操作翻译为 `siid/piid` 调用 +- 通过 `specs/resolver.go` 从 SPEC 中自动解析出对应的 siid/piid +- 用户不关心 `siid=2, piid=3` 是什么,只关心 `ac.SetTargetTemp(26)` / `ac.SetMode(ModeCool)` + +### 5.2 BaseDevice + +```go +package devices + +// BaseDevice 所有设备控制类的公共基础 +type BaseDevice struct { + client *xiaomi.Client + info *xiaomi.DeviceInfo +} + +// DID 返回设备 DID +func (d *BaseDevice) DID() string +// Name 返回设备名称 +func (d *BaseDevice) Name() string +// Model 返回设备型号 +func (d *BaseDevice) Model() string +// Online 返回设备在线状态 +func (d *BaseDevice) Online() bool +// Info 返回完整设备信息 +func (d *BaseDevice) Info() *xiaomi.DeviceInfo + +// GetProp 快捷属性读取(委托给 xiaomi.Client.GetProp) +func (d *BaseDevice) GetProp(siid, piid int) (*xiaomi.PropertyValue, error) +// SetProp 快捷属性写入 +func (d *BaseDevice) SetProp(siid, piid int, value interface{}) error +// Action 快捷 Action 调用 +func (d *BaseDevice) Action(siid, aiid int, params []interface{}) (*xiaomi.ActionResult, error) + +// SubProp 订阅属性变化(委托给 xiaomi.Client.SubProp) +func (d *BaseDevice) SubProp(siid, piid int, handler xiaomi.PropHandler) (string, error) +// SubEvent 订阅事件 +func (d *BaseDevice) SubEvent(siid, eiid int, handler xiaomi.EventHandler) (string, error) +// SubDeviceState 订阅在线状态 +func (d *BaseDevice) SubDeviceState(handler xiaomi.DeviceStateHandler) error +``` + +### 5.3 空调控制接口 + +```go +// AirConditioner 空调统一控制接口 +type AirConditioner interface { + // 基础能力 + TurnOn() error + TurnOff() error + IsOn() (bool, error) + + // 模式控制 + SetMode(mode ACMode) error + GetMode() (ACMode, error) + + // 温度控制 + SetTargetTemp(celsius float64) error + GetTargetTemp() (float64, error) + GetCurrentTemp() (float64, error) // 室内温度 + + // 风速控制 + SetFanSpeed(speed FanSpeed) error + GetFanSpeed() (FanSpeed, error) + + // 摆风控制 + SetSwing(mode SwingMode) error + GetSwing() (SwingMode, error) + + // 湿度(部分空调支持) + SetTargetHumidity(pct int) error + GetTargetHumidity() (int, error) + GetCurrentHumidity() (int, error) + + // 电辅热 + SetAuxHeat(on bool) error + GetAuxHeat() (bool, error) + + // 节能 + SetEco(on bool) error + GetEco() (bool, error) + + // 订阅 + OnStateChanged(handler func(state ACState)) (string, error) +} + +// ACMode 空调模式 +type ACMode string +const ( + ModeCool ACMode = "cool" + ModeHeat ACMode = "heat" + ModeFan ACMode = "fan" + ModeDry ACMode = "dry" + ModeAuto ACMode = "auto" +) + +// FanSpeed 风速 +type FanSpeed string +const ( + SpeedLow FanSpeed = "low" + SpeedMedium FanSpeed = "medium" + SpeedHigh FanSpeed = "high" + SpeedAuto FanSpeed = "auto" +) + +// SwingMode 摆风 +type SwingMode string +const ( + SwingOff SwingMode = "off" + SwingVertical SwingMode = "vertical" + SwingHorizontal SwingMode = "horizontal" + SwingBoth SwingMode = "both" +) + +// ACState 空调完整状态 +type ACState struct { + Power bool `json:"power"` + Mode ACMode `json:"mode"` + TargetTemp float64 `json:"target_temp"` + CurrentTemp float64 `json:"current_temp"` + TargetHumidity int `json:"target_humidity"` + CurrentHumidity int `json:"current_humidity"` + FanSpeed FanSpeed `json:"fan_speed"` + Swing SwingMode `json:"swing"` + AuxHeat bool `json:"aux_heat"` + Eco bool `json:"eco"` +} +``` + +**实现原理**:`acDevice` 结构体内部维护一个 `specMap`,通过 `specs/resolver.go` 从 SPEC 中找出: +- `power` → `siid=2, piid=1` +- `mode` → `siid=2, piid=2` +- `target_temp` → `siid=2, piid=3` +- `fan_speed` → `siid=2, piid=4` +- ... + +### 5.4 灯光控制接口 + +```go +// Light 灯光统一控制接口 +type Light interface { + TurnOn() error + TurnOff() error + Toggle() error + + // 亮度 + SetBrightness(pct int) error // 1-100, -1 表示不支持 + GetBrightness() (int, error) + + // 色温 + SetColorTemp(kelvin int) error // 2700-6500, -1 表示不支持 + GetColorTemp() (int, error) + + // 颜色 + SetColor(r, g, b int) error // 0-255, -1 表示不支持 + GetColor() (r, g, b int, error) + + OnStateChanged(handler func(state LightState)) (string, error) +} + +type LightState struct { + Power bool `json:"power"` + Brightness int `json:"brightness"` + ColorTemp int `json:"color_temp"` + ColorRGB [3]int `json:"color_rgb"` +} +``` + +### 5.5 开关控制接口 + +```go +// Switch 开关统一控制接口 +type Switch interface { + TurnOn() error + TurnOff() error + Toggle() error + IsOn() (bool, error) + + OnStateChanged(handler func(on bool)) (string, error) +} +``` + +### 5.6 其他设备接口(概要) + +```go +// Fan 风扇接口 +type Fan interface { + TurnOn() error + TurnOff() error + SetSpeed(speed FanLevel) error // 0=off, 1-4 档位 + SetOscillation(on bool) error + SetNaturalWind(on bool) error +} + +// Cover 窗帘/晾衣架 +type Cover interface { + Open() error + Close() error + Stop() error + SetPosition(pct int) error // 0=关, 100=全开 + GetPosition() (int, error) +} + +// Humidifier 加湿器/除湿机 +type Humidifier interface { + TurnOn() error + TurnOff() error + SetTargetHumidity(pct int) error + SetFanSpeed(speed FanLevel) error +} + +// Vacuum 扫地机 +type Vacuum interface { + Start() error + Stop() error + ReturnToDock() error + SetFanSpeed(speed FanLevel) error + GetStatus() (VacuumStatus, error) +} + +// WaterHeater 热水器 +type WaterHeater interface { + TurnOn() error + TurnOff() error + SetTargetTemp(celsius float64) error + GetCurrentTemp() (float64, error) +} + +// Thermostat 温控器 +type Thermostat interface { + SetTargetTemp(celsius float64) error + GetCurrentTemp() (float64, error) + SetHeating(on bool) error +} +``` + +--- + +## 六、specs/resolver.go — SPEC 语义解析器 + +为了让设备控制类能自动找到 `power` / `mode` / `target_temp` 等语义属性对应的 `siid/piid`,需要一个规约解析器: + +```go +package specs + +// PropertyResolver 属性语义解析器 +// 输入:设备的 SPEC Instance +// 输出:语义 → (siid, piid) 映射 +type PropertyResolver struct { + spec *miot.MIoTSpecInstance + // cache + byType map[string][]*miot.MIoTSpecProperty // "power" → [{siid:2, piid:1}] + byDesc map[string]*miot.MIoTSpecProperty // "开关" → {siid:2, piid:1} + byFormat map[string][]*miot.MIoTSpecProperty // "bool" → [...] +} + +// NewPropertyResolver 创建解析器 +func NewPropertyResolver(spec *miot.MIoTSpecInstance) *PropertyResolver + +// FindByType 按 SPEC property type 查找 +// type 如: "on", "mode", "target-temperature", "fan-level", "brightness" +func (r *PropertyResolver) FindByType(propType string) (siid, piid int, found bool) + +// FindByFormat 按数据格式查找 +// format 如: "bool", "uint8", "float", "string" +// 返回第一个匹配的 +func (r *PropertyResolver) FindByFormat(format string, siid int) (piid int, found bool) + +// FindAction 查找 Action +func (r *PropertyResolver) FindAction(siid int, actionType string) (aiid int, found bool) +``` + +### 解析策略 + +每种设备控制接口的构造流程: + +```go +// acDevice 构造函数 +func NewAirConditioner(client *xiaomi.Client, info *xiaomi.DeviceInfo) (AirConditioner, error) { + // 1. 获取设备 SPEC + device := client.GetMIoTDevice(info.DID) // 内部可能获取 + spec := device.SpecInstance() + + // 2. 创建解析器 + resolver := specs.NewPropertyResolver(spec) + + // 3. 解析语义映射 + powerSIID, powerPIID, _ := resolver.FindByType("on") + modeSIID, modePIID, _ := resolver.FindByType("mode") + tempSIID, tempPIID, _ := resolver.FindByType("target-temperature") + fanSIID, fanPIID, _ := resolver.FindByType("fan-level") + + // 4. 如果某个语义映射找不到,该功能标记为不支持 + return &acDevice{ + BaseDevice: NewBaseDevice(client, info), + propMap: propMap{ + power: {powerSIID, powerPIID}, + mode: {modeSIID, modePIID}, + temp: {tempSIID, tempPIID}, + fan: {fanSIID, fanPIID}, + }, + }, nil +} +``` + +--- + +## 七、关键设计决策 + +### ADR-001:封装层不修改 miot 包 + +**状态**:Proposed + +**背景**:miot 包需要与 Python 严格对齐,API 签名和行为不能轻易改变。 + +**决定**:上层封装 `xiaomi/` 作为独立 Go module(`module xiaomihome/xiaomi`),通过 import miot 使用,不做任何修改。封装层专注于转换和简化。 + +**后果**: +- ✅ miot 对齐性不受影响 +- ✅ 封装层可独立测试 +- ❌ 封装层需要处理 miot 类型转换(额外开销但可控) + +--- + +### ADR-002:设备控制接口按功能语义而非 SPEC 编号 + +**状态**:Proposed + +**背景**:不同型号空调的 `power` 属性可能是 `siid=2,piid=1` 或 `siid=3,piid=1`,必须抽象。 + +**决定**:接口方法使用功能语义(`SetTargetTemp`、`SetMode`),由 `specs.PropertyResolver` 在构造时从 SPEC 自动解析映射关系。解析失败的功能标记为不支持。 + +**后果**: +- ✅ 用户无需关心 siid/piid +- ✅ 跨型号兼容 +- ❌ 需要在 SPEC 解析阶段做额外工作 +- ❌ 部分非常规设备可能解析不完全,需要降级方案 + +--- + +### ADR-003:Interface 而非 Struct 嵌入 + +**状态**:Proposed + +**背景**:有两种设计方式:纯 interface 或 struct 嵌入 + 方法重写。 + +**决定**:每种设备类型定义 interface + 私有 struct 实现。工厂函数返回 interface。这样用户代码只依赖接口,便于 mock 测试。 + +**后果**: +- ✅ 可测试性强(mock 接口即可) +- ✅ 未来可替换实现 +- ❌ 增加了一点 boilerplate(接口 + 实现双定义) + +--- + +### ADR-004:回调使用强类型事件对象 + +**状态**:Proposed + +**背景**:底层回调全是 `func(map[string]interface{}, interface{})`。 + +**决定**:封装层在内部转换底层 map 回调为强类型事件对象(`*PropertyValue`、`*EventOccurred`),对上层暴露强类型回调签名。 + +**后果**: +- ✅ 类型安全 +- ✅ IDE 补全 +- ❌ 封包/解包开销(每个回调一次类型断言,影响极小) + +--- + +### ADR-005:Filter 模式用于设备过滤 + +**状态**:Proposed + +**背景**:用户需要按家庭/房间/在线状态筛选设备。 + +**决定**:使用函数式 `DeviceFilter` 模式(`func(*DeviceInfo) bool`),提供预设 Filter 组合。 + +**后果**: +- ✅ 灵活可组合 +- ✅ 无额外依赖 +- ❌ 性能对比预建索引略差(但对家庭设备量级无影响) + +--- + +## 八、实施计划 + +### Phase 1 — 基础封装(优先级 P0) + +| 任务 | 文件 | 工作量 | +|------|------|--------| +| 项目骨架搭建 | `xiaomi/go.mod`, module 初始化 | 小 | +| types.go 结构体定义 | `xiaomi/types.go` | 中 | +| Client 基础框架 | `xiaomi/client.go` | 中 | +| 用户/家庭/房间信息封装 | `xiaomi/user.go`, `homes.go` | 中 | +| 设备列表封装 | `xiaomi/devices.go` | 中 | +| 属性读写封装 | `xiaomi/properties.go` | 中 | +| Action 封装 | `xiaomi/actions.go` | 小 | +| 错误定义 | `xiaomi/errors.go` | 小 | + +### Phase 2 — 订阅与通知(优先级 P0) + +| 任务 | 文件 | 工作量 | +|------|------|--------| +| 订阅通知接口 | `xiaomi/subscribe.go` | 中 | +| 底层回调 → 强类型事件转换 | `xiaomi/subscribe.go` | 中 | +| 设备状态订阅 | `xiaomi/subscribe.go` | 小 | + +### Phase 3 — 设备控制抽象(优先级 P1) + +| 任务 | 文件 | 工作量 | +|------|------|--------| +| SPEC 语义解析器 | `xiaomi/specs/resolver.go` | 大 | +| BaseDevice 基础实现 | `xiaomi/devices/base.go` | 中 | +| Switch 接口 + 实现 | `xiaomi/devices/switch.go` | 小 | +| Light 接口 + 实现 | `xiaomi/devices/light.go` | 中 | +| AirConditioner 接口 + 实现 | `xiaomi/devices/air_conditioner.go` | 大 | +| Fan 接口 + 实现 | `xiaomi/devices/fan.go` | 中 | +| Cover 接口 + 实现 | `xiaomi/devices/cover.go` | 中 | +| Humidifier/Vacuum/WaterHeater | 各文件 | 各小 | +| 工厂函数(NewXXX 从 DeviceInfo 创建) | `xiaomi/devices/factory.go` | 中 | + +### Phase 4 — 测试和文档(优先级 P2) + +| 任务 | 文件 | 工作量 | +|------|------|--------| +| 单元测试 | `*_test.go` | 大 | +| 集成示例 | `xiaomi/examples/` | 中 | +| API 文档 | `xiaomi/README.md` | 中 | + +--- + +## 九、使用示例(目标形态) + +```go +package main + +import ( + "fmt" + "xiaomihome/xiaomi" + "xiaomihome/xiaomi/devices" +) + +func main() { + // 1. 创建底层 miot 客户端(照常) + // ... miot.NewMIoTClient(...) ... + + // 2. 用上层封装包装 + client := xiaomi.NewClient(miotClient) + + // 3. 获取用户信息(强类型!) + user, _ := client.GetUserInfo() + fmt.Printf("用户: %s (UID: %s)\n", user.NickName, user.UID) + + // 4. 获取家庭和房间 + homes, _ := client.GetHomeList() + for _, h := range homes { + fmt.Printf("家庭: %s\n", h.Name) + for _, r := range h.Rooms { + fmt.Printf(" 房间: %s (%d 设备)\n", r.Name, len(r.DidList)) + } + } + + // 5. 获取设备列表(可过滤) + devices_, _ := client.GetDevices( + xiaomi.FilterByHome(homes[0].ID), + xiaomi.FilterByOnline(), + ) + for _, d := range devices_ { + fmt.Printf("设备: %s (%s) 在线:%v\n", d.Name, d.Model, d.Online) + } + + // 6. 创建空调控制对象(自动解析 SPEC) + acInfo := devices_[0] // 假设是个空调 + ac, err := devices.NewAirConditioner(client, acInfo) + if err != nil { + // 该设备不是空调或不支持 + return + } + + // 7. 像操作普通对象一样控制(隐藏了 siid/piid!) + ac.TurnOn() + ac.SetMode(devices.ModeCool) + ac.SetTargetTemp(26) + ac.SetFanSpeed(devices.SpeedAuto) + + // 8. 订阅变化 + ac.OnStateChanged(func(state devices.ACState) { + fmt.Printf("空调状态变化: 模式=%s 温度=%.1f°C\n", + state.Mode, state.TargetTemp) + }) + + // 9. 灯光控制 + light, _ := devices.NewLight(client, lightInfo) + light.TurnOn() + light.SetBrightness(80) + light.SetColorTemp(4000) + + // 10. 开关控制 + sw, _ := devices.NewSwitch(client, switchInfo) + sw.Toggle() +} +``` + +--- + +## 十、待讨论问题 + +1. **工厂模式**:`devices.NewAirConditioner(client, info)` 还是 `client.NewAirConditioner(info)`?前者职责分离更清晰,后者使用稍方便。 + +2. **SPEC 解析失败降级**:当某个功能语义找不到时,是返回 error(构造失败)还是返回一个部分功能的对象(不支持的方法返回 `ErrNotSupported`)?建议后者——空调不能调摆风总比整个空调不能用好。 + +3. **go-xiaomihome 主模块 go.mod**:目前 `go-xiaomihome/go.mod` 是 `module xiaomihome`,miot 是子包 `xiaomihome/miot`。新封装包是否作为 `xiaomihome/xiaomi` 子包,还是独立 repo?建议子包,降低引用复杂度。 + +4. **是否需要 Cloud 直连简化**:`xiaomi.Client` 的构造函数是否应支持直接传配置,内部自建 miot 客户端(完整封装),这样用户连 miot 都不需要 import。建议 Phase 4 再做。 + +--- + +*本文档为架构规划,尚未执行。* diff --git a/miot/miot_device_test.go b/miot/miot_device_test.go index 54f5d43..441eb8d 100644 --- a/miot/miot_device_test.go +++ b/miot/miot_device_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - miot "miot" + miot "xiaomihome/miot" ) // ============================================================================ @@ -16,24 +16,24 @@ import ( func TestDevice_NewAllFields(t *testing.T) { // nil client is valid for testing basic field parsing deviceInfo := map[string]interface{}{ - "did": "device_001", - "name": "Test Light", - "model": "lumi.plug.v1", - "online": true, - "home_id": "home_123", - "home_name": "My Home", - "room_id": "room_456", - "room_name": "Living Room", - "group_id": "group_789", - "fw_version": "1.0.0", - "manufacturer": "Xiaomi", - "icon": "https://example.com/icon.png", - "token": "abc123token", - "local_ip": "192.168.1.100", - "connect_type": float64(2), - "rssi": float64(-65), - "ssid": "MyWiFi", - "bssid": "00:11:22:33:44:55", + "did": "device_001", + "name": "Test Light", + "model": "lumi.plug.v1", + "online": true, + "home_id": "home_123", + "home_name": "My Home", + "room_id": "room_456", + "room_name": "Living Room", + "group_id": "group_789", + "fw_version": "1.0.0", + "manufacturer": "Xiaomi", + "icon": "https://example.com/icon.png", + "token": "abc123token", + "local_ip": "192.168.1.100", + "connect_type": float64(2), + "rssi": float64(-65), + "ssid": "MyWiFi", + "bssid": "00:11:22:33:44:55", } dev := miot.NewMIoTDevice(nil, deviceInfo, nil) @@ -377,23 +377,23 @@ func TestDevice_SubEventNoClient(t *testing.T) { func TestDevice_ToDict(t *testing.T) { dev := miot.NewMIoTDevice(nil, map[string]interface{}{ - "did": "dev1", - "name": "Test Device", - "model": "test.model.v1", - "online": true, - "home_id": "h1", - "home_name": "Home", - "room_id": "r1", - "room_name": "Room", - "group_id": "g1", - "fw_version": "1.0", - "icon": "icon.png", - "token": "tok", - "local_ip": "127.0.0.1", + "did": "dev1", + "name": "Test Device", + "model": "test.model.v1", + "online": true, + "home_id": "h1", + "home_name": "Home", + "room_id": "r1", + "room_name": "Room", + "group_id": "g1", + "fw_version": "1.0", + "icon": "icon.png", + "token": "tok", + "local_ip": "127.0.0.1", "connect_type": float64(1), - "rssi": float64(-50), - "ssid": "wifi", - "bssid": "aa:bb", + "rssi": float64(-50), + "ssid": "wifi", + "bssid": "aa:bb", }, nil) dict := dev.ToDict() @@ -449,24 +449,24 @@ func TestDevice_FromDict(t *testing.T) { func TestDevice_ToDictFromDictRoundtrip(t *testing.T) { original := miot.NewMIoTDevice(nil, map[string]interface{}{ - "did": "roundtrip", - "name": "Roundtrip", - "model": "m.v", - "online": true, - "manufacturer": "Mfr", - "fw_version": "2.0", - "icon": "i.png", - "home_id": "h", - "home_name": "hn", - "room_id": "r", - "room_name": "rn", - "group_id": "g", - "connect_type": float64(3), - "rssi": float64(-30), - "ssid": "s", - "bssid": "b", - "token": "t", - "local_ip": "10.0.0.1", + "did": "roundtrip", + "name": "Roundtrip", + "model": "m.v", + "online": true, + "manufacturer": "Mfr", + "fw_version": "2.0", + "icon": "i.png", + "home_id": "h", + "home_name": "hn", + "room_id": "r", + "room_name": "rn", + "group_id": "g", + "connect_type": float64(3), + "rssi": float64(-30), + "ssid": "s", + "bssid": "b", + "token": "t", + "local_ip": "10.0.0.1", }, nil) dict := original.ToDict() diff --git a/miot/miot_error_test.go b/miot/miot_error_test.go index 1c85819..0c60567 100644 --- a/miot/miot_error_test.go +++ b/miot/miot_error_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - miot "miot" + miot "xiaomihome/miot" ) // ============================================================================ diff --git a/miot/miot_i18n_test.go b/miot/miot_i18n_test.go index e276af7..478b1cd 100644 --- a/miot/miot_i18n_test.go +++ b/miot/miot_i18n_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - miot "miot" + miot "xiaomihome/miot" ) // ============================================================================ diff --git a/miot/miot_matcher_test.go b/miot/miot_matcher_test.go index f2e12ad..566875a 100644 --- a/miot/miot_matcher_test.go +++ b/miot/miot_matcher_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - miot "miot" + miot "xiaomihome/miot" ) // ============================================================================ diff --git a/miot/miot_storage_test.go b/miot/miot_storage_test.go index 45aeffc..cdbfaa8 100644 --- a/miot/miot_storage_test.go +++ b/miot/miot_storage_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - miot "miot" + miot "xiaomihome/miot" ) // ============================================================================ diff --git a/miot/spec_parser_test.go b/miot/spec_parser_test.go index 77d361a..b819779 100644 --- a/miot/spec_parser_test.go +++ b/miot/spec_parser_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - miot "miot" + miot "xiaomihome/miot" ) // ============================================================================ diff --git a/miot/web_pages_test.go b/miot/web_pages_test.go index f1934e0..5690f34 100644 --- a/miot/web_pages_test.go +++ b/miot/web_pages_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - miot "miot" + miot "xiaomihome/miot" ) // ============================================================================