1.1.0: begin(username, password, rootTopic) - the library signature now matches every example, the readme and the website (as published since 2024 no sketch could authenticate: the 64-hex password became the MQTT username). Discovery answers go out over MQTT directly, every device, resumed from loop() under heap pressure within the backend's 5 s window, instead of the 5-slot HTTP queue that silently dropped the rest (which the backend's reconcile then deleted from Alexa). JSON aligned with Alexa / alex2node: event.payload present, ActionMapping payload an object, semantics only when a capability has mappings. Pointer-stable device/capability containers (std::deque), the MQTT payload is copied by length before parsing (no write past the buffer), fragmented directive ids are ignored (a Discover is answered on its first fragment), deprecated ArduinoJson 7 calls replaced, credential Serial prints removed, ESP32 include guards (untested; the ESP8266 is the target). Examples: WIFI_PASSWORD, LED_BUILTIN fallback, string+int print fixes. library.json + library.properties restored (1.1.0; AsyncMqttClient, ArduinoJson 7, ESP Async TCP), LICENSE (MIT), a real keywords.txt, .gitattributes text=auto eol=lf (tree normalised to LF). readme: Forgejo URLs, platformio.ini snippet, Library Manager install, begin() order, INTERIOR_BLIND for blinds, 1.1.0 changelog. All five examples compile warning-free for d1_mini (PlatformIO 6.2, espressif8266) on pve-B450.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 04:58:38 +00:00
parent 1313f47856
commit 87caad25be
20 changed files with 3117 additions and 2782 deletions

View file

@ -1,167 +1,171 @@
#include <ArduinoJson.h>
#include <Alex2ESP.h>
#include <AlexaUtils.h>
#ifndef ALEXA_STATUS_MESSAGE_H
#define ALEXA_STATUS_MESSAGE_H
#define MAX_STATUS_REPORT_SIZE 2048
enum class EndpointHealth
{
OK,
UNREACHABLE
};
enum class PowerController
{
ON,
OFF
};
enum class TemperatureSensorScale
{
CELSIUS,
FAHRENHEIT
};
class AlexaStatusMessage
{
public:
AlexaStatusMessage(const String &correlationToken, const String &rootTopic, const String &endpointId, const bool isResponse)
{
this->endpointId = endpointId;
this->rootTopic = rootTopic;
JsonObject event = doc["event"].to<JsonObject>();
JsonObject event_header = event["header"].to<JsonObject>();
event_header["namespace"] = "Alexa";
if (isResponse)
{
event_header["name"] = "Response";
}
else
{
event_header["name"] = "StateReport";
}
event_header["payloadVersion"] = "3";
event_header["messageId"] = generateMessageId();
event_header["correlationToken"] = correlationToken;
JsonObject event_endpoint = event["endpoint"].to<JsonObject>();
event_endpoint["endpointId"] = endpointId;
contextProperties = doc["context"]["properties"].to<JsonArray>();
}
void setEndpointId(const String &endpointId)
{
// event["endpoint"]["endpointId"] = endpointId;
}
AlexaStatusMessage &AddHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0)
{
JsonDocument healthValue;
healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE";
return AddProperty(AlexaInterfaceType::ENDPOINT_HEALTH, "connectivity", healthValue.as<JsonObject>(), uncertaintyInMs);
}
AlexaStatusMessage &AddPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaceType::POWER_CONTROLLER, "powerState", value, uncertaintyInMs);
}
AlexaStatusMessage &AddTemperatureSensorProp(TemperatureSensorScale tempSensor,float value, unsigned int uncertaintyInMs = 0)
{
JsonDocument tempValue;
tempValue["scale"] = "CELSIUS";
tempValue["value"] = value;
if((tempSensor == TemperatureSensorScale::FAHRENHEIT)){
tempValue["value"] = (value - 32) * 5.0 / 9.0;
}
return AddProperty(AlexaInterfaceType::TEMPERATURE_SENSOR, "temperature", tempValue.as<JsonObject>(), uncertaintyInMs);
}
AlexaStatusMessage &AddBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaceType::BRIGHTNESS_CONTROLLER, "brightness", brightness, uncertaintyInMs);
}
AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER, "colorTemperatureInKelvin", colorTemperature, uncertaintyInMs);
}
AlexaStatusMessage &AddToggleControllerProp(PowerController powerController,String instanceName, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaceType::TOGGLE_CONTROLLER, "toggleState", value, uncertaintyInMs,instanceName);
}
AlexaStatusMessage &AddContextProp(const JsonObject &property)
{
contextProperties.add(property);
return *this; // Return a reference to the current object
}
void send()
{
doc.shrinkToFit();
serializeJson(doc, outputString);
doc.clear();
String topic = rootTopic + "/" + endpointId + "/alexaResponce";
AlexaUtils::enqueue(outputString, topic.c_str());
}
private:
String rootTopic;
String endpointId;
JsonDocument doc;
JsonArray contextProperties;
static char outputString[MAX_STATUS_REPORT_SIZE];
// TODO: make real uuid4 gen function
String generateMessageId()
{
char buffer[38];
const char charset[] = "0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // Allowed characters
// Seed the random number generator (optional)
srand(static_cast<unsigned int>(time(nullptr)));
// Generate 37 random characters
for (int i = 0; i < 37; ++i)
{
buffer[i] = charset[rand() % (sizeof(charset) - 1)]; // Pick a random character
}
buffer[37] = '\0'; // Null-terminate the string
return String(buffer);
}
template <typename T>
AlexaStatusMessage &AddProperty(AlexaInterfaceType type, const String &propertyName, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="")
{
JsonDocument prop;
prop["namespace"] = AlexaInterfaceUtils::toString(type);
prop["name"] = propertyName;
if (!instanceName.isEmpty()) {
prop["instance"] = instanceName;
}
if constexpr (std::is_same_v<T, JsonObject>)
{
prop["value"] = value;
}
else
{
prop["value"] = value;
}
prop["timeOfSample"] = "{REPLACE_WITH_DATETIME}";
prop["uncertaintyInMilliseconds"] = uncertaintyInMs;
contextProperties.add(prop.as<JsonObject>());
return *this;
}
};
#endif
#ifndef ALEXA_STATUS_MESSAGE_H
#define ALEXA_STATUS_MESSAGE_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include "AlexaInterface.h"
#include "AlexaUtils.h"
#define MAX_STATUS_REPORT_SIZE 2048
enum class EndpointHealth
{
OK,
UNREACHABLE
};
enum class PowerController
{
ON,
OFF
};
enum class TemperatureSensorScale
{
CELSIUS,
FAHRENHEIT
};
class AlexaStatusMessage
{
public:
AlexaStatusMessage(const String &correlationToken, const String &rootTopic, const String &endpointId, const bool isResponse)
{
this->endpointId = endpointId;
this->rootTopic = rootTopic;
JsonObject event = doc["event"].to<JsonObject>();
JsonObject event_header = event["header"].to<JsonObject>();
event_header["namespace"] = "Alexa";
if (isResponse)
{
event_header["name"] = "Response";
}
else
{
event_header["name"] = "StateReport";
}
event_header["payloadVersion"] = "3";
event_header["messageId"] = generateMessageId();
event_header["correlationToken"] = correlationToken;
JsonObject event_endpoint = event["endpoint"].to<JsonObject>();
event_endpoint["endpointId"] = endpointId;
event["payload"].to<JsonObject>(); // required by Alexa.Response / StateReport, empty when there is nothing to add
contextProperties = doc["context"]["properties"].to<JsonArray>();
}
AlexaStatusMessage &AddHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0)
{
JsonDocument healthValue;
healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE";
return AddProperty(AlexaInterfaceType::ENDPOINT_HEALTH, "connectivity", healthValue.as<JsonObject>(), uncertaintyInMs);
}
AlexaStatusMessage &AddPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaceType::POWER_CONTROLLER, "powerState", value, uncertaintyInMs);
}
AlexaStatusMessage &AddTemperatureSensorProp(TemperatureSensorScale tempSensor,float value, unsigned int uncertaintyInMs = 0)
{
JsonDocument tempValue;
tempValue["scale"] = "CELSIUS";
tempValue["value"] = value;
if((tempSensor == TemperatureSensorScale::FAHRENHEIT)){
tempValue["value"] = (value - 32) * 5.0 / 9.0;
}
return AddProperty(AlexaInterfaceType::TEMPERATURE_SENSOR, "temperature", tempValue.as<JsonObject>(), uncertaintyInMs);
}
AlexaStatusMessage &AddBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaceType::BRIGHTNESS_CONTROLLER, "brightness", brightness, uncertaintyInMs);
}
AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER, "colorTemperatureInKelvin", colorTemperature, uncertaintyInMs);
}
AlexaStatusMessage &AddToggleControllerProp(PowerController powerController,String instanceName, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaceType::TOGGLE_CONTROLLER, "toggleState", value, uncertaintyInMs,instanceName);
}
AlexaStatusMessage &AddContextProp(const JsonObject &property)
{
contextProperties.add(property);
return *this; // Return a reference to the current object
}
// Queue the report for delivery. Returns false (and says so on Serial) when the report does not fit the
// MAX_STATUS_REPORT_SIZE buffer or the send queue is full: a report is never sent truncated.
bool send()
{
doc.shrinkToFit();
String topic = rootTopic + "/" + endpointId + "/alexaResponce";
size_t needed = measureJson(doc);
if (needed > sizeof(outputString) - 1)
{
Serial.printf("[Alex2ESP] status report for %s is %u bytes, limit is %u - not sent\n",
endpointId.c_str(), (unsigned)needed, (unsigned)(sizeof(outputString) - 1));
doc.clear();
return false;
}
serializeJson(doc, outputString);
doc.clear();
if (!AlexaUtils::enqueue(outputString, topic.c_str()))
{
Serial.println("[Alex2ESP] send queue full, status report dropped");
return false;
}
return true;
}
private:
String rootTopic;
String endpointId;
JsonDocument doc;
JsonArray contextProperties;
static char outputString[MAX_STATUS_REPORT_SIZE];
// TODO: make real uuid4 gen function
String generateMessageId()
{
char buffer[38];
const char charset[] = "0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // Allowed characters
// Seed the random number generator (optional)
srand(static_cast<unsigned int>(time(nullptr)));
// Generate 37 random characters
for (int i = 0; i < 37; ++i)
{
buffer[i] = charset[rand() % (sizeof(charset) - 1)]; // Pick a random character
}
buffer[37] = '\0'; // Null-terminate the string
return String(buffer);
}
template <typename T>
AlexaStatusMessage &AddProperty(AlexaInterfaceType type, const String &propertyName, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="")
{
JsonDocument prop;
prop["namespace"] = AlexaInterfaceUtils::toString(type);
prop["name"] = propertyName;
if (!instanceName.isEmpty()) {
prop["instance"] = instanceName;
}
prop["value"] = value;
prop["timeOfSample"] = "{REPLACE_WITH_DATETIME}";
prop["uncertaintyInMilliseconds"] = uncertaintyInMs;
contextProperties.add(prop.as<JsonObject>());
return *this;
}
};
#endif