AlexaStatusMessage gets a helper for color, percentage, power level, range, mode, lock state, contact, motion, humidity and the thermostat (target, lower and upper setpoint, mode), and addProperty(row, place, value) for a property without one. Each is a function of its own on one shared builder; a value outside of the range of its property is reported as the nearest of the range with an error. AlexaResources.h holds the 103 asset ids of resources-and-assets.html (AlexaAssets, AlexaUnits); AlexaState has the seven states that were missing; addStateMapping() takes a number of any type. A setter that refuses its argument makes isValid() false, and the capability is left out of discovery with an error instead of being announced without what was refused. basicLight: static RAM 30,520 B (was 30,540), flash 333,389 B (was 334,981); 130 host tests (was 112). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
206 lines
8.4 KiB
C++
206 lines
8.4 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);
|
|
|
|
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 valid : 1;
|
|
};
|
|
|
|
#endif // ALEXA_CAPABILITY_H
|