1.1.0: begin(username, password, rootTopic) - the library signature now matches every example, the readme and the website (as published since 2024 no sketch could authenticate: the 64-hex password became the MQTT username). Discovery answers go out over MQTT directly, every device, resumed from loop() under heap pressure within the backend's 5 s window, instead of the 5-slot HTTP queue that silently dropped the rest (which the backend's reconcile then deleted from Alexa). JSON aligned with Alexa / alex2node: event.payload present, ActionMapping payload an object, semantics only when a capability has mappings. Pointer-stable device/capability containers (std::deque), the MQTT payload is copied by length before parsing (no write past the buffer), fragmented directive ids are ignored (a Discover is answered on its first fragment), deprecated ArduinoJson 7 calls replaced, credential Serial prints removed, ESP32 include guards (untested; the ESP8266 is the target). Examples: WIFI_PASSWORD, LED_BUILTIN fallback, string+int print fixes. library.json + library.properties restored (1.1.0; AsyncMqttClient, ArduinoJson 7, ESP Async TCP), LICENSE (MIT), a real keywords.txt, .gitattributes text=auto eol=lf (tree normalised to LF). readme: Forgejo URLs, platformio.ini snippet, Library Manager install, begin() order, INTERIOR_BLIND for blinds, 1.1.0 changelog. All five examples compile warning-free for d1_mini (PlatformIO 6.2, espressif8266) on pve-B450.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 04:58:38 +00:00
parent 1313f47856
commit 87caad25be
20 changed files with 3117 additions and 2782 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto eol=lf

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 David (chaos511)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,93 +1,99 @@
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <Alex2ESP.h>
// Define constants for WiFi and Alexa Client configuration
const char* WIFI_SSID = "";
const char* ALEXA_USERNAME = "";
const char* ALEXA_PASSWORD = "";
const char* ALEXA_ROOT_TOPIC = "";
// Create the Alexa client object
Alex2ESP alexClient;
AlexaDevice* device1;
// Initial state for the light (PowerController)
PowerController outputState = PowerController::OFF;
void setup() {
// Initialize the LED as output
pinMode(LED_BUILTIN, OUTPUT);
// Initialize serial communication for debugging
Serial.begin(74880);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID);
Serial.print("[WIFI] Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(100);
}
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Initialize the Alexa client
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Register a new device with a unique ID and name
device1 = alexClient.getDevice("Test Lamp", "ESP-01");
// Set the device's display category to LIGHT
device1->setDisplayCategory(DisplayCategory::LIGHT);
// Add a power controller capability to the device
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
// Register event listener for "ReportState" directive
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.send();
});
// Register event listener for "Event" directive to handle state changes
device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
if (type == AlexaInterfaceType::POWER_CONTROLLER) {
if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") {
outputState = PowerController::OFF;
Serial.println("Turning OFF");
}
// Send the updated state after change
device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.send();
}
});
}
void loop() {
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state
digitalWrite(LED_BUILTIN, outputState != PowerController::ON);
}
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <Alex2ESP.h>
// The on-board LED: GPIO2 on a Wemos D1 mini (active-low). Defined here for boards whose variant leaves it out.
#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif
// Define constants for WiFi and Alexa Client configuration
const char* WIFI_SSID = "";
const char* WIFI_PASSWORD = "";
const char* ALEXA_USERNAME = "";
const char* ALEXA_PASSWORD = "";
const char* ALEXA_ROOT_TOPIC = "";
// Create the Alexa client object
Alex2ESP alexClient;
AlexaDevice* device1;
// Initial state for the light (PowerController)
PowerController outputState = PowerController::OFF;
void setup() {
// Initialize the LED as output
pinMode(LED_BUILTIN, OUTPUT);
// Initialize serial communication for debugging
Serial.begin(74880);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WIFI] Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(100);
}
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Initialize the Alexa client (MQTT username, MQTT password, root topic)
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Register a new device with a unique ID and name
device1 = alexClient.getDevice("Test Lamp", "ESP-01");
// Set the device's display category to LIGHT
device1->setDisplayCategory(DisplayCategory::LIGHT);
// Add a power controller capability to the device
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
// Register event listener for "ReportState" directive
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.send();
});
// Register event listener for "Event" directive to handle state changes
device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
if (type == AlexaInterfaceType::POWER_CONTROLLER) {
if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") {
outputState = PowerController::OFF;
Serial.println("Turning OFF");
}
// Send the updated state after change
device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.send();
}
});
}
void loop() {
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state (the on-board LED is active-low)
digitalWrite(LED_BUILTIN, outputState != PowerController::ON);
}

View file

@ -1,127 +1,133 @@
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <Alex2ESP.h>
// Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = "";
// Create the Alexa client object
Alex2ESP alexClient;
AlexaDevice *device1;
// Initial state for the blinds (PowerController)
PowerController outputState = PowerController::OFF;
void setup()
{
// Initialize the LED as output
pinMode(LED_BUILTIN, OUTPUT);
// Initialize serial communication for debugging
Serial.begin(74880);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID);
Serial.print("[WIFI] Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(100);
}
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Initialize the Alexa client
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
String deviceName="Bedroom Blinds";
// Register a new device with a unique ID and name
device1 = alexClient.getDevice(deviceName, "ESP-01");
// Set the device's display category to LIGHT
device1->setDisplayCategory(DisplayCategory::LIGHT);
//a power controller will allow us to "turn on/off" the blinds (not really usefull but is possable)
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
// Add a toggle controller capability to the device
AlexaInterface* toggleController=device1->addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER);
//toggle controllers are unique in that they require some additinal attrabutes by default we first need to set an instance name
toggleController->setInstance("ESP-01.Toggle");
//we also need to set a name for the interface, this is a sub device listed under the main device name
//to allow open/close commands as well as on/off commands we can use the same name as device name
toggleController->addFriendlyName(deviceName.c_str(),"en-US");
// Example of adding action mappings to map open/close to on/off
ActionMapping closeMapping({AlexaActions::Close}, "TurnOn");
ActionMapping openMapping({AlexaActions::Open}, "TurnOff");
// the full supported list of alexa action mappings can be found on
// https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-discovery-objects.html#action-mapping
//note, only 'generic controller interffaces support action mappings for a full list visit
//https://developer.amazon.com/en-US/docs/alexa/device-apis/generic-controllers.html
toggleController->addActionMapping(closeMapping);
toggleController->addActionMapping(openMapping);
// Register event listener for "ReportState" directive
device1->registerEvent("ReportState", [](const JsonDocument &directive, const AlexaInterfaceType &type)
{
Serial.println("Report state requested.");
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
.AddToggleControllerProp(outputState,"ESP-01.Toggle")
.AddPowerControllerProp(outputState)
.send(); });
// Register event listener for "Event" directive to handle state changes
device1->registerEvent("Event", [](const JsonDocument &directive, const AlexaInterfaceType &type)
{
if (type == AlexaInterfaceType::TOGGLE_CONTROLLER||type == AlexaInterfaceType::POWER_CONTROLLER) {
if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") {
outputState = PowerController::OFF;
Serial.println("Turning OFF");
}
// Send the updated state after change
device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
.AddToggleControllerProp(outputState,"ESP-01.Toggle")
.AddPowerControllerProp(outputState)
.send();
} });
}
void loop()
{
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state
digitalWrite(LED_BUILTIN, outputState != PowerController::ON);
}
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <Alex2ESP.h>
// The on-board LED: GPIO2 on a Wemos D1 mini (active-low). Defined here for boards whose variant leaves it out.
#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif
// Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
const char *WIFI_PASSWORD = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = "";
// Create the Alexa client object
Alex2ESP alexClient;
AlexaDevice *device1;
// Initial state for the blinds (PowerController)
PowerController outputState = PowerController::OFF;
void setup()
{
// Initialize the LED as output
pinMode(LED_BUILTIN, OUTPUT);
// Initialize serial communication for debugging
Serial.begin(74880);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WIFI] Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(100);
}
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Initialize the Alexa client (MQTT username, MQTT password, root topic)
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
String deviceName="Bedroom Blinds";
// Register a new device with a unique ID and name
device1 = alexClient.getDevice(deviceName, "ESP-01");
// Set the device's display category to INTERIOR_BLIND
device1->setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
//a power controller will allow us to "turn on/off" the blinds (not really usefull but is possable)
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
// Add a toggle controller capability to the device
AlexaInterface* toggleController=device1->addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER);
//toggle controllers are unique in that they require some additinal attrabutes by default we first need to set an instance name
toggleController->setInstance("ESP-01.Toggle");
//we also need to set a name for the interface, this is a sub device listed under the main device name
//to allow open/close commands as well as on/off commands we can use the same name as device name
toggleController->addFriendlyName(deviceName.c_str(),"en-US");
// Example of adding action mappings to map open/close to on/off
ActionMapping closeMapping({AlexaActions::Close}, "TurnOn");
ActionMapping openMapping({AlexaActions::Open}, "TurnOff");
// the full supported list of alexa action mappings can be found on
// https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-discovery-objects.html#action-mapping
//note, only 'generic controller interffaces support action mappings for a full list visit
//https://developer.amazon.com/en-US/docs/alexa/device-apis/generic-controllers.html
toggleController->addActionMapping(closeMapping);
toggleController->addActionMapping(openMapping);
// Register event listener for "ReportState" directive
device1->registerEvent("ReportState", [](const JsonDocument &directive, const AlexaInterfaceType &type)
{
Serial.println("Report state requested.");
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
.AddToggleControllerProp(outputState,"ESP-01.Toggle")
.AddPowerControllerProp(outputState)
.send(); });
// Register event listener for "Event" directive to handle state changes
device1->registerEvent("Event", [](const JsonDocument &directive, const AlexaInterfaceType &type)
{
if (type == AlexaInterfaceType::TOGGLE_CONTROLLER||type == AlexaInterfaceType::POWER_CONTROLLER) {
if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") {
outputState = PowerController::OFF;
Serial.println("Turning OFF");
}
// Send the updated state after change
device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
.AddToggleControllerProp(outputState,"ESP-01.Toggle")
.AddPowerControllerProp(outputState)
.send();
} });
}
void loop()
{
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state (the on-board LED is active-low)
digitalWrite(LED_BUILTIN, outputState != PowerController::ON);
}

View file

@ -1,110 +1,116 @@
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <Alex2ESP.h>
// Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = "";
// Create the Alexa client object
Alex2ESP alexClient;
AlexaDevice* device1;
// Initial state for the light (PowerController)
PowerController outputState = PowerController::OFF;
int brightness=100;
void setup() {
// Initialize the LED as output
pinMode(LED_BUILTIN, OUTPUT);
// Initialize serial communication for debugging
Serial.begin(74880);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID);
Serial.print("[WIFI] Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(100);
}
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Initialize the Alexa client
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Register a new device with a unique ID and name
device1 = alexClient.getDevice("Test Lamp", "ESP-01");
// Set the device's display category to LIGHT
device1->setDisplayCategory(DisplayCategory::LIGHT);
// Add a power controller capability to the device
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
// Add a brightness controller to the device to allow dimming
device1->addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER);
// Register event listener for "ReportState" directive
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.AddBrightnessControllerProp(brightness)
.send();
});
// Register event listener for "Event" directive to handle state changes
device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
if (type == AlexaInterfaceType::POWER_CONTROLLER) {
if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") {
outputState = PowerController::OFF;
Serial.println("Turning OFF");
}
}else if (type == AlexaInterfaceType::BRIGHTNESS_CONTROLLER) {
if(directive["header"]["name"]=="SetBrightness"){
brightness=directive["payload"]["brightness"];
Serial.println("setting brightness to: "+brightness);
outputState = PowerController::ON;
}
}else{
return;
}
// Send the updated state after change
device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.AddBrightnessControllerProp(brightness)
.send();
});
}
void loop() {
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state and brightness
if(outputState == PowerController::ON){
analogWrite(LED_BUILTIN,map(brightness,0,100,255,0));
}else{
digitalWrite(LED_BUILTIN, true);
}
}
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <Alex2ESP.h>
// The on-board LED: GPIO2 on a Wemos D1 mini (active-low). Defined here for boards whose variant leaves it out.
#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif
// Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
const char *WIFI_PASSWORD = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = "";
// Create the Alexa client object
Alex2ESP alexClient;
AlexaDevice* device1;
// Initial state for the light (PowerController)
PowerController outputState = PowerController::OFF;
int brightness=100;
void setup() {
// Initialize the LED as output
pinMode(LED_BUILTIN, OUTPUT);
// Initialize serial communication for debugging
Serial.begin(74880);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WIFI] Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(100);
}
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Initialize the Alexa client (MQTT username, MQTT password, root topic)
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Register a new device with a unique ID and name
device1 = alexClient.getDevice("Test Lamp", "ESP-01");
// Set the device's display category to LIGHT
device1->setDisplayCategory(DisplayCategory::LIGHT);
// Add a power controller capability to the device
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
// Add a brightness controller to the device to allow dimming
device1->addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER);
// Register event listener for "ReportState" directive
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.AddBrightnessControllerProp(brightness)
.send();
});
// Register event listener for "Event" directive to handle state changes
device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
if (type == AlexaInterfaceType::POWER_CONTROLLER) {
if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") {
outputState = PowerController::OFF;
Serial.println("Turning OFF");
}
}else if (type == AlexaInterfaceType::BRIGHTNESS_CONTROLLER) {
if(directive["header"]["name"]=="SetBrightness"){
brightness=directive["payload"]["brightness"];
Serial.printf("setting brightness to: %d\n", brightness);
outputState = PowerController::ON;
}
}else{
return;
}
// Send the updated state after change
device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.AddBrightnessControllerProp(brightness)
.send();
});
}
void loop() {
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state and brightness (the on-board LED is active-low)
if(outputState == PowerController::ON){
analogWrite(LED_BUILTIN,map(brightness,0,100,255,0));
}else{
digitalWrite(LED_BUILTIN, true);
}
}

View file

@ -1,122 +1,128 @@
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <Alex2ESP.h>
// Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = "";
// Create the Alexa client object
Alex2ESP alexClient;
AlexaDevice* device1;
// Initial state for the light (PowerController)
PowerController outputState = PowerController::OFF;
int brightness=100;
int colorTemp=0;
void setup() {
// Initialize the LED as output
pinMode(LED_BUILTIN, OUTPUT);
// Initialize serial communication for debugging
Serial.begin(74880);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID);
Serial.print("[WIFI] Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(100);
}
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Initialize the Alexa client
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Register a new device with a unique ID and name
device1 = alexClient.getDevice("Test Lamp ESP", "ESP-01");
// Set the device's display category to LIGHT
device1->setDisplayCategory(DisplayCategory::LIGHT);
// Add a power controller capability to the device
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
// Add a brightness controller to the device to allow dimming
device1->addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER);
// Add a color temperature controller
device1->addCapability(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER);
// Register event listener for "ReportState" directive
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.AddBrightnessControllerProp(brightness)
.AddColorTemperatureControllerProp(colorTemp)
.send();
});
// Register event listener for "Event" directive to handle state changes
device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
if (type == AlexaInterfaceType::POWER_CONTROLLER) {
if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") {
outputState = PowerController::OFF;
Serial.println("Turning OFF");
}
}else if (type == AlexaInterfaceType::BRIGHTNESS_CONTROLLER) {
if(directive["header"]["name"]=="SetBrightness"){
brightness=directive["payload"]["brightness"];
Serial.println("setting brightness to: "+brightness);
outputState = PowerController::ON;
}
}else if (type == AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER) {
if(directive["header"]["name"]=="SetColorTemperature"){
colorTemp=directive["payload"]["colorTemperatureInKelvin"];
Serial.println("setting color temperature to: "+colorTemp);
outputState = PowerController::ON;
}
}else{
return;
}
// Send the updated state after change
device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.AddBrightnessControllerProp(brightness)
.AddColorTemperatureControllerProp(colorTemp)
.send();
});
}
void loop() {
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state and brightness
if(outputState == PowerController::ON){
analogWrite(LED_BUILTIN,map(brightness,0,100,255,0));
}else{
digitalWrite(LED_BUILTIN, true);
}
}
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <Alex2ESP.h>
// The on-board LED: GPIO2 on a Wemos D1 mini (active-low). Defined here for boards whose variant leaves it out.
#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif
// Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
const char *WIFI_PASSWORD = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = "";
// Create the Alexa client object
Alex2ESP alexClient;
AlexaDevice* device1;
// Initial state for the light (PowerController)
PowerController outputState = PowerController::OFF;
int brightness=100;
int colorTemp=0;
void setup() {
// Initialize the LED as output
pinMode(LED_BUILTIN, OUTPUT);
// Initialize serial communication for debugging
Serial.begin(74880);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WIFI] Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(100);
}
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Initialize the Alexa client (MQTT username, MQTT password, root topic)
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Register a new device with a unique ID and name
device1 = alexClient.getDevice("Test Lamp ESP", "ESP-01");
// Set the device's display category to LIGHT
device1->setDisplayCategory(DisplayCategory::LIGHT);
// Add a power controller capability to the device
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
// Add a brightness controller to the device to allow dimming
device1->addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER);
// Add a color temperature controller
device1->addCapability(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER);
// Register event listener for "ReportState" directive
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.AddBrightnessControllerProp(brightness)
.AddColorTemperatureControllerProp(colorTemp)
.send();
});
// Register event listener for "Event" directive to handle state changes
device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
if (type == AlexaInterfaceType::POWER_CONTROLLER) {
if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") {
outputState = PowerController::OFF;
Serial.println("Turning OFF");
}
}else if (type == AlexaInterfaceType::BRIGHTNESS_CONTROLLER) {
if(directive["header"]["name"]=="SetBrightness"){
brightness=directive["payload"]["brightness"];
Serial.printf("setting brightness to: %d\n", brightness);
outputState = PowerController::ON;
}
}else if (type == AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER) {
if(directive["header"]["name"]=="SetColorTemperature"){
colorTemp=directive["payload"]["colorTemperatureInKelvin"];
Serial.printf("setting color temperature to: %d\n", colorTemp);
outputState = PowerController::ON;
}
}else{
return;
}
// Send the updated state after change
device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.AddBrightnessControllerProp(brightness)
.AddColorTemperatureControllerProp(colorTemp)
.send();
});
}
void loop() {
// Process Alexa client communication
alexClient.loop();
// Update the state of the LED based on the power controller state and brightness (the on-board LED is active-low)
if(outputState == PowerController::ON){
analogWrite(LED_BUILTIN,map(brightness,0,100,255,0));
}else{
digitalWrite(LED_BUILTIN, true);
}
}

View file

@ -1,67 +1,68 @@
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <Alex2ESP.h>
// Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = "";
// Create the Alexa client object
Alex2ESP alexClient;
AlexaDevice* device1;
void setup() {
// Initialize serial communication for debugging
Serial.begin(74880);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID);
Serial.print("[WIFI] Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(100);
}
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Initialize the Alexa client
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Register a new device with a unique ID and name
device1 = alexClient.getDevice("ESP Temp", "ESP-01");
// Set the device's display category to TEMPERATURE_SENSOR
device1->setDisplayCategory(DisplayCategory::TEMPERATURE_SENSOR);
// Add a temperature sensor capability to the device
device1->addCapability(AlexaInterfaceType::TEMPERATURE_SENSOR);
// Register event listener for "ReportState" directive
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
.AddTemperatureSensorProp(TemperatureSensorScale::FAHRENHEIT,69)
.send();
});
}
void loop() {
// Process Alexa client communication
alexClient.loop();
}
#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
#include <Alex2ESP.h>
// Define constants for WiFi and Alexa Client configuration
const char *WIFI_SSID = "";
const char *WIFI_PASSWORD = "";
const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = "";
// Create the Alexa client object
Alex2ESP alexClient;
AlexaDevice* device1;
void setup() {
// Initialize serial communication for debugging
Serial.begin(74880);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WIFI] Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(100);
}
Serial.println();
Serial.printf("[WIFI] Connected to SSID: %s, IP address: %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
// Initialize the Alexa client (MQTT username, MQTT password, root topic)
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
// Register a new device with a unique ID and name
device1 = alexClient.getDevice("ESP Temp", "ESP-01");
// Set the device's display category to TEMPERATURE_SENSOR
device1->setDisplayCategory(DisplayCategory::TEMPERATURE_SENSOR);
// Add a temperature sensor capability to the device
device1->addCapability(AlexaInterfaceType::TEMPERATURE_SENSOR);
// Register event listener for "ReportState" directive
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
.AddTemperatureSensorProp(TemperatureSensorScale::FAHRENHEIT,69)
.send();
});
}
void loop() {
// Process Alexa client communication
alexClient.loop();
}

View file

@ -1,7 +1,81 @@
#######################################
# Syntax Coloring Map For Espalexa
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
#######################################
# Syntax Coloring Map For Alex2ESP
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
Alex2ESP KEYWORD1
Alex2ESPState KEYWORD1
AlexaDevice KEYWORD1
AlexaInterface KEYWORD1
AlexaInterfaceType KEYWORD1
AlexaInterfaceUtils KEYWORD1
AlexaStatusMessage KEYWORD1
ActionMapping KEYWORD1
AlexaActions KEYWORD1
AlexaActionsUtils KEYWORD1
FriendlyName KEYWORD1
DisplayCategory KEYWORD1
DisplayCategoryUtils KEYWORD1
EndpointHealth KEYWORD1
PowerController KEYWORD1
TemperatureSensorScale KEYWORD1
AlexaUtils KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
begin KEYWORD2
loop KEYWORD2
getState KEYWORD2
getDisconnectReason KEYWORD2
getDevice KEYWORD2
setName KEYWORD2
getName KEYWORD2
getEndpointId KEYWORD2
setDisplayCategory KEYWORD2
getDisplayCategory KEYWORD2
setDescription KEYWORD2
getDescription KEYWORD2
setManufacturerName KEYWORD2
getManufacturerName KEYWORD2
setManufacturer KEYWORD2
getManufacturer KEYWORD2
setModel KEYWORD2
getModel KEYWORD2
getSoftwareVersion KEYWORD2
getDeviceJSON KEYWORD2
addCapability KEYWORD2
registerEvent KEYWORD2
triggerEvent KEYWORD2
buildStatusMessage KEYWORD2
getType KEYWORD2
getTypeString KEYWORD2
getVersion KEYWORD2
getProps KEYWORD2
setInstance KEYWORD2
addFriendlyName KEYWORD2
isRetrievable KEYWORD2
isProactivelyReported KEYWORD2
setRetrievable KEYWORD2
setProactivelyReported KEYWORD2
addActionMapping KEYWORD2
getJSON KEYWORD2
AddHealthProp KEYWORD2
AddPowerControllerProp KEYWORD2
AddTemperatureSensorProp KEYWORD2
AddBrightnessControllerProp KEYWORD2
AddColorTemperatureControllerProp KEYWORD2
AddToggleControllerProp KEYWORD2
AddContextProp KEYWORD2
send KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################
MAX_STATUS_REPORT_SIZE LITERAL1
MAX_EVENTS LITERAL1

47
library.json Normal file
View file

@ -0,0 +1,47 @@
{
"name": "Alex2ESP",
"version": "1.1.0",
"description": "Companion library for the Alex2MQTT Alexa skill: exposes ESP8266 devices to Alexa through alex2mqtt.stormysdream.club over MQTT (discovery, directives, state reports).",
"keywords": [
"alexa",
"mqtt",
"esp8266",
"smart home",
"voice assistant",
"home automation"
],
"repository": {
"type": "git",
"url": "https://git.stormysdream.club/platformio/Alex2ESP.git"
},
"authors": [
{
"name": "David",
"email": "Alex2ESP@stormysdream.club",
"maintainer": true
}
],
"license": "MIT",
"frameworks": [
"arduino"
],
"platforms": [
"espressif8266"
],
"headers": "Alex2ESP.h",
"examples": [
"examples/*.cpp"
],
"dependencies": [
{
"owner": "marvinroger",
"name": "AsyncMqttClient",
"version": "^0.9.0"
},
{
"owner": "bblanchon",
"name": "ArduinoJson",
"version": "^7.2.1"
}
]
}

11
library.properties Normal file
View file

@ -0,0 +1,11 @@
name=Alex2ESP
version=1.1.0
author=David <Alex2ESP@stormysdream.club>
maintainer=David <Alex2ESP@stormysdream.club>
sentence=A companion library for the Alex2MQTT Alexa Skill (ESP8266).
paragraph=Connects ESP8266 devices to alex2mqtt.stormysdream.club so Alexa can discover and control them over MQTT. Needs AsyncMqttClient (with ESPAsyncTCP) and ArduinoJson 7.
category=Communication
url=https://git.stormysdream.club/platformio/Alex2ESP
architectures=esp8266
includes=Alex2ESP.h
depends=AsyncMqttClient,ArduinoJson,ESP Async TCP

460
readme.md
View file

@ -1,199 +1,261 @@
# Alex2ESP
Alex2ESP is a lightweight Arduino/PlatformIO library for integrating ESP8266 and ESP32 microcontrollers with Amazon Alexa smart home APIs. The library simplifies the process of creating Alexa-compatible devices using MQTT.
For more details on how to configure Alex2ESP, visit [Alex2MQTT Documentation](https://alex2mqtt.stormysdream.club/).
---
## Features
- Supports "All" Alexa smart home capabilities.
- Provides action mapping for advanced directive customization including Open,Close,Raise,and Lower commands.
- Allows custom JSON injection in both discovery and state reporting to allow for unsupported devices.
- Event-driven architecture for handling Alexa directives.
---
## Alternatives
one of the most popular library for controlling esp like devices using alexa is [FauxmoESP](https://github.com/vintlabs/fauxmoESP)
however FauxmoESP works by emulating a light bulb so it is limited in what commands it accepts.
---
## Quick Start
### Installation
Download the library as zip or via PlatformIO and include it in your project:
```cpp
#include <Alex2ESP.h>
```
---
## Creating a Basic Device
### Example: Light Control
Here’s how to create a simple device that controls a light:
First create an instance of the alexa client and initilize it with your MQTT Credentials
```cpp
Alex2ESP alexClient;
```
Credentials can be found at https://alex2mqtt.stormysdream.club/ after logging in with amazon.
```cpp
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
```
Once initilized you can begin to add virtual devices, in this example we add a power controller to toggle a light
```cpp
AlexaDevice* device1 = alexClient.getDevice("Test Lamp", "ESP-01");
device1->setDisplayCategory(DisplayCategory::LIGHT);
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.send();
});
// Register event listener for "Event" directive to handle state changes
device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
if (type == AlexaInterfaceType::POWER_CONTROLLER) {
if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") {
outputState = PowerController::OFF;
Serial.println("Turning OFF");
}
// Send the updated state after change
device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.send();
}
});
```
---
### Example: Toggle Controller for Blinds
Alex2ESP also supports action mapping so we can map keywords such as "open" and "close" to a toggle controller
For a comprehensive list of Alexa actions and mappings, visit the [Alexa Developer Documentation](https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-discovery-objects.html#action-mapping)
```cpp
AlexaDevice* device = alexClient.getDevice("Bedroom Blinds", "ESP-01");
device->setDisplayCategory(DisplayCategory::LIGHT);
AlexaInterface* toggleController = device->addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER);
toggleController->setInstance("ESP-01.Toggle");
toggleController->addFriendlyName("Bedroom Blinds", "en-US");
ActionMapping closeMapping({AlexaActions::Close}, "TurnOn");
ActionMapping openMapping({AlexaActions::Open}, "TurnOff");
toggleController->addActionMapping(closeMapping);
toggleController->addActionMapping(openMapping);
```
---
## Interface Types
| Alexa Interface Type | Status |
|-----------------------------------------------------|-------------|
| AlexaInterfaceType::ENDPOINT_HEALTH | Fully Supported |
| AlexaInterfaceType::POWER_CONTROLLER | Fully Supported |
| AlexaInterfaceType::BRIGHTNESS_CONTROLLER | Fully Supported |
| AlexaInterfaceType::TOGGLE_CONTROLLER | Fully Supported |
| AlexaInterfaceType::TEMPERATURE_SENSOR | Fully Supported |
| AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER | Fully Supported |
| AlexaInterfaceType::AUTOMATION_MANAGEMENT | Supported* |
| AlexaInterfaceType::CHANNEL_CONTROLLER | Supported* |
| AlexaInterfaceType::COLOR_CONTROLLER | Supported* |
| AlexaInterfaceType::CONTACT_SENSOR | Supported* |
| AlexaInterfaceType::APPLICATION_STATE_REPORTER | Supported* |
| AlexaInterfaceType::AUDIO_PLAY_QUEUE | Supported* |
| AlexaInterfaceType::AUTHORIZATION_CONTROLLER | Supported* |
| AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA | Supported* |
| AlexaInterfaceType::CAMERA_LIVE_VIEW_CONTROLLER | Supported* |
| AlexaInterfaceType::CAMERA_STREAM_CONTROLLER | Supported* |
| AlexaInterfaceType::COMMISSIONABLE | Supported* |
| AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER | Supported* |
| AlexaInterfaceType::COOKING | Supported* |
| AlexaInterfaceType::DATA_CONTROLLER | Supported* |
| AlexaInterfaceType::DEVICE_USAGE_ESTIMATION | Supported* |
| AlexaInterfaceType::DEVICE_USAGE_METER | Supported* |
| AlexaInterfaceType::DOORBELL_EVENT_SOURCE | Supported* |
| AlexaInterfaceType::EQUALIZER_CONTROLLER | Supported* |
| AlexaInterfaceType::INPUT_CONTROLLER | Supported* |
| AlexaInterfaceType::INVENTORY_LEVEL_SENSOR | Supported* |
| AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR | Supported* |
| AlexaInterfaceType::INVENTORY_USAGE_SENSOR | Supported* |
| AlexaInterfaceType::KEYPAD_CONTROLLER | Supported* |
| AlexaInterfaceType::LAUNCHER | Supported* |
| AlexaInterfaceType::LOCK_CONTROLLER | Supported* |
| AlexaInterfaceType::MEDIA_PLAYBACK | Supported* |
| AlexaInterfaceType::MEDIA_SEARCH | Supported* |
| AlexaInterfaceType::MODE_CONTROLLER | Supported* |
| AlexaInterfaceType::MOTION_SENSOR | Supported* |
| AlexaInterfaceType::PERCENTAGE_CONTROLLER | Supported* |
| AlexaInterfaceType::PLAYBACK_CONTROLLER | Supported* |
| AlexaInterfaceType::PLAYBACK_STATE_REPORTER | Supported* |
| AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE | Supported* |
| AlexaInterfaceType::RANGE_CONTROLLER | Supported* |
| AlexaInterfaceType::RECORD_CONTROLLER | Supported* |
| AlexaInterfaceType::REMOTE_VIDEO_PLAYER | Supported* |
| AlexaInterfaceType::RTC_SESSION_CONTROLLER | Supported* |
| AlexaInterfaceType::SCENE_CONTROLLER | Supported* |
| AlexaInterfaceType::SECURITY_PANEL_CONTROLLER | Supported* |
| AlexaInterfaceType::SEEK_CONTROLLER | Supported* |
| AlexaInterfaceType::SIMPLE_EVENT_SOURCE | Supported* |
| AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR | Supported* |
| AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER | Supported* |
| AlexaInterfaceType::SPEAKER | Supported* |
| AlexaInterfaceType::STEP_SPEAKER | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE | Supported* |
| AlexaInterfaceType::TIME_HOLD_CONTROLLER | Supported* |
| AlexaInterfaceType::UI_CONTROLLER | Supported* |
| AlexaInterfaceType::USER_PREFERENCE | Supported* |
| AlexaInterfaceType::VIDEO_RECORDER | Supported* |
| AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER | Supported* |
(*Partial support or limited implementation advanced configuration is required)
---
## Advanced ussage
For devices with limited or partial support, Alex2ESP provides functions that allow you to attach custom JSON objects to the status report. For example, to report the status of a PowerController type, you can use the `AddPowerControllerProp` function. However, if a specific "add props" function does not exist for your use case, you can utilize the `AddContextProp` function to pass a custom JSON object.
For instance, to manually report the state of a PowerController, you can use the following code:
```cpp
JsonDocument doc;
doc["namespace"] = "Alexa.PowerController";
doc["name"] = "powerState";
doc["value"] = "ON";
doc["timeOfSample"] = "{REPLACE_WITH_DATETIME}"; // Alex2MQTT server wil do the replace
doc["uncertaintyInMilliseconds"] = 0;
AddContextProp(doc.as<JsonObject>())
```
---
## Contributing
Feel free to submit pull requests or issues for feature requests and bug fixes.
---
## License
This project is licensed under the MIT License. See the LICENSE file for details.
# Alex2ESP
Alex2ESP is a lightweight Arduino/PlatformIO library for integrating ESP8266 microcontrollers with Amazon Alexa smart home APIs through the [Alex2MQTT](https://alex2mqtt.stormysdream.club/) skill. The library simplifies the process of creating Alexa-compatible devices using MQTT.
**Supported target: ESP8266** (developed and tested on a Wemos D1 mini). ESP32: untested - the code carries include guards for it, but it has never been compiled or run there.
For more details on how to configure Alex2ESP, visit [Alex2MQTT Documentation](https://alex2mqtt.stormysdream.club/).
---
## Features
- Supports "All" Alexa smart home capabilities.
- Provides action mapping for advanced directive customization including Open,Close,Raise,and Lower commands.
- Allows custom JSON injection in both discovery and state reporting to allow for unsupported devices.
- Event-driven architecture for handling Alexa directives.
---
## Alternatives
one of the most popular library for controlling esp like devices using alexa is [FauxmoESP](https://github.com/vintlabs/fauxmoESP)
however FauxmoESP works by emulating a light bulb so it is limited in what commands it accepts.
---
## Quick Start
### Installation
The library lives on Forgejo at https://git.stormysdream.club/platformio/Alex2ESP (public). It is not in the PlatformIO registry or the Arduino Library Manager: install it from git.
#### PlatformIO
```ini
[env:d1_mini]
platform = espressif8266
board = d1_mini
framework = arduino
monitor_speed = 74880
lib_deps =
https://git.stormysdream.club/platformio/Alex2ESP.git
marvinroger/AsyncMqttClient@^0.9.0
bblanchon/ArduinoJson@^7
```
To pin a release instead of `main`, append the tag: `https://git.stormysdream.club/platformio/Alex2ESP.git#v1.1.0`. AsyncMqttClient pulls in ESPAsyncTCP by itself; if PlatformIO notes that more than one `ESPAsyncTCP` package matches, add `esp32async/ESPAsyncTCP@^2.0.0` to `lib_deps` to pick one explicitly. The examples print at 74880 baud, the ESP8266 boot-ROM rate, so the boot messages stay readable in the same monitor.
#### Arduino IDE
Download the repository as a ZIP (https://git.stormysdream.club/platformio/Alex2ESP/archive/main.zip) and add it with *Sketch -> Include Library -> Add .ZIP Library*. Install **AsyncMqttClient**, **ArduinoJson** (7.x) and **ESP Async TCP** (by ESP32Async) from the Library Manager. Install the ESP8266 board package and select your board (for example *LOLIN(WEMOS) D1 R2 & mini*).
Then include the library in your project:
```cpp
#include <Alex2ESP.h>
```
Complete sketches are in [`examples/`](examples/): `basicLight.cpp`, `lightWithBrightness.cpp`, `lightWithColorTemp.cpp`, `tempSensor.cpp` and `blindControl.cpp`. Every example joins Wi-Fi with `WiFi.begin(WIFI_SSID, WIFI_PASSWORD)`; fill in the SSID, the password and your Alex2MQTT credentials before flashing.
---
## Creating a Basic Device
### Example: Light Control
Here’s how to create a simple device that controls a light:
First create an instance of the alexa client and initilize it with your MQTT Credentials
```cpp
Alex2ESP alexClient;
```
Credentials can be found at https://alex2mqtt.stormysdream.club/ after logging in with amazon. `begin()` takes the MQTT username, the MQTT password and the root topic, in that order (the same order as alex2node's `new Alex2MQTT(username, password, rootTopic)`).
```cpp
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
```
Once initilized you can begin to add virtual devices, in this example we add a power controller to toggle a light
```cpp
AlexaDevice* device1 = alexClient.getDevice("Test Lamp", "ESP-01");
device1->setDisplayCategory(DisplayCategory::LIGHT);
device1->addCapability(AlexaInterfaceType::POWER_CONTROLLER);
device1->registerEvent("ReportState", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
// Build and send status report
device1->buildStatusMessage(directive["header"]["correlationToken"])
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.send();
});
// Register event listener for "Event" directive to handle state changes
device1->registerEvent("Event", [](const JsonDocument& directive, const AlexaInterfaceType& type) {
if (type == AlexaInterfaceType::POWER_CONTROLLER) {
if (directive["header"]["name"] == "TurnOn") {
outputState = PowerController::ON;
Serial.println("Turning ON");
} else if (directive["header"]["name"] == "TurnOff") {
outputState = PowerController::OFF;
Serial.println("Turning OFF");
}
// Send the updated state after change
device1->buildStatusMessage(directive["header"]["correlationToken"], true)
.AddHealthProp(EndpointHealth::OK)
.AddPowerControllerProp(outputState)
.send();
}
});
```
The pointers returned by `getDevice()` and `addCapability()` stay valid for the lifetime of the client, so keeping them in globals and adding more devices or capabilities later is fine.
---
### Example: Toggle Controller for Blinds
Alex2ESP also supports action mapping so we can map keywords such as "open" and "close" to a toggle controller
For a comprehensive list of Alexa actions and mappings, visit the [Alexa Developer Documentation](https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-discovery-objects.html#action-mapping)
```cpp
AlexaDevice* device = alexClient.getDevice("Bedroom Blinds", "ESP-01");
device->setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
AlexaInterface* toggleController = device->addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER);
toggleController->setInstance("ESP-01.Toggle");
toggleController->addFriendlyName("Bedroom Blinds", "en-US");
ActionMapping closeMapping({AlexaActions::Close}, "TurnOn");
ActionMapping openMapping({AlexaActions::Open}, "TurnOff");
toggleController->addActionMapping(closeMapping);
toggleController->addActionMapping(openMapping);
```
An `ActionMapping` takes an optional third argument, the directive payload as JSON text (for example `"{\"rangeValue\": 0}"` for a RangeController). It is emitted as an object in the discovery response and omitted when empty.
---
## How it talks to Alex2MQTT
- **Discovery.** On `<root>/discover` the library answers with one discovery object per device, published straight to `<root>/discover_r` over MQTT (no HTTP round trip, no queue slot). The backend accepts one endpoint object per message and collects everything that arrives within 1 s for Alexa's discovery answer (up to 5 s for its proactive AddOrUpdate push), so all devices are published back to back the moment the request arrives. Each object sits on the heap (about 1 KB) until the broker acknowledges it; when the MQTT client refuses another one (free heap under 4 KB), the library prints `[Alex2ESP] discovery publish deferred at <endpointId>` and sends the rest from `loop()` as the queue drains, for up to 5 s after the request. `[Alex2ESP] discovery gave up: N device(s) not announced` means those devices missed this answer - on the backend's proactive discovery that can remove them from Alexa until the next one.
- **Directives.** A directive arrives as a short id on `<root>/<endpointId>/alexaDirective_e`; the library fetches the full directive over HTTP, fires `ReportState` or `Event` (and `DirectiveReceived`, if registered), and your status report is queued for an HTTP POST that the backend republishes on `<root>/<endpointId>/alexaResponce`, replacing `{REPLACE_WITH_DATETIME}` with the current time.
- **Limits.** The send queue holds 5 reports of up to 2047 bytes each. `send()` returns `false` and prints a `[Alex2ESP]` line on Serial when a report does not fit or the queue is full; nothing is ever sent truncated. Debug logging of the library's internals is compiled in by defining `Alex2ESP_DEBUG` in `AlexaUtils.cpp`; credentials are never printed.
---
## Interface Types
| Alexa Interface Type | Status |
|-----------------------------------------------------|-------------|
| AlexaInterfaceType::ENDPOINT_HEALTH | Fully Supported |
| AlexaInterfaceType::POWER_CONTROLLER | Fully Supported |
| AlexaInterfaceType::BRIGHTNESS_CONTROLLER | Fully Supported |
| AlexaInterfaceType::TOGGLE_CONTROLLER | Fully Supported |
| AlexaInterfaceType::TEMPERATURE_SENSOR | Fully Supported |
| AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER | Fully Supported |
| AlexaInterfaceType::AUTOMATION_MANAGEMENT | Supported* |
| AlexaInterfaceType::CHANNEL_CONTROLLER | Supported* |
| AlexaInterfaceType::COLOR_CONTROLLER | Supported* |
| AlexaInterfaceType::CONTACT_SENSOR | Supported* |
| AlexaInterfaceType::APPLICATION_STATE_REPORTER | Supported* |
| AlexaInterfaceType::AUDIO_PLAY_QUEUE | Supported* |
| AlexaInterfaceType::AUTHORIZATION_CONTROLLER | Supported* |
| AlexaInterfaceType::AUTOMOTIVE_VEHICLE_DATA | Supported* |
| AlexaInterfaceType::CAMERA_LIVE_VIEW_CONTROLLER | Supported* |
| AlexaInterfaceType::CAMERA_STREAM_CONTROLLER | Supported* |
| AlexaInterfaceType::COMMISSIONABLE | Supported* |
| AlexaInterfaceType::CONSENT_MANAGEMENT_CONSENT_REQUIRED_REPORTER | Supported* |
| AlexaInterfaceType::COOKING | Supported* |
| AlexaInterfaceType::DATA_CONTROLLER | Supported* |
| AlexaInterfaceType::DEVICE_USAGE_ESTIMATION | Supported* |
| AlexaInterfaceType::DEVICE_USAGE_METER | Supported* |
| AlexaInterfaceType::DOORBELL_EVENT_SOURCE | Supported* |
| AlexaInterfaceType::EQUALIZER_CONTROLLER | Supported* |
| AlexaInterfaceType::INPUT_CONTROLLER | Supported* |
| AlexaInterfaceType::INVENTORY_LEVEL_SENSOR | Supported* |
| AlexaInterfaceType::INVENTORY_LEVEL_USAGE_SENSOR | Supported* |
| AlexaInterfaceType::INVENTORY_USAGE_SENSOR | Supported* |
| AlexaInterfaceType::KEYPAD_CONTROLLER | Supported* |
| AlexaInterfaceType::LAUNCHER | Supported* |
| AlexaInterfaceType::LOCK_CONTROLLER | Supported* |
| AlexaInterfaceType::MEDIA_PLAYBACK | Supported* |
| AlexaInterfaceType::MEDIA_SEARCH | Supported* |
| AlexaInterfaceType::MODE_CONTROLLER | Supported* |
| AlexaInterfaceType::MOTION_SENSOR | Supported* |
| AlexaInterfaceType::PERCENTAGE_CONTROLLER | Supported* |
| AlexaInterfaceType::PLAYBACK_CONTROLLER | Supported* |
| AlexaInterfaceType::PLAYBACK_STATE_REPORTER | Supported* |
| AlexaInterfaceType::PROACTIVE_NOTIFICATION_SOURCE | Supported* |
| AlexaInterfaceType::RANGE_CONTROLLER | Supported* |
| AlexaInterfaceType::RECORD_CONTROLLER | Supported* |
| AlexaInterfaceType::REMOTE_VIDEO_PLAYER | Supported* |
| AlexaInterfaceType::RTC_SESSION_CONTROLLER | Supported* |
| AlexaInterfaceType::SCENE_CONTROLLER | Supported* |
| AlexaInterfaceType::SECURITY_PANEL_CONTROLLER | Supported* |
| AlexaInterfaceType::SEEK_CONTROLLER | Supported* |
| AlexaInterfaceType::SIMPLE_EVENT_SOURCE | Supported* |
| AlexaInterfaceType::SMART_VISION_OBJECT_DETECTION_SENSOR | Supported* |
| AlexaInterfaceType::SMART_VISION_SNAPSHOT_PROVIDER | Supported* |
| AlexaInterfaceType::SPEAKER | Supported* |
| AlexaInterfaceType::STEP_SPEAKER | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_CONFIGURATION | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_HVAC_COMPONENTS | Supported* |
| AlexaInterfaceType::THERMOSTAT_CONTROLLER_SCHEDULE | Supported* |
| AlexaInterfaceType::TIME_HOLD_CONTROLLER | Supported* |
| AlexaInterfaceType::UI_CONTROLLER | Supported* |
| AlexaInterfaceType::USER_PREFERENCE | Supported* |
| AlexaInterfaceType::VIDEO_RECORDER | Supported* |
| AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER | Supported* |
(*Partial support or limited implementation advanced configuration is required)
---
## Advanced usage
For devices with limited or partial support, Alex2ESP provides functions that allow you to attach custom JSON objects to the status report. For example, to report the status of a PowerController type, you can use the `AddPowerControllerProp` function. However, if a specific "add props" function does not exist for your use case, you can utilize the `AddContextProp` function to pass a custom JSON object.
For instance, to manually report the state of a PowerController, you can use the following code:
```cpp
JsonDocument doc;
doc["namespace"] = "Alexa.PowerController";
doc["name"] = "powerState";
doc["value"] = "ON";
doc["timeOfSample"] = "{REPLACE_WITH_DATETIME}"; // Alex2MQTT server wil do the replace
doc["uncertaintyInMilliseconds"] = 0;
AddContextProp(doc.as<JsonObject>())
```
---
## Changelog
### 1.1.0
Behaviour changes:
- `begin()` now takes `(username, password, rootTopic)`, the order every example and this readme always used (and the order alex2node uses). 1.0.0 declared `(rootTopic, username, password)`, so a sketch written from the examples could not authenticate with the broker.
- Discovery answers go straight to MQTT (`<root>/discover_r`), one object per device, all devices at once; if the MQTT client cannot take another object (free heap under 4 KB) the rest is sent from `loop()` as the client's queue drains, for up to 5 s. 1.0.0 pushed them one per `loop()` through a 5-slot HTTP queue and silently dropped the sixth device onwards, which the backend then removed from Alexa.
- `AlexaStatusMessage::send()` returns `bool`: an oversized report (over 2047 bytes) or a full queue is reported on Serial and dropped instead of being sent truncated. `AlexaUtils::enqueue()` refuses packets that would not fit instead of truncating them.
- Discovery JSON: `semantics` is emitted only when a capability has action mappings; an `ActionMapping` directive payload is emitted as a JSON object (parsed from the text you pass) or omitted when empty, instead of the string `"{}"`.
- `Response`/`StateReport` events include the required `"payload": {}`.
- Devices and capabilities are stored in `std::deque`; pointers from `getDevice()`/`addCapability()` no longer dangle once another device or capability is added.
- The MQTT payload is copied using its length (no write past the client's buffer); fragmented directive ids are ignored (a Discover is answered on its first fragment, its payload is unused).
- `begin()` no longer prints the MQTT credentials to Serial in debug builds.
- `DirectiveReceived` no longer prints "No event registered" when the sketch has not registered it.
- Removed: `Alex2ESP::messageSplitAndSend` (declared, never defined) and `AlexaStatusMessage::setEndpointId` (did nothing).
- Reported `softwareVersion`/`firmwareVersion` in the discovery attributes are now `1.1.0`.
Examples: `WiFi.begin(WIFI_SSID, WIFI_PASSWORD)` (1.0.0 could only join open networks), `LED_BUILTIN` fallback, fixed brightness/colour-temperature Serial output, blinds use `DisplayCategory::INTERIOR_BLIND`.
Packaging: `library.json` and `library.properties` restored with the Forgejo URL and the dependencies, `LICENSE` (MIT), a real `keywords.txt`, LF line endings (`.gitattributes`), ArduinoJson 7 deprecated calls replaced (warning-free build). ESP8266 is the supported target; ESP32 is untested.
---
## Contributing
Feel free to submit pull requests or issues for feature requests and bug fixes.
---
## License
This project is licensed under the MIT License. See the LICENSE file for details.

View file

@ -1,389 +1,441 @@
/*
* @title Alex2ESP Library
* @version 1.0.0
* @author David
* @license MIT
* @contributors chaos511
*
* @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill,
* providing seamless integration between ESP-based devices and the Alex2MQTT server.
* This library connects to alex2mqtt.stormysdream.club, where the skill is hosted,
* allowing your devices to communicate effortlessly with the Alexa Voice Service
* using MQTT as the backbone.
*/
#include "Alex2ESP.h"
Alex2ESP::Alex2ESP()
: rootTopic(), mqttUsername(nullptr), mqttPassword(nullptr), _state(Alex2ESPState::UNINITIALIZED), _disconnectReason(AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {}
void Alex2ESP::begin(const char *rootTopic, const char *username, const char *password)
{
_state = Alex2ESPState::INITIALIZED;
_disconnectReason = AsyncMqttClientDisconnectReason::TCP_DISCONNECTED;
this->rootTopic = rootTopic;
this->mqttUsername = username;
this->mqttPassword = password;
discoverTopic = (String(this->rootTopic) + String("/discover"));
discoverTopicSend = (String(this->rootTopic) + String("/discover_r"));
TopicESP = (String(this->rootTopic) + String("/+/alexaDirective_e"));
// Log the root topic and credentials
AlexaUtils::log("Setting root topic: ");
AlexaUtils::log(rootTopic);
AlexaUtils::log(" with creds ");
AlexaUtils::log(username);
AlexaUtils::log(":");
AlexaUtils::logln(password);
AlexaUtils::printMemoryInfo();
// Set up MQTT client callbacks
mqttClient.onConnect([this](bool sessionPresent)
{ this->onMqttConnect(sessionPresent); });
mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason)
{ this->onMqttDisconnect(reason); });
mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos)
{ this->onSubscribe(packetId, qos); });
mqttClient.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total)
{ this->onMessage(topic, payload, properties, len, index, total); });
// Configure MQTT client
mqttClient.setServer(mqttServer, mqttPort);
mqttClient.setCredentials(username, password);
// TODO: Throw error if dns fails or has no internet access?
_state = Alex2ESPState::CONNECTING;
mqttClient.connect();
}
void Alex2ESP::onMqttConnect(bool sessionPresent)
{
_state = Alex2ESPState::SUBSCRIBING;
AlexaUtils::log("Connected to MQTT server, Now subscribing to: ");
AlexaUtils::log(discoverTopic.c_str());
AlexaUtils::log(" ");
AlexaUtils::logln(TopicESP.c_str());
mqttClient.subscribe(discoverTopic.c_str(), 1);
mqttClient.subscribe(TopicESP.c_str(), 1);
}
void Alex2ESP::onSubscribe(uint16_t packetId, uint8_t qos)
{
_state = Alex2ESPState::CONNECTED;
AlexaUtils::log("Subscribed with packetId: ");
AlexaUtils::log(packetId);
AlexaUtils::log(" and QoS: ");
AlexaUtils::logln(qos);
}
// Internal: Handle disconnection
// TODO: auto reconnect?
void Alex2ESP::onMqttDisconnect(AsyncMqttClientDisconnectReason reason)
{
_state = Alex2ESPState::DISCONNECTED;
_disconnectReason = reason;
}
void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total)
{
AlexaUtils::logln("OnMessage Start");
AlexaUtils::printMemoryInfo();
clearToSend = false;
if (strcmp(topic, discoverTopic.c_str()) == 0)
{
// boolean successful = messagePackExtractor(topic, payload, length, index, total);
// if (successful && inputDoc["name"] == "Discover")
// {
for (const AlexaDevice &device : devices)
{
AlexaUtils::logln("Getting device json");
JsonDocument jsonData = device.getDeviceJSON();
String jsonString;
serializeJson(jsonData, jsonString);
jsonData.clear();
AlexaUtils::enqueue(jsonString.c_str(), discoverTopicSend.c_str());
}
// }
}
else
{
for (auto &device : devices)
{
String directiveTopic = String(rootTopic) + "/" + device.getEndpointId() + "/alexaDirective_e";
if (strcmp(topic, directiveTopic.c_str()) == 0)
{
AlexaUtils::logln(topic);
payload[length] = '\0';
AlexaUtils::logln(payload);
AlexaUtils::enqueueReceive(payload);
}
}
}
AlexaUtils::logln("OnMessage End");
AlexaUtils::printMemoryInfo();
clearToSend = true;
}
// boolean Alex2ESP::messagePackExtractor(char *topic, char *payload, size_t length, size_t index, size_t total)
// {
// payload[length] = '\0'; // Truncate the payload
// uint8_t messageId = payload[0]; // Message ID (byte 0)
// uint8_t fragmentId = payload[1]; // Fragment ID (byte 1)
// uint8_t totalFragments = payload[2]; // Total Fragments (byte 2)
// if (totalFragments > MAX_FRAGMENT_COUNT)
// {
// return false; //Message too long, skip.
// }
// if (fragmentId == 0)
// {
// clearCache();
// cache.totalParts = totalFragments;
// cache.messageId = String(messageId);
// }
// if (cache.messageId != String(messageId))
// {
// clearCache();
// cache.messageId = String(messageId);
// cache.totalParts = totalFragments;
// }
// if (fragmentId < cache.totalParts)
// {
// strncpy(cache.message + cache.fragmentOffset, payload + 3, length - 3); // Copy the fragment data
// cache.fragmentOffset=cache.fragmentOffset+(length-3); //
// cache.receivedParts++;
// Serial.printf("Received fragment %d of %d, length %d receivedParts: %d \n", fragmentId + 1, totalFragments, length, cache.receivedParts);
// if (isMessageComplete())
// {
// AlexaUtils::logln("Full message received.");
// AlexaUtils::logln(cache.message);
// DeserializationError error = deserializeJson(inputDoc, cache.message);
// if (error)
// {
// AlexaUtils::log("Failed to parse message: ");
// AlexaUtils::logln(String(error.f_str()));
// clearCache();
// return false;
// }
// else
// {
// AlexaUtils::logln("JSON message parsed successfully!");
// clearCache(true);
// return true;
// }
// }
// }
// return false;
// }
AlexaDevice *Alex2ESP::getDevice(const String &name, const String &endpointId)
{
// Check if a device with the given endpointId already exists
for (auto &device : devices)
{
if (device.getEndpointId() == endpointId)
{
return &device; // Return the existing device
}
}
// If the device doesn't exist, create a new one
devices.emplace_back(name, rootTopic, endpointId);
Serial.print("Created new device: ");
Serial.print(name);
Serial.print(" with endpointId: ");
Serial.println(endpointId);
// Return a pointer to the newly created device
return &devices.back();
}
// void Alex2ESP::clearCache(boolean skipDocClear)
// {
// if (!skipDocClear)
// {
// inputDoc.clear();
// }
// cache.receivedParts = 0; // Reset the count of received parts
// cache.messageId = ""; // Clear the message ID
// cache.totalParts = 0; // Reset the total parts count
// cache.fragmentOffset = 0;
// memset(cache.message, 0, sizeof(cache.message)); // Clear the message buffer
// }
// bool Alex2ESP::isMessageComplete()
// {
// return cache.receivedParts == cache.totalParts; // Check if we have received all parts
// }
Alex2ESPState Alex2ESP::getState() const
{
return _state;
}
AsyncMqttClientDisconnectReason Alex2ESP::getDisconnectReason() const
{
return _disconnectReason;
}
void Alex2ESP::handleMqttReconnection()
{
if (!mqttClient.connected() && _disconnectReason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED)
{
if (millis() - lastReconnectTime > 5000)
{
lastReconnectTime = millis();
reconnectAttempt++;
mqttClient.connect();
}
}
}
void Alex2ESP::processHttpPost()
{
if (!AlexaUtils::isQueueEmpty())
{
AlexaUtils::logln("processHttpPost");
const char *payload = AlexaUtils::dequeueVals(false);
httpPOST.begin(wifiPOST, "http://alex2mqtt.stormysdream.club/Alex2ESP");
httpPOST.setAuthorization(mqttUsername, mqttPassword);
httpPOST.addHeader("Content-Type", "text/plain");
AlexaUtils::log("Sending Data: ");
AlexaUtils::logln(payload);
int httpResponseCode = httpPOST.POST(payload);
if (httpResponseCode != 200)
{
if (retryCountPOST >= MAX_RETRY_COUNT)
{
AlexaUtils::dequeueVals(true);
}
AlexaUtils::log("POST Error code: ");
AlexaUtils::logln(httpResponseCode);
retryCountPOST++;
if (httpResponseCode > 0)
{
String response = httpPOST.getString();
Serial.println("Response:");
Serial.println(response);
}
}
else
{
retryCountPOST = 0;
AlexaUtils::dequeueVals(true);
}
httpPOST.end();
}
}
void Alex2ESP::processHttpGet()
{
if (!AlexaUtils::isReceiveQueueEmpty())
{
AlexaUtils::logln("processHttpGet");
String uuid;
AlexaUtils::dequeueReceive(uuid, false);
AlexaUtils::log("Sending get request for uuid: ");
// AlexaUtils::logln(uuid);
httpGET.begin(wifiGET, "http://alex2mqtt.stormysdream.club/Alex2ESP/" + uuid);
httpGET.setAuthorization(mqttUsername, mqttPassword);
int httpResponseCode = httpGET.GET();
Serial.print("HTTP Response code: ");
Serial.println(httpResponseCode);
if (httpResponseCode != 200)
{
Serial.print("Error code: ");
Serial.println(httpResponseCode);
if (retryCountGET >= MAX_RETRY_COUNT)
{
AlexaUtils::dequeueReceive(uuid, true);
}
retryCountGET++;
}
else
{
retryCountGET = 0;
AlexaUtils::dequeueReceive(uuid, true);
String payloadStr = httpGET.getString();
size_t length = payloadStr.length();
if (length < AlexaUtils::MAX_PAYLOAD_LENGTH - 1)
{
payloadStr.toCharArray(AlexaUtils::receivePayload, length + 1);
payloadStr = "";
AlexaUtils::receivePayload[length + 1] = '\0';
// AlexaUtils::logln(AlexaUtils::receivePayload);
DeserializationError error = deserializeJson(inputDoc, AlexaUtils::receivePayload);
if (error)
{
AlexaUtils::log("Failed to parse message: ");
AlexaUtils::logln(String(error.f_str()));
inputDoc.clear();
}
else
{
AlexaUtils::logln("JSON message parsed successfully!");
for (auto &device : devices)
{
if (strcmp(device.getEndpointId().c_str(), inputDoc["directive"]["endpoint"]["endpointId"]) == 0)
{
serializeJson(inputDoc, Serial);
Serial.println();
device.triggerEvent("DirectiveReceived", inputDoc["directive"], AlexaInterfaceType::UNKNOWN);
if (inputDoc["directive"]["header"]["name"] == "ReportState")
{
device.triggerEvent("ReportState", inputDoc["directive"], AlexaInterfaceType::UNKNOWN);
}
else
{
device.triggerEvent("Event", inputDoc["directive"], AlexaInterfaceUtils::fromString(inputDoc["directive"]["header"]["namespace"]));
}
}
}
}
}
}
httpGET.end();
}
}
void Alex2ESP::loop()
{
handleMqttReconnection();
if(clearToSend){
processHttpPost();
processHttpGet();
}
return;
}
/*
* @title Alex2ESP Library
* @version 1.1.0
* @author David
* @license MIT
* @contributors chaos511
*
* @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill,
* providing seamless integration between ESP-based devices and the Alex2MQTT server.
* This library connects to alex2mqtt.stormysdream.club, where the skill is hosted,
* allowing your devices to communicate effortlessly with the Alexa Voice Service
* using MQTT as the backbone.
*/
#include "Alex2ESP.h"
Alex2ESP::Alex2ESP()
: rootTopic(), mqttUsername(nullptr), mqttPassword(nullptr), lastReconnectTime(0), reconnectAttempt(0), _state(Alex2ESPState::UNINITIALIZED), _disconnectReason(AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {}
void Alex2ESP::begin(const char *username, const char *password, const char *rootTopic)
{
_state = Alex2ESPState::INITIALIZED;
_disconnectReason = AsyncMqttClientDisconnectReason::TCP_DISCONNECTED;
this->rootTopic = rootTopic;
this->mqttUsername = username;
this->mqttPassword = password;
discoverTopic = (String(this->rootTopic) + String("/discover"));
discoverTopicSend = (String(this->rootTopic) + String("/discover_r"));
TopicESP = (String(this->rootTopic) + String("/+/alexaDirective_e"));
// Log the root topic (never the credentials)
AlexaUtils::log("Setting root topic: ");
AlexaUtils::logln(rootTopic);
AlexaUtils::printMemoryInfo();
// Set up MQTT client callbacks
mqttClient.onConnect([this](bool sessionPresent)
{ this->onMqttConnect(sessionPresent); });
mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason)
{ this->onMqttDisconnect(reason); });
mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos)
{ this->onSubscribe(packetId, qos); });
mqttClient.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total)
{ this->onMessage(topic, payload, properties, len, index, total); });
// Configure MQTT client
mqttClient.setServer(mqttServer, mqttPort);
mqttClient.setCredentials(username, password);
// TODO: Throw error if dns fails or has no internet access?
_state = Alex2ESPState::CONNECTING;
mqttClient.connect();
}
void Alex2ESP::onMqttConnect(bool sessionPresent)
{
_state = Alex2ESPState::SUBSCRIBING;
AlexaUtils::log("Connected to MQTT server, Now subscribing to: ");
AlexaUtils::log(discoverTopic.c_str());
AlexaUtils::log(" ");
AlexaUtils::logln(TopicESP.c_str());
mqttClient.subscribe(discoverTopic.c_str(), 1);
mqttClient.subscribe(TopicESP.c_str(), 1);
}
void Alex2ESP::onSubscribe(uint16_t packetId, uint8_t qos)
{
_state = Alex2ESPState::CONNECTED;
AlexaUtils::log("Subscribed with packetId: ");
AlexaUtils::log(packetId);
AlexaUtils::log(" and QoS: ");
AlexaUtils::logln(qos);
}
// Internal: Handle disconnection
// TODO: auto reconnect?
void Alex2ESP::onMqttDisconnect(AsyncMqttClientDisconnectReason reason)
{
_state = Alex2ESPState::DISCONNECTED;
_disconnectReason = reason;
}
void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t length, size_t index, size_t total)
{
AlexaUtils::logln("OnMessage Start");
AlexaUtils::printMemoryInfo();
clearToSend = false;
if (strcmp(topic, discoverTopic.c_str()) == 0)
{
// The payload is unused: answer once per message even when TCP split it, starting over if an answer is still going out
if (index == 0)
{
discoveryNext = 0;
discoveryPending = false;
discoveryStarted = millis();
publishDiscovery();
}
}
else if (index != 0 || total != length)
{
// AsyncMqttClient hands a large publish over in fragments; a directive id never is, so a fragment can only be part of one
AlexaUtils::logln("Ignoring fragmented directive id");
}
else
{
// The payload is not NUL-terminated and belongs to the MQTT client: copy exactly `length` bytes out of it
String uuid;
uuid.concat(payload, length);
for (auto &device : devices)
{
String directiveTopic = String(rootTopic) + "/" + device.getEndpointId() + "/alexaDirective_e";
if (strcmp(topic, directiveTopic.c_str()) == 0)
{
AlexaUtils::logln(topic);
AlexaUtils::logln(uuid);
if (!AlexaUtils::enqueueReceive(uuid.c_str()))
{
Serial.println("[Alex2ESP] receive queue full, directive dropped");
}
}
}
}
AlexaUtils::logln("OnMessage End");
AlexaUtils::printMemoryInfo();
clearToSend = true;
}
// Answer a Discover: one discovery object per device, published straight to <root>/discover_r over MQTT (async, no
// HTTP round trip, no queue slot). The backend accepts one endpoint object per message and collects everything that
// arrives within 1 s for Alexa's answer (5 s for its proactive push). publish() copies the object into the client's
// out-queue, where it stays on the heap until the broker has acknowledged it, and returns 0 once the free heap drops
// under 4 KB (or the client is not connected) - so rather than skip that device we stop, remember where we got to and
// let loop() carry on once the queue has drained, inside the backend's 5 s window.
void Alex2ESP::publishDiscovery()
{
while (discoveryNext < devices.size())
{
const AlexaDevice &device = devices[discoveryNext];
AlexaUtils::logln("Getting device json");
JsonDocument jsonData = device.getDeviceJSON();
String jsonString;
serializeJson(jsonData, jsonString);
jsonData.clear();
if (mqttClient.publish(discoverTopicSend.c_str(), 0, false, jsonString.c_str(), jsonString.length()) == 0)
{
if (!discoveryPending)
{
Serial.print("[Alex2ESP] discovery publish deferred at ");
Serial.println(device.getEndpointId());
}
discoveryPending = true;
return;
}
discoveryNext++;
}
discoveryNext = 0;
discoveryPending = false;
}
// Finish a discovery answer that publishDiscovery() had to cut short, or give it up once the backend has stopped listening
void Alex2ESP::finishDiscovery()
{
if (!discoveryPending)
{
return;
}
if (millis() - discoveryStarted > DISCOVERY_WINDOW_MS)
{
Serial.printf("[Alex2ESP] discovery gave up: %u device(s) not announced\n", (unsigned)(devices.size() - discoveryNext));
discoveryNext = 0;
discoveryPending = false;
}
else if (mqttClient.connected())
{
publishDiscovery();
}
}
// boolean Alex2ESP::messagePackExtractor(char *topic, char *payload, size_t length, size_t index, size_t total)
// {
// payload[length] = '\0'; // Truncate the payload
// uint8_t messageId = payload[0]; // Message ID (byte 0)
// uint8_t fragmentId = payload[1]; // Fragment ID (byte 1)
// uint8_t totalFragments = payload[2]; // Total Fragments (byte 2)
// if (totalFragments > MAX_FRAGMENT_COUNT)
// {
// return false; //Message too long, skip.
// }
// if (fragmentId == 0)
// {
// clearCache();
// cache.totalParts = totalFragments;
// cache.messageId = String(messageId);
// }
// if (cache.messageId != String(messageId))
// {
// clearCache();
// cache.messageId = String(messageId);
// cache.totalParts = totalFragments;
// }
// if (fragmentId < cache.totalParts)
// {
// strncpy(cache.message + cache.fragmentOffset, payload + 3, length - 3); // Copy the fragment data
// cache.fragmentOffset=cache.fragmentOffset+(length-3); //
// cache.receivedParts++;
// Serial.printf("Received fragment %d of %d, length %d receivedParts: %d \n", fragmentId + 1, totalFragments, length, cache.receivedParts);
// if (isMessageComplete())
// {
// AlexaUtils::logln("Full message received.");
// AlexaUtils::logln(cache.message);
// DeserializationError error = deserializeJson(inputDoc, cache.message);
// if (error)
// {
// AlexaUtils::log("Failed to parse message: ");
// AlexaUtils::logln(String(error.f_str()));
// clearCache();
// return false;
// }
// else
// {
// AlexaUtils::logln("JSON message parsed successfully!");
// clearCache(true);
// return true;
// }
// }
// }
// return false;
// }
AlexaDevice *Alex2ESP::getDevice(const String &name, const String &endpointId)
{
// Check if a device with the given endpointId already exists
for (auto &device : devices)
{
if (device.getEndpointId() == endpointId)
{
return &device; // Return the existing device
}
}
// If the device doesn't exist, create a new one
devices.emplace_back(name, rootTopic, endpointId);
Serial.print("Created new device: ");
Serial.print(name);
Serial.print(" with endpointId: ");
Serial.println(endpointId);
// Return a pointer to the newly created device
return &devices.back();
}
// void Alex2ESP::clearCache(boolean skipDocClear)
// {
// if (!skipDocClear)
// {
// inputDoc.clear();
// }
// cache.receivedParts = 0; // Reset the count of received parts
// cache.messageId = ""; // Clear the message ID
// cache.totalParts = 0; // Reset the total parts count
// cache.fragmentOffset = 0;
// memset(cache.message, 0, sizeof(cache.message)); // Clear the message buffer
// }
// bool Alex2ESP::isMessageComplete()
// {
// return cache.receivedParts == cache.totalParts; // Check if we have received all parts
// }
Alex2ESPState Alex2ESP::getState() const
{
return _state;
}
AsyncMqttClientDisconnectReason Alex2ESP::getDisconnectReason() const
{
return _disconnectReason;
}
void Alex2ESP::handleMqttReconnection()
{
if (!mqttClient.connected() && _disconnectReason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED)
{
if (millis() - lastReconnectTime > 5000)
{
lastReconnectTime = millis();
reconnectAttempt++;
mqttClient.connect();
}
}
}
void Alex2ESP::processHttpPost()
{
if (!AlexaUtils::isQueueEmpty())
{
AlexaUtils::logln("processHttpPost");
const char *payload = AlexaUtils::dequeueVals(false);
httpPOST.begin(wifiPOST, "http://alex2mqtt.stormysdream.club/Alex2ESP");
httpPOST.setAuthorization(mqttUsername, mqttPassword);
httpPOST.addHeader("Content-Type", "text/plain");
AlexaUtils::log("Sending Data: ");
AlexaUtils::logln(payload);
int httpResponseCode = httpPOST.POST(payload);
if (httpResponseCode != 200)
{
if (retryCountPOST >= MAX_RETRY_COUNT)
{
AlexaUtils::dequeueVals(true);
}
AlexaUtils::log("POST Error code: ");
AlexaUtils::logln(httpResponseCode);
retryCountPOST++;
if (httpResponseCode > 0)
{
String response = httpPOST.getString();
Serial.println("Response:");
Serial.println(response);
}
}
else
{
retryCountPOST = 0;
AlexaUtils::dequeueVals(true);
}
httpPOST.end();
}
}
void Alex2ESP::processHttpGet()
{
if (!AlexaUtils::isReceiveQueueEmpty())
{
AlexaUtils::logln("processHttpGet");
String uuid;
AlexaUtils::dequeueReceive(uuid, false);
AlexaUtils::log("Sending get request for uuid: ");
// AlexaUtils::logln(uuid);
httpGET.begin(wifiGET, "http://alex2mqtt.stormysdream.club/Alex2ESP/" + uuid);
httpGET.setAuthorization(mqttUsername, mqttPassword);
int httpResponseCode = httpGET.GET();
Serial.print("HTTP Response code: ");
Serial.println(httpResponseCode);
if (httpResponseCode != 200)
{
Serial.print("Error code: ");
Serial.println(httpResponseCode);
if (retryCountGET >= MAX_RETRY_COUNT)
{
AlexaUtils::dequeueReceive(uuid, true);
}
retryCountGET++;
}
else
{
retryCountGET = 0;
AlexaUtils::dequeueReceive(uuid, true);
String payloadStr = httpGET.getString();
size_t length = payloadStr.length();
if (length < (size_t)(AlexaUtils::MAX_PAYLOAD_LENGTH - 1))
{
payloadStr.toCharArray(AlexaUtils::receivePayload, length + 1);
payloadStr = "";
AlexaUtils::receivePayload[length + 1] = '\0';
// AlexaUtils::logln(AlexaUtils::receivePayload);
DeserializationError error = deserializeJson(inputDoc, AlexaUtils::receivePayload);
if (error)
{
AlexaUtils::log("Failed to parse message: ");
AlexaUtils::logln(String(error.f_str()));
inputDoc.clear();
}
else
{
AlexaUtils::logln("JSON message parsed successfully!");
for (auto &device : devices)
{
if (strcmp(device.getEndpointId().c_str(), inputDoc["directive"]["endpoint"]["endpointId"] | "") == 0)
{
serializeJson(inputDoc, Serial);
Serial.println();
device.triggerEvent("DirectiveReceived", inputDoc["directive"], AlexaInterfaceType::UNKNOWN, false);
if (inputDoc["directive"]["header"]["name"] == "ReportState")
{
device.triggerEvent("ReportState", inputDoc["directive"], AlexaInterfaceType::UNKNOWN);
}
else
{
device.triggerEvent("Event", inputDoc["directive"], AlexaInterfaceUtils::fromString(inputDoc["directive"]["header"]["namespace"]));
}
}
}
}
}
}
httpGET.end();
}
}
void Alex2ESP::loop()
{
handleMqttReconnection();
finishDiscovery();
if(clearToSend){
processHttpPost();
processHttpGet();
}
return;
}

View file

@ -1,104 +1,115 @@
/*
* @title Alex2ESP Library
* @version 1.0.0
* @author David
* @license MIT
* @contributors chaos511
*
* @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill,
* providing seamless integration between ESP-based devices and the Alex2MQTT server.
* This library connects to alex2mqtt.stormysdream.club, where the skill is hosted,
* allowing your devices to communicate effortlessly with the Alexa Voice Service
* using MQTT as the backbone.
*/
#ifndef ALEX2ESP_H
#define ALEX2ESP_H
class AlexaDevice;
#include <Arduino.h>
#include <AsyncMqttClient.h>
#include <ArduinoJson.h>
#include "AlexaDevice.h"
#include <AlexaInterface.h>
#include <vector>
#include <AlexaUtils.h>
#include <ESP8266HTTPClient.h>
enum class Alex2ESPState
{
UNINITIALIZED,
INITIALIZED,
CONNECTING,
SUBSCRIBING,
CONNECTED,
DISCONNECTED
};
class Alex2ESP
{
public:
// Constructor
Alex2ESP();
// Begin function for initialization
void begin(const char *rootTopic, const char *username, const char *password);
Alex2ESPState getState() const;
void loop();
AsyncMqttClientDisconnectReason getDisconnectReason() const;
AlexaDevice *getDevice(const String &name, const String &endpointId);
void messageSplitAndSend(String messagepackString, String topic);
private:
static const int MAX_RETRY_COUNT = 2; // Define maximum retry count
int retryCountPOST=0;
int retryCountGET=0;
boolean clearToSend=false;
AsyncMqttClient mqttClient; // MQTT client instance
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 TopicESP; // The topic we listen on for esp messages
const char *mqttUsername; // Username for authentication
const char *mqttPassword; // Password for authentication
unsigned long lastReconnectTime;
int reconnectAttempt;
HTTPClient httpGET;
HTTPClient httpPOST;
WiFiClient wifiGET;
WiFiClient wifiPOST;
JsonDocument inputDoc;
std::vector<AlexaDevice> devices; // Collection of devices
Alex2ESPState _state;
AsyncMqttClientDisconnectReason _disconnectReason;
// MQTT connection details
const char *mqttServer = "alex2mqtt.stormysdream.club";
uint16_t mqttPort = 1883;
// Internal event handlers
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 handleMqttReconnection();
void processHttpPost();
void processHttpGet();
};
#endif // ALEX2ESP_H
/*
* @title Alex2ESP Library
* @version 1.1.0
* @author David
* @license MIT
* @contributors chaos511
*
* @description The Alex2ESP library is a companion to the Alex2MQTT Alexa Skill,
* providing seamless integration between ESP-based devices and the Alex2MQTT server.
* This library connects to alex2mqtt.stormysdream.club, where the skill is hosted,
* allowing your devices to communicate effortlessly with the Alexa Voice Service
* using MQTT as the backbone.
*/
#ifndef ALEX2ESP_H
#define ALEX2ESP_H
#include <Arduino.h>
#include <AsyncMqttClient.h>
#include <ArduinoJson.h>
#include "AlexaDevice.h"
#include "AlexaInterface.h"
#include "AlexaUtils.h"
#include <deque>
#ifdef ESP32
#include <HTTPClient.h> // ESP32: untested
#else
#include <ESP8266HTTPClient.h>
#endif
enum class Alex2ESPState
{
UNINITIALIZED,
INITIALIZED,
CONNECTING,
SUBSCRIBING,
CONNECTED,
DISCONNECTED
};
class Alex2ESP
{
public:
// Constructor
Alex2ESP();
// Begin function for initialization: MQTT username, MQTT password, root topic (the same order as alex2node)
void begin(const char *username, const char *password, const char *rootTopic);
Alex2ESPState getState() const;
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: devices live in a std::deque, which never relocates its elements when another one is added.
AlexaDevice *getDevice(const String &name, const String &endpointId);
private:
static const int MAX_RETRY_COUNT = 2; // Define maximum retry count
static const unsigned long DISCOVERY_WINDOW_MS = 5000; // How long the backend keeps collecting a discovery answer
int retryCountPOST=0;
int retryCountGET=0;
size_t discoveryNext = 0; // Next device to announce while a discovery answer is still going out
bool discoveryPending = false; // publishDiscovery() stopped early (client out-queue full); loop() finishes it
unsigned long discoveryStarted = 0; // millis() when the Discover arrived
boolean clearToSend=false;
AsyncMqttClient mqttClient; // MQTT client instance
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 TopicESP; // The topic we listen on for esp messages
const char *mqttUsername; // Username for authentication
const char *mqttPassword; // Password for authentication
unsigned long lastReconnectTime;
int reconnectAttempt;
HTTPClient httpGET;
HTTPClient httpPOST;
WiFiClient wifiGET;
WiFiClient wifiPOST;
JsonDocument inputDoc;
std::deque<AlexaDevice> devices; // Collection of devices (deque: pointers handed out by getDevice stay valid)
Alex2ESPState _state;
AsyncMqttClientDisconnectReason _disconnectReason;
// MQTT connection details
const char *mqttServer = "alex2mqtt.stormysdream.club";
uint16_t mqttPort = 1883;
// Internal event handlers
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 handleMqttReconnection();
void publishDiscovery();
void finishDiscovery();
void processHttpPost();
void processHttpGet();
};
#endif // ALEX2ESP_H

View file

@ -1,164 +1,166 @@
#include "AlexaDevice.h"
AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const String& endpointId)
: name(name), endpointId(endpointId), rootTopic(rootTopic) {
// Initialize event arrays to nullptr
for (int i = 0; i < MAX_EVENTS; ++i) {
eventNames[i] = nullptr; // Set all event names to nullptr
eventCallbacks[i] = nullptr; // Set all callbacks to nullptr
}
}
// Setters and Getters
void AlexaDevice::setName(const String& name) {
this->name = name;
}
String AlexaDevice::getName() const {
return name;
}
String AlexaDevice::getEndpointId() const {
return endpointId;
}
DisplayCategory AlexaDevice::getDisplayCategory() const {
return displayCategory;
}
void AlexaDevice::setDisplayCategory(DisplayCategory category) {
displayCategory = category;
}
String AlexaDevice::getDescription() const {
return description;
}
void AlexaDevice::setDescription(const String& description) {
this->description = description;
}
String AlexaDevice::getManufacturerName() const {
return manufacturerName;
}
void AlexaDevice::setManufacturerName(const String& manufacturerName) {
this->manufacturerName = manufacturerName;
}
String AlexaDevice::getManufacturer() const {
return manufacturer;
}
void AlexaDevice::setManufacturer(const String& manufacturer) {
this->manufacturer = manufacturer;
}
String AlexaDevice::getModel() const {
return model;
}
void AlexaDevice::setModel(const String& model) {
this->model = model;
}
String AlexaDevice::getSoftwareVersion() const {
return softwareVersion;
}
AlexaInterface* AlexaDevice::addCapability(AlexaInterfaceType type) {
// Check if a interface with the given type already exists
for (auto& iface : capabilities) {
if (iface.getType() == type) {
return &iface; // Return the existing interface
}
}
// If the interface doesn't exist, create a new one
capabilities.emplace_back(type);
Serial.print("Created new interface with type: ");
Serial.println(capabilities.back().getTypeString());
// Return a pointer to the newly created device
return &capabilities.back();
}
JsonDocument AlexaDevice::getDeviceJSON() const {
JsonDocument json;
// Main device attributes
json["endpointId"] = endpointId;
json["friendlyName"] = name;
json["description"] = description;
json["manufacturerName"] = manufacturerName;
JsonArray displayCategories = json["displayCategories"].to<JsonArray>();
displayCategories.add(DisplayCategoryUtils::toString(displayCategory));
JsonObject additionalAttributes = json["additionalAttributes"].to<JsonObject>();
additionalAttributes["manufacturer"] = manufacturer;
additionalAttributes["model"] = model;
additionalAttributes["serialNumber"] = "ESP2Alex";
additionalAttributes["firmwareVersion"] = "1.0.0";
additionalAttributes["softwareVersion"] = softwareVersion;
additionalAttributes["customIdentifier"] = "ESP2Alex";
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
for (AlexaInterface capability : capabilities) {
capabilitiesArray.add(capability.getJSON().as<JsonObject>());
}
// 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) {
// Find an empty slot for the new event
if (eventNames[i] == nullptr) {
eventNames[i] = eventName; // Store the event name
eventCallbacks[i] = callback; // Store the callback function
break;
}
}
}
// Trigger the event and invoke the corresponding callback
void AlexaDevice::triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type) const {
for (int i = 0; i < MAX_EVENTS; ++i) {
if (eventNames[i] != nullptr && strcmp(eventNames[i], eventName) == 0) {
// Found the event name, call the corresponding callback function
eventCallbacks[i](directive, type);
return;
}
}
// If no event was found, print an error message
Serial.print("No event registered for: ");
Serial.println(eventName);
}
#include "AlexaDevice.h"
AlexaDevice::AlexaDevice(const String& name, const String& rootTopic, const String& endpointId)
: name(name), endpointId(endpointId), rootTopic(rootTopic) {
// Initialize event arrays to nullptr
for (int i = 0; i < MAX_EVENTS; ++i) {
eventNames[i] = nullptr; // Set all event names to nullptr
eventCallbacks[i] = nullptr; // Set all callbacks to nullptr
}
}
// Setters and Getters
void AlexaDevice::setName(const String& name) {
this->name = name;
}
String AlexaDevice::getName() const {
return name;
}
String AlexaDevice::getEndpointId() const {
return endpointId;
}
DisplayCategory AlexaDevice::getDisplayCategory() const {
return displayCategory;
}
void AlexaDevice::setDisplayCategory(DisplayCategory category) {
displayCategory = category;
}
String AlexaDevice::getDescription() const {
return description;
}
void AlexaDevice::setDescription(const String& description) {
this->description = description;
}
String AlexaDevice::getManufacturerName() const {
return manufacturerName;
}
void AlexaDevice::setManufacturerName(const String& manufacturerName) {
this->manufacturerName = manufacturerName;
}
String AlexaDevice::getManufacturer() const {
return manufacturer;
}
void AlexaDevice::setManufacturer(const String& manufacturer) {
this->manufacturer = manufacturer;
}
String AlexaDevice::getModel() const {
return model;
}
void AlexaDevice::setModel(const String& model) {
this->model = model;
}
String AlexaDevice::getSoftwareVersion() const {
return softwareVersion;
}
AlexaInterface* AlexaDevice::addCapability(AlexaInterfaceType type) {
// Check if a interface with the given type already exists
for (auto& iface : capabilities) {
if (iface.getType() == type) {
return &iface; // Return the existing interface
}
}
// If the interface doesn't exist, create a new one
capabilities.emplace_back(type);
Serial.print("Created new interface with type: ");
Serial.println(capabilities.back().getTypeString());
// Return a pointer to the newly created device
return &capabilities.back();
}
JsonDocument AlexaDevice::getDeviceJSON() const {
JsonDocument json;
// Main device attributes
json["endpointId"] = endpointId;
json["friendlyName"] = name;
json["description"] = description;
json["manufacturerName"] = manufacturerName;
JsonArray displayCategories = json["displayCategories"].to<JsonArray>();
displayCategories.add(DisplayCategoryUtils::toString(displayCategory));
JsonObject additionalAttributes = json["additionalAttributes"].to<JsonObject>();
additionalAttributes["manufacturer"] = manufacturer;
additionalAttributes["model"] = model;
additionalAttributes["serialNumber"] = "ESP2Alex";
additionalAttributes["firmwareVersion"] = "1.1.0";
additionalAttributes["softwareVersion"] = softwareVersion;
additionalAttributes["customIdentifier"] = "ESP2Alex";
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
for (const AlexaInterface& capability : capabilities) {
capabilitiesArray.add(capability.getJSON().as<JsonObject>());
}
// 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) {
// Find an empty slot for the new event
if (eventNames[i] == nullptr) {
eventNames[i] = eventName; // Store the event name
eventCallbacks[i] = callback; // Store the callback function
break;
}
}
}
// Trigger the event and invoke the corresponding callback
void AlexaDevice::triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing) const {
for (int i = 0; i < MAX_EVENTS; ++i) {
if (eventNames[i] != nullptr && strcmp(eventNames[i], eventName) == 0) {
// Found the event name, call the corresponding callback function
eventCallbacks[i](directive, type);
return;
}
}
// If no event was found, print an error message
if (warnIfMissing) {
Serial.print("No event registered for: ");
Serial.println(eventName);
}
}

View file

@ -1,204 +1,207 @@
#ifndef ALEXADEVICE_H
#define ALEXADEVICE_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include "AlexaInterface.h"
#include <functional>
#include <map>
#include <unordered_map>
#include <string>
#include <AlexaStatusMessage.h>
#include <Alex2ESP.h>
#define MAX_EVENTS 10
enum class DisplayCategory {
ACTIVITY_TRIGGER,
AIR_CONDITIONER,
AIR_FRESHENER,
AIR_PURIFIER,
AIR_QUALITY_MONITOR,
ALEXA_VOICE_ENABLED,
AUTO_ACCESSORY,
BLUETOOTH_SPEAKER,
CAMERA,
CHRISTMAS_TREE,
COFFEE_MAKER,
COMPUTER,
CONTACT_SENSOR,
DISHWASHER,
DOOR,
DOORBELL,
DRYER,
EXTERIOR_BLIND,
FAN,
GAME_CONSOLE,
GARAGE_DOOR,
HEADPHONES,
HUB,
INTERIOR_BLIND,
LAPTOP,
LIGHT,
MICROWAVE,
MOBILE_PHONE,
MOTION_SENSOR,
MUSIC_SYSTEM,
NETWORK_HARDWARE,
OTHER,
OVEN,
PHONE,
PRINTER,
REMOTE,
ROUTER,
SCENE_TRIGGER,
SCREEN,
SECURITY_PANEL,
SECURITY_SYSTEM,
SLOW_COOKER,
SMARTLOCK,
SMARTPLUG,
SPEAKER,
STREAMING_DEVICE,
SWITCH,
TABLET,
TEMPERATURE_SENSOR,
THERMOSTAT,
TV,
VACUUM_CLEANER,
VEHICLE,
WASHER,
WATER_HEATER,
WEARABLE
};
class DisplayCategoryUtils {
public:
static String toString(DisplayCategory category) {
static const std::unordered_map<DisplayCategory, String> categoryMap = {
{DisplayCategory::ACTIVITY_TRIGGER, "ACTIVITY_TRIGGER"},
{DisplayCategory::AIR_CONDITIONER, "AIR_CONDITIONER"},
{DisplayCategory::AIR_FRESHENER, "AIR_FRESHENER"},
{DisplayCategory::AIR_PURIFIER, "AIR_PURIFIER"},
{DisplayCategory::AIR_QUALITY_MONITOR, "AIR_QUALITY_MONITOR"},
{DisplayCategory::ALEXA_VOICE_ENABLED, "ALEXA_VOICE_ENABLED"},
{DisplayCategory::AUTO_ACCESSORY, "AUTO_ACCESSORY"},
{DisplayCategory::BLUETOOTH_SPEAKER, "BLUETOOTH_SPEAKER"},
{DisplayCategory::CAMERA, "CAMERA"},
{DisplayCategory::CHRISTMAS_TREE, "CHRISTMAS_TREE"},
{DisplayCategory::COFFEE_MAKER, "COFFEE_MAKER"},
{DisplayCategory::COMPUTER, "COMPUTER"},
{DisplayCategory::CONTACT_SENSOR, "CONTACT_SENSOR"},
{DisplayCategory::DISHWASHER, "DISHWASHER"},
{DisplayCategory::DOOR, "DOOR"},
{DisplayCategory::DOORBELL, "DOORBELL"},
{DisplayCategory::DRYER, "DRYER"},
{DisplayCategory::EXTERIOR_BLIND, "EXTERIOR_BLIND"},
{DisplayCategory::FAN, "FAN"},
{DisplayCategory::GAME_CONSOLE, "GAME_CONSOLE"},
{DisplayCategory::GARAGE_DOOR, "GARAGE_DOOR"},
{DisplayCategory::HEADPHONES, "HEADPHONES"},
{DisplayCategory::HUB, "HUB"},
{DisplayCategory::INTERIOR_BLIND, "INTERIOR_BLIND"},
{DisplayCategory::LAPTOP, "LAPTOP"},
{DisplayCategory::LIGHT, "LIGHT"},
{DisplayCategory::MICROWAVE, "MICROWAVE"},
{DisplayCategory::MOBILE_PHONE, "MOBILE_PHONE"},
{DisplayCategory::MOTION_SENSOR, "MOTION_SENSOR"},
{DisplayCategory::MUSIC_SYSTEM, "MUSIC_SYSTEM"},
{DisplayCategory::NETWORK_HARDWARE, "NETWORK_HARDWARE"},
{DisplayCategory::OTHER, "OTHER"},
{DisplayCategory::OVEN, "OVEN"},
{DisplayCategory::PHONE, "PHONE"},
{DisplayCategory::PRINTER, "PRINTER"},
{DisplayCategory::REMOTE, "REMOTE"},
{DisplayCategory::ROUTER, "ROUTER"},
{DisplayCategory::SCENE_TRIGGER, "SCENE_TRIGGER"},
{DisplayCategory::SCREEN, "SCREEN"},
{DisplayCategory::SECURITY_PANEL, "SECURITY_PANEL"},
{DisplayCategory::SECURITY_SYSTEM, "SECURITY_SYSTEM"},
{DisplayCategory::SLOW_COOKER, "SLOW_COOKER"},
{DisplayCategory::SMARTLOCK, "SMARTLOCK"},
{DisplayCategory::SMARTPLUG, "SMARTPLUG"},
{DisplayCategory::SPEAKER, "SPEAKER"},
{DisplayCategory::STREAMING_DEVICE, "STREAMING_DEVICE"},
{DisplayCategory::SWITCH, "SWITCH"},
{DisplayCategory::TABLET, "TABLET"},
{DisplayCategory::TEMPERATURE_SENSOR, "TEMPERATURE_SENSOR"},
{DisplayCategory::THERMOSTAT, "THERMOSTAT"},
{DisplayCategory::TV, "TV"},
{DisplayCategory::VACUUM_CLEANER, "VACUUM_CLEANER"},
{DisplayCategory::VEHICLE, "VEHICLE"},
{DisplayCategory::WASHER, "WASHER"},
{DisplayCategory::WATER_HEATER, "WATER_HEATER"},
{DisplayCategory::WEARABLE, "WEARABLE"}
};
auto it = categoryMap.find(category);
if (it != categoryMap.end()) {
return it->second;
}
return "UNKNOWN";
}
};
class AlexaDevice {
public:
AlexaDevice(const String& name, const String& rootTopic, const String& endpointId);
void setName(const String& name);
String getName() const;
String getEndpointId() 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;
AlexaInterface* addCapability(AlexaInterfaceType type);
void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&));
void triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type) const;
AlexaStatusMessage buildStatusMessage(const String& correlationToken,const bool isResponse=false) {
return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse);
}
private:
String name;
String endpointId;
String rootTopic;
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 = "1.0.0";
std::vector<AlexaInterface> capabilities;
};
#endif
#ifndef ALEXADEVICE_H
#define ALEXADEVICE_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include "AlexaInterface.h"
#include <functional>
#include <map>
#include <unordered_map>
#include <string>
#include <deque>
#include "AlexaStatusMessage.h"
#define MAX_EVENTS 10
enum class DisplayCategory {
ACTIVITY_TRIGGER,
AIR_CONDITIONER,
AIR_FRESHENER,
AIR_PURIFIER,
AIR_QUALITY_MONITOR,
ALEXA_VOICE_ENABLED,
AUTO_ACCESSORY,
BLUETOOTH_SPEAKER,
CAMERA,
CHRISTMAS_TREE,
COFFEE_MAKER,
COMPUTER,
CONTACT_SENSOR,
DISHWASHER,
DOOR,
DOORBELL,
DRYER,
EXTERIOR_BLIND,
FAN,
GAME_CONSOLE,
GARAGE_DOOR,
HEADPHONES,
HUB,
INTERIOR_BLIND,
LAPTOP,
LIGHT,
MICROWAVE,
MOBILE_PHONE,
MOTION_SENSOR,
MUSIC_SYSTEM,
NETWORK_HARDWARE,
OTHER,
OVEN,
PHONE,
PRINTER,
REMOTE,
ROUTER,
SCENE_TRIGGER,
SCREEN,
SECURITY_PANEL,
SECURITY_SYSTEM,
SLOW_COOKER,
SMARTLOCK,
SMARTPLUG,
SPEAKER,
STREAMING_DEVICE,
SWITCH,
TABLET,
TEMPERATURE_SENSOR,
THERMOSTAT,
TV,
VACUUM_CLEANER,
VEHICLE,
WASHER,
WATER_HEATER,
WEARABLE
};
class DisplayCategoryUtils {
public:
static String toString(DisplayCategory category) {
static const std::unordered_map<DisplayCategory, String> categoryMap = {
{DisplayCategory::ACTIVITY_TRIGGER, "ACTIVITY_TRIGGER"},
{DisplayCategory::AIR_CONDITIONER, "AIR_CONDITIONER"},
{DisplayCategory::AIR_FRESHENER, "AIR_FRESHENER"},
{DisplayCategory::AIR_PURIFIER, "AIR_PURIFIER"},
{DisplayCategory::AIR_QUALITY_MONITOR, "AIR_QUALITY_MONITOR"},
{DisplayCategory::ALEXA_VOICE_ENABLED, "ALEXA_VOICE_ENABLED"},
{DisplayCategory::AUTO_ACCESSORY, "AUTO_ACCESSORY"},
{DisplayCategory::BLUETOOTH_SPEAKER, "BLUETOOTH_SPEAKER"},
{DisplayCategory::CAMERA, "CAMERA"},
{DisplayCategory::CHRISTMAS_TREE, "CHRISTMAS_TREE"},
{DisplayCategory::COFFEE_MAKER, "COFFEE_MAKER"},
{DisplayCategory::COMPUTER, "COMPUTER"},
{DisplayCategory::CONTACT_SENSOR, "CONTACT_SENSOR"},
{DisplayCategory::DISHWASHER, "DISHWASHER"},
{DisplayCategory::DOOR, "DOOR"},
{DisplayCategory::DOORBELL, "DOORBELL"},
{DisplayCategory::DRYER, "DRYER"},
{DisplayCategory::EXTERIOR_BLIND, "EXTERIOR_BLIND"},
{DisplayCategory::FAN, "FAN"},
{DisplayCategory::GAME_CONSOLE, "GAME_CONSOLE"},
{DisplayCategory::GARAGE_DOOR, "GARAGE_DOOR"},
{DisplayCategory::HEADPHONES, "HEADPHONES"},
{DisplayCategory::HUB, "HUB"},
{DisplayCategory::INTERIOR_BLIND, "INTERIOR_BLIND"},
{DisplayCategory::LAPTOP, "LAPTOP"},
{DisplayCategory::LIGHT, "LIGHT"},
{DisplayCategory::MICROWAVE, "MICROWAVE"},
{DisplayCategory::MOBILE_PHONE, "MOBILE_PHONE"},
{DisplayCategory::MOTION_SENSOR, "MOTION_SENSOR"},
{DisplayCategory::MUSIC_SYSTEM, "MUSIC_SYSTEM"},
{DisplayCategory::NETWORK_HARDWARE, "NETWORK_HARDWARE"},
{DisplayCategory::OTHER, "OTHER"},
{DisplayCategory::OVEN, "OVEN"},
{DisplayCategory::PHONE, "PHONE"},
{DisplayCategory::PRINTER, "PRINTER"},
{DisplayCategory::REMOTE, "REMOTE"},
{DisplayCategory::ROUTER, "ROUTER"},
{DisplayCategory::SCENE_TRIGGER, "SCENE_TRIGGER"},
{DisplayCategory::SCREEN, "SCREEN"},
{DisplayCategory::SECURITY_PANEL, "SECURITY_PANEL"},
{DisplayCategory::SECURITY_SYSTEM, "SECURITY_SYSTEM"},
{DisplayCategory::SLOW_COOKER, "SLOW_COOKER"},
{DisplayCategory::SMARTLOCK, "SMARTLOCK"},
{DisplayCategory::SMARTPLUG, "SMARTPLUG"},
{DisplayCategory::SPEAKER, "SPEAKER"},
{DisplayCategory::STREAMING_DEVICE, "STREAMING_DEVICE"},
{DisplayCategory::SWITCH, "SWITCH"},
{DisplayCategory::TABLET, "TABLET"},
{DisplayCategory::TEMPERATURE_SENSOR, "TEMPERATURE_SENSOR"},
{DisplayCategory::THERMOSTAT, "THERMOSTAT"},
{DisplayCategory::TV, "TV"},
{DisplayCategory::VACUUM_CLEANER, "VACUUM_CLEANER"},
{DisplayCategory::VEHICLE, "VEHICLE"},
{DisplayCategory::WASHER, "WASHER"},
{DisplayCategory::WATER_HEATER, "WATER_HEATER"},
{DisplayCategory::WEARABLE, "WEARABLE"}
};
auto it = categoryMap.find(category);
if (it != categoryMap.end()) {
return it->second;
}
return "UNKNOWN";
}
};
class AlexaDevice {
public:
AlexaDevice(const String& name, const String& rootTopic, const String& endpointId);
void setName(const String& name);
String getName() const;
String getEndpointId() 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 type, 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(AlexaInterfaceType type);
void registerEvent(const char* eventName, void (*callback)(const JsonDocument&, const AlexaInterfaceType&));
// warnIfMissing=false keeps optional events (DirectiveReceived) quiet when the sketch did not register them
void triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type, bool warnIfMissing = true) const;
AlexaStatusMessage buildStatusMessage(const String& correlationToken,const bool isResponse=false) {
return AlexaStatusMessage(correlationToken,rootTopic,endpointId,isResponse);
}
private:
String name;
String endpointId;
String rootTopic;
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 = "1.1.0";
std::deque<AlexaInterface> capabilities; // deque: pointers handed out by addCapability stay valid
};
#endif

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,3 @@
#include "AlexaStatusMessage.h"
char AlexaStatusMessage::outputString[MAX_STATUS_REPORT_SIZE];
#include "AlexaStatusMessage.h"
char AlexaStatusMessage::outputString[MAX_STATUS_REPORT_SIZE];

View file

@ -1,167 +1,171 @@
#include <ArduinoJson.h>
#include <Alex2ESP.h>
#include <AlexaUtils.h>
#ifndef ALEXA_STATUS_MESSAGE_H
#define ALEXA_STATUS_MESSAGE_H
#define MAX_STATUS_REPORT_SIZE 2048
enum class EndpointHealth
{
OK,
UNREACHABLE
};
enum class PowerController
{
ON,
OFF
};
enum class TemperatureSensorScale
{
CELSIUS,
FAHRENHEIT
};
class AlexaStatusMessage
{
public:
AlexaStatusMessage(const String &correlationToken, const String &rootTopic, const String &endpointId, const bool isResponse)
{
this->endpointId = endpointId;
this->rootTopic = rootTopic;
JsonObject event = doc["event"].to<JsonObject>();
JsonObject event_header = event["header"].to<JsonObject>();
event_header["namespace"] = "Alexa";
if (isResponse)
{
event_header["name"] = "Response";
}
else
{
event_header["name"] = "StateReport";
}
event_header["payloadVersion"] = "3";
event_header["messageId"] = generateMessageId();
event_header["correlationToken"] = correlationToken;
JsonObject event_endpoint = event["endpoint"].to<JsonObject>();
event_endpoint["endpointId"] = endpointId;
contextProperties = doc["context"]["properties"].to<JsonArray>();
}
void setEndpointId(const String &endpointId)
{
// event["endpoint"]["endpointId"] = endpointId;
}
AlexaStatusMessage &AddHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0)
{
JsonDocument healthValue;
healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE";
return AddProperty(AlexaInterfaceType::ENDPOINT_HEALTH, "connectivity", healthValue.as<JsonObject>(), uncertaintyInMs);
}
AlexaStatusMessage &AddPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaceType::POWER_CONTROLLER, "powerState", value, uncertaintyInMs);
}
AlexaStatusMessage &AddTemperatureSensorProp(TemperatureSensorScale tempSensor,float value, unsigned int uncertaintyInMs = 0)
{
JsonDocument tempValue;
tempValue["scale"] = "CELSIUS";
tempValue["value"] = value;
if((tempSensor == TemperatureSensorScale::FAHRENHEIT)){
tempValue["value"] = (value - 32) * 5.0 / 9.0;
}
return AddProperty(AlexaInterfaceType::TEMPERATURE_SENSOR, "temperature", tempValue.as<JsonObject>(), uncertaintyInMs);
}
AlexaStatusMessage &AddBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaceType::BRIGHTNESS_CONTROLLER, "brightness", brightness, uncertaintyInMs);
}
AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER, "colorTemperatureInKelvin", colorTemperature, uncertaintyInMs);
}
AlexaStatusMessage &AddToggleControllerProp(PowerController powerController,String instanceName, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaceType::TOGGLE_CONTROLLER, "toggleState", value, uncertaintyInMs,instanceName);
}
AlexaStatusMessage &AddContextProp(const JsonObject &property)
{
contextProperties.add(property);
return *this; // Return a reference to the current object
}
void send()
{
doc.shrinkToFit();
serializeJson(doc, outputString);
doc.clear();
String topic = rootTopic + "/" + endpointId + "/alexaResponce";
AlexaUtils::enqueue(outputString, topic.c_str());
}
private:
String rootTopic;
String endpointId;
JsonDocument doc;
JsonArray contextProperties;
static char outputString[MAX_STATUS_REPORT_SIZE];
// TODO: make real uuid4 gen function
String generateMessageId()
{
char buffer[38];
const char charset[] = "0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // Allowed characters
// Seed the random number generator (optional)
srand(static_cast<unsigned int>(time(nullptr)));
// Generate 37 random characters
for (int i = 0; i < 37; ++i)
{
buffer[i] = charset[rand() % (sizeof(charset) - 1)]; // Pick a random character
}
buffer[37] = '\0'; // Null-terminate the string
return String(buffer);
}
template <typename T>
AlexaStatusMessage &AddProperty(AlexaInterfaceType type, const String &propertyName, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="")
{
JsonDocument prop;
prop["namespace"] = AlexaInterfaceUtils::toString(type);
prop["name"] = propertyName;
if (!instanceName.isEmpty()) {
prop["instance"] = instanceName;
}
if constexpr (std::is_same_v<T, JsonObject>)
{
prop["value"] = value;
}
else
{
prop["value"] = value;
}
prop["timeOfSample"] = "{REPLACE_WITH_DATETIME}";
prop["uncertaintyInMilliseconds"] = uncertaintyInMs;
contextProperties.add(prop.as<JsonObject>());
return *this;
}
};
#endif
#ifndef ALEXA_STATUS_MESSAGE_H
#define ALEXA_STATUS_MESSAGE_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include "AlexaInterface.h"
#include "AlexaUtils.h"
#define MAX_STATUS_REPORT_SIZE 2048
enum class EndpointHealth
{
OK,
UNREACHABLE
};
enum class PowerController
{
ON,
OFF
};
enum class TemperatureSensorScale
{
CELSIUS,
FAHRENHEIT
};
class AlexaStatusMessage
{
public:
AlexaStatusMessage(const String &correlationToken, const String &rootTopic, const String &endpointId, const bool isResponse)
{
this->endpointId = endpointId;
this->rootTopic = rootTopic;
JsonObject event = doc["event"].to<JsonObject>();
JsonObject event_header = event["header"].to<JsonObject>();
event_header["namespace"] = "Alexa";
if (isResponse)
{
event_header["name"] = "Response";
}
else
{
event_header["name"] = "StateReport";
}
event_header["payloadVersion"] = "3";
event_header["messageId"] = generateMessageId();
event_header["correlationToken"] = correlationToken;
JsonObject event_endpoint = event["endpoint"].to<JsonObject>();
event_endpoint["endpointId"] = endpointId;
event["payload"].to<JsonObject>(); // required by Alexa.Response / StateReport, empty when there is nothing to add
contextProperties = doc["context"]["properties"].to<JsonArray>();
}
AlexaStatusMessage &AddHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0)
{
JsonDocument healthValue;
healthValue["value"] = (endpointHealth == EndpointHealth::OK) ? "OK" : "UNREACHABLE";
return AddProperty(AlexaInterfaceType::ENDPOINT_HEALTH, "connectivity", healthValue.as<JsonObject>(), uncertaintyInMs);
}
AlexaStatusMessage &AddPowerControllerProp(PowerController powerController, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaceType::POWER_CONTROLLER, "powerState", value, uncertaintyInMs);
}
AlexaStatusMessage &AddTemperatureSensorProp(TemperatureSensorScale tempSensor,float value, unsigned int uncertaintyInMs = 0)
{
JsonDocument tempValue;
tempValue["scale"] = "CELSIUS";
tempValue["value"] = value;
if((tempSensor == TemperatureSensorScale::FAHRENHEIT)){
tempValue["value"] = (value - 32) * 5.0 / 9.0;
}
return AddProperty(AlexaInterfaceType::TEMPERATURE_SENSOR, "temperature", tempValue.as<JsonObject>(), uncertaintyInMs);
}
AlexaStatusMessage &AddBrightnessControllerProp(unsigned int brightness, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaceType::BRIGHTNESS_CONTROLLER, "brightness", brightness, uncertaintyInMs);
}
AlexaStatusMessage &AddColorTemperatureControllerProp(unsigned int colorTemperature, unsigned int uncertaintyInMs = 0)
{
return AddProperty(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER, "colorTemperatureInKelvin", colorTemperature, uncertaintyInMs);
}
AlexaStatusMessage &AddToggleControllerProp(PowerController powerController,String instanceName, unsigned int uncertaintyInMs = 0)
{
String value = (powerController == PowerController::ON) ? "ON" : "OFF";
return AddProperty(AlexaInterfaceType::TOGGLE_CONTROLLER, "toggleState", value, uncertaintyInMs,instanceName);
}
AlexaStatusMessage &AddContextProp(const JsonObject &property)
{
contextProperties.add(property);
return *this; // Return a reference to the current object
}
// Queue the report for delivery. Returns false (and says so on Serial) when the report does not fit the
// MAX_STATUS_REPORT_SIZE buffer or the send queue is full: a report is never sent truncated.
bool send()
{
doc.shrinkToFit();
String topic = rootTopic + "/" + endpointId + "/alexaResponce";
size_t needed = measureJson(doc);
if (needed > sizeof(outputString) - 1)
{
Serial.printf("[Alex2ESP] status report for %s is %u bytes, limit is %u - not sent\n",
endpointId.c_str(), (unsigned)needed, (unsigned)(sizeof(outputString) - 1));
doc.clear();
return false;
}
serializeJson(doc, outputString);
doc.clear();
if (!AlexaUtils::enqueue(outputString, topic.c_str()))
{
Serial.println("[Alex2ESP] send queue full, status report dropped");
return false;
}
return true;
}
private:
String rootTopic;
String endpointId;
JsonDocument doc;
JsonArray contextProperties;
static char outputString[MAX_STATUS_REPORT_SIZE];
// TODO: make real uuid4 gen function
String generateMessageId()
{
char buffer[38];
const char charset[] = "0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // Allowed characters
// Seed the random number generator (optional)
srand(static_cast<unsigned int>(time(nullptr)));
// Generate 37 random characters
for (int i = 0; i < 37; ++i)
{
buffer[i] = charset[rand() % (sizeof(charset) - 1)]; // Pick a random character
}
buffer[37] = '\0'; // Null-terminate the string
return String(buffer);
}
template <typename T>
AlexaStatusMessage &AddProperty(AlexaInterfaceType type, const String &propertyName, const T &value, unsigned int uncertaintyInMs = 0,const String &instanceName="")
{
JsonDocument prop;
prop["namespace"] = AlexaInterfaceUtils::toString(type);
prop["name"] = propertyName;
if (!instanceName.isEmpty()) {
prop["instance"] = instanceName;
}
prop["value"] = value;
prop["timeOfSample"] = "{REPLACE_WITH_DATETIME}";
prop["uncertaintyInMilliseconds"] = uncertaintyInMs;
contextProperties.add(prop.as<JsonObject>());
return *this;
}
};
#endif

View file

@ -1,220 +1,227 @@
#include "AlexaUtils.h"
// #define Alex2ESP_DEBUG
uint8_t AlexaUtils::nextMessageId = 0;
char AlexaUtils::receiveQueue[MAX_QUEUE_LENGTH][16]={};
char AlexaUtils::receivePayload[MAX_PAYLOAD_LENGTH];
char AlexaUtils::topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH] = {};
char AlexaUtils::packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH] = {};
char AlexaUtils::combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32];
int AlexaUtils::queueStart = 0;
int AlexaUtils::queueEnd = 0;
int AlexaUtils::queueCount = 0;
int AlexaUtils::queueStartReceive = 0;
int AlexaUtils::queueEndReceive = 0;
int AlexaUtils::queueCountReceive = 0;
// Enqueue a packet into the receive queue
bool AlexaUtils::enqueueReceive(const char* packet) {
if (isReceiveQueueFull()) {
return false; // Queue is full
}
AlexaUtils::log("Enqueing: ");
AlexaUtils::logln(packet);
// Copy the packet into the receive queue
strncpy(receiveQueue[queueEndReceive], packet, sizeof(receiveQueue[0]) - 1);
receiveQueue[queueEndReceive][sizeof(receiveQueue[0]) - 1] = '\0'; // Ensure null-termination
// Update the queueEnd and queueCount
queueEndReceive = (queueEndReceive + 1) % MAX_QUEUE_LENGTH;
queueCountReceive++;
return true;
}
// Dequeue a packet from the receive queue
bool AlexaUtils::dequeueReceive(String& packet,bool remove) {
if (isReceiveQueueEmpty()) {
return false; // Queue is empty
}
// Read the front of the receive queue
packet = String(receiveQueue[queueStartReceive]);
if(remove){
// Clear the dequeued slot (optional but good for debugging)
memset(receiveQueue[queueStartReceive], 0, sizeof(receiveQueue[0]));
// Update the queueStart and queueCount
queueStartReceive = (queueStartReceive + 1) % MAX_QUEUE_LENGTH;
queueCountReceive--;
}
return true;
}
// Check if the receive queue is empty
bool AlexaUtils::isReceiveQueueEmpty() {
return queueCountReceive == 0;
}
// Check if the receive queue is full
bool AlexaUtils::isReceiveQueueFull() {
return queueCountReceive == MAX_QUEUE_LENGTH;
}
// Enqueue a topic and packet into the queue
bool AlexaUtils::enqueue(const char* packet,const char* topic) {
if (isQueueFull()) {
return false; // Queue is full
}
// Copy the topic and packet into the respective arrays
strncpy(topicQueue[queueEnd], topic, MAX_TOPIC_LENGTH - 1);
topicQueue[queueEnd][MAX_TOPIC_LENGTH - 1] = '\0'; // Ensure null-termination
strncpy(packetQueue[queueEnd], packet, MAX_PACKET_LENGTH - 1);
packetQueue[queueEnd][MAX_PACKET_LENGTH - 1] = '\0'; // Ensure null-termination
// Update the queueEnd and queueCount
queueEnd = (queueEnd + 1) % MAX_QUEUE_LENGTH;
queueCount++;
return true;
}
// Dequeue a topic and packet from the queue
bool AlexaUtils::dequeue(String& topic, String& packet) {
if (isQueueEmpty()) {
return false; // Queue is empty
}
// Read the front of the queue
topic = String(topicQueue[queueStart]);
packet = String(packetQueue[queueStart]);
// Clear the dequeued slot (optional but good for debugging)
memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH);
memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH);
// Update the queueStart and queueCount
queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH;
queueCount--;
return true;
}
const char* AlexaUtils::dequeueVals(bool remove) {
if (isQueueEmpty()) {
return nullptr; // Queue is empty
}
// Reset combinedData before use to ensure no leftover data from previous calls
memset(combinedData, 0, sizeof(combinedData));
// Get lengths of the topic and packet
int topicLen = strlen(topicQueue[queueStart]);
int packetLen = strlen(packetQueue[queueStart]);
// Calculate available space in the combinedData buffer
size_t availableSpace = sizeof(combinedData) - 1; // Reserve space for the null terminator
// Check if the combined data can fit in the buffer
if ((topicLen + packetLen + strlen(MESSAGE_PAYLOAD_SPLIT)) > availableSpace) {
// Return null if the combined data is too large for the buffer
AlexaUtils::logln("Error: Combined data exceeds buffer size.");
return nullptr;
}
// Concatenate the topic, separator, and packet into combinedData
snprintf(combinedData, sizeof(combinedData), "%s%s%s", topicQueue[queueStart], MESSAGE_PAYLOAD_SPLIT, packetQueue[queueStart]);
if (remove) {
// Clear the dequeued slot
memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH);
memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH);
// Update the queueStart and queueCount
queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH;
queueCount--;
}
// Return the pointer to the combined data
return combinedData;
}
// Check if the queue is empty
bool AlexaUtils::isQueueEmpty() {
return queueCount == 0;
}
// Check if the queue is full
bool AlexaUtils::isQueueFull() {
return queueCount == MAX_QUEUE_LENGTH;
}
void AlexaUtils::log(const char *string) {
#ifdef Alex2ESP_DEBUG
Serial.print(string);
#endif
}
void AlexaUtils::logln(const char *string) {
#ifdef Alex2ESP_DEBUG
Serial.println(string);
#endif
}
void AlexaUtils::log(int num) {
#ifdef Alex2ESP_DEBUG
Serial.print(num);
#endif
}
void AlexaUtils::logln(int num) {
#ifdef Alex2ESP_DEBUG
Serial.println(num);
#endif
}
void AlexaUtils::log(uint16_t num) {
#ifdef Alex2ESP_DEBUG
Serial.print(num);
#endif
}
void AlexaUtils::logln(uint16_t num) {
#ifdef Alex2ESP_DEBUG
Serial.println(num);
#endif
}
void AlexaUtils::log(uint8_t num) {
#ifdef Alex2ESP_DEBUG
Serial.print(num);
#endif
}
void AlexaUtils::logln(uint8_t num) {
#ifdef Alex2ESP_DEBUG
Serial.println(num);
#endif
}
void AlexaUtils::logln(String string) {
#ifdef Alex2ESP_DEBUG
Serial.println(string);
#endif
}
#include "AlexaUtils.h"
// #define Alex2ESP_DEBUG
uint8_t AlexaUtils::nextMessageId = 0;
char AlexaUtils::receiveQueue[MAX_QUEUE_LENGTH][16]={};
char AlexaUtils::receivePayload[MAX_PAYLOAD_LENGTH];
char AlexaUtils::topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH] = {};
char AlexaUtils::packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH] = {};
char AlexaUtils::combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32];
int AlexaUtils::queueStart = 0;
int AlexaUtils::queueEnd = 0;
int AlexaUtils::queueCount = 0;
int AlexaUtils::queueStartReceive = 0;
int AlexaUtils::queueEndReceive = 0;
int AlexaUtils::queueCountReceive = 0;
// Enqueue a packet into the receive queue
bool AlexaUtils::enqueueReceive(const char* packet) {
if (isReceiveQueueFull()) {
return false; // Queue is full
}
AlexaUtils::log("Enqueing: ");
AlexaUtils::logln(packet);
// Copy the packet into the receive queue
strncpy(receiveQueue[queueEndReceive], packet, sizeof(receiveQueue[0]) - 1);
receiveQueue[queueEndReceive][sizeof(receiveQueue[0]) - 1] = '\0'; // Ensure null-termination
// Update the queueEnd and queueCount
queueEndReceive = (queueEndReceive + 1) % MAX_QUEUE_LENGTH;
queueCountReceive++;
return true;
}
// Dequeue a packet from the receive queue
bool AlexaUtils::dequeueReceive(String& packet,bool remove) {
if (isReceiveQueueEmpty()) {
return false; // Queue is empty
}
// Read the front of the receive queue
packet = String(receiveQueue[queueStartReceive]);
if(remove){
// Clear the dequeued slot (optional but good for debugging)
memset(receiveQueue[queueStartReceive], 0, sizeof(receiveQueue[0]));
// Update the queueStart and queueCount
queueStartReceive = (queueStartReceive + 1) % MAX_QUEUE_LENGTH;
queueCountReceive--;
}
return true;
}
// Check if the receive queue is empty
bool AlexaUtils::isReceiveQueueEmpty() {
return queueCountReceive == 0;
}
// Check if the receive queue is full
bool AlexaUtils::isReceiveQueueFull() {
return queueCountReceive == MAX_QUEUE_LENGTH;
}
// Enqueue a topic and packet into the queue
bool AlexaUtils::enqueue(const char* packet,const char* topic) {
if (isQueueFull()) {
return false; // Queue is full
}
// Refuse anything that would be cut short by the slot size - a truncated JSON packet is worthless
if (strlen(topic) > MAX_TOPIC_LENGTH - 1 || strlen(packet) > MAX_PACKET_LENGTH - 1) {
Serial.printf("[Alex2ESP] packet of %u bytes does not fit the %d byte send slot - dropped\n",
(unsigned)strlen(packet), MAX_PACKET_LENGTH - 1);
return false;
}
// Copy the topic and packet into the respective arrays
strncpy(topicQueue[queueEnd], topic, MAX_TOPIC_LENGTH - 1);
topicQueue[queueEnd][MAX_TOPIC_LENGTH - 1] = '\0'; // Ensure null-termination
strncpy(packetQueue[queueEnd], packet, MAX_PACKET_LENGTH - 1);
packetQueue[queueEnd][MAX_PACKET_LENGTH - 1] = '\0'; // Ensure null-termination
// Update the queueEnd and queueCount
queueEnd = (queueEnd + 1) % MAX_QUEUE_LENGTH;
queueCount++;
return true;
}
// Dequeue a topic and packet from the queue
bool AlexaUtils::dequeue(String& topic, String& packet) {
if (isQueueEmpty()) {
return false; // Queue is empty
}
// Read the front of the queue
topic = String(topicQueue[queueStart]);
packet = String(packetQueue[queueStart]);
// Clear the dequeued slot (optional but good for debugging)
memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH);
memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH);
// Update the queueStart and queueCount
queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH;
queueCount--;
return true;
}
const char* AlexaUtils::dequeueVals(bool remove) {
if (isQueueEmpty()) {
return nullptr; // Queue is empty
}
// Reset combinedData before use to ensure no leftover data from previous calls
memset(combinedData, 0, sizeof(combinedData));
// Get lengths of the topic and packet
int topicLen = strlen(topicQueue[queueStart]);
int packetLen = strlen(packetQueue[queueStart]);
// Calculate available space in the combinedData buffer
size_t availableSpace = sizeof(combinedData) - 1; // Reserve space for the null terminator
// Check if the combined data can fit in the buffer
if ((topicLen + packetLen + strlen(MESSAGE_PAYLOAD_SPLIT)) > availableSpace) {
// Return null if the combined data is too large for the buffer
AlexaUtils::logln("Error: Combined data exceeds buffer size.");
return nullptr;
}
// Concatenate the topic, separator, and packet into combinedData
snprintf(combinedData, sizeof(combinedData), "%s%s%s", topicQueue[queueStart], MESSAGE_PAYLOAD_SPLIT, packetQueue[queueStart]);
if (remove) {
// Clear the dequeued slot
memset(topicQueue[queueStart], 0, MAX_TOPIC_LENGTH);
memset(packetQueue[queueStart], 0, MAX_PACKET_LENGTH);
// Update the queueStart and queueCount
queueStart = (queueStart + 1) % MAX_QUEUE_LENGTH;
queueCount--;
}
// Return the pointer to the combined data
return combinedData;
}
// Check if the queue is empty
bool AlexaUtils::isQueueEmpty() {
return queueCount == 0;
}
// Check if the queue is full
bool AlexaUtils::isQueueFull() {
return queueCount == MAX_QUEUE_LENGTH;
}
void AlexaUtils::log(const char *string) {
#ifdef Alex2ESP_DEBUG
Serial.print(string);
#endif
}
void AlexaUtils::logln(const char *string) {
#ifdef Alex2ESP_DEBUG
Serial.println(string);
#endif
}
void AlexaUtils::log(int num) {
#ifdef Alex2ESP_DEBUG
Serial.print(num);
#endif
}
void AlexaUtils::logln(int num) {
#ifdef Alex2ESP_DEBUG
Serial.println(num);
#endif
}
void AlexaUtils::log(uint16_t num) {
#ifdef Alex2ESP_DEBUG
Serial.print(num);
#endif
}
void AlexaUtils::logln(uint16_t num) {
#ifdef Alex2ESP_DEBUG
Serial.println(num);
#endif
}
void AlexaUtils::log(uint8_t num) {
#ifdef Alex2ESP_DEBUG
Serial.print(num);
#endif
}
void AlexaUtils::logln(uint8_t num) {
#ifdef Alex2ESP_DEBUG
Serial.println(num);
#endif
}
void AlexaUtils::logln(String string) {
#ifdef Alex2ESP_DEBUG
Serial.println(string);
#endif
}

View file

@ -1,83 +1,89 @@
#ifndef ALEXA_UTILS_H
#define ALEXA_UTILS_H
#include <vector>
#include <string>
#include <Arduino.h>
#include <AsyncMqttClient.h>
#include <cstring> // Include for strcpy and memset
class AlexaUtils
{
public:
static const int MAX_PAYLOAD_LENGTH = 2048; // Maximum length for receive packets
static char receivePayload[MAX_PAYLOAD_LENGTH];
static bool enqueueReceive(const char* packet);
static bool dequeueReceive(String& packet,bool remove);
static bool isReceiveQueueEmpty();
static bool isReceiveQueueFull();
static bool enqueue(const char* topic, const char* packet);
static bool dequeue(String& topic, String& packet);
static bool isQueueEmpty();
static bool isQueueFull();
static const char* dequeueVals(bool remove);
static void log(const char *string);
static void logln(const char *string);
static void log(int num);
static void logln(int num);
static void log(uint16_t num);
static void logln(uint16_t num);
static void log(uint8_t num);
static void logln(uint8_t num);
static void logln(String string);
// Incrementing message ID
static uint8_t nextMessageId;
// Static function to set the MQTT client reference
static void printMemoryInfo()
{
size_t freeHeap = ESP.getFreeHeap();
size_t freeStack = ESP.getFreeContStack(); // Requires ESP8266 core 3.0.0+
size_t sketchSize = ESP.getSketchSize();
size_t freeSketchSpace = ESP.getFreeSketchSpace();
// Print in custom format
Serial.printf("freeHeap: %u, freeStack: %u, freeROM: %u, usedROM: %u\n",
freeHeap,
freeStack,
freeSketchSpace,
sketchSize);
}
private:
static const int MAX_QUEUE_LENGTH = 5; // Define maximum queue length
static const int MAX_TOPIC_LENGTH = 128; // Maximum length for topics
static const int MAX_PACKET_LENGTH = 2048; // Maximum length for sending packets
static constexpr char MESSAGE_PAYLOAD_SPLIT[32] = "{MESSAGE_PAYLOAD_SPLIT}";
// Static arrays for topics and packets
static char topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH];
static char packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH];
static char combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32];
static char receiveQueue[MAX_QUEUE_LENGTH][16];
static int queueStart; // Index of the front of the queue
static int queueEnd; // Index of the back of the queue
static int queueCount; // Number of items in the queue
static int queueStartReceive;
static int queueEndReceive;
static int queueCountReceive;
};
#endif // ALEXA_UTILS_H
#ifndef ALEXA_UTILS_H
#define ALEXA_UTILS_H
#include <vector>
#include <string>
#include <Arduino.h>
#include <AsyncMqttClient.h>
#include <cstring> // Include for strcpy and memset
class AlexaUtils
{
public:
static const int MAX_PAYLOAD_LENGTH = 2048; // Maximum length for receive packets
static char receivePayload[MAX_PAYLOAD_LENGTH];
static bool enqueueReceive(const char* packet);
static bool dequeueReceive(String& packet,bool remove);
static bool isReceiveQueueEmpty();
static bool isReceiveQueueFull();
// Queue a packet for the HTTP sender. Returns false (nothing queued) when the queue is full or the
// packet/topic would not fit its slot - a packet is never truncated.
static bool enqueue(const char* packet, const char* topic);
static bool dequeue(String& topic, String& packet);
static bool isQueueEmpty();
static bool isQueueFull();
static const char* dequeueVals(bool remove);
static void log(const char *string);
static void logln(const char *string);
static void log(int num);
static void logln(int num);
static void log(uint16_t num);
static void logln(uint16_t num);
static void log(uint8_t num);
static void logln(uint8_t num);
static void logln(String string);
// Incrementing message ID
static uint8_t nextMessageId;
// Static function to set the MQTT client reference
static void printMemoryInfo()
{
size_t freeHeap = ESP.getFreeHeap();
#ifdef ESP8266
size_t freeStack = ESP.getFreeContStack(); // Requires ESP8266 core 3.0.0+
#else
size_t freeStack = 0; // ESP32: untested, no getFreeContStack()
#endif
size_t sketchSize = ESP.getSketchSize();
size_t freeSketchSpace = ESP.getFreeSketchSpace();
// Print in custom format
Serial.printf("freeHeap: %u, freeStack: %u, freeROM: %u, usedROM: %u\n",
freeHeap,
freeStack,
freeSketchSpace,
sketchSize);
}
private:
static const int MAX_QUEUE_LENGTH = 5; // Define maximum queue length
static const int MAX_TOPIC_LENGTH = 128; // Maximum length for topics
static const int MAX_PACKET_LENGTH = 2048; // Maximum length for sending packets
static constexpr char MESSAGE_PAYLOAD_SPLIT[32] = "{MESSAGE_PAYLOAD_SPLIT}";
// Static arrays for topics and packets
static char topicQueue[MAX_QUEUE_LENGTH][MAX_TOPIC_LENGTH];
static char packetQueue[MAX_QUEUE_LENGTH][MAX_PACKET_LENGTH];
static char combinedData[MAX_TOPIC_LENGTH + MAX_PACKET_LENGTH + 32];
static char receiveQueue[MAX_QUEUE_LENGTH][16];
static int queueStart; // Index of the front of the queue
static int queueEnd; // Index of the back of the queue
static int queueCount; // Number of items in the queue
static int queueStartReceive;
static int queueEndReceive;
static int queueCountReceive;
};
#endif // ALEXA_UTILS_H