mirror of
https://github.com/alexhopeoconnor/arduino-home-assistant.git
synced 2026-10-03 18:42:02 +10:00
docs: correct MQTT example and ESP8266 guidance
This commit is contained in:
@@ -8,7 +8,11 @@ ArduinoHA lets an Arduino, ESP8266, or ESP32 application publish MQTT discovery
|
||||
## Start with a sensor
|
||||
|
||||
```cpp
|
||||
#if defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#elif defined(ESP32)
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
#include <ArduinoHA.h>
|
||||
|
||||
WiFiClient client;
|
||||
@@ -24,11 +28,11 @@ void setup() {
|
||||
// Your application connects Wi-Fi before MQTT begins.
|
||||
temperature.setName("Temperature");
|
||||
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() {
|
||||
mqtt.loop();
|
||||
mqtt.loop(); // Maintains MQTT and publishes discovery after connecting.
|
||||
// Call temperature.setValue(...) when your reading changes.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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).
|
||||
@@ -6,6 +6,7 @@
|
||||
| [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 |
|
||||
| [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 |
|
||||
| [Compatibility baseline](compatibility.md) | Supported Home Assistant capabilities and fork-specific compatibility notes |
|
||||
|
||||
|
||||
@@ -40,8 +40,11 @@ Defaults:
|
||||
Override before `begin()` if needed:
|
||||
|
||||
```cpp
|
||||
mqtt.setDiscoveryPrefix("myHaPrefix");
|
||||
mqtt.setDataPrefix("myDataPrefix");
|
||||
void configureTopicPrefixes() {
|
||||
// Set both before mqtt.begin(...) publishes any discovery data.
|
||||
mqtt.setDiscoveryPrefix("myHaPrefix");
|
||||
mqtt.setDataPrefix("myDataPrefix");
|
||||
}
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```cpp
|
||||
device.setModelId("esp32-s3-devkit");
|
||||
device.setHardwareVersion("rev-b");
|
||||
device.setSerialNumber("SN-00042");
|
||||
device.setSuggestedArea("Garage");
|
||||
device.setViaDevice("main_gateway");
|
||||
device.addConnection("mac", "AA:BB:CC:DD:EE:FF");
|
||||
mqtt.setOriginSupportUrl("https://example.com/device-help");
|
||||
void configureDeviceDiscovery() {
|
||||
// These strings are borrowed, so keep literals or other long-lived storage.
|
||||
device.setModelId("esp32-s3-devkit");
|
||||
device.setHardwareVersion("rev-b");
|
||||
device.setSerialNumber("SN-00042");
|
||||
device.setSuggestedArea("Garage");
|
||||
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()`**.
|
||||
|
||||
+12
-5
@@ -40,20 +40,24 @@ HADevice device(mac, sizeof(mac));
|
||||
HAMqtt mqtt(client, device);
|
||||
|
||||
void setup() {
|
||||
Ethernet.begin(mac);
|
||||
mqtt.begin("192.168.1.50", "mqtt_user", "mqtt_password");
|
||||
Ethernet.begin(mac); // Bring up the network Client before MQTT can connect.
|
||||
mqtt.begin("192.168.1.50", "mqtt_user", "mqtt_password"); // Connection begins in mqtt.loop().
|
||||
}
|
||||
|
||||
void loop() {
|
||||
Ethernet.maintain();
|
||||
mqtt.loop();
|
||||
Ethernet.maintain(); // Renews DHCP leases where the Ethernet library requires it.
|
||||
mqtt.loop(); // Services MQTT connection, discovery, and entity traffic.
|
||||
}
|
||||
```
|
||||
|
||||
### ESP8266 / ESP32 (example)
|
||||
|
||||
```cpp
|
||||
#if defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#elif defined(ESP32)
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
#include <ArduinoHA.h>
|
||||
|
||||
WiFiClient client;
|
||||
@@ -66,15 +70,18 @@ void setup() {
|
||||
device.setUniqueId(mac, sizeof(mac));
|
||||
|
||||
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) {
|
||||
delay(500);
|
||||
}
|
||||
|
||||
// begin() stores broker settings; mqtt.loop() performs connection and recovery.
|
||||
mqtt.begin("192.168.1.50", "mqtt_user", "mqtt_password");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
mqtt.loop();
|
||||
mqtt.loop(); // Service reconnects, subscriptions, and discovery publishing.
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+76
-40
@@ -2,21 +2,25 @@
|
||||
|
||||
## 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:
|
||||
|
||||
```cpp
|
||||
// These callbacks are registered before mqtt.begin(...).
|
||||
void onMessage(const char* topic, const uint8_t* payload, uint16_t length) { /* ... */ }
|
||||
void onConnected() { /* ... */ }
|
||||
void onDisconnected() { /* ... */ }
|
||||
void onStateChanged(HAMqtt::ConnectionState state) { /* ... */ }
|
||||
|
||||
void setup() {
|
||||
// A successful reconnect creates a new MQTT session, so onConnected runs again.
|
||||
mqtt.onMessage(onMessage);
|
||||
mqtt.onConnected(onConnected);
|
||||
mqtt.onDisconnected(onDisconnected);
|
||||
mqtt.onStateChanged(onStateChanged);
|
||||
mqtt.setBufferSize(512); // default 256
|
||||
mqtt.setKeepAlive(60); // seconds, default 15
|
||||
mqtt.setBufferSize(512); // Increase only when a larger MQTT packet is required.
|
||||
mqtt.setKeepAlive(60); // Seconds; the default is 15.
|
||||
mqtt.begin("192.168.1.50", "user", "pass");
|
||||
}
|
||||
```
|
||||
@@ -27,6 +31,7 @@ Subscribe after each successful connection (for example in `onConnected`), becau
|
||||
|
||||
```cpp
|
||||
void onConnected() {
|
||||
// MQTT subscriptions belong to this session and must be restored after reconnecting.
|
||||
mqtt.subscribe("my/custom/topic");
|
||||
}
|
||||
```
|
||||
@@ -36,8 +41,11 @@ Handle payloads in `onMessage`.
|
||||
## Publishing arbitrary payloads
|
||||
|
||||
```cpp
|
||||
mqtt.publish("customTopic", "payload", false); // not retained
|
||||
mqtt.publish("customTopic", "payload", true); // retained
|
||||
// Call after mqtt.begin(...); retain controls whether the broker keeps this value.
|
||||
void publishApplicationState() {
|
||||
mqtt.publish("customTopic", "payload", false); // Not retained.
|
||||
mqtt.publish("customTopic", "payload", true); // Retained.
|
||||
}
|
||||
```
|
||||
|
||||
## 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):
|
||||
|
||||
```cpp
|
||||
device.enableSharedAvailability();
|
||||
device.setPayloadAvailable("up");
|
||||
device.setPayloadNotAvailable("down");
|
||||
device.enableLastWill(); // broker publishes offline when TCP drops
|
||||
// device.setAvailability(false); // optional: start as offline
|
||||
// Configure device-wide availability before mqtt.begin(...).
|
||||
void configureSharedAvailability() {
|
||||
device.enableSharedAvailability();
|
||||
device.setPayloadAvailable("up");
|
||||
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/`.
|
||||
@@ -57,16 +68,22 @@ device.enableLastWill(); // broker publishes offline when TCP drops
|
||||
Custom per-entity payloads are supported:
|
||||
|
||||
```cpp
|
||||
sensor.setPayloadAvailable("ready");
|
||||
sensor.setPayloadNotAvailable("lost");
|
||||
// Configure the relevant entity before mqtt.begin(...).
|
||||
void configureSensorAvailability() {
|
||||
sensor.setPayloadAvailable("ready");
|
||||
sensor.setPayloadNotAvailable("lost");
|
||||
}
|
||||
```
|
||||
|
||||
For multi-topic availability discovery, add full MQTT topics and a mode:
|
||||
|
||||
```cpp
|
||||
sensor.setAvailabilityMode("all");
|
||||
sensor.addAvailabilityEntry("bridge/status");
|
||||
sensor.addAvailabilityEntry("sensor/status", "{{ value_json.state }}");
|
||||
// `sensor` is the entity whose availability depends on these external topics.
|
||||
void configureExternalAvailability() {
|
||||
sensor.setAvailabilityMode("all");
|
||||
sensor.addAvailabilityEntry("bridge/status");
|
||||
sensor.addAvailabilityEntry("sensor/status", "{{ value_json.state }}");
|
||||
}
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```cpp
|
||||
entity.setEnabledByDefault(false);
|
||||
entity.setEntityPicture("https://example.com/entity.png");
|
||||
entity.setQos(1);
|
||||
entity.setEncoding("utf-8");
|
||||
entity.setEntityCategory("diagnostic");
|
||||
// `entity` is an entity type that supports these common discovery fields.
|
||||
void configureCommonDiscovery(HABaseDeviceType& entity) {
|
||||
entity.setEnabledByDefault(false);
|
||||
entity.setEntityPicture("https://example.com/entity.png");
|
||||
entity.setQos(1);
|
||||
entity.setEncoding("utf-8");
|
||||
entity.setEntityCategory("diagnostic");
|
||||
}
|
||||
```
|
||||
|
||||
Read-only sensor presentation/template helpers:
|
||||
|
||||
```cpp
|
||||
sensor.setSuggestedDisplayPrecision(2);
|
||||
sensor.setValueTemplate("{{ value_json.temperature }}");
|
||||
sensor.setJsonAttributesTemplate("{{ value_json.attrs | tojson }}");
|
||||
sensor.setLastResetValueTemplate("{{ value_json.last_reset }}");
|
||||
sensor.setDeviceClass("enum");
|
||||
sensor.setOptions("idle;charging;discharging;fault");
|
||||
// `sensor` is a read-only HASensor instance.
|
||||
void configureSensorPresentation(HASensor& sensor) {
|
||||
sensor.setSuggestedDisplayPrecision(2);
|
||||
sensor.setValueTemplate("{{ value_json.temperature }}");
|
||||
sensor.setJsonAttributesTemplate("{{ value_json.attrs | tojson }}");
|
||||
sensor.setLastResetValueTemplate("{{ value_json.last_reset }}");
|
||||
sensor.setDeviceClass("enum");
|
||||
sensor.setOptions("idle;charging;discharging;fault");
|
||||
}
|
||||
```
|
||||
|
||||
Writable entity template/payload helpers:
|
||||
|
||||
```cpp
|
||||
mySwitch.setPayloadOn("ENABLE");
|
||||
mySwitch.setPayloadOff("DISABLE");
|
||||
mySwitch.setStateOn("running");
|
||||
mySwitch.setStateOff("stopped");
|
||||
mySwitch.setValueTemplate("{{ value_json.state }}");
|
||||
mySwitch.setCommandTemplate("{{ value_json.command }}");
|
||||
// These names refer to the matching writable entity instances in the application.
|
||||
void configureWritableEntities(HASwitch& mySwitch,
|
||||
HANumber& myNumber,
|
||||
HASelect& mySelect,
|
||||
HAText& myText,
|
||||
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.setCommandTemplate("{{ value | float | round(1) }}");
|
||||
myNumber.setPayloadReset("RESET");
|
||||
myNumber.setCommandTemplate("{{ value | float | round(1) }}");
|
||||
|
||||
mySelect.setCommandTemplate("{{ value_json.choice }}");
|
||||
myText.setCommandTemplate("{{ value_json.text }}");
|
||||
myButton.setPayloadPress("PRESS");
|
||||
mySelect.setCommandTemplate("{{ value_json.choice }}");
|
||||
myText.setCommandTemplate("{{ value_json.text }}");
|
||||
myButton.setPayloadPress("PRESS");
|
||||
}
|
||||
```
|
||||
|
||||
## Compiler macros
|
||||
@@ -121,12 +151,9 @@ Defined in `ArduinoHADefines.h` or via build flags.
|
||||
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)`.
|
||||
|
||||
Structured logging is available through:
|
||||
Structured logging is available through a long-lived sink:
|
||||
|
||||
```cpp
|
||||
arduinoHASetLogEnabled(true);
|
||||
arduinoHASetLogLevel(ArduinoHALogLevel::Trace);
|
||||
|
||||
class MyLogSink : public ArduinoHALogSink {
|
||||
public:
|
||||
void log(const ArduinoHALogMessage& msg) override {
|
||||
@@ -138,6 +165,15 @@ public:
|
||||
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.
|
||||
|
||||
@@ -12,12 +12,14 @@ HAMqtt mqtt(client, device);
|
||||
void onMqttMessage(const char* topic, const uint8_t* payload, uint16_t length) {
|
||||
// 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.
|
||||
// For example: if (memcmp(topic, "myCustomTopic") == 0) { ... }
|
||||
// For example: if (strcmp(topic, "myCustomTopic") == 0) { ... }
|
||||
|
||||
Serial.print("New message on topic: ");
|
||||
Serial.println(topic);
|
||||
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");
|
||||
}
|
||||
|
||||
+31
-1
@@ -8,6 +8,32 @@ required=(
|
||||
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
|
||||
[[ -f "$root/$path" ]] || { echo "Missing required documentation: $path" >&2; exit 1; }
|
||||
done
|
||||
@@ -22,7 +48,7 @@ while IFS= read -r -d '' markdown; do
|
||||
esac
|
||||
[[ -e "$candidate" ]] || { echo "Broken relative link in ${markdown#$root/}: $target" >&2; exit 1; }
|
||||
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
|
||||
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)
|
||||
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user