// 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 #include #include #include #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 actions, const char *directiveName, const char *directivePayload = nullptr) : directiveName(directiveName), directivePayload(directivePayload) { for (AlexaAction action : actions) { this->actions |= static_cast(1u << static_cast(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 states, float value); AlexaCapability &addStateMapping(std::initializer_list states, float minimum, float maximum); AlexaCapability &addStateMapping(std::initializer_list 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 ::value>::type> AlexaCapability &addStateMapping(std::initializer_list states, Number value) { return addStateMapping(states, static_cast(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 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