Getting started
Write, register and run a motion-activated light in about forty lines.
You will build the same app the hub ships as Simple Motion Light: when a motion sensor reports motion, one or more lights turn on; when motion has been clear for a few minutes, they turn off.
Before you start
- A Novon hub with at least one motion sensor and one light paired to it.
- Access to the Novon admin console (to register code) and the Novon app (to install it).
The code
Describe the app
definition is what people see in the install picker. The hub identifies the app by
namespace + name, so pick a namespace you control.
const definition = {
name: 'Simple Motion Light',
namespace: 'com.example',
description: 'Turns lights on when motion is detected and off after motion stops for a set time.',
category: 'lighting',
};Ask for devices and options
preferences describes the install form. Every input's key becomes a variable you can use
directly in your code: motionSensor will be a device handle, lights one handle or an array of
them, offAfterMinutes a number.
const preferences = {
pages: [
{
id: 'main',
title: 'Simple Motion Light',
inputs: [
{
key: 'motionSensor',
label: 'Motion sensor',
type: 'entity',
domainFilter: 'binary_sensor',
deviceClassFilter: 'motion',
required: true,
},
{
key: 'lights',
label: 'Lights to control',
type: 'entity',
domainFilter: 'light',
multiple: true,
required: true,
},
{
key: 'offAfterMinutes',
label: 'Turn off after no motion for (minutes)',
type: 'number',
default: 1,
minValue: 1,
},
],
},
],
};Wire up the lifecycle
The hub calls installed once and updated every time someone changes the settings. Both set up
subscriptions through a shared initialize; updated tears the old ones down first so a swapped
sensor does not leave a stale subscription behind.
function installed() {
initialize();
}
function updated() {
unsubscribe(); // drop every subscription this instance made
unschedule(); // and every pending timer
initialize();
}
function initialize() {
subscribe(motionSensor, { to: 'on' }, onMotion);
subscribe(motionSensor, { to: 'off' }, onMotionCleared);
}'on' and 'off' are the sensor's raw tokens. You could also write the labels the app shows for a
motion sensor, { to: 'Detected' } and { to: 'Clear' }; see
Raw tokens and friendly labels.
React to motion
Handlers are plain top-level functions; the hub calls them by name. state is the only memory
that survives between calls, so the motion counter lives there.
function onMotion(evt) {
unschedule(turnLightsOff); // motion resumed: cancel a pending off
state.motionCount = (state.motionCount ?? 0) + 1;
state.lastMotionAt = new Date().toISOString();
console.log(`Motion #${state.motionCount} from ${evt.entityId}`);
lights.turnOn(); // one light or many: the call fans out
}
function onMotionCleared() {
const minutes = typeof offAfterMinutes === 'number' && offAfterMinutes > 0 ? offAfterMinutes : 1;
runIn(minutes * 60, turnLightsOff); // re-arming replaces the previous timer
}
function turnLightsOff() {
lights.turnOff();
}Register and install
In the Novon admin console open Smart apps → Register a smart app, paste the file and save.
If the hub rejects it, the message names the problem (an unknown subscription filter key, a banned
API such as setTimeout, or a file over the size limit). See Validation rules.
In the Novon app open Automations → Smart apps, add Simple Motion Light, and pick your sensor,
lights and delay. Saving the form runs installed.
Wave at the sensor. Open the instance to see the log lines from console.log, then stand still
and watch the lights go off after the delay.
What just happened
- Each handler ran in a fresh sandbox.
onMotionandturnLightsOffnever shared a variable; the counter persisted only because it lives instate. Read How smart apps run. runInreplaced its own timer. Every clear report re-armed the off-timer, so the lights switched off a fixed time after the last motion. See Scheduling.lights.turnOn()worked for one light or five. A multi-select input hydrates to a proxy that fans the call out. See Entities and services.- Changing the settings re-ran
updated. That is why it tears everything down first. See Lifecycle.
Next
- Add a brightness option and apply it with
lights.turnOn({ brightnessPercent })— the light capability lists the parameters. - Only run at night: compare
getSunTimes()withformatDateParts(new Date(), getHomeTimeZone())insideonMotion. See Scheduling › Time zones. - Prefer TypeScript? The same app with types is in TypeScript authoring.