Docs

Subscriptions and events

React to device changes and to named location events, and know who caused a change.

Subscribing to a device

subscribe(door, { to: 'unlocked' }, onUnlocked);               // state becomes unlocked
subscribe(door, { from: 'locked', to: 'unlocked' }, onUnlocked); // only this exact transition
subscribe(frontDoor, { to: 'Open' }, onDoorOpen);              // friendly label: 'Open' is `on` for a door sensor
subscribe(sensor, {}, onAnyChange);                            // any state or attribute change
subscribe(light, { attribute: { name: 'brightnessPercent' } }, onDim);
subscribe(lights, { to: 'on' }, onAnyLightOn);                 // an array or multi-select proxy subscribes every member

The filter is checked strictly when subscribe runs: an unknown key throws, which fails the installed / updated call that made it. from / to fire only on a real transition, never on a repeated report of the same state. The exact contract is on Subscription filter.

Raw tokens and friendly labels

Every entity reports a raw token (on, locked, open, ARMED_AWAY). The app shows a friendly label for it that depends on the device: a door sensor's on is shown as Open, a motion sensor's as Detected, a leak sensor's as Wet. Wherever you compare a state you may write either form:

WhereAcceptsNotes
subscribe(…, { from, to })raw token or labelresolved when the event is matched; stored as written
entity.is(value) / entity.isNot(value)raw token or labeltests the live state
matchesState(entityId, state, value)raw token or labeltests a snapshot, e.g. the event's newState.state
entity.getState(), evt.newState.state, ===raw token onlywhat the hub stores; labels are never stored

Matching is case-insensitive and treats spaces, hyphens and underscores alike, so 'Armed Away', 'armed-away' and 'ARMED_AWAY' are the same. A raw token always matches itself; labels are only an input alias resolved through the entity's domain and device class, so 'Open' means on for a door sensor and open for a cover. Numeric domains (sensor, input_number, counter) have readings, not labels, and never resolve.

function onDoor(evt) {
  // Test the snapshot you were handed, not the live state (it may have moved on).
  if (matchesState(evt.entityId, evt.newState.state, 'Open') && panel.is('Armed Away')) {
    sendPushNotification('Door opened while armed away', 'Security', true);
  }
}

Each capability page lists the raw tokens with their labels per device class, for example the binary sensor and lock.

entity.is(), isNot() and matchesState(), and label support in subscribe filters, need a hub running the friendly-state runtime. On an older hub the methods do not exist and a label in a filter never matches; raw tokens work everywhere.

Subscriptions are persisted and restored after a reboot. You do not re-subscribe on start-up; you do clear and re-create them in updated so settings changes take effect.

The event

The handler receives an EntityChangeEvent:

function onUnlocked(evt) {
  evt.entityId;                 // 'lock.front_door'
  evt.oldState.state;           // 'locked'
  evt.newState.state;           // 'unlocked'
  evt.newState.attributes;      // { batteryLevel: 80, … }
  evt.newState.context?.actorType;  // who did it
}

context.actorType tells you the cause: 'physical' (the device itself: a wall paddle or key), 'user' (a person in the app), 'smart-app' (an automation, with context.smartApp.smartAppId), 'alarm-panel-user', 'lan-device', or 'system' / absent for internal transitions. Gating on 'physical' is how you react to a manual press without reacting to your own commands.

Things that trip people up

  • Presence is a binary_sensor. People, places and the household presence entities are binary_sensor with device class presence: on = home (label Present), off = away (Not Present). Subscribe with { to: 'Present' } or { from: 'off', to: 'on' } for arrivals.
  • Every entity can report unavailable and unknown. Do not treat "not on" as "off" where it matters, and do not trigger user-facing behaviour on those transient states. is('on') is false for both, which is usually what you want.
  • Sensors report values, not enums. A temperature sensor's state is a number (or numeric string); subscribe with {} and compare inside the handler. Labels do not apply to numeric domains.
  • Attribute filters compare by reference for object-valued attributes, so they can fire on every report. Prefer scalar attributes, or compare old and new yourself.
  • Raw tokens are usually lowercase (on, locked, open) but not always: modes (DAY, NIGHT), alarm panels (ARMED_AWAY) and the sun domain (ABOVE_HORIZON) are uppercase. This only bites a strict getState() === … comparison; filters, is() and matchesState() ignore case.
  • A label is per device, not global. 'Open' resolves for door sensors, covers and valves, but a motion sensor's on is Detected; check the capability page when unsure.

Location events

A location event is a named message rather than a device change. The hub fires one itself:

  • systemStart — once per boot, after subscriptions and schedules have been restored. Use it to re-arm one-shot sunrise / sunset timers or to re-check state after an outage.
function installed() { initialize(); }
function initialize() {
  subscribeToLocationEvent('systemStart', onBoot);
  scheduleAtSunset(onSunset);
}
function onBoot() { scheduleAtSunset(onSunset); }   // the pending one-shot was lost with the reboot? re-arm to be safe

Any app can send its own with sendLocationEvent(name, data), and every app subscribed to that name receives it. That makes location events the way apps talk to each other, including a parent and its children:

// in a child
sendLocationEvent('com.example.rule.fired', { rule: definition.name, at: Date.now() });

// in the parent
subscribeToLocationEvent('com.example.rule.fired', onRuleFired);
function onRuleFired(evt) { console.log('rule fired', evt.value); }

Location subscriptions persist like device subscriptions.

Unsubscribing

unsubscribe() with no arguments removes every subscription this instance made; pass a handler to remove only that handler's, or a target (and optionally a filter) to be more specific. See unsubscribe.

On this page