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:
parent
42af413a6e
commit
3215457f91
12 changed files with 1093 additions and 90 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue