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


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

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

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

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

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

理论上 HA 能"第一时间"收到,但实际依赖两点:

  1. 设备主动上报(米家 WiFi 设备为省电,通常 2-3 分钟才上报一次传感器数据)
  2. 推送可靠性(官方仓库 Issue 里提到 WiFi 设备推送丢失率接近 30%)

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

而本地直连的路径是:

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

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


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

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

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

关键点:

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

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

第 1 步:获取设备 token

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

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

如果你已经在 HA 装了官方米家集成,token 已经明文存在配置里:

/config/.storage/xiaomi_home/miot_devices/<你的uid>_cn.dict

这个文件是 JSON,里面每个设备都带 token 字段。

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

安卓米家 APP(旧版 5.4.54)的数据库里有 token,用 mihome-binary-extractor 提取。

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

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

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

本地直连需要设备的局域网 IP。几种方式:

  • 路由器后台看 DHCP 客户端列表
  • ip neigh / arp -a 看 ARP 表
  • python-miio 的发现功能

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

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}")

哪个 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):

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%

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

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

看输出里哪个值像温度(20-30)、哪个像湿度(40-70),就能对上号。也可以查设备的 MIoT Spec。

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

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

架构:

除湿机 ──python-miio──▶ 采集脚本 ──paho-mqtt──▶ mosquitto ──discovery──▶ HA

完整采集脚本:

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)

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 秒一次,而且云端报错温度时本地依然准确 —— 这就是本地直连的价值。


参考资料