chore: 修复 import 路径为 xiaomihome/miot,补充 README 项目文档
go.mod 模块名为 xiaomihome,将所有内部 import 从 "miot" 更新为 "xiaomihome/miot",涉及所有测试文件和示例文件。新增 README.md 项目 介绍文档(架构概览、模块说明、移植进度等)。新增 ARCH_PLAN.md 架构 设计文档。
This commit is contained in:
@@ -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*
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
miot "miot"
|
miot "xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -259,7 +259,7 @@ func TestSlugifyName(t *testing.T) {
|
|||||||
{"", ""},
|
{"", ""},
|
||||||
{" spaces ", "spaces"},
|
{" spaces ", "spaces"},
|
||||||
{"special!@#chars", "special_chars"},
|
{"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
|
{"dash-separated", "dash_separated"}, // dash is non-alphanumeric
|
||||||
{"123numbers456", "123numbers456"},
|
{"123numbers456", "123numbers456"},
|
||||||
{"__leading_trailing__", "leading_trailing"},
|
{"__leading_trailing__", "leading_trailing"},
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
miot "miot"
|
miot "xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"miot"
|
"xiaomihome/miot"
|
||||||
"miot/examples/config"
|
"xiaomihome/miot/examples/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"miot"
|
"xiaomihome/miot"
|
||||||
"miot/examples/config"
|
"xiaomihome/miot/examples/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miot"
|
"xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthInfo mirrors the OAuth token stored in Python miot_config/{uid}_{cloud_server}.dict.
|
// 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
|
RedirectURL string `json:"redirect_url"` // OAuth callback URL
|
||||||
|
|
||||||
// === entry_data optional fields ===
|
// === entry_data optional fields ===
|
||||||
Language string `json:"language"` // integration_language: en/zh-Hans/de etc.
|
Language string `json:"language"` // integration_language: en/zh-Hans/de etc.
|
||||||
CtrlMode string `json:"ctrl_mode"` // auto/cloud/lan
|
CtrlMode string `json:"ctrl_mode"` // auto/cloud/lan
|
||||||
StoragePath string `json:"storage_path"` // data directory for miot_storage
|
StoragePath string `json:"storage_path"` // data directory for miot_storage
|
||||||
NickName string `json:"nick_name"` // display name in Xiaomi Home
|
NickName string `json:"nick_name"` // display name in Xiaomi Home
|
||||||
|
|
||||||
// internal
|
// internal
|
||||||
configPath string // path to this config file
|
configPath string // path to this config file
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miot"
|
"xiaomihome/miot"
|
||||||
"miot/examples/config"
|
"xiaomihome/miot/examples/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miot"
|
"xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miot"
|
"xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miot"
|
"xiaomihome/miot"
|
||||||
"miot/examples/config"
|
"xiaomihome/miot/examples/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miot"
|
"xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"miot"
|
"xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"miot"
|
"xiaomihome/miot"
|
||||||
"miot/examples/config"
|
"xiaomihome/miot/examples/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -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 再做。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文档为架构规划,尚未执行。*
|
||||||
+53
-53
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
miot "miot"
|
miot "xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -16,24 +16,24 @@ import (
|
|||||||
func TestDevice_NewAllFields(t *testing.T) {
|
func TestDevice_NewAllFields(t *testing.T) {
|
||||||
// nil client is valid for testing basic field parsing
|
// nil client is valid for testing basic field parsing
|
||||||
deviceInfo := map[string]interface{}{
|
deviceInfo := map[string]interface{}{
|
||||||
"did": "device_001",
|
"did": "device_001",
|
||||||
"name": "Test Light",
|
"name": "Test Light",
|
||||||
"model": "lumi.plug.v1",
|
"model": "lumi.plug.v1",
|
||||||
"online": true,
|
"online": true,
|
||||||
"home_id": "home_123",
|
"home_id": "home_123",
|
||||||
"home_name": "My Home",
|
"home_name": "My Home",
|
||||||
"room_id": "room_456",
|
"room_id": "room_456",
|
||||||
"room_name": "Living Room",
|
"room_name": "Living Room",
|
||||||
"group_id": "group_789",
|
"group_id": "group_789",
|
||||||
"fw_version": "1.0.0",
|
"fw_version": "1.0.0",
|
||||||
"manufacturer": "Xiaomi",
|
"manufacturer": "Xiaomi",
|
||||||
"icon": "https://example.com/icon.png",
|
"icon": "https://example.com/icon.png",
|
||||||
"token": "abc123token",
|
"token": "abc123token",
|
||||||
"local_ip": "192.168.1.100",
|
"local_ip": "192.168.1.100",
|
||||||
"connect_type": float64(2),
|
"connect_type": float64(2),
|
||||||
"rssi": float64(-65),
|
"rssi": float64(-65),
|
||||||
"ssid": "MyWiFi",
|
"ssid": "MyWiFi",
|
||||||
"bssid": "00:11:22:33:44:55",
|
"bssid": "00:11:22:33:44:55",
|
||||||
}
|
}
|
||||||
|
|
||||||
dev := miot.NewMIoTDevice(nil, deviceInfo, nil)
|
dev := miot.NewMIoTDevice(nil, deviceInfo, nil)
|
||||||
@@ -377,23 +377,23 @@ func TestDevice_SubEventNoClient(t *testing.T) {
|
|||||||
|
|
||||||
func TestDevice_ToDict(t *testing.T) {
|
func TestDevice_ToDict(t *testing.T) {
|
||||||
dev := miot.NewMIoTDevice(nil, map[string]interface{}{
|
dev := miot.NewMIoTDevice(nil, map[string]interface{}{
|
||||||
"did": "dev1",
|
"did": "dev1",
|
||||||
"name": "Test Device",
|
"name": "Test Device",
|
||||||
"model": "test.model.v1",
|
"model": "test.model.v1",
|
||||||
"online": true,
|
"online": true,
|
||||||
"home_id": "h1",
|
"home_id": "h1",
|
||||||
"home_name": "Home",
|
"home_name": "Home",
|
||||||
"room_id": "r1",
|
"room_id": "r1",
|
||||||
"room_name": "Room",
|
"room_name": "Room",
|
||||||
"group_id": "g1",
|
"group_id": "g1",
|
||||||
"fw_version": "1.0",
|
"fw_version": "1.0",
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
"token": "tok",
|
"token": "tok",
|
||||||
"local_ip": "127.0.0.1",
|
"local_ip": "127.0.0.1",
|
||||||
"connect_type": float64(1),
|
"connect_type": float64(1),
|
||||||
"rssi": float64(-50),
|
"rssi": float64(-50),
|
||||||
"ssid": "wifi",
|
"ssid": "wifi",
|
||||||
"bssid": "aa:bb",
|
"bssid": "aa:bb",
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
dict := dev.ToDict()
|
dict := dev.ToDict()
|
||||||
@@ -449,24 +449,24 @@ func TestDevice_FromDict(t *testing.T) {
|
|||||||
|
|
||||||
func TestDevice_ToDictFromDictRoundtrip(t *testing.T) {
|
func TestDevice_ToDictFromDictRoundtrip(t *testing.T) {
|
||||||
original := miot.NewMIoTDevice(nil, map[string]interface{}{
|
original := miot.NewMIoTDevice(nil, map[string]interface{}{
|
||||||
"did": "roundtrip",
|
"did": "roundtrip",
|
||||||
"name": "Roundtrip",
|
"name": "Roundtrip",
|
||||||
"model": "m.v",
|
"model": "m.v",
|
||||||
"online": true,
|
"online": true,
|
||||||
"manufacturer": "Mfr",
|
"manufacturer": "Mfr",
|
||||||
"fw_version": "2.0",
|
"fw_version": "2.0",
|
||||||
"icon": "i.png",
|
"icon": "i.png",
|
||||||
"home_id": "h",
|
"home_id": "h",
|
||||||
"home_name": "hn",
|
"home_name": "hn",
|
||||||
"room_id": "r",
|
"room_id": "r",
|
||||||
"room_name": "rn",
|
"room_name": "rn",
|
||||||
"group_id": "g",
|
"group_id": "g",
|
||||||
"connect_type": float64(3),
|
"connect_type": float64(3),
|
||||||
"rssi": float64(-30),
|
"rssi": float64(-30),
|
||||||
"ssid": "s",
|
"ssid": "s",
|
||||||
"bssid": "b",
|
"bssid": "b",
|
||||||
"token": "t",
|
"token": "t",
|
||||||
"local_ip": "10.0.0.1",
|
"local_ip": "10.0.0.1",
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
dict := original.ToDict()
|
dict := original.ToDict()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
miot "miot"
|
miot "xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
miot "miot"
|
miot "xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
miot "miot"
|
miot "xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
miot "miot"
|
miot "xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
miot "miot"
|
miot "xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
miot "miot"
|
miot "xiaomihome/miot"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user