夏天到了,晚上吹着空调睡觉,最怕两件事:一是吹一整宿,早上冻得鼻子发酸、电表也跑得欢;二是想定时关,可我家这台空调本身没有定时关机功能。
厂商的小程序里其实是有定时的——但你想想那个画面:人都躺平了,还得摸黑解锁手机、打开小程序、等它连上云、点进去设定时……困意全没了。我想要的是躺在床上,对着 Apple Home 或 Siri 拨一下,就告诉它“X 小时后关”,然后安心闭眼。
这篇文章记录我把这个“会睡觉的定时器”做出来的完整过程。它最终很简单,但中间撞了好几堵看不见的墙——而那些墙,恰恰是这篇东西最有价值的部分。如果你也在折腾 Home Assistant 接空调,这些坑能帮你少走两个晚上的弯路。
先看最终成果
在 Apple Home 里,多了一个叫「天窗空调定时」的滑块控件。
- 躺床上拖一下:拖到 30% = 3 小时后关,25% = 2.5 小时,0.5 小时一档;
- 想取消:拖回 0 / 关掉它;
- 还有一道保底:万一我设完忘了、或者压根没设就睡着了,到了 23:00 它会自己判断——空调还开着就再给 3 小时,凌晨自动关;
- 只管卧室那一台,白天开空调完全不受影响,绝不会无缘无故把你白天的空调关掉。
整条链路是这样的:
1
2
3
4
5
6
7
8
9
10
11
Apple Home / Siri
↓ 拖滑块 / 语音
HomeKit 桥(Home Assistant 自带)
↓
fan.skylight_ac_timer ← 一个“风扇壳”滑块(别急,下面解释为什么是风扇)
↓ 百分比 ÷ 10 = 小时
input_number.skylight_ac_hours ← 真正存“几小时”的地方
↓
timer.skylight_ac_manual ← 倒计时
↓ 到点
climate.turn_off → 卧室空调(本地局域网控制)
看着平平无奇对吧?但“为什么偏偏是一个风扇”“为什么不直接用空调自己的定时”——这两个问题背后,是整整一晚的探案。
需求先拆清楚
动手前我把脑子里的模糊愿望拆成了五条硬指标,这一步特别重要,能帮你判断每个方案“到底行不行”:
- 手动、任意时长:今晚想 2 小时,明晚可能 3 小时,得能自己定。
- 保底兜底:人是会忘事的。啥都没设就睡着了,也得有个安全网把它关掉。
- 只针对一间卧室:家里两台空调,另一台不许碰。
- 白天不误伤:白天开空调是正常使用,定时逻辑绝不能在白天把它关了。
- 躺着能操作:最好就在 Apple Home / Siri 里,不用再开第三个 App。
带着这五条,我们出发。
第一道弯:Home App 自己能定时吗?
最自然的想法:Apple Home 不是能做自动化吗,直接设个“X 小时后关”不就行了?
不行。 Apple Home 的自动化触发器只有那么几种(时间、配件状态、有人到家/离开、传感器),它没有“倒计时 / 延时 X 小时”这种原生控件。你能做的只有“每天固定几点关”——可我的时长每天不一样,固定时间直接出局。
iOS 的「快捷指令」里倒是有“等待”动作,但手机上的“等待”跑在你手机上,锁屏久了就断——拿来扛 2 小时极不靠谱。
结论:真正的倒计时得交给一台一直醒着的机器。我家正好有一台树莓派跑着 Home Assistant(HA),它就是干这个的最佳人选。于是问题变成:HA 怎么控这台空调、又怎么把控件递回 Apple Home。
第二道弯(探案时间):空调自己不就有定时吗?
这是我钻进去最久、也最长见识的一段。
我家这台是华凌(美的体系),在 HA 里通过 midea_ac_lan 这个本地局域网集成接入——也就是说 HA 直接在内网跟空调对话,不走云。那么,小程序里那个“定时关机”,到底是云端在帮我倒计时,还是空调自己在数?
我一度想当然地说“云端”。但转念一想不对:厂商要是给千千万万台空调在云上各跑一个倒计时,这成本和逻辑都说不通。倒计时几乎一定是写进空调本机、由它固件自己数的,小程序只是把“N 分钟后关”这条指令下发下去而已。
如果是这样,那这条本地指令能不能我自己发?我把 midea-local 库的代码扒开看了。空调的设置命令是协议里的 0x40 包,我直接去读它构造命令体的那段:
1
2
3
4
5
6
7
8
9
10
11
# midealocal/devices/ac/message.py · MessageGeneralSet._body
return bytearray([
power | prompt_tone,
mode | target_temperature,
fan_speed,
0x00, # ← 这几个写死的 0x00……
0x00, # ← 正是 Midea 协议里 on-timer / off-timer 的槽位
0x00,
swing_mode,
...
])
真相大白:协议槽位就在那儿(那几个 0x00 正是定时字节),但这个本地库根本没去填它。再去翻状态回包的解析(XC0MessageBody),它读了三十来个字段——温度、模式、睡眠、防冻……唯独不读 timer。全库唯一的 timer 只是子协议里的一个布尔“能力标志”,不是时长。
所以结论很清醒:
- 我的直觉对了——空调固件确实支持本地定时,不是云端逻辑;
- 但这个开源本地集成没把这功能接出来;
- 想用,就得魔改这个库(改命令编码、加可写实体),代价是每次更新被覆盖、还得为你这具体型号把字节编码试准;
- 而且最关键:就算接通了,HomeKit 那头照样没有“时长控件”可用,UI 问题一点没解决。
💡 给同路人的提醒:遇到“某功能 App 能用、集成里没有”,先去读集成/底层库的源码,往往能在十分钟内判断“是没接出来”还是“协议不支持”。这一步省下的是你瞎试的好几个晚上。
于是我果断收手:别碰库。倒计时放 HA 自己做就好——一个 timer 助手足够稳,连重启都能续上(restore: true)。“倒计时跑在哪”其实根本不影响体验,真正没解决的是下一关。
第三道弯(翻车现场):HomeKit 的三道墙
需求第 5 条是“躺着在 Home 里操作”。我想要一个像调空调温度那样、能 +/- 连续拨的控件,一眼看到“几小时”。听起来不难吧?Apple 用三堵墙教我做人。
第一堵:HomeKit 没有“数字 / 时长滑块”。 我翻了 HA 的 HomeKit 桥源码,它支持的控件类型就这些:fans / lights / covers / humidifiers / thermostats / locks / switches / sensors…——没有 number。你的 input_number 根本递不过去。
第二堵:能连续滑的,只有两种单位。 翻下来,Apple 原生 Home 里能渲染成“连续滑块/表盘”的就两类:
| 控件 | 来源 | 显示单位 |
|---|---|---|
| 灯亮度 / 风扇转速 / 窗帘 / 加湿器 | 百分比特征 | 锁死 % |
| 恒温器 | 温度特征 | 锁死 ° |
单位是协议特征写死的,Apple 不让你把 ° 改成“小时”。(第三方 HomeKit App 如 Eve 能显示自定义单位,但用苹果原生 Home 就这两种。)
那……恒温器至少数字是真的啊!我设 2.5,它就显示 2.5,只是旁边带个 °,能忍。于是我真的用 generic_thermostat 造了个假恒温器,把“温度”当“小时”,范围声明成 0–8、步进 0.5。
第三堵,也是压垮这个方案的那根稻草:Apple Home 给恒温器目标温度设了一个 ~7° 的硬地板。 我在 HA 里明明写了 min_temp: 0,可手机上那个表盘只能拨 7.0 / 7.5 / 8.0 三档——0 到 7 全被 Apple 吞了。总不能让人“最少定 7 小时”吧?这个方案当场报废。
🧱 一句话记住:在 Apple 原生 Home 里,想要全程连续的控件,要么接受“显示是 %”,要么接受“恒温器最低 7°”。鱼与熊掌,real-number 和 full-range 不可兼得。
最终方案:用一个“风扇”收尾
撞完三堵墙,路反而清楚了:既然“显示真实小时数”做不到,那就退一步选全程不卡的百分比滑块,把“是 %”这个瑕疵用一个好记的映射抹平。
我选了风扇(也可以用灯,风扇胜在能设“档数”、好对齐):
- 设
speed_count: 20,于是滑块有 20 档,每档 5% = 0.5 小时,最高 100% = 10 小时,拖动时会卡档,半小时精度轻松对准; - 映射做成最好心算的:百分比 ÷ 10 = 小时。30% → 3 小时,25% → 2.5 小时;
- 这个“风扇”其实是个空壳,真正存小时数的是背后的
input_number,风扇只是它递给 HomeKit 的外衣。
整套配置如下,HA 用户可以直接抄(我用 packages 的方式把一整块功能塞进一个文件,方便整体回滚)。你只需要把那个真·空调实体 climate.197912093838856_climate 换成你自己的,其余照搬即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# packages/skylight_ac_timer.yaml
# 卧室空调·睡眠定时关
# Apple Home 里一个“风扇壳滑块” fan.skylight_ac_timer:
# 百分比 ÷ 10 = 小时(30%→3h,25%→2.5h),20 档、每档 0.5h、最高 10h,拖到 0 / 关闭 = 取消。
# 背后 input_number.skylight_ac_hours 存小时数;倒计时到点关空调并复位 0。
# 保底:23:00 判断一次,空调还开着且没设定时 → 起 3 小时(→02:00 关)。白天开空调不进此逻辑。
input_number:
skylight_ac_hours:
name: 卧室空调定时小时数
min: 0
max: 10
step: 0.5
mode: slider
icon: mdi:timer-outline
timer:
skylight_ac_manual:
name: 卧室空调手动倒计时
duration: "02:00:00"
restore: true
skylight_ac_fallback:
name: 卧室空调保底倒计时
duration: "03:00:00"
restore: true
# ⚠️ 用新版 template 语法(旧 `fan: platform: template` 将在 2026.6 移除)
template:
- fan:
- name: 卧室空调定时
default_entity_id: fan.skylight_ac_timer # 锁住英文 entity_id,HomeKit 桥引用更稳
state: "{{ 'on' if (states('input_number.skylight_ac_hours') | float(0) > 0) else 'off' }}"
percentage: "{{ (states('input_number.skylight_ac_hours') | float(0) * 10) | int }}"
speed_count: 20
turn_on:
- choose:
- conditions:
- condition: template
value_template: "{{ states('input_number.skylight_ac_hours') | float(0) < 0.5 }}"
sequence:
- action: input_number.set_value
target: { entity_id: input_number.skylight_ac_hours }
data: { value: 2 } # 直接点“开”不拖滑块 = 默认 2 小时
- action: timer.start
target: { entity_id: timer.skylight_ac_manual }
data: { duration: "02:00:00" }
turn_off:
- action: input_number.set_value
target: { entity_id: input_number.skylight_ac_hours }
data: { value: 0 }
- action: timer.cancel
target: { entity_id: timer.skylight_ac_manual }
set_percentage:
- variables:
hrs: "{{ ((percentage | float(0) / 10) * 2) | round(0) / 2 }}" # 折到最近的 0.5
- action: input_number.set_value
target: { entity_id: input_number.skylight_ac_hours }
data: { value: "{{ hrs }}" }
- choose:
- conditions:
- condition: template
value_template: "{{ hrs >= 0.5 }}"
sequence:
- action: timer.start
target: { entity_id: timer.skylight_ac_manual }
data:
duration: >
{% set s = (hrs * 3600) | int %}
{{ '%02d:%02d:00' % (s // 3600, (s % 3600) // 60) }}
default:
- action: timer.cancel
target: { entity_id: timer.skylight_ac_manual }
automation:
# 倒计时到点:关空调 + 复位小时数到 0
- id: skylight_ac_manual_finish
alias: 卧室空调·倒计时到→关机并复位
trigger:
- platform: event
event_type: timer.finished
event_data: { entity_id: timer.skylight_ac_manual }
action:
- service: climate.turn_off
target: { entity_id: climate.197912093838856_climate } # ← 换成你的空调
- service: input_number.set_value
target: { entity_id: input_number.skylight_ac_hours }
data: { value: 0 }
# 保底:23:00 定点判断
- id: skylight_ac_fallback_check_2300
alias: 卧室空调·23点判断→若开着且没设定时则起3小时保底
trigger:
- platform: time
at: "23:00:00"
condition:
- condition: template
value_template: >
{{ states('climate.197912093838856_climate') not in ['off','unavailable','unknown'] }}
- condition: state
entity_id: timer.skylight_ac_manual
state: idle
action:
- service: timer.start
target: { entity_id: timer.skylight_ac_fallback }
data: { duration: "03:00:00" }
- id: skylight_ac_fallback_disarm
alias: 卧室空调·关机→取消保底
trigger:
- platform: state
entity_id: climate.197912093838856_climate
to: "off"
action:
- service: timer.cancel
target: { entity_id: timer.skylight_ac_fallback }
- id: skylight_ac_fallback_finish
alias: 卧室空调·保底3小时到→关机
trigger:
- platform: event
event_type: timer.finished
event_data: { entity_id: timer.skylight_ac_fallback }
action:
- service: climate.turn_off
target: { entity_id: climate.197912093838856_climate }
几个设计点,值得单独说说
为什么不直接让风扇存小时数,要多一个 input_number? 因为模板风扇本身是“无状态”的外壳,它的百分比需要一个真实的地方存。input_number 就是那个“单一事实来源”:风扇读它显示、写它生效,倒计时到点把它清零,整条逻辑就闭环了,也不怕重启丢值。
保底为什么是“23:00 判断一次”,而不是“开机就起 3 小时”? 这正是需求第 4 条(白天不误伤)的关键。如果做成“一开机就 arm 3 小时”,那你白天开空调,3 小时后也会被自动关——这不是我们要的。改成只在 23:00 这个点判断一次:那时候空调还开着、又没设手动定时,才认为“这人是开着空调睡了”,给它兜底。白天的空调,从头到尾不进这套逻辑。
(小取舍:如果你 23:00 之后才进卧室开空调,那一晚保底不生效——这种情况手动拖一下滑块即可。我作息规律,基本卡得上,就接受了这个边界。)
手动定时优先于保底。 23:00 判断时有一条 timer.skylight_ac_manual == idle 的条件:只要你已经拖了滑块(手动倒计时在跑),保底就不插手;它俩还能自然叠加——手动更短会先关,关机那一刻又会触发“取消保底”,不会重复。
怎么把这个控件递进 Apple Home
HA 自带一个 HomeKit 桥集成,能把指定实体暴露成 Apple Home 里的配件。把上面的 fan.skylight_ac_timer 加进去就行:
设置 → 设备与服务 → 找到 HomeKit Bridge → 配置 → 在“要包含的实体”里勾上
fan.skylight_ac_timer→ 保存。
稍等片刻,Apple Home 里就会冒出一个「卧室空调定时」的风扇配件。是的,图标是个风扇——这是我们为了“全程连续滑块”付出的代价,你就当它是“把热气吹走的倒计时”好了 🌬️。拖它、或者对 Siri 说“把卧室空调定时设成 30%”,3 小时后空调自己就睡了。
临门一脚的小坑:旧版模板语法要迁移
做完没几天,HA 后台弹了个“修复”提示:旧的 fan: platform: template 写法将在 2026.6 移除,让我迁到新版 template: → fan: 语法。
好在 HA 的修复向导会直接生成迁移好的新配置,照着替换就行。上面那份完整配置我已经是迁移后的新语法了,你直接抄不会再碰到这个提醒。这里也顺手记一条经验:
🔧 定期看一眼 HA 的“修复(Repairs)”面板。很多弃用都会提前一两个版本预警,并附带自动生成的新配置——趁早处理,别等升级当天炸给你看。
踩坑速查表
把这一晚的弯路浓缩成一张表,方便你对号入座:
| 坑 | 真相 | 对策 |
|---|---|---|
| Home App 想直接定时 | 没有原生“延时/倒计时”控件 | 倒计时交给常醒的 HA(timer 助手) |
| 想用空调自带定时 | 固件支持,但本地集成没把它接出来(命令字节写死 0) | 别魔改库,HA 自己做倒计时即可 |
| 想要数字滑块 | HomeKit 桥没有 number 类型 |
借“风扇/灯”的百分比壳 |
| 想用恒温器显示真实小时 | Apple Home 把目标温度卡在 ~7° 地板 | 放弃“真实数字”,用 % 滑块 + 好记映射 |
| 百分比不直观 | 单位改不了 | 选 ÷10 这种心算友好的映射 + speed_count 卡档 |
| 旧模板语法弃用 | 2026.6 移除 fan: platform: template |
用 Repairs 向导迁到新版 template: |
写在最后
绕了一大圈,最后落地的东西特别朴素:躺床上拖一下滑块,空调就会在我设定的时间安静睡去;哪怕我先睡了,也有一道 23 点的安全网替我兜着。 一个不起眼的小工具,却实实在在地把“起夜关空调”这件小事从我的生活里抹掉了。
我很喜欢这种折腾。它表面是在跟空调较劲,本质是在跟那些“看不见的限制”打交道——Apple 的地板、协议里写死的零、即将被移除的旧语法。每撞一堵墙,你对这套系统的理解就厚一分。而最有意思的部分,往往不是最终那段能抄的配置,而是“为什么偏偏是一个风扇”背后的那一整晚。
愿你也能给自己的生活,装上一两个这样“会睡觉”的小开关。
—— 给你的生活加点阳光 ☀️