看到 ponytail 这个词,大多数人第一反应是马尾辫怎么扎才好看。但如果你搜索的是 ponytail skill、ponytail 插件、插件 ponytail 如何使用,那你大概率和我一样,是个在智能家居自动化配置里挣扎过的玩家。我给 Home Assistant 写过一个叫 ponytail 的开源小插件,社区里也有人习惯叫它技能包。
它不搞发型,专门负责把家里的自动化规则“扎成一束”,解决自动化一多就互相打架、想整体启停却无从下手的问题。这篇就讲讲我这个插件当初为什么这么设计、安装怎么弄、核心三件套怎么配,以及我自己跑了两个月后在真实环境里踩过的一堆坑。适合手里已经攒了几十条 automation、开始觉得配置失控的人;刚入坑的看到也能少走弯路。
1. 先别急着扎头发:这个 ponytail 插件解决的其实是自动化失控问题
1.1 自动化失控的典型症状,你中了几个?
我在智能家居群里混了几年,发现大部分折腾 Home Assistant 的人都会在某个阶段遇到下面这些情况:
- 家里有几十条 automation,新增一个灯都不知道该改哪条规则,全靠全局搜索 configuration.yaml。
- 两条自动化互相触发,开灯关灯“鬼打墙”,日志刷屏也找不到起点。
- 出门模式反而把窗帘打开了,排查半天发现是另一条自动化响应了“门锁状态变化”。
- 想临时禁用某组逻辑,只能一条条手动关闭,关完还经常忘记恢复。
这些问题的本质不是哪条自动化写错了,而是自动化之间缺少“分组封装”和“执行约束”。Home Assistant 里的 automation 本质上是一堆全局规则,规则之间没有容器去管理它们的关系。单个看都很正常,放在一起就像洗过的头发没有护理,风一吹就开始缠。
当时我就在想,能不能有一个东西,把一组高度相关的规则“扎成一束”,像扎马尾辫一样利落。然后我可以给这一束一个总开关,让它整体启用、整体禁用、一键触发。这就是 ponytail 最初的动机。
1.2 为什么叫 ponytail 而不是 task 或 scene
很多朋友问我:Home Assistant 本身就有 scene(场景)和 script(脚本),为什么要再造一个“技能包”?
其实我一开始也纠结过。scene 适合做临时状态快照,比如“看电影模式”一键调暗一屋子灯,它的核心是保存/恢复实体状态;script 则是按顺序执行一串动作,强调的是“顺序”。但我的需求比这两者都复杂一点:
- 我需要把多条 automation、script、实体状态收拢到一个逻辑组里管理。
- 我需要这个组有一个统一的条件闸门,条件不满足时整个组都不能跑。
- 我还需要这个组执行完之后自动做一轮收尾,而不是让现场状态一直悬在那。
scene 和 script 都做不到这三件事的完整组合。所以才有了 ponytail 的三件套设计:bundle、scrunchie、tie-off。散落的头发——也就是一条条自动化规则——需要先分股,再用发圈扎紧,最后处理发尾。名字就是这么来的,没有任何玄学。
1.3 为什么叫 skill 而不是叫 rule
这也是热搜词里出现“ponytail skill”的原因。我故意没有把每个配置单元叫 rule 或 automation,而是叫 skill(技能),是为了和 Home Assistant 原生的 automation 在语义上做区分。
automation 是“如果 A 那么 B”,本质是一条规则;skill 是“我要完成 N 件事”,本质是一个需求集合。举个例子,“我出门了”是一个需求,展开之后可能包括关灯、关空调、锁门、扫地机器人开始清扫、给手机推通知——这五件事如果全写成 automation,它们之间其实没有强绑定。而在 ponytail 里,它们属于同一个 skill,触发方式可以是传感器、时间、语音助手,也可以就是手动按一个按钮。语义清晰了,配置才不会变成一团乱麻。
2. 安装 ponytail 的两种姿势:HACS 仓库装和手动放目录
2.1 准备条件:先确认你的 HA 版本和目录结构
在动手之前,先确认三件事,免得装到一半发现问题:
- Home Assistant 版本建议在 2023.10 以上。我主要是在 2024.x 上测试的,旧版本也能跑,但个别特性(比如服务返回数据)会缺失。
- 你的 HA 配置目录里要有 custom_components 这个文件夹。如果你用 HAOS 安装,一般路径是 /config/custom_components;如果用的是 Docker 映射,就看你的挂载目录。
- 强烈建议先备份 config 目录,尤其是 .storage 文件夹和 configuration.yaml。装插件本身风险不大,但后面配置改起来免不了要反复重载,有备份心里不慌。
如果你还没装过任何自定义集成,先花十分钟把 HACS(Home Assistant Community Store)装上会省很多事。当然,不想用 HACS 也完全可以,我下面给手动方案。
2.2 方法一:HACS 自定义仓库安装
我的项目目前直接托管在 GitHub 上,HACS 添加自定义仓库的方式和其他集成大同小异。操作路径是:HACS → 右上角三个点 → 自定义存储库 → 粘贴仓库地址 → 类别选“集成” → 点击添加。
这里我要单独强调一个坑:类别一定不要选错。我自己在测试群被问得最多的就是“为什么仓库地址粘贴了但 HACS 里搜不到”。十有八九是类别选了“前端”或者“插件”,HACS 会按你给的类别去解析 manifest 文件,类别对不上就刷不出来。一定要选 Integration(集成)。
添加完成后,在 HACS 里搜索 ponytail,点击下载,然后重启 Home Assistant。重启完成后,进入“设置 → 设备与服务 → 添加集成”,搜索 ponytail 就能看到入口,按提示提交即可完成初始化。整个过程大概五分钟。
2.3 方法二:手动安装,适用于不想碰 HACS 的人
如果你有洁癖,不想在 HACS 里维护第三方仓库,或者你的环境装不了 HACS,那就手动拷贝。
去 GitHub 仓库把代码下载下来,解压后你会看到一个 custom_components 目录,里面是 ponytail 这个子目录。把整个 ponytail 子目录放到你 HA 配置目录下的 custom_components 里,最终路径应该是custom_components/ponytail/__init__.py,目录结构不能错位。
放好之后重启 HA,再在集成页面添加。手动安装最容易出问题的点有三个:
- 目录层级错位,比如把 ponytail 的上级目录一起拖进去了,导致 HA 识别不到 domain。
- 压缩包解压后多出一层嵌套目录,
custom_components/custom_components/ponytail,这种低级错误真的不少见。 - Windows 拷贝时混入隐藏文件,比如 desktop.ini,或者压缩包残留文件,有时会导致 manifest 解析失败。
我建议用 Samba 或 SFTP 方式传输,尽量避免从 Windows 直接拖拽到挂载目录。传完后可以先检查一下文件数量,__init__.py、manifest.json、services.yaml这几个必须都在。
2.4 安装完怎么确认已经加载成功
重启完成后,不要急着配置。打开“系统 → 日志”,看有没有类似这样的一行:
Loaded ponytail integration from custom_components.ponytail看到这行说明集成加载成功。如果没有,按下面的表格排查:
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| 日志里完全没有 ponytail 字样 | custom_components 目录路径不对 | 检查最终路径是否为custom_components/ponytail |
日志报Invalid manifest | manifest.json 格式被破坏 | 重新下载,确认 JSON 没有 BOM 头和多余逗号 |
| 集成页面搜不到 ponytail | 重启没有完成或集成已损坏 | 再重启一次,还不行就删掉目录重新放 |
| 加载成功但配置报错 | version 兼容问题 | 检查 manifest.json 里 requirements 字段,按日志提示处理 |
3. 核心配置三件套:bundle、scrunchie、tie-off 的用法解析
安装好之后,我们来看配置。ponytail 的配置入口是 configuration.yaml 里的ponytail:块。先给一个最小可用的示例,后面我会拆开解释:
ponytail: skills: leave_home: bundle: - automation.turn_off_lights - automation.lock_door scrunchie: - condition: state entity_id: binary_sensor.someone_home state: "off" tie_off: - service: script.cleanup_after_leave单看这段你可能觉得和 automation 差不多,别急,关键是这几个字段的执行语义和 automation 完全不同。
3.1 bundle:发束,装下这个技能要牵扯的所有实体与自动化
bundle 字面意思是“发束”,作用是把你这个技能要牵扯到的所有东西收拢在一起。它可以填实体 ID、服务名、自动化 ID,也可以是另一个 script 的 ID。
我实行的原则是:bundle 只负责“收拢”,不负责“排序”。bundle 里的多个动作会并发发起,它们之间没有严格的先后保证。如果你需要先开灯再开空调这种顺序逻辑,正确做法是把这个顺序抽成一个 script,再把 script 塞进 bundle,让 script 内部去处理顺序。等到 bundle 里的所有子项都执行完毕,技能才算完成。
我还会在 bundle 里给每个子项加一个 alias 标签,方便日志定位。比如:
bundle: - entity_id: light.living_room alias: 客厅灯 - entity_id: script.activate_ac alias: 打开空调这里提醒一个坑:bundle 里如果写了不存在的实体 ID,配置阶段不会报错,要等到触发时才在日志里报Entity not found。所以新增技能后,先在开发工具里手动调用一次,不要直接挂到自动化触发链上。
3.2 scrunchie:发圈,负责“什么条件下这个技能可以执行”
scrunchie 翻译过来是“发圈”。它是整个技能包的总闸门,作用等同于一堆 condition,但和 automation 里的 condition 有一个显著区别:它的管辖范围是整个技能,而不是单一触发。
举个例子,我家里有一个“下班回家”技能,触发条件我不能只写“门锁打开”这一条,因为白天有人回家、半夜有人回家、拿快递回家都算“门锁打开”。所以我把下面这些条件全部收进 scrunchie:
- 当前时间在 17:30 到 23:00 之间
- 家里没人
- 手机已经连接到书房 Wi-Fi(说明人真的回来了)
- 30 分钟内没有执行过这个技能
这些条件如果散落在不同 automation 里,后期维护非常容易漏。集中在 scrunchie 里的好处是,你打开一个技能包就能看到它完整的执行边界。
条件写法基本沿用 HA 自带的 condition 语法,支持state、numeric_state、template、time、and、or等。需要注意的是,任何一个条件失败,整个技能立即中止,bundle 里已经触发但还没完成的动作不会自动回滚。
scrunchie: - condition: template value_template: "{{ now().hour >= 17 and now().hour < 23 }}" - condition: state entity_id: binary_sensor.someone_home state: "off" - condition: template value_template: "{{ is_state('binary_sensor.phone_home', 'on') }}"3.3 tie-off:发尾固定,技能跑完之后必须做的收尾动作
tie-off 是我认为这个插件里最实用、也最容易被新手忽略的模块。它的职责是:技能执行完毕之后,做一次“发尾固定”,保证现场状态不悬空。
比如说,一个“出门”技能执行完后,我通常需要:
- 推送一条通知到手机,确认执行结果。
- 把中间状态实体(比如临时关闭的某个二进制传感器)恢复原状。
- 执行一次“异常清理”脚本,把可能误触发的下一段逻辑锁住。
这些动作写在 tie_off 里,意味着它们只会在技能正常结束或超时结束时执行。如果你通过ponytail.cancel_skill中途取消技能,tie-off 默认不会执行。这个默认行为我在后面踩坑部分会细说,因为我的空调就因为这个习惯被冻了一晚。
tie_off: - service: notify.mobile_app_phone data: title: 出门技能 message: 已执行完毕 - service: script.reset_door_sensor3.4 服务接口和前端卡片
插件核心提供两个服务:ponytail.run_skill和ponytail.cancel_skill。重点参数是skill_name,也就是你在配置里定义的技能名。run_skill还支持bundle_overrides参数,可以在运行时临时覆盖 bundle 里的部分动作,这个我会在后面实际场景里讲。
前端方面,我配套写了一个 Lovelace 卡片,叫 ponytail-card。如果你通过 HACS 安装,可以在前端类别里额外安装这个卡片。卡片能展示每个技能包的当前状态、最近运行时间、下次可触发时间,并提供一个手动触发按钮。这个卡片不是必须装的,但调试技能时非常直观,我建议装上。
4. 一条完整的“下班回家”技能是怎么搭出来的
4.1 需求拆解
第 3 章的语法看得再多,不如跟着一条真实需求跑通一遍。我拿家里最常用的一条“下班回家”技能来说。
需求看起来很简单:回家之后玄关灯亮、空调打开、热水器启动、客厅音乐播起来、阳台窗帘拉上。但真实生活里的约束条件远不止这些:
- 只有 17:30 之后打开家门才执行,避免白天在家办公时误触发。
- 如果手机定位一直没离开家附近,说明人根本没从外面回来,门锁可能是误报,不执行。
- 执行过一次以后,如果家里继续保持有人状态,半小时内不要重复执行。
- 所有动作执行完以后,往手机推一条“回家安排已完成”的通知。
这条需求如果用原生的 automation 写,condition 会堆到二三十行,而且后续每加一个约束都要在自动化列表里翻半天。放到 ponytail 里,逻辑就清晰很多。
4.2 完整配置示例
我直接在 configuration.yaml 里写了一个go_home技能,配置如下:
ponytail: skills: go_home: trigger_type: state trigger_entity: binary_sensor.front_door trigger_to_state: "on" scrunchie: - condition: template value_template: "{{ now().hour >= 17 and now().hour <= 23 }}" - condition: state entity_id: binary_sensor.someone_home state: "off" - condition: template value_template: "{{ as_timestamp(now()) - as_timestamp(states('automation.last_go_home_kickoff')) > 1800 }}" bundle: - script: scène_home_door_light alias: 玄关灯 - script: script.ac_on_arrive alias: 空调按季节启动 - script: water_heater_on alias: 热水器开启 - script: media_home_play alias: 客厅音乐 - service: cover.open_cover entity_id: cover.living_room_curtain alias: 客厅窗帘 tie_off: - service: notify.mobile_app_phone data: title: 回家技能 message: 所有回家动作已按计划执行讲几个容易看漏的细节:
trigger_type、trigger_entity、trigger_to_state这三个字段是技能的核心触发条件。你也可以完全不写 trigger,把技能只交给语音助手或按钮手动触发。- 我这里把玄关灯、空调、热水器、音乐都抽成了 script,原因就是前面说的:它们之间有季节判断、门窗判断等逻辑,不适合写成散装服务。
- 防重复执行的第三段条件,借用了自动化状态的更新时间戳来计算冷却时间。如果你有更好的定时器实体,可以直接用定时器替代。
4.3 接入语音助手和实体按钮
技能配置好后,第二步是接入真实的触发入口。我在实际使用中最常用的三种触发方式:
- 语音助手。我把“我回来了”这句自定义唤醒词绑到了
ponytail.run_skill服务上,参数skill_name填go_home。现在进门只要喊一声,整个技能就跑起来了。这里要注意,使用 custom sentence 或者 Alexa 技能时,服务名和参数都需要写成全限定格式,避免被语音平台缓存问题坑到。 - 实体按钮。玄关放了一个无线智能按钮,我把按钮的单击动作通过一条极简 automation 接到服务调用上。注意:按钮触发最好走 automation,不要直接写进 bundle,因为按钮需要脱手操作,状态反馈也依赖 automation 的追踪。
- 前端卡片兜底。手机没电、语音助手离线时,直接在仪表盘点 ponytail-card 上的“执行”按钮。这个方式一定要留,智能家居总会有一天所有自动触发方式都失灵,一个手动入口能救命。
5. 跑了两个月后在真实环境里踩到的坑和我的修复方案
5.1 坑一:并行 bundle 里的“先后依赖”被想当然
我第一次把“下班回家”技能的全部动作一股脑写进 bundle,结果发现经常是客厅音乐比玄关灯先响。表面上看 Home Assistant 的服务调用是按顺序发出的,但多个 service 同时进入内部调度器后,执行顺序并没有严格保证。
修复方式很简单,把有依赖关系的动作改写成 script,塞进 bundle。现在我配置里的习惯是:bundle 里绝大多数项目都是 script 引用,而不是散装服务。只有像“打开窗帘”这种独立无依赖的动作,才直接填service+entity_id。
5.2 坑二:scrunchie 的求值时机和 automation 的 condition 不一样
automation 的 condition 在事件触发后立即求值,值决定这条自动化是否继续跑。而 ponytail 的技能包,scrunchie是在服务被调用那一刻求值,并且只在那一刻求值。技能一旦跑起来,中途实体状态发生变化,已经执行的动作不会回滚。
有一次我家里门铃技能设置了一个“只有家里有人才播放声音”的 scrunchie 条件,结果触发后 3 秒内我老婆用 App 关了“有人”状态,声音照样播了。这个不算 bug,是设计取舍。我的对策是:在真正关键的分支动作前,再加一层条件判断,不完全依赖 scrunchie。比如播放声音前,让 script 自己检查一次传感器状态。
5.3 坑三:cancel_skill 不执行 tie-off,导致状态卡死
这是我最惨痛的一个坑。某个晚上我出门时误触发了“离家”技能,后来发现不用出门,我直接在 App 上调用了ponytail.cancel_skill。按照默认行为,技能被取消后 tie-off 不会执行,结果就是空调压缩机因为缺少复位动作,一晚上没有正常恢复待机。
那晚之后我在插件版本里给cancel_skill加了一个force_tieoff参数,默认关,但可以在调用时显式传true。如果你不想升级,也有一个简单的兜底办法:在技能内部把“复位”动作拆成独立的 script,让另一个无论什么情况都会执行的自动化去调用它,相当于自己实现了一个 finally 块。
5.4 坑四:HACS 升级后配置突然失效
我遇到过升级插件后,部分技能配置报错的问题。原因是版本更新里我把默认参数改名了,比如 bundle 子项的alias字段被统一改成了display_name,旧配置没有跟上。这种事情在自维护插件里太常见了。
我建议每次升级前,先把 skills 配置导出留底;升级后马上在开发者工具的服务页面跑一遍我这里内置的校验服务ponytail.validate_skills,它会逐项检查语法和实体存在性,比看日志高效得多。不夸张地说,这个校验服务帮我省了很多次半夜排查时间。
5.5 坑五:技能包实体太多,数据库和启动速度都被拖累
如果你家里技能包建得很多,而且每个技能都默认注册了实体作为状态展示,时间长了你会发现实体数量暴涨。Home Assistant 的启动速度和 recorder 数据库占用都会受影响,尤其是跑在树莓派上的朋友,感觉会非常明显。
我的建议:把不常用的技能包实体通过ponytail配置里的register_entities选项关掉,只保留真正需要在卡片上展示的那几个。按我的经验,每 50 条 automation 对应 8 到 10 个展示实体比较合理,再多就是负担了。
我现在家里一共跑了十几个 ponytail 技能包。最明显的好处不是“自动化跑得更快”,而是“我敢随手改配置了”。以前改一条自动化要担心连锁反应,现在只要把改动圈在某个 skill 内部,验证一下 scrunchie 和 tie-off 就好。如果你家里自动化也开始失控,不妨把它们像扎马尾一样,先分股,再扎紧,最后固定好发尾。
最后分享一个小技巧:给技能包命名尽量用英文短横线风格,比如go_home_bundle、good_night_routine,不要用中文,也尽量不要带空格。因为跑日志和语音调用的时候,特殊字符容易造成各种诡异问题。我吃过命名不规范的亏,现在所有新增技能都强制走这个规范。你如果也有更好的“扎头发”姿势,欢迎交流。