-
Notifications
You must be signed in to change notification settings - Fork 0
Timers
A timer is one-shot: do this once, in n minutes, then forget about it. "Switch off in 45 minutes" is the whole idea.
For anything recurring or time-of-day, you want Programs, schedules and curves.
curl -X POST http://server:8420/api/timers \
-H "X-API-Key: $KEY" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"unit_ids": ["153931628470980"], "minutes": 45}'That is deliberate, and it is the one design decision on this page worth understanding.
The server computes the moment from its own clock, and every response
carries seconds_remaining, computed server-side, so a client counts down
from that rather than from its own idea of the time. The alternative — a client
sending a wall-clock time — puts the burden of knowing the server's timezone on
every client, when the phone may be in another zone and its clock may simply be
wrong.
minutes has no default. "In zero minutes" and "you forgot to say when"
are different things, and a request without it is refused rather than guessed
at. It must be 1 to 1440 — a timer is a nap, not a calendar, and anything
longer belongs in a schedule. At most 64 units in one timer.
Nothing but switch the unit off, unless you say otherwise. settings defaults
to power_state: false.
But it is a full control payload, so "switch to eco in an hour" needs no second feature:
-d '{"unit_ids": ["1539…"], "minutes": 60,
"settings": {"eco": true, "target_temperature": 26}}'Anything Control schema accepts goes in there, and the bounds are checked when you create the timer, not when it fires. A timer that could never work is refused up front rather than failing silently in an hour.
unit_ids is a list. One timer can switch off the whole house, and it is one
object to cancel rather than four.
| Path | Does | |
|---|---|---|
GET |
/api/timers |
pending timers, each with seconds_remaining
|
POST |
/api/timers |
create one |
DELETE |
/api/timers/{id} |
cancel |
GET |
/api/timers/status |
is the runner alive, and when did it last look |
All four need full auth — API key and a device credential.
A stored timer looks like this:
created_at and fires_at are naive server-local strings — the same clock
the scheduler works in. They are there to be read by a human; a client should
use seconds_remaining and not parse them.
A single background thread wakes every AC_TIMER_TICK seconds (15 by default),
looks for anything due, and applies it.
Fifteen seconds is finer than the scheduler's thirty, on purpose: a schedule only has to land inside the right minute, but a timer is a promise about a moment. "Off in 45 minutes" landing 40 seconds late is a worse answer than a schedule firing at 07:00:20.
The timer goes through exactly the same code path as a control request from the panel — the same validation, the same per-unit lock, the same retry behaviour. A timer cannot do anything to a unit that you could not do yourself over HTTP.
They live in timers.json alongside your other state, so restarting the
service — or upgrading it — does not lose a pending timer.
A timer whose moment passed while the server was down fires on the next tick after it comes back, rather than being dropped. Worth knowing if a machine was off for an hour: "switch off in 20 minutes" will happen shortly after boot, which is usually what you wanted and occasionally a surprise.
Everything scheduled here uses the server's local clock. On a normal
install that is whatever the machine is set to. In a container it is UTC
unless you set TZ, and a timer that fires an hour or two off is almost
always that and not a bug — check utc_offset_seconds on GET /api/system.
| Symptom | Cause |
|---|---|
422 on create |
a missing or out-of-range minutes (1–1440), more than 64 units, or a settings value outside Control schema bounds |
404 on create |
an unknown unit id — ids are strings, and sending one as a JSON number silently rounds it |
| fires at the wrong time | server timezone; see above |
| never fires | check GET /api/timers/status for the runner's last pass |
| fired but nothing happened | the unit was unreachable. The attempt is real; the air conditioner declined |
Breeze Core · Breeze for Android · Packages · AGPL-3.0
Start here
Install it
- From packages (apt/dnf/…)
- With containers
- On Windows
- On the BSDs
- On OPNsense
- From source (any OS/init/libc)
Use it
Reference
Run it safely
Coming from an earlier version
Develop and port
{ "id": "9f3c1ab27d04", // 12 hex chars "unit_ids": ["153931628470980"], "minutes": 45, "created_at": "2026-09-09T14:02:11", // server-local, naive "fires_at": "2026-09-09T14:47:11", "settings": { "power_state": false }, "label": "" }