Files
app/docs/getting-started/facility-automation.md
T
stroblmeandClaude Opus 5 bdad6d7fc2
Docs / docs (push) Successful in 37s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m35s
Playwright Tests / test-playwright (2, 2) (push) Failing after 17s
pre-commit / pre-commit (push) Failing after 2m8s
Test Backend / test-backend (push) Failing after 2m48s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m25s
Make the docs state things rather than argue them
The site read as a design journal: rationale paragraphs, hedges
("deliberately", "on purpose", "genuinely"), meta-commentary about the docs
themselves, and one em-dash every ten lines carrying an aside.

Roughly twenty rationale blocks are gone or reduced to what a reader needs
in order to use the thing. Em-dashes go from 507 to 135, and what is left is
structural rather than prose: list and definition separators, table cells,
and four inside code blocks that quote what the CLI actually prints.

Also: api.example.com becomes api.fluksio.com (the emails stay, since
bootstrap.py really defaults to admin@example.com and RFC 2606 reserves it);
the mqtt table gains the two settings it had drifted behind on and inject's
wording matches the engine; llms.txt lists the two connector pages that were
in the nav but not in it; and the two device/device_policy notes now agree.

Builds clean under `zensical build --strict`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
2026-08-31 10:49:58 +02:00

12 KiB

Getting started: facility automation

You have a box in a cupboard, a handful of sensors that already publish somewhere, and an ambition to make the house do something about them. This page brings up a Fluksio instance you can leave running for years, then wires the first sensor through to a dashboard.

Budget an afternoon. Most of it is the browser, which is the point: you will be in this interface a lot, so it is worth learning it properly.

What you are standing up

   sensors ──MQTT──▶  ┌──────────────┐  ──▶ InfluxDB   (history)
                      │ Fluksio      │
   HTTP / webhooks ──▶│  flow engine │  ──▶ dashboards (what you look at)
                      │              │
   schedules ────────▶└──────────────┘  ──▶ MQTT / HTTP (what you control)
                             │
                             └── workers on other boxes (optional)

One engine holds every flow. Nothing here is a plugin you install separately: the broker client, the time-series writer, the dashboards and the alerting are all part of the same process, editing the same graph.

Prerequisites

  • Docker and Compose v2 on the host
  • The address you will type into a browser. On a home network that is the server's own address, 192.168.1.50, and nothing else is needed. A hostname is optional, and covered below
  • Optionally: an MQTT broker and an InfluxDB you already run. If not, the stack can start both for you

Configure it

Clone the app repository and write its one configuration file:

git clone https://git.stroblme.de/Fluksio/app.git ~/fluksio
cd ~/fluksio
cp .env.example .env
$EDITOR .env                # FIRST_SUPERUSER, ENVIRONMENT=production

.env is the whole configuration. The four settings that matter on day one:

Setting What it does
DOMAIN the hostname everything is served under, if you use one. Left alone it only names the issuer that agents authenticate against
FIRST_SUPERUSER the account you sign in with
FIRST_SUPERUSER_PASSWORD leave it as changethis and one is generated for you
ENVIRONMENT production closes the interactive API schema; local leaves it open

Everything the instance owns (the database, your flows, secrets, artifacts, the packages your node code imports) is on one Docker volume. Backing that volume up is backing up the instance.

??? note "Even smaller: no Docker at all"

`pip install fluksio && fluksio serve` gives you the same engine with no
containers, keeping its data in `~/.fluksio`. What it does not give you is
the web interface, which the SPA container serves, so you would drive it
from the API, or [pair it with a portal](../interface/portal.md) that
serves the dashboard for you. Good for a Raspberry Pi that only runs flows;
less good as your main instance.

Reach it from another machine

Start it one of two ways, depending on whether you have a hostname to point at it. Neither is more supported than the other.

On your own network, by address

The usual case: the box is in a cupboard at 192.168.1.50 and you open it from a laptop. Layer docker/compose.lan.yml on, which publishes the interface on a port and serves the API from that same origin:

APP_PORT=8080 docker compose -p fluksio-app --env-file .env \
  -f docker/compose.yml -f docker/compose.lan.yml up -d --build

Open http://192.168.1.50:8080. That one address is the whole application: the interface, the API under /api, and the websocket that puts live values on the canvas.

Nothing in that URL is configured anywhere. The interface is built to address the API relative to whatever origin served the page, so the same containers answer to the machine's address, to a hostname you add later, and through an ssh tunnel, without being rebuilt. It is also why no CORS list needs maintaining: the browser is talking to one origin.

!!! danger "This is your network, not the internet"

A published port is plain HTTP with no certificate. Between your own
machines that is fine, and it is not something to forward from a router. To
reach the house from outside, put a reverse proxy with TLS in front of it,
or [pair it with a portal](../interface/portal.md), which needs no inbound
route at all.

With a hostname, behind a reverse proxy

If you have DNS, or want certificates, set DOMAIN and run make up instead. The interface is then served on app.${DOMAIN} and the API on api.${DOMAIN}, and nothing is published on a host port.

The stack emits Traefik labels and ships a Traefik you can bring up alongside it (docker/compose.traefik.yml). With Nginx Proxy Manager or Caddy instead, attach it to the proxy network and forward app.${DOMAIN}fluksio-app:80 and api.${DOMAIN}fluksio-api:8000.

!!! tip "Developing on the same machine"

`make dev-lan` is the same idea for the integrated dev stack: the app on
`http://<host-ip>:8080`, `APP_PORT` to move it. `make dev` rebuilds without
the published port, so re-run `make dev-lan` after one.

Sign in

Open whichever address you chose. You should be looking at Home: an empty brain graph, a health summary, and a flow list with nothing in it.

Your first flow

Go to Flows → New flow and call it house. You land on the canvas.

A flow is a set of nodes that talk to each other through named messages. You do not draw wires: a node says which messages it needs and which it produces, and the canvas draws the graph that follows. Renaming is safe, fan-in is free, and two flows share a value by naming it.

Read a sensor

Press Add node (or ⌘P / Ctrl-P, which opens the command palette) and pick MQTT. In its panel on the right:

  • Broker host — your broker's hostname, mosquitto if you are using the one the stack can start
  • Topic — map each output to a topic: {"living_temperature": "zigbee2mqtt/living/temperature"}
  • Provides — add one output port named living_temperature, type float

That is a working node. Press Publish (⌘S) and the engine picks it up.

The canvas now draws your node with a live value on its output as soon as the broker sends one. Click the wire to see the last payload and its history.

Do something with it

Add a Function node. This is a Python node, and the code editor opens in its panel:

def process(living_temperature, comfortable=21.0):
    """Ask for heat when the room is below the comfort point."""
    return {"heat_wanted": living_temperature < comfortable}

Declare living_temperature as an input (type float) and heat_wanted as an output (type bool). comfortable is not a port but a setting, because it is a constant of this node rather than something the graph carries. It shows up as a field in the node's Settings section.

The canvas now draws MQTT → your function, because the message names line up. Nothing else was needed.

Act on it

Add a second MQTT node, this time with heat_wanted as an input, and a topic mapping to whatever your relay listens on. A node with inputs publishes; a node with outputs subscribes.

Publish the flow. You have a thermostat.

!!! warning "Test before it touches a relay"

A flow can be **paused** (it holds messages instead of running them) and
**stepped** (release exactly one). Together with the run button, which
injects a value by hand, that is how you convince yourself the logic is
right before the contactor finds out. Both live on the dock at the bottom
of the canvas.

Store the history

A live value is enough to control something and useless for answering "was last February colder?". That is what the InfluxDB node is for.

Add one, and configure it to write the message you already have:

{
  "url": "http://influxdb:8086",
  "token": {"$secret": "influx-token"},
  "org": "home",
  "bucket": "sensors",
  "writes": {
    "living_temperature": {
      "measurement": "environment",
      "field": "temp_c",
      "tags": {"room": "living"}
    }
  }
}

Give it living_temperature as an input and every value that passes gets a point.

Note the token. Credentials never sit in a flow: {"$secret": "influx-token"} is a reference into an encrypted store, and the editor renders those fields as a secret picker. Add the actual value once under Secrets. Flows are a git repository you might well push somewhere, so this keeps a password out of it.

Reading back is the same node with queries instead of writes, or, for anything a chart asks for, a pair of small Python nodes on either side that build a Flux query and shape its rows. That indirection is deliberate: the database node holds the connection and nothing else, so a dashboard widget never learns which database answered it.

Put it on a screen

Dashboards → New dashboard, then drag widgets onto the grid and bind each one to a message. A gauge on house.living_temperature, a switch on house.heat_wanted, a chart on the history.

Widgets are typed the same way ports are: a switch binds to a bool, a gauge to a number, an agenda to a list. Bind it wrong and the editor says so rather than drawing nothing.

Controls work in the other direction: a switch on a dashboard publishes the message it is bound to, exactly as a node would. The canvas draws it as a labelled endpoint feeding the nodes that read it, so a value never appears from nowhere.

For a tablet on the wall, see Dashboards and panels: a panel is a named device, it pairs with a six-character code instead of a login, and it can only reach the dashboards you gave it.

Spread it across machines

You now have one box doing everything. Two reasons to change that: something lives on a different network, or something needs hardware the engine's host does not have.

The unit of distribution is the worker. It runs the code of nodes you mark for it, and it dials out to the engine, so the Pi in the shed does not need an inbound route, and the engine does not need to reach it.

On the engine, mint a token:

curl -X POST https://api.${DOMAIN}/api/v1/workers/tokens \
  -H "Authorization: Bearer $TOKEN" -d '{"name": "shed-pi"}'

On the other machine:

pip install fluksio-worker
fluksio-worker \
  --url wss://api.${DOMAIN}/api/v1/workers/attach \
  --token "$WORKER_TOKEN" \
  --labels shed,gpio \
  --parallel 2

Then mark the node that talks to the shed's GPIO with device: shed, and it runs there. Everything else stays where it is. A node bound to a label no attached worker carries simply waits rather than failing, so you can write the flow before the hardware arrives.

!!! note "What a worker is not"

It is not a second engine. Subscriptions, schedules, webhooks and the
dashboards all stay in one process, which keeps a value having one
definition. A worker executes node bodies, nothing else. Scaling *the
engine* to several processes is not supported: run one.

Make it tell you when something breaks

Under Alerts, add a channel and a rule. Channels are ntfy (a push notification on your phone), SMTP, a webhook, or a message a dashboard notification widget reads.

What you probably want on day one is everything, to ntfy:

  • add an ntfy channel with your server and topic
  • add a rule with no events ticked, which means all of them

The engine deduplicates aggressively, so the same node failing every second is one alert rather than thirty-six thousand, and caps the total at ten an hour however bad it gets. See Secrets, modules and alerts.

Keep it alive

  • Back up the data volume. That is the database, the flows, the secrets and the artifacts. Everything else is rebuildable.
  • Watch Home. The health summary names what is wrong — a quarantined flow, a node that will not load, a stalled queue, a flow that cannot run because its graph does not validate.
  • Flows are git. git log inside the flow store is the history of every change anyone made, and reverting one is a revert.

Where to go next