Files
soilMoistureSystem/docs/MQTT_SETUP.md
T
mattandClaude f6630c547c 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>
2026-08-14 14:17:02 -07:00

6.2 KiB

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:

openssl rand -base64 24 > ~/soil-node.pass    # then open the file to read it

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:

#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

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:

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.