docs: 补充「与 Python 原版的有意差异」说明与设备剔除/风速映射文档

- README 新增有意差异一节:设备列表语义(剔除米家已删设备)与缓存持久化内容的两条有意偏离及其原因、剔除判定规则
- README 架构概览补齐 xiaomi/、bridge/ 两层目录说明
- xiaomi/README 更新 GetDevices/RefreshDevices 签名(ctx),补充设备列表语义、家庭范围刷新示例与数字档位风速映射说明
This commit is contained in:
2026-09-15 19:51:38 +08:00
parent a595330144
commit 7ae32bd012
2 changed files with 67 additions and 6 deletions
+41 -3
View File
@@ -5,6 +5,7 @@
**go-xiaomihome** 是小米智能家居 Home Assistant 集成(`ha_xiaomi_home`)的 **Golang 移植版**。 **go-xiaomihome** 是小米智能家居 Home Assistant 集成(`ha_xiaomi_home`)的 **Golang 移植版**。
将原 Python 版核心 `miot` 包完整移植为 Go 版本,保持 API 语义与行为严格对齐,目标是提供一个高性能、可独立使用的小米 IoT(MIoT)核心 SDK。 将原 Python 版核心 `miot` 包完整移植为 Go 版本,保持 API 语义与行为严格对齐,目标是提供一个高性能、可独立使用的小米 IoT(MIoT)核心 SDK。
(对齐是默认原则,少数**有意偏离**的地方集中在[「与 Python 原版的有意差异」](#与-python-原版的有意差异)一节,改动前请先读它。)
> Python 原版:`ha_xiaomi_home/custom_components/xiaomi_home/miot/` > Python 原版:`ha_xiaomi_home/custom_components/xiaomi_home/miot/`
> Go 移植版:`go-xiaomihome/miot/` > Go 移植版:`go-xiaomihome/miot/`
@@ -13,13 +14,36 @@
## 核心目标 ## 核心目标
- **严格对齐**:Go 版 `miot` 包与 Python 版功能严格对齐,行为一致 - **严格对齐**:Go 版 `miot` 包与 Python 版功能严格对齐,行为一致(**有意差异见下一节**)
- **独立可用**:移植后的 `miot` 包可作为独立 SDK 被任何 Go 项目引用 - **独立可用**:移植后的 `miot` 包可作为独立 SDK 被任何 Go 项目引用
- **高性能**:利用 Go 的并发优势,提升设备发现、消息订阅等场景的性能 - **高性能**:利用 Go 的并发优势,提升设备发现、消息订阅等场景的性能
- **易维护**:保留 Python 版的文件/模块结构,对应关系清晰 - **易维护**:保留 Python 版的文件/模块结构,对应关系清晰
--- ---
## 与 Python 原版的有意差异
移植以「行为对齐」为默认原则,但以下差异是**有意为之**,请勿以「对齐上游」为由改回去:
| # | 差异点 | Python 原版 | 本库 | 原因 |
|---|--------|-------------|------|------|
| 1 | **设备列表语义** | 设备从云端消失时只把 `_device_list_cloud[did]['online']` 置为 `None`、并把 `_device_list_cache` 里该项重算为离线(`miot_client.py` `__update_devices_from_cloud_async`),**永不删除缓存项** | `RefreshDevices` 会把云端已不存在的设备**真正剔除**(`pruneRemovedDevicesLocked`),并连带清理网关/局域网状态、设备实例与 MQTT 订阅 | 原版服务于 Home Assistant 的设备注册表,需要长期保留历史设备;本库使用方的契约是「**设备列表 == 米家当前设备**」,列表接口不该返回米家已删除的设备 |
| 2 | 设备缓存持久化的内容 | `.dict`(`miot_devices`)保留所有曾见过的设备,含已被删除的 | `.dict` 只保留米家当前设备 | 剔除结果在刷新末尾即落盘;否则下次启动 `loadCacheDevice` 会把已删设备从磁盘「复活」 |
第 1 条的剔除判定规则(实现在 `miot/miot_client_device.go`):
1. 云端返回**空列表**时跳过剔除(接口抖动/鉴权异常不清库);
2. 候选集是「**缓存 ∪ 云列表**」——必须遍历缓存,否则启动时 `deviceListCloud` 尚为空,
会漏掉「服务停机期间在米家被删除」的设备;
3. 网关或局域网仍报告在线的设备保留(云端列表可能还没同步到);
4. 子设备 DID(`xxx.s1`)不参与判定,它被归并到父设备的 `sub_devices` 中;
5. 传入 `homeIDs` 时只在**这些家庭范围内**剔除,不会误删其它家庭的设备。
`bridge` 层同步处理:`pruneControllers` 会剔除已删设备的控制器,`AC()`/`Switch()` 等
访问器不再为米家已删除的设备返回实例。
---
## 架构概览 ## 架构概览
``` ```
@@ -27,7 +51,7 @@ go-xiaomihome/
├── go.mod # Go 模块定义 (module: xiaomihome) ├── go.mod # Go 模块定义 (module: xiaomihome)
├── go.sum ├── go.sum
├── README.md # (待完善) ├── README.md # (待完善)
└── miot/ # 核心 MIoT SDK (与 Python miot/ 严格对齐) ├── miot/ # 核心 MIoT SDK (与 Python miot/ 严格对齐)
├── miot_client.go # 主客户端 (设备管理和协调核心) ├── miot_client.go # 主客户端 (设备管理和协调核心)
├── miot_client_api.go # 外部 API 接口 ├── miot_client_api.go # 外部 API 接口
├── miot_client_device.go # 设备管理方法 ├── miot_client_device.go # 设备管理方法
@@ -53,6 +77,20 @@ go-xiaomihome/
├── lan/ # LAN 控制配套文件 ├── lan/ # LAN 控制配套文件
├── migration/ # 移植文档 (13个模块详细对照) ├── migration/ # 移植文档 (13个模块详细对照)
└── *_test.go # 单元测试 └── *_test.go # 单元测试
├── xiaomi/ # miot 的强类型上层封装 (Client + devices 控制抽象)
├── client.go # xiaomi.Client,包装 MIoTClient + MIoTHttpClient
├── devices.go # GetDevices/GetDevice/RefreshDevices + DeviceFilter
├── properties.go # 属性读写
├── actions.go # Action 调用
├── subscribe.go # 属性/事件/在线状态订阅
├── devices/ # 设备控制抽象 (AirConditioner/Light/Switch/Fan/Cover/...)
└── specs/ # SPEC 属性解析器 (按功能语义查 siid/piid)
└── bridge/ # 业务桥接层 (Pool 连接池 + Bridge 类型化访问器)
├── bridge.go # Bridge/Pool、控制器集合、pruneControllers
├── account.go # 账号/Token/家庭/设备刷新
└── subscribe.go # 属性变化 / 在线状态回调分发
``` ```
--- ---
@@ -268,4 +306,4 @@ require (
--- ---
*最后更新:2026-06-28* *最后更新:2026-09-15*
+26 -3
View File
@@ -64,11 +64,27 @@ dids, err := client.GetRoomDevices(homeID, roomID) // → []string
### 设备列表 (`devices.go`) ### 设备列表 (`devices.go`)
```go ```go
devices, err := client.GetDevices(opts...) // → []*DeviceInfo devices, err := client.GetDevices(ctx, opts...) // → []*DeviceInfo
device, err := client.GetDevice(did) // → *DeviceInfo device, err := client.GetDevice(ctx, did) // → *DeviceInfo
err := client.RefreshDevices() // 强制刷新设备列表 err = client.RefreshDevices(ctx) // 强制刷新设备列表(全量)
``` ```
**设备列表语义:`GetDevices()` 返回的即「米家当前设备」**
`GetDevices()` 读的是 SDK 的设备缓存,而 `RefreshDevices()` 会在刷新时**剔除米家在 App 里
已删除的设备**(底层 `miot.pruneRemovedDevicesLocked`),所以列表不会返回已删设备;
磁盘缓存 `miot_devices/*.dict` 也会同步清理,不会在重启后复活。
```go
// 全量刷新:所有家庭;云端返回空列表时不会清库(接口异常保护)
err = client.RefreshDevices(ctx)
// 只刷新指定家庭:剔除范围也限定在该家庭内,不影响其它家庭
err = client.RefreshDevices(ctx, "984001863127")
```
注意:离线设备仍在列表里(`DeviceInfo.Online == false`),只有**从米家删除**的设备才会消失。
**过滤器(组合使用):** **过滤器(组合使用):**
```go ```go
@@ -221,6 +237,13 @@ ac.SetEco(true)
ac.OnPropsChanged(func(state ACState) { ... }) ac.OnPropsChanged(func(state ACState) { ... })
``` ```
> **风速档位映射(数字档位设备)**:部分设备的 `fan-level` 枚举是 `Auto + Level1~Level5`,
> 没有 `low/medium/high` 字面值(如 `klwdz1.airc.001`、`tofan.airrtc.wk01`)。此时按数字档位的
> **相对位置**映射:`low`→最低档、`medium`→中间档、`high`→最高档(`0`/Auto 不参与)。
> 设备上报的数字档位会**反向解析**回 `low/medium/high`,保证「控制写什么、通知就回报什么」;
> 落在映射之外的档位仍返回 SPEC 描述原文。枚举本身就是 `low/medium/high` 的设备走精确匹配,
> 行为不受影响。
#### 风扇 (`Fan`) #### 风扇 (`Fan`)
```go ```go