Add EMQX broker setup guide and bring-up watcher
Target broker is EMQX 5.x at 192.168.1.31. Probed its current state and documented it in docs/MQTT_SETUP.md: all listeners up, retained messages working, management API protected (401), but the MQTT data plane has no authenticator -- anonymous connects are accepted, and so are arbitrary made-up credentials. Until an authenticator exists, the broker account values in the local config header are decorative and a successful connection proves nothing about them. The guide covers creating the password-based authenticator, separate soil-node and homeassistant accounts, per-account ACLs (including setting no-match to Deny, without which the rules are cosmetic), pointing the HA MQTT integration at EMQX, and a CONNACK/PubSubClient troubleshooting table. tools/mqtt_watch.py watches the broker directly during bring-up. It answers what the HA UI cannot: whether the retained discovery configs are actually on the broker. Those are what HA rebuilds entities from after a restart, so a node can look healthy today and disappear on the next HA restart if they are missing. It reads its broker account secret from a file or environment variable rather than argv, which is visible process-wide. The broker IP is deliberately kept out of the committed template header, since the Gitea repo is public. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
ad9c457ea7
commit
f6630c547c
3 files changed
+312
No files matched your search
@@ -0,0 +1,174 @@
|
||||
# EMQX + Home Assistant setup
|
||||
|
||||
Broker for this deployment: **EMQX 5.x at 192.168.1.31**.
|
||||
|
||||
## 0. Starting state (probed 2026-08-14)
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Listeners 1883 / 8883 / 8083 / 8084 / 18083 | all open |
|
||||
| Anonymous MQTT connect | **accepted** |
|
||||
| Connect with a made-up username/password | **accepted** |
|
||||
| Retained message publish + replay to a new client | works |
|
||||
| Existing retained topics on the broker | none — broker is empty |
|
||||
| Management API (`/api/v5/clients` etc.) | protected, HTTP 401 |
|
||||
| Public `/api/v5/status` | 200 (normal, by design) |
|
||||
|
||||
**The MQTT data plane has no authenticator configured.** Any client on the LAN
|
||||
can connect, subscribe to `#`, and publish anywhere — including publishing to
|
||||
`homeassistant/...` discovery topics, which means anything on the network can
|
||||
create or overwrite entities in Home Assistant. The dashboard itself is fine;
|
||||
it's only the broker side that is open.
|
||||
|
||||
Until step 1 is done, the `MQTT_USER` / `MQTT_PASSWORD` values in `secrets.h`
|
||||
are decorative — EMQX accepts them without checking, so a working node proves
|
||||
nothing about whether the credentials are right.
|
||||
|
||||
## 1. Enable authentication in EMQX
|
||||
|
||||
Dashboard → **http://192.168.1.31:18083**
|
||||
|
||||
1. **Access Control → Authentication → Create**
|
||||
2. Mechanism **Password-Based**, backend **Built-in Database**
|
||||
3. Configure:
|
||||
- User ID Type: `username`
|
||||
- Password Hash: `bcrypt` (salt rounds 10)
|
||||
4. Create.
|
||||
|
||||
The moment an authenticator exists, EMQX stops accepting anonymous
|
||||
connections — so create the users in step 2 before flashing anything, and
|
||||
expect Home Assistant to drop off the broker until you give it credentials too.
|
||||
|
||||
## 2. Create one account per client
|
||||
|
||||
Under the authenticator you just made → **Users → Add**:
|
||||
|
||||
| Username | Used by |
|
||||
|---|---|
|
||||
| `soil-node` | every ESP32 node (they share this account; the MQTT *client id* is what stays unique, derived from each MAC) |
|
||||
| `homeassistant` | the HA MQTT integration |
|
||||
|
||||
Separate accounts matter here: it lets you write the ACLs in step 3, and it
|
||||
means rotating the node credential doesn't knock HA off the broker.
|
||||
|
||||
Use long random passwords. Generate them where they will not land in a
|
||||
terminal transcript, e.g. write straight to a file:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 24 > ~/soil-node.pass # then open the file to read it
|
||||
```
|
||||
|
||||
## 3. Restrict what each account may do (recommended)
|
||||
|
||||
**Access Control → Authorization → Built-in Database**, then add rules.
|
||||
|
||||
For `soil-node` — it only ever publishes, and never subscribes:
|
||||
|
||||
| Action | Topic | Permission |
|
||||
|---|---|---|
|
||||
| Publish | `soil/#` | Allow |
|
||||
| Publish | `homeassistant/sensor/#` | Allow |
|
||||
| Subscribe | `#` | Deny |
|
||||
|
||||
For `homeassistant`:
|
||||
|
||||
| Action | Topic | Permission |
|
||||
|---|---|---|
|
||||
| Subscribe | `homeassistant/#` | Allow |
|
||||
| Subscribe | `soil/#` | Allow |
|
||||
| Publish | `homeassistant/status` | Allow |
|
||||
|
||||
Then set **no-match action to `Deny`** in the authorization settings.
|
||||
Leaving it at the default `Allow` makes the rules above cosmetic — anything
|
||||
not explicitly matched is still permitted.
|
||||
|
||||
## 4. Fill in `include/secrets.h`
|
||||
|
||||
Already pre-filled with the broker address. Replace the four `CHANGEME` values:
|
||||
|
||||
```c
|
||||
#define WIFI_SSID "..."
|
||||
#define WIFI_PASSWORD "..."
|
||||
#define MQTT_HOST "192.168.1.31"
|
||||
#define MQTT_PORT 1883
|
||||
#define MQTT_USER "soil-node"
|
||||
#define MQTT_PASSWORD "..."
|
||||
```
|
||||
|
||||
This file is gitignored. The broker IP is deliberately **not** in
|
||||
`secrets.h.example`, because that template is committed and the Gitea repo is
|
||||
public.
|
||||
|
||||
### Why port 1883 and not 8883
|
||||
|
||||
TLS is available on 8883, but PubSubClient over `WiFiClientSecure` means
|
||||
shipping a CA bundle and paying a full handshake on every deep-sleep wake —
|
||||
meaningful battery cost on the remote nodes for a sensor reading that is not
|
||||
confidential, on a LAN. Plain 1883 with per-account ACLs is the sensible trade
|
||||
here. Revisit if these ever cross a network you don't control.
|
||||
|
||||
## 5. Point Home Assistant at EMQX
|
||||
|
||||
Settings → Devices & Services → **MQTT** → Configure.
|
||||
|
||||
- Broker `192.168.1.31`, port `1883`
|
||||
- Username `homeassistant`, password from step 2
|
||||
- Enable discovery, discovery prefix `homeassistant` (must match
|
||||
`HA_DISCOVERY_PREFIX` in `include/config.h`)
|
||||
|
||||
If MQTT is already configured against a *different* broker, repoint it —
|
||||
nodes publishing to EMQX will never appear in an HA that is listening to
|
||||
Mosquitto somewhere else. This is the single most common reason a node looks
|
||||
healthy on the wire but produces no entities.
|
||||
|
||||
## 6. Flash and verify
|
||||
|
||||
```bash
|
||||
pio run -e mains -t upload -t monitor
|
||||
```
|
||||
|
||||
Expected serial output:
|
||||
|
||||
```
|
||||
=== Soil Node A1B2C3 (aa:bb:cc:a1:b2:c3) fw 1.0.0 ===
|
||||
profile: mains | publishing every 60s
|
||||
wifi: 192.168.1.x, rssi -55
|
||||
mqtt: connecting to 192.168.1.31:1883 as soil-node-a1b2c3
|
||||
discovery: publishing entities for soil-node-a1b2c3
|
||||
state: {"node_id":"soil-node-a1b2c3","moisture_1":...}
|
||||
```
|
||||
|
||||
Watch the broker side independently:
|
||||
|
||||
```bash
|
||||
python3 tools/mqtt_watch.py --host 192.168.1.31 --user soil-node
|
||||
```
|
||||
|
||||
Then confirm in HA: Settings → Devices & Services → MQTT → *Soil Node XXXXXX*
|
||||
with 6 moisture entities plus the diagnostics.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
`mqtt: connect failed, state N` — N is PubSubClient's code:
|
||||
|
||||
| N | Meaning | Likely cause |
|
||||
|---|---|---|
|
||||
| 4 | Bad credentials | `MQTT_USER`/`MQTT_PASSWORD` don't match the EMQX user |
|
||||
| 5 | Not authorized | Authenticated, but ACL denied the connection |
|
||||
| 3 | Server unavailable | Broker reachable but refusing |
|
||||
| -2 | Connect failed | TCP never established — wrong IP, firewall, VLAN |
|
||||
| -4 | Connection timeout | Broker accepted TCP but never sent CONNACK |
|
||||
|
||||
**Node publishes but no entities in HA.** Almost always HA pointed at a
|
||||
different broker (step 5), or a discovery-prefix mismatch. Check that
|
||||
`homeassistant/sensor/soil-node-xxxxxx/moisture_1/config` exists on the broker
|
||||
with the watcher script.
|
||||
|
||||
**Entities appear then go *unavailable*.** On the mains profile that is the
|
||||
availability topic plus last-will doing its job — the node really did drop.
|
||||
On the battery profile it means `expire_after` elapsed, so the node missed
|
||||
more than ~2.5 sleep intervals.
|
||||
|
||||
**Entities show *unknown* rather than a number.** That is the firmware
|
||||
reporting a probe outside its calibrated window as `null` on purpose, not a
|
||||
transport problem. See `CALIBRATION.md`.
|
||||
Reference in new issue
Block a user