Setting Up a RUAL Nano for Home Automation
A runbook, in order: Redis, an MQTT broker, Zigbee2MQTT against a ZBT-2, the node's config.toml, and how to prove the radio is actually connected before you start writing flows.
This page assumes a machine that already runs rual-core. The hardware choice, the minimum specifications and the growth path are on RUAL Core Nano; which radio to buy, and why one dongle cannot do Zigbee and Thread at the same time, is in the Local Home Automation tutorial. Everything below is what happens after the box and the dongle are on the desk.
The order matters. Each step verifies before the next one depends on it, because a home-automation stack that is wrong in the middle presents identically to one that is wrong at either end: no devices.
What You Are Building
Four processes, three of them not RUAL:
| Process | Role | Reached over |
|---|---|---|
| Redis | Cache and pub/sub the node already needs, plus last-known device state | 127.0.0.1:6379 |
| Mosquitto | MQTT broker: the bus the radio daemons publish to | 127.0.0.1:1883 |
| Zigbee2MQTT | Owns the USB coordinator, speaks Zigbee, publishes to the broker | the broker |
| rual-core | Reads the broker, translates into capabilities, runs your flows | the broker |
Add Z-Wave JS UI or Matter Server later and they join the same pattern: their own process, their own radio, publishing where the node can read them.
Step 1: Redis
Redis is not home-automation specific: the node uses it for its document cache and for the pub/sub channel that reloads the trigger index when you edit a blueprint. Home automation adds one job to it, holding last-known device state so a restart does not re-fire every already-active sensor. If your node already runs, Redis already runs, and you can skip to step 2.
brew install redis
brew services start redis
redis-cli ping # PONGsudo apt install redis-server
sudo systemctl enable --now redis-server
redis-cli ping # PONGBind it to localhost. It holds cached documents and nothing on it needs to be reachable from the network.
Step 2: The MQTT Broker
Mosquitto is the broker both MQTT adapters speak to. It needs a config file before it will listen on anything other than the loopback interface with no authentication, and for a single box that default is exactly what you want.
brew install mosquitto
brew services start mosquittosudo apt install mosquitto mosquitto-clients
sudo systemctl enable --now mosquittoProve it works before anything depends on it. Two terminals: subscribe in one, publish in the other.
# terminal 1
mosquitto_sub -h 127.0.0.1 -t 'test/#' -v
# terminal 2
mosquitto_pub -h 127.0.0.1 -t 'test/hello' -m 'it works'If terminal 1 prints test/hello it works, the bus is up. If it does not, stop here: everything after this point fails the same way, silently, with no devices and no error.
If you set a username and password, put the same pair in the node's [homeassist.zigbee] section and in Zigbee2MQTT's own config. An authenticated broker with one side unconfigured looks like a broken radio.
Step 3: Zigbee2MQTT Against the ZBT-2
This is the step with the sharp edges. Zigbee2MQTT owns the USB coordinator, so it needs the actual device node, and that constrains how you may run it.
Find the Serial Port
Plug the ZBT-2 in, on its extension cable, away from the case. USB 3 ports and NVMe drives emit broadband noise right across 2.4 GHz, and a coordinator plugged directly into the back of a machine sits inside that. Then find the device:
# macOS
ls /dev/cu.usbmodem*
# Linux: use the by-id path, it survives a reboot and a re-plug
ls -l /dev/serial/by-id/Use the /dev/serial/by-id/... path on Linux, never /dev/ttyUSB0. With two dongles attached, the numbered paths swap on reboot, and a Zigbee daemon that opens the Z-Wave stick fails in a way that takes an hour to recognise.
Docker Cannot See the Dongle on a Mac
Worth knowing before you follow a tutorial that assumes otherwise. Docker Desktop on macOS runs containers inside a Linux VM, and that VM has no USB passthrough: --device /dev/cu.usbmodem... does not work, and no flag makes it. So on a Mac Mini, Zigbee2MQTT runs natively under Node.js, while Redis and Mosquitto can live in Docker or under Homebrew, whichever you prefer.
On Linux the ordinary Docker setup works, with the coordinator passed through by its by-id path.
# Node 20 or newer
git clone --depth 1 https://github.com/Koenkk/zigbee2mqtt.git ~/zigbee2mqtt
cd ~/zigbee2mqtt
npm ci
npm run build
# writes data/configuration.yaml on first run
npm startconfiguration.yaml
The parts that matter, in data/configuration.yaml:
mqtt:
# This must match [homeassist.zigbee] base_topic on the node.
base_topic: zigbee2mqtt
server: mqtt://127.0.0.1:1883
# user: rual
# password: ...
serial:
# macOS: /dev/cu.usbmodem... Linux: /dev/serial/by-id/...
port: /dev/cu.usbmodem2101
# The ZBT-2 is Silicon Labs EmberZNet silicon.
adapter: ember
baudrate: 460800
# The web UI. Useful for pairing and for reading the map; not required by RUAL.
frontend:
port: 8099
advanced:
# Leave permit_join off. RUAL opens the window when you ask it to, for a
# bounded number of seconds.
log_level: info
permit_join: falseThe adapter and baud rate are coordinator-specific. Those two lines are for the Home Assistant Connect ZBT-2; if you bought something else, check the Zigbee2MQTT adapter list rather than guessing, because a wrong adapter setting produces a daemon that starts and then never sees a device.
Leave permit_join false. A coordinator left permanently joinable is how a mesh acquires devices nobody added. Pairing is opened deliberately, for a bounded window, from RUAL: see Discovering and Adopting Devices.
Prove the Radio Is Talking
Start Zigbee2MQTT and watch the broker rather than the log. This is the single most useful diagnostic on the whole page, because it separates "the radio works" from "RUAL is configured correctly" once and for all:
mosquitto_sub -h 127.0.0.1 -t 'zigbee2mqtt/#' -vWithin a few seconds you should see zigbee2mqtt/bridge/state carrying online, and zigbee2mqtt/bridge/devices carrying a JSON array. That array is the device knowledge base: every device on the mesh with its full exposes tree. On a fresh coordinator it is empty, which is correct.
If bridge/state says offline, the broker is fine and Zigbee2MQTT is not: it is either not running or it has lost the coordinator. That distinction matters enough that the node logs it separately; see Troubleshooting.
Step 4: The Node's Configuration
Home automation is off by default, and the switch is one key. Start from configs/nano.example.toml in the rual-core repository, which is this section plus the SQLite storage half.
[homeassist]
enabled = true
# Which tenant owns the devices. A radio is a physical building, and buildings
# do not have tenants. On a single-entity node this may be omitted and falls
# back to the one active entity; on a node serving several it is required.
entity = 1
# Adopt every discovered device on sight. Leave this false anywhere real.
auto_adopt = false
# Ceiling on how long a pairing request may open a radio for joining.
pairing_window = "2m"
[homeassist.zigbee]
broker = "tcp://127.0.0.1:1883"
# Must match Zigbee2MQTT's own mqtt.base_topic.
base_topic = "zigbee2mqtt"
# username = ""
# password = ""
# Only needed when several RUAL nodes share one broker: two MQTT connections
# with the same client id kick each other off forever.
# client_id = ""Four keys are worth understanding rather than copying.
| Key | Default | Why it exists |
|---|---|---|
entity |
the single active entity | Devices belong to one tenant. With exactly one entry in general.active_entities the node fills it in; on a node serving several it refuses to start rather than guess which tenant owns the building. |
auto_adopt |
false |
Adopts everything on sight. It exists for a bench mesh that is five minutes old, where adopting by hand between every pair is friction with no safety value. Anywhere real it turns "when any sensor activates" into "when any hardware in radio range activates". |
pairing_window |
2m |
The ceiling a pairing open call is clamped to. A request for an hour becomes two minutes rather than being refused. |
base_topic |
zigbee2mqtt |
Must match the daemon's own value. A mismatch is the most common cause of a node that connects to the broker happily and never sees a single device. |
The Other Adapters
Each one is independent: configure the ones you own, leave the rest out. An adapter with no configuration is never constructed.
# --- Z-Wave, through Z-Wave JS UI ---
# Its MQTT gateway MUST be set to "ValueID topics". See the note below.
[homeassist.zwave]
broker = "tcp://127.0.0.1:1883"
base_topic = "zwave"
gateway_name = "zwave-js-ui"
# --- Thread and Matter, through Matter Server ---
# Thread devices additionally need a Thread Border Router. Matter over WiFi
# needs only the server.
[homeassist.matter]
url = "ws://127.0.0.1:5580/ws"
# --- Philips Hue bridge, on the LAN ---
[homeassist.hue]
# Use a DHCP reservation: bridges do not have stable hostnames.
host = "192.168.1.50"
# Leave empty on the first run, then pair against the bridge's link button.
app_key = ""
# --- Daikin air conditioning, on the LAN ---
# Covers BOTH the legacy BRP069/BRP072 units and the newer Onecta firmware.
# The dialect is detected on the first poll; there is no extra key for it.
[homeassist.daikin]
hosts = ["192.168.1.60"]
# There is no event channel on this API, so this is the resolution of every
# climate trigger.
poll_interval = "30s"
# --- UniFi Access and Protect, on the LAN ---
# Two apps on one console, each with its own integration API and its own key.
# Enabling one does not enable the other, and a key for one does not
# authenticate to the other. Leave a key empty to disable that half.
[homeassist.unifi]
host = "192.168.1.1"
access_key = "" # doors, NFC and card swipes, entry events, door stations
protect_key = "" # cameras, doorbell cameras, motion eventsZ-Wave JS UI must publish ValueID topics. Its MQTT gateway offers two layouts and only one of them is machine-parseable. "Named topics" interpolates your own location and node names into the topic path, so zwave/kitchen/ceiling/switch_binary/... cannot be told apart from a node whose name contains a slash. ValueID topics are zwave/<nodeId>/<commandClass>/<endpoint>/<property>, which always parses. Set it in Z-Wave JS UI under Settings, Gateway, Gateway type.
The Hue app key is created against the link button. There is no inclusion mode to open on a Hue bridge: it is already a coordinator with its own devices, and what the node needs is authorisation. Leave app_key empty, start the node, press the physical button on the bridge, and within thirty seconds call the pairing endpoint. The key is printed to the node log; paste it into config.toml so it survives a restart. It is deliberately not stored in the device registry, because a bridge credential does not belong somewhere a blueprint can read it.
curl -X POST https://<your-node>/_system/homeassist/pair \
-H 'Authorization: Bearer <token>' \
-d '{"adapter":"hue"}'UniFi Needs Two Keys, and Both Must Be Enabled Console-Side
Access and Protect are separate apps on the same console, with separate integration APIs and separate keys. Enabling one does not enable the other, and a key issued by one does not authenticate to the other. This is the single most likely reason a UniFi adapter connects and reports nothing.
| Key | Issued in | Enables |
|---|---|---|
access_key | Access, then Security, then Advanced, then API | Doors, card and NFC swipes, entry granted and denied, door stations, lock and unlock commands |
protect_key | Protect, then Settings, then Control Plane, then Integrations | Cameras, doorbell cameras, motion events |
Give the console a DHCP reservation: consoles do not have stable hostnames. TLS verification is skipped for the same narrow reason as the Hue bridge, a self-signed certificate on a hard-coded LAN address with no chain to verify.
RUAL cannot appear inside the UniFi app. UniFi has no third-party integrator surface that renders in its own UI; the integration APIs are read and command only. The node is a client of the console, not a plugin to it.
Daikin Onecta Needs No Extra Configuration
The [homeassist.daikin] section covers both firmware generations. The legacy BRP069 and BRP072 units expose a flat k=v API at /aircon/*; the newer BRP069C4x "Onecta" firmware replaced it with a nested JSON tree at /dsiot/*, which is a different protocol on the same port. A unit that answers 404 on the legacy endpoint is detected as Onecta on the first poll, and the node remembers which dialect each host speaks.
Power on and off is reliable on Onecta. The temperature and mode decoding follows community reverse engineering and the hex encoding varies by model, so treat those readings as best-effort until a live unit confirms them. Every /dsiot reading is also carried untranslated on the device's raw map under dsiot, so a flow that finds a field mis-decoded can read the raw tree instead.
History Recording
Device analytics is on by default, because a home automation system that cannot answer "what has the temperature been doing" is missing the obvious question. It is bounded by a sampling policy rather than by luck; the reasoning is on Device History and Energy.
[homeassist.analytics]
# Set false to record nothing. The history block then reports recording = false,
# which is deliberately distinguishable from an empty result.
enabled = true
# How long readings are kept. Zero disables pruning entirely, which is only
# correct where somebody else is watching the disk.
retention = "2160h" # 90 days, the default
flush_interval = "30s"Step 5: First Boot
Restart the node and read the log. Four lines tell you whether this worked:
| Log line | Means |
|---|---|
HomeAssist: enabled for entity 1, adapters: [zigbee] | The service started and constructed the adapters you configured. If an adapter you expected is missing, its config section is missing or empty. |
zigbee2mqtt: bridge online | The broker is reachable and the daemon is running. |
zigbee2mqtt: N device(s) in mesh | The device list arrived and was parsed. N is 0 on a fresh coordinator. |
Restored N device(s) for entity 1 | Identity data came back from the store. Absent on a first boot, expected on every one after. |
Two warnings are worth recognising immediately:
[homeassist] enabled but no adapter is configured. The service is running and no radio is attached to it, so the blocks stop reportingHOMEASSIST_NOT_AVAILABLEand start reporting nothing at all. This is loud rather than fatal on purpose: a node whose broker is briefly down must still boot.zigbee2mqtt: bridge OFFLINE. The broker is reachable but Zigbee2MQTT is not running, or it has lost the coordinator. Different problem, different fix.
Nothing here is fatal, and that is deliberate. A stick that is unplugged at boot, a broker that is down, a Hue bridge on a router that has not finished starting: all ordinary conditions in a house, none of them a reason for the node to refuse to serve auth, pages and blueprints. Adapters retry with backoff and report through the status API.
Step 6: Prove the Radio Is Connected
Two ways, and they answer slightly different questions.
From a blueprint, the get status block reports one entry per adapter with its connection state, plus device counts. Its healthy pin is true only when every configured adapter is connected, and a node with no adapters at all is not healthy: the service being enabled with nothing wired is a misconfiguration, and reporting it green would hide exactly the case you are checking for.
Over HTTP, the same payload:
curl -s https://<your-node>/_system/homeassist/status \
-H 'Authorization: Bearer <token>'
# {"entity":1,"adapters":[{"name":"zigbee","connected":true}],
# "devices_total":0,"devices_adopted":0}The home-automation routes require the blueprints / simulating scope, which is stronger than the scope that guards blueprint metadata. That is on purpose: a device list is a floor plan. It names every room, says which are occupied right now, and says which doors are open.
A node with home automation switched off answers 503 with HOMEASSIST_NOT_AVAILABLE and a hint, rather than 404. The route exists on every node, and "this build has no such endpoint" and "this node has home automation off" are exactly the two things a client discovering capabilities needs to tell apart.
Next Steps
Discovering and Adopting Devices Open pairing, adopt what appears, and give it a room. Troubleshooting No devices, bridge offline, triggers that do not fire. RUAL Core Nano The machine this runs on, and the rest of its configuration.Frequently asked
What do I need to install to run home automation on a RUAL Nano?
Redis and an MQTT broker such as Mosquitto for the node itself, plus one daemon per protocol: Zigbee2MQTT for Zigbee, Z-Wave JS UI for Z-Wave, or Matter Server for Thread and Matter. RUAL does not drive the radios itself; it reads what those daemons publish and translates it.
How do I switch on home automation in a RUAL node?
Set enabled = true in the [homeassist] section of the node's config.toml and configure at least one adapter, for example [homeassist.zigbee] with a broker and a base_topic. It is off by default, and a node without it starts no radio goroutine and reports HOMEASSIST_NOT_AVAILABLE from every home-automation block.
Can Zigbee2MQTT run in Docker on a Mac?
Not with a USB coordinator. Docker Desktop on macOS runs containers inside a Linux VM with no USB passthrough, so the dongle cannot be handed to a container. Run Zigbee2MQTT natively under Node.js on a Mac; on Linux the ordinary Docker setup works with the coordinator passed through by its /dev/serial/by-id path.
