docs: correct MQTT example and ESP8266 guidance

This commit is contained in:
2026-09-17 09:11:58 +10:00
parent e6dee4727d
commit e3c9388ffc
8 changed files with 171 additions and 59 deletions
+6 -2
View File
@@ -8,7 +8,11 @@ ArduinoHA lets an Arduino, ESP8266, or ESP32 application publish MQTT discovery
## Start with a sensor ## Start with a sensor
```cpp ```cpp
#if defined(ESP8266)
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
#include <ArduinoHA.h> #include <ArduinoHA.h>
WiFiClient client; WiFiClient client;
@@ -24,11 +28,11 @@ void setup() {
// Your application connects Wi-Fi before MQTT begins. // Your application connects Wi-Fi before MQTT begins.
temperature.setName("Temperature"); temperature.setName("Temperature");
temperature.setUnitOfMeasurement("°C"); temperature.setUnitOfMeasurement("°C");
mqtt.begin("mqtt.local", "mqtt_user", "mqtt_password"); mqtt.begin("mqtt.local", "mqtt_user", "mqtt_password"); // Connection work begins in mqtt.loop().
} }
void loop() { void loop() {
mqtt.loop(); mqtt.loop(); // Maintains MQTT and publishes discovery after connecting.
// Call temperature.setValue(...) when your reading changes. // Call temperature.setValue(...) when your reading changes.
} }
``` ```
+26
View File
@@ -0,0 +1,26 @@
# ESP8266 Postmortem linker workaround
The ESP8266 Arduino framework pin used by the DeviceFramework, WiFiManager,
and DFTE test projects is intentional:
```ini
platform_packages =
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
```
That upstream commit fixes Postmortem's large-jump failure,
[`dangerous relocation: j: cannot encode`](https://github.com/esp8266/Arduino/commit/521ae60a89e64bb0d1eb7a0b7addf620ced5cad3).
It changes the restart wrapper to use a relaxed jump and adds an EPC1 address
check. The failure is a framework linker/runtime-support issue, not an ArduinoHA
or application-source error.
Keep this exact framework snapshot in ESP8266 environments that need the
maintained test contract. It is unrelated to ESP32, whose pioarduino platform
selects its framework and compiler as a unit. Do not replace the SHA with a
version range: remove or advance the pin only after an upstream release includes
the fix and the affected large firmware has compiled successfully.
The corresponding ESP32 Core 3 pin and shared PlatformIO-cache recovery steps
are documented in [DeviceFramework's toolchain guide](https://github.com/alexhopeoconnor/DeviceFramework/blob/main/docs/TOOLCHAINS.md).
Back to the [documentation map](README.md).
+1
View File
@@ -6,6 +6,7 @@
| [Device & discovery](device-and-discovery.md) | `HADevice`, discovery modes, metadata, identifiers, and migration | | [Device & discovery](device-and-discovery.md) | `HADevice`, discovery modes, metadata, identifiers, and migration |
| [MQTT usage](mqtt-usage.md) | Callbacks, custom topics, availability, logging, and footprint flags | | [MQTT usage](mqtt-usage.md) | Callbacks, custom topics, availability, logging, and footprint flags |
| [Entities](entities.md) | Supported Home Assistant entity classes and the best matching example | | [Entities](entities.md) | Supported Home Assistant entity classes and the best matching example |
| [ESP8266 linker workaround](ESP8266-LINKER-WORKAROUND.md) | Exact framework pin for the Postmortem large-jump fix |
| [Examples](../examples/README.md) | Guided PlatformIO projects and focused entity recipes | | [Examples](../examples/README.md) | Guided PlatformIO projects and focused entity recipes |
| [Compatibility baseline](compatibility.md) | Supported Home Assistant capabilities and fork-specific compatibility notes | | [Compatibility baseline](compatibility.md) | Supported Home Assistant capabilities and fork-specific compatibility notes |
+15 -9
View File
@@ -40,8 +40,11 @@ Defaults:
Override before `begin()` if needed: Override before `begin()` if needed:
```cpp ```cpp
mqtt.setDiscoveryPrefix("myHaPrefix"); void configureTopicPrefixes() {
mqtt.setDataPrefix("myDataPrefix"); // Set both before mqtt.begin(...) publishes any discovery data.
mqtt.setDiscoveryPrefix("myHaPrefix");
mqtt.setDataPrefix("myDataPrefix");
}
``` ```
### Single-component vs device discovery ### Single-component vs device discovery
@@ -106,13 +109,16 @@ The migration stage is held in RAM. If the board reboots before completion, star
Device discovery can also publish richer origin/device metadata, for example: Device discovery can also publish richer origin/device metadata, for example:
```cpp ```cpp
device.setModelId("esp32-s3-devkit"); void configureDeviceDiscovery() {
device.setHardwareVersion("rev-b"); // These strings are borrowed, so keep literals or other long-lived storage.
device.setSerialNumber("SN-00042"); device.setModelId("esp32-s3-devkit");
device.setSuggestedArea("Garage"); device.setHardwareVersion("rev-b");
device.setViaDevice("main_gateway"); device.setSerialNumber("SN-00042");
device.addConnection("mac", "AA:BB:CC:DD:EE:FF"); device.setSuggestedArea("Garage");
mqtt.setOriginSupportUrl("https://example.com/device-help"); device.setViaDevice("main_gateway");
device.addConnection("mac", "AA:BB:CC:DD:EE:FF");
mqtt.setOriginSupportUrl("https://example.com/device-help");
}
``` ```
For entity identifiers in Home Assistant, prefer **`setDefaultEntityId()`** over legacy **`setObjectId()`**. For entity identifiers in Home Assistant, prefer **`setDefaultEntityId()`** over legacy **`setObjectId()`**.
+12 -5
View File
@@ -40,20 +40,24 @@ HADevice device(mac, sizeof(mac));
HAMqtt mqtt(client, device); HAMqtt mqtt(client, device);
void setup() { void setup() {
Ethernet.begin(mac); Ethernet.begin(mac); // Bring up the network Client before MQTT can connect.
mqtt.begin("192.168.1.50", "mqtt_user", "mqtt_password"); mqtt.begin("192.168.1.50", "mqtt_user", "mqtt_password"); // Connection begins in mqtt.loop().
} }
void loop() { void loop() {
Ethernet.maintain(); Ethernet.maintain(); // Renews DHCP leases where the Ethernet library requires it.
mqtt.loop(); mqtt.loop(); // Services MQTT connection, discovery, and entity traffic.
} }
``` ```
### ESP8266 / ESP32 (example) ### ESP8266 / ESP32 (example)
```cpp ```cpp
#if defined(ESP8266)
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
#include <ArduinoHA.h> #include <ArduinoHA.h>
WiFiClient client; WiFiClient client;
@@ -66,15 +70,18 @@ void setup() {
device.setUniqueId(mac, sizeof(mac)); device.setUniqueId(mac, sizeof(mac));
WiFi.begin("SSID", "password"); WiFi.begin("SSID", "password");
// Keep this blocking wait only in a minimal sketch. Production firmware
// should retry, time out, or hand control to its provisioning flow.
while (WiFi.status() != WL_CONNECTED) { while (WiFi.status() != WL_CONNECTED) {
delay(500); delay(500);
} }
// begin() stores broker settings; mqtt.loop() performs connection and recovery.
mqtt.begin("192.168.1.50", "mqtt_user", "mqtt_password"); mqtt.begin("192.168.1.50", "mqtt_user", "mqtt_password");
} }
void loop() { void loop() {
mqtt.loop(); mqtt.loop(); // Service reconnects, subscriptions, and discovery publishing.
} }
``` ```
+76 -40
View File
@@ -2,21 +2,25 @@
## Callbacks and tuning ## Callbacks and tuning
The following snippets extend a sketch that already owns long-lived `HADevice`, `HAMqtt`, and entity objects. Configure callbacks, discovery metadata, and availability before `mqtt.begin(...)`; call the publishing snippets later from normal application logic.
`HAMqtt` supports optional callbacks and PubSubClient tuning: `HAMqtt` supports optional callbacks and PubSubClient tuning:
```cpp ```cpp
// These callbacks are registered before mqtt.begin(...).
void onMessage(const char* topic, const uint8_t* payload, uint16_t length) { /* ... */ } void onMessage(const char* topic, const uint8_t* payload, uint16_t length) { /* ... */ }
void onConnected() { /* ... */ } void onConnected() { /* ... */ }
void onDisconnected() { /* ... */ } void onDisconnected() { /* ... */ }
void onStateChanged(HAMqtt::ConnectionState state) { /* ... */ } void onStateChanged(HAMqtt::ConnectionState state) { /* ... */ }
void setup() { void setup() {
// A successful reconnect creates a new MQTT session, so onConnected runs again.
mqtt.onMessage(onMessage); mqtt.onMessage(onMessage);
mqtt.onConnected(onConnected); mqtt.onConnected(onConnected);
mqtt.onDisconnected(onDisconnected); mqtt.onDisconnected(onDisconnected);
mqtt.onStateChanged(onStateChanged); mqtt.onStateChanged(onStateChanged);
mqtt.setBufferSize(512); // default 256 mqtt.setBufferSize(512); // Increase only when a larger MQTT packet is required.
mqtt.setKeepAlive(60); // seconds, default 15 mqtt.setKeepAlive(60); // Seconds; the default is 15.
mqtt.begin("192.168.1.50", "user", "pass"); mqtt.begin("192.168.1.50", "user", "pass");
} }
``` ```
@@ -27,6 +31,7 @@ Subscribe after each successful connection (for example in `onConnected`), becau
```cpp ```cpp
void onConnected() { void onConnected() {
// MQTT subscriptions belong to this session and must be restored after reconnecting.
mqtt.subscribe("my/custom/topic"); mqtt.subscribe("my/custom/topic");
} }
``` ```
@@ -36,8 +41,11 @@ Handle payloads in `onMessage`.
## Publishing arbitrary payloads ## Publishing arbitrary payloads
```cpp ```cpp
mqtt.publish("customTopic", "payload", false); // not retained // Call after mqtt.begin(...); retain controls whether the broker keeps this value.
mqtt.publish("customTopic", "payload", true); // retained void publishApplicationState() {
mqtt.publish("customTopic", "payload", false); // Not retained.
mqtt.publish("customTopic", "payload", true); // Retained.
}
``` ```
## Availability ## Availability
@@ -45,11 +53,14 @@ mqtt.publish("customTopic", "payload", true); // retained
**Shared availability (recommended):** one availability topic for the whole device — works well with **Last Will** (LWT): **Shared availability (recommended):** one availability topic for the whole device — works well with **Last Will** (LWT):
```cpp ```cpp
device.enableSharedAvailability(); // Configure device-wide availability before mqtt.begin(...).
device.setPayloadAvailable("up"); void configureSharedAvailability() {
device.setPayloadNotAvailable("down"); device.enableSharedAvailability();
device.enableLastWill(); // broker publishes offline when TCP drops device.setPayloadAvailable("up");
// device.setAvailability(false); // optional: start as offline device.setPayloadNotAvailable("down");
device.enableLastWill(); // Broker publishes offline when TCP drops.
// device.setAvailability(false); // Optional: start as offline.
}
``` ```
**Per-entity availability:** call `someEntity.setAvailability(true/false)` on each type. Does not use LWT the same way as shared mode; see examples under `examples/availability/`. **Per-entity availability:** call `someEntity.setAvailability(true/false)` on each type. Does not use LWT the same way as shared mode; see examples under `examples/availability/`.
@@ -57,16 +68,22 @@ device.enableLastWill(); // broker publishes offline when TCP drops
Custom per-entity payloads are supported: Custom per-entity payloads are supported:
```cpp ```cpp
sensor.setPayloadAvailable("ready"); // Configure the relevant entity before mqtt.begin(...).
sensor.setPayloadNotAvailable("lost"); void configureSensorAvailability() {
sensor.setPayloadAvailable("ready");
sensor.setPayloadNotAvailable("lost");
}
``` ```
For multi-topic availability discovery, add full MQTT topics and a mode: For multi-topic availability discovery, add full MQTT topics and a mode:
```cpp ```cpp
sensor.setAvailabilityMode("all"); // `sensor` is the entity whose availability depends on these external topics.
sensor.addAvailabilityEntry("bridge/status"); void configureExternalAvailability() {
sensor.addAvailabilityEntry("sensor/status", "{{ value_json.state }}"); sensor.setAvailabilityMode("all");
sensor.addAvailabilityEntry("bridge/status");
sensor.addAvailabilityEntry("sensor/status", "{{ value_json.state }}");
}
``` ```
## Discovery helpers by entity ## Discovery helpers by entity
@@ -74,40 +91,53 @@ sensor.addAvailabilityEntry("sensor/status", "{{ value_json.state }}");
Common entity discovery metadata is available on most entity classes: Common entity discovery metadata is available on most entity classes:
```cpp ```cpp
entity.setEnabledByDefault(false); // `entity` is an entity type that supports these common discovery fields.
entity.setEntityPicture("https://example.com/entity.png"); void configureCommonDiscovery(HABaseDeviceType& entity) {
entity.setQos(1); entity.setEnabledByDefault(false);
entity.setEncoding("utf-8"); entity.setEntityPicture("https://example.com/entity.png");
entity.setEntityCategory("diagnostic"); entity.setQos(1);
entity.setEncoding("utf-8");
entity.setEntityCategory("diagnostic");
}
``` ```
Read-only sensor presentation/template helpers: Read-only sensor presentation/template helpers:
```cpp ```cpp
sensor.setSuggestedDisplayPrecision(2); // `sensor` is a read-only HASensor instance.
sensor.setValueTemplate("{{ value_json.temperature }}"); void configureSensorPresentation(HASensor& sensor) {
sensor.setJsonAttributesTemplate("{{ value_json.attrs | tojson }}"); sensor.setSuggestedDisplayPrecision(2);
sensor.setLastResetValueTemplate("{{ value_json.last_reset }}"); sensor.setValueTemplate("{{ value_json.temperature }}");
sensor.setDeviceClass("enum"); sensor.setJsonAttributesTemplate("{{ value_json.attrs | tojson }}");
sensor.setOptions("idle;charging;discharging;fault"); sensor.setLastResetValueTemplate("{{ value_json.last_reset }}");
sensor.setDeviceClass("enum");
sensor.setOptions("idle;charging;discharging;fault");
}
``` ```
Writable entity template/payload helpers: Writable entity template/payload helpers:
```cpp ```cpp
mySwitch.setPayloadOn("ENABLE"); // These names refer to the matching writable entity instances in the application.
mySwitch.setPayloadOff("DISABLE"); void configureWritableEntities(HASwitch& mySwitch,
mySwitch.setStateOn("running"); HANumber& myNumber,
mySwitch.setStateOff("stopped"); HASelect& mySelect,
mySwitch.setValueTemplate("{{ value_json.state }}"); HAText& myText,
mySwitch.setCommandTemplate("{{ value_json.command }}"); HAButton& myButton) {
mySwitch.setPayloadOn("ENABLE");
mySwitch.setPayloadOff("DISABLE");
mySwitch.setStateOn("running");
mySwitch.setStateOff("stopped");
mySwitch.setValueTemplate("{{ value_json.state }}");
mySwitch.setCommandTemplate("{{ value_json.command }}");
myNumber.setPayloadReset("RESET"); myNumber.setPayloadReset("RESET");
myNumber.setCommandTemplate("{{ value | float | round(1) }}"); myNumber.setCommandTemplate("{{ value | float | round(1) }}");
mySelect.setCommandTemplate("{{ value_json.choice }}"); mySelect.setCommandTemplate("{{ value_json.choice }}");
myText.setCommandTemplate("{{ value_json.text }}"); myText.setCommandTemplate("{{ value_json.text }}");
myButton.setPayloadPress("PRESS"); myButton.setPayloadPress("PRESS");
}
``` ```
## Compiler macros ## Compiler macros
@@ -121,12 +151,9 @@ Defined in `ArduinoHADefines.h` or via build flags.
lifecycle callbacks remain ordinary function pointers. lifecycle callbacks remain ordinary function pointers.
- **`ARDUINOHA_DEBUG`** — enables ArduinoHA logging by default and sets the initial maximum verbosity to `Debug`. Without this flag, structured logs are compiled in but remain disabled until you call `arduinoHASetLogEnabled(true)`. - **`ARDUINOHA_DEBUG`** — enables ArduinoHA logging by default and sets the initial maximum verbosity to `Debug`. Without this flag, structured logs are compiled in but remain disabled until you call `arduinoHASetLogEnabled(true)`.
Structured logging is available through: Structured logging is available through a long-lived sink:
```cpp ```cpp
arduinoHASetLogEnabled(true);
arduinoHASetLogLevel(ArduinoHALogLevel::Trace);
class MyLogSink : public ArduinoHALogSink { class MyLogSink : public ArduinoHALogSink {
public: public:
void log(const ArduinoHALogMessage& msg) override { void log(const ArduinoHALogMessage& msg) override {
@@ -138,6 +165,15 @@ public:
Serial.println(msg.text); Serial.println(msg.text);
} }
}; };
MyLogSink logSink; // The installed sink must outlive ArduinoHA logging.
void setup() {
Serial.begin(115200);
arduinoHASetLogSink(&logSink);
arduinoHASetLogEnabled(true);
arduinoHASetLogLevel(ArduinoHALogLevel::Trace);
}
``` ```
`arduinoHALog(...)` and `arduinoHALogf(...)` support subsystem-tagged messages such as `mqtt`, `discovery`, `availability`, `serializer`, `entity`, and `device`. Install a sink when you want those messages to join your application’s normal serial or structured log stream. `arduinoHALog(...)` and `arduinoHALogf(...)` support subsystem-tagged messages such as `mqtt`, `discovery`, `availability`, `serializer`, `entity`, and `device`. Install a sink when you want those messages to join your application’s normal serial or structured log stream.
+4 -2
View File
@@ -12,12 +12,14 @@ HAMqtt mqtt(client, device);
void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) { void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) {
// This callback is called when message from MQTT broker is received. // This callback is called when message from MQTT broker is received.
// Please note that you should always verify if the message's topic is the one you expect. // Please note that you should always verify if the message's topic is the one you expect.
// For example: if (memcmp(topic, "myCustomTopic") == 0) { ... } // For example: if (strcmp(topic, "myCustomTopic") == 0) { ... }
Serial.print("New message on topic: "); Serial.print("New message on topic: ");
Serial.println(topic); Serial.println(topic);
Serial.print("Data: "); Serial.print("Data: ");
Serial.println((const char*)payload); // MQTT payloads are length-delimited; they are not guaranteed to end in NUL.
Serial.write(payload, length);
Serial.println();
mqtt.publish("myPublishTopic", "hello"); mqtt.publish("myPublishTopic", "hello");
} }
+31 -1
View File
@@ -8,6 +8,32 @@ required=(
examples/README.md examples/README.md
) )
check_cpp_fence_scope() {
local markdown="$1"
awk '
function brace_delta(line, copy) {
copy = line
return gsub(/\{/, "{", copy) - gsub(/\}/, "}", copy)
}
/^```cpp[[:space:]]*$/ { in_cpp = 1; depth = 0; next }
in_cpp && /^```[[:space:]]*$/ { in_cpp = 0; next }
in_cpp {
line = $0
sub(/^[[:space:]]+/, "", line)
if (depth == 0 &&
(line ~ /^(if|for|while|switch)[[:space:]]*\(/ ||
line ~ /^[A-Za-z_][A-Za-z0-9_:]*::[A-Za-z0-9_]+[[:space:]]*\(/ ||
line ~ /^[A-Za-z_][A-Za-z0-9_]*\./ ||
line ~ /^[A-Za-z_][A-Za-z0-9_]*[[:space:]]*\(/)) {
printf "%s:%d: C++ expression appears at namespace scope; wrap it in a function.\n", FILENAME, FNR > "/dev/stderr"
failed = 1
}
depth += brace_delta($0)
}
END { exit failed }
' "$markdown"
}
for path in "${required[@]}"; do for path in "${required[@]}"; do
[[ -f "$root/$path" ]] || { echo "Missing required documentation: $path" >&2; exit 1; } [[ -f "$root/$path" ]] || { echo "Missing required documentation: $path" >&2; exit 1; }
done done
@@ -22,7 +48,7 @@ while IFS= read -r -d '' markdown; do
esac esac
[[ -e "$candidate" ]] || { echo "Broken relative link in ${markdown#$root/}: $target" >&2; exit 1; } [[ -e "$candidate" ]] || { echo "Broken relative link in ${markdown#$root/}: $target" >&2; exit 1; }
done < <(sed -nE 's/.*\]\(([^ )]+)( "[^"]*")?\).*/\1/p' "$markdown") done < <(sed -nE 's/.*\]\(([^ )]+)( "[^"]*")?\).*/\1/p' "$markdown")
done < <(find "$root" -path "$root/.git" -prune -o -name '*.md' -type f -print0) done < <(find "$root" -path "$root/.git" -prune -o -path '*/.pio' -prune -o -name '*.md' -type f -print0)
while IFS= read -r example; do while IFS= read -r example; do
for required in README.md platformio.ini; do for required in README.md platformio.ini; do
@@ -34,4 +60,8 @@ while IFS= read -r example; do
} }
done < <(find "$root/examples" -mindepth 1 -maxdepth 1 -type d -name '[0-9][0-9]-*' -print | sort) done < <(find "$root/examples" -mindepth 1 -maxdepth 1 -type d -name '[0-9][0-9]-*' -print | sort)
while IFS= read -r markdown; do
check_cpp_fence_scope "$markdown"
done < <(find "$root" -path "$root/.git" -prune -o -path '*/.pio' -prune -o -name '*.md' -type f -print)
echo "Documentation links and required files passed" echo "Documentation links and required files passed"