Examples
Alarm webhook
Post every alarm panel change to a webhook, including who caused it, with a secret kept out of the log.
Tell something outside the hub (a chat channel, a home server, IFTTT) when the alarm panel
changes. httpRequest is fire-and-forget: the hub performs the call off-thread and the app never
sees the response, which is exactly right for a webhook.
What it shows
httpRequestwith a JSON body and anAuthorizationheader, and what its return value does and does not tell you. See HTTP and egress.- Reading the cause of a change from
evt.newState.context(who armed it, from where). - A
passwordinput for the secret, and astringinput with a placeholder. - Comparing panel states with
matchesStatesoTRIGGEREDand'Triggered'both work.
const definition = {
name: 'Alarm Webhook',
namespace: 'com.example',
description: 'Posts alarm panel changes to a webhook, with who caused them.',
category: 'security',
};
const preferences = {
pages: [{
id: 'main',
title: 'Alarm Webhook',
inputs: [
{ key: 'panel', label: 'Alarm panel', type: 'entity', domainFilter: 'intrusion_alarm_panel', required: true },
{ key: 'url', label: 'Webhook URL', type: 'string', required: true, placeholder: 'https://hooks.example.com/…' },
{ key: 'token', label: 'Bearer token', type: 'password',
description: 'Sent as an Authorization header. Never written to the log.' },
{ key: 'alarmsOnly', label: 'Only send Pending and Triggered', type: 'boolean', default: false },
],
}],
};
function installed() { initialize(); }
function updated() { unsubscribe(); unschedule(); initialize(); }
function initialize() {
subscribe(panel, {}, onPanelChange); // every change; the handler decides what matters
}
function onPanelChange(evt) {
const from = evt.oldState.state;
const to = evt.newState.state;
if (from === to) return; // attribute-only change (battery, zones, …)
const isAlarm = matchesState(evt.entityId, to, 'Pending') || matchesState(evt.entityId, to, 'Triggered');
if (alarmsOnly === true && !isAlarm) return;
const ctx = evt.newState.context ?? {};
const payload = {
event: 'alarm.state',
entityId: evt.entityId,
from,
to,
actor: ctx.actorType ?? 'system', // 'user', 'alarm-panel-user', 'smart-app', 'physical', …
user: ctx.user ? `${ctx.user.firstName ?? ''} ${ctx.user.lastName ?? ''}`.trim() || undefined : undefined,
at: new Date().toISOString(),
};
const headers = { 'Content-Type': 'application/json' };
if (typeof token === 'string' && token) headers.Authorization = `Bearer ${token}`;
const result = httpRequest({ url, method: 'POST', headers, body: JSON.stringify(payload) });
if (result.ok) {
state.sent = (state.sent ?? 0) + 1;
console.log(`Webhook queued: ${from} → ${to} (${payload.actor})`);
} else {
state.refused = (state.refused ?? 0) + 1;
console.warn('Webhook refused:', result.reason); // bad URL or a blocked destination; see the egress policy
}
}How it works
subscribe(panel, {})fires on state and attribute changes, so the handler first drops events where the state did not move. Subscribing with{ to: 'Triggered' }would be simpler for a single state; the empty filter is right when every transition should go out.result.okmeans accepted, not delivered. The hub checked the URL and the egress policy (loopback, link-local and multicast are refused; LAN and internet are fine) and queued the request. Delivery failures are logged on the hub, not returned to the app.- The token stays out of the log. It is sent in a header and never interpolated into a
console.log. Everything you log is visible to every admin of the location. context.useris present when a person armed or disarmed from the app or a keypad; a wall keypad reports'alarm-panel-user', your own automations'smart-app'.
Try changing
- Send a plain
GETto a LAN device:httpRequest({ url: 'http://192.168.1.50/api/flash', method: 'GET' }). You cannot read its answer; if you need state back, that is an integration. - Record what was never acknowledged: the hub does not retry, so keep a small queue in
stateand resend from arunDailyhandler if your endpoint can tell you what it received.