折腾 Home Assistant + 米家设备时,最让人头疼的就是"数据延迟"。官方集成走米家云,温湿度 2-3 分钟才更新一次,偶尔云端数据还会"抽风"报错。本文记录我用 python-miio 绕过米家云、局域网直连除湿机,把数据采集频率做到秒级、并接入 HA 的完整实战。


一、痛点:为什么需要本地直连

先看一张真实对比(同一台除湿机,同一时刻):

数据来源当前值状态
米家云(官方集成)30 °F ❌云端数据坏了,实际应是 84 °F
本地直连(python-miio)29.5 °C ✅准确

官方 HA 米家集成的工作方式是 MQTT 订阅推送

设备属性变化 → 设备上报米家云 → 米家云 MQTT 推送 → HA
```plain

理论上 HA 能"第一时间"收到,但实际依赖两点:
1. **设备主动上报**(米家 WiFi 设备为省电,通常 2-3 分钟才上报一次传感器数据)
2. **推送可靠性**(官方仓库 Issue 里提到 WiFi 设备推送丢失率接近 30%)

结果就是:**延迟大、还不一定准**。

而**本地直连**的路径是:

```plain
你的脚本 → 局域网直连设备(miio 协议)→ 秒级返回
```plain

不经过米家云,不依赖设备上报节奏,你想读就读,又快又准。

---

## 二、原理:米家设备的本地通信

小米的 WiFi 设备(除湿机、空气净化器、扫地机等)在局域网内会开放一个 **miio 协议**(UDP 54321 端口)。只要你有设备的 **token**(一个 32 位十六进制密钥),就能在局域网内直接和设备通信。

```plain
┌──────────┐   局域网 (UDP 54321)   ┌──────────┐
│  你的脚本 │ ──── miio 协议 ──────▶│  小米设备 │
│(python-miio)│ ◀── 加密响应 ────── │          │
└──────────┘                        └──────────┘
```plain

关键点:
- **token** 是设备配对时生成的密钥,控制设备的"钥匙"
- miio 协议是加密的,token 用于加解密
- [python-miio](https://github.com/rytilahti/python-miio) 是开源的 Python 库,封装了这套协议
- MIoT(小米新一代物模型)设备用 `siid/piid`(服务ID/属性ID)寻址属性

---

## 三、实战:除湿机本地直连全流程

### 第 1 步:获取设备 token

token 是本地直连的必需品。获取方式有几种:

**方法 A:从 Home Assistant 官方集成配置里提取(最省事)**

如果你已经在 HA 装了官方米家集成,token 已经明文存在配置里:
```plain
/config/.storage/xiaomi_home/miot_devices/<你的uid>_cn.dict
```plain
这个文件是 JSON,里面每个设备都带 `token` 字段。

**方法 B:用旧版米家 APP 提取数据库**

安卓米家 APP(旧版 5.4.54)的数据库里有 token,用 [mihome-binary-extractor](https://github.com/Maxmudjon/Get_MiHome_devices_token) 提取。

**方法 C:在线工具**(不推荐,有账号泄露风险)

> ⚠️ **token 等于设备控制权**,千万不要提交到公开仓库或发到群里。

### 第 2 步:找到设备局域网 IP

本地直连需要设备的局域网 IP。几种方式:
- 路由器后台看 DHCP 客户端列表
- `ip neigh` / `arp -a` 看 ARP 表
- python-miio 的发现功能

但设备 MAC 可能随机化,最稳的办法是**用 token 遍历候选 IP 试连**:

```python
from miio import MiotDevice

token = "你的token"
for ip in ["192.168.1.98", "192.168.1.107", "192.168.1.89"]:
    try:
        dev = MiotDevice(ip=ip, token=token, mapping={})
        info = dev.info()
        print(f"✅ {ip}: {info.model}")
    except Exception:
        print(f"❌ {ip}")
```plain

哪个 IP 返回了 `model`,就是它(我的除湿机是 `192.168.1.89`,model `xiaomi.derh.13l`)。

### 第 3 步:读数据 —— 理解 MIoT 的 siid/piid

MIoT 设备用**服务(siid) + 属性(piid)** 来组织功能。比如除湿机:
- `siid=3, piid=2` → 温度
- `siid=3, piid=1` → 湿度
- `siid=2, piid=5` → 目标湿度

读取用 `get_property_by(siid, piid)`:

```python
from miio import MiotDevice

dev = MiotDevice(ip="192.168.1.89", token="你的token", mapping={})

temp = dev.get_property_by(3, 2)[0]["value"]     # 温度
humidity = dev.get_property_by(3, 1)[0]["value"]  # 湿度
print(f"温度 {temp}°C, 湿度 {humidity}%")
# 输出: 温度 29.5°C, 湿度 58%
```plain

**怎么知道哪个 siid/piid 是温度?** —— 扫描:

```python
for siid in range(1, 9):
    for piid in range(1, 8):
        try:
            r = dev.get_property_by(siid, piid)[0]
            if r.get("code") == 0 and r.get("value") is not None:
                print(f"siid={siid} piid={piid}: {r['value']}")
        except Exception:
            pass
```plain

看输出里哪个值像温度(20-30)、哪个像湿度(40-70),就能对上号。也可以查设备的 [MIoT Spec](https://home.miot-spec.com/)。

### 第 4 步:MQTT 推送到 HA,秒级显示

光自己读到数据还不够,要让它进 HA、能在仪表盘显示,最优雅的方式是 **MQTT Discovery**:你的脚本发布一个 discovery 消息,HA 自动创建对应传感器。

架构:
```plain
除湿机 ──python-miio──▶ 采集脚本 ──paho-mqtt──▶ mosquitto ──discovery──▶ HA
```plain

完整采集脚本:

```python
import time, json
import paho.mqtt.client as mqtt
from miio import MiotDevice

MQTT_HOST = "127.0.0.1"
DEV_IP = "192.168.1.89"
DEV_TOKEN = "你的token"
INTERVAL = 10  # 每10秒采集一次

# 连 MQTT
client = mqtt.Client()
client.connect(MQTT_HOST, 1883, 60)
client.loop_start()

# 发布 discovery,HA 会自动创建传感器
discovery = {
    "name": "除湿机温度(本地)",
    "state_topic": "dehumi_local/state",
    "value_template": "{{ value_json.temperature }}",
    "unit_of_measurement": "°C",
    "device_class": "temperature",
    "state_class": "measurement",
    "unique_id": "dehumi_local_temp_001",
}
client.publish("homeassistant/sensor/dehumi_local_temp/config",
               json.dumps(discovery), retain=True)
# 湿度同理,略

dev = MiotDevice(ip=DEV_IP, token=DEV_TOKEN, mapping={})

while True:
    try:
        temp = dev.get_property_by(3, 2)[0]["value"]
        humid = dev.get_property_by(3, 1)[0]["value"]
        # 一次发一个 JSON,HA 用 value_template 拆分
        payload = json.dumps({"temperature": temp, "humidity": humid})
        client.publish("dehumi_local/state", payload, retain=True)
        print(f"{time.strftime('%H:%M:%S')} 温度={temp}°C 湿度={humid}%")
    except Exception as e:
        print(f"采集失败: {e}")
    time.sleep(INTERVAL)
```plain

HA 那边只要装了 MQTT 集成(连同一个 broker),几秒内就会自动出现「除湿机温度(本地)」「除湿机湿度(本地)」两个传感器,**10 秒刷新一次**。

---

## 四、效果对比

| | 官方集成(云端) | 本地直连(python-miio+MQTT) |
|--|--|--|
| 更新频率 | 2-3 分钟 | **10 秒(可调更短)** |
| 数据准确性 | 依赖云端(偶发错误) | **直读设备,准** |
| 断网能否工作 | ❌ | ✅ |
| 响应延迟 | 分钟级 | **秒级** |
| 部署复杂度 | 一键装集成 | 需要写脚本 + broker |

---

## 五、踩坑记录

1. **netifaces 编译失败**:python-miio 依赖 netifaces,在 `python:3-slim` 镜像里没有 gcc 会编译失败。解决:用完整的 `python:3` 镜像,或装 `gcc python3-dev`。

2. **新版 python-miio 的 API 变了**:网上的老教程用 `get_properties_by`,新版改成了 `get_property_by(siid, piid)`,而且 `MiotDevice` 创建时必须传 `mapping={}`(否则报 "Neither the class nor the parameter defines the mapping")。

3. **`miiocli` 命令找不到**:pip 装完 python-miio 后,`miiocli` 有时不在这个 PATH。直接用 Python API 调用更稳。

4. **MQTT 集成配置方式**:新版 HA 不能在 `configuration.yaml` 里写 `mqtt: broker: ...` 了(会报 invalid option),必须通过「设置 → 集成 → 添加 MQTT」用 UI 配置,或直接写 config_entries。

5. **discovery 的 entity_id 是中文名 slugify**:如果 discovery 里 `name` 写中文,HA 生成的 entity_id 会是拼音(如 `chu_shi_ji_wen_du`),自动化里引用要注意。

6. **设备 MAC 随机化**:现在很多设备开启了 MAC 随机化,没法靠厂商 OUI 识别。用 token 遍历 IP 试连最靠谱。

---

## 六、总结

本地直连的本质就三件事:**拿到 token → 找到 IP → 用 miio 协议读写**。

适合的场景:
- 对数据实时性要求高(自动化、监控)
- 米家云数据不稳/不准
- 想完全本地化、断网可控

不适合的场景:
- 蓝牙/Zigbee 设备(不是 IP 设备,走不了 miio)
- 不想折腾、一键党(直接用官方集成就行)

如果你的核心诉求是"数据又快又准",这套方案值得搞。**我的除湿机从 2-3 分钟更新一次,变成了 10 秒一次,而且云端报错温度时本地依然准确** —— 这就是本地直连的价值。

---

**参考资料**
- [python-miio](https://github.com/rytilahti/python-miio)
- [MIoT Spec 查询](https://home.miot-spec.com/)
- [HA MQTT Discovery 文档](https://www.home-assistant.io/docs/mqtt/discovery/)
- [米家官方 HA 集成](https://github.com/XiaoMi/ha_xiaomi_home)