Discovering and Adopting Devices

Devices appear on their own, but a discovered device fires no triggers and refuses every command until you adopt it. Open pairing, put the device in pairing mode, adopt it and give it a name and a room.

Adoption decides what is part of your system

A coordinator hears every device in radio range that has ever been paired to its network, including the ones somebody paired to test and forgot. A second-hand Z-Wave stick inherits whatever the previous owner left in its NVM. Without an explicit adopt, "when any sensor activates" means "when anything within thirty metres twitches".

The gate is enforced in the trigger dispatcher and the command path rather than in each block, so a new trigger block cannot forget it. The one exception is device discovered, which fires for unadopted devices because noticing them is its purpose.

DiscoveredAdopted
Appears in the device listOnly when you ask for unadopted devicesYes, by default
State is trackedYesYes
Fires device discoveredYes, onceNot applicable
Fires any other triggerNoYes
Accepts a commandNo, HOMEASSIST_DEVICE_NOT_ADOPTEDYes
Records historyNoYes, per the sampling policy

The onboarding loop

  1. Open the radio for joining

    The pairing open block, or POST /_system/homeassist/pair. The duration you ask for is clamped to the node's pairing_window, so a request for an hour becomes two minutes rather than being refused. Pass 0 to close pairing immediately.

  2. Put the device into pairing mode

    Usually a long press, sometimes a specific number of taps. Check the vendor's instructions.

  3. It appears as discovered

    The device discovered trigger fires, once, the first time that device is ever seen.

  4. Adopt it, with a name and a room

    From that moment its triggers fire and blocks can command it.

The last step can be automated: wire device discovered into device adopt, which takes optional name and room pins. See Example Flows. For a bench mesh, auto_adopt = true in the config skips the gate entirely; keep it off anywhere real.

Adopting by hand

List what is waiting, then take it into service:

List unadopted devices, then adopt one
# Everything the radio has heard that you have not taken into service.
curl -s 'https://<node>/_system/homeassist/devices?unadopted=1' \
  -H 'Authorization: Bearer <token>'

# Adopt it, naming it and placing it in one call.
curl -X POST 'https://<node>/_system/homeassist/devices/zigbee:0x00158d0007e1a2b3/adopt' \
  -H 'Authorization: Bearer <token>' \
  -d '{"name":"Hallway motion","room":"Hallway"}'
  • The default list returns adopted devices only; unadopted=1 asks for the rest.
  • Adoption is idempotent. Adopting an already-adopted device succeeds and reports newly = false, so a flow can call it without checking first.

Pairing means something different on every radio

The pairing open block takes an adapter name and a duration.

AdapterWhat "open pairing" does
zigbee Permit-join on the coordinator, for the requested seconds. The coordinator enforces the window itself.
zwave Starts inclusion, with S2 security where the device supports it. Z-Wave inclusion is not time-bounded by the controller the way permit-join is, so the node closes the window itself when the duration expires.
matter Opens the fabric's commissioning window so another controller can join. Adding a new device to Matter is not "open a window and wait": it needs the 11-digit pairing code or the MT: string printed on the device, which is a different operation.
hue Creates an application key. A Hue bridge has no inclusion mode: it is already a coordinator with its own devices, and what the node needs is authorisation. The bridge grants it only in the thirty seconds after somebody physically presses the link button.
daikin Not supported, and it says so: HOMEASSIST_PAIRING_UNSUPPORTED. A Daikin unit is on the WiFi or it is not. Add its IP to [homeassist.daikin] hosts.
unifi Also unsupported, for a similar reason. UniFi devices are enrolled in the UniFi app, and the node reaches them through the console's integration APIs afterwards. What RUAL needs is a key per app, not an inclusion window: see Setting Up a RUAL Nano.

Every device has two identifiers, a name and a room

FieldExampleWhat it is for
idzigbee:0x00158d0007e1a2b3The platform identifier, <adapter>:<native>. Stable across restarts and renames, which is why blueprints pin to it. The prefix means two radios can carry the same native address without colliding.
native_id0x00158d0007e1a2b3, 12, a Hue UUIDThe address in the adapter's own namespace. What you would type into Zigbee2MQTT or Z-Wave JS UI.
nameHallway motionWhat a human calls it. Seeded from the vendor's friendly name, then yours.
roomHallwayFree text, set by you. Empty until somebody says.

Any block that addresses a device accepts the platform id, the native id or the exact name, case-insensitively. A name must match exactly: a prefix match would make renaming one device silently re-point another's flows.

  • Your name and room are yours. Once set, a re-announce from the radio never overwrites them, even though a Zigbee re-pair resets the vendor's friendly name to the model number. Everything else merges: newly reported capabilities are added, a blank manufacturer or model is filled in.
  • Renaming in RUAL does not rename at the radio. The Zigbee2MQTT friendly name and the Hue bridge name are unchanged, so those apps keep showing the old name.

Rooms are free text

There is no room registry. A room exists because at least one device is assigned to it, and get rooms returns the distinct non-empty room names across all adopted devices, sorted. Room filters compare case-insensitively, so "Hallway" and "hallway" are the same room, but "Hall" is a different one.

A trigger with an empty device pin and room = Hallway is "any matching device in the hallway": one block instead of one flow per sensor.

Device knowledge comes off the wire

Zigbee2MQTT publishes its full device definition, the exposes tree covering every attribute a device reports with its type, unit and range, on zigbee2mqtt/bridge/devices, refreshed whenever the mesh changes. It covers roughly 4,000 devices and arrives already matched to the device on your mesh. Z-Wave JS does the same through its own config index.

A small profile table sits on top for devices whose exposes tree is correct but produces a bad default. Aqara sensors are the clearest case, reporting battery, voltage, link quality and device temperature alongside the one attribute anybody automates on. A profile:

  • guarantees the useful capabilities are present before the first report of that kind arrives;
  • normalises the vendor string ("LUMI" becomes "Aqara");
  • overrides the derived type where derivation is wrong, such as a button that would otherwise classify as a sensor.

A device with no profile gets exactly what its exposes tree said, which is almost always right. Profiles are applied on discovery and on adopt, so a device paired before its profile existed picks it up the next time somebody adopts it.

Forget is not unpair

device forget, and DELETE /_system/homeassist/devices/<id>, drop the device from RUAL: its name, room and adoption are discarded. It does not unpair the device from the Zigbee, Z-Wave or Matter network. The device stays on the mesh and reappears, unadopted, on its next report.

  • Unpairing is done in Zigbee2MQTT or Z-Wave JS UI, and is not exposed as a block.
  • A device that leaves the mesh on its own is marked unavailable and kept registered, so a dead battery does not discard its name, room and adoption.

Identity and state both survive a restart

They live in two different places, for two different reasons.

IdentityLast-known state
WhatWhich devices exist, name, room, adoption and when, capabilities shown, manufacturer, model, matched profileThe last value per capability: whether the light is on, the last temperature, whether the door is open
WhereThe document store, alongside everything else on the nodeRedis
Why thereIt is an operator's work and must not be lostIt is hot, small, rewritten constantly and worthless once stale: the profile of a cache, and the opposite of identity
Written whenOn the changes that matter: discovery, adoption, rename, a new capabilityOn every reading, coalesced per device and flushed every two seconds
ExpiresNeverAfter 30 days, refreshed on each write

Keeping state out of the document store stops a motion sensor in a busy hallway from becoming the node's dominant write load. One Zigbee message routinely carries occupancy, illuminance, battery and link quality: four changed capabilities and one write.

A restored value is a belief, not an observation

  • Restoring state does not set available or touch last_seen. available stays false until the device actually reports.
  • Momentary capabilities are excluded from the restored map: replaying the last button press as current state would make it look freshly pressed.
  • Forgetting a device deletes its stored state too, so re-adopting one later does not restore a belief from before it was removed.

Why a restart does not fire every sensor

State is restored before the adapters connect, so the first real report has something to compare against and an unchanged re-announce stays unchanged. Independently of that, an unknown previous value seeds silently rather than firing, except for smoke, gas and water_leak, where a missed event is worse than a repeated one. The full rule is on the trigger reference.

In RUAL Studio

The device list, the per-device state and the history charts live under Devices, then Home Automation. That screen covers the onboarding view (which uses the unadopted query), adopt, rename, forget, and a one-capability command, so that having adopted a bulb you can check it responds without building a blueprint.

Every trigger and action block that addresses a single device carries the metadata for an in-editor device picker: which device type to offer, so light on offers lights and not thermostats.

The picker UI is not shipped yet The block metadata and the API it reads are in place; the studio side is not. Until it lands, type an id or an exact name into the block's device pin, which is fully supported and is what the examples in this section do. When the picker lands, the device pin always beats the picker, so a flow that computes which lamp to address is never overridden by an old click in the editor.

The screen does not poll. Device events are pushed onto the WebSocket bus as they arrive, on the homeassistdevices:updates:<device id> channel for one device and homeassistdevices:updates:all for the fleet. Unlike the trigger dispatcher, the broadcast does not filter on adoption, because the onboarding screen has to show devices that are not adopted yet.

The device API

All of it is under /_system/homeassist and all of it requires the blueprints / simulating scope.

RouteDoes
GET /statusPer-adapter connection state and device counts.
GET /devicesThe device list. Filters: type, adapter, room, capability, plus unadopted=1 or include_unadopted=1.
GET /devices/{id}One device, resolved by id, native id or exact name.
GET /devices/{id}/historyRecorded readings, and buckets when buckets is set.
POST /devices/{id}/adoptAdopt, with an optional name and room body.
POST /devices/{id}/commandWrite one capability. One per call: the caller here is a human moving a slider, not an automation.
POST /devices/{id}/renameSet name and/or room.
DELETE /devices/{id}Forget. Does not unpair.
POST /pairOpen or close pairing on one adapter.
GET /roomsThe distinct room names in use.
GET /capabilitiesThe vocabulary itself: sensor capabilities, device types, per-capability units, decimals, axis bounds and chart type, and the adapter names.

GET /capabilities needs no running service and therefore no node with a radio, because the studio has to populate capability dropdowns while editing a blueprint that will run somewhere else. Units, decimals and chart types come from here too.

Next steps

Trigger Reference All 32 blocks that start a flow, and the three rules that decide how often they fire. Example Flows Including auto-adopting a newly discovered device into a room. Action Reference Adopt, rename, forget and pairing from a flow.

Frequently asked

Why does my RUAL device not respond even though it appears in the list?

It is discovered but not adopted. An unadopted device is tracked and its state is followed, but it fires no triggers and refuses every command. Adopt it through the studio, through POST /_system/homeassist/devices/{id}/adopt, or from a flow with the device adopt block.

Why does RUAL make me adopt devices instead of using all of them?

A coordinator hears every device ever paired to its network, including ones somebody paired to test years ago, and a second-hand Z-Wave stick inherits whatever the previous owner left on it. Without an explicit adopt, a trigger like when any sensor activates would fire on hardware nobody remembers installing.

Does forgetting a device in RUAL unpair it from the radio?

No. Forget removes the device from RUAL, discarding its name, room and adoption, and the device stays paired to the Zigbee, Z-Wave or Matter network and reappears unadopted on its next report. Unpairing is done in Zigbee2MQTT or Z-Wave JS UI, because stop automating this and evict this from my mesh are different intentions.