mirror of
https://github.com/alexhopeoconnor/arduino-home-assistant.git
synced 2026-10-04 02:48:13 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72e80f236e | ||
|
|
3892ce1ca9 | ||
|
|
ea6f1cf353 | ||
|
|
e3c9388ffc | ||
|
|
e6dee4727d |
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 3.2.1
|
||||
|
||||
- Clarify discovery migration and entity documentation, including the choices
|
||||
for new devices, existing installations, and no-device-discovery projects.
|
||||
- Refresh the canonical PlatformIO and Arduino IDE package metadata.
|
||||
|
||||
## 3.2.0
|
||||
|
||||
**Discovery publication and resilience:**
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
```
|
||||
@@ -57,7 +61,7 @@ Build [ESP Sensor](examples/01-esp-sensor/) for a complete ESP8266/ESP32 project
|
||||
|
||||
```ini
|
||||
lib_deps =
|
||||
home-assistant-integration=https://github.com/alexhopeoconnor/arduino-home-assistant.git#v3.2.0
|
||||
home-assistant-integration=https://github.com/alexhopeoconnor/arduino-home-assistant.git#v3.2.1
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# ESP8266 Postmortem linker workaround
|
||||
|
||||
The ESP8266 Arduino framework pin used by the DeviceFramework, WiFiManager,
|
||||
DFTE, and ArduinoHA maintained builds 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.
|
||||
|
||||
ArduinoHA's root `pio test -e esp8266` environment and every guided ESP8266
|
||||
PlatformIO example now use this same snapshot. There is no root-test exception
|
||||
that can hide a linker regression from the example test harness.
|
||||
|
||||
Keep this exact framework snapshot in ESP8266 environments that need the
|
||||
maintained test harness. 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. In
|
||||
particular, changing the ESP32 validation lane does not justify changing this
|
||||
ESP8266 pin.
|
||||
|
||||
The corresponding ESP32 Core 3.3.11 pin is 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 |
|
||||
|
||||
|
||||
@@ -16,6 +16,45 @@ The only imported upstream code fix is the four missing-device-ID guards from
|
||||
upstream commit `9c9d074`. Device discovery migration, JSON validation,
|
||||
lifecycle handling, and tests are fork-native changes.
|
||||
|
||||
## Arduino framework validation lanes
|
||||
|
||||
ArduinoHA compiles its embedded test suites and guided PlatformIO examples in
|
||||
two maintained lanes:
|
||||
|
||||
| Selector | Target | Pinned stack | Role |
|
||||
| --- | --- | --- | --- |
|
||||
| `esp8266` | ESP8266 D1 mini | Arduino-ESP8266 commit `521ae60` | Maintained Postmortem linker-fix test-harness lane |
|
||||
| `esp32` | ESP32 Dev Module | pioarduino `55.03.311` / Arduino-ESP32 3.3.11 / ESP-IDF 5.5.5 | Maintained baseline |
|
||||
|
||||
Every lane pins its complete framework stack. The ESP32 runner keeps its
|
||||
maintained pioarduino graph in the persistent cache shared by these maintained
|
||||
framework repositories, so stale global metadata cannot select an uploader or
|
||||
compiler by accident without redownloading matching inputs for every repository.
|
||||
The ESP8266 framework pin is independent of the ESP32 baseline; see the [linker
|
||||
workaround](ESP8266-LINKER-WORKAROUND.md).
|
||||
|
||||
Run the current ESP32 lane locally with:
|
||||
|
||||
```bash
|
||||
./scripts/test.sh compile --platform esp32
|
||||
./scripts/test.sh examples --platform esp32
|
||||
```
|
||||
|
||||
The script keeps that lane in the persistent PlatformIO Core/cache shared by
|
||||
the maintained framework repositories by default
|
||||
(`${XDG_CACHE_HOME:-$HOME/.cache}/arduino-framework-platformio/core-3.3.11`).
|
||||
This prevents stale global `tool-esptoolpy` metadata from shadowing the current
|
||||
pioarduino package-form uploader while avoiding duplicate Core 3.3.11
|
||||
downloads. Set `ARDUINOHA_PLATFORMIO_CORE_DIR`,
|
||||
`ARDUINOHA_PLATFORMIO_PACKAGES_DIR`, and
|
||||
`ARDUINOHA_PLATFORMIO_CACHE_DIR` for a dedicated disk. The script never clears
|
||||
that cache or repairs it by overriding one compiler package.
|
||||
|
||||
The embedded multi-suite compile path also defaults
|
||||
`PLATFORMIO_RUN_JOBS=1`. A caller may explicitly set another value, but the
|
||||
serial default avoids a reproducible PlatformIO archive missing-object race
|
||||
seen in the Core 3.3.11 lane. Example builds remain parallel.
|
||||
|
||||
## Deliberate exclusions
|
||||
|
||||
- No merge or wholesale cherry-pick of upstream `develop` / WIP 2.2.0.
|
||||
|
||||
@@ -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
|
||||
@@ -51,9 +54,19 @@ mqtt.setDataPrefix("myDataPrefix");
|
||||
|
||||
Both formats remain supported by Home Assistant. Device discovery requires Home Assistant **2024.11.0 or newer**. Use `enableDeviceDiscovery()` only for a new device which has never published this library's single-component discovery topics.
|
||||
|
||||
Choose the mode before connecting a device:
|
||||
|
||||
- **New device:** call `enableDeviceDiscovery()` before connecting.
|
||||
- **Existing device using single-component discovery:** follow the migration
|
||||
procedure below.
|
||||
- **No need for device discovery:** keep the default behavior.
|
||||
|
||||
### Migrating an existing device to device discovery
|
||||
|
||||
Do not switch an existing device by calling `enableDeviceDiscovery()` alone. Home Assistant derives an entity's discovery identity from the topic, and a direct switch causes retained-topic conflicts. The migration deliberately publishes, in order:
|
||||
Do not switch an existing device by calling `enableDeviceDiscovery()` alone.
|
||||
Home Assistant derives an entity's discovery identity from the topic, and a
|
||||
direct switch causes retained-topic conflicts. The migration publishes, in
|
||||
order:
|
||||
|
||||
1. `{"migrate_discovery":true}` to every old retained component config topic;
|
||||
2. the retained `/device/<deviceId>/config` payload; and only then
|
||||
@@ -96,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()`**.
|
||||
|
||||
+3
-1
@@ -22,7 +22,9 @@ ArduinoHA supports these Home Assistant MQTT discovery entity classes. Choose th
|
||||
| Tag scanner | [tag-scanner](../examples/tag-scanner/tag-scanner.ino) |
|
||||
| Text | Use the API header; no dedicated sketch yet |
|
||||
|
||||
The library does not currently implement alarm control panels, events, humidifiers, images, lawn mowers, sirens, updates, vacuums, valves, or water heaters. Those are deliberate unsupported surfaces, not configuration switches.
|
||||
The library does not currently implement alarm control panels, events,
|
||||
humidifiers, images, lawn mowers, sirens, updates, vacuums, valves, or water
|
||||
heaters. They cannot be enabled through configuration.
|
||||
|
||||
## Common lifecycle
|
||||
|
||||
|
||||
+13
-6
@@ -11,7 +11,7 @@ ArduinoHA talks to Home Assistant over **MQTT** (TCP). You need an MQTT broker r
|
||||
|
||||
```ini
|
||||
lib_deps =
|
||||
home-assistant-integration=https://github.com/alexhopeoconnor/arduino-home-assistant.git#v3.2.0
|
||||
home-assistant-integration=https://github.com/alexhopeoconnor/arduino-home-assistant.git#v3.2.1
|
||||
```
|
||||
|
||||
**Arduino IDE:** this fork is not indexed by Library Manager. Download the
|
||||
@@ -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.
|
||||
|
||||
@@ -16,7 +16,7 @@ platform_packages =
|
||||
|
||||
[env:esp32]
|
||||
extends = common
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
build_unflags = -std=gnu++11
|
||||
build_flags =
|
||||
|
||||
@@ -16,7 +16,7 @@ platform_packages =
|
||||
|
||||
[env:esp32]
|
||||
extends = common
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
build_unflags = -std=gnu++11
|
||||
build_flags =
|
||||
|
||||
@@ -16,7 +16,7 @@ platform_packages =
|
||||
|
||||
[env:esp32]
|
||||
extends = common
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
build_unflags = -std=gnu++11
|
||||
build_flags =
|
||||
|
||||
@@ -16,7 +16,7 @@ platform_packages =
|
||||
|
||||
[env:esp32]
|
||||
extends = common
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
build_unflags = -std=gnu++11
|
||||
build_flags =
|
||||
|
||||
@@ -7,6 +7,9 @@ pio run -d examples/01-esp-sensor -e esp8266
|
||||
pio run -d examples/01-esp-sensor -e esp8266 -t upload
|
||||
```
|
||||
|
||||
The checked-in `esp32` environment uses Arduino-ESP32 3.3.11. A consuming
|
||||
application should choose and pin its complete PlatformIO platform stack.
|
||||
|
||||
| Guided example | What it demonstrates |
|
||||
| --- | --- |
|
||||
| [ESP Sensor](01-esp-sensor/) | Wi-Fi application wiring, a unique device ID, and changing numeric telemetry |
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "home-assistant-integration",
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.1",
|
||||
"description": "Maintained Home Assistant MQTT discovery and entity integration for Arduino and ESP devices.",
|
||||
"keywords": [
|
||||
"mqtt",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name=home-assistant-integration
|
||||
version=3.2.0
|
||||
version=3.2.1
|
||||
author=Dawid Chyrzynski <dev@chyrzynski.pl>, Alex Hope-O'Connor <alex.hope.oconnor@pomonaqld.au>
|
||||
maintainer=Alex Hope-O'Connor <alex.hope.oconnor@pomonaqld.au>
|
||||
sentence=Home Assistant MQTT integration for Arduino
|
||||
|
||||
+6
-1
@@ -3,6 +3,7 @@
|
||||
; Full suite:
|
||||
; pio test -e esp8266
|
||||
; pio test -e esp32
|
||||
; ./scripts/test.sh compile --platform esp32
|
||||
;
|
||||
; Single suite (examples):
|
||||
; pio test -e esp8266 --filter test_utils_serializers
|
||||
@@ -30,11 +31,15 @@ extends = common
|
||||
test_ignore = test_native_core
|
||||
platform = espressif8266
|
||||
board = d1_mini
|
||||
; Keep root Unity coverage on the same Postmortem relocation-fix snapshot as
|
||||
; the maintained ESP8266 examples. See docs/ESP8266-LINKER-WORKAROUND.md.
|
||||
platform_packages =
|
||||
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
|
||||
|
||||
[env:esp32]
|
||||
extends = common
|
||||
test_ignore = test_native_core
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
|
||||
board = esp32dev
|
||||
build_unflags = -std=gnu++11
|
||||
build_flags = ${common.build_flags} -std=gnu++14
|
||||
|
||||
+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"
|
||||
|
||||
+27
-2
@@ -13,9 +13,34 @@ case "${3:-}" in
|
||||
esac
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
pio_for_platform() {
|
||||
if [[ "$environment" != "esp32" ]]; then
|
||||
pio "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
# Keep Core 3.3.11's package-form esptool and generated pioarduino
|
||||
# environment in the persistent cache shared by the maintained framework
|
||||
# repositories. It is never cleared by this script, and isolates this
|
||||
# tested graph from stale global metadata without duplicate downloads.
|
||||
local core_dir packages_dir cache_dir
|
||||
core_dir="${ARDUINOHA_PLATFORMIO_CORE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/arduino-framework-platformio/core-3.3.11}"
|
||||
packages_dir="${ARDUINOHA_PLATFORMIO_PACKAGES_DIR:-$core_dir/packages}"
|
||||
cache_dir="${ARDUINOHA_PLATFORMIO_CACHE_DIR:-$core_dir/cache}"
|
||||
install -d -m 700 "$core_dir" "$packages_dir" "$cache_dir"
|
||||
PLATFORMIO_CORE_DIR="$core_dir" PLATFORMIO_PACKAGES_DIR="$packages_dir" \
|
||||
PLATFORMIO_CACHE_DIR="$cache_dir" pio "$@"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
compile)
|
||||
pio test -d "$root" -e "$environment" --without-uploading --without-testing
|
||||
# PlatformIO's test command starts many archive jobs by default. This
|
||||
# multi-suite project has a reproducible missing-object race on Core
|
||||
# 3.3.11 at high parallelism, so use a deterministic default while
|
||||
# preserving an explicit caller override.
|
||||
export PLATFORMIO_RUN_JOBS="${PLATFORMIO_RUN_JOBS:-1}"
|
||||
pio_for_platform test -d "$root" -e "$environment" --without-uploading --without-testing
|
||||
echo "ArduinoHA compile check passed for $environment"
|
||||
;;
|
||||
examples)
|
||||
@@ -25,7 +50,7 @@ case "$1" in
|
||||
exit 1
|
||||
fi
|
||||
for example in "${examples[@]}"; do
|
||||
pio run -d "$example" -e "$environment" </dev/null
|
||||
pio_for_platform run -d "$example" -e "$environment" </dev/null
|
||||
done
|
||||
echo "ArduinoHA examples compile check passed for $environment"
|
||||
;;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
#endif
|
||||
|
||||
// Current library version used in discovery origin metadata.
|
||||
#define ARDUINOHA_LIBRARY_VERSION "3.2.0"
|
||||
#define ARDUINOHA_LIBRARY_VERSION "3.2.1"
|
||||
|
||||
#if defined(ARDUINOHA_DEBUG)
|
||||
#include <Arduino.h>
|
||||
|
||||
@@ -42,34 +42,34 @@ class HomeAssistantClient:
|
||||
token_file = state / "ha-token"
|
||||
cls.wait_until_ready(base_url)
|
||||
if token_file.exists():
|
||||
return cls(base_url, token_file.read_text(encoding="utf-8").strip())
|
||||
|
||||
client_id = "http://ha-mqtt-test-harness.local/"
|
||||
user = {
|
||||
"client_id": client_id,
|
||||
"name": owner_name,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"language": "en",
|
||||
}
|
||||
created = cls._response_json(
|
||||
requests.post(f"{base_url}/api/onboarding/users", json=user, timeout=10),
|
||||
"Home Assistant onboarding user creation",
|
||||
)
|
||||
auth_code = created.get("auth_code")
|
||||
if not auth_code:
|
||||
raise TestHarnessError("Home Assistant onboarding did not return an auth_code")
|
||||
token_response = cls._response_json(
|
||||
requests.post(
|
||||
f"{base_url}/auth/token",
|
||||
data={"client_id": client_id, "grant_type": "authorization_code", "code": auth_code},
|
||||
timeout=10,
|
||||
),
|
||||
"Home Assistant token exchange",
|
||||
)
|
||||
token = token_response.get("access_token")
|
||||
if not token:
|
||||
raise TestHarnessError("Home Assistant token exchange did not return an access_token")
|
||||
token = token_file.read_text(encoding="utf-8").strip()
|
||||
else:
|
||||
client_id = "http://ha-mqtt-test-harness.local/"
|
||||
user = {
|
||||
"client_id": client_id,
|
||||
"name": owner_name,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"language": "en",
|
||||
}
|
||||
created = cls._response_json(
|
||||
requests.post(f"{base_url}/api/onboarding/users", json=user, timeout=10),
|
||||
"Home Assistant onboarding user creation",
|
||||
)
|
||||
auth_code = created.get("auth_code")
|
||||
if not auth_code:
|
||||
raise TestHarnessError("Home Assistant onboarding did not return an auth_code")
|
||||
token_response = cls._response_json(
|
||||
requests.post(
|
||||
f"{base_url}/auth/token",
|
||||
data={"client_id": client_id, "grant_type": "authorization_code", "code": auth_code},
|
||||
timeout=10,
|
||||
),
|
||||
"Home Assistant token exchange",
|
||||
)
|
||||
token = token_response.get("access_token")
|
||||
if not token:
|
||||
raise TestHarnessError("Home Assistant token exchange did not return an access_token")
|
||||
|
||||
client = cls(base_url, token)
|
||||
for path, payload in (
|
||||
@@ -81,6 +81,51 @@ class HomeAssistantClient:
|
||||
raise TestHarnessError(
|
||||
f"Home Assistant onboarding step {path} failed: {response.status_code} {response.text}"
|
||||
)
|
||||
|
||||
# Complete the final server-side step before any browser test starts.
|
||||
# Leaving it to first-login UI redirects makes a shared disposable
|
||||
# instance timing-dependent and can leave it stuck on the onboarding
|
||||
# route. Matching Home Assistant's own origin keeps the IndieAuth
|
||||
# client and redirect URI locally verifiable.
|
||||
integration_response = requests.post(
|
||||
f"{base_url}/api/onboarding/integration",
|
||||
headers=client._headers,
|
||||
json={
|
||||
"client_id": f"{base_url}/",
|
||||
"redirect_uri": f"{base_url}/onboarding.html?auth_callback=1",
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
if integration_response.status_code not in (200, 201, 400, 403, 404):
|
||||
raise TestHarnessError(
|
||||
"Home Assistant onboarding integration step failed: "
|
||||
f"{integration_response.status_code} {integration_response.text}"
|
||||
)
|
||||
|
||||
onboarding_response = requests.get(f"{base_url}/api/onboarding", timeout=10)
|
||||
if not onboarding_response.ok:
|
||||
raise TestHarnessError(
|
||||
"Home Assistant onboarding status check failed: "
|
||||
f"{onboarding_response.status_code} {onboarding_response.text}"
|
||||
)
|
||||
try:
|
||||
onboarding_steps = onboarding_response.json()
|
||||
except ValueError as error:
|
||||
raise TestHarnessError("Home Assistant onboarding status returned invalid JSON") from error
|
||||
if not isinstance(onboarding_steps, list):
|
||||
raise TestHarnessError(
|
||||
"Home Assistant onboarding status returned unexpected JSON: "
|
||||
f"{onboarding_steps}"
|
||||
)
|
||||
pending_steps = [
|
||||
str(step.get("step", "unknown"))
|
||||
for step in onboarding_steps
|
||||
if isinstance(step, dict) and step.get("done") is not True
|
||||
]
|
||||
if pending_steps:
|
||||
raise TestHarnessError(
|
||||
"Home Assistant onboarding remains incomplete after bootstrap: " + ", ".join(pending_steps)
|
||||
)
|
||||
state.mkdir(parents=True, exist_ok=True)
|
||||
token_file.write_text(token, encoding="utf-8")
|
||||
return client
|
||||
|
||||
@@ -81,7 +81,7 @@ void onNativeStateChanged(HAMqtt::ConnectionState)
|
||||
}
|
||||
|
||||
|
||||
void test_json_helpers_escape_control_bytes_and_preserve_cursor_contract()
|
||||
void test_json_helpers_escape_control_bytes_and_preserve_cursor_position()
|
||||
{
|
||||
const char value[] = "quote\" slash\\ newline\n tab\t control\x01";
|
||||
char output[96] = {};
|
||||
@@ -341,7 +341,7 @@ void tearDown(void) { }
|
||||
int main(int, char**)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_json_helpers_escape_control_bytes_and_preserve_cursor_contract);
|
||||
RUN_TEST(test_json_helpers_escape_control_bytes_and_preserve_cursor_position);
|
||||
RUN_TEST(test_availability_and_serializer_array_escape_json_values);
|
||||
RUN_TEST(test_discovery_topic_tokens_are_rejected_before_topic_generation);
|
||||
RUN_TEST(test_streaming_serializer_writes_exact_escaped_payload_to_mqtt_mock);
|
||||
|
||||
Reference in New Issue
Block a user