mirror of
https://github.com/alexhopeoconnor/arduino-home-assistant.git
synced 2026-10-04 02:48:13 +10:00
feat: improve MQTT discovery publishing and examples
This commit is contained in:
@@ -43,3 +43,4 @@ jobs:
|
||||
python-version: '3.11'
|
||||
- run: python -m pip install --upgrade platformio==6.1.19
|
||||
- run: ./scripts/test.sh compile --platform ${{ matrix.platform }}
|
||||
- run: ./scripts/test.sh examples --platform ${{ matrix.platform }}
|
||||
|
||||
@@ -18,8 +18,10 @@ jobs:
|
||||
python-version: '3.11'
|
||||
- run: python -m pip install --upgrade platformio==6.1.19
|
||||
- run: ./scripts/test.sh compile --platform esp8266
|
||||
- run: ./scripts/test.sh examples --platform esp8266
|
||||
- run: pio test -e native --filter test_native_core
|
||||
- run: ./scripts/test.sh compile --platform esp32
|
||||
- run: ./scripts/test.sh examples --platform esp32
|
||||
- run: ./scripts/check-docs.sh
|
||||
- run: ./scripts/prepare-release.sh "$GITHUB_REF_NAME"
|
||||
- run: ./scripts/release-notes.sh "$GITHUB_REF_NAME" > "$RUNNER_TEMP/release-notes.md"
|
||||
|
||||
@@ -3,15 +3,7 @@
|
||||
[](https://github.com/alexhopeoconnor/arduino-home-assistant/releases)
|
||||
[](docs/README.md)
|
||||
|
||||
ArduinoHA is the maintained MQTT-discovery library behind compact Arduino, ESP8266, and ESP32 integrations with Home Assistant. It uses Arduino's standard network `Client` API and is continuously compile-tested on ESP8266 and ESP32. Device discovery requires Home Assistant 2024.11.0 or newer; single-component discovery remains supported.
|
||||
|
||||
## Why use it
|
||||
|
||||
- **Home Assistant discovery:** entities appear automatically from retained MQTT discovery payloads.
|
||||
- **Two-way entities:** report local state and receive Home Assistant commands with a small, explicit API.
|
||||
- **One physical device:** group multiple entities, metadata, shared availability, and MQTT Last Will under `HADevice`.
|
||||
- **Control the footprint:** compile out entity implementations a firmware does not use.
|
||||
- **Two discovery shapes:** start with one payload per entity or opt into a single device-discovery payload.
|
||||
ArduinoHA lets an Arduino, ESP8266, or ESP32 application publish MQTT discovery data that Home Assistant understands. Give it a connected Arduino `Client`, declare entities, and Home Assistant creates the controls and telemetry automatically.
|
||||
|
||||
## Start with a sensor
|
||||
|
||||
@@ -29,9 +21,7 @@ void setup() {
|
||||
WiFi.macAddress(mac);
|
||||
device.setUniqueId(mac, sizeof(mac));
|
||||
|
||||
WiFi.begin("SSID", "password");
|
||||
while (WiFi.status() != WL_CONNECTED) delay(500);
|
||||
|
||||
// Your application connects Wi-Fi before MQTT begins.
|
||||
temperature.setName("Temperature");
|
||||
temperature.setUnitOfMeasurement("°C");
|
||||
mqtt.begin("mqtt.local", "mqtt_user", "mqtt_password");
|
||||
@@ -43,7 +33,25 @@ void loop() {
|
||||
}
|
||||
```
|
||||
|
||||
Read [Getting started](docs/getting-started.md) before copying this into production: it explains object lifetime, MQTT lifecycle, ESP32 includes, and install routes.
|
||||
Build [ESP Sensor](examples/01-esp-sensor/) for a complete ESP8266/ESP32 project. It clearly marks the network and broker values you must provide without committing credentials.
|
||||
|
||||
## What you can build
|
||||
|
||||
- **Automatic Home Assistant discovery:** retained MQTT discovery messages create entities without hand-written Home Assistant YAML.
|
||||
- **Two-way controls:** sensors publish state while switches, lights, covers, and other writable entities receive explicit callbacks.
|
||||
- **One physical device:** `HADevice` groups metadata, multiple entities, shared availability, and MQTT Last Will.
|
||||
- **Device discovery:** publish one compact discovery document for a new multi-entity device, or keep traditional per-entity discovery.
|
||||
- **Small footprint:** exclude entity implementations your firmware does not use.
|
||||
|
||||
## Choose an example
|
||||
|
||||
| Example | Learn how to… |
|
||||
| --- | --- |
|
||||
| [ESP Sensor](examples/01-esp-sensor/) | connect an ESP application and publish changing numeric telemetry |
|
||||
| [Switch Callback](examples/02-switch-callback/) | reflect Home Assistant commands in a physical output and report state back |
|
||||
| [Multi-entity Device](examples/03-multi-entity-device/) | group controls and telemetry with shared availability and Last Will |
|
||||
| [Device Discovery](examples/04-device-discovery/) | publish one Home Assistant device-discovery document for a new device |
|
||||
| [Entity recipes](examples/README.md#entity-recipes) | find the existing focused Arduino sketches for each supported entity |
|
||||
|
||||
## Install
|
||||
|
||||
@@ -52,29 +60,6 @@ lib_deps =
|
||||
home-assistant-integration=https://github.com/alexhopeoconnor/arduino-home-assistant.git#v3.1.0
|
||||
```
|
||||
|
||||
PlatformIO clones the Git repository and checks out the ref after `#`; that ref is a release tag, not a GitHub Release asset. Arduino IDE is supported through the included [`library.properties`](library.properties); see [Getting started](docs/getting-started.md#install-the-library).
|
||||
PlatformIO clones the Git repository and checks out the ref after `#`; that ref is a release tag, not a GitHub Release asset. Arduino IDE is supported through the included [`library.properties`](library.properties).
|
||||
|
||||
## Documentation
|
||||
|
||||
The [documentation map](docs/README.md) is the starting point:
|
||||
|
||||
- [Getting started](docs/getting-started.md): connection lifecycle and minimal sketches.
|
||||
- [Device and discovery](docs/device-and-discovery.md): device metadata, discovery modes, and runtime refresh.
|
||||
- [MQTT usage](docs/mqtt-usage.md): callbacks, availability, custom MQTT, logging, and footprint flags.
|
||||
- [Entity guide](docs/entities.md): supported entity types and the best matching example.
|
||||
- [Examples](examples/README.md): curated entry points and the full example index.
|
||||
|
||||
## Development and releases
|
||||
|
||||
```bash
|
||||
./scripts/bump-version.sh vMAJOR.MINOR.PATCH
|
||||
# Replace the generated CHANGELOG TODO with the release summary.
|
||||
./scripts/test.sh compile --platform esp8266
|
||||
./scripts/test.sh compile --platform esp32
|
||||
./scripts/check-docs.sh
|
||||
./scripts/prepare-release.sh vMAJOR.MINOR.PATCH --tag
|
||||
```
|
||||
|
||||
The release preflight validates both package manifests and the matching changelog section. A pushed tag repeats the board-free compile checks and creates a GitHub Release from that section; it does not publish to the PlatformIO Registry or deploy firmware.
|
||||
|
||||
See the [changelog](CHANGELOG.md) and [licence](LICENSE).
|
||||
See [getting started](docs/getting-started.md), the [documentation map](docs/README.md), [examples](examples/README.md), [release history](CHANGELOG.md), and [licence](LICENSE).
|
||||
|
||||
+7
-9
@@ -1,14 +1,12 @@
|
||||
# Documentation
|
||||
|
||||
User-facing notes for this library. API details live in the headers under [`src/`](../src/).
|
||||
# ArduinoHA documentation
|
||||
|
||||
| Topic | What it covers |
|
||||
| ----- | ---------------- |
|
||||
| [Getting started](getting-started.md) | Prerequisites, installing the library, minimal sketches |
|
||||
| [Device & discovery](device-and-discovery.md) | `HADevice`, MQTT connection, discovery prefixes, entity IDs |
|
||||
| [MQTT usage](mqtt-usage.md) | Callbacks, custom topics, availability, compiler flags |
|
||||
| [Entities](entities.md) | Supported Home Assistant entity classes and example selection |
|
||||
| [Examples](../examples/README.md) | Curated paths through the standalone sketches |
|
||||
| [Getting started](getting-started.md) | Connecting a `Client`, installing the library, and the first MQTT lifecycle |
|
||||
| [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 |
|
||||
| [Examples](../examples/README.md) | Guided PlatformIO projects and focused entity recipes |
|
||||
| [Compatibility baseline](compatibility.md) | Supported Home Assistant capabilities and fork-specific compatibility notes |
|
||||
|
||||
Class-level API details live in headers under [`src/`](../src/). Return to the [project overview](../README.md).
|
||||
| [Compatibility baseline](compatibility.md) | Audited upstream/HA targets and deliberate exclusions |
|
||||
|
||||
+2
-2
@@ -140,9 +140,9 @@ public:
|
||||
};
|
||||
```
|
||||
|
||||
`arduinoHALog(...)` and `arduinoHALogf(...)` support subsystem-tagged messages such as `mqtt`, `discovery`, `availability`, `serializer`, `entity`, and `device`. `DeviceFramework` installs a structured sink so ArduinoHA messages follow the same main log stream as the rest of the firmware.
|
||||
`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.
|
||||
|
||||
Optional transport context for diagnostics (registered automatically by **DeviceFramework**): **`arduinoHASetNetworkStatusFn`** (for example returning `WiFi.status()` so publish failure lines can include `wifi=`).
|
||||
Applications that use Wi-Fi can register **`arduinoHASetNetworkStatusFn`** with a callback returning `WiFi.status()`. Publish failure diagnostics can then include the current Wi-Fi status without coupling ArduinoHA to a particular network library.
|
||||
|
||||
**Exclude unused device types** (saves flash from vtables), e.g.:
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# ESP Sensor
|
||||
|
||||
This is the first complete ArduinoHA project for an ESP8266 D1 mini or ESP32 development board. It connects Wi-Fi, derives a stable device ID from the board MAC address, and publishes a changing **Uptime** sensor to Home Assistant.
|
||||
|
||||
Before uploading, replace the `EXAMPLE_*` values in `include/ExampleNetwork.h`, or provide them as PlatformIO build flags. The header contains safe placeholders and must not contain real credentials when committed.
|
||||
|
||||
After MQTT connects, Home Assistant discovers **ArduinoHA sensor example** and its Uptime entity. Rebooting the board keeps the same Home Assistant identity because its MAC address is stable.
|
||||
|
||||
See [getting started](../../docs/getting-started.md) and the shared [examples guide](../README.md).
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#if defined(ESP32)
|
||||
#include <Network.h>
|
||||
#include <WiFi.h>
|
||||
#else
|
||||
#include <ESP8266WiFi.h>
|
||||
#endif
|
||||
|
||||
#ifndef EXAMPLE_WIFI_SSID
|
||||
#define EXAMPLE_WIFI_SSID "replace-with-wifi-name"
|
||||
#endif
|
||||
#ifndef EXAMPLE_WIFI_PASSWORD
|
||||
#define EXAMPLE_WIFI_PASSWORD "replace-with-wifi-password"
|
||||
#endif
|
||||
#ifndef EXAMPLE_MQTT_HOST
|
||||
#define EXAMPLE_MQTT_HOST "replace-with-mqtt-host"
|
||||
#endif
|
||||
#ifndef EXAMPLE_MQTT_USER
|
||||
#define EXAMPLE_MQTT_USER ""
|
||||
#endif
|
||||
#ifndef EXAMPLE_MQTT_PASSWORD
|
||||
#define EXAMPLE_MQTT_PASSWORD ""
|
||||
#endif
|
||||
|
||||
inline void connectExampleWiFi() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(EXAMPLE_WIFI_SSID, EXAMPLE_WIFI_PASSWORD);
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(250);
|
||||
}
|
||||
}
|
||||
|
||||
inline void setExampleUniqueId(HADevice& device) {
|
||||
uint8_t mac[6];
|
||||
WiFi.macAddress(mac);
|
||||
device.setUniqueId(mac, sizeof(mac));
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[platformio]
|
||||
default_envs = esp8266
|
||||
|
||||
[common]
|
||||
framework = arduino
|
||||
lib_ldf_mode = deep+
|
||||
lib_deps =
|
||||
home-assistant-integration=symlink://../..
|
||||
|
||||
[env:esp8266]
|
||||
extends = common
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
platform_packages =
|
||||
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
|
||||
|
||||
[env:esp32]
|
||||
extends = common
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
build_unflags = -std=gnu++11
|
||||
build_flags =
|
||||
-std=gnu++14
|
||||
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
|
||||
@@ -0,0 +1,30 @@
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoHA.h>
|
||||
#include "ExampleNetwork.h"
|
||||
|
||||
WiFiClient client;
|
||||
HADevice device;
|
||||
HAMqtt mqtt(client, device);
|
||||
HASensorNumber uptime("uptime");
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
connectExampleWiFi();
|
||||
setExampleUniqueId(device);
|
||||
|
||||
device.setName("ArduinoHA sensor example");
|
||||
device.setSoftwareVersion("1.0.0");
|
||||
uptime.setName("Uptime");
|
||||
uptime.setUnitOfMeasurement("s");
|
||||
mqtt.begin(EXAMPLE_MQTT_HOST, EXAMPLE_MQTT_USER, EXAMPLE_MQTT_PASSWORD);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
mqtt.loop();
|
||||
|
||||
static unsigned long lastUpdate = 0;
|
||||
if (millis() - lastUpdate >= 1000) {
|
||||
uptime.setValue(static_cast<uint32_t>(millis() / 1000));
|
||||
lastUpdate = millis();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Switch Callback
|
||||
|
||||
This example creates one writable Home Assistant switch. When Home Assistant sends a command, `onOutputCommand()` changes the local output and immediately reports the resulting state back through the supplied `HASwitch`.
|
||||
|
||||
The ESP8266 build uses `LED_BUILTIN`; the ESP32 build uses GPIO 2. Confirm your board’s LED polarity before treating that output as a real load.
|
||||
|
||||
Set the Wi-Fi and MQTT placeholders in `include/ExampleNetwork.h`, flash the board, then toggle **Example output** in Home Assistant.
|
||||
|
||||
See [MQTT usage](../../docs/mqtt-usage.md) and the shared [examples guide](../README.md).
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#if defined(ESP32)
|
||||
#include <Network.h>
|
||||
#include <WiFi.h>
|
||||
#else
|
||||
#include <ESP8266WiFi.h>
|
||||
#endif
|
||||
|
||||
#ifndef EXAMPLE_WIFI_SSID
|
||||
#define EXAMPLE_WIFI_SSID "replace-with-wifi-name"
|
||||
#endif
|
||||
#ifndef EXAMPLE_WIFI_PASSWORD
|
||||
#define EXAMPLE_WIFI_PASSWORD "replace-with-wifi-password"
|
||||
#endif
|
||||
#ifndef EXAMPLE_MQTT_HOST
|
||||
#define EXAMPLE_MQTT_HOST "replace-with-mqtt-host"
|
||||
#endif
|
||||
#ifndef EXAMPLE_MQTT_USER
|
||||
#define EXAMPLE_MQTT_USER ""
|
||||
#endif
|
||||
#ifndef EXAMPLE_MQTT_PASSWORD
|
||||
#define EXAMPLE_MQTT_PASSWORD ""
|
||||
#endif
|
||||
|
||||
inline void connectExampleWiFi() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(EXAMPLE_WIFI_SSID, EXAMPLE_WIFI_PASSWORD);
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(250);
|
||||
}
|
||||
}
|
||||
|
||||
inline void setExampleUniqueId(HADevice& device) {
|
||||
uint8_t mac[6];
|
||||
WiFi.macAddress(mac);
|
||||
device.setUniqueId(mac, sizeof(mac));
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[platformio]
|
||||
default_envs = esp8266
|
||||
|
||||
[common]
|
||||
framework = arduino
|
||||
lib_ldf_mode = deep+
|
||||
lib_deps =
|
||||
home-assistant-integration=symlink://../..
|
||||
|
||||
[env:esp8266]
|
||||
extends = common
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
platform_packages =
|
||||
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
|
||||
|
||||
[env:esp32]
|
||||
extends = common
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
build_unflags = -std=gnu++11
|
||||
build_flags =
|
||||
-std=gnu++14
|
||||
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
|
||||
@@ -0,0 +1,36 @@
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoHA.h>
|
||||
#include "ExampleNetwork.h"
|
||||
|
||||
#if defined(ESP32)
|
||||
constexpr uint8_t kOutputPin = 2;
|
||||
#else
|
||||
constexpr uint8_t kOutputPin = LED_BUILTIN;
|
||||
#endif
|
||||
|
||||
WiFiClient client;
|
||||
HADevice device;
|
||||
HAMqtt mqtt(client, device);
|
||||
HASwitch output("output");
|
||||
|
||||
void onOutputCommand(bool state, HASwitch* sender) {
|
||||
digitalWrite(kOutputPin, state ? HIGH : LOW);
|
||||
sender->setState(state);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
pinMode(kOutputPin, OUTPUT);
|
||||
digitalWrite(kOutputPin, LOW);
|
||||
connectExampleWiFi();
|
||||
setExampleUniqueId(device);
|
||||
|
||||
device.setName("ArduinoHA switch example");
|
||||
output.setName("Example output");
|
||||
output.onCommand(onOutputCommand);
|
||||
mqtt.begin(EXAMPLE_MQTT_HOST, EXAMPLE_MQTT_USER, EXAMPLE_MQTT_PASSWORD);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
mqtt.loop();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Multi-entity Device
|
||||
|
||||
This example groups an Uptime sensor and writable output under one `HADevice`. It adds common metadata, one shared availability topic, and MQTT Last Will so Home Assistant marks the complete device unavailable if its network connection disappears.
|
||||
|
||||
Configure `include/ExampleNetwork.h`, flash the selected target, and inspect the device page in Home Assistant. Both entities belong to **ArduinoHA multi-entity example** and share its availability state.
|
||||
|
||||
Use this shape when one physical board exposes several related controls or sensors.
|
||||
|
||||
See [device and discovery](../../docs/device-and-discovery.md) and [MQTT usage](../../docs/mqtt-usage.md).
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#if defined(ESP32)
|
||||
#include <Network.h>
|
||||
#include <WiFi.h>
|
||||
#else
|
||||
#include <ESP8266WiFi.h>
|
||||
#endif
|
||||
|
||||
#ifndef EXAMPLE_WIFI_SSID
|
||||
#define EXAMPLE_WIFI_SSID "replace-with-wifi-name"
|
||||
#endif
|
||||
#ifndef EXAMPLE_WIFI_PASSWORD
|
||||
#define EXAMPLE_WIFI_PASSWORD "replace-with-wifi-password"
|
||||
#endif
|
||||
#ifndef EXAMPLE_MQTT_HOST
|
||||
#define EXAMPLE_MQTT_HOST "replace-with-mqtt-host"
|
||||
#endif
|
||||
#ifndef EXAMPLE_MQTT_USER
|
||||
#define EXAMPLE_MQTT_USER ""
|
||||
#endif
|
||||
#ifndef EXAMPLE_MQTT_PASSWORD
|
||||
#define EXAMPLE_MQTT_PASSWORD ""
|
||||
#endif
|
||||
|
||||
inline void connectExampleWiFi() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(EXAMPLE_WIFI_SSID, EXAMPLE_WIFI_PASSWORD);
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(250);
|
||||
}
|
||||
}
|
||||
|
||||
inline void setExampleUniqueId(HADevice& device) {
|
||||
uint8_t mac[6];
|
||||
WiFi.macAddress(mac);
|
||||
device.setUniqueId(mac, sizeof(mac));
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[platformio]
|
||||
default_envs = esp8266
|
||||
|
||||
[common]
|
||||
framework = arduino
|
||||
lib_ldf_mode = deep+
|
||||
lib_deps =
|
||||
home-assistant-integration=symlink://../..
|
||||
|
||||
[env:esp8266]
|
||||
extends = common
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
platform_packages =
|
||||
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
|
||||
|
||||
[env:esp32]
|
||||
extends = common
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
build_unflags = -std=gnu++11
|
||||
build_flags =
|
||||
-std=gnu++14
|
||||
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
|
||||
@@ -0,0 +1,48 @@
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoHA.h>
|
||||
#include "ExampleNetwork.h"
|
||||
|
||||
#if defined(ESP32)
|
||||
constexpr uint8_t kOutputPin = 2;
|
||||
#else
|
||||
constexpr uint8_t kOutputPin = LED_BUILTIN;
|
||||
#endif
|
||||
|
||||
WiFiClient client;
|
||||
HADevice device;
|
||||
HAMqtt mqtt(client, device);
|
||||
HASensorNumber uptime("uptime");
|
||||
HASwitch output("output");
|
||||
|
||||
void onOutputCommand(bool state, HASwitch* sender) {
|
||||
digitalWrite(kOutputPin, state ? HIGH : LOW);
|
||||
sender->setState(state);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
pinMode(kOutputPin, OUTPUT);
|
||||
connectExampleWiFi();
|
||||
setExampleUniqueId(device);
|
||||
|
||||
device.setName("ArduinoHA multi-entity example");
|
||||
device.setManufacturer("Example Devices");
|
||||
device.setSoftwareVersion("1.0.0");
|
||||
device.enableSharedAvailability();
|
||||
device.enableLastWill();
|
||||
|
||||
uptime.setName("Uptime");
|
||||
uptime.setUnitOfMeasurement("s");
|
||||
output.setName("Example output");
|
||||
output.onCommand(onOutputCommand);
|
||||
mqtt.begin(EXAMPLE_MQTT_HOST, EXAMPLE_MQTT_USER, EXAMPLE_MQTT_PASSWORD);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
mqtt.loop();
|
||||
static unsigned long lastUpdate = 0;
|
||||
if (millis() - lastUpdate >= 1000) {
|
||||
uptime.setValue(static_cast<uint32_t>(millis() / 1000));
|
||||
lastUpdate = millis();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Device Discovery
|
||||
|
||||
This example opts into Home Assistant MQTT device discovery. Instead of one retained discovery document per entity, ArduinoHA publishes one device document containing the component definitions and richer board metadata.
|
||||
|
||||
Use device discovery for a **new** device on Home Assistant 2024.11.0 or newer. Do not enable it on a device that has already published traditional single-component discovery without following the documented migration procedure.
|
||||
|
||||
After configuring `include/ExampleNetwork.h` and flashing the board, Home Assistant discovers **ArduinoHA device discovery example** and its Uptime sensor.
|
||||
|
||||
See [device discovery](../../docs/device-and-discovery.md#single-component-vs-device-discovery) and the shared [examples guide](../README.md).
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#if defined(ESP32)
|
||||
#include <Network.h>
|
||||
#include <WiFi.h>
|
||||
#else
|
||||
#include <ESP8266WiFi.h>
|
||||
#endif
|
||||
|
||||
#ifndef EXAMPLE_WIFI_SSID
|
||||
#define EXAMPLE_WIFI_SSID "replace-with-wifi-name"
|
||||
#endif
|
||||
#ifndef EXAMPLE_WIFI_PASSWORD
|
||||
#define EXAMPLE_WIFI_PASSWORD "replace-with-wifi-password"
|
||||
#endif
|
||||
#ifndef EXAMPLE_MQTT_HOST
|
||||
#define EXAMPLE_MQTT_HOST "replace-with-mqtt-host"
|
||||
#endif
|
||||
#ifndef EXAMPLE_MQTT_USER
|
||||
#define EXAMPLE_MQTT_USER ""
|
||||
#endif
|
||||
#ifndef EXAMPLE_MQTT_PASSWORD
|
||||
#define EXAMPLE_MQTT_PASSWORD ""
|
||||
#endif
|
||||
|
||||
inline void connectExampleWiFi() {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(EXAMPLE_WIFI_SSID, EXAMPLE_WIFI_PASSWORD);
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(250);
|
||||
}
|
||||
}
|
||||
|
||||
inline void setExampleUniqueId(HADevice& device) {
|
||||
uint8_t mac[6];
|
||||
WiFi.macAddress(mac);
|
||||
device.setUniqueId(mac, sizeof(mac));
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[platformio]
|
||||
default_envs = esp8266
|
||||
|
||||
[common]
|
||||
framework = arduino
|
||||
lib_ldf_mode = deep+
|
||||
lib_deps =
|
||||
home-assistant-integration=symlink://../..
|
||||
|
||||
[env:esp8266]
|
||||
extends = common
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
platform_packages =
|
||||
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
|
||||
|
||||
[env:esp32]
|
||||
extends = common
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
build_unflags = -std=gnu++11
|
||||
build_flags =
|
||||
-std=gnu++14
|
||||
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
|
||||
@@ -0,0 +1,33 @@
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoHA.h>
|
||||
#include "ExampleNetwork.h"
|
||||
|
||||
WiFiClient client;
|
||||
HADevice device;
|
||||
HAMqtt mqtt(client, device);
|
||||
HASensorNumber uptime("uptime");
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
connectExampleWiFi();
|
||||
setExampleUniqueId(device);
|
||||
|
||||
device.setName("ArduinoHA device discovery example");
|
||||
device.setManufacturer("Example Devices");
|
||||
device.setModel("ESP example");
|
||||
device.setSoftwareVersion("1.0.0");
|
||||
uptime.setName("Uptime");
|
||||
uptime.setUnitOfMeasurement("s");
|
||||
|
||||
mqtt.enableDeviceDiscovery();
|
||||
mqtt.begin(EXAMPLE_MQTT_HOST, EXAMPLE_MQTT_USER, EXAMPLE_MQTT_PASSWORD);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
mqtt.loop();
|
||||
static unsigned long lastUpdate = 0;
|
||||
if (millis() - lastUpdate >= 1000) {
|
||||
uptime.setValue(static_cast<uint32_t>(millis() / 1000));
|
||||
lastUpdate = millis();
|
||||
}
|
||||
}
|
||||
+15
-12
@@ -1,19 +1,24 @@
|
||||
# ArduinoHA examples
|
||||
|
||||
Each directory is an Arduino sketch. Start with the network example matching your board, then pick an entity example from the table. Copy credentials into your local development configuration; do not commit them in a sketch.
|
||||
Start with a guided PlatformIO project when you are new to the library. Each one builds for ESP8266 and ESP32, contains safe placeholder credentials, and explains the Home Assistant result you should see.
|
||||
|
||||
| Starting point | Use it for |
|
||||
```bash
|
||||
pio run -d examples/01-esp-sensor -e esp8266
|
||||
pio run -d examples/01-esp-sensor -e esp8266 -t upload
|
||||
```
|
||||
|
||||
| Guided example | What it demonstrates |
|
||||
| --- | --- |
|
||||
| [nodemcu](nodemcu/nodemcu.ino) | Basic ESP8266 Wi-Fi and MQTT connection |
|
||||
| [nano33iot](nano33iot/nano33iot.ino) | Basic Arduino Nano 33 IoT connection |
|
||||
| [mqtt-with-credentials](mqtt-with-credentials/mqtt-with-credentials.ino) | MQTT authentication |
|
||||
| [mqtt-advanced](mqtt-advanced/mqtt-advanced.ino) | Custom MQTT subscriptions and publishing |
|
||||
| [availability](availability/availability.ino) | Per-entity availability |
|
||||
| [advanced-availability](advanced-availability/advanced-availability.ino) | Shared availability and MQTT Last Will |
|
||||
| [ESP Sensor](01-esp-sensor/) | Wi-Fi application wiring, a unique device ID, and changing numeric telemetry |
|
||||
| [Switch Callback](02-switch-callback/) | A Home Assistant command callback and local state acknowledgement |
|
||||
| [Multi-entity Device](03-multi-entity-device/) | Multiple entities, shared availability, and MQTT Last Will |
|
||||
| [Device Discovery](04-device-discovery/) | One device-discovery document for a newly deployed device |
|
||||
|
||||
## Entity examples
|
||||
## Entity recipes
|
||||
|
||||
| Example | Home Assistant behaviour |
|
||||
The original focused sketches remain useful as short API recipes. They are intentionally transport-specific Arduino sketches, rather than full product firmware.
|
||||
|
||||
| Recipe | Home Assistant behaviour |
|
||||
| --- | --- |
|
||||
| [binary-sensor](binary-sensor/binary-sensor.ino) | Door/contact-style binary state |
|
||||
| [button](button/button.ino) | Press action |
|
||||
@@ -34,6 +39,4 @@ Each directory is an Arduino sketch. Start with the network example matching you
|
||||
| [sensor-integer](sensor-integer/sensor-integer.ino) | Integer uptime measurement |
|
||||
| [tag-scanner](tag-scanner/tag-scanner.ino) | RFID tag reporting |
|
||||
|
||||
The examples are intentionally small, not production firmware frameworks. For reusable Wi-Fi configuration, MQTT wiring, profiles, OTA, and migrations, use [DeviceFramework](https://github.com/alexhopeoconnor/DeviceFramework) in a consuming firmware.
|
||||
|
||||
See the [entity guide](../docs/entities.md) and [project overview](../README.md).
|
||||
|
||||
@@ -24,4 +24,14 @@ while IFS= read -r -d '' markdown; do
|
||||
done < <(sed -nE 's/.*\]\(([^ )]+)( "[^"]*")?\).*/\1/p' "$markdown")
|
||||
done < <(find "$root" -path "$root/.git" -prune -o -name '*.md' -type f -print0)
|
||||
|
||||
while IFS= read -r example; do
|
||||
for required in README.md platformio.ini; do
|
||||
[[ -f "$example/$required" ]] || { echo "Incomplete guided example: ${example#$root/} is missing $required" >&2; exit 1; }
|
||||
done
|
||||
find "$example" -maxdepth 2 -type f \( -name '*.ino' -o -name '*.cpp' \) -print -quit | grep -q . || {
|
||||
echo "Incomplete guided example: ${example#$root/} has no sketch source" >&2
|
||||
exit 1
|
||||
}
|
||||
done < <(find "$root/examples" -mindepth 1 -maxdepth 1 -type d -name '[0-9][0-9]-*' -print | sort)
|
||||
|
||||
echo "Documentation links and required files passed"
|
||||
|
||||
+19
-5
@@ -2,17 +2,31 @@
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 compile --platform esp8266|esp32" >&2
|
||||
echo "Usage: $0 compile|examples --platform esp8266|esp32" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[[ "${1:-}" == "compile" && "${2:-}" == "--platform" && $# -eq 3 ]] || usage
|
||||
|
||||
[[ $# -eq 3 && ( "${1:-}" == "compile" || "${1:-}" == "examples" ) && "${2:-}" == "--platform" ]] || usage
|
||||
case "${3:-}" in
|
||||
esp8266|esp32) environment="${3}" ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
pio test -d "$root" -e "$environment" --without-uploading --without-testing
|
||||
echo "ArduinoHA compile check passed for ${3}"
|
||||
case "$1" in
|
||||
compile)
|
||||
pio test -d "$root" -e "$environment" --without-uploading --without-testing
|
||||
echo "ArduinoHA compile check passed for $environment"
|
||||
;;
|
||||
examples)
|
||||
mapfile -t examples < <(find "$root/examples" -mindepth 1 -maxdepth 1 -type d -name '[0-9][0-9]-*' -print | sort)
|
||||
if (( ${#examples[@]} == 0 )); then
|
||||
echo "No example projects found" >&2
|
||||
exit 1
|
||||
fi
|
||||
for example in "${examples[@]}"; do
|
||||
pio run -d "$example" -e "$environment" </dev/null
|
||||
done
|
||||
echo "ArduinoHA examples compile check passed for $environment"
|
||||
;;
|
||||
esac
|
||||
|
||||
+152
-85
@@ -80,6 +80,8 @@ HAMqtt::HAMqtt(
|
||||
uint8_t maxDevicesTypesNb
|
||||
) :
|
||||
_mqtt(pubSub),
|
||||
_directPublishBufferLength(0),
|
||||
_directPublishActive(false),
|
||||
HAMQTT_INIT
|
||||
{
|
||||
_instance = this;
|
||||
@@ -93,6 +95,8 @@ HAMqtt::HAMqtt(
|
||||
) :
|
||||
_mqttStorage(netClient),
|
||||
_mqtt(&_mqttStorage),
|
||||
_directPublishBufferLength(0),
|
||||
_directPublishActive(false),
|
||||
HAMQTT_INIT
|
||||
{
|
||||
_instance = this;
|
||||
@@ -276,18 +280,6 @@ void HAMqtt::loop()
|
||||
|
||||
if (!result) {
|
||||
_lastDisconnectReason = DiagnosticDisconnectReason::LoopReturnedFalse;
|
||||
const uint32_t now = millis();
|
||||
arduinoHALog(
|
||||
ArduinoHALogLevel::Warn,
|
||||
kMqtt,
|
||||
String(F("loop returned false pubsub=")) + String(rawState) +
|
||||
F(" deferred=") + String(_deferredCount) +
|
||||
F(" depth=") + String(_messageDispatchDepth) +
|
||||
F(" sinceLastMsgMs=") +
|
||||
String(_lastMessageAt ? (now - _lastMessageAt) : static_cast<uint32_t>(0)) +
|
||||
F(" sinceLastPubMs=") +
|
||||
String(_lastPublishAt ? (now - _lastPublishAt) : static_cast<uint32_t>(0))
|
||||
);
|
||||
connectToServer();
|
||||
}
|
||||
|
||||
@@ -415,36 +407,16 @@ bool HAMqtt::publish(const char* topic, const char* payload, bool retained)
|
||||
);
|
||||
}
|
||||
|
||||
const bool connBeforePub = isConnected();
|
||||
const int psBeforePub = getPubSubState();
|
||||
if (!_mqtt->beginPublish(topic, payloadLength, retained)) {
|
||||
arduinoHALog(
|
||||
ArduinoHALogLevel::Warn,
|
||||
kMqtt,
|
||||
String(F("beginPublish failed topic=")) + topic + F(" len=") + String(payloadLength) +
|
||||
formatDirectPublishFailureDiagnostics(connBeforePub, psBeforePub)
|
||||
);
|
||||
if (!beginPublish(topic, payloadLength, retained)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool written = writePayload(
|
||||
reinterpret_cast<const uint8_t*>(payload),
|
||||
payloadLength
|
||||
);
|
||||
const bool connBeforeEnd = isConnected();
|
||||
const int psBeforeEnd = getPubSubState();
|
||||
const bool ended = _mqtt->endPublish();
|
||||
const bool ok = written && ended;
|
||||
if (!ok) {
|
||||
arduinoHALog(
|
||||
ArduinoHALogLevel::Warn,
|
||||
kMqtt,
|
||||
String(written ? F("endPublish failed topic=") : F("payload write failed topic=")) + topic +
|
||||
formatDirectPublishFailureDiagnostics(connBeforeEnd, psBeforeEnd)
|
||||
);
|
||||
} else {
|
||||
_lastPublishAt = millis();
|
||||
}
|
||||
return ok;
|
||||
const bool ended = endPublish();
|
||||
return written && ended;
|
||||
}
|
||||
|
||||
bool HAMqtt::beginPublish(
|
||||
@@ -465,6 +437,10 @@ bool HAMqtt::beginPublish(
|
||||
}
|
||||
|
||||
if (!isProcessingMessage()) {
|
||||
if (_directPublishActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool connBefore = isConnected();
|
||||
const int psBefore = getPubSubState();
|
||||
const bool ok = _mqtt->beginPublish(topic, payloadLength, retained);
|
||||
@@ -475,8 +451,12 @@ bool HAMqtt::beginPublish(
|
||||
String(F("beginPublish failed topic=")) + topic + F(" len=") + String(payloadLength) +
|
||||
formatDirectPublishFailureDiagnostics(connBefore, psBefore)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return ok;
|
||||
|
||||
clearDirectPublishBuffer();
|
||||
_directPublishActive = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_deferredBuilder.active) {
|
||||
@@ -530,6 +510,10 @@ bool HAMqtt::writePayload(const uint8_t* data, const uint16_t length)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_directPublishActive) {
|
||||
return appendDirectPublishPayload(data, length);
|
||||
}
|
||||
|
||||
return _mqtt->write(data, length) == length;
|
||||
}
|
||||
|
||||
@@ -556,6 +540,10 @@ bool HAMqtt::writePayload(const __FlashStringHelper* src)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_directPublishActive) {
|
||||
return appendDirectPublishProgmemPayload(src);
|
||||
}
|
||||
|
||||
const uint16_t length = static_cast<uint16_t>(strlen_P(reinterpret_cast<PGM_P>(src)));
|
||||
return _mqtt->print(src) == length;
|
||||
}
|
||||
@@ -563,9 +551,17 @@ bool HAMqtt::writePayload(const __FlashStringHelper* src)
|
||||
bool HAMqtt::endPublish()
|
||||
{
|
||||
if (!isProcessingMessage()) {
|
||||
if (!_directPublishActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool payloadFlushed = flushDirectPublishBuffer();
|
||||
const bool connBefore = isConnected();
|
||||
const int psBefore = getPubSubState();
|
||||
const bool ok = _mqtt->endPublish();
|
||||
const bool ended = _mqtt->endPublish();
|
||||
clearDirectPublishBuffer();
|
||||
_directPublishActive = false;
|
||||
const bool ok = payloadFlushed && ended;
|
||||
if (ok) {
|
||||
_lastPublishAt = millis();
|
||||
} else {
|
||||
@@ -603,6 +599,72 @@ bool HAMqtt::endPublish()
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool HAMqtt::flushDirectPublishBuffer()
|
||||
{
|
||||
if (_directPublishBufferLength == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint16_t length = _directPublishBufferLength;
|
||||
_directPublishBufferLength = 0;
|
||||
return _mqtt->write(_directPublishBuffer, length) == length;
|
||||
}
|
||||
|
||||
bool HAMqtt::appendDirectPublishPayload(const uint8_t* data, uint16_t length)
|
||||
{
|
||||
while (length > 0) {
|
||||
if (_directPublishBufferLength == DirectPublishBufferSize &&
|
||||
!flushDirectPublishBuffer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint16_t available =
|
||||
static_cast<uint16_t>(DirectPublishBufferSize - _directPublishBufferLength);
|
||||
const uint16_t copied = length < available ? length : available;
|
||||
memcpy(_directPublishBuffer + _directPublishBufferLength, data, copied);
|
||||
_directPublishBufferLength = static_cast<uint16_t>(_directPublishBufferLength + copied);
|
||||
data += copied;
|
||||
length = static_cast<uint16_t>(length - copied);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HAMqtt::appendDirectPublishProgmemPayload(const __FlashStringHelper* src)
|
||||
{
|
||||
PGM_P data = reinterpret_cast<PGM_P>(src);
|
||||
uint16_t remaining = static_cast<uint16_t>(strlen_P(data));
|
||||
uint16_t offset = 0;
|
||||
while (remaining > 0) {
|
||||
if (_directPublishBufferLength == DirectPublishBufferSize &&
|
||||
!flushDirectPublishBuffer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint16_t available =
|
||||
static_cast<uint16_t>(DirectPublishBufferSize - _directPublishBufferLength);
|
||||
const uint16_t copied = remaining < available ? remaining : available;
|
||||
memcpy_P(_directPublishBuffer + _directPublishBufferLength, data + offset, copied);
|
||||
_directPublishBufferLength = static_cast<uint16_t>(_directPublishBufferLength + copied);
|
||||
offset = static_cast<uint16_t>(offset + copied);
|
||||
remaining = static_cast<uint16_t>(remaining - copied);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void HAMqtt::clearDirectPublishBuffer()
|
||||
{
|
||||
_directPublishBufferLength = 0;
|
||||
}
|
||||
|
||||
void HAMqtt::abortDirectPublish()
|
||||
{
|
||||
clearDirectPublishBuffer();
|
||||
_directPublishActive = false;
|
||||
_mqtt->disconnect();
|
||||
}
|
||||
|
||||
bool HAMqtt::subscribe(const char* topic)
|
||||
{
|
||||
arduinoHALog(ArduinoHALogLevel::Debug, kMqtt, String(F("subscribe ")) + (topic ? topic : ""));
|
||||
@@ -945,6 +1007,10 @@ void HAMqtt::onConnectedLogic()
|
||||
publishDeviceDiscovery();
|
||||
}
|
||||
|
||||
if (!isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_connectedCallback) {
|
||||
_connectedCallback();
|
||||
}
|
||||
@@ -965,6 +1031,26 @@ bool HAMqtt::publishDeviceDiscovery()
|
||||
return publishDeviceDiscoveryPayload();
|
||||
}
|
||||
|
||||
HASerializer* HAMqtt::buildDeviceDiscoveryComponentSerializer(
|
||||
HABaseDeviceType* deviceType,
|
||||
HABaseDeviceType* removalType
|
||||
)
|
||||
{
|
||||
if (deviceType != removalType) {
|
||||
return deviceType->buildDeviceDiscoverySerializer();
|
||||
}
|
||||
|
||||
HASerializer* serializer = new (std::nothrow) HASerializer(deviceType, 1);
|
||||
if (serializer) {
|
||||
serializer->set(
|
||||
AHATOFSTR(HAPlatformProperty),
|
||||
deviceType->componentName(),
|
||||
HASerializer::ProgmemPropertyValue
|
||||
);
|
||||
}
|
||||
return serializer;
|
||||
}
|
||||
|
||||
bool HAMqtt::publishDeviceDiscoveryPayload(HABaseDeviceType* removalType)
|
||||
{
|
||||
const char* deviceUniqueId = _device.getUniqueId();
|
||||
@@ -986,7 +1072,6 @@ bool HAMqtt::publishDeviceDiscoveryPayload(HABaseDeviceType* removalType)
|
||||
}
|
||||
|
||||
HABaseDeviceType* componentTypes[_devicesTypesNb];
|
||||
HASerializer* componentSerializers[_devicesTypesNb];
|
||||
uint8_t componentSerializerCount = 0;
|
||||
uint32_t componentsPayloadLength = 2; // {}
|
||||
|
||||
@@ -999,9 +1084,6 @@ bool HAMqtt::publishDeviceDiscoveryPayload(HABaseDeviceType* removalType)
|
||||
const char* uniqueId = deviceType->uniqueId();
|
||||
if (!uniqueId || !HAJson::isValidDiscoveryTopicToken(uniqueId)) {
|
||||
arduinoHALog(ArduinoHALogLevel::Error, kDiscovery, F("device discovery rejected invalid component ID"));
|
||||
for (uint8_t j = 0; j < componentSerializerCount; j++) {
|
||||
delete componentSerializers[j];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1009,24 +1091,9 @@ bool HAMqtt::publishDeviceDiscoveryPayload(HABaseDeviceType* removalType)
|
||||
continue;
|
||||
}
|
||||
|
||||
HASerializer* serializer = nullptr;
|
||||
if (deviceType == removalType) {
|
||||
serializer = new (std::nothrow) HASerializer(deviceType, 1);
|
||||
if (serializer) {
|
||||
serializer->set(
|
||||
AHATOFSTR(HAPlatformProperty),
|
||||
deviceType->componentName(),
|
||||
HASerializer::ProgmemPropertyValue
|
||||
);
|
||||
}
|
||||
} else {
|
||||
serializer = deviceType->buildDeviceDiscoverySerializer();
|
||||
}
|
||||
HASerializer* serializer = buildDeviceDiscoveryComponentSerializer(deviceType, removalType);
|
||||
if (!serializer) {
|
||||
arduinoHALog(ArduinoHALogLevel::Error, kDiscovery, F("device discovery component serializer unavailable"));
|
||||
for (uint8_t j = 0; j < componentSerializerCount; j++) {
|
||||
delete componentSerializers[j];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1039,9 +1106,6 @@ bool HAMqtt::publishDeviceDiscoveryPayload(HABaseDeviceType* removalType)
|
||||
) {
|
||||
delete serializer;
|
||||
arduinoHALog(ArduinoHALogLevel::Error, kDiscovery, F("device discovery component serializer invalid or too large"));
|
||||
for (uint8_t j = 0; j < componentSerializerCount; j++) {
|
||||
delete componentSerializers[j];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1051,14 +1115,11 @@ bool HAMqtt::publishDeviceDiscoveryPayload(HABaseDeviceType* removalType)
|
||||
componentsPayloadLength += componentKeySize + 1 + serializerSize;
|
||||
if (componentsPayloadLength > UINT16_MAX) {
|
||||
delete serializer;
|
||||
for (uint8_t j = 0; j < componentSerializerCount; j++) {
|
||||
delete componentSerializers[j];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentTypes[componentSerializerCount] = deviceType;
|
||||
componentSerializers[componentSerializerCount++] = serializer;
|
||||
componentTypes[componentSerializerCount++] = deviceType;
|
||||
delete serializer;
|
||||
}
|
||||
|
||||
if (componentSerializerCount == 0) {
|
||||
@@ -1076,9 +1137,6 @@ bool HAMqtt::publishDeviceDiscoveryPayload(HABaseDeviceType* removalType)
|
||||
}
|
||||
const uint16_t originSerializerSize = originSerializer.calculateSize();
|
||||
if (originSerializerSize == 0) {
|
||||
for (uint8_t i = 0; i < componentSerializerCount; i++) {
|
||||
delete componentSerializers[i];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1088,9 +1146,6 @@ bool HAMqtt::publishDeviceDiscoveryPayload(HABaseDeviceType* removalType)
|
||||
strlen(deviceUniqueId) + 1 +
|
||||
strlen_P(HAConfigTopic) + 1;
|
||||
if (topicLength == 0 || topicLength > UINT16_MAX) {
|
||||
for (uint8_t i = 0; i < componentSerializerCount; i++) {
|
||||
delete componentSerializers[i];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1112,9 +1167,6 @@ bool HAMqtt::publishDeviceDiscoveryPayload(HABaseDeviceType* removalType)
|
||||
componentsPayloadLength +
|
||||
strlen_P(HASerializerJsonDataSuffix);
|
||||
if (payloadLength > UINT16_MAX) {
|
||||
for (uint8_t i = 0; i < componentSerializerCount; i++) {
|
||||
delete componentSerializers[i];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1137,9 +1189,6 @@ bool HAMqtt::publishDeviceDiscoveryPayload(HABaseDeviceType* removalType)
|
||||
F(" len=") + String(payloadLength) +
|
||||
formatDirectPublishFailureDiagnostics(discConnBefore, discPsBefore)
|
||||
);
|
||||
for (uint8_t i = 0; i < componentSerializerCount; i++) {
|
||||
delete componentSerializers[i];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1166,23 +1215,41 @@ bool HAMqtt::publishDeviceDiscoveryPayload(HABaseDeviceType* removalType)
|
||||
}
|
||||
|
||||
if (written) {
|
||||
const char* componentId = componentTypes[i]->uniqueId();
|
||||
HABaseDeviceType* componentType = componentTypes[i];
|
||||
HASerializer* serializer =
|
||||
buildDeviceDiscoveryComponentSerializer(componentType, removalType);
|
||||
if (!serializer) {
|
||||
written = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* componentId = componentType->uniqueId();
|
||||
const char quote = '"';
|
||||
const char colon = ':';
|
||||
written = writePayload("e, 1) &&
|
||||
writePayload(componentId, strlen(componentId)) &&
|
||||
writePayload("e, 1) &&
|
||||
writePayload(&colon, 1) &&
|
||||
componentSerializers[i]->flush();
|
||||
serializer->flush();
|
||||
delete serializer;
|
||||
}
|
||||
delete componentSerializers[i];
|
||||
}
|
||||
|
||||
if (written) {
|
||||
written = writePayload(AHATOFSTR(HASerializerJsonDataSuffix)) &&
|
||||
writePayload(AHATOFSTR(HASerializerJsonDataSuffix));
|
||||
if (!written) {
|
||||
abortDirectPublish();
|
||||
arduinoHALog(ArduinoHALogLevel::Warn, kDiscovery, F("device discovery stream aborted"));
|
||||
return false;
|
||||
}
|
||||
const bool published = endPublish() && written;
|
||||
|
||||
written = writePayload(AHATOFSTR(HASerializerJsonDataSuffix)) &&
|
||||
writePayload(AHATOFSTR(HASerializerJsonDataSuffix));
|
||||
if (!written) {
|
||||
abortDirectPublish();
|
||||
arduinoHALog(ArduinoHALogLevel::Warn, kDiscovery, F("device discovery stream aborted"));
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool published = endPublish();
|
||||
if (!published) {
|
||||
arduinoHALog(ArduinoHALogLevel::Warn, kDiscovery, F("device discovery publish failed"));
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class PubSubClientMock;
|
||||
|
||||
class HADevice;
|
||||
class HABaseDeviceType;
|
||||
class HASerializer;
|
||||
|
||||
#if defined(ARDUINO_API_VERSION)
|
||||
using namespace arduino;
|
||||
@@ -609,6 +610,11 @@ private:
|
||||
|
||||
bool publishDeviceDiscoveryPayload(HABaseDeviceType* removalType = nullptr);
|
||||
|
||||
HASerializer* buildDeviceDiscoveryComponentSerializer(
|
||||
HABaseDeviceType* deviceType,
|
||||
HABaseDeviceType* removalType
|
||||
);
|
||||
|
||||
bool clearDeviceDiscoveryConfig();
|
||||
|
||||
bool publishDeviceDiscoveryMigrationMarker(HABaseDeviceType* deviceType);
|
||||
@@ -687,6 +693,15 @@ private:
|
||||
*/
|
||||
String formatDirectPublishFailureDiagnostics(bool hamqttConnectedBefore, int pubsubStateBefore) const;
|
||||
|
||||
// Buffer direct MQTT streaming writes so serializers do not issue hundreds
|
||||
// of tiny TCP writes while a PubSubClient publish is open.
|
||||
static const uint16_t DirectPublishBufferSize = 128;
|
||||
bool flushDirectPublishBuffer();
|
||||
bool appendDirectPublishPayload(const uint8_t* data, uint16_t length);
|
||||
bool appendDirectPublishProgmemPayload(const __FlashStringHelper* src);
|
||||
void clearDirectPublishBuffer();
|
||||
void abortDirectPublish();
|
||||
|
||||
#ifdef ARDUINOHA_TEST
|
||||
PubSubClientMock* _mqtt;
|
||||
#else
|
||||
@@ -697,6 +712,10 @@ private:
|
||||
PubSubClient* _mqtt;
|
||||
#endif
|
||||
|
||||
uint8_t _directPublishBuffer[DirectPublishBufferSize];
|
||||
uint16_t _directPublishBufferLength;
|
||||
bool _directPublishActive;
|
||||
|
||||
/// Instance of the HADevice passed to the constructor.
|
||||
const HADevice& _device;
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ HASerializer::HASerializer(
|
||||
_deviceType(deviceType),
|
||||
_entriesNb(0),
|
||||
_maxEntriesNb(maxEntriesNb),
|
||||
_entries(new SerializerEntry[maxEntriesNb])
|
||||
_entries(new (std::nothrow) SerializerEntry[maxEntriesNb])
|
||||
{
|
||||
|
||||
}
|
||||
@@ -340,6 +340,10 @@ void HASerializer::topic(const __FlashStringHelper* topic)
|
||||
|
||||
HASerializer::SerializerEntry* HASerializer::addEntry()
|
||||
{
|
||||
if (!_entries) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (_entriesNb >= _maxEntriesNb) {
|
||||
if (_maxEntriesNb == UINT8_MAX) {
|
||||
return nullptr;
|
||||
@@ -370,6 +374,10 @@ HASerializer::SerializerEntry* HASerializer::addEntry()
|
||||
|
||||
uint16_t HASerializer::calculateSize() const
|
||||
{
|
||||
if (!_entries) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t size =
|
||||
strlen_P(HASerializerJsonDataPrefix) +
|
||||
strlen_P(HASerializerJsonDataSuffix);
|
||||
|
||||
@@ -31,7 +31,16 @@ HA_VERSION=2024.11.3 docker compose -f tests/ha-contract/compose.yaml down -v
|
||||
```
|
||||
|
||||
Use `HA_VERSION=stable` and `HA_VERSION=dev` for the current supported and
|
||||
development Home Assistant images. The test uses only ephemeral named volumes;
|
||||
development Home Assistant images.
|
||||
|
||||
The checked-in GitHub workflow runs the baseline, stable, and development images
|
||||
as a compatibility audit. When it is on the repository's default branch, GitHub
|
||||
exposes it for manual dispatch and the weekly schedule. It is not a tag-release
|
||||
gate.
|
||||
|
||||
For a local development-image test, add `CONTRACT_EXPECT_DISABLED_CLEANUP=1` to
|
||||
both `docker compose run ... tests` commands. That lane checks that initially
|
||||
disabled device components are cleaned. The test uses only ephemeral named volumes;
|
||||
`down -v` removes its broker data, Home Assistant config, owner token, and
|
||||
registry state.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user