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:
parent
1313f47856
commit
87caad25be
20 changed files with 3117 additions and 2782 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
||||
|
|
@ -8,8 +8,14 @@
|
|||
|
||||
#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 = "";
|
||||
|
|
@ -31,7 +37,7 @@ void setup() {
|
|||
|
||||
// Connect to Wi-Fi
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
|
||||
Serial.print("[WIFI] Connecting to WiFi");
|
||||
|
||||
|
|
@ -43,7 +49,7 @@ void setup() {
|
|||
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
|
||||
// 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
|
||||
|
|
@ -88,6 +94,6 @@ void loop() {
|
|||
// Process Alexa client communication
|
||||
alexClient.loop();
|
||||
|
||||
// Update the state of the LED based on the power controller state
|
||||
// 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,14 @@
|
|||
|
||||
#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 = "";
|
||||
|
|
@ -32,7 +38,7 @@ void setup()
|
|||
|
||||
// Connect to Wi-Fi
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
|
||||
Serial.print("[WIFI] Connecting to WiFi");
|
||||
|
||||
|
|
@ -45,7 +51,7 @@ void setup()
|
|||
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
|
||||
// Initialize the Alexa client (MQTT username, MQTT password, root topic)
|
||||
alexClient.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
|
||||
|
||||
String deviceName="Bedroom Blinds";
|
||||
|
|
@ -53,8 +59,8 @@ void setup()
|
|||
// 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);
|
||||
// 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);
|
||||
|
|
@ -122,6 +128,6 @@ void loop()
|
|||
// Process Alexa client communication
|
||||
alexClient.loop();
|
||||
|
||||
// Update the state of the LED based on the power controller state
|
||||
// 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,14 @@
|
|||
|
||||
#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 = "";
|
||||
|
|
@ -31,7 +37,7 @@ void setup() {
|
|||
|
||||
// Connect to Wi-Fi
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
|
||||
Serial.print("[WIFI] Connecting to WiFi");
|
||||
|
||||
|
|
@ -43,7 +49,7 @@ void setup() {
|
|||
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
|
||||
// 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
|
||||
|
|
@ -81,7 +87,7 @@ void setup() {
|
|||
}else if (type == AlexaInterfaceType::BRIGHTNESS_CONTROLLER) {
|
||||
if(directive["header"]["name"]=="SetBrightness"){
|
||||
brightness=directive["payload"]["brightness"];
|
||||
Serial.println("setting brightness to: "+brightness);
|
||||
Serial.printf("setting brightness to: %d\n", brightness);
|
||||
outputState = PowerController::ON;
|
||||
}
|
||||
}else{
|
||||
|
|
@ -101,7 +107,7 @@ void loop() {
|
|||
// Process Alexa client communication
|
||||
alexClient.loop();
|
||||
|
||||
// Update the state of the LED based on the power controller state and brightness
|
||||
// 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{
|
||||
|
|
|
|||
|
|
@ -8,8 +8,14 @@
|
|||
|
||||
#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 = "";
|
||||
|
|
@ -32,7 +38,7 @@ void setup() {
|
|||
|
||||
// Connect to Wi-Fi
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
|
||||
Serial.print("[WIFI] Connecting to WiFi");
|
||||
|
||||
|
|
@ -44,7 +50,7 @@ void setup() {
|
|||
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
|
||||
// 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
|
||||
|
|
@ -86,13 +92,13 @@ void setup() {
|
|||
}else if (type == AlexaInterfaceType::BRIGHTNESS_CONTROLLER) {
|
||||
if(directive["header"]["name"]=="SetBrightness"){
|
||||
brightness=directive["payload"]["brightness"];
|
||||
Serial.println("setting brightness to: "+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.println("setting color temperature to: "+colorTemp);
|
||||
Serial.printf("setting color temperature to: %d\n", colorTemp);
|
||||
outputState = PowerController::ON;
|
||||
}
|
||||
}else{
|
||||
|
|
@ -113,7 +119,7 @@ void loop() {
|
|||
// Process Alexa client communication
|
||||
alexClient.loop();
|
||||
|
||||
// Update the state of the LED based on the power controller state and brightness
|
||||
// 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{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
// Define constants for WiFi and Alexa Client configuration
|
||||
const char *WIFI_SSID = "";
|
||||
const char *WIFI_PASSWORD = "";
|
||||
|
||||
const char *ALEXA_USERNAME = "";
|
||||
const char *ALEXA_PASSWORD = "";
|
||||
|
|
@ -26,7 +27,7 @@ void setup() {
|
|||
|
||||
// Connect to Wi-Fi
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
|
||||
Serial.print("[WIFI] Connecting to WiFi");
|
||||
|
||||
|
|
@ -38,7 +39,7 @@ void setup() {
|
|||
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
|
||||
// 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
|
||||
|
|
|
|||
76
keywords.txt
76
keywords.txt
|
|
@ -1,7 +1,81 @@
|
|||
#######################################
|
||||
# Syntax Coloring Map For Espalexa
|
||||
# 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
47
library.json
Normal 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
11
library.properties
Normal 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
|
||||
74
readme.md
74
readme.md
|
|
@ -1,6 +1,8 @@
|
|||
# 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.
|
||||
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/).
|
||||
|
||||
|
|
@ -24,11 +26,36 @@ For more details on how to configure Alex2ESP, visit [Alex2MQTT Documentation](h
|
|||
|
||||
## Quick Start
|
||||
### Installation
|
||||
Download the library as zip or via PlatformIO and include it in your project:
|
||||
|
||||
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
|
||||
|
|
@ -41,7 +68,7 @@ First create an instance of the alexa client and initilize it with your MQTT Cre
|
|||
Alex2ESP alexClient;
|
||||
```
|
||||
|
||||
Credentials can be found at https://alex2mqtt.stormysdream.club/ after logging in with amazon.
|
||||
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);
|
||||
```
|
||||
|
|
@ -80,6 +107,9 @@ Once initilized you can begin to add virtual devices, in this example we add a p
|
|||
}
|
||||
});
|
||||
```
|
||||
|
||||
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
|
||||
|
|
@ -87,7 +117,7 @@ For a comprehensive list of Alexa actions and mappings, visit the [Alexa Develop
|
|||
|
||||
```cpp
|
||||
AlexaDevice* device = alexClient.getDevice("Bedroom Blinds", "ESP-01");
|
||||
device->setDisplayCategory(DisplayCategory::LIGHT);
|
||||
device->setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
|
||||
|
||||
AlexaInterface* toggleController = device->addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER);
|
||||
toggleController->setInstance("ESP-01.Toggle");
|
||||
|
|
@ -99,6 +129,16 @@ 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
|
||||
|
|
@ -171,7 +211,7 @@ toggleController->addActionMapping(openMapping);
|
|||
---
|
||||
|
||||
|
||||
## Advanced ussage
|
||||
## 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:
|
||||
|
|
@ -189,6 +229,29 @@ For instance, to manually report the state of a PowerController, you can use the
|
|||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
|
|
@ -196,4 +259,3 @@ 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.
|
||||
|
||||
|
|
|
|||
108
src/Alex2ESP.cpp
108
src/Alex2ESP.cpp
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* @title Alex2ESP Library
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @author David
|
||||
* @license MIT
|
||||
* @contributors chaos511
|
||||
|
|
@ -14,9 +14,9 @@
|
|||
#include "Alex2ESP.h"
|
||||
|
||||
Alex2ESP::Alex2ESP()
|
||||
: rootTopic(), mqttUsername(nullptr), mqttPassword(nullptr), _state(Alex2ESPState::UNINITIALIZED), _disconnectReason(AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {}
|
||||
: rootTopic(), mqttUsername(nullptr), mqttPassword(nullptr), lastReconnectTime(0), reconnectAttempt(0), _state(Alex2ESPState::UNINITIALIZED), _disconnectReason(AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {}
|
||||
|
||||
void Alex2ESP::begin(const char *rootTopic, const char *username, const char *password)
|
||||
void Alex2ESP::begin(const char *username, const char *password, const char *rootTopic)
|
||||
{
|
||||
|
||||
_state = Alex2ESPState::INITIALIZED;
|
||||
|
|
@ -30,13 +30,9 @@ void Alex2ESP::begin(const char *rootTopic, const char *username, const char *pa
|
|||
|
||||
TopicESP = (String(this->rootTopic) + String("/+/alexaDirective_e"));
|
||||
|
||||
// Log the root topic and credentials
|
||||
// Log the root topic (never the credentials)
|
||||
AlexaUtils::log("Setting root topic: ");
|
||||
AlexaUtils::log(rootTopic);
|
||||
AlexaUtils::log(" with creds ");
|
||||
AlexaUtils::log(username);
|
||||
AlexaUtils::log(":");
|
||||
AlexaUtils::logln(password);
|
||||
AlexaUtils::logln(rootTopic);
|
||||
AlexaUtils::printMemoryInfo();
|
||||
|
||||
// Set up MQTT client callbacks
|
||||
|
|
@ -98,24 +94,26 @@ void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessagePrope
|
|||
|
||||
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)
|
||||
// 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)
|
||||
{
|
||||
AlexaUtils::logln("Getting device json");
|
||||
JsonDocument jsonData = device.getDeviceJSON();
|
||||
String jsonString;
|
||||
serializeJson(jsonData, jsonString);
|
||||
jsonData.clear();
|
||||
AlexaUtils::enqueue(jsonString.c_str(), discoverTopicSend.c_str());
|
||||
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";
|
||||
|
|
@ -123,9 +121,11 @@ void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessagePrope
|
|||
if (strcmp(topic, directiveTopic.c_str()) == 0)
|
||||
{
|
||||
AlexaUtils::logln(topic);
|
||||
payload[length] = '\0';
|
||||
AlexaUtils::logln(payload);
|
||||
AlexaUtils::enqueueReceive(payload);
|
||||
AlexaUtils::logln(uuid);
|
||||
if (!AlexaUtils::enqueueReceive(uuid.c_str()))
|
||||
{
|
||||
Serial.println("[Alex2ESP] receive queue full, directive dropped");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -134,6 +134,57 @@ void Alex2ESP::onMessage(char *topic, char *payload, AsyncMqttClientMessagePrope
|
|||
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
|
||||
|
|
@ -333,7 +384,7 @@ void Alex2ESP::processHttpGet()
|
|||
String payloadStr = httpGET.getString();
|
||||
size_t length = payloadStr.length();
|
||||
|
||||
if (length < AlexaUtils::MAX_PAYLOAD_LENGTH - 1)
|
||||
if (length < (size_t)(AlexaUtils::MAX_PAYLOAD_LENGTH - 1))
|
||||
{
|
||||
payloadStr.toCharArray(AlexaUtils::receivePayload, length + 1);
|
||||
payloadStr = "";
|
||||
|
|
@ -353,11 +404,11 @@ void Alex2ESP::processHttpGet()
|
|||
|
||||
for (auto &device : devices)
|
||||
{
|
||||
if (strcmp(device.getEndpointId().c_str(), inputDoc["directive"]["endpoint"]["endpointId"]) == 0)
|
||||
if (strcmp(device.getEndpointId().c_str(), inputDoc["directive"]["endpoint"]["endpointId"] | "") == 0)
|
||||
{
|
||||
serializeJson(inputDoc, Serial);
|
||||
Serial.println();
|
||||
device.triggerEvent("DirectiveReceived", inputDoc["directive"], AlexaInterfaceType::UNKNOWN);
|
||||
device.triggerEvent("DirectiveReceived", inputDoc["directive"], AlexaInterfaceType::UNKNOWN, false);
|
||||
|
||||
if (inputDoc["directive"]["header"]["name"] == "ReportState")
|
||||
{
|
||||
|
|
@ -380,6 +431,7 @@ void Alex2ESP::loop()
|
|||
{
|
||||
|
||||
handleMqttReconnection();
|
||||
finishDiscovery();
|
||||
if(clearToSend){
|
||||
processHttpPost();
|
||||
processHttpGet();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* @title Alex2ESP Library
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @author David
|
||||
* @license MIT
|
||||
* @contributors chaos511
|
||||
|
|
@ -14,17 +14,20 @@
|
|||
|
||||
#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 "AlexaInterface.h"
|
||||
#include "AlexaUtils.h"
|
||||
#include <deque>
|
||||
|
||||
#ifdef ESP32
|
||||
#include <HTTPClient.h> // ESP32: untested
|
||||
#else
|
||||
#include <ESP8266HTTPClient.h>
|
||||
#endif
|
||||
|
||||
|
||||
enum class Alex2ESPState
|
||||
|
|
@ -43,21 +46,27 @@ public:
|
|||
// Constructor
|
||||
Alex2ESP();
|
||||
|
||||
// Begin function for initialization
|
||||
void begin(const char *rootTopic, const char *username, const char *password);
|
||||
// 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);
|
||||
void messageSplitAndSend(String messagepackString, String topic);
|
||||
|
||||
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
|
||||
|
|
@ -79,7 +88,7 @@ private:
|
|||
WiFiClient wifiPOST;
|
||||
JsonDocument inputDoc;
|
||||
|
||||
std::vector<AlexaDevice> devices; // Collection of devices
|
||||
std::deque<AlexaDevice> devices; // Collection of devices (deque: pointers handed out by getDevice stay valid)
|
||||
|
||||
Alex2ESPState _state;
|
||||
AsyncMqttClientDisconnectReason _disconnectReason;
|
||||
|
|
@ -96,6 +105,8 @@ private:
|
|||
|
||||
//loop processing function
|
||||
void handleMqttReconnection();
|
||||
void publishDiscovery();
|
||||
void finishDiscovery();
|
||||
void processHttpPost();
|
||||
void processHttpGet();
|
||||
|
||||
|
|
|
|||
|
|
@ -106,13 +106,13 @@ JsonDocument AlexaDevice::getDeviceJSON() const {
|
|||
additionalAttributes["manufacturer"] = manufacturer;
|
||||
additionalAttributes["model"] = model;
|
||||
additionalAttributes["serialNumber"] = "ESP2Alex";
|
||||
additionalAttributes["firmwareVersion"] = "1.0.0";
|
||||
additionalAttributes["firmwareVersion"] = "1.1.0";
|
||||
additionalAttributes["softwareVersion"] = softwareVersion;
|
||||
additionalAttributes["customIdentifier"] = "ESP2Alex";
|
||||
|
||||
JsonArray capabilitiesArray = json["capabilities"].to<JsonArray>();
|
||||
|
||||
for (AlexaInterface capability : capabilities) {
|
||||
for (const AlexaInterface& capability : capabilities) {
|
||||
capabilitiesArray.add(capability.getJSON().as<JsonObject>());
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ void AlexaDevice::registerEvent(const char* eventName, void (*callback)(const Js
|
|||
}
|
||||
|
||||
// Trigger the event and invoke the corresponding callback
|
||||
void AlexaDevice::triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type) const {
|
||||
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
|
||||
|
|
@ -159,6 +159,8 @@ void AlexaDevice::triggerEvent(const char* eventName, const JsonDocument& direct
|
|||
}
|
||||
}
|
||||
// If no event was found, print an error message
|
||||
Serial.print("No event registered for: ");
|
||||
Serial.println(eventName);
|
||||
if (warnIfMissing) {
|
||||
Serial.print("No event registered for: ");
|
||||
Serial.println(eventName);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,8 @@
|
|||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <AlexaStatusMessage.h>
|
||||
#include <Alex2ESP.h>
|
||||
#include <deque>
|
||||
#include "AlexaStatusMessage.h"
|
||||
|
||||
|
||||
#define MAX_EVENTS 10
|
||||
|
|
@ -173,11 +173,14 @@ public:
|
|||
|
||||
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&));
|
||||
|
||||
void triggerEvent(const char* eventName, const JsonDocument& directive, const AlexaInterfaceType& type) const;
|
||||
// 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);
|
||||
|
|
@ -195,9 +198,9 @@ private:
|
|||
String manufacturerName = "Alexa2MQTT";
|
||||
String manufacturer = "Alexa2MQTT";
|
||||
String model = "Alexa2MQTT";
|
||||
const String softwareVersion = "1.0.0";
|
||||
const String softwareVersion = "1.1.0";
|
||||
|
||||
std::vector<AlexaInterface> capabilities;
|
||||
std::deque<AlexaInterface> capabilities; // deque: pointers handed out by addCapability stay valid
|
||||
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
#define ALEXA_INTERFACE_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
// Enum for Alexa Interface Types
|
||||
enum class AlexaInterfaceType
|
||||
|
|
@ -584,11 +586,11 @@ public:
|
|||
std::vector<AlexaActions> actions; // Array of AlexaActions
|
||||
struct Directive {
|
||||
String name; // Name of the directive
|
||||
String payload; // Payload for the directive (can be empty)
|
||||
String payload; // JSON text of the directive's payload object, empty when there is none
|
||||
} directive; // Directive object containing name and payload
|
||||
|
||||
// Constructor
|
||||
ActionMapping(std::vector<AlexaActions> actions, String directiveName, String directivePayload="{}")
|
||||
ActionMapping(std::vector<AlexaActions> actions, String directiveName, String directivePayload="")
|
||||
: actions(actions) {
|
||||
directive.name = directiveName;
|
||||
directive.payload = directivePayload;
|
||||
|
|
@ -597,7 +599,7 @@ public:
|
|||
|
||||
|
||||
// Get the JSON representation of ActionMapping
|
||||
JsonDocument getJSON() {
|
||||
JsonDocument getJSON() const {
|
||||
JsonDocument doc;
|
||||
doc["@type"] = type;
|
||||
|
||||
|
|
@ -609,8 +611,15 @@ public:
|
|||
|
||||
JsonObject directiveObj = doc["directive"].to<JsonObject>();
|
||||
directiveObj["name"] = directive.name;
|
||||
if (directive.payload) {
|
||||
directiveObj["payload"] = directive.payload;
|
||||
if (directive.payload.length() > 0) {
|
||||
// Alexa expects the payload as an object, not as a string holding JSON: parse the text we were given
|
||||
JsonDocument payloadDoc;
|
||||
if (deserializeJson(payloadDoc, directive.payload) == DeserializationError::Ok && payloadDoc.is<JsonObject>()) {
|
||||
directiveObj["payload"] = payloadDoc.as<JsonObject>();
|
||||
} else {
|
||||
Serial.print("[Alex2ESP] action mapping payload is not a JSON object, omitted: ");
|
||||
Serial.println(directive.payload);
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
|
@ -630,7 +639,7 @@ private:
|
|||
|
||||
|
||||
String instance;
|
||||
bool nameSet;
|
||||
bool nameSet = false;
|
||||
public:
|
||||
// Constructor
|
||||
AlexaInterface(AlexaInterfaceType type, bool retrievable = true, bool proactivelyReported = false)
|
||||
|
|
@ -662,10 +671,9 @@ public:
|
|||
actionMappings.push_back(actionMapping);
|
||||
}
|
||||
|
||||
JsonDocument getJSON()
|
||||
JsonDocument getJSON() const
|
||||
{
|
||||
JsonDocument doc;
|
||||
String ret;
|
||||
|
||||
// Set base properties
|
||||
doc["interface"] = getTypeString();
|
||||
|
|
@ -688,12 +696,13 @@ public:
|
|||
}
|
||||
|
||||
|
||||
JsonObject semantics = doc["semantics"].to<JsonObject>();
|
||||
|
||||
// Add action mappings
|
||||
JsonArray actionMappingArray = semantics["actionMappings"].to<JsonArray>();
|
||||
for (ActionMapping& actionMapping : actionMappings) {
|
||||
actionMappingArray.add(actionMapping.getJSON().as<JsonObject>());
|
||||
// Add action mappings - semantics only exist when there is something to map (as alex2node does)
|
||||
if (!actionMappings.empty()) {
|
||||
JsonObject semantics = doc["semantics"].to<JsonObject>();
|
||||
JsonArray actionMappingArray = semantics["actionMappings"].to<JsonArray>();
|
||||
for (const ActionMapping& actionMapping : actionMappings) {
|
||||
actionMappingArray.add(actionMapping.getJSON().as<JsonObject>());
|
||||
}
|
||||
}
|
||||
if(nameSet){
|
||||
doc["instance"]=instance;
|
||||
|
|
@ -702,14 +711,14 @@ public:
|
|||
|
||||
// Add capabilityResources if friendlyNames is not empty
|
||||
if (!friendlyNames.empty()) {
|
||||
JsonObject capabilityResources = doc.createNestedObject("capabilityResources");
|
||||
JsonArray friendlyNamesArray = capabilityResources.createNestedArray("friendlyNames");
|
||||
JsonObject capabilityResources = doc["capabilityResources"].to<JsonObject>();
|
||||
JsonArray friendlyNamesArray = capabilityResources["friendlyNames"].to<JsonArray>();
|
||||
|
||||
for (const FriendlyName& fn : friendlyNames) {
|
||||
JsonObject friendlyNameObj = friendlyNamesArray.createNestedObject();
|
||||
JsonObject friendlyNameObj = friendlyNamesArray.add<JsonObject>();
|
||||
friendlyNameObj["@type"] = "text";
|
||||
|
||||
JsonObject valueObj = friendlyNameObj.createNestedObject("value");
|
||||
JsonObject valueObj = friendlyNameObj["value"].to<JsonObject>();
|
||||
valueObj["text"] = fn.text;
|
||||
valueObj["locale"] = fn.locale;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
#include <ArduinoJson.h>
|
||||
#include <Alex2ESP.h>
|
||||
#include <AlexaUtils.h>
|
||||
|
||||
#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
|
||||
|
|
@ -50,14 +51,10 @@ public:
|
|||
|
||||
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>();
|
||||
}
|
||||
|
||||
void setEndpointId(const String &endpointId)
|
||||
{
|
||||
// event["endpoint"]["endpointId"] = endpointId;
|
||||
}
|
||||
|
||||
AlexaStatusMessage &AddHealthProp(EndpointHealth endpointHealth, unsigned int uncertaintyInMs = 0)
|
||||
{
|
||||
JsonDocument healthValue;
|
||||
|
|
@ -102,14 +99,28 @@ public:
|
|||
return *this; // Return a reference to the current object
|
||||
}
|
||||
|
||||
void send()
|
||||
// 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();
|
||||
String topic = rootTopic + "/" + endpointId + "/alexaResponce";
|
||||
AlexaUtils::enqueue(outputString, topic.c_str());
|
||||
|
||||
if (!AlexaUtils::enqueue(outputString, topic.c_str()))
|
||||
{
|
||||
Serial.println("[Alex2ESP] send queue full, status report dropped");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
|
|
@ -148,14 +159,7 @@ private:
|
|||
if (!instanceName.isEmpty()) {
|
||||
prop["instance"] = instanceName;
|
||||
}
|
||||
if constexpr (std::is_same_v<T, JsonObject>)
|
||||
{
|
||||
prop["value"] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
prop["value"] = value;
|
||||
}
|
||||
prop["value"] = value;
|
||||
|
||||
prop["timeOfSample"] = "{REPLACE_WITH_DATETIME}";
|
||||
prop["uncertaintyInMilliseconds"] = uncertaintyInMs;
|
||||
|
|
|
|||
|
|
@ -75,6 +75,13 @@ bool AlexaUtils::enqueue(const char* packet,const char* topic) {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ public:
|
|||
static bool isReceiveQueueEmpty();
|
||||
static bool isReceiveQueueFull();
|
||||
|
||||
static bool enqueue(const char* topic, const char* packet);
|
||||
// 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();
|
||||
|
|
@ -45,7 +47,11 @@ public:
|
|||
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();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue