Alex2ESP/src/AlexaCapability.h
David c3d02f462e Announce a doorbell with proactivelyReported and a scene with supportsDeactivation
An interface without properties says beside its name what Alexa has to know
of it; the library left both out. With a board and Alexa on 2026-09-28 the
discovery of a doorbell was accepted (202), the endpoint was not listed, and
its DoorbellPress was answered with 500 INTERNAL_SERVICE_EXCEPTION.

DoorbellEventSource now always carries "proactivelyReported": true.
SceneController carries "supportsDeactivation", false until the sketch calls
setSupportsDeactivation(true); on another interface the call is refused and
logged. The Scene example says that it can be undone.

148 host tests. Light: static RAM 30,616 B, flash 332,705 B (+140 B).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 22:20:34 +00:00

211 lines
8.7 KiB
C++

// 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 <type_traits>
#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). EcoOn and
// EcoOff are for a ToggleController; Low, Empty, Full, Done and Stuck are what Alexa announces a device for.
enum class AlexaState : uint8_t
{
Open,
Closed,
EcoOn,
EcoOff,
Low,
Empty,
Full,
Done,
Stuck
};
// "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. The
// chain goes on after a call that was refused; isValid() is false from then on, and the device leaves the
// capability out of discovery, where Alexa would otherwise learn half of it.
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; }
// false when a setter has refused what it was given. The reason was printed when it happened.
bool isValid() const { return valid; }
// 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: AlexaAssets::Setting_Opening of AlexaResources.h,
// or 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);
// For a scene: true when it can be undone, and Alexa then sends Deactivate as well as Activate. A scene is
// announced with "supportsDeactivation": false until this is called.
AlexaCapability &setSupportsDeactivation(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);
// A number of another type, addStateMapping({AlexaState::Closed}, 0): without this a 0 is as much a text
// that is missing as it is a number, and the call does not compile
template <typename Number, typename = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
AlexaCapability &addStateMapping(std::initializer_list<AlexaState> states, Number value)
{
return addStateMapping(states, static_cast<float>(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. So does one that is not valid.
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
{
uint16_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);
AlexaCapability &refused();
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;
bool deactivation : 1;
bool valid : 1;
};
#endif // ALEXA_CAPABILITY_H