/* * @title Alex2ESP Library * @author David * @license MIT * @contributors chaos511 * * @description Companion library of the Alex2MQTT Alexa skill: the devices a sketch declares become Alexa * endpoints through the MQTT broker at alex2mqtt.stormysdream.club, or the one of setServer(). * * /discover in answered with one discovery object per device on /discover_r * //alexaDirective in the directive, handed to the handler of the device (onDirective(), or * the ReportState / Event handlers of 1.x) * //alexaResponce out the answer the handler built: Response, StateReport, ErrorResponse, * DeferredResponse, the events of a scene * //deferredResponse out the answer that follows a DeferredResponse (sendAsync()) * /changeReport out ChangeReport: properties that changed without a directive * /event out what a device reports unasked, DoorbellPress */ #ifndef ALEX2ESP_H #define ALEX2ESP_H #include #include #include #include "AlexaBridgeLogic.h" #include "AlexaDevice.h" #include "AlexaInterface.h" #include "AlexaLimits.h" #include "AlexaLog.h" #include "AlexaTransport.h" #include "AlexaUtils.h" enum class Alex2ESPState { UNINITIALIZED, // begin() has not been called INITIALIZED, // begin() has been called; the first connect waits for the clock CONNECTING, SUBSCRIBING, // session open, the subscriptions are not acknowledged yet CONNECTED, // subscribed: discovery requests and directives arrive DISCONNECTED }; class Alex2ESP : public AlexaTransport { public: // Constructor Alex2ESP(); // Begin function for initialization: MQTT username, MQTT password, root topic (the same order as alex2node). // The username and the password are not copied: they have to stay valid for as long as the client is used. // Starts SNTP; loop() opens the MQTT session once Wi-Fi is up and the clock is set, or after 5 s of Wi-Fi // without an answer. A session that ends or is refused is opened again after 1 s, then 2 s, 4 s ... up to once // a minute, while Wi-Fi is up; the reason is printed and returned by getDisconnectReason(). The wait is 1 s // again after a session that lasted a minute. void begin(const char *username, const char *password, const char *rootTopic); // Before begin(), for a broker other than alex2mqtt.stormysdream.club:1883: a name or an address as text. // The host is not copied: it has to stay valid for as long as the client is used. After begin() the call is // ignored with an error. void setServer(const char *host, uint16_t port); Alex2ESPState getState() const; // Call from the sketch's loop(): connects, answers discovery requests and hands one directive per call to its // device. It does not block; the handlers of the sketch run inside it. void loop(); AsyncMqttClientDisconnectReason getDisconnectReason() const; // Returns the device with this endpointId, creating it on first use. The pointer stays valid for the lifetime // of the client. nullptr when the heap has no room for another device; the reason is printed. // Call it after begin(): a device takes the root topic of its reports when it is created. AlexaDevice *getDevice(const String &name, const String &endpointId); // What the library prints on Serial: AlexaLogLevel::NONE, ERROR, INFO (the default) or DEBUG void setLogLevel(AlexaLogLevel level); // false before begin(): the sketch sets the clock itself (its own configTime() with a time zone, an RTC). // begin() then leaves SNTP alone; the library only reads time(). void setTimeSource(bool useSntp); // AlexaTransport: what the devices' reports are sent and stamped with AlexaSendResult publish(const char *topic, JsonDocument &doc) override; void timestamp(char *buffer, size_t size) override; private: static const unsigned long CLOCK_WAIT_MS = 5000; // How long the first connect waits for SNTP static const unsigned long CLOCK_WARNING_MS = 60000; // How often an unset clock is reported while it is used static const unsigned long CONNECT_TIMEOUT_MS = 30000; // How long a connect may stay without an answer static const unsigned long DISCOVERY_WINDOW_MS = 5000; // How long the backend keeps collecting a discovery answer static const unsigned long DISCOVERY_RETRY_MS = 20; // Pause before a refused discovery publish is tried again AsyncMqttClient mqttClient; // MQTT client instance const char *serverHost; // The broker: the default, or what setServer() was given uint16_t serverPort; String rootTopic; // Root topic for communication String discoverTopic; // The topic we listen on for discovery messages String discoverTopicSend; // The topic we send discovery messages String directiveFilter; // The subscription that delivers the directives of every endpoint AlexaDeviceList devices; // On the heap, one by one: pointers handed out by getDevice stay valid Alex2ESPState state; AsyncMqttClientDisconnectReason disconnectReason; bool useSntp; unsigned long clockWaitStarted; // millis() when begin() ran, or when Wi-Fi was last seen down before the first connect bool linkWaitLogged; // The wait for Wi-Fi before the first connect has been reported bool linkWasUp; // Wi-Fi was up when loop() looked last bool attemptTimedOut; // The connect that is being closed had no answer within CONNECT_TIMEOUT_MS bool clockWarned; // The unset clock has been reported unsigned long lastClockWarning; // millis() of that report unsigned long waitingSince; // millis() of the connect while CONNECTING, of the disconnect while DISCONNECTED AlexaReconnectBackoff backoff; // The wait before the next connect uint16_t discoverSubscription; // Packet ids of the two SUBSCRIBEs, to match their acknowledgements uint16_t directiveSubscription; uint8_t subscriptionsPending; bool discoveryRequested; // Set by the MQTT callback, taken by loop() bool discoveryActive; // An answer is going out bool discoveryDeferred; // The answer had to pause: the MQTT client or the heap was full AlexaDevice *discoveryNext; // Next device to announce size_t discoveryAnnounced; // Devices announced in this answer unsigned long discoveryStarted; // millis() when the Discover arrived unsigned long discoveryLastAttempt; // millis() of the publish that was refused AlexaDirectiveBuffer directives; // The directives that wait for loop(), and the one that is arriving AlexaRecentIds recentIds; // messageIds of the last directives handled // Internal event handlers (called by the MQTT client from the network context: they only take notes) void onMqttConnect(bool sessionPresent); void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); void onSubscribe(uint16_t packetId, uint8_t qos); void onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total); //loop processing function void watchLink(); void connectWhenClockIsSet(); void connect(); void warnAboutClock(); void handleMqttReconnection(); void continueDiscovery(); void publishDiscovery(); void processDirective(); AlexaSendResult trySend(const char *topic, JsonDocument &doc, size_t *length); void logRefusal(const char *topic, AlexaSendResult result, size_t length); const char *logName(const char *topic) const; // The topic as the log may print it: without the root topic }; #endif // ALEX2ESP_H