Docs
Examples

Morning routine

The first motion inside a morning window disarms the alarm and runs a scene, once per day.

"When someone gets up, disarm and start the morning scene, but only once, and not at 3 a.m." The interesting part is once per day: a date key stored in state is both the guard and the midnight reset, with no cron job at all.

What it shows

  • Time-of-day logic in the home's clock with formatDateParts(new Date(), getHomeTimeZone()). See Time zones.
  • A state field that resets itself when the date changes. See State.
  • Calling services on an alarm panel and a scene, checking with isNot() first.
  • A run() handler so the Run button tests the routine without consuming today's run.
morning-routine.js
const definition = {
  name: 'Morning Routine',
  namespace: 'com.example',
  description: 'The first motion inside the morning window disarms the alarm and runs a scene, once per day.',
  category: 'automation',
};

const preferences = {
  pages: [{
    id: 'main',
    title: 'Morning Routine',
    inputs: [
      { key: 'motionSensors', label: 'Motion sensors', type: 'entity',
        domainFilter: 'binary_sensor', deviceClassFilter: 'motion', multiple: true, required: true },
      { key: 'startTime', label: 'Window starts', type: 'time', default: '05:00', required: true },
      { key: 'endTime', label: 'Window ends', type: 'time', default: '10:00', required: true },
      { key: 'alarmPanel', label: 'Alarm panel to disarm', type: 'entity', domainFilter: 'intrusion_alarm_panel' },
      { key: 'scene', label: 'Scene to run', type: 'entity', domainFilter: 'scene', required: true },
      { key: 'weekdaysOnly', label: 'Weekdays only', type: 'boolean', default: false },
    ],
  }],
};

function installed() { initialize(); }
function updated() { unsubscribe(); unschedule(); initialize(); }

function initialize() {
  subscribe(motionSensors, { to: 'Detected' }, onMotion);   // one sensor or many: every member is subscribed
}

function onMotion(evt) {
  const now = formatDateParts(new Date(), getHomeTimeZone());
  const today = `${now.year}-${now.month}-${now.day}`;
  if (state.ranOn === today) return;                        // already ran today

  if (weekdaysOnly === true && (now.weekday === 'Sat' || now.weekday === 'Sun')) return;

  const minutes = Number(now.hour) * 60 + Number(now.minute);
  if (minutes < toMinutes(startTime, '05:00') || minutes > toMinutes(endTime, '10:00')) return;

  state.ranOn = today;                                      // mark first: a failing action must not re-trigger
  state.runCount = (state.runCount ?? 0) + 1;
  console.log(`Morning routine #${state.runCount}, triggered by ${evt.entityId}`);
  runRoutine();
}

function runRoutine() {
  if (alarmPanel && alarmPanel.isNot('Disarmed')) alarmPanel.disarm();
  scene.turnOn();
}

function run() {
  runRoutine();                                             // Run button: fire it now, keep today's run available
}

function toMinutes(value, fallback) {
  const text = typeof value === 'string' && /^\d{1,2}:\d{2}$/.test(value) ? value : fallback;
  const [h, m] = text.split(':');
  return Number(h) * 60 + Number(m);
}

How it works

  • The date key is the whole mechanism. state.ranOn holds the home-local date of the last run. Once the clock passes midnight the key no longer matches, so the guard clears on its own. It survives reboots because state is persisted.
  • Motion outside the window does nothing and sets nothing, so a later in-window motion can still run the routine that day.
  • Mark before acting. state.ranOn is written before the services are called. If the scene call throws, the hub logs it and the routine does not fire again every time someone walks past.
  • isNot('Disarmed') avoids a pointless disarm command and reads like the app shows it. Raw getState() !== 'DISARMED' would work too but is case-sensitive.

Try changing

  • Let the window cross midnight (22:00 to 06:00): when start > end, the check becomes minutes >= start || minutes <= end.
  • Announce it: add sendPushNotification('Good morning', 'Home', false, settings.recipients) with an accountUserId input, as in Door left open.

On this page