Files
kvida-os/components/transport/README.md
Ronny Eia 1bdc19b695 Add MQTT client + Home Assistant Discovery, verified end-to-end
Adds espressif/mqtt and mdns as the project's first external managed
component dependencies (neither ships with ESP-IDF core in v6.0.2
anymore). Once Wi-Fi connects, the device advertises itself on the LAN
as kvida-xxxxxx.local and serves a persistent settings page there for
MQTT broker host/port and credentials -- the ongoing config channel
beyond the one-time Wi-Fi captive portal.

mDNS auto-discovery of _mqtt._tcp was the original plan, but verified
against a real Home Assistant OS + Mosquitto add-on that it doesn't
reliably answer PTR queries on the LAN (confirmed with a raw mDNS
query from the host, parsed properly with dnspython after an earlier
naive byte-check gave a false positive). Manual host/port entry is
now the primary path; mDNS discovery still runs as a secondary
fallback attempt, retried every 30s, in case it works on other
networks.

On MQTT_EVENT_CONNECTED, publishes retained HA MQTT Discovery for a
diagnostic "connectivity" binary_sensor (grouped under a Kvida/Xylon
device block) plus an online/offline availability topic driven by
MQTT's own LWT. Verified end-to-end on real hardware: the device
appears in Home Assistant with a "Connected" Connectivity sensor.

Also extracts url_decode() (previously local to commissioning.cpp)
and a new device_id() helper into shared headers, since both
commissioning's Wi-Fi form and the new MQTT settings form need them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 18:13:34 +02:00

4.8 KiB

transport

Wi-Fi connectivity, MQTT client, and Home Assistant MQTT Discovery payloads. Exposes a semantic publish(topic, value)-style API — application code (including Lua) never depends on MQTT directly, per the transport abstraction principle in AGENTS.md. No dependency on other Kvida components.

Also owns Wi-Fi/MQTT configuration storage for now (not broken out into its own component since AGENTS.md doesn't call out a separate config layer — revisit if this grows).

Future transports (Zigbee, Thread, Matter) should implement the same publish API as alternate backends behind this component's interface, without changing callers.

Wi-Fi commissioning

include/commissioning.h / src/commissioning.cpp implement first-time Wi-Fi setup, no companion app required:

  • On boot, try_stored_credentials() attempts to connect using credentials saved in NVS from a previous session.
  • If none exist (or the caller chooses to), start_commissioning() opens a SoftAP (Kvida-XXXXXX, derived from the MAC) with a captive portal: a DNS server (src/dns_server.c/.h, vendored from ESP-IDF's official captive_portal example, Unlicense/CC0) answers every query with the AP's own IP, and DHCP option 114 plus a 404-redirects-to-/ HTTP handler get most phones/laptops to auto-open the setup page (src/portal.html) in a plain browser.
  • Submitting the form (POST /connect) saves the credentials to NVS and switches Wi-Fi to STA mode. The AP is open (no password) by design -- see the comment above ap_config.ap.authmode in commissioning.cpp for the reasoning and the tradeoff.
  • A 5-minute esp_timer closes the commissioning window automatically if nobody submits credentials.
  • IPv6: once STA gets its IPv4 address (IP_EVENT_STA_GOT_IP -- not WIFI_EVENT_STA_CONNECTED, see the comment in commissioning.cpp for why that timing matters), esp_netif_create_ip6_linklocal() requests a link-local address (CONFIG_LWIP_IPV6=y is set explicitly in sdkconfig.defaults); IP_EVENT_GOT_IP6 is logged when it arrives. Verified end-to-end on real ESP32-C6 hardware.
  • Not yet wired up: re-entering commissioning on an already-configured device needs a physical trigger (the user button) per AGENTS.md's "configuration mode requires physical user interaction" principle -- blocked on drivers having a real button implementation. Network scanning (SSID dropdown instead of free text) was left out of this first pass to keep scope tight.

MQTT + Home Assistant Discovery

include/mqtt.h / src/mqtt.cpp. Neither esp_mqtt_client (mqtt_client.h) nor mdns.h ship with ESP-IDF core in this version (both were moved to the IDF Component Registry) -- pulled in via idf_component.yml (espressif/mqtt, mdns), the project's first external managed dependencies.

  • Once Wi-Fi STA has an IPv4 address, start_mqtt() advertises the device on the LAN as kvida-xxxxxx.local (mDNS hostname, matching the commissioning AP's SSID) and starts a persistent settings HTTP server there -- the ongoing configuration channel for anything beyond the one-time Wi-Fi bootstrap (MQTT broker/credentials now, more later).
  • Broker address: the settings page has a required host/port field, used directly and given priority over mDNS auto-discovery. mDNS discovery of _mqtt._tcp (mdns_query_ptr) is attempted as a fallback only if no manual host is saved, retried every 30s -- verified against a real Home Assistant + Mosquitto add-on setup that this mDNS discovery does not reliably find the broker (confirmed with a from-scratch raw mDNS query test from the host machine: no PTR answer for _mqtt._tcp.local was ever received on that network), hence the manual field being the primary path rather than a rarely-needed fallback.
  • Credentials: also collected on the same settings page (blank username = anonymous; blank password on a resubmit means "keep the current one", since the password is never echoed back into the page for basic hygiene). Real brokers, including Home Assistant's Mosquitto add-on, require auth -- confirmed on real hardware.
  • On MQTT_EVENT_CONNECTED: publishes (retained) an HA MQTT Discovery config for a diagnostic connectivity binary_sensor, grouped under a device block (identifiers/name = the same kvida-xxxxxx id, manufacturer "Xylon", model "Kvida"), then publishes online to the availability topic. MQTT's own LWT (session.last_will, set at client init) publishes offline to the same topic if the connection drops. Verified end-to-end: device appears in Home Assistant with a "Connected" Connectivity sensor.
  • No generic semantic publish(topic, value) API yet -- deferred until profiles has real sensor data to publish; building it now would be speculative.

TODO: A/B OTA update-checking belongs here, driven by ESP-IDF's native esp_ota_ops against the ota_0/ota_1 partitions in partitions.csv — no extra dependency needed either.