The guide asked for a hostname and served the interface on
`app.${DOMAIN}` with the API on `api.${DOMAIN}` — which is not what
someone with a box in a cupboard sets up, and could not be made to work
by typing an address into a browser. `compose.lan.yml` already solves it
for the dev stack; the same file layers onto the production one, so the
guide now opens with that and keeps the hostname as the other option.
The reference gained the setting behind it. `VITE_API_URL` is a build
argument of the frontend image rather than something the stack reads,
which is worth saying once: empty means the interface addresses the API
relative to whatever origin served it, so one image answers on an
address, a hostname and an ssh tunnel alike, and no CORS list has to be
kept in step.
Verified against the production compose layered with `compose.lan.yml`:
the page, `/api` on the same port, a cross-origin-free login, the flow
websocket upgrading through nginx, and `/docs` still refused there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
318 lines
13 KiB
Markdown
318 lines
13 KiB
Markdown
# 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
|
|
|
|
```text
|
|
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:
|
|
|
|
```sh
|
|
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 installation 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 installation.
|
|
|
|
??? 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:
|
|
|
|
```sh
|
|
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 from those names. That
|
|
sounds like a small difference and turns out to be a large one — renaming is
|
|
safe, fan-in is free, and two flows can share a value by naming it.
|
|
|
|
### Read a sensor
|
|
|
|
Press **Add node** (or ⌘K / Ctrl-K, 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 — the code editor opens in its
|
|
panel:
|
|
|
|
```python
|
|
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 — it is 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:
|
|
|
|
```json
|
|
{
|
|
"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 — this is what 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](../interface/dashboards.md):
|
|
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:
|
|
|
|
```sh
|
|
curl -X POST https://api.${DOMAIN}/api/v1/workers/tokens \
|
|
-H "Authorization: Bearer $TOKEN" -d '{"name": "shed-pi"}'
|
|
```
|
|
|
|
On the other machine:
|
|
|
|
```sh
|
|
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 — that is what 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 — the same node failing every second is
|
|
one alert, not thirty-six thousand — and caps the total at ten an hour however
|
|
bad it gets. See [Secrets, modules and alerts](../interface/operations.md).
|
|
|
|
## 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
|
|
|
|
- [Flows, nodes and messages](../concepts/flows.md) — the model, properly
|
|
- [Node types](../reference/node-types.md) — everything you can put on a canvas
|
|
without writing Python
|
|
- [The flow editor](../interface/flow-editor.md) — the canvas, in detail
|
|
- [Keeping state in a flow](../concepts/state.md) — running totals, debounces,
|
|
and the one rule that makes them safe
|
|
- [Accounts and the portal](../interface/portal.md) — reach the installation
|
|
from outside the house without opening a port
|