AlexaCapability: instance, asset and text names, configuration, action and state mappings, nonControllable

AlexaCapability replaces the header-only AlexaInterface as what a device holds: 116 bytes with fixed places for
3 names, 4 action mappings and 2 state mappings, no std::vector or std::string. AlexaInterface and AlexaActions
stay as 1.x names; the five examples compile unchanged and announce the same bytes.
addCapability(row, instance) adds several capabilities of one generic controller. An instanced interface without
an instance or a name, an instance or semantics on an interface that takes none, and a full store are refused
with an ERROR line that says what to change.
PlaybackController and WakeOnLANController announce "properties": {} (AIF_EMPTY_PROPERTIES).
basicLight on a D1 mini: static RAM 30,788 -> 30,672 B, flash 330,509 -> 332,541 B; host tests 61 -> 77.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 19:33:57 +00:00
parent 8b11cc2394
commit 7afc4346e8
12 changed files with 1084 additions and 253 deletions

View file

@ -1,202 +1,17 @@
// The names of library 1.x for what 2.0 calls AlexaCapability and AlexaAction. They stay through 2.x, so that a
// sketch written for 1.x compiles as it is; new sketches use the 2.0 names.
#ifndef ALEXA_INTERFACE_H
#define ALEXA_INTERFACE_H
#include <string>
#include <vector>
#include <Arduino.h>
#include <ArduinoJson.h>
#include "AlexaInterfaces.h"
#include "AlexaLog.h"
enum class AlexaActions {
Open,
Close,
Raise,
Lower,
SetEcoOn,
SetEcoOff
};
#include "AlexaCapability.h"
typedef AlexaCapability AlexaInterface;
typedef AlexaAction AlexaActions;
class AlexaActionsUtils
{
public:
// Helper function to convert AlexaActions enum to string
static String toString(AlexaActions action) {
switch (action) {
case AlexaActions::Open: return "Alexa.Actions.Open";
case AlexaActions::Close: return "Alexa.Actions.Close";
case AlexaActions::Raise: return "Alexa.Actions.Raise";
case AlexaActions::Lower: return "Alexa.Actions.Lower";
case AlexaActions::SetEcoOn: return "Alexa.Actions.SetEcoOn";
case AlexaActions::SetEcoOff: return "Alexa.Actions.SetEcoOff";
default: return "Unknown";
}
}
};
class FriendlyName {
public:
FriendlyName(const std::string& t, const std::string& l)
: text(t), locale(l) {}
std::string text;
std::string locale;
};
// Updated ActionMapping class definition
class ActionMapping {
public:
String type = "ActionsToDirective"; // Type of action mapping (always "ActionsToDirective")
std::vector<AlexaActions> actions; // Array of AlexaActions
struct Directive {
String name; // Name of the directive
String payload; // JSON text of the directive's payload object, empty when there is none
} directive; // Directive object containing name and payload
// Constructor
ActionMapping(std::vector<AlexaActions> actions, String directiveName, String directivePayload="")
: actions(actions) {
directive.name = directiveName;
directive.payload = directivePayload;
}
// Get the JSON representation of ActionMapping
JsonDocument getJSON() const {
JsonDocument doc;
doc["@type"] = type;
// Convert actions array to strings
JsonArray actionArray = doc["actions"].to<JsonArray>();
for (const auto& action : actions) {
actionArray.add(AlexaActionsUtils::toString(action));
}
JsonObject directiveObj = doc["directive"].to<JsonObject>();
directiveObj["name"] = directive.name;
if (directive.payload.length() > 0) {
// Alexa expects the payload as an object, not as a string holding JSON: parse the text we were given
JsonDocument payloadDoc;
if (deserializeJson(payloadDoc, directive.payload) == DeserializationError::Ok && payloadDoc.is<JsonObject>()) {
directiveObj["payload"] = payloadDoc.as<JsonObject>();
} else {
ALEX2ESP_LOGE("action mapping payload is not a JSON object, omitted: %s", directive.payload.c_str());
}
}
return doc;
}
};
// A capability of a device: a row of AlexaInterfaces and what this device adds to it
class AlexaInterface
{
private:
const AlexaInterfaceDesc *row; // in program memory, never nullptr
bool retrievable;
bool proactivelyReported;
std::vector<ActionMapping> actionMappings; // Array of ActionMappings
std::vector<FriendlyName> friendlyNames; // Array of FriendlyNames
String instance;
bool nameSet = false;
public:
// Constructor
AlexaInterface(const AlexaInterfaceDesc &row, bool retrievable = true, bool proactivelyReported = false)
: row(&row), retrievable(retrievable), proactivelyReported(proactivelyReported) {}
// Getters
const AlexaInterfaceDesc &getRow() const { return *row; }
AlexaInterfaceType getType() const { return row->interfaceType(); }
String getTypeString() const { return String(FPSTR(row->ns)); }
String getVersion() const { return String(FPSTR(row->version)); }
std::vector<String> getProps() const
{
std::vector<String> props;
for (uint8_t i = 0; i < row->propertyCount(); i++)
{
props.push_back(String(FPSTR(row->property(i))));
}
return props;
}
void setInstance(const char* name){
instance=name;
nameSet=true;
}
void addFriendlyName(const char* name,const char* locale){
friendlyNames.emplace_back(name,locale);
}
bool isRetrievable() const { return retrievable; }
bool isProactivelyReported() const { return proactivelyReported; }
// Setters
void setRetrievable(bool value) { retrievable = value; }
void setProactivelyReported(bool value) { proactivelyReported = value; }
void addActionMapping(const ActionMapping& actionMapping) {
actionMappings.push_back(actionMapping);
}
JsonDocument getJSON() const
{
JsonDocument doc;
// Set base properties
doc["interface"] = FPSTR(row->ns);
doc["version"] = FPSTR(row->version);
doc["type"] = "AlexaInterface";
// Add properties, unless the interface has none to report (a scene, a doorbell)
if (!row->has(AIF_NO_PROPERTIES)) {
JsonObject properties = doc["properties"].to<JsonObject>();
properties["retrievable"] = retrievable;
properties["proactivelyReported"] = proactivelyReported;
JsonArray supported = properties["supported"].to<JsonArray>();
for (uint8_t i = 0; i < row->propertyCount(); i++)
{
supported.add<JsonObject>()["name"] = FPSTR(row->property(i));
}
}
// Add action mappings - semantics only exist when there is something to map (as alex2node does)
if (!actionMappings.empty()) {
JsonObject semantics = doc["semantics"].to<JsonObject>();
JsonArray actionMappingArray = semantics["actionMappings"].to<JsonArray>();
for (const ActionMapping& actionMapping : actionMappings) {
actionMappingArray.add(actionMapping.getJSON().as<JsonObject>());
}
}
if(nameSet){
doc["instance"]=instance;
}
// Add capabilityResources if friendlyNames is not empty
if (!friendlyNames.empty()) {
JsonObject capabilityResources = doc["capabilityResources"].to<JsonObject>();
JsonArray friendlyNamesArray = capabilityResources["friendlyNames"].to<JsonArray>();
for (const FriendlyName& fn : friendlyNames) {
JsonObject friendlyNameObj = friendlyNamesArray.add<JsonObject>();
friendlyNameObj["@type"] = "text";
JsonObject valueObj = friendlyNameObj["value"].to<JsonObject>();
valueObj["text"] = fn.text;
valueObj["locale"] = fn.locale;
}
}
doc.shrinkToFit();
return doc;
}
static String toString(AlexaActions action) { return String(FPSTR(alexaActionName(action))); }
};
#endif // ALEXA_INTERFACE_H