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:
parent
8b11cc2394
commit
7afc4346e8
12 changed files with 1084 additions and 253 deletions
436
src/AlexaCapability.cpp
Normal file
436
src/AlexaCapability.cpp
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
#include "AlexaCapability.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "AlexaLog.h"
|
||||
|
||||
// The names are looked up by a value that is only known when the sketch runs, so both tables are always linked
|
||||
static const char ACTION_OPEN[] PROGMEM = "Alexa.Actions.Open";
|
||||
static const char ACTION_CLOSE[] PROGMEM = "Alexa.Actions.Close";
|
||||
static const char ACTION_RAISE[] PROGMEM = "Alexa.Actions.Raise";
|
||||
static const char ACTION_LOWER[] PROGMEM = "Alexa.Actions.Lower";
|
||||
static const char ACTION_SET_ECO_ON[] PROGMEM = "Alexa.Actions.SetEcoOn";
|
||||
static const char ACTION_SET_ECO_OFF[] PROGMEM = "Alexa.Actions.SetEcoOff";
|
||||
static const char *const ACTION_NAMES[] PROGMEM = {ACTION_OPEN, ACTION_CLOSE, ACTION_RAISE,
|
||||
ACTION_LOWER, ACTION_SET_ECO_ON, ACTION_SET_ECO_OFF};
|
||||
static const uint8_t ACTION_COUNT = sizeof(ACTION_NAMES) / sizeof(ACTION_NAMES[0]);
|
||||
|
||||
static const char STATE_OPEN[] PROGMEM = "Alexa.States.Open";
|
||||
static const char STATE_CLOSED[] PROGMEM = "Alexa.States.Closed";
|
||||
static const char *const STATE_NAMES[] PROGMEM = {STATE_OPEN, STATE_CLOSED};
|
||||
static const uint8_t STATE_COUNT = sizeof(STATE_NAMES) / sizeof(STATE_NAMES[0]);
|
||||
|
||||
static PGM_P nameAt(const char *const *names, uint8_t place)
|
||||
{
|
||||
return static_cast<PGM_P>(pgm_read_ptr(&names[place]));
|
||||
}
|
||||
|
||||
PGM_P alexaActionName(AlexaAction action)
|
||||
{
|
||||
const uint8_t place = static_cast<uint8_t>(action);
|
||||
return place < ACTION_COUNT ? nameAt(ACTION_NAMES, place) : PSTR("Unknown");
|
||||
}
|
||||
|
||||
PGM_P alexaStateName(AlexaState state)
|
||||
{
|
||||
const uint8_t place = static_cast<uint8_t>(state);
|
||||
return place < STATE_COUNT ? nameAt(STATE_NAMES, place) : PSTR("Unknown");
|
||||
}
|
||||
|
||||
// The names of the bits that are set, in the order of the table
|
||||
static void addNames(JsonArray target, uint8_t bits, const char *const *names, uint8_t count)
|
||||
{
|
||||
for (uint8_t place = 0; place < count; place++)
|
||||
{
|
||||
if ((bits & (1u << place)) != 0)
|
||||
{
|
||||
target.add(FPSTR(nameAt(names, place)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A copy on the heap; nullptr for no text, an empty text and a heap without room
|
||||
static char *copyText(const char *text)
|
||||
{
|
||||
if (text == nullptr || text[0] == '\0')
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const size_t size = strlen(text) + 1;
|
||||
char *copy = static_cast<char *>(malloc(size));
|
||||
if (copy != nullptr)
|
||||
{
|
||||
memcpy(copy, text, size);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
AlexaCapabilityName::AlexaCapabilityName(const AlexaInterfaceDesc &row, const char *instance)
|
||||
{
|
||||
strncpy_P(text, row.ns, sizeof(text) - 1);
|
||||
text[sizeof(text) - 1] = '\0';
|
||||
if (instance != nullptr && instance[0] != '\0')
|
||||
{
|
||||
strncat(text, " ", sizeof(text) - strlen(text) - 1);
|
||||
strncat(text, instance, sizeof(text) - strlen(text) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
AlexaCapability::AlexaCapability(const AlexaInterfaceDesc &row, const char *instance)
|
||||
: row(&row), retrievable(true), proactivelyReported(false), nonControllable(false)
|
||||
{
|
||||
setInstance(instance);
|
||||
}
|
||||
|
||||
AlexaCapability::~AlexaCapability()
|
||||
{
|
||||
free(instance);
|
||||
for (uint8_t i = 0; i < nameCount; i++)
|
||||
{
|
||||
if (names[i].locale != nullptr)
|
||||
{
|
||||
free(const_cast<char *>(names[i].value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AlexaCapability &AlexaCapability::setInstance(const char *newInstance)
|
||||
{
|
||||
free(instance);
|
||||
instance = copyText(newInstance);
|
||||
if (instance == nullptr && newInstance != nullptr && newInstance[0] != '\0')
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: instance not set: the heap has no room for it",
|
||||
AlexaCapabilityName(*row, nullptr).text);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
AlexaCapability &AlexaCapability::addFriendlyName(const char *text, const char *locale)
|
||||
{
|
||||
const AlexaCapabilityName name(*row, instance);
|
||||
if (text == nullptr || text[0] == '\0' || locale == nullptr || locale[0] == '\0')
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: friendly name not added: pass a text and a locale, addFriendlyName(\"Lift\", \"en-US\")",
|
||||
name.text);
|
||||
return *this;
|
||||
}
|
||||
if (nameCount >= ALEX2ESP_MAX_FRIENDLY_NAMES)
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: friendly name \"%s\" not added: it has %d already (ALEX2ESP_MAX_FRIENDLY_NAMES)",
|
||||
name.text, text, ALEX2ESP_MAX_FRIENDLY_NAMES);
|
||||
return *this;
|
||||
}
|
||||
const char *copy = copyText(text);
|
||||
if (copy == nullptr)
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: friendly name \"%s\" not added: the heap has no room for it", name.text, text);
|
||||
return *this;
|
||||
}
|
||||
names[nameCount++] = {copy, locale};
|
||||
return *this;
|
||||
}
|
||||
|
||||
AlexaCapability &AlexaCapability::addFriendlyAsset(PGM_P assetId)
|
||||
{
|
||||
const AlexaCapabilityName name(*row, instance);
|
||||
if (assetId == nullptr)
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: asset not added: pass its id, addFriendlyAsset(PSTR(\"Alexa.Setting.Opening\"))", name.text);
|
||||
return *this;
|
||||
}
|
||||
if (nameCount >= ALEX2ESP_MAX_FRIENDLY_NAMES)
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: asset not added: it has %d names already (ALEX2ESP_MAX_FRIENDLY_NAMES)",
|
||||
name.text, ALEX2ESP_MAX_FRIENDLY_NAMES);
|
||||
return *this;
|
||||
}
|
||||
names[nameCount++] = {assetId, nullptr};
|
||||
return *this;
|
||||
}
|
||||
|
||||
AlexaCapability &AlexaCapability::setRetrievable(bool value)
|
||||
{
|
||||
retrievable = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
AlexaCapability &AlexaCapability::setProactivelyReported(bool value)
|
||||
{
|
||||
proactivelyReported = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
AlexaCapability &AlexaCapability::setNonControllable(bool value)
|
||||
{
|
||||
nonControllable = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
AlexaCapability &AlexaCapability::setConfiguration(AlexaConfigurationFiller fill, void *context)
|
||||
{
|
||||
fillConfiguration = fill;
|
||||
configurationContext = context;
|
||||
return *this;
|
||||
}
|
||||
|
||||
AlexaCapability &AlexaCapability::addActionMapping(const ActionMapping &mapping)
|
||||
{
|
||||
const AlexaCapabilityName name(*row, instance);
|
||||
if (!row->has(AIF_SEMANTICS))
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: action mapping not added: only RangeController, ModeController and ToggleController take one",
|
||||
name.text);
|
||||
return *this;
|
||||
}
|
||||
if (mapping.actions == 0 || mapping.directiveName == nullptr || mapping.directiveName[0] == '\0')
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: action mapping not added: it needs an action and a directive, ActionMapping({AlexaAction::Open}, \"TurnOn\")",
|
||||
name.text);
|
||||
return *this;
|
||||
}
|
||||
if (actionCount >= ALEX2ESP_MAX_ACTION_MAPPINGS)
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: action mapping to %s not added: it has %d already (ALEX2ESP_MAX_ACTION_MAPPINGS)",
|
||||
name.text, mapping.directiveName, ALEX2ESP_MAX_ACTION_MAPPINGS);
|
||||
return *this;
|
||||
}
|
||||
actionMappings[actionCount++] = mapping;
|
||||
return *this;
|
||||
}
|
||||
|
||||
AlexaCapability::StateMapping *AlexaCapability::newStateMapping(std::initializer_list<AlexaState> states,
|
||||
StateMappingKind kind)
|
||||
{
|
||||
const AlexaCapabilityName name(*row, instance);
|
||||
if (!row->has(AIF_SEMANTICS))
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: state mapping not added: only RangeController, ModeController and ToggleController take one",
|
||||
name.text);
|
||||
return nullptr;
|
||||
}
|
||||
if (states.size() == 0)
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: state mapping not added: it needs a state, addStateMapping({AlexaState::Closed}, 0.0f)",
|
||||
name.text);
|
||||
return nullptr;
|
||||
}
|
||||
if (stateCount >= ALEX2ESP_MAX_STATE_MAPPINGS)
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: state mapping not added: it has %d already (ALEX2ESP_MAX_STATE_MAPPINGS)",
|
||||
name.text, ALEX2ESP_MAX_STATE_MAPPINGS);
|
||||
return nullptr;
|
||||
}
|
||||
StateMapping *mapping = &stateMappings[stateCount++];
|
||||
mapping->states = 0;
|
||||
for (AlexaState state : states)
|
||||
{
|
||||
mapping->states |= static_cast<uint8_t>(1u << static_cast<uint8_t>(state));
|
||||
}
|
||||
mapping->kind = kind;
|
||||
mapping->maximum = 0;
|
||||
return mapping;
|
||||
}
|
||||
|
||||
AlexaCapability &AlexaCapability::addStateMapping(std::initializer_list<AlexaState> states, float value)
|
||||
{
|
||||
StateMapping *mapping = newStateMapping(states, STATES_TO_VALUE);
|
||||
if (mapping != nullptr)
|
||||
{
|
||||
mapping->value = value;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
AlexaCapability &AlexaCapability::addStateMapping(std::initializer_list<AlexaState> states, float minimum, float maximum)
|
||||
{
|
||||
StateMapping *mapping = newStateMapping(states, STATES_TO_RANGE);
|
||||
if (mapping != nullptr)
|
||||
{
|
||||
mapping->value = minimum;
|
||||
mapping->maximum = maximum;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
AlexaCapability &AlexaCapability::addStateMapping(std::initializer_list<AlexaState> states, const char *value)
|
||||
{
|
||||
if (value == nullptr)
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: state mapping not added: it needs a value, addStateMapping({AlexaState::Closed}, \"OFF\")",
|
||||
AlexaCapabilityName(*row, instance).text);
|
||||
return *this;
|
||||
}
|
||||
StateMapping *mapping = newStateMapping(states, STATES_TO_TEXT);
|
||||
if (mapping != nullptr)
|
||||
{
|
||||
mapping->text = value;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool AlexaCapability::matches(const char *ns, const char *directiveInstance) const
|
||||
{
|
||||
if (ns == nullptr || strcmp_P(ns, row->ns) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!row->has(AIF_INSTANCED))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return directiveInstance != nullptr && instance != nullptr && strcmp(directiveInstance, instance) == 0;
|
||||
}
|
||||
|
||||
// The keys that only some capabilities have are F() strings: they stay in flash and are copied into the document
|
||||
// of a capability that has them. A plain literal takes static RAM in every sketch.
|
||||
bool AlexaCapability::toJson(JsonArray capabilities, const char *endpointId) const
|
||||
{
|
||||
if (row->has(AIF_INSTANCED) && (instance == nullptr || nameCount == 0))
|
||||
{
|
||||
const AlexaCapabilityName name(*row, instance);
|
||||
if (instance == nullptr)
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: %s left out of discovery: it has no instance, call setInstance(\"Blind.Lift\") on it",
|
||||
endpointId, name.text);
|
||||
}
|
||||
else
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: %s left out of discovery: it has no name, call addFriendlyName(\"Lift\", \"en-US\") on it",
|
||||
endpointId, name.text);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonObject capability = capabilities.add<JsonObject>();
|
||||
capability["interface"] = FPSTR(row->ns);
|
||||
capability["version"] = FPSTR(row->version);
|
||||
capability["type"] = "AlexaInterface";
|
||||
|
||||
if (row->has(AIF_EMPTY_PROPERTIES))
|
||||
{
|
||||
capability["properties"].to<JsonObject>();
|
||||
}
|
||||
else if (!row->has(AIF_NO_PROPERTIES))
|
||||
{
|
||||
JsonObject properties = capability["properties"].to<JsonObject>();
|
||||
properties["retrievable"] = static_cast<bool>(retrievable);
|
||||
properties["proactivelyReported"] = static_cast<bool>(proactivelyReported);
|
||||
|
||||
JsonArray supported = properties["supported"].to<JsonArray>();
|
||||
for (uint8_t i = 0; i < row->propertyCount(); i++)
|
||||
{
|
||||
supported.add<JsonObject>()["name"] = FPSTR(row->property(i));
|
||||
}
|
||||
if (nonControllable)
|
||||
{
|
||||
properties[F("nonControllable")] = true;
|
||||
}
|
||||
}
|
||||
|
||||
writeSemantics(capability);
|
||||
if (instance != nullptr)
|
||||
{
|
||||
capability["instance"] = static_cast<const char *>(instance);
|
||||
}
|
||||
writeNames(capability);
|
||||
if (fillConfiguration != nullptr)
|
||||
{
|
||||
fillConfiguration(capability[F("configuration")].to<JsonObject>(), configurationContext);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Semantics exist only when there is something to map, as alex2node announces them
|
||||
void AlexaCapability::writeSemantics(JsonObject capability) const
|
||||
{
|
||||
if (actionCount == 0 && stateCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
JsonObject semantics = capability["semantics"].to<JsonObject>();
|
||||
|
||||
if (actionCount > 0)
|
||||
{
|
||||
JsonArray mappings = semantics["actionMappings"].to<JsonArray>();
|
||||
for (uint8_t i = 0; i < actionCount; i++)
|
||||
{
|
||||
const ActionMapping &action = actionMappings[i];
|
||||
JsonObject mapping = mappings.add<JsonObject>();
|
||||
mapping["@type"] = "ActionsToDirective";
|
||||
addNames(mapping["actions"].to<JsonArray>(), action.actions, ACTION_NAMES, ACTION_COUNT);
|
||||
|
||||
JsonObject directive = mapping["directive"].to<JsonObject>();
|
||||
directive["name"] = action.directiveName;
|
||||
if (action.directivePayload == nullptr || action.directivePayload[0] == '\0')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Alexa expects the payload as an object, not as a string that holds JSON. Parsed with its length,
|
||||
// as the bridge parses a directive: the sketch links one parser and not two.
|
||||
JsonDocument payload;
|
||||
const DeserializationError error =
|
||||
deserializeJson(payload, action.directivePayload, strlen(action.directivePayload));
|
||||
if (error == DeserializationError::Ok && payload.is<JsonObject>())
|
||||
{
|
||||
directive["payload"] = payload.as<JsonObject>();
|
||||
}
|
||||
else
|
||||
{
|
||||
ALEX2ESP_LOGE("%s: payload of the action mapping to %s left out: it is not the JSON text of an object",
|
||||
AlexaCapabilityName(*row, instance).text, action.directiveName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stateCount > 0)
|
||||
{
|
||||
JsonArray mappings = semantics[F("stateMappings")].to<JsonArray>();
|
||||
for (uint8_t i = 0; i < stateCount; i++)
|
||||
{
|
||||
const StateMapping &state = stateMappings[i];
|
||||
JsonObject mapping = mappings.add<JsonObject>();
|
||||
mapping["@type"] = state.kind == STATES_TO_RANGE ? F("StatesToRange") : F("StatesToValue");
|
||||
addNames(mapping[F("states")].to<JsonArray>(), state.states, STATE_NAMES, STATE_COUNT);
|
||||
|
||||
if (state.kind == STATES_TO_RANGE)
|
||||
{
|
||||
JsonObject range = mapping[F("range")].to<JsonObject>();
|
||||
range[F("minimumValue")] = state.value;
|
||||
range[F("maximumValue")] = state.maximum;
|
||||
}
|
||||
else if (state.kind == STATES_TO_TEXT)
|
||||
{
|
||||
mapping["value"] = state.text;
|
||||
}
|
||||
else
|
||||
{
|
||||
mapping["value"] = state.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AlexaCapability::writeNames(JsonObject capability) const
|
||||
{
|
||||
if (nameCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
JsonArray friendlyNames = capability["capabilityResources"]["friendlyNames"].to<JsonArray>();
|
||||
for (uint8_t i = 0; i < nameCount; i++)
|
||||
{
|
||||
JsonObject friendlyName = friendlyNames.add<JsonObject>();
|
||||
if (names[i].locale == nullptr)
|
||||
{
|
||||
friendlyName["@type"] = F("asset");
|
||||
friendlyName["value"][F("assetId")] = FPSTR(names[i].value);
|
||||
}
|
||||
else
|
||||
{
|
||||
friendlyName["@type"] = "text";
|
||||
JsonObject value = friendlyName["value"].to<JsonObject>();
|
||||
value["text"] = names[i].value;
|
||||
value["locale"] = names[i].locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
181
src/AlexaCapability.h
Normal file
181
src/AlexaCapability.h
Normal 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
|
||||
|
|
@ -72,28 +72,34 @@ String AlexaDevice::getSoftwareVersion() const {
|
|||
}
|
||||
|
||||
|
||||
AlexaInterface* AlexaDevice::addCapability(const AlexaInterfaceDesc& row) {
|
||||
// Check if a interface with the given row already exists
|
||||
for (auto& iface : capabilities) {
|
||||
if (&iface.getRow() == &row) {
|
||||
return &iface; // Return the existing interface
|
||||
AlexaCapability* AlexaDevice::addCapability(const AlexaInterfaceDesc& row, const char* instance) {
|
||||
const bool hasInstance = instance != nullptr && instance[0] != '\0';
|
||||
if (row.has(AIF_INSTANCED) && !hasInstance) {
|
||||
ALEX2ESP_LOGE("%s: capability %s not added: it needs an instance, addCapability(row, \"Blind.Lift\")",
|
||||
endpointId.c_str(), AlexaCapabilityName(row, nullptr).text);
|
||||
return nullptr;
|
||||
}
|
||||
if (!row.has(AIF_INSTANCED) && hasInstance) {
|
||||
ALEX2ESP_LOGE("%s: capability %s not added: the interface takes no instance, leave it out",
|
||||
endpointId.c_str(), AlexaCapabilityName(row, instance).text);
|
||||
return nullptr;
|
||||
}
|
||||
return capabilityOf(row, instance);
|
||||
}
|
||||
|
||||
AlexaCapability* AlexaDevice::capabilityOf(const AlexaInterfaceDesc& row, const char* instance) {
|
||||
for (AlexaCapability& capability : capabilities) {
|
||||
if (&capability.getRow() == &row && (instance == nullptr || strcmp(capability.getInstance(), instance) == 0)) {
|
||||
return &capability;
|
||||
}
|
||||
}
|
||||
|
||||
// If the interface doesn't exist, create a new one
|
||||
capabilities.emplace_back(row);
|
||||
|
||||
// The namespace is in program memory, where the %s of the log cannot read it
|
||||
char interfaceName[40];
|
||||
strncpy_P(interfaceName, row.ns, sizeof(interfaceName) - 1);
|
||||
interfaceName[sizeof(interfaceName) - 1] = '\0';
|
||||
ALEX2ESP_LOGI("%s: capability %s added", endpointId.c_str(), interfaceName);
|
||||
|
||||
// Return a pointer to the newly created device
|
||||
capabilities.emplace_back(row, instance);
|
||||
ALEX2ESP_LOGI("%s: capability %s added", endpointId.c_str(), AlexaCapabilityName(row, instance).text);
|
||||
return &capabilities.back();
|
||||
}
|
||||
|
||||
AlexaInterface* AlexaDevice::refuseCapability(AlexaInterfaceType type) {
|
||||
AlexaCapability* AlexaDevice::refuseCapability(AlexaInterfaceType type) {
|
||||
ALEX2ESP_LOGE("%s: capability not added: the library has no description of interface type %d; "
|
||||
"readme.md lists the interfaces it has (Interface Types)",
|
||||
endpointId.c_str(), static_cast<int>(type));
|
||||
|
|
@ -101,7 +107,7 @@ AlexaInterface* AlexaDevice::refuseCapability(AlexaInterfaceType type) {
|
|||
}
|
||||
|
||||
AlexaInterfaceType AlexaDevice::getInterfaceType(const char* interfaceName) const {
|
||||
for (const AlexaInterface& capability : capabilities) {
|
||||
for (const AlexaCapability& capability : capabilities) {
|
||||
if (strcmp_P(interfaceName, capability.getRow().ns) == 0) {
|
||||
return capability.getType();
|
||||
}
|
||||
|
|
@ -136,31 +142,14 @@ JsonDocument AlexaDevice::getDeviceJSON() const {
|
|||
|
||||
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
|
||||
|
||||
for (const AlexaInterface& capability : capabilities) {
|
||||
capabilitiesArray.add(capability.getJSON().as<JsonObject>());
|
||||
// A capability that cannot be announced says so itself; the device is announced with the others
|
||||
for (const AlexaCapability& capability : capabilities) {
|
||||
capability.toJson(capabilitiesArray, endpointId.c_str());
|
||||
}
|
||||
|
||||
// capabilitiesArray.add(capability.getJSON().as<JsonObject>());
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
// void AlexaDevice::registerEvent(const String& eventName, std::function<void(const JsonDocument&,const AlexaInterfaceType&)> callback) {
|
||||
// eventCallbacks[eventName] = callback;
|
||||
// }
|
||||
|
||||
// void AlexaDevice::triggerEvent(const String& eventName, const JsonDocument& directive,const AlexaInterfaceType& type) const {
|
||||
// auto it = eventCallbacks.find(eventName);
|
||||
// if (it != eventCallbacks.end()) {
|
||||
// // Call the registered callback function
|
||||
// it->second(directive,type);
|
||||
// } else {
|
||||
// Serial.print("No event registered for: ");
|
||||
// Serial.println(eventName);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// Register an event callback function
|
||||
void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)) {
|
||||
for (int i = 0; i < MAX_EVENTS; ++i) {
|
||||
|
|
|
|||
|
|
@ -58,17 +58,20 @@ public:
|
|||
|
||||
JsonDocument getDeviceJSON() const;
|
||||
|
||||
// Returns the capability that describes this interface, creating it on first use. The pointer stays valid for
|
||||
// the lifetime of the device: capabilities live in a std::deque, which never relocates its elements when
|
||||
// another one is added.
|
||||
AlexaInterface* addCapability(const AlexaInterfaceDesc& row);
|
||||
// Returns the capability of this device for the interface, creating it on first use. An interface that a
|
||||
// device may have several of (RangeController, ModeController, ToggleController) takes the instance that tells
|
||||
// them apart, "Blind.Lift"; without one, and with an instance for any other interface, nothing is added, the
|
||||
// reason is printed and nullptr returned. The pointer stays valid for the lifetime of the device:
|
||||
// capabilities live in a std::deque, which never relocates its elements when another one is added.
|
||||
AlexaCapability* addCapability(const AlexaInterfaceDesc& row, const char* instance = nullptr);
|
||||
|
||||
// The same by the type of the interface, as 1.x sketches write it. A type without a row in AlexaInterfaces
|
||||
// cannot be described: nothing is added, the reason is printed and nullptr returned. Inlined, so that a type
|
||||
// written out in the sketch links its row only (alexaInterfaceRow).
|
||||
__attribute__((always_inline)) AlexaInterface* addCapability(AlexaInterfaceType type) {
|
||||
// The same by the type of the interface, as 1.x sketches write it: they call setInstance() on what they get,
|
||||
// so the instance is not asked for here but when the device is announced. A type without a row in
|
||||
// AlexaInterfaces cannot be described: nothing is added, the reason is printed and nullptr returned. Inlined,
|
||||
// so that a type written out in the sketch links its row only (alexaInterfaceRow).
|
||||
__attribute__((always_inline)) AlexaCapability* addCapability(AlexaInterfaceType type) {
|
||||
const AlexaInterfaceDesc* row = alexaInterfaceRow(type);
|
||||
return row != nullptr ? addCapability(*row) : refuseCapability(type);
|
||||
return row != nullptr ? capabilityOf(*row, nullptr) : refuseCapability(type);
|
||||
}
|
||||
|
||||
// The type of the capability of this device with the namespace of a directive ("Alexa.PowerController"),
|
||||
|
|
@ -86,7 +89,9 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
AlexaInterface* refuseCapability(AlexaInterfaceType type);
|
||||
// The capability with this row and, when one is given, this instance; added when the device has none
|
||||
AlexaCapability* capabilityOf(const AlexaInterfaceDesc& row, const char* instance);
|
||||
AlexaCapability* refuseCapability(AlexaInterfaceType type);
|
||||
|
||||
String name;
|
||||
String endpointId;
|
||||
|
|
@ -102,7 +107,7 @@ private:
|
|||
String model = "Alexa2MQTT";
|
||||
const String softwareVersion = ALEX2ESP_VERSION;
|
||||
|
||||
std::deque<AlexaInterface> capabilities; // deque: pointers handed out by addCapability stay valid
|
||||
std::deque<AlexaCapability> capabilities; // deque: pointers handed out by addCapability stay valid
|
||||
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -110,13 +110,13 @@ ALEXA_ROW(DoorbellEventSource, NS_DOORBELL, VERSION_3, nullptr, 0, AIF_NO_PROPER
|
|||
ALEXA_ROW(TimeHoldController, NS_TIME_HOLD, VERSION_3, PROPS_TIME_HOLD, 2, AIF_NONE, TIME_HOLD_CONTROLLER);
|
||||
ALEXA_ROW(Speaker, NS_SPEAKER, VERSION_3, PROPS_SPEAKER, 2, AIF_NONE, SPEAKER);
|
||||
ALEXA_ROW(StepSpeaker, NS_STEP_SPEAKER, VERSION_3, nullptr, 0, AIF_NO_PROPERTIES, STEP_SPEAKER);
|
||||
ALEXA_ROW(PlaybackController, NS_PLAYBACK, VERSION_3, nullptr, 0, AIF_NONE, PLAYBACK_CONTROLLER);
|
||||
ALEXA_ROW(PlaybackController, NS_PLAYBACK, VERSION_3, nullptr, 0, AIF_EMPTY_PROPERTIES, PLAYBACK_CONTROLLER);
|
||||
ALEXA_ROW(PlaybackStateReporter, NS_PLAYBACK_STATE, VERSION_3, PROPS_PLAYBACK_STATE, 1, AIF_NONE, PLAYBACK_STATE_REPORTER);
|
||||
ALEXA_ROW(InputController, NS_INPUT, VERSION_3, PROPS_INPUT, 1, AIF_NONE, INPUT_CONTROLLER);
|
||||
ALEXA_ROW(ChannelController, NS_CHANNEL, VERSION_3, PROPS_CHANNEL, 1, AIF_NONE, CHANNEL_CONTROLLER);
|
||||
ALEXA_ROW(InventoryLevelSensor, NS_INVENTORY_LEVEL, VERSION_3, PROPS_INVENTORY_LEVEL, 1, AIF_INSTANCED, INVENTORY_LEVEL_SENSOR);
|
||||
ALEXA_ROW(SimpleEventSource, NS_SIMPLE_EVENT, VERSION_1_0, nullptr, 0, AIF_INSTANCED | AIF_NO_PROPERTIES, SIMPLE_EVENT_SOURCE);
|
||||
ALEXA_ROW(WakeOnLANController, NS_WAKE_ON_LAN, VERSION_3, nullptr, 0, AIF_NONE, WAKE_ON_LAN_CONTROLLER);
|
||||
ALEXA_ROW(WakeOnLANController, NS_WAKE_ON_LAN, VERSION_3, nullptr, 0, AIF_EMPTY_PROPERTIES, WAKE_ON_LAN_CONTROLLER);
|
||||
|
||||
// The display categories in the order of the enum. Their names are looked up by a value that is only known when
|
||||
// the sketch runs, so this table is always linked (about 1 KB of flash).
|
||||
|
|
|
|||
|
|
@ -88,9 +88,10 @@ enum class AlexaInterfaceType : uint8_t
|
|||
enum AlexaInterfaceFlags : uint8_t
|
||||
{
|
||||
AIF_NONE = 0,
|
||||
AIF_INSTANCED = 1, // a generic controller: needs an instance and friendly names
|
||||
AIF_NO_PROPERTIES = 2, // the discovery object has no "properties" (scenes, doorbells, step speakers)
|
||||
AIF_SEMANTICS = 4 // may carry action and state mappings
|
||||
AIF_INSTANCED = 1, // a device may have several: each needs an instance and a friendly name
|
||||
AIF_NO_PROPERTIES = 2, // the discovery object has no "properties" (scenes, doorbells, step speakers)
|
||||
AIF_SEMANTICS = 4, // may carry action and state mappings
|
||||
AIF_EMPTY_PROPERTIES = 8 // the discovery object has "properties": {}, as the page of the interface shows it
|
||||
};
|
||||
|
||||
// 16 bytes in program memory. The ESP8266 reads flash in whole words only: the pointers can be read as they are,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,20 @@
|
|||
#define ALEX2ESP_MAX_QUEUED_BYTES (4 * ALEX2ESP_MAX_DIRECTIVE)
|
||||
#endif
|
||||
|
||||
// What one capability holds: the names Alexa calls it by, the actions and the states it maps. Each place takes 8
|
||||
// or 12 bytes in every capability of the sketch, used or not. One more than fits is refused with an error.
|
||||
#ifndef ALEX2ESP_MAX_FRIENDLY_NAMES
|
||||
#define ALEX2ESP_MAX_FRIENDLY_NAMES 3
|
||||
#endif
|
||||
|
||||
#ifndef ALEX2ESP_MAX_ACTION_MAPPINGS
|
||||
#define ALEX2ESP_MAX_ACTION_MAPPINGS 4
|
||||
#endif
|
||||
|
||||
#ifndef ALEX2ESP_MAX_STATE_MAPPINGS
|
||||
#define ALEX2ESP_MAX_STATE_MAPPINGS 2
|
||||
#endif
|
||||
|
||||
#if ALEX2ESP_MAX_QUEUED_DIRECTIVES < 1 || ALEX2ESP_MAX_QUEUED_DIRECTIVES > 64
|
||||
#error "ALEX2ESP_MAX_QUEUED_DIRECTIVES has to be between 1 and 64"
|
||||
#endif
|
||||
|
|
@ -40,4 +54,9 @@
|
|||
#error "ALEX2ESP_MAX_QUEUED_BYTES has to be ALEX2ESP_MAX_DIRECTIVE or more: the largest directive has to fit the empty queue"
|
||||
#endif
|
||||
|
||||
#if ALEX2ESP_MAX_FRIENDLY_NAMES < 1 || ALEX2ESP_MAX_FRIENDLY_NAMES > 255 || ALEX2ESP_MAX_ACTION_MAPPINGS < 1 || \
|
||||
ALEX2ESP_MAX_ACTION_MAPPINGS > 255 || ALEX2ESP_MAX_STATE_MAPPINGS < 1 || ALEX2ESP_MAX_STATE_MAPPINGS > 255
|
||||
#error "ALEX2ESP_MAX_FRIENDLY_NAMES, ALEX2ESP_MAX_ACTION_MAPPINGS and ALEX2ESP_MAX_STATE_MAPPINGS have to be between 1 and 255"
|
||||
#endif
|
||||
|
||||
#endif // ALEXA_LIMITS_H
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue