Docs
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

  • httpRequest with a JSON body and an Authorization header, 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 password input for the secret, and a string input with a placeholder.
  • Comparing panel states with matchesState so TRIGGERED and 'Triggered' both work.
alarm-webhook.js
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.ok means 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.user is 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 GET to 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 state and resend from a runDaily handler if your endpoint can tell you what it received.

On this page