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

181
src/AlexaCapability.h Normal file
View file

@ -0,0 +1,181 @@
// One capability of one device: a row of AlexaInterfaces and what this device adds to it. For most interfaces that
// is nothing. A generic controller (RangeController, ModeController, ToggleController) has an instance and the
// names Alexa calls it by, a configuration, and the words Alexa maps to its directives and states.
// alexa-discovery-objects.html, generic-controllers.html under
// https://developer.amazon.com/en-US/docs/alexa/device-apis/
#ifndef ALEXA_CAPABILITY_H
#define ALEXA_CAPABILITY_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include <initializer_list>
#include "AlexaInterfaces.h"
#include "AlexaLimits.h"
// What a user says: "open", "close", "raise", "lower" (alexa-discovery-objects.html#action-mapping)
enum class AlexaAction : uint8_t
{
Open,
Close,
Raise,
Lower,
SetEcoOn,
SetEcoOff
};
// What Alexa says about the device: "the blind is open" (alexa-discovery-objects.html#state-mapping)
enum class AlexaState : uint8_t
{
Open,
Closed
};
// "Alexa.Actions.Open", in program memory
PGM_P alexaActionName(AlexaAction action);
// "Alexa.States.Open", in program memory
PGM_P alexaStateName(AlexaState state);
// The directive Alexa sends for one or more actions. The name of the directive and its payload, the JSON text of
// an object, are kept as pointers and not copied: pass literals. Actions are announced in the order of AlexaAction.
class ActionMapping
{
public:
ActionMapping() {}
ActionMapping(std::initializer_list<AlexaAction> actions, const char *directiveName,
const char *directivePayload = nullptr)
: directiveName(directiveName), directivePayload(directivePayload)
{
for (AlexaAction action : actions)
{
this->actions |= static_cast<uint8_t>(1u << static_cast<uint8_t>(action));
}
}
uint8_t actions = 0; // one bit per AlexaAction
const char *directiveName = nullptr;
const char *directivePayload = nullptr; // nullptr or "" for a directive without payload
};
// Writes the "configuration" object of a capability when the device is announced; context is what the sketch
// passed to setConfiguration()
typedef void (*AlexaConfigurationFiller)(JsonObject configuration, void *context);
// "Alexa.RangeController Blind.Lift" for a log line. The namespace is in program memory, where the %s of the log
// cannot read it.
struct AlexaCapabilityName
{
AlexaCapabilityName(const AlexaInterfaceDesc &row, const char *instance);
char text[72];
};
// 116 bytes on the heap of an ESP8266, whatever the capability holds. Every setter returns the capability:
// device->addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
// ->addFriendlyName("Lift", "en-US").setConfiguration(fillLift).addStateMapping({AlexaState::Closed}, 0.0f);
// What a capability refuses is printed at ERROR with what to change, and leaves the capability as it was.
class AlexaCapability
{
public:
explicit AlexaCapability(const AlexaInterfaceDesc &row, const char *instance = nullptr);
~AlexaCapability();
// It owns copies of its instance and its names
AlexaCapability(const AlexaCapability &) = delete;
AlexaCapability &operator=(const AlexaCapability &) = delete;
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)); }
// "" when the capability has none
const char *getInstance() const { return instance != nullptr ? instance : ""; }
bool isRetrievable() const { return retrievable; }
bool isProactivelyReported() const { return proactivelyReported; }
bool isNonControllable() const { return nonControllable; }
// The instance tells the capabilities of one interface on a device apart ("Blind.Lift", "Blind.Tilt"). It is
// copied. addCapability(row, instance) sets it; this is for a capability that was added by its type.
AlexaCapability &setInstance(const char *instance);
// A name of the capability in words. The text is copied, the locale ("en-US") is not: pass a literal.
AlexaCapability &addFriendlyName(const char *text, const char *locale);
// A name from the catalog of Alexa, which Alexa translates: PSTR("Alexa.Setting.Opening"). Not copied.
AlexaCapability &addFriendlyAsset(PGM_P assetId);
AlexaCapability &setRetrievable(bool value);
AlexaCapability &setProactivelyReported(bool value);
// true: Alexa reports the state and sends no directive that changes it
AlexaCapability &setNonControllable(bool value);
AlexaCapability &setConfiguration(AlexaConfigurationFiller fill, void *context = nullptr);
// Only for an interface that takes semantics: RangeController, ModeController, ToggleController
AlexaCapability &addActionMapping(const ActionMapping &mapping);
// The states a value stands for: a number or a range of a RangeController, the text of a mode or "ON"/"OFF"
// of a ToggleController. A text is not copied: pass a literal.
AlexaCapability &addStateMapping(std::initializer_list<AlexaState> states, float value);
AlexaCapability &addStateMapping(std::initializer_list<AlexaState> states, float minimum, float maximum);
AlexaCapability &addStateMapping(std::initializer_list<AlexaState> states, const char *value);
// Whether a directive with this namespace and instance is for this capability. The instance counts only
// for an interface that has instances.
bool matches(const char *ns, const char *directiveInstance) const;
// Adds the discovery object of the capability to the capabilities of its endpoint. A capability that Alexa
// would reject the endpoint for, a generic controller without an instance or without a name, adds nothing,
// prints what is missing and returns false.
bool toJson(JsonArray capabilities, const char *endpointId) const;
private:
struct Name
{
const char *value; // the text, a copy on the heap, or the asset id in program memory
const char *locale; // nullptr for an asset
};
enum StateMappingKind : uint8_t
{
STATES_TO_VALUE,
STATES_TO_RANGE,
STATES_TO_TEXT
};
struct StateMapping
{
uint8_t states; // one bit per AlexaState
uint8_t kind; // StateMappingKind
union
{
float value; // the value, or the lowest of a range
const char *text;
};
float maximum;
};
StateMapping *newStateMapping(std::initializer_list<AlexaState> states, StateMappingKind kind);
void writeSemantics(JsonObject capability) const;
void writeNames(JsonObject capability) const;
const AlexaInterfaceDesc *row; // in program memory, never nullptr
char *instance = nullptr; // a copy on the heap
AlexaConfigurationFiller fillConfiguration = nullptr;
void *configurationContext = nullptr;
Name names[ALEX2ESP_MAX_FRIENDLY_NAMES];
ActionMapping actionMappings[ALEX2ESP_MAX_ACTION_MAPPINGS];
StateMapping stateMappings[ALEX2ESP_MAX_STATE_MAPPINGS];
uint8_t nameCount = 0;
uint8_t actionCount = 0;
uint8_t stateCount = 0;
bool retrievable : 1;
bool proactivelyReported : 1;
bool nonControllable : 1;
};
#endif // ALEXA_CAPABILITY_H