Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

小米智能门铃 4 Pro → Home Assistant → 安卓电视弹窗

按门铃时,客厅电视自动弹出门口实时画面,而门铃不会因此被反复弄响。

设备是小米智能门铃 4 Pro(型号代码 midr.cateye.sd400)。其它型号思路相同,但 siid/aiid 编号可能不一样,需要自己查 spec。

这不是一个"装个插件就能用"的教程。整条链路上有四个独立的响铃源一个休眠拉不到流的 死结,每一个都得单独解决。本仓库把全过程写下来,包括失败的尝试——找到那个正确的唤醒 接口前,我们试错了八种方法,其中还有一次假阳性把结论带偏了好几轮。

四个响铃源速查

如果你的门铃正在莫名其妙地响,直接跳到对应章节:

症状 原因 在哪解决
每次拉流/打开页面就响一声 go2rtc 的唤醒链末尾是"接听" 坑 1
没人操作,每 90–120 秒自己响 媒体缓冲满 → 报错 → 重连 → 重跑唤醒链 坑 2
仪表盘明明改好了还是会响 Home Assistant 的摄像头实体自己在连原始流 第三部分
重启或重载集成后响一声 event 实体状态恢复导致自动化误触发 第五部分

目录


成品效果

  • 有人按门铃 → 电视弹出门口画面,持续 60 秒(自动化在按铃后约 5 秒落地)
  • 端到端画面延迟 2 秒,长时间播放不漂移
  • 全程只响一次铃(就是访客按的那一次),不会自己叮咚
  • 另外可以主动查看:在 Home Assistant 里拨一个开关,13 秒后出图,同样不响铃
  • 弹窗不抢焦点,电视上原本在放的东西继续放

整体架构

                  小米智能门铃 4 Pro (midr.cateye.sd400)
                              │
        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
   [按铃事件]            [视频流]              [唤醒指令]
        │                     │                     │
   米家中枢网关           go2rtc               本地 miIO
   (MQTT 推送)      (miio 协议, 局域网)      (UDP 54321, 局域网)
        │                     │                     │
        ▼                     ▼                     ▼
   ┌───────────────────────────────────────────────────┐
   │              Home Assistant                       │
   │   event 实体      camera 实体     shell_command    │
   │        └──────── 自动化 ────────────┘              │
   └───────────────────┬───────────────────────────────┘
                       │ rest_command (HTTP)
                       ▼
              TvOverlay (安卓电视, 端口 5001)
                       │
                       ▼
                  电视上弹出画面

三条通道各自独立,互相不能替代

通道 干什么 为什么不能合并
按铃事件 告诉 HA "有人按了" 官方集成不提供可用的视频
视频流 取画面 go2rtc 收不到按铃事件
唤醒指令 让休眠的门铃出图 go2rtc 自带的唤醒会响铃

依赖与前提

本方案不是纯局域网方案。 这点必须说清楚,因为它决定了断网时哪些功能还在。

环节 是否需要小米云
门铃配对 需要,必须用米家 APP 完成
获取设备 token 需要,token 要从小米云提取
Home Assistant 官方集成 xiaomi_home 需要,用小米账号 OAuth 登录
按铃事件推送 有中枢网关时经网关局域网 MQTT 推送;没有网关时取决于集成的连接方式,通常走云端
唤醒指令 需要,经中枢网关下发;早期版本写的"纯局域网 UDP"是错的,见这一节
视频流 不需要,go2rtc 局域网直连门铃
电视弹窗 不需要,局域网 HTTP

关于中枢网关还有一个有时限的依赖:网关的 MQTT 客户端证书有效期约 3 周,到期要联网 续期。所以短时间断网不影响,断网超过三周会连本地控制一起失去

需要准备的东西

  • 小米智能门铃 4 Pro,已用米家 APP 配对完成
  • 米家中枢网关——早期版本写的是"强烈建议",现在是必需:静默唤醒要靠它下发。没有网关 时唤醒动作是否还能经云端到达,本项目没有验证过,不要当成已知可行
  • Home Assistant,装好官方 xiaomi_home 集成
  • 一台能跑 go2rtc 的机器(NAS、软路由、任何 Linux 都行)
  • 安卓电视(本文以 TCL 为例)
  • 设备 token,用 Xiaomi-cloud-tokens-extractor 提取(取流仍然需要,唤醒不再需要)

第一部分:用 go2rtc 取到视频流

为什么不用官方集成取视频

官方 xiaomi_home 集成对这个门铃暴露了 88 个实体,覆盖极广——但没有 camera 平台, 也没有 image 平台。这是设计如此,不是缺陷或 bug,上游有多个 issue 明确说明不做。

所以视频只能靠 go2rtc,它实现了小米自有的 miio/miss 私有协议。

基本配置

streams:
  doorbell:
    - "miio://<门铃IP>:54321?token=<TOKEN>&model=midr.cateye.sd400"

到这里能出画面了。然后你会发现——每拉一次流,门铃就响一次

坑 1:拉流就响铃

go2rtc 的小米源在建立连接前会跑一段唤醒流程,触发条件是这么判断的:

if strings.Contains(model, ".cateye.") { _ = wakeUpCamera(url) }

wakeUpCamera最后一步无条件调用 MIOT 动作 Remote Call(siid 2, aiid 1)

这个动作的真实语义是**"接听来电"**——你在米家 APP 里点"接听"时执行的就是它。对门铃来说, 接听会走完整的呼叫流程,所以它响铃

它自己的注释说前面几个静默步骤在设备清醒时就够了,但代码里没有任何一处停下来检查, 所以无论设备醒着还是睡着,最后都会走到 Remote Call

解法:让 go2rtc 认不出这是猫眼设备。model 参数对这台设备只影响唤醒行为, 不影响传输——会话是靠 did 协商的,IsLegacy 和 miss 客户端的判断对 sd400 全都 走默认分支。所以把 model 写成一个不含 .cateye. 的字符串即可:

streams:
  # 不唤醒、不响铃。日常用这个
  doorbell_live:
    - "miio://<门铃IP>:54321?token=<TOKEN>&model=midr.liveonly.sd400"

  # 第二摄像头(门铃有两路,ch1 朝下看门口地面)
  doorbell_ch1_live:
    - "miio://<门铃IP>:54321?token=<TOKEN>&model=midr.liveonly.sd400&channel=1"

midr.liveonly.sd400编造的字符串,唯一要求是不含 .cateye.

2026-09 更正:miio:// 这个 scheme 在新版 go2rtc 里已经不存在了。 1.9.14 的 internal/xiaomi/xiaomi.go 只注册了一个 xiaomi,源地址形如 xiaomi://<小米账号uid>:@<门铃IP>?did=<did>&model=midr.liveonly.sd400&transport=udp, 认证走 go2rtc 自己存的小米账号凭据,不再用设备 token。上面的 miio:// 写法请按你的 go2rtc 版本对照 internal/xiaomi/README.md

另外 channel取值无关紧要。go2rtc 官方文档写 channel=2,本文写 channel=1, 读源码会发现它只判断空/非空:

if channel == "" { {"videoquality":Q}          }  // 主镜头
else             { {"videoquality":-1,"videoquality2":Q} }  // 副镜头

也就是说副镜头不是"另开一路",而是同一会话里把主镜头关掉、切到副镜头

两路可以同时拉。 2026-09 用真实浏览器复测三次,两张卡片同时挂着,主摄 1280×832、副摄 960×540 同时在播,不需要错开。

但不要用反复抓单帧的方式去测它。 每次 frame.jpeg 都会建立并拆掉一个 P2P 会话, 每 4 秒一次的轮询会让门铃跟不上会话翻转,实测 6 次里失败 4 次——而同一时刻用浏览器 长连接是好的。这个失败是测法造成的,不是设备的毛病。

如果你看到 miss: read media: cs2: read udp [::]:42057: i/o timeout: 本文早期版本说这是"用了会唤醒的原始流、两条唤醒链撞车"。那个解释是错的—— 全程只用 _live 流也会出现。它的真实含义是会话建立了但设备没在这条通道上送视频, 最常见的原因就是上面说的会话翻转太快。

坑 2:门铃每 90 秒自己响一次

改完上面这条,还会遇到一个更隐蔽的:没人操作,门铃自己每 90 到 120 秒响一声

go2rtc 日志里同时有一条 pop buffer is full。这两件事是同一个事件,但它被当成无害警告 记录了三次才被真正调查。

根因在 pkg/xiaomi/miss/cs2/conn.go:媒体通道是一个 100 槽的 Go channelPush() 用非阻塞发送,满了既不等待也不丢弃,而是返回 error。这个 error 冒泡成 miss: read media: ...,导致生产者重连,而每次重连都会重跑唤醒链 → Remote Call响铃

按 17 fps 算,100 槽连一秒都撑不住,一个关键帧突发就满了。

关键在于这是 go2rtc 自己的代码不一致:项目里其它地方(pkg/core/track.goSender.Input)把缓冲区满当作普通背压处理——非阻塞发送,满了就给 Drops 计数器加一, 继续跑,从不返回 error。而且 go2rtc 自己的视频发送缓冲区是 4096 槽,不是 100。

小米这个包在两个方面都是异常值。所以修法不是"调参数",而是让它跟项目自己的约定一致

补丁 作用
patch.py MIOT 动作接口的请求体需要用 params 包一层
patch2.py 电池门铃的唤醒链
patch3.py cs2 媒体缓冲 100 → 4096,对齐 core.NewSender
patch4.py 缓冲满时丢帧计数、绝不返错,对齐 core.Sender.Input

改完后 Push() 在缓冲满时根本不可能返回 error,这条路径再也不会引发重连和响铃。 四轮拆流验证:警告 0 次,任何级别的 WRN/ERR 都是 0 条,容器空闲内存 14 MB。

一个通用教训:把限额调大只是掩盖了糟糕的失败模式,并没有移除它。 第一次把缓冲从 100 调到 2000,响铃确实停了,看起来像修好了——两小时后又在另一个摄像头上 复现,每次都发生在观看者离开、没人排空通道的瞬间。任何缓冲区都会满。

坑 3:电视放不出画面

门铃原生输出 H.265 + OPUS。安卓电视的弹窗播放器不认这个组合:通知出来了,就是没画面。

必须转码。这里每一个参数都是实测出来的,去掉任何一个都会让某项指标退化:

  doorbell_tv_live:
    - "exec:ffmpeg -hide_banner -v error -fflags nobuffer -flags low_delay
       -use_wallclock_as_timestamps 1 -rtsp_transport tcp
       -i rtsp://127.0.0.1:8554/doorbell_live
       -fps_mode passthrough -vf scale=1280:720
       -c:v h264_nvenc -preset p1 -tune ull -profile:v main -g 15 -bf 0
       -rc cbr -b:v 2500k -c:a aac -ar 48000 -ac 2
       -rtsp_transport tcp -f rtsp {output}"

(实际文件里必须写成一行。用 exec: 而不是 ffmpeg: 模板,因为 go2rtc 的 API 拒绝 含空格的源地址。)

参数 为什么
scale=1280:720 电视解不动这些摄像头的 1080p。这是唯一一个止住延迟持续增长的改动
h264_nvenc H.264 在安卓上解码可靠;NVENC 把编码从 CPU 上挪走
-g 15 源关键帧间隔实测 3.49 秒,播放器看不到关键帧就没法开始。约 20fps 下这样是 0.75 秒
-tune ull NVENC 超低延迟模式
-fps_mode passthrough -use_wallclock_as_timestamps 1 让输出节奏跟着到达时间走,而不是假定帧率
-c:a aac 摄像头出 OPUS,电视播放器不收

没有独显就把 h264_nvenc 换成 libx264,CPU 占用会明显上升。

延迟有两种,原因不同,别混为一谈。 恒定滞后是源关键帧间隔造成的;递增滞后是电视 解不动、越积越多(实测 96 秒内累积到 25 秒,最后卡死)。调时间戳标志对两者都没用, 因为时间戳从来不是问题所在。

完整示例见 examples/go2rtc.yaml


第二部分:静默唤醒休眠中的门铃

上一部分的 _live 流有一个代价:它无法唤醒休眠中的门铃。门铃睡着时拉这个流, 拿到的是 0 字节。

于是问题变成:有没有一个既能唤醒、又不响铃的接口?

米家 APP 显然能做到——你在 APP 里点开门铃就能看画面,它不会响。所以这个接口一定存在。

找它花了八次失败

这一节是本仓库最有价值的部分。完整的调用记录和结果见 docs/miot-interfaces.md,这里给结论表:

# 尝试的接口 调用方式 结果
1 keep-alive(13/1) 本地 返回成功,无效
2 Start P2P Stream(10/1) 云端下发 返回 code: 0无效
3 上面两个组合 先 keep-alive 再 Start P2P 无效(此处出过一次假阳性,见下)
4 Start P2P Stream 在一次拉流正在进行的中途 无效
5 Remove Sleep Switch 本地 返回成功,无效
6 云端 legacy /home/rpc wakeup 云端 返回无错误无效
7 打开 Switch Status(2/1 属性) 本地 无效,且这是不该动的用户设置
8 Start P2P Stream(10/1) 本地 miIO 下发 返回成功,无效

八个全部返回成功,八个全部拉不到画面,八个全都不响铃。

Start P2P Stream 是最大的陷阱:名字直译就是"开始 P2P 流",语义上完美对应"直播查看", 所以它被反复重试、变着花样调用。它每次都返回 code: 0

一次假阳性,把结论带偏了好几轮

中途出现过一次"成功":keep-alive 后接 Start P2P Stream5.4 秒拿到 101 KB 画面, 不响铃。看起来两步唤醒法成立了。

它是假的。设备当时本来就是醒着的——前面的测试流量已经把它弄醒了。

做了受控对照才拆穿:每次测试前先拉一次流,必须拿到 0 字节证明设备确实睡着,然后 从冷启动重测。结果所有变体全部失败。

这是本项目最重要的一条方法论任何唤醒测试,必须先确认设备是睡着的,否则测的什么都不是。 这个坑造成了至少两次错误结论。

答案:Upload Video,siid 10 aiid 3

正确答案从第一轮起就躺在能力列表里,每一轮都被跳过了,因为名字看起来像"上传录像"

它不上传任何录像。它做的是:给摄像头模块上电,并把网络协议栈拉起来

siid 10 (P2P 视频流服务) / aiid 3 / 无输入参数

两轮独立冷启动验证(每轮前都用 0 字节拉流证明确实睡着):12–13 秒后出图,不响铃

通用教训:厂商给动作起的名字不代表它的语义。 Remote Call 听起来像"远程调用",实际是"接听来电"。 Upload Video 听起来像"上传录像",实际是"给摄像头上电"。 Start P2P Stream 听起来像"开始直播",实际什么都不做。 判断一个接口只能靠实测。

唤醒必须走中枢,不能走局域网

2026-09 更正。 本节此前写的是"必须走本地 miIO,门铃休眠时保持在网"。那是错的, 而且是本项目目前为止代价最大的一个错误结论。下面是更正后的实测。

门铃休眠时会把 WiFi 整个关掉,不是"在线但不响应"。

验证方法是向整个 /24 网段的 254 个地址逐个发 miIO 握手包:17 台小米设备应答了——中枢网关、 两台浴霸、所有音箱和灯、三台常供电摄像机——门铃一个地址都没应答。它也不出现在路由器的 在线客户端列表里。

所以局域网通道在休眠期间是不存在的,不是"照常工作"。

一次干净的前后对比,同一个地址:

时刻 门铃固定 IP 上的 miIO 握手
唤醒前 无应答
经中枢唤醒后 +5 秒 应答,did 正确

门铃醒来后 5 秒内重新上线到原地址。 这就构成一个死循环:局域网 miIO 只有在门铃已经被 别的东西唤醒之后才够得着它——作为唤醒手段在结构上不可能成立

那当初为什么测通了

因为"用 0 字节拉流证明它睡着了"这个前提不够严格

拉不到流只证明"没在推流",不证明"已经离网"。门铃停止推流之后还会在 WiFi 上挂一段时间, 在那个窗口里局域网 miIO 确实能把它叫醒——两次冷启验证都落在这个窗口内。等它真正睡透、 从 WiFi 上掉下去之后,同样的脚本就再也没成功过。

通用教训:先问"这个状态最严格的可观测形式是什么",再去测它。 这里最严格的形式是"全网段任何地址都不应答",而任何拉流探针都永远看不到这一层。

正确做法:官方集成的按钮实体

同一条 MIOT 动作(Upload Video,siid 10 aiid 3),交给官方 xiaomi_home 集成下发, 它经中枢网关到达设备,不依赖门铃的 IP。

集成会把这个动作直接暴露成一个 button 实体:

button.<厂商>_cn_<did>_<型号>_upload_video_a_10_3

实测(从"全网段无应答"的确认休眠态开始):按下到出图 10 秒,两路通道同时上来,不响铃。

比原来的方案少了一整层依赖——不需要 IP、不需要 token、不需要 cryptography、不需要自己 实现 miIO 协议。

miio_wake.py 还留着,但它不是唤醒方案

仓库里的 miio_wake.py 不能唤醒睡透的门铃,原因见上。它保留下来有两个 用途:

  • 排查工具。 门铃醒着的时候可以直接读属性、发动作,绕过 HA 和云端,用来确认某个 siid/aiid 到底做了什么。
  • 对那些常供电、不休眠的小米设备(摄像机、插座、灯)仍然是完整可用的本地控制通道。
pip install cryptography

export MIIO_IP=192.168.1.50
export MIIO_TOKEN=0123456789abcdef0123456789abcdef

python3 miio_wake.py info          # 读设备信息,验证 token 和连通性
python3 miio_wake.py wake          # 只在设备当前在网时有效

不知道 IP 就 python3 miio_wake.py discover 广播发现。如果 discover 扫不到你的门铃, 那不是脚本坏了,是门铃正睡着——这恰恰是本节要说明的现象。

协议细节(AES-128-CBC、握手、校验和计算范围、报文逐字段结构)见 docs/miot-interfaces.md

一个必须知道的副作用

只要有程序占着流,门铃就睡不着。 go2rtc 在有消费者时会一直保持生产者连接,等于让门铃 持续满负荷工作。一下午反复测试,电量从 68% 掉到 65%

这一点在改用"卡片常驻"的布局后仍然成立,但只在门铃已经醒着的时候:卡片连的是 _live 流, 它不带唤醒逻辑,所以门铃睡着时卡片只是空着,不会把它拽起来。真正要注意的是看完之后别一直 开着那个页面


第三部分:接入 Home Assistant

摄像头实体:一个容易漏掉的响铃源

在 HA 里建 generic 摄像头实体时,stream_source 必须指向 _live

rtsp://<go2rtc地址>:8554/doorbell_live
rtsp://<go2rtc地址>:8554/doorbell_ch1_live

指向原始流的话,Home Assistant 自己会去连它生成预览,于是没人操作也会响铃。

这条教训可以推广:能占住门铃流的东西全都要排查,不只是仪表盘。 我们在仪表盘改完之后,还是被 HA 自建的摄像头实体咬了一次。

另外:generic 摄像头的配置流程会验证流可用,而睡着的门铃会让验证超时失败。 先唤醒再添加。

按铃事件实体

官方集成提供专门的门铃事件实体,命名规则里编进了 siid/aiid:

event.midr_cn_<你的DID>_sd400_doorbell_ring_e_2_1
                                              └ siid 2, event 1

早期版本是去读摄像头的 motion_video_type 属性来判断按铃,那是错的:这个属性描述的是 最近一段录像,白天有路人经过就会把按铃那段覆盖掉,导致真实按铃被漏掉。它之所以看起来能用, 只是因为晚上没人路过

唤醒服务

官方集成已经把动作暴露成 button 实体,所以不需要 shell_command,也不需要任何脚本。 包一层 script 只是为了给界面一个"正在唤醒"的状态:

script:
  doorbell_wake:
    alias: 唤醒门铃
    icon: mdi:doorbell-video
    mode: restart
    sequence:
      - action: button.press
        target:
          entity_id: button.midr_cn_<你的did>_sd400_upload_video_a_10_3
      # 实测 10 秒出图,写 12 秒让"唤醒中"的提示不会提前消失
      - delay: "00:00:12"

早期版本用的是 shell_command + Python 脚本。那条路已经不通,见 唤醒必须走中枢。顺带一提,加 shell_command 需要完整重启 Home Assistant,reload_all 不注册这个域——改成 script 之后连这个限制也一起没有了。

界面:一个按钮,两张常驻画面

早期版本用两个布尔量doorbell_view 意图 / doorbell_awake 是否真有画面),摄像头卡片 用 conditional 包起来,等 doorbell_awake 为真才显示。

这个设计有两个问题,都建议不要照抄:

  1. doorbell_awake 是个假信号。 门铃自己不上报"我醒了"——status 属性从头到尾是 Idle。 那个布尔量本质上是"发完指令数 13 秒",是拿定时器冒充设备状态。
  2. 唤醒一旦失败,界面表现和"这个按钮没接线"完全一样。 自动化正确地检测到失败并把开关 回滚,用户看到的就是开关亮一下又灭掉。上面那个局域网唤醒失效了十天没被发现,就是因为这个。

改成:

  • 两张摄像头卡片常驻,不加任何显示条件;
  • 上面放一个满宽的按钮,点击调 script.doorbell_wake
  • "唤醒中"的提示由 script 自己的运行状态驱动——脚本在跑就是在跑,这是真状态。

画面本身就是唯一诚实的反馈。 门铃睡着时两张卡片是空的,点一下,约 10 秒后两路画面自己 接上——custom:webrtc-camera 自带重连,不需要刷新页面。

完整示例见 examples/homeassistant-package.yaml


第四部分:安卓电视端 TvOverlay

详细步骤见 docs/tvoverlay.md,这里是主线。

为什么是 TvOverlay

TvOverlay 是安卓电视上的悬浮通知应用, 它是唯一能在通知里播 RTSP/HLS/DASH 视频的。常被拿来比较的 PiPup 不支持视频, PiPup 自己的 README 就把这个场景指向了 TvOverlay。

安装

$d = '<电视IP>:5555'
adb connect $d
adb -s $d install -r .\tvoverlay.apk

TCL 等品牌会拦第三方安装。网上到处抄的那两个设置在这类固件上根本不存在package_verifier_enableverifier_verify_adb_installs 读回来是 null,改了没用)。 真正拦你的是系统自带的安装器,临时停用它:

adb -s $d shell "pm disable-user --user 0 com.android.packageinstaller"
adb -s $d install -r .\tvoverlay.apk
adb -s $d shell "pm enable com.android.packageinstaller"

重新启用不是可选项。 有报告称停用状态下做系统更新会把电视卡死,需要恢复出厂。

授权与启动

REST 服务只有在悬浮窗权限授予后才会启动。不用碰遥控器:

adb -s $d shell "appops set com.tabdeveloper.tvoverlay SYSTEM_ALERT_WINDOW allow"
adb -s $d shell "am start -n com.tabdeveloper.tvoverlay/.SetupActivity"
adb -s $d shell "dumpsys deviceidle whitelist +com.tabdeveloper.tvoverlay"

它没有普通的启动器入口,只有 LEANBACK 入口,所以要用 am start 显式拉起。

推送

$body = @{
    id = 'doorbell'; title = '门铃'; message = '有人按门铃'
    duration = 60
    video = 'rtsp://<go2rtc地址>:8554/doorbell_tv_live'
} | ConvertTo-Json -Compress

Invoke-WebRequest -Uri 'http://<电视IP>:5001/notify' -Method Post `
    -Body $body -ContentType 'application/json'

三个必须知道的行为

这三条是设计约束,不是 bug,绕不过去:

  1. 一次只显示一条通知,占满整个 duration,其余排队。duration: 120 做测试,会把接下来两分钟的推送全堵住,最后弹出来的是过期的那条。 调试时用 10–15 秒。 这一条曾经浪费了两小时,去追一个根本不存在的 HA 问题。

  2. 没有任何取消手段。重用同一个 id 不会替换正在显示的通知。 实测:先推 duration: 300,再用同一个 id 推 duration: 1 且不带视频——截图显示屏幕上 还是原来那条,视频照播,第二条在排队。REST 接口没有 cancel、没有 replace、 不能缩短。所以 duration 必须一次定对。

    唯一的真取消是重启应用(约 6 秒放开流,且不抢焦点):

    adb shell am force-stop com.tabdeveloper.tvoverlay
    adb shell am start-foreground-service -n com.tabdeveloper.tvoverlay/.OverlayService
  3. HTTP 200 只代表被接收,不代表显示了。 排在队列里的通知也返回 200。一定要截图验证。

duration 是真实成本

视频弹窗会在整个 duration 期间占着 RTSP 会话,对电池门铃来说这就是它醒着的时间。 这个数字是实打实的耗电,不是审美偏好。


第五部分:把整条链路串起来

rest_command

Home Assistant 没有内置的 HTTP 动作,所以要用 rest_command

rest_command:
  tv_notify:
    url: "http://<电视IP>:5001/notify"
    method: POST
    content_type: "application/json"
    timeout: 15
    payload: >-
      {
        "id": "{{ id | default('ha') }}",
        "title": "{{ title | default('Home Assistant') }}",
        "message": "{{ message | default('') }}",
        "source": "Home Assistant",
        "duration": {{ duration | default(30) }}
        {%- if video is defined %}, "video": "{{ video }}"{% endif %}
      }

自动化

- alias: 门铃响时在电视上弹出门口画面
  triggers:
    - trigger: state
      entity_id: event.midr_cn_<你的DID>_sd400_doorbell_ring_e_2_1
  conditions:
    - condition: template
      alias: 不是状态恢复造成的跳变
      value_template: >-
        {{ trigger.from_state is not none
           and trigger.from_state.state not in ['unknown','unavailable','none']
           and trigger.to_state is not none
           and trigger.to_state.state not in ['unknown','unavailable','none'] }}
    - condition: template
      alias: 事件是新鲜的
      value_template: >-
        {{ (as_timestamp(now()) - as_timestamp(trigger.to_state.state, 0)) | abs < 90 }}
    - condition: template
      alias: 60 秒冷却
      value_template: >-
        {{ (as_timestamp(now())
            - as_timestamp(state_attr('automation.<自动化实体ID>','last_triggered'), 0)) > 60 }}
  actions:
    - action: rest_command.tv_notify
      data:
        id: doorbell
        title: 门铃
        message: 有人按门铃
        duration: 60
        video: rtsp://<go2rtc地址>:8554/doorbell_tv_live
  mode: single

三个条件,每一个都是踩出来的,别简化

条件一:不是状态恢复造成的跳变。

event 实体会被状态恢复。重启或重载集成后它先变 unavailable,再带着上一次的旧时间戳 回来——如果只检查 to_state,这跟一次全新按铃完全无法区分

实际发生过的事故:

14:02:22  event -> unavailable
14:02:24  event -> 2026-08-19 13:34:17   (恢复的旧值)
14:02:24  自动化触发                      ← 误触发
14:02:33  门铃真的响了

为什么会响?误触发的弹窗去拉了门铃流,那时候用的还是会唤醒的流,唤醒链走到 Remote Call响铃。9 秒就是实测的唤醒耗时。

验证这类修复的唯一方法:重载 config entry,然后确认 last_triggered 没有变化。

条件二:事件时间戳在 90 秒内。 恢复的旧值永远满足不了这条,是上一条的双保险。

条件三:60 秒冷却。 给任何自触发循环封顶。

曾经有第四个条件,它误伤了真实按铃

早期还有一条"必须在 300 秒内检测到移动",理由是访客必然先走近再按铃,用移动事件把真人和 自家拉流的回声区分开。

它把 18:02:33 的一次真实按铃拦掉了,电视什么都没显示。

原因在设备设置里,而且一直可读:motion_detection_p_4_1move_detection_p_11_1 都是关的human_detection_mode_p_13_4 要求停留 6 秒。走到门口直接按铃, 不产生任何移动事件。

之所以当初看起来成立,是因为它只被一个正面样本验证过——那次恰好是个逗留了一会儿的人。

教训:一条规则如果只有一个正面观察支撑,而所有反例都是同一类,那它没被验证过。

完整验证

一次真实按铃跑通全程:触发 → 三个条件全过 → 弹窗 → 电视出画面, logbook 里那次按铃只有一条响铃事件(没有回声)。


实测数据

项目 实测值
自动化落地时间 按铃后约 5 秒
静默唤醒到能出图(经中枢) 10 秒
静默唤醒到能出图(早期走局域网,仅在门铃尚未离网时有效) 12–13 秒
唤醒后门铃重新出现在原 IP 5 秒内
主动查看:唤醒到画面上电视 25 秒
端到端画面延迟 2 秒,97 秒内无漂移
按铃后画面可拉取的窗口 短于 100 秒(按铃 100 秒后再拉已经拉不到)
误触发时的唤醒耗时 9 秒(从拉流到铃响)
一下午反复测试的耗电 68% → 65%
go2rtc 容器空闲内存 14 MB
源关键帧间隔 3.49 秒
休眠时全网段 miIO 应答 0 个地址(同网段 17 台其它小米设备正常应答)

测试方法学

这一节看着啰嗦,但不这么做,测出来的结论大概率是错的。本项目每一条都用错误结论买过单。

  1. 每次唤醒测试前,必须先证明设备睡着了。 拉一次流拿到 0 字节才算数。否则你测的是上一次操作留下的清醒状态。 这个坑造成了至少两次假阳性。

  2. 但要先问清楚:这个状态最严格的可观测形式是什么? 上面第 1 条本身就不够严格,而这正是本项目最贵的一次翻车。"0 字节拉流"只证明"没在推流", 不证明"已经离网"。真正严格的形式是全网段 miIO 握手无应答。用不够严格的前提做出的 "两轮独立冷启验证",得出了一个在结构上不可能成立的结论,并且写进了 README 十天。

    判断标准:如果你的"前提检查"和"要测的结论"用的是同一个通道,那它证明不了什么。

  3. 先校准探针,再判断失败。 拉流工具本身可能是坏的。用一台常供电、不休眠的摄像头做对照,确认能正常出图、 耗时多少,之后 0 字节才能被解释成"设备没醒"。

  4. 不要用延迟推断结论。 已经处于目标状态的设备,指令会在几毫秒内返回——快不代表成功,只代表被短路了。

  5. HTTP 200 / code: 0 都不代表事情发生了。 本项目里八个失败接口全部返回成功。必须用独立手段验证结果。

  6. 周期性警告和周期性症状,在证明无关之前就是同一件事。 pop buffer is full 每 90 秒一次、门铃每 90 秒响一次,被分别记录了三次才有人把它们连起来。

  7. 验证要走用户真正走的那条路。 直接调 API 能证明"动作有效",但证明不了"已经挂在页面上的摄像头卡片会不会自己重连"。 后者才是用户说"点了没反应"时真正坏掉的东西。要用真浏览器:页面开着、设备睡着、点卡片、 看画面出不出来。

  8. 失败进了自己的错误分支,看起来和什么都没做一模一样。 唤醒失败时自动化正确地回滚了开关,界面表现就是"开关亮一下又灭"。用户报"点了没用"的时候, 先去读他按的那个东西的返回值,不要相信界面。

  9. 探针本身会污染被测系统,尤其是"连上再断开"这种。 反复用 frame.jpeg 抓单帧去测第二路摄像头,6 次失败 4 次;同一时刻用浏览器长连接却一直 正常。原因是每次抓帧都建立并拆掉一个 P2P 会话,门铃跟不上这个翻转速度。 要用用户真实的使用方式去测——持续连接就用持续连接测。

  10. 自动化测试脚本要保证异常路径也清理资源。 Playwright 脚本中途报错退出,没执行到 browser.close(),浏览器进程死了但 go2rtc 那边 的消费者条目还在。四个这样的幽灵连接堆在主摄流上,把门铃的会话占满,第二路再也连不上—— 用来找问题的工具自己制造了问题,而且只能靠重启 go2rtc 清掉。用 try/finally 包起来。


延伸阅读

  • docs/miot-interfaces.md —— 完整接口速查表、八次失败的详细调用记录、miIO 协议报文格式
  • docs/tvoverlay.md —— 安卓电视全套:ADB、安装、权限、全屏播放、延迟测量
  • examples/ —— go2rtc、Home Assistant 配置样例

适用范围

完整验证 小米智能门铃 4 Pro(midr.cateye.sd400
思路通用 其它带 p2p-stream 服务的小米电池门铃与摄像头
必须自行确认 siid/aiid 编号因型号而异

查你自己设备的规格定义(公开接口,无需认证):

https://miot-spec.org/miot-spec-v2/instance?type=<你的设备URN>

在别的型号上验证成功或失败,欢迎提 issue 补充。


许可证

MIT,见 LICENSE。本项目与小米公司无关,所有接口信息来自公开的 MIOT 规格定义 和自有设备实测。

About

小米智能门铃 4 Pro 接入 Home Assistant,按门铃时安卓电视自动弹出门口画面且不响铃。含完整踩坑记录:四个响铃源、八次失败的接口探索、TvOverlay 配置。

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages