AlexaStatusMessage: DeferredResponse, ChangeReport, ErrorResponse, scene and doorbell events, UUID-shaped messageId

A message has a kind, and the kind decides its topic: answers on
<root>/<id>/alexaResponce, the answer after a DeferredResponse on
<root>/<id>/deferredResponse (sendAsync()), a ChangeReport on
<root>/changeReport with the changed properties apart from the others
(context()), DoorbellPress on <root>/event. AlexaDirective gains deferred(),
error(type, message), sceneStarted() and sceneStopped(); thermostat error
types go out in the namespace of the thermostat. The helpers are add*Prop,
the Add*Prop names stay; a temperature keeps its scale (69 FAHRENHEIT, was
20.56 CELSIUS). messageId is a version 4 UUID from the hardware random
source, where rand() seeded per second gave two messages one id.
basicLight: static RAM 30,540 B (-76), flash 334,981 B (+1,280);
112 host tests (bridge logic 52, messages 19, new).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 20:35:56 +00:00
parent 42af413a6e
commit 3215457f91
12 changed files with 1093 additions and 90 deletions

View file

@ -10,7 +10,11 @@
* <root>/discover in answered with one discovery object per device on <root>/discover_r
* <root>/<endpointId>/alexaDirective in the directive, handed to the handler of the device (onDirective(), or
* the ReportState / Event handlers of 1.x)
* <root>/<endpointId>/alexaResponce out the report the handler built with buildStatusMessage()
* <root>/<endpointId>/alexaResponce out the answer the handler built: Response, StateReport, ErrorResponse,
* DeferredResponse, the events of a scene
* <root>/<endpointId>/deferredResponse out the answer that follows a DeferredResponse (sendAsync())
* <root>/changeReport out ChangeReport: properties that changed without a directive
* <root>/event out what a device reports unasked, DoorbellPress
*/
#ifndef ALEX2ESP_H

View file

@ -260,6 +260,40 @@ bool AlexaBridgeLogic::formatTimestamp(time_t instant, char *buffer, size_t size
return true;
}
bool AlexaBridgeLogic::formatMessageId(const uint32_t random[4], char *buffer, size_t size)
{
if (buffer == nullptr || size == 0)
{
return false;
}
if (size < ALEXA_MESSAGE_ID_SIZE)
{
buffer[0] = '\0';
return false;
}
char *next = buffer;
for (uint8_t digit = 0; digit < 32; digit++)
{
if (digit == 8 || digit == 12 || digit == 16 || digit == 20)
{
*next++ = '-';
}
uint8_t value = (random[digit / 8] >> (28 - 4 * (digit % 8))) & 0x0F;
if (digit == 12)
{
value = 4; // the version: made of random numbers
}
else if (digit == 16)
{
value = 8 | (value & 3); // the variant: 8, 9, a or b
}
*next++ = static_cast<char>(value < 10 ? '0' + value : 'a' + value - 10);
}
*next = '\0';
return true;
}
const char *AlexaBridgeLogic::directiveEndpoint(const char *topic, const char *rootTopic, size_t *length)
{
static const char suffix[] = "/alexaDirective";

View file

@ -13,6 +13,9 @@
#include "AlexaLimits.h"
#include "AlexaTransport.h"
// "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" and the terminating NUL
#define ALEXA_MESSAGE_ID_SIZE 37
// The last values it was given, as many as CAPACITY: what a repeat is recognised by.
class AlexaRecentHashes
{
@ -213,6 +216,11 @@ namespace AlexaBridgeLogic
// is smaller than ALEXA_TIMESTAMP_SIZE or the year has more than four digits.
bool formatTimestamp(time_t instant, char *buffer, size_t size);
// Writes 128 random bits as the messageId of a message: a UUID of version 4 (RFC 4122), which keeps 122 of
// them, in lower case. Returns false and leaves an empty string when the buffer is smaller than
// ALEXA_MESSAGE_ID_SIZE.
bool formatMessageId(const uint32_t random[4], char *buffer, size_t size);
// The endpoint id in "<root>/<endpointId>/alexaDirective": a pointer into the topic and the id's length.
// nullptr for any other topic, among them <root>/discover and the <root>/<endpointId>/alexaDirective_e token
// topic that 1.x boards use.

View file

@ -1,6 +1,6 @@
// Lets the sources that have no hardware dependency compile on a host (pio test -e native), where the
// program-memory helpers of the Arduino cores do not exist: there a PROGMEM object is an ordinary constant and a
// pointer to one an ordinary pointer. On a board this is Arduino.h and nothing else.
// pointer to one an ordinary pointer. On a board this is Arduino.h and the random source of the hardware.
#ifndef ALEXA_COMPAT_H
#define ALEXA_COMPAT_H
@ -9,6 +9,7 @@
#else
#include <stdint.h>
#include <string.h>
#include <random>
#define PROGMEM
#define PGM_P const char *
#define PSTR(text) (text)
@ -20,4 +21,17 @@
#define strncpy_P strncpy
#endif
// 32 bits that cannot be predicted and differ after every reset
inline uint32_t alexaRandom32()
{
#if defined(ESP8266)
return RANDOM_REG32; // fed by the noise of the radio
#elif defined(ESP32)
return esp_random(); // ESP32: untested
#else
static std::random_device source;
return source();
#endif
}
#endif // ALEXA_COMPAT_H

View file

@ -7,6 +7,10 @@ static const char ERROR_INVALID_DIRECTIVE[] PROGMEM = "INVALID_DIRECTIVE";
static const char ERROR_NO_HANDLER[] PROGMEM = "The endpoint has no handler for directives";
static const char ERROR_NO_CAPABILITY[] PROGMEM = "The endpoint does not have this capability";
static const char EVENT_ACTIVATION_STARTED[] PROGMEM = "ActivationStarted";
static const char EVENT_DEACTIVATION_STARTED[] PROGMEM = "DeactivationStarted";
static const char EVENT_DOORBELL_PRESS[] PROGMEM = "DoorbellPress";
AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport)
: name(name), endpointId(endpointId), rootTopic(rootTopic), transport(transport) {
// Initialize event arrays to nullptr
@ -260,8 +264,65 @@ void AlexaDevice::handleDirective(const JsonDocument& message) {
buildStatusMessage(directive.correlationToken, true).asErrorResponse(ERROR_INVALID_DIRECTIVE, reason).send();
}
AlexaStatusMessage AlexaDirective::deferred(unsigned int estimatedSeconds) const {
AlexaStatusMessage message = device->buildStatusMessage(correlationToken, true);
message.toDeferredResponse(estimatedSeconds);
return message;
}
AlexaStatusMessage AlexaDirective::error(AlexaErrorType type, const char* message) const {
AlexaStatusMessage answer = device->buildStatusMessage(correlationToken, true);
answer.asErrorResponse(type, message);
return answer;
}
AlexaStatusMessage AlexaDirective::sceneStarted() const {
return device->sceneEvent(correlationToken, EVENT_ACTIVATION_STARTED);
}
AlexaStatusMessage AlexaDirective::sceneStopped() const {
return device->sceneEvent(correlationToken, EVENT_DEACTIVATION_STARTED);
}
AlexaStatusMessage AlexaDevice::sceneEvent(const char* correlationToken, PGM_P name) {
AlexaStatusMessage message = buildStatusMessage(correlationToken, true);
message.toEvent(AlexaMessageKind::SCENE_EVENT, AlexaInterfaces::SceneController.ns, name);
message.setCause(AlexaCause::VOICE_INTERACTION); // a directive comes from Alexa
return message;
}
AlexaStatusMessage AlexaDevice::changeReport(AlexaCause cause) {
AlexaStatusMessage message = buildStatusMessage("", true);
message.toChangeReport(cause);
return message;
}
AlexaStatusMessage AlexaDevice::doorbellPress(AlexaCause cause) {
AlexaStatusMessage message = event(AlexaInterfaces::DoorbellEventSource, EVENT_DOORBELL_PRESS);
message.setCause(cause);
return message;
}
AlexaStatusMessage AlexaDevice::event(const AlexaInterfaceDesc& row, const char* name) {
bool announced = false;
for (uint8_t i = 0; i < capabilityCount; ++i) {
announced = announced || &capabilities[i]->getRow() == &row;
}
if (!announced) {
ALEX2ESP_LOGE("%s: event of an interface the device does not have, Alexa will drop it: add the capability with addCapability()",
endpointId.c_str());
}
AlexaStatusMessage message = buildStatusMessage("", true);
message.toEvent(AlexaMessageKind::EVENT, row.ns, name);
return message;
}
AlexaSendResult AlexaDevice::publish(const char* topic, JsonDocument& doc) {
answered = true;
// A change report or an event that the handler sends is not the answer to its directive: an answer repeats
// the correlationToken
if (!doc["event"]["header"]["correlationToken"].isNull()) {
answered = true;
}
return transport->publish(topic, doc);
}

View file

@ -43,6 +43,20 @@ struct AlexaDirective {
// device and send()
AlexaStatusMessage response() const;
AlexaStatusMessage stateReport() const;
// The answer to a directive that takes longer than the 7 s the backend waits, a lock that is still turning:
// send() it, keep a copy of the correlationToken, and when the device is done send the answer itself with
// device->response(token) ... sendAsync(). Alexa is told the estimate when it is not 0. Alexa accepts a
// deferred answer for LockController and WakeOnLANController only.
AlexaStatusMessage deferred(unsigned int estimatedSeconds = 0) const;
// The answer to a directive that was not carried out: send() it. The message is for the log of the skill.
// What a type carries beside it goes into payload(), the validRange of VALUE_OUT_OF_RANGE.
AlexaStatusMessage error(AlexaErrorType type, const char* message) const;
// What a scene answers Activate and Deactivate with, in place of a Response: send() it
AlexaStatusMessage sceneStarted() const;
AlexaStatusMessage sceneStopped() const;
};
// One function can serve several devices: the directive says which one it is for
@ -136,9 +150,32 @@ public:
return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse,transport != nullptr ? this : nullptr);
}
// The same by name, for the answer that is sent outside of the handler: after a DeferredResponse, with the
// correlationToken the sketch has kept and sendAsync()
AlexaStatusMessage response(const String& correlationToken) { return buildStatusMessage(correlationToken, true); }
AlexaStatusMessage stateReport(const String& correlationToken) { return buildStatusMessage(correlationToken, false); }
// Tells Alexa that properties changed, whatever changed them; Alexa expects it within 3 s, also after a
// directive, beside the Response. For the properties of a capability with setProactivelyReported(true):
// device->changeReport(AlexaCause::PHYSICAL_INTERACTION).addPowerControllerProp(PowerController::ON)
// .context().addHealthProp(EndpointHealth::OK).send()
// The properties before context() are those that changed, at least one; the ones after it are the others.
AlexaStatusMessage changeReport(AlexaCause cause);
// The button of a doorbell was pressed: send() it. The device has the capability DoorbellEventSource.
AlexaStatusMessage doorbellPress(AlexaCause cause = AlexaCause::PHYSICAL_INTERACTION);
// An event of another interface, by the row of the interface and the name of the event. Its payload is
// empty: what the page of the interface asks for goes into payload().
AlexaStatusMessage event(const AlexaInterfaceDesc& row, const char* name);
private:
friend class AlexaDeviceList;
friend struct AlexaDirective;
AlexaStatusMessage sceneEvent(const char* correlationToken, PGM_P name);
// AlexaTransport, for the messages of this device: notes the answer and passes it on to the bridge
AlexaSendResult publish(const char* topic, JsonDocument& doc) override;
void timestamp(char* buffer, size_t size) override;

View file

@ -1,14 +1,43 @@
#include "AlexaStatusMessage.h"
#include "AlexaBridgeLogic.h"
#include "AlexaLog.h"
#include <stdlib.h>
#include <time.h>
// What a 1.x sketch wrote where the time belongs when it built a property by hand: the backend's HTTP route
// replaced it. Reports leave over MQTT now, where nothing rewrites them, so the library fills the time in.
static const char TIME_PLACEHOLDER[] PROGMEM = "{REPLACE_WITH_DATETIME}";
static const char NS_THERMOSTAT_ERROR[] PROGMEM = "Alexa.ThermostatController";
// The names of the error types in the order of the enum: both are made of ALEXA_ERROR_TYPES. The table is linked
// into a sketch that sends an error by its AlexaErrorType, about 1 KB of flash.
#define ALEXA_ERROR_TEXT(name) static const char ERROR_TYPE_##name[] PROGMEM = #name;
ALEXA_ERROR_TYPES(ALEXA_ERROR_TEXT)
#define ALEXA_ERROR_ENTRY(name) ERROR_TYPE_##name,
static const char *const ERROR_TYPE_NAMES[] PROGMEM = {ALEXA_ERROR_TYPES(ALEXA_ERROR_ENTRY)};
static PGM_P causeName(AlexaCause cause)
{
switch (cause)
{
case AlexaCause::APP_INTERACTION:
return PSTR("APP_INTERACTION");
case AlexaCause::PERIODIC_POLL:
return PSTR("PERIODIC_POLL");
case AlexaCause::VOICE_INTERACTION:
return PSTR("VOICE_INTERACTION");
case AlexaCause::PHYSICAL_INTERACTION:
break;
}
return PSTR("PHYSICAL_INTERACTION");
}
AlexaStatusMessage::AlexaStatusMessage(const String &correlationToken, const String &rootTopic, const String &endpointId, const bool isResponse, AlexaTransport *transport)
: rootTopic(rootTopic), endpointId(endpointId), transport(transport)
: rootTopic(rootTopic),
endpointId(endpointId),
transport(transport),
kind(isResponse ? AlexaMessageKind::RESPONSE : AlexaMessageKind::STATE_REPORT),
changing(false)
{
JsonObject event = doc["event"].to<JsonObject>();
@ -23,20 +52,61 @@ AlexaStatusMessage::AlexaStatusMessage(const String &correlationToken, const Str
event_header["name"] = "StateReport";
}
event_header["payloadVersion"] = "3";
event_header["messageId"] = generateMessageId();
// Alexa takes a message with the id of an earlier one for its repeat. The id is made of the random source of
// the hardware, which does not begin with the same numbers after every reset.
const uint32_t random[4] = {alexaRandom32(), alexaRandom32(), alexaRandom32(), alexaRandom32()};
char messageId[ALEXA_MESSAGE_ID_SIZE];
AlexaBridgeLogic::formatMessageId(random, messageId, sizeof(messageId));
event_header["messageId"] = messageId;
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>();
doc["context"]["properties"].to<JsonArray>();
}
AlexaStatusMessage &AlexaStatusMessage::AddContextProp(const JsonObject &property)
JsonArray AlexaStatusMessage::properties()
{
if (contextProperties.add(property))
if (changing)
{
JsonObject added = contextProperties[contextProperties.size() - 1];
return doc["event"]["payload"]["change"]["properties"].as<JsonArray>();
}
// A kind without properties has no list: what is added to it goes nowhere
return doc["context"]["properties"].as<JsonArray>();
}
JsonObject AlexaStatusMessage::payload()
{
return doc["event"]["payload"].as<JsonObject>();
}
AlexaStatusMessage &AlexaStatusMessage::addTemperatureSensorProp(float value, TemperatureSensorScale scale, unsigned int uncertaintyInMs)
{
JsonDocument temperature;
temperature["value"] = value;
if (scale == TemperatureSensorScale::FAHRENHEIT)
{
temperature["scale"] = F("FAHRENHEIT");
}
else if (scale == TemperatureSensorScale::KELVIN)
{
temperature["scale"] = F("KELVIN");
}
else
{
temperature["scale"] = F("CELSIUS");
}
return AddProperty(AlexaInterfaces::TemperatureSensor, temperature.as<JsonObject>(), uncertaintyInMs);
}
AlexaStatusMessage &AlexaStatusMessage::addContextProp(JsonObjectConst property)
{
JsonArray list = properties();
if (list.add(property))
{
JsonObject added = list[list.size() - 1];
const char *timeOfSample = added["timeOfSample"] | "";
if (timeOfSample[0] == '\0' || strcmp_P(timeOfSample, TIME_PLACEHOLDER) == 0)
{
@ -46,18 +116,109 @@ AlexaStatusMessage &AlexaStatusMessage::AddContextProp(const JsonObject &propert
return *this; // a property that did not fit leaves the document marked as overflowed: send() refuses it
}
AlexaStatusMessage &AlexaStatusMessage::asErrorResponse(AlexaErrorType type, PGM_P message)
{
size_t place = static_cast<size_t>(type);
if (place >= sizeof(ERROR_TYPE_NAMES) / sizeof(ERROR_TYPE_NAMES[0]))
{
place = static_cast<size_t>(AlexaErrorType::INTERNAL_ERROR);
}
asErrorResponse(static_cast<PGM_P>(pgm_read_ptr(&ERROR_TYPE_NAMES[place])), message);
if (place >= static_cast<size_t>(AlexaErrorType::REQUESTED_SETPOINTS_TOO_CLOSE))
{
doc["event"]["header"]["namespace"] = FPSTR(NS_THERMOSTAT_ERROR);
}
return *this;
}
AlexaStatusMessage &AlexaStatusMessage::asErrorResponse(PGM_P type, PGM_P message)
{
kind = AlexaMessageKind::ERROR_RESPONSE;
changing = false;
JsonObject event = doc["event"];
event["header"]["name"] = F("ErrorResponse");
event["payload"][F("type")] = FPSTR(type);
event["payload"][F("message")] = FPSTR(message);
doc.remove("context");
contextProperties = JsonArray(); // the array went with the context: what is added now goes nowhere
return *this;
}
void AlexaStatusMessage::toDeferredResponse(unsigned int estimatedSeconds)
{
kind = AlexaMessageKind::DEFERRED_RESPONSE;
JsonObject event = doc["event"];
event["header"]["name"] = F("DeferredResponse");
if (estimatedSeconds > 0)
{
event["payload"][F("estimatedDeferralInSeconds")] = estimatedSeconds;
}
doc.remove("context");
}
void AlexaStatusMessage::toChangeReport(AlexaCause cause)
{
kind = AlexaMessageKind::CHANGE_REPORT;
changing = true;
JsonObject event = doc["event"];
JsonObject header = event["header"];
header["name"] = F("ChangeReport");
header.remove("correlationToken"); // no directive asked for the report
JsonObject change = event["payload"][F("change")].to<JsonObject>();
change[F("cause")][F("type")] = FPSTR(causeName(cause));
change[F("properties")].to<JsonArray>();
}
void AlexaStatusMessage::toEvent(AlexaMessageKind eventKind, PGM_P ns, PGM_P name)
{
kind = eventKind;
JsonObject header = doc["event"]["header"];
header["namespace"] = FPSTR(ns);
header["name"] = FPSTR(name);
if (kind == AlexaMessageKind::EVENT)
{
header.remove("correlationToken");
}
doc["context"].to<JsonObject>(); // the pages of the interfaces show their events with an empty context
}
void AlexaStatusMessage::setCause(AlexaCause cause)
{
JsonObject event_payload = payload();
event_payload[F("cause")][F("type")] = FPSTR(causeName(cause));
char now[ALEXA_TIMESTAMP_SIZE] = "";
if (transport != nullptr)
{
transport->timestamp(now, sizeof(now));
}
event_payload[F("timestamp")] = now;
}
bool AlexaStatusMessage::send()
{
switch (kind)
{
case AlexaMessageKind::CHANGE_REPORT:
return publishOn(PSTR("/changeReport"), false);
case AlexaMessageKind::EVENT:
return publishOn(PSTR("/event"), false);
default:
return publishOn(PSTR("/alexaResponce"), true);
}
}
bool AlexaStatusMessage::sendAsync()
{
if (kind != AlexaMessageKind::RESPONSE && kind != AlexaMessageKind::STATE_REPORT && kind != AlexaMessageKind::ERROR_RESPONSE)
{
ALEX2ESP_LOGE("message for %s not sent: sendAsync() is for the Response, StateReport or ErrorResponse that follows a DeferredResponse, use send()", endpointId.c_str());
doc.clear();
return false;
}
return publishOn(PSTR("/deferredResponse"), true);
}
bool AlexaStatusMessage::publishOn(PGM_P topicEnd, bool ofEndpoint)
{
if (transport == nullptr)
{
@ -65,32 +226,25 @@ bool AlexaStatusMessage::send()
doc.clear();
return false;
}
if (kind == AlexaMessageKind::CHANGE_REPORT && doc["event"]["payload"]["change"]["properties"].size() == 0)
{
ALEX2ESP_LOGE("change report for %s not sent: it has no property that changed, add one before context()", endpointId.c_str());
doc.clear();
return false;
}
String topic = rootTopic + "/" + endpointId + "/alexaResponce";
String topic = rootTopic;
if (ofEndpoint)
{
topic += "/";
topic += endpointId;
}
topic += FPSTR(topicEnd);
AlexaSendResult result = transport->publish(topic.c_str(), doc);
doc.clear();
return result == AlexaSendResult::OK;
}
// TODO: make real uuid4 gen function
String AlexaStatusMessage::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);
}
void AlexaStatusMessage::setTimeOfSample(JsonObject property)
{
// A char array is copied into the document; the buffer does not have to outlive this call

View file

@ -1,3 +1,15 @@
// What a device tells Alexa: the answer to a directive, a change of its state, an event. One class builds them
// all; the kind of a message decides what it carries and the topic it is published on.
//
// kind from topic
// Response d.response(), device->response(token) <root>/<endpointId>/alexaResponce
// StateReport d.stateReport() <root>/<endpointId>/alexaResponce
// ErrorResponse d.error(type, message) <root>/<endpointId>/alexaResponce
// DeferredResponse d.deferred(seconds) <root>/<endpointId>/alexaResponce
// the answer after it device->response(token) ... sendAsync() <root>/<endpointId>/deferredResponse
// scene event d.sceneStarted(), d.sceneStopped() <root>/<endpointId>/alexaResponce
// ChangeReport device->changeReport(cause) <root>/changeReport
// event device->doorbellPress(), device->event() <root>/event
#ifndef ALEXA_STATUS_MESSAGE_H
#define ALEXA_STATUS_MESSAGE_H
@ -20,83 +32,186 @@ enum class PowerController
enum class TemperatureSensorScale
{
CELSIUS,
FAHRENHEIT
FAHRENHEIT,
KELVIN
};
enum class AlexaMessageKind : uint8_t
{
RESPONSE, // the answer to a directive that was carried out
STATE_REPORT, // the answer to ReportState
ERROR_RESPONSE, // the answer to a directive that was not carried out
DEFERRED_RESPONSE, // the answer follows later, with sendAsync()
SCENE_EVENT, // ActivationStarted or DeactivationStarted: what a scene answers with
CHANGE_REPORT, // properties changed without a directive, or after one
EVENT // what a device reports unasked, DoorbellPress
};
// Why a property changed or an event happened (the cause object of alexa-changereport.html)
enum class AlexaCause : uint8_t
{
APP_INTERACTION, // through the app of the device
PERIODIC_POLL, // the device was asked for its state and it had changed
PHYSICAL_INTERACTION, // somebody used the device itself
VOICE_INTERACTION // through Alexa, by voice or in the Alexa app
};
// The types of alexa-errorresponse.html, and after them those of Alexa.ThermostatController, which are sent in
// the namespace of that interface (alexa-thermostatcontroller-errorresponse.html)
#define ALEXA_ERROR_TYPES(X) \
X(ALREADY_IN_OPERATION) X(BRIDGE_UNREACHABLE) X(CLOUD_CONTROL_DISABLED) X(DEVICE_STUCK) X(DO_NOT_DISTURB_MODE) \
X(ENDPOINT_BUSY) X(ENDPOINT_CONTROL_UNAVAILABLE) X(ENDPOINT_LOW_POWER) X(ENDPOINT_UNREACHABLE) \
X(EXPIRED_AUTHORIZATION_CREDENTIAL) X(FIRMWARE_OUT_OF_DATE) X(HARDWARE_MALFUNCTION) \
X(INSUFFICIENT_PERMISSIONS) X(INSUFFICIENT_RESOURCE) X(INTERNAL_ERROR) X(INVALID_AUTHORIZATION_CREDENTIAL) \
X(INVALID_DIRECTIVE) X(INVALID_VALUE) X(MAINTENANCE_REQUIRED) X(NO_SUCH_ENDPOINT) X(NOT_CALIBRATED) \
X(NOT_IN_OPERATION) X(NOT_SUPPORTED_IN_CURRENT_MODE) X(NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE) \
X(PARTNER_APPLICATION_REDIRECTION) X(POWER_LEVEL_NOT_SUPPORTED) X(RATE_LIMIT_EXCEEDED) \
X(TEMPERATURE_VALUE_OUT_OF_RANGE) X(TOO_MANY_FAILED_ATTEMPTS) X(UNABLE_TO_CHARGE) X(VALUE_OUT_OF_RANGE) \
X(REQUESTED_SETPOINTS_TOO_CLOSE) X(THERMOSTAT_IS_OFF) X(UNSUPPORTED_THERMOSTAT_MODE) \
X(DUAL_SETPOINTS_UNSUPPORTED) X(TRIPLE_SETPOINTS_UNSUPPORTED) X(UNWILLING_TO_SET_SCHEDULE) \
X(UNWILLING_TO_SET_VALUE)
enum class AlexaErrorType : uint8_t
{
#define ALEXA_ERROR_ENUMERATOR(name) name,
ALEXA_ERROR_TYPES(ALEXA_ERROR_ENUMERATOR)
#undef ALEXA_ERROR_ENUMERATOR
};
class AlexaStatusMessage
{
public:
// Built by AlexaDevice::buildStatusMessage(), which passes the bridge as the transport: it publishes the report
// and supplies the time of every property. A message without a transport cannot be sent.
// A Response (isResponse) or a StateReport. Built by AlexaDevice::buildStatusMessage(), which passes the
// bridge as the transport: it publishes the message and supplies the time of every property. A message
// without a transport cannot be sent.
AlexaStatusMessage(const String &correlationToken, const String &rootTopic, const String &endpointId, const bool isResponse, AlexaTransport *transport = nullptr);
AlexaStatusMessage &AddHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0)
AlexaMessageKind getKind() const { return kind; }
// The properties of the device. In a ChangeReport the properties added before context() are those that
// changed and the ones after it the others; every other kind has one list. An ErrorResponse, a
// DeferredResponse and an event carry no properties: what is added to them is not sent.
AlexaStatusMessage &addHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0)
{
JsonDocument healthValue;
healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE";
return AddProperty(AlexaInterfaces::EndpointHealth, healthValue.as<JsonObject>(), uncertaintyInMs);
}
AlexaStatusMessage &AddPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
AlexaStatusMessage &addPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaces::PowerController, 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(AlexaInterfaces::TemperatureSensor, tempValue.as<JsonObject>(), uncertaintyInMs);
}
// The temperature is reported in the scale it is given in
AlexaStatusMessage &addTemperatureSensorProp(float value, TemperatureSensorScale scale = TemperatureSensorScale::CELSIUS, unsigned int uncertaintyInMs = 0);
AlexaStatusMessage &AddBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
AlexaStatusMessage &addBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaces::BrightnessController, brightness, uncertaintyInMs);
}
AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
AlexaStatusMessage &addColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaces::ColorTemperatureController, colorTemperature, uncertaintyInMs);
}
AlexaStatusMessage &AddToggleControllerProp(PowerController powerController,String instanceName, unsigned int uncertaintyInMs = 0)
AlexaStatusMessage &addToggleControllerProp(const String &instance, PowerController toggleState, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaces::ToggleController, value, uncertaintyInMs,instanceName);
String value = (toggleState == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaces::ToggleController, value, uncertaintyInMs, instance);
}
// Adds a property the sketch built itself. Its timeOfSample is set to the current time when the object has
// none or still carries the 1.x placeholder "{REPLACE_WITH_DATETIME}".
AlexaStatusMessage &AddContextProp(const JsonObject &property);
AlexaStatusMessage &addContextProp(JsonObjectConst property);
// The names of 1.x. Until 1.1.0 AddTemperatureSensorProp() converted Fahrenheit to Celsius.
AlexaStatusMessage &AddHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0)
{
return addHealthProp(endpointHealth, uncertaintyInMs);
}
AlexaStatusMessage &AddPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
{
return addPowerControllerProp(powerController, uncertaintyInMs);
}
AlexaStatusMessage &AddTemperatureSensorProp(TemperatureSensorScale tempSensor, float value, unsigned int uncertaintyInMs = 0)
{
return addTemperatureSensorProp(value, tempSensor, uncertaintyInMs);
}
AlexaStatusMessage &AddBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
{
return addBrightnessControllerProp(brightness, uncertaintyInMs);
}
AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
{
return addColorTemperatureControllerProp(colorTemperature, uncertaintyInMs);
}
AlexaStatusMessage &AddToggleControllerProp(PowerController powerController, String instanceName, unsigned int uncertaintyInMs = 0)
{
return addToggleControllerProp(instanceName, powerController, uncertaintyInMs);
}
AlexaStatusMessage &AddContextProp(const JsonObject &property) { return addContextProp(property); }
// In a ChangeReport: the properties that changed have been added, the ones that follow are the others
AlexaStatusMessage &context()
{
changing = false;
return *this;
}
// The payload of the event, for what a kind carries beside the properties:
// d.error(AlexaErrorType::VALUE_OUT_OF_RANGE, "0 to 100").payload()["validRange"]["maximumValue"] = 100
JsonObject payload();
// Turns the message into the ErrorResponse of alexa-errorresponse.html, which has no context: properties
// added before or after are not sent. The type is one of that page, PSTR("INVALID_DIRECTIVE"); the message
// is for the log of the skill, Alexa does not read it to the user. Both may be in program memory.
// added before or after are not sent. The message is for the log of the skill, Alexa does not read it to the
// user; it may be in program memory. A type of Alexa.ThermostatController is sent in that namespace.
AlexaStatusMessage &asErrorResponse(AlexaErrorType type, PGM_P message);
// The same with the type as text, PSTR("INVALID_DIRECTIVE"), in the namespace Alexa
AlexaStatusMessage &asErrorResponse(PGM_P type, PGM_P message);
// Publishes the report on <root>/<endpointId>/alexaResponce now. Returns false when nothing was sent: no
// session with the broker, the MQTT client or the heap cannot take the report, or it is over
// ALEX2ESP_MAX_MESSAGE bytes. The reason is on Serial; a report is never sent truncated.
// Publishes the message now, on the topic of its kind. Returns false when nothing was sent: no session with
// the broker, the MQTT client or the heap cannot take the message, it is over ALEX2ESP_MAX_MESSAGE bytes, or
// it is a ChangeReport without a property that changed. The reason is on Serial; a message is never sent
// truncated. A message is sent once: it is empty afterwards.
bool send();
// Publishes the answer that follows a DeferredResponse, on <root>/<endpointId>/deferredResponse: a Response,
// a StateReport or an ErrorResponse with the correlationToken of the directive, which the sketch has kept.
// Returns false as send() does, and for any other kind.
bool sendAsync();
private:
friend class AlexaDevice;
friend struct AlexaDirective;
String rootTopic;
String endpointId;
AlexaTransport *transport;
JsonDocument doc;
JsonArray contextProperties;
AlexaMessageKind kind;
bool changing; // a ChangeReport before context(): what is added has changed
String generateMessageId();
// What the device and the directive make of a Response
void toDeferredResponse(unsigned int estimatedSeconds);
void toChangeReport(AlexaCause cause);
void toEvent(AlexaMessageKind eventKind, PGM_P ns, PGM_P name);
void setCause(AlexaCause cause); // with the time, as the payload of a scene and of a doorbell has it
// The list that takes the next property. Looked up for every property: a message is returned by value, and
// a reference into the document that is kept in the message would not survive the move.
JsonArray properties();
void setTimeOfSample(JsonObject property);
bool publishOn(PGM_P topicEnd, bool ofEndpoint);
// Adds the first property of the row: every interface with a helper above reports one
template <typename T>
AlexaStatusMessage &AddProperty(const AlexaInterfaceDesc &row, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="")
{
JsonObject prop = contextProperties.add<JsonObject>();
JsonObject prop = properties().add<JsonObject>();
prop["namespace"] = FPSTR(row.ns);
prop["name"] = FPSTR(row.property(0));