Docs

Scheduling

Timers, daily and cron schedules, sunrise and sunset, and the time-zone rules they follow.

You wantUseNotes
run something in N secondsrunIn(seconds, handler, data?, options?)re-arming the same handler replaces its pending timer
run once at an instantrunOnce(isoString, handler)silently ignored if the instant is past
every day at a timerunDaily('HH:MM', handler)home time zone
a cron expressionschedule('0 22 * * 1-5', handler)5 fields, optional leading seconds field; home time zone
at the next sunrise / sunsetscheduleAtSunrise(handler, offsetMinutes?) / scheduleAtSunset(...)one-shot; re-arm in the handler
cancelunschedule(handler?)no argument cancels everything this instance scheduled

All of them are persisted by the hub and re-armed after a reboot; a timer that came due while the hub was off fires once on boot. Handlers are top-level named functions, as everywhere else. Signatures and examples are on the Scheduling reference.

Inactivity timers

Because runIn replaces the pending timer for the same handler, the classic "off after no motion for N minutes" needs no bookkeeping:

function onMotionCleared() { runIn(offAfterMinutes * 60, turnOff); }   // each clear pushes the deadline out
function onMotion()        { unschedule(turnOff); }                    // motion resumed: cancel

Pass { overwrite: false } to stack timers instead, and data to tell them apart:

runIn(300, release, { token: evt.entityId }, { overwrite: false });
function release(data) { console.log('releasing', data.token); }

Daily sun-anchored behaviour

scheduleAtSunrise / scheduleAtSunset arm one firing at the next occurrence (plus an offset in minutes; negative is before). For something that should happen every evening, re-arm as the last step of the handler, and again on systemStart in case a reboot dropped the pending one:

function initialize() {
  subscribeToLocationEvent('systemStart', armSunset);
  armSunset();
}
function armSunset() { scheduleAtSunset(onSunset, -30); }
function onSunset() {
  porch.turnOn();
  armSunset();
}

To test whether it is currently dark, instead of firing at the boundary, use getSunTimes() and compare with the home's clock (below). available: false means the hub has no location yet.

Time zones

Cron and daily schedules fire in the home's time zone (getHomeTimeZone(), taken from the hub's address; UTC until one is set). The isolate's own Date runs on the hub process clock, usually UTC, so never use getHours(), getDay() or toLocaleString() for wall-clock logic:

const now = formatDateParts(new Date(), getHomeTimeZone());   // { year, month, day, hour, minute, second, weekday }
const minutes = Number(now.hour) * 60 + Number(now.minute);
const sun = getSunTimes();
if (sun.available) {
  const set = formatDateParts(new Date(sun.sunsetIso), getHomeTimeZone());
  const sunsetMinutes = Number(set.hour) * 60 + Number(set.minute);
  if (minutes >= sunsetMinutes) { /* after sunset today */ }
}

Date.now() and getTime() are zone-independent and fine for durations.

Cron format

Standard five fields: minute hour day-of-month month day-of-week, evaluated in the home's zone. A sixth leading field for seconds is accepted. Examples: 0 7 * * 1-5 (07:00 weekdays), */15 * * * * (every 15 minutes), 0 0 1 * * (midnight on the 1st).

Cleaning up

Call unschedule() in updated before re-initialising, so a changed time does not leave the old schedule firing. unschedule(handler) is precise enough to cancel one pending runIn without touching daily schedules.

On this page