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:
parent
1313f47856
commit
87caad25be
20 changed files with 3117 additions and 2782 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue