从「巴法云公网桥」到「全本地语音网关」:一次把智能家居搬回家的完整改造记录
TL;DR(先说结论)
- 家里这台海信 CF24BD/UW 除湿机,海信智慧家 APP 能控制,但米家和 Home Assistant 官方都不支持。
- 第一版方案:小爱 → 米家 → 巴法云(公网 MQTT) → HA → 海信云 → 设备。能跑通,但很脆。
- 最终方案:小爱 → 小米云端对话记录 → 本地网关(词库 + 本地 MQTT) → HAOS → 海信云 → 设备。全链路除了小爱本身的语音识别,不再依赖任何第三方公网服务。
- 硬件成本:两台上百元的旧电视盒子 + 一根网线,软件全部免费。
- 文末附带可复用的 Codex Skill 和一键安装包,想抄作业的直接拉到最后。
一、为什么会有这篇文章
南方回南天,湿度计永远在 60% 以上徘徊。买除湿机的时候没想太多,海信这款 CF24BD/UW 除湿功能很扎实,但生态很"孤儿":
- 不支持米家,小爱同学叫不动它;
- Home Assistant 官方集成里没有它;
- 只有海信智慧家 APP 能远程控制。
也就是说,人不在家时想开除湿机,得掏手机、开 APP、等广告、点开关。而智能家居的意义不就在于"一句话的事"吗?
于是有了这两版方案的折腾。文章前面先讲第一版(巴法云),因为它代表了很多 DIY 玩家的第一反应,也确实能通;但最终让我下决心换掉的,不是"能不能用",而是"用着多不踏实"。
二、先看方案一:巴法云通道(能跑,但为什么最后弃了)
2.1 巴法云这条路怎么走
巴法云(bemfa.com)是国内一个免费物联网 MQTT 平台,它和米家有官方合作:米家 APP → 我的 → 其他平台设备 → 添加 → 巴法,登录巴法云账号后,巴法云上的"设备"会同步进米家,小爱同学就能"看到"这些虚拟设备。
当时完整的链路是这样的:
小爱音箱 ──语音──> 米家 APP ──云云绑定──> 巴法云 (MQTT, bemfa.com:9501)
│
Home Assistant (bemfa 插件) ◄──┘
│
控制实体 (switch / input_boolean 虚拟开关)
│
hisense 集成 ──HTTPS──> 海信云 API
│
除湿机 CF24BD/UW
思路很经典:厂商设备大多有可逆的云 API(APP 能控制 = 云端有接口)。HA 集成负责"设备 ↔ 厂商云",巴法云负责"米家/小爱 ↔ HA",两个桥接拼起来就是完整链路。
2.2 它到底行不行?行,但都是坑
巴法云方案最终跑通了,小爱确实能开除湿机。但用下来,每一个环节都在提醒我"这是条临时路":
| 问题 | 实际体验 |
|---|---|
| 所有指令绕公网 | 语音 → 米家云 → 巴法云 → 本地 HA,任何一环抽风,指令就断。免费云没有 SLA,坏不坏、什么时候修,你说了不算。 |
| 设备模型只有四类 | 巴法云接入米家只支持插座、灯泡、风扇、传感器四种模型。除湿机这种设备只能伪装成开关/传感器,模式、风速、湿度这些复杂控制全靠 hack。 |
| 消息不转发第三方客户端 | 巴法云只转发平台内消息,自己写的 MQTT 客户端 publish 过去它不收。调试的时候只能靠日志 + 对着设备实测,链路断了都不知道断在哪。 |
| 插件随 HA 版本崩 | bemfa 插件在 HA 2026.x 一次版本升级后直接加载失败(light 组件移除 mireds 属性),得手动改源码才能继续用。 |
| 米家绑定缓存 | 巴法云改名后米家不更新,只能删绑定重新加,不然小爱永远叫旧名字。 |
| 调试链路太长 | 语音 → 米家 → 巴法云 → HA → 设备,出问题先猜是哪一环,排查成本极高。 |
| 隐私 | 你每次说的指令、触发的状态,全部过一遍第三方公网。 |
其中最有意思的是"消息不转发"那条:这意味着你没法用标准 MQTT 工具去模拟测试,只能靠真实语音 + 看日志。对开发者来说,这基本等于"盲人摸象"。
2.3 什么情况下巴法云仍然合适
不是否定巴法云。如果你的设备本来就只有开关两种状态(灯、插座)、你又不想折腾 HA,巴法云 + 米家"其他平台设备"确实是最短路径,免费、几分钟就能通。
但只要设备有模式、档位、数值这类复杂控制,或者你希望链路可调试、可控、可维护,就该考虑换赛道了。
三、最终方案:全本地语音网关
3.1 设计目标
新方案只有一条硬约束:除了小爱音箱自己的云端语音识别,不再依赖任何第三方公网服务。所有桥接都在家里完成,断网了家里依然能控制(语音识别本身除外)。
3.2 完整链路
小爱音箱 (3 台: 红外版 + 2×SoundPro)
│ 对音箱说的话
▼
小米云端 conversation 接口(小爱 ASR 文本)
│ miservice_listener 按设备轮询拉取对话记录
▼
网关盒子 192.168.3.154 (Armbian + Docker)
├─ miservice_listener.py 拉 ASR → MQTT xiaoai/asr
├─ xiaoai_listener.py 词库匹配 → 标准化指令 xiaoai/command
├─ ha_command_router.py 记录 + 转发 → ha/command
└─ Mosquitto 本地 MQTT broker (1883 / 9001)
│
▼
HAOS 盒子 192.168.3.150 (冬瓜 HAOS + Home Assistant)
├─ MQTT 集成(订阅 ha/command)
├─ 自动化 xiaoai_dehumidifier_power(9 个分支)
└─ hisense 自定义集成 v1.7.0(海信云 API)
│
▼
海信云 API ──HTTPS──> 海信 CF24BD/UW 除湿机
一句话概括:网关盒子负责"听懂小爱在说什么",HAOS 负责"让除湿机动起来",中间用本地 MQTT 解耦。
3.3 三个关键决策
为什么不用音箱侧插件/端口方案?
小爱音箱没有开放端口(三台音箱全端口扫描无一开放),想在音箱本地抓 ASR 得刷机、root、装插件,风险和成本都高。所以选择用小米官方账号接口轮询对话记录——这也是 mi-gpt、MioT Auto 这些成熟项目采用的方案,是被验证过的路。
为什么是轮询?它省资源吗?
小米 conversation 接口没有公开推送通道,只能轮询。但轮询不意味着傻轮询,做成了自适应节奏:
| 场景 | 轮询间隔 |
|---|---|
| 检测到新对话后(保持 120 秒活跃窗口) | 1.5 秒 |
| 空闲时:中午 11:30–14:00 / 晚上 18:00 后 / 周末节假日 | 1.5 秒(灵敏优先) |
| 空闲时:工作日白天 | 10 秒(省请求) |
| 登录态失效期间 | 30 秒 + 日志节流 |
即"平时慢、一说就快",既保证响应速度,又把对小米云端的请求压到最低。
扫码到底授权了什么?(重点说清楚)
这是用户最该搞清楚的一件事。部署时打开 http://<网关IP>:8124,用米家 APP 扫码后才触发授权,授权内容如下:
- 授权范围:读取该小米账号下已绑定小爱音箱的对话记录(ASR 识别文本 + 时间戳),用于把"对音箱说的话"转发到本地网关;
- 不授权:不控制音箱、不修改设备、不读取家庭设备清单以外的数据;
- 凭证:扫码拿到的是 passToken(用于换 serviceToken),token 文件保存在网关盒子本地(
/root/.mi.token),密码不落盘; - 可撤销:删除 token 文件、或修改小米账号密码后即失效;在米家 APP 里也能看到并管理该账号的登录设备。
3.4 组件拆解
① 扫码授权服务(端口 8124)
用小米官方长轮询接口(account.xiaomi.com/longPolling/loginUrl,sid=micoapi)生成二维码。米家 APP 扫码后,服务通过长轮询拿到 passToken/userId,再走 serviceLogin 换 serviceToken,写入 token 文件,自动拉取账号下的音箱列表写入配置,并启动监听服务。
这里有个反直觉的坑:长轮询超时不能设太短。设成 20 秒会频繁重连被小米风控 403,永远收不到扫码结果;改成单次 50 秒超时 + 超时立即续接后,实测连续两分钟无报错。
② ASR 监听服务(miservice_listener)
按音箱逐台轮询 userprofile.mina.mi.com/device_profile/v2/conversation,把新对话发布到 MQTT xiaoai/asr,按 requestId 去重。
它同时承担了登录态自治:一旦发现 401/403,自动用 passToken 换新 serviceToken,用户完全无感;连 passToken 也失效时,日志会明确提示去 8124 重新扫码。
③ 词库服务(xiaoai_listener)
订阅 xiaoai/asr,用 词库/commands.json 做关键词匹配,把口语转成标准化意图。比如:
| 用户说的 | 意图 |
|---|---|
| 打开除湿机 / 开除湿 / 开始除湿 | dehumidifier_on |
| 关闭除湿机 / 除湿机关 | dehumidifier_off |
| 自动除湿 / 切到自动 | dehumidifier_auto |
| 强力除湿 / 连续除湿 | dehumidifier_continuous |
| 大风 / 高风 | dehumidifier_high_fan |
| 湿度50 / 湿度调到50 | dehumidifier_humidity_50 |
词库文件每次收到消息都会重新加载,改完不用重启服务,攒词成本极低。
④ 路由服务(ha_command_router)
订阅 xiaoai/command,把标准化指令转发到 ha/command,同时把所有真实指令追加到 collected_commands.jsonl 作为语料沉淀——以后想扩词,先看用户真实怎么说的。
⑤ HAOS 侧
HAOS 跑在海信 IP208H 机顶盒(冬瓜 HAOS 6.1.161,HA 2026.8.1),核心三件:
- MQTT 集成:broker 指向网关盒子
192.168.3.154:1883; - 自动化:一条
xiaoai_dehumidifier_power自动化订阅ha/command,内含 9 个 choose 分支,分别对应开关、模式、风速、目标湿度; - hisense 自定义集成 v1.7.0:负责和海信云 API 通信,暴露实体:
| 实体 | 能力 |
|---|---|
| switch.…_power | 电源开关(cmdId=1) |
| select.…_dehumidifier_mode | 自动 / 连续除湿(cmdId=3) |
| select.…_fan_speed | 低风 / 高风(cmdId=2,低风固件不响应) |
| number.…_target_humidity | 目标湿度 30–80%(云端不可控,见下文) |
| sensor.… ×5 | 当前湿度 / 温度 / PM2.5 / 目标湿度 / 空气质量 |
⑥ 海信云 API 逆向
海信智慧家 APP 能控制 = 云端有接口。核心发现:
- 登录
portal-account.hismarttv.com/mobile/signon,设备列表按 deviceTypeName 含「除湿」匹配; - 控制走
uploadRemoteLogicCmd,命令映射是逐条实测出来的:cmdId=1电源、cmdId=2风速、cmdId=3模式; - 状态是 29 个逗号分隔整数的数组,按索引解析:
[21]当前湿度、[22]温度、[23]PM2.5…… - 指令保持机制:下发后 60 秒内状态读取返回指令值,防止单向响应的设备状态把 UI 回滚成旧值。
四、指令实测结果(能做什么、不能做什么)
| 指令 | 状态 | 说明 |
|---|---|---|
| 打开/关闭除湿机 | ✅ | 实测:MQTT 发 dehumidifier_off,3 秒内电源实体变 off |
| 自动模式 / 连续除湿 | ✅ | cmdId=3,0=自动,2=连续 |
| 高风 | ✅ | cmdId=2 parm=1 |
| 低风 | ❌ | 云端返回成功但设备忽略(固件限制) |
| 目标湿度(40/50/60) | ❌ | cmdId 1–30 全量探测 + 抓包比对,未找到有效湿度命令(固件限制) |
| 湿度/温度/PM2.5 监测 | ✅ | 5 个传感器实体实时更新 |
注意一个容易被误判的点:云端"接受"(result=True)≠ 设备"执行"。低风和目标湿度就是典型的"伪有效"命令——云端不报错,设备不理你。这也是为什么所有指令必须对着设备实测,不能只看 HA 界面。
五、踩过的坑(每一条都是真金白银)
坑 1:token 静默过期,指令全哑 + 日志涨到 118MB
某天用户说"关闭除湿机没反应"。排查发现小米 serviceToken 早就过期,轮询接口全部返回 401,但代码里 resp.json() 抛错被 except 吞掉了——服务"活着",其实一直在空转,日志滚到 118MB。
修复:401/403 统一抛 AuthError → 自动用 passToken 换新 token;日志改成 RotatingFileHandler(10MB × 3)轮转。
教训:服务"没死"不等于"在工作",吞异常是最贵的偷懒。
坑 2:"没叫它,它自己开了"——其实是两个问题叠一起
用户一度怀疑有"鬼指令"。核对后发现:
- 积压重放:token 恢复的瞬间,小米接口把过期期间积压的旧对话(含几天前的"打开除湿机")带着新时间戳一次性返回,被当成新指令执行了一遍;
- 自动模式自启停:除湿机本身处于自动模式(目标湿度 45%,当时湿度 58%),湿度高于目标就自行启动压缩机——这是设备正常行为,不是指令。
修复:登录态恢复后的第一次轮询只记账、不发布,跳过积压旧对话。
坑 3:原 HisenseHA 代码把电源命令用错了
逆向中发现 cmdId=1 才是电源开关,而参考的开源实现误把 cmdId=4 当电源——云端接受、设备不理。cmdId 映射这种东西,注释和别人的代码都不可信,必须自己逐条实测。
坑 4:HA 新版把脚本登录全封了
HA 2026.x 禁掉了脚本密码登录,/auth/login_flow API 全 401。最终用 Playwright 模拟浏览器登录才拿到 token。机顶盒磁盘只有 0.9GB,装不了 add-on,文件传输全靠 ttyd 终端 + wget。
坑 5:内置 WiFi 和 HAOS 互斥
两台盒子的内置 WiFi 芯片都没有可用主线驱动(一台 SDIO 无应答、一台是紫光展锐 UWE5621),折腾 DTB 还差点把生产盒子搞崩(恢复用了 5 小时)。结论:盒子有线部署,WiFi 缺口用 USB 网卡补。HAOS 6.1.161 实测自带驱动 + 固件的芯片:RTL8188EU/8188FU/8192CU/8192EU、MT7601U、RT5370、RTL8723DU/8822CU/8822BU 等。
六、成本与硬件
| 项目 | 说明 | 成本 |
|---|---|---|
| 网关盒子 | 旧魔百盒刷 Armbian(S905L2,192.168.3.154) | ~几十元 |
| HAOS 盒子 | 海信 IP208H 机顶盒刷冬瓜 HAOS(192.168.3.150) | 手头旧件 |
| 网络 | 两台盒子都插网线 | 网线几块钱 |
| MQTT Broker | Mosquitto(Docker) | 免费 |
| 语音网关 | miservice_listener + 词库 + 路由(Python) | 免费 |
| HA 侧 | HAOS + hisense 集成 + 自动化 | 免费 |
| 可选 | USB WiFi 网卡(8188EU/MT7601U 等) | 10–20 元 |
整个改造没有购买任何商业服务,也没有月费。
七、稳定性与安全设计
- token 自治:serviceToken 过期自动续期,用户无感;连 passToken 都失效才需要重新扫码(扫码页永远在线)。
- 防重放:恢复登录后跳过积压旧对话,避免"过期指令"被误执行。
- 日志治理:滚动日志 + 失效期 30 秒慢轮询 + 提示节流,不再刷屏。
- 本地优先:除语音识别外全链路不出家门;MQTT 可加账号密码(安装包内置说明)。
- 授权可控:扫码只授权"读取音箱对话记录",凭证本地保存,可随时撤销。
八、复用:Skill 与一键安装包
这套链路已经整理成可复用的交付物(与本文同目录/同仓库):
xiaomi-ha-gateway-v1.0/:一键安装包。复制到新网关盒子的/opt/gateway后跑install.sh,自动装 Docker/Mosquitto/三个 Python 服务/四个 systemd 单元;verify.sh做部署自检;内含词库模板、HA 自动化模板、hisense 集成源码、授权范围说明和完整部署手册。SKILL.md:把整个方法论固化成 Codex Skill(复制到~/.codex/skills/即可),下次任何"小爱语音控制非米家设备"的需求,AI 都能按这套流程直接干,不再从零摸索。
换设备时只需要改三处:词库关键词、HA 自动化分支、厂商集成(如果设备不是海信,就替换 hisense 集成为对应厂商的自定义集成——方法论完全一致:APP 能控制 = 云端有 API = 能桥接)。
九、风险与边界(丑话说前面)
- 小米账号条款:通过账号接口轮询对话记录属于灰色地带,有被风控或接口变更的风险。本项目只做读取、轮询频率压到很低,但请自行评估。
- 固件限制:低风、目标湿度是海信云端固件的硬限制,逆向不出来就是没有,别硬刚。
- HA 版本耦合:hisense 集成、自动化配置跟随 HA 版本演进,升级大版本前先备份。
- 不是所有设备都能复刻:前提是设备有可逆的厂商云 API。纯本地局域网协议(如某些 Zigbee/BLE 直连)的设备,需要走 HA 的其他集成路线。
结尾:把智能家居搬回自己家
折腾完这一圈,最大的感受不是"省了几块钱",而是确定性:链路每一环都在自己手里,断了知道断在哪,坏了知道怎么修。巴法云很好,它让无数人第一次体验到"小爱控制万物";但当你想要的是"可控、可调、可维护"时,本地优先才是终局。
附:本文所有代码、配置、踩坑记录均已沉淀到安装包与 Skill 中。觉得有用可以收藏转发,也欢迎按需改造成你自己的"小爱控制万物"网关。
浙公网安备 33010602011771号