按门铃时,客厅电视自动弹出门口实时画面,而门铃不会因此被反复弄响。
设备是小米智能门铃 4 Pro(型号代码 midr.cateye.sd400)。其它型号思路相同,但
siid/aiid 编号可能不一样,需要自己查 spec。
这不是一个"装个插件就能用"的教程。整条链路上有四个独立的响铃源和一个休眠拉不到流的 死结,每一个都得单独解决。本仓库把全过程写下来,包括失败的尝试——找到那个正确的唤醒 接口前,我们试错了八种方法,其中还有一次假阳性把结论带偏了好几轮。
如果你的门铃正在莫名其妙地响,直接跳到对应章节:
| 症状 | 原因 | 在哪解决 |
|---|---|---|
| 每次拉流/打开页面就响一声 | go2rtc 的唤醒链末尾是"接听" | 坑 1 |
| 没人操作,每 90–120 秒自己响 | 媒体缓冲满 → 报错 → 重连 → 重跑唤醒链 | 坑 2 |
| 仪表盘明明改好了还是会响 | Home Assistant 的摄像头实体自己在连原始流 | 第三部分 |
| 重启或重载集成后响一声 | event 实体状态恢复导致自动化误触发 |
第五部分 |
- 成品效果
- 整体架构
- 依赖与前提(请先读这节,本方案并非纯离线)
- 第一部分:用 go2rtc 取到视频流
- 第二部分:静默唤醒休眠中的门铃
- 第三部分:接入 Home Assistant
- 第四部分:安卓电视端 TvOverlay
- 第五部分:把整条链路串起来
- 实测数据
- 测试方法学
- 延伸阅读
- 有人按门铃 → 电视弹出门口画面,持续 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 提取(取流仍然需要,唤醒不再需要)
官方 xiaomi_home 集成对这个门铃暴露了 88 个实体,覆盖极广——但没有 camera 平台,
也没有 image 平台。这是设计如此,不是缺陷或 bug,上游有多个 issue 明确说明不做。
所以视频只能靠 go2rtc,它实现了小米自有的 miio/miss 私有协议。
streams:
doorbell:
- "miio://<门铃IP>:54321?token=<TOKEN>&model=midr.cateye.sd400"到这里能出画面了。然后你会发现——每拉一次流,门铃就响一次。
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流也会出现。它的真实含义是会话建立了但设备没在这条通道上送视频, 最常见的原因就是上面说的会话翻转太快。
改完上面这条,还会遇到一个更隐蔽的:没人操作,门铃自己每 90 到 120 秒响一声。
go2rtc 日志里同时有一条 pop buffer is full。这两件事是同一个事件,但它被当成无害警告
记录了三次才被真正调查。
根因在 pkg/xiaomi/miss/cs2/conn.go:媒体通道是一个 100 槽的 Go channel,
Push() 用非阻塞发送,满了既不等待也不丢弃,而是返回 error。这个 error 冒泡成
miss: read media: ...,导致生产者重连,而每次重连都会重跑唤醒链 →
Remote Call → 响铃。
按 17 fps 算,100 槽连一秒都撑不住,一个关键帧突发就满了。
关键在于这是 go2rtc 自己的代码不一致:项目里其它地方(pkg/core/track.go 的
Sender.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,响铃确实停了,看起来像修好了——两小时后又在另一个摄像头上 复现,每次都发生在观看者离开、没人排空通道的瞬间。任何缓冲区都会满。
门铃原生输出 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 Stream,5.4 秒拿到 101 KB 画面,
不响铃。看起来两步唤醒法成立了。
它是假的。设备当时本来就是醒着的——前面的测试流量已经把它弄醒了。
做了受控对照才拆穿:每次测试前先拉一次流,必须拿到 0 字节证明设备确实睡着,然后 从冷启动重测。结果所有变体全部失败。
这是本项目最重要的一条方法论: 任何唤醒测试,必须先确认设备是睡着的,否则测的什么都不是。 这个坑造成了至少两次错误结论。
正确答案从第一轮起就躺在能力列表里,每一轮都被跳过了,因为名字看起来像"上传录像"。
它不上传任何录像。它做的是:给摄像头模块上电,并把网络协议栈拉起来。
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 不能唤醒睡透的门铃,原因见上。它保留下来有两个
用途:
- 排查工具。 门铃醒着的时候可以直接读属性、发动作,绕过 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 流,
它不带唤醒逻辑,所以门铃睡着时卡片只是空着,不会把它拽起来。真正要注意的是看完之后别一直
开着那个页面。
在 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 为真才显示。
这个设计有两个问题,都建议不要照抄:
doorbell_awake是个假信号。 门铃自己不上报"我醒了"——status属性从头到尾是Idle。 那个布尔量本质上是"发完指令数 13 秒",是拿定时器冒充设备状态。- 唤醒一旦失败,界面表现和"这个按钮没接线"完全一样。 自动化正确地检测到失败并把开关 回滚,用户看到的就是开关亮一下又灭掉。上面那个局域网唤醒失效了十天没被发现,就是因为这个。
改成:
- 两张摄像头卡片常驻,不加任何显示条件;
- 上面放一个满宽的按钮,点击调
script.doorbell_wake; - "唤醒中"的提示由
script自己的运行状态驱动——脚本在跑就是在跑,这是真状态。
画面本身就是唯一诚实的反馈。 门铃睡着时两张卡片是空的,点一下,约 10 秒后两路画面自己
接上——custom:webrtc-camera 自带重连,不需要刷新页面。
完整示例见 examples/homeassistant-package.yaml。
详细步骤见 docs/tvoverlay.md,这里是主线。
TvOverlay 是安卓电视上的悬浮通知应用, 它是唯一能在通知里播 RTSP/HLS/DASH 视频的。常被拿来比较的 PiPup 不支持视频, PiPup 自己的 README 就把这个场景指向了 TvOverlay。
$d = '<电视IP>:5555'
adb connect $d
adb -s $d install -r .\tvoverlay.apkTCL 等品牌会拦第三方安装。网上到处抄的那两个设置在这类固件上根本不存在
(package_verifier_enable、verifier_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,绕不过去:
-
一次只显示一条通知,占满整个 duration,其余排队。 用
duration: 120做测试,会把接下来两分钟的推送全堵住,最后弹出来的是过期的那条。 调试时用 10–15 秒。 这一条曾经浪费了两小时,去追一个根本不存在的 HA 问题。 -
没有任何取消手段。重用同一个 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
-
HTTP 200 只代表被接收,不代表显示了。 排在队列里的通知也返回 200。一定要截图验证。
视频弹窗会在整个 duration 期间占着 RTSP 会话,对电池门铃来说这就是它醒着的时间。 这个数字是实打实的耗电,不是审美偏好。
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_1 和 move_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 台其它小米设备正常应答) |
这一节看着啰嗦,但不这么做,测出来的结论大概率是错的。本项目每一条都用错误结论买过单。
-
每次唤醒测试前,必须先证明设备睡着了。 拉一次流拿到 0 字节才算数。否则你测的是上一次操作留下的清醒状态。 这个坑造成了至少两次假阳性。
-
但要先问清楚:这个状态最严格的可观测形式是什么? 上面第 1 条本身就不够严格,而这正是本项目最贵的一次翻车。"0 字节拉流"只证明"没在推流", 不证明"已经离网"。真正严格的形式是全网段 miIO 握手无应答。用不够严格的前提做出的 "两轮独立冷启验证",得出了一个在结构上不可能成立的结论,并且写进了 README 十天。
判断标准:如果你的"前提检查"和"要测的结论"用的是同一个通道,那它证明不了什么。
-
先校准探针,再判断失败。 拉流工具本身可能是坏的。用一台常供电、不休眠的摄像头做对照,确认能正常出图、 耗时多少,之后 0 字节才能被解释成"设备没醒"。
-
不要用延迟推断结论。 已经处于目标状态的设备,指令会在几毫秒内返回——快不代表成功,只代表被短路了。
-
HTTP 200 /
code: 0都不代表事情发生了。 本项目里八个失败接口全部返回成功。必须用独立手段验证结果。 -
周期性警告和周期性症状,在证明无关之前就是同一件事。
pop buffer is full每 90 秒一次、门铃每 90 秒响一次,被分别记录了三次才有人把它们连起来。 -
验证要走用户真正走的那条路。 直接调 API 能证明"动作有效",但证明不了"已经挂在页面上的摄像头卡片会不会自己重连"。 后者才是用户说"点了没反应"时真正坏掉的东西。要用真浏览器:页面开着、设备睡着、点卡片、 看画面出不出来。
-
失败进了自己的错误分支,看起来和什么都没做一模一样。 唤醒失败时自动化正确地回滚了开关,界面表现就是"开关亮一下又灭"。用户报"点了没用"的时候, 先去读他按的那个东西的返回值,不要相信界面。
-
探针本身会污染被测系统,尤其是"连上再断开"这种。 反复用
frame.jpeg抓单帧去测第二路摄像头,6 次失败 4 次;同一时刻用浏览器长连接却一直 正常。原因是每次抓帧都建立并拆掉一个 P2P 会话,门铃跟不上这个翻转速度。 要用用户真实的使用方式去测——持续连接就用持续连接测。 -
自动化测试脚本要保证异常路径也清理资源。 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 规格定义 和自有设备实测。