Automation & jobs
Scenarios are short Groovy scripts. Jobs decide when they run. Events decide what wakes them up.
The model
Scenario a script — what to do
▲
│ bound to
Job when to do it: cron trigger(s), event trigger(s), or manual
▲
│ fired by
CronTrigger a schedule
EventTrigger an event topic pattern
a human the Run button, a favourite, a voice commandA job carries its own status (ACTIVE / inactive), tags, and an execution history. Only
active jobs with a scenario are offered to the voice assistant.
The scenario DSL
A scenario is a Groovy closure evaluated with a composite delegate. Method calls resolve, in order,
against a read-only knowledge layer (predicates about the world), then an
action layer (things that change it), and finally against any Spring bean named
<name>Service that implements the command interface.
if (isEvening() && isNight()) {
switchOn([portIds: [123, 456]])
}
if (isRaining()) {
switchOff([peripheralIds: [42]])
}
if (currentExternTemperature() < 5) {
switchOn([peripheralIds: [77]]) // frost protection on the outside tap
}Predicates
| Call | Returns | Source |
|---|---|---|
isRaining() | boolean | A rain-sensor peripheral if you have one, otherwise the meteo precipitation reading against a threshold. |
rainAmount(minutes) | mm, one decimal | Accumulated precipitation over the lookback window. |
isDay() / isNight() | boolean | The sunrise/sunset window reported by the weather integration. |
isEvening() | boolean | Legacy time-of-day predicate. |
currentExternTemperature() | °C, integer, or null | The average of the TEMP peripherals in the exterior zone; falls back to the meteo temperature. |
Actions
| Call | Does |
|---|---|
switchOn([portIds: […], peripheralIds: […]]) | Turns ports on. Either key may be used, or both together; peripherals resolve to their connected ports. |
switchOff([...]) | The same, off. |
switchToggle([...]) | Inverts current state. |
pause(milliseconds) | Sleeps inside the script — useful for pulse sequences. |
mowerCommand([deviceId: …, action: 'DOCK']) | Commands the robotic mower: START, STOP, PAUSE, RESUME, DOCK. |
wakeup([...]) | Wake-on-LAN style trigger for devices that support it. |
Every state change made by a scenario is attributed in the audit log with source
SCENARIO and the actor that triggered it — CRON, EVENT, or the
real username when someone pressed Run.
They are authored by administrators through the UI and executed without a sandbox or a
method allowlist. Treat write access to scenarios as equivalent to shell access on the server, and
grant ROLE_ADMIN accordingly.
Adding your own action
The final fallback in the delegate chain resolves fooBar(...) to a Spring bean named
fooBarService implementing DslCommand. Dropping a new service into the
application is therefore enough to make it callable from every scenario — no DSL grammar to extend, no
parser to touch.
Triggers
Cron triggers
Standard Quartz cron expressions, evaluated in the server's timezone (UTC by convention). A job may carry several, which is how “weekdays at 06:30 and weekends at 09:00” is expressed.
Event triggers
A job can subscribe to an event topic instead of — or as well as — a schedule. When a matching event is published, the job's scenario runs. This is how device activity drives automation: a motion event from a camera, a gate opening, a button press on a controller.
The event system
Events are the seam everything hangs off. They are published to named topics and carry a fixed positional payload, which keeps the bus schema-free while remaining greppable.
| Field | Holds |
|---|---|
p0 | Event topic name — EVT_LIGHT, EVT_GATE, evt_switch, evt_sprinkler, … |
p1 | Entity type: PERIPHERAL, PORT, ZONE, DEVICE. |
p2 | Entity id. |
p3 | Source description. |
p4 | The action or value. |
p5 | Additional metadata, JSON. |
p6 | Who did it — a username, or shared:<token> for a guest link. |
The UI, scenarios, Telegram, the voice assistant and shared links all publish the same
evt_switch-family events. They are handled by one service, which resolves
PERIPHERAL / PORT / ZONE to concrete ports, applies the
action and writes the audit row. Adding a new control surface means publishing an event, not
reimplementing the control logic.
Built-in scheduled jobs
Alongside your own jobs, myHAB runs a fixed set of housekeeping and integration jobs on Quartz. Intervals and toggles are configuration, not code — see Configuration → Quartz job intervals.
| Job | What it does |
|---|---|
| Device controller state sync | Polls controllers that support HTTP for their real state and reconciles the database against it. This is what raises “device offline” messages. |
| Port value sync trigger | Asks devices to re-publish their port values, so a client that missed an MQTT message converges anyway. |
| Switch off on timeout | Turns off any peripheral past its key.on.timeout. The safety net behind “I left the pump running”. |
| Heating control | Drives thermostats from room temperature, setpoints and degree-minutes. |
| Config sync | Pulls the configuration repository so a commit takes effect without a restart. |
| Event log reader | Drains the event log queue into persistent storage. |
| Statistics rollups | Aggregates port values into hourly, daily and monthly series. |
| Integration syncs | Huawei, NIBE, Navimow and Open-Meteo polling, plus the OAuth token refreshes each of them needs. |
| RGB effects | Random colours and a rainbow sweep. Off by default. |
Heating automation
The heating job is the most opinionated piece of automation in the product, because heating is the one thing where naive on/off control costs real money.
- Setpoints are per zone, with a global fallback (
heat.temp.allDay). - Degree-minutes from the heat pump are used to decide whether the system is keeping up, rather than reacting to instantaneous temperature alone.
- Heat modes — economy, comfort, vacation — shift the setpoints without editing every zone.
- Weather forecast data is available to scenarios, so predictive strategies are possible.
- The whole job can be disabled if you would rather let the heat pump's own controller run the show.
Irrigation
The sprinkler scheduler is a front end over ordinary jobs: each row is a job with a cron trigger and a scenario that switches a sprinkler peripheral on for a duration. That means irrigation schedules appear in job execution history like everything else, and a scenario can override them — skipping a cycle after rain, for example:
// don't water if more than 3 mm fell in the last 12 hours
if (rainAmount(720) < 3.0) {
switchOn([peripheralIds: [501]])
pause(90 * 60 * 1000)
switchOff([peripheralIds: [501]])
}pause().
A scenario that sleeps for ninety minutes holds a thread and does not survive a restart. Setting
key.on.timeout on the peripheral lets the switch-off job handle it, and it still works
if the application is restarted mid-cycle.
Execution history
Every scheduled run is recorded with its start time, status and any error. Combined with the per-peripheral event log — which records who changed what, from which surface, and from which address — you can reconstruct exactly why a light is on.