Docs
Examples

Simple trigger

Let the installer compose "when this device reaches this state, do these things" from the hub's own service catalog.

A tiny rule engine. The installer picks a trigger device and one of its states, then adds a list of actions, each "device + service", with a level where the service takes one. Nothing about devices or services is hard-coded: the form asks the hub's catalog, so new domains show up without editing the app.

What it shows

  • preferences as a function of the answers so far, re-run on submitOnChange. See Dynamic preferences.
  • Catalog introspection: getDomainStateOptions, getActionDomains, getServicesForDomain, getServiceParameters.
  • Array rows with filterOptionsByEntityDomain and requiresSiblingOneOf, which only work inside rows. See Conditions and visibility.
  • Calling a service by name on a wrapper built from an id: new EntityWrapper(id)[service](params).
simple-trigger.js
const definition = {
  name: 'Simple Trigger',
  namespace: 'com.example',
  description: 'When a device reaches a state, run a list of actions.',
  category: 'automation',
};

// A function rather than an object: the hub calls it (inside the isolate) every time the form is
// rendered, with the answers so far, so the option lists come from the hub's live catalog.
const preferences = (values) => ({
  pages: [
    {
      id: 'trigger',
      title: 'When',
      inputs: [
        { key: 'trigger', label: 'Device', type: 'entity', required: true, submitOnChange: true },
        { key: 'triggerState', label: 'Changes to', type: 'enum', required: true,
          options: stateOptionsFor(values.trigger),               // narrowed to the picked device's domain
          visible: { input: 'trigger', operator: 'truthy' } },
      ],
    },
    {
      id: 'actions',
      title: 'Then',
      visible: { input: 'triggerState', operator: 'truthy' },
      inputs: [
        {
          key: 'actions',
          label: 'Actions',
          type: 'array',
          minItems: 1,
          maxItems: 10,
          itemSummaryKeys: ['device', 'service'],
          itemSchema: {
            inputs: [
              { key: 'device', label: 'Device', type: 'entity', required: true },
              { key: 'service', label: 'Do', type: 'enum', required: true,
                options: serviceOptions(),                           // every 'domain.service' the catalog offers
                filterOptionsByEntityDomain: 'device' },             // shown per row for the picked device's domain
              { key: 'level', label: 'Level (0–100)', type: 'number', minValue: 0, maxValue: 100,
                requiresSiblingOneOf: { siblingKey: 'service', values: ['light.turnOn', 'fan.setPercentage', 'cover.setCoverPosition'] } },
            ],
          },
        },
        { key: 'delaySeconds', label: 'Wait before acting (seconds)', type: 'number', default: 0, minValue: 0, maxValue: 3600 },
      ],
    },
  ],
});

// 'light.on', 'lock.locked', … for the trigger's domain; everything while nothing is picked yet.
function stateOptionsFor(entityId) {
  const all = getDomainStateOptions();
  if (typeof entityId !== 'string') return all;
  const domain = entityId.split('.')[0];
  return all.filter((o) => o.value.startsWith(`${domain}.`));
}

function serviceOptions() {
  const out = [];
  for (const d of getActionDomains()) {
    for (const s of getServicesForDomain(d.value)) {
      out.push({ value: `${d.value}.${s.value}`, label: `${d.label} — ${s.label}`, description: s.description });
    }
  }
  return out;
}

function installed() { initialize(); }
function updated() { unsubscribe(); unschedule(); initialize(); }

function initialize() {
  if (!trigger || typeof triggerState !== 'string') return;
  const token = triggerState.split('.')[1];                        // 'binary_sensor.on' → 'on'
  subscribe(trigger, { to: token }, onTrigger);
}

function onTrigger(evt) {
  console.log(`${evt.entityId} → ${evt.newState.state}`);
  const wait = typeof delaySeconds === 'number' ? delaySeconds : 0;
  if (wait > 0) runIn(wait, runActions);                           // a second trigger inside the wait just re-arms
  else runActions();
}

function run() {                                                   // Run button: the actions, without the trigger
  runActions();
}

function runActions() {
  for (const row of actions ?? []) {
    if (typeof row.device !== 'string' || typeof row.service !== 'string') continue;
    const [domain, service] = row.service.split('.');
    const params = {};
    if (typeof row.level === 'number') {
      // Ask the catalog what this service calls its level, rather than hard-coding parameter names.
      const numeric = getServiceParameters(domain, service).filter((p) => p.type === 'number');
      const p = numeric.find((p) => p.required) ?? numeric[0];
      if (p) params[p.key] = row.level;
    }
    new EntityWrapper(row.device)[service](params);
  }
}

How it works

  • Two places narrow options, two mechanisms. At the top level the function does it: stateOptionsFor(values.trigger) filters by the picked device's domain, because filterOptionsByEntityDomain is only honoured inside array rows. Inside the rows it is the declarative hint that filters service per row.
  • Stored values are strings. triggerState is 'light.on'; the row's device is an entity id and service is 'light.turnOn'. The code splits both and builds a wrapper per action.
  • The catalog answers twice. serviceOptions() builds the dropdown from getActionDomains and getServicesForDomain; runActions asks getServiceParameters for the name of the numeric parameter (brightnessPercent, percentage, position) so the same level field drives all three.
  • preferences runs during editing, not installation. It must only read (catalog calls are fine); the subscription is made in initialize, from the stored triggerState.

Try changing

  • Add a condition page: an optional entity + enum state pair, checked with new EntityWrapper(settings.conditionEntity).is(token) at the top of runActions.
  • Support numeric sensors as triggers: when the trigger's domain is sensor, replace the state enum with a comparison and a threshold, subscribe with {}, and compare in onTrigger as in Temperature alert.

On this page