A message has a kind, and the kind decides its topic: answers on <root>/<id>/alexaResponce, the answer after a DeferredResponse on <root>/<id>/deferredResponse (sendAsync()), a ChangeReport on <root>/changeReport with the changed properties apart from the others (context()), DoorbellPress on <root>/event. AlexaDirective gains deferred(), error(type, message), sceneStarted() and sceneStopped(); thermostat error types go out in the namespace of the thermostat. The helpers are add*Prop, the Add*Prop names stay; a temperature keeps its scale (69 FAHRENHEIT, was 20.56 CELSIUS). messageId is a version 4 UUID from the hardware random source, where rand() seeded per second gave two messages one id. basicLight: static RAM 30,540 B (-76), flash 334,981 B (+1,280); 112 host tests (bridge logic 52, messages 19, new). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
150 lines
7.8 KiB
C++
150 lines
7.8 KiB
C++
/*
|
|
* @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().
|
|
*
|
|
* <root>/discover in answered with one discovery object per device on <root>/discover_r
|
|
* <root>/<endpointId>/alexaDirective in the directive, handed to the handler of the device (onDirective(), or
|
|
* the ReportState / Event handlers of 1.x)
|
|
* <root>/<endpointId>/alexaResponce out the answer the handler built: Response, StateReport, ErrorResponse,
|
|
* DeferredResponse, the events of a scene
|
|
* <root>/<endpointId>/deferredResponse out the answer that follows a DeferredResponse (sendAsync())
|
|
* <root>/changeReport out ChangeReport: properties that changed without a directive
|
|
* <root>/event out what a device reports unasked, DoorbellPress
|
|
*/
|
|
|
|
#ifndef ALEX2ESP_H
|
|
#define ALEX2ESP_H
|
|
|
|
#include <Arduino.h>
|
|
#include <AsyncMqttClient.h>
|
|
#include <ArduinoJson.h>
|
|
#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
|