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 command

A 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

CallReturnsSource
isRaining()booleanA rain-sensor peripheral if you have one, otherwise the meteo precipitation reading against a threshold.
rainAmount(minutes)mm, one decimalAccumulated precipitation over the lookback window.
isDay() / isNight()booleanThe sunrise/sunset window reported by the weather integration.
isEvening()booleanLegacy time-of-day predicate.
currentExternTemperature()°C, integer, or nullThe average of the TEMP peripherals in the exterior zone; falls back to the meteo temperature.

Actions

CallDoes
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.

Scenario scripts are trusted input.

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.

FieldHolds
p0Event topic name — EVT_LIGHT, EVT_GATE, evt_switch, evt_sprinkler, …
p1Entity type: PERIPHERAL, PORT, ZONE, DEVICE.
p2Entity id.
p3Source description.
p4The action or value.
p5Additional metadata, JSON.
p6Who did it — a username, or shared:<token> for a guest link.
One execution path for everything.

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.

JobWhat it does
Device controller state syncPolls controllers that support HTTP for their real state and reconciles the database against it. This is what raises “device offline” messages.
Port value sync triggerAsks devices to re-publish their port values, so a client that missed an MQTT message converges anyway.
Switch off on timeoutTurns off any peripheral past its key.on.timeout. The safety net behind “I left the pump running”.
Heating controlDrives thermostats from room temperature, setpoints and degree-minutes.
Config syncPulls the configuration repository so a commit takes effect without a restart.
Event log readerDrains the event log queue into persistent storage.
Statistics rollupsAggregates port values into hourly, daily and monthly series.
Integration syncsHuawei, NIBE, Navimow and Open-Meteo polling, plus the OAuth token refreshes each of them needs.
RGB effectsRandom 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.

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]])
}
Prefer the auto-off timeout to a long 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.