#ifndef ALEXADEVICE_H #define ALEXADEVICE_H #include #include #include "AlexaInterface.h" #include #include #include #include "AlexaLimits.h" #include "AlexaStatusMessage.h" #include "AlexaTransport.h" #include "AlexaVersion.h" #define MAX_EVENTS 10 class DisplayCategoryUtils { public: static String toString(DisplayCategory category) { return String(FPSTR(alexaDisplayCategoryName(category))); } }; class AlexaDevice; // A directive as the handler of its device gets it. The texts belong to the directive: they are valid until the // handler returns, so a handler that answers later keeps a copy of the correlationToken. struct AlexaDirective { AlexaDevice* device; AlexaCapability* capability; // nullptr for ReportState, and when the device has no capability for the directive AlexaInterfaceType type; // of the capability, UNKNOWN without one const char* ns; // "Alexa.PowerController" const char* name; // "TurnOn" const char* instance; // "Blind.Lift", "" for an interface without instances const char* correlationToken; JsonObjectConst payload; bool is(const char* directiveName) const { return strcmp(name, directiveName) == 0; } bool isReportState() const { return strcmp(ns, "Alexa") == 0 && is("ReportState"); } // The answer to a directive that changed something, and the answer to ReportState: add the properties of the // device and send() AlexaStatusMessage response() const; AlexaStatusMessage stateReport() const; }; // One function can serve several devices: the directive says which one it is for typedef void (*AlexaDirectiveHandler)(AlexaDirective& directive); // A device is the transport of its own messages: it passes them on to the bridge and so knows whether a handler // answered the directive it was given. class AlexaDevice final : private AlexaTransport { public: // Devices are created by Alex2ESP::getDevice(), which passes the bridge as the transport that publishes the // device's reports. A device built without one can describe itself, but its reports cannot be sent. AlexaDevice(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport = nullptr); ~AlexaDevice(); // It owns its capabilities, and the pointers to it that the sketch holds have to stay valid AlexaDevice(const AlexaDevice&) = delete; AlexaDevice& operator=(const AlexaDevice&) = delete; void setName(const String& name); String getName() const; String getEndpointId() const; // The device that was created after this one, nullptr for the last AlexaDevice* nextDevice() const { return next; } // Compares the endpoint id with `length` characters at `id` (not NUL-terminated: a part of an MQTT topic) bool hasEndpointId(const char* id, size_t length) const; DisplayCategory getDisplayCategory() const; void setDisplayCategory(DisplayCategory category); String getDescription() const; void setDescription(const String& description); String getManufacturerName() const; void setManufacturerName(const String& manufacturerName); String getManufacturer() const; void setManufacturer(const String& manufacturer); String getModel() const; void setModel(const String& model); String getSoftwareVersion() const; JsonDocument getDeviceJSON() const; // 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. A device holds ALEX2ESP_MAX_CAPABILITIES capabilities; one more is // refused in the same way. The pointer stays valid for the lifetime of the device. AlexaCapability* addCapability(const AlexaInterfaceDesc& row, const char* instance = nullptr); // 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 ? capabilityOf(*row, nullptr) : refuseCapability(type); } // The type of the capability of this device with the namespace of a directive ("Alexa.PowerController"), // UNKNOWN when the device has none AlexaInterfaceType getInterfaceType(const char* interfaceName) const; // The capability a directive with this namespace and instance is for, nullptr when the device has none AlexaCapability* findCapability(const char* ns, const char* instance) const; // The handler of every directive for this device, ReportState included. With one, the "ReportState" and // "Event" handlers of registerEvent() are not called. void onDirective(AlexaDirectiveHandler handler); // The handlers of 1.x: "ReportState", "Event" for every other directive, and "DirectiveReceived", which is // called before either void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&)); // Returns false when no handler has this name. warnIfMissing=false keeps that quiet. bool triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing = true) const; // Hands a directive, {"header": ..., "endpoint": ..., "payload": ...}, to the handler of the device. The // device answers with the ErrorResponse INVALID_DIRECTIVE itself when it has no handler, and when the // directive is for a capability it does not have and the handler sent nothing. A handler that sends nothing // for a capability of the device leaves the directive unanswered; that is printed as an error. void handleDirective(const JsonDocument& message); // A device without a transport gets none for its messages: send() says what is wrong AlexaStatusMessage buildStatusMessage(const String& correlationToken,const bool isResponse=false) { return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse,transport != nullptr ? this : nullptr); } private: friend class AlexaDeviceList; // AlexaTransport, for the messages of this device: notes the answer and passes it on to the bridge AlexaSendResult publish(const char* topic, JsonDocument& doc) override; void timestamp(char* buffer, size_t size) override; // 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; String rootTopic; AlexaTransport* transport; AlexaDevice* next = nullptr; // in the list of the bridge AlexaDirectiveHandler directiveHandler = nullptr; bool answered = false; // a message has been sent since the handler of the directive was called const char* eventNames[MAX_EVENTS]; void (*eventCallbacks[MAX_EVENTS])(const JsonDocument&, const AlexaInterfaceType&); DisplayCategory displayCategory = DisplayCategory::OTHER; String description = "Alexa2MQTT Default Device"; String manufacturerName = "Alexa2MQTT"; String manufacturer = "Alexa2MQTT"; String model = "Alexa2MQTT"; const String softwareVersion = ALEX2ESP_VERSION; AlexaCapability* capabilities[ALEX2ESP_MAX_CAPABILITIES]; // on the heap, owned uint8_t capabilityCount = 0; }; inline AlexaStatusMessage AlexaDirective::response() const { return device->buildStatusMessage(correlationToken, true); } inline AlexaStatusMessage AlexaDirective::stateReport() const { return device->buildStatusMessage(correlationToken, false); } // The devices of a bridge in the order they were created, as a linked list: a device is on the heap from the // moment the sketch asks for it, and a sketch without devices pays for none. class AlexaDeviceList { public: AlexaDeviceList() {} ~AlexaDeviceList(); AlexaDeviceList(const AlexaDeviceList&) = delete; AlexaDeviceList& operator=(const AlexaDeviceList&) = delete; // Creates a device at the end of the list. nullptr when the heap has no room for it. AlexaDevice* add(const String& name, const String& rootTopic, const String& endpointId, AlexaTransport* transport); // The device with this endpoint id, `length` characters at `id`; nullptr when the list has none AlexaDevice* find(const char* id, size_t length) const; AlexaDevice* first() const { return head; } size_t size() const { return count; } private: AlexaDevice* head = nullptr; AlexaDevice* tail = nullptr; size_t count = 0; }; #endif