Examples
Door left open
Notify chosen people when a door or window stays open too long, keep reminding, and say when it closes.
Several contact sensors, one app. Each sensor that opens gets its own timer; if it is still open when the timer fires, the people the installer picked get a push notification.
What it shows
- One timer per entity with
runIn(..., data, { overwrite: false }), so sensors do not cancel each other. See Inactivity timers. - Letting the installer choose recipients with an
accountUserIdinput read fromsettings. See Choosing recipients. - Per-entity bookkeeping in
statekeyed by entity id. See State › Patterns. - Friendly labels (
'Open','Closed') that mean the right raw token for door and window sensors.
const definition = {
name: 'Door Left Open',
namespace: 'com.example',
description: 'Notifies chosen people when a door or window stays open too long, and again when it closes.',
category: 'security',
};
const preferences = {
pages: [{
id: 'main',
title: 'Door Left Open',
inputs: [
{ key: 'openings', label: 'Doors and windows', type: 'entity', domainFilter: 'binary_sensor',
multiple: true, required: true, description: 'Contact sensors (device class door or window).' },
{ key: 'afterMinutes', label: 'Notify after open for (minutes)', type: 'number', default: 10, minValue: 1 },
{ key: 'recipients', label: 'Notify', type: 'accountUserId', multiple: true,
description: 'Leave empty to notify everyone on the account.' },
{ key: 'critical', label: 'Time-sensitive push', type: 'boolean', default: false },
{ key: 'repeat', label: 'Keep reminding while it stays open', type: 'boolean', default: false },
],
}],
};
function installed() { initialize(); }
function updated() { unsubscribe(); unschedule(); initialize(); }
function initialize() {
subscribe(openings, { to: 'Open' }, onOpened); // 'Open' is `on` for door and window sensors
subscribe(openings, { to: 'Closed' }, onClosed);
}
function onOpened(evt) {
state.openSince ??= {};
state.openSince[evt.entityId] = Date.now();
// One timer per sensor: `data` says which, overwrite:false keeps the other sensors' timers alive.
runIn(minutes() * 60, remind, { entityId: evt.entityId }, { overwrite: false });
}
function onClosed(evt) {
const since = state.openSince?.[evt.entityId];
if (state.openSince) delete state.openSince[evt.entityId];
if (since && Date.now() - since >= minutes() * 60 * 1000) {
notify(`${nameOf(evt.entityId)} is closed again.`); // only after we had alerted
}
}
function remind(data) {
const since = state.openSince?.[data.entityId];
if (!since) return; // closed before the timer fired: stale timer, ignore
if (new EntityWrapper(data.entityId).isNot('Open')) return; // belt and braces: check the live state
const openFor = Math.round((Date.now() - since) / 60000);
notify(`${nameOf(data.entityId)} has been open for ${openFor} minutes.`);
if (repeat === true) runIn(minutes() * 60, remind, { entityId: data.entityId }, { overwrite: false });
}
function notify(message) {
sendPushNotification(message, 'Door left open', critical === true, settings.recipients);
}
function nameOf(entityId) {
try { return String(new EntityWrapper(entityId).getAttribute('friendlyName')); } catch { return entityId; }
}
function minutes() {
return typeof afterMinutes === 'number' && afterMinutes > 0 ? afterMinutes : 10;
}How it works
- Stacked timers. By default
runInreplaces the pending timer for the same handler, which would make a second door cancel the first door's reminder.{ overwrite: false }stacks them, and thedataargument tellsremindwhich door it is about. - Stale timers are expected. A closed door does not cancel its timer (
unschedule(remind)would cancel every door's). Insteadremindchecksstate.openSinceand returns if the door closed in the meantime. Cheap, and it also makes the handler safe to run twice after a reboot. settings.recipients.accountUserIdinputs are not hydrated as globals;settingshas the raw array (orundefined, whichsendPushNotificationtreats as everyone).friendlyNameis read insidetry/catchbecausegetAttributethrows for an entity that no longer exists.
Try changing
- Only alert when the house is armed: check
panel.is('Armed Away')inremindwith anintrusion_alarm_panelinput. See Alarm webhook for the panel states. - Email instead of push: add
{ key: 'emailTo', type: 'accountUserEmail' }and callsendEmail(settings.emailTo, 'Door left open', message).