Examples
Motion light by time of day
Motion turns on different lights at different brightness depending on the time of day, from a list of windows the installer defines.
A richer motion light. Instead of one set of lights, the installer defines windows: at 06:00 the hall at 30 %, at 17:00 hall and kitchen at 100 %, at 22:00 just the hall at 10 %. On motion the app works out which window is active right now and turns on that window's lights.
What it shows
- An
arrayinput whose rows hold a time, a multi-entity field and a gated number. See Array inputs. - Entity fields inside rows arrive as ids, so the code builds
EntityWrappers itself. - Evaluating "which window is active" fresh on every event instead of scheduling anything, which keeps it reboot-proof.
- Several motion sensors with an off-timer that only starts once all of them are clear.
const definition = {
name: 'Motion Light by Time of Day',
namespace: 'com.example',
description: 'Motion turns on different lights at different brightness depending on the time of day.',
category: 'lighting',
};
const preferences = {
pages: [
{
id: 'sensors',
title: 'Motion',
inputs: [
{ key: 'motionSensors', label: 'Motion sensors', type: 'entity',
domainFilter: 'binary_sensor', deviceClassFilter: 'motion', multiple: true, required: true },
{ key: 'offAfterMinutes', label: 'Turn off after no motion for (minutes)', type: 'number', default: 5, minValue: 1 },
],
},
{
id: 'windows',
title: 'Time windows',
description: 'Each window starts at its time and lasts until the next one starts. Before the first window of the day, motion does nothing.',
inputs: [
{
key: 'windows',
label: 'Windows',
type: 'array',
minItems: 1,
maxItems: 6,
itemSummaryKeys: ['startTime', 'brightness'],
itemSchema: {
inputs: [
{ key: 'startTime', label: 'Starts at', type: 'time', required: true },
{ key: 'lights', label: 'Lights', type: 'entity', domainFilter: 'light', multiple: true, required: true },
{ key: 'brightness', label: 'Brightness (1–100)', type: 'number', minValue: 1, maxValue: 100,
requiresEntityFeature: { siblingKey: 'lights', domain: 'light', colorMode: 'brightness' } },
],
},
},
],
},
],
};
function installed() { initialize(); }
function updated() { unsubscribe(); unschedule(); initialize(); }
function initialize() {
subscribe(motionSensors, { to: 'Detected' }, onMotion);
subscribe(motionSensors, { to: 'Clear' }, onMotionCleared);
}
function onMotion() {
unschedule(lightsOff); // motion resumed: cancel a pending off
const win = activeWindow();
if (!win) return; // before the first window of the day
const params = typeof win.brightness === 'number' ? { brightnessPercent: win.brightness } : {};
const ids = idList(win.lights);
for (const id of ids) new EntityWrapper(id).turnOn(params);
state.activeLights = ids; // remember the set, so we turn off exactly these
}
function onMotionCleared() {
if (anyMotion()) return; // another sensor still sees someone
const minutes = typeof offAfterMinutes === 'number' && offAfterMinutes > 0 ? offAfterMinutes : 5;
runIn(minutes * 60, lightsOff);
}
function lightsOff() {
for (const id of state.activeLights ?? []) new EntityWrapper(id).turnOff();
state.activeLights = [];
}
function anyMotion() {
const sensors = Array.isArray(motionSensors) ? motionSensors : motionSensors ? [motionSensors] : [];
return sensors.some((s) => s.is('Detected'));
}
// The window whose start time most recently passed, in the home's clock.
function activeWindow() {
const now = formatDateParts(new Date(), getHomeTimeZone());
const nowMinutes = Number(now.hour) * 60 + Number(now.minute);
let best = null;
for (const row of windows ?? []) {
const start = toMinutes(row.startTime);
if (start === null || start > nowMinutes) continue;
if (!best || start > toMinutes(best.startTime)) best = row;
}
return best;
}
function toMinutes(value) {
if (typeof value !== 'string' || !/^\d{1,2}:\d{2}$/.test(value)) return null;
const [h, m] = value.split(':');
return Number(h) * 60 + Number(m);
}
// A `multiple` entity field inside a row is a list of ids; a single one is a string.
function idList(value) {
return Array.isArray(value) ? value : typeof value === 'string' ? [value] : [];
}How it works
- Rows are plain JSON.
windowshydrates to an array of{ startTime, lights, brightness }objects.lightsis a list of entity ids, not wrappers, hencenew EntityWrapper(id). - No schedules. The active window is computed when motion happens, so there is nothing to
re-arm after a reboot and no cron to tear down. Changing the windows only needs
updated. - Turn off what you turned on. The window may have changed by the time the off-timer fires, so
the ids that were switched on are kept in
state.activeLightsand those are what go off. - All clear means all sensors. A
Clearfrom one sensor only starts the timer if no other sensor still reportsDetected; a laterDetectedcancels it again.
Try changing
- Anchor a window to sunset instead of a clock time: add an
anchorenum per row and, forsunset, derive the start fromgetSunTimes().sunsetIsowithformatDateParts. See Time zones. - Add a
daysenum per row (multiple: true) and skip rows whose days do not includeformatDateParts(...).weekday.