Examples
Fan auto-off
Turn a fan off a set number of minutes after it turns on, optionally only when a person switched it on.
An exhaust fan that someone switches on by hand and forgets. The app watches the fan, and whenever it turns on, arms a timer to turn it off again. Switching it off early cancels the timer; switching it off and on again restarts the countdown.
What it shows
runInreplaces its own pending timer, so there is no bookkeeping. See Scheduling.- Gating on
evt.newState.context.actorType === 'physical'to react only to the wall switch, not to other automations. See The event. - A
loggingpreference read at call time, so turning it on takes effect on the next event.
const definition = {
name: 'Fan Auto-off',
namespace: 'com.example',
description: 'Turns a fan off a set number of minutes after it turns on.',
category: 'climate',
};
const preferences = {
pages: [{
id: 'main',
title: 'Fan Auto-off',
inputs: [
{ key: 'fan', label: 'Fan', type: 'entity', domainFilter: 'fan', required: true },
{ key: 'offAfterMinutes', label: 'Turn off after (minutes)', type: 'number', default: 15, minValue: 1, maxValue: 240 },
{ key: 'manualOnly', label: 'Only when switched on by hand',
description: 'Ignore the fan being turned on by another automation or from the app.',
type: 'boolean', default: false },
{ key: 'logging', label: 'Enable logging', type: 'boolean', default: false },
],
}],
};
function installed() { initialize(); }
function updated() { unsubscribe(); unschedule(); initialize(); }
function initialize() {
if (!fan) { console.warn('No fan configured'); return; }
subscribe(fan, { to: 'on' }, onFanOn);
subscribe(fan, { to: 'off' }, onFanOff);
}
function onFanOn(evt) {
const actor = evt.newState.context?.actorType ?? 'system';
if (manualOnly === true && actor !== 'physical') {
logInfo(`Fan turned on by ${actor}; leaving it alone`);
return;
}
const minutes = typeof offAfterMinutes === 'number' && offAfterMinutes > 0 ? offAfterMinutes : 15;
state.lastOnAt = new Date().toISOString();
logInfo(`Fan on, off again in ${minutes} min`);
runIn(minutes * 60, turnFanOff); // re-arming replaces any pending timer
}
function onFanOff() {
unschedule(turnFanOff); // switched off early, by hand or by us: nothing left to do
}
function turnFanOff() {
logInfo('Auto-off elapsed');
fan.turnOff();
}
function logInfo(...args) {
if (logging === true) console.log(...args);
}How it works
- Two subscriptions, one timer.
{ to: 'on' }armsturnFanOff;{ to: 'off' }cancels it. When the timer itself turns the fan off, theoffevent runsonFanOffonce more, which cancels a timer that no longer exists. That is harmless, and simpler than tracking who turned it off. - Re-arming restarts the countdown. A second
onevent while the timer is pending callsrunInagain, and the hub replaces the pending timer with a fresh full-length one. actorTypeis'physical'when the device reported a change the hub did not command. Anything else ('user'from the app,'smart-app','system') is skipped when manual only is on.- The timer survives a reboot. If the hub restarts while the timer is pending it fires once on boot, so a fan is never left running because of a power blip.
Try changing
- Make it work for a
switchas well: add'switch'to a second entity input, or dropdomainFilterand let the installer pick anything withturnOff. - Replace the fixed delay with a humidity sensor: subscribe to the sensor with
{}and turn the fan off when the reading drops below a threshold, as in Temperature alert.