Docs
Examples

Scene scheduler

A parent app that hosts any number of child "run this scene at this time" rules, and hears back from them.

Two definitions, registered separately. Scene Scheduler is installed once and shows a list of its children with an Add new button; each Scheduled Scene child runs one scene at one time on chosen days. The children tell the parent when they ran with a location event, so the parent can keep a tally without any shared state.

What it shows

  • The parent / childApps relationship and the app input that renders the children. See Parent and child apps.
  • getChildApps() and runSmartApp() from the parent; runDaily and an enum with multiple in the child.
  • Location events as the back-channel between instances. See Location events.

Declaring the relationship

The link is metadata on the registered definitions, matched by namespace + name:

Scene Scheduler:  childApps: [{ namespace: 'com.example', name: 'Scheduled Scene' }], singleInstance: true
Scheduled Scene:  parent:    { namespace: 'com.example', name: 'Scene Scheduler' }

Register the child first so the parent's childRef resolves when its form is rendered.

The parent

scene-scheduler.js
const definition = {
  name: 'Scene Scheduler',
  namespace: 'com.example',
  description: 'Hosts any number of scheduled scenes.',
  category: 'automation',
};

const preferences = {
  pages: [{
    id: 'main',
    title: 'Scene Scheduler',
    inputs: [
      { key: 'schedules', label: 'Scheduled scenes', type: 'app', multiple: true,
        childRef: { namespace: 'com.example', name: 'Scheduled Scene' } },
      { key: 'logging', label: 'Log each run', type: 'boolean', default: false },
    ],
  }],
};

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

function initialize() {
  subscribeToLocationEvent('com.example.scheduledScene.ran', onChildRan);
  console.log(`${getChildApps().length} scheduled scene(s)`);
}

function onChildRan(evt) {
  state.runs = (state.runs ?? 0) + 1;
  state.lastRun = evt.value;                         // { appId, scene, at } from the child
  if (logging === true) console.log('Scene ran:', JSON.stringify(evt.value));
}

function run() {                                     // Run on the parent runs every child now
  for (const child of getChildApps()) {
    const result = runSmartApp(child.appId);
    if (!result.ok) console.warn(`Could not run ${child.appName}: ${result.reason}`);
  }
}

The child

scheduled-scene.js
const definition = {
  name: 'Scheduled Scene',
  namespace: 'com.example',
  description: 'Runs one scene at a time of day on chosen days.',
  category: 'automation',
};

const preferences = {
  pages: [{
    id: 'main',
    title: 'Scheduled Scene',
    inputs: [
      { key: 'scene', label: 'Scene', type: 'entity', domainFilter: 'scene', required: true },
      { key: 'time', label: 'Run at', type: 'time', required: true },
      { key: 'days', label: 'Days', type: 'enum', multiple: true, description: 'Leave empty for every day.',
        options: [
          { value: 'Mon', label: 'Monday' }, { value: 'Tue', label: 'Tuesday' }, { value: 'Wed', label: 'Wednesday' },
          { value: 'Thu', label: 'Thursday' }, { value: 'Fri', label: 'Friday' }, { value: 'Sat', label: 'Saturday' },
          { value: 'Sun', label: 'Sunday' },
        ] },
    ],
  }],
};

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

function initialize() {
  if (typeof time !== 'string') return;
  runDaily(time, onSchedule);                        // home time zone; persists across reboots
}

function onSchedule() {
  const today = formatDateParts(new Date(), getHomeTimeZone()).weekday;   // 'Mon' … 'Sun'
  const picked = Array.isArray(days) ? days : typeof days === 'string' && days ? [days] : [];
  if (picked.length > 0 && !picked.includes(today)) return;
  activate();
}

function run() {                                     // the parent's runSmartApp and this child's own Run button
  activate();
}

function activate() {
  scene.turnOn();
  state.lastRunAt = new Date().toISOString();
  const me = getParent();                            // null if someone installed the child on its own
  sendLocationEvent('com.example.scheduledScene.ran', { appId: me ? me.appId : null, scene: scene.entityId, at: state.lastRunAt });
}

How it works

  • The parent has almost no behaviour. Its app input is a live view of the installed children (not stored in its settings and not hydrated), so the children are the only thing it needs to show.
  • Each child is an ordinary app with its own settings, state and schedule. Installing a new one from the parent's form sets parentAppId; uninstalling the parent uninstalls the children first.
  • Instances do not share state. getChildApps() returns identity handles only. The child publishes what the parent wants to know with sendLocationEvent, and the parent subscribes by name. Any other app on the hub could subscribe too; namespace the event name.
  • run is reused. runSmartApp(appId) from the parent fires the child's run(), the same entry point as the child's own Run button, so both skip the day check deliberately.

Try changing

  • Pause a child from the parent (pauseSmartApp(child.appId, 3600)) for a one-hour snooze.
  • Give the child a second scene to run at a second time, or replace the time with scheduleAtSunset and re-arm it as in Porch light.

On this page