AlexaStatusMessage gets a helper for color, percentage, power level, range, mode, lock state, contact, motion, humidity and the thermostat (target, lower and upper setpoint, mode), and addProperty(row, place, value) for a property without one. Each is a function of its own on one shared builder; a value outside of the range of its property is reported as the nearest of the range with an error. AlexaResources.h holds the 103 asset ids of resources-and-assets.html (AlexaAssets, AlexaUnits); AlexaState has the seven states that were missing; addStateMapping() takes a number of any type. A setter that refuses its argument makes isValid() false, and the capability is left out of discovery with an error instead of being announced without what was refused. basicLight: static RAM 30,520 B (was 30,540), flash 333,389 B (was 334,981); 130 host tests (was 112). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|---|---|---|
| examples | ||
| src | ||
| test | ||
| .gitattributes | ||
| .gitignore | ||
| keywords.txt | ||
| library.json | ||
| library.properties | ||
| LICENSE | ||
| platformio.ini | ||
| readme.md | ||
Alex2ESP
Alex2ESP is a lightweight Arduino/PlatformIO library for integrating ESP8266 microcontrollers with Amazon Alexa smart home APIs through the Alex2MQTT 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.
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 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
[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:
#include <Alex2ESP.h>
Complete sketches are in 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
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)).
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
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
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.
Example: a blind with a position
RangeController, ModeController and ToggleController are generic controllers: a device may have several of one interface, each with an instance and at least one name. addCapability() takes the row and the instance and returns an AlexaCapability; its setters return the capability, so they can be chained.
void fillLift(JsonObject configuration, void*) {
JsonObject range = configuration["supportedRange"].to<JsonObject>();
range["minimumValue"] = 0;
range["maximumValue"] = 100;
range["precision"] = 1;
configuration["unitOfMeasure"] = FPSTR(AlexaUnits::Percent);
}
device->addCapability(AlexaInterfaces::RangeController, "Blind.Lift")
->addFriendlyAsset(AlexaAssets::Setting_Opening)
.addFriendlyName("Lift", "en-US")
.setConfiguration(fillLift)
.addActionMapping(ActionMapping({AlexaAction::Close, AlexaAction::Lower}, "SetRangeValue", "{\"rangeValue\":0}"))
.addActionMapping(ActionMapping({AlexaAction::Open, AlexaAction::Raise}, "SetRangeValue", "{\"rangeValue\":100}"))
.addStateMapping({AlexaState::Closed}, 0)
.addStateMapping({AlexaState::Open}, 1, 100);
AlexaAssets and AlexaUnits (src/AlexaResources.h) hold the names and units of the catalog of Alexa, which Alexa translates: AlexaAssets::Setting_Opening is Alexa.Setting.Opening, AlexaUnits::Temperature_Celsius is Alexa.Unit.Temperature.Celsius. An id that the sketch does not name takes no flash.
A setter that refuses what it is given prints the reason and the chain goes on. isValid() of the capability is false from then on, and the device leaves the capability out of discovery with an error, so that Alexa does not learn half of it.
addCapability() returns nullptr for a generic controller without an instance: check the pointer when the instance is not a literal. A capability holds 3 names, 4 action mappings and 2 state mappings (-DALEX2ESP_MAX_FRIENDLY_NAMES, -DALEX2ESP_MAX_ACTION_MAPPINGS, -DALEX2ESP_MAX_STATE_MAPPINGS in build_flags change that). The instance and the text of a friendly name are copied. The locale, an asset id, the text value of a state mapping and the directive name and payload of an ActionMapping are kept as pointers: pass literals. setNonControllable(true) announces a capability that reports its state and takes no directives.
A device holds 8 capabilities (-DALEX2ESP_MAX_CAPABILITIES); addCapability() for one more prints an error and returns nullptr.
Example: one handler for several devices
onDirective() registers a function that gets every directive of its device, ReportState included, together with the device and the capability the directive is for. The same function can serve any number of devices:
AlexaDevice* lamps[2];
bool on[2];
void onLamp(AlexaDirective& d) {
bool& state = on[d.device == lamps[0] ? 0 : 1];
if (d.isReportState()) {
d.stateReport().AddHealthProp(EndpointHealth::OK).AddPowerControllerProp(state ? PowerController::ON : PowerController::OFF).send();
return;
}
if (d.type != AlexaInterfaceType::POWER_CONTROLLER) {
return; // not for a capability of the lamp: the library answers INVALID_DIRECTIVE
}
state = d.is("TurnOn");
d.response().AddHealthProp(EndpointHealth::OK).AddPowerControllerProp(state ? PowerController::ON : PowerController::OFF).send();
}
// in setup(), after begin()
lamps[0] = alexClient.getDevice("Desk Lamp", "ESP-01");
lamps[1] = alexClient.getDevice("Floor Lamp", "ESP-02");
for (AlexaDevice* lamp : lamps) {
lamp->setDisplayCategory(DisplayCategory::LIGHT);
lamp->addCapability(AlexaInterfaces::PowerController);
lamp->onDirective(onLamp);
}
d.ns, d.name, d.instance, d.correlationToken and d.payload are those of the directive and valid until the handler returns. d.capability is the capability of the device with the namespace and the instance of the directive, nullptr when the device has none and for ReportState. A device with such a handler does not call the ReportState and Event handlers of registerEvent(), which keep working for a device without one.
How it talks to Alex2MQTT
Everything goes over MQTT (port 1883 of alex2mqtt.stormysdream.club); the library makes no HTTP requests. alexClient.setServer("broker.example.org", 1883) before begin() names another broker, for example your own instance of Alex2MQTT. The host is a name or an address as text and is not copied, like the username and the password: pass a literal or a buffer that stays.
-
Session.
begin()starts SNTP (pool.ntp.org,time.nist.gov) and returns;loop()opens the MQTT session once Wi-Fi is up and the clock is set, or after 5 s of Wi-Fi without an answer, and subscribes to<root>/discoverand<root>/+/alexaDirective.getState()isCONNECTEDwhen the broker has acknowledged both subscriptions;[Alex2ESP] error: the broker refused the subscription to <root>/discovermeans that the root topic is not the one of the account. A session that ends or cannot be opened prints[Alex2ESP] error: disconnected: <reason>; next attempt in N sand is opened again after 1 s, then 2 s, 4 s and so on up to once a minute, for as long as Wi-Fi is up. The wait starts at 1 s again after a session that lasted a minute: a session that the broker closes right after it has accepted it, as it does to one of two boards with the same client id, is opened again after the longer wait.the broker refused the username or the passwordis the reason to look for when a board never shows up in Alexa,the broker did not answer within 30 sstands for an attempt that the library gave up. When the link goes down the library prints[Alex2ESP] error: Wi-Fi is down, waiting for it, once per loss; the MQTT client reports the lost connection when its keep-alive runs out, and the session is opened again when Wi-Fi is back. A sketch that sets the clock itself (its ownconfigTime()with a time zone, an RTC) callsalexClient.setTimeSource(false)beforebegin(). The board has to reach an NTP server: DNS for the two names and outbound UDP port 123. Without the time of day the session still opens, the reports carry atimeOfSamplein 1970, and the library prints[Alex2ESP] error: the clock is not set, ...when it connects and then at most once a minute while reports are sent. -
Discovery. On
<root>/discoverthe library answers with one discovery object per device on<root>/discover_r. 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 from the nextloop(). Each object sits on the heap (about 1 KB) until the MQTT client has sent it; when the client cannot take another one (free heap under 4 KB), the library prints[Alex2ESP] discovery deferred at <endpointId>and sends the rest fromloop()as the queue drains, for up to 5 s after the request.[Alex2ESP] error: discovery gave up: N device(s) not announcedmeans those devices missed this answer - on the backend's proactive discovery that can remove them from Alexa until the next one. -
Directives. The directive arrives as JSON on
<root>/<endpointId>/alexaDirective. A directive larger than one TCP segment arrives in fragments, which are put together in one heap block that exists only untilloop()has parsed it.loop()then calls the handler of the device: the one ofonDirective(), or elseReportStateorEventofregisterEvent()(and before itDirectiveReceived, if registered) with the directive:directive["header"],directive["endpoint"],directive["payload"]. A device without a handler answers with the ErrorResponseINVALID_DIRECTIVE, and so does a device whose handler sent nothing for a directive that names a capability the device does not have; both print an error. A handler that sends nothing for a capability of its device leaves the directive unanswered, which prints[Alex2ESP] error: <endpointId>: the handler sent no answer to .... Every call ofloop()handles one directive, in the order of arrival. Up to eight directives wait for it: Alexa sends a group command ("turn off the kitchen") as one directive per endpoint, and they arrive faster than a busy sketch callsloop(). A directive that arrives twice (a broker that mirrors its topics delivers every message twice) is handled once; the repeat is recognised while it arrives and takes no place among the waiting ones. -
Reports.
send()publishes a message at once, on the topic of its kind. Every property carries the board's UTC time astimeOfSample, every message amessageIdof its own, a UUID made of the random source of the hardware.Kind Built with Topic Response d.response()<root>/<endpointId>/alexaResponceStateReport d.stateReport()<root>/<endpointId>/alexaResponceErrorResponse d.error(AlexaErrorType::VALUE_OUT_OF_RANGE, "0 to 100")<root>/<endpointId>/alexaResponceDeferredResponse d.deferred(7)<root>/<endpointId>/alexaResponcethe answer after it device->response(token)...sendAsync()<root>/<endpointId>/deferredResponseActivationStarted, DeactivationStarted d.sceneStarted(),d.sceneStopped()<root>/<endpointId>/alexaResponceChangeReport device->changeReport(AlexaCause::PHYSICAL_INTERACTION)<root>/changeReportDoorbellPress device->doorbellPress()<root>/eventThe backend waits 7 s for the answer to a directive, so answer from the handler. A device that takes longer (Alexa accepts this for locks and Wake-on-LAN) sends
d.deferred(seconds), keeps a copy ofd.correlationTokenand sends the answer when it is done:device->response(token).addHealthProp(EndpointHealth::OK).sendAsync(). AnErrorResponsetakes what its type asks for inpayload(), for examplepayload()["validRange"]["maximumValue"] = 100; the types of a thermostat (REQUESTED_SETPOINTS_TOO_CLOSE, ...) are sent in the namespaceAlexa.ThermostatController. AChangeReporttells Alexa that properties of a capability withsetProactivelyReported(true)changed: the properties added beforecontext()are those that changed, the ones after it the others.device->changeReport(AlexaCause::PHYSICAL_INTERACTION) .addPowerControllerProp(PowerController::ON) .context() .addHealthProp(EndpointHealth::OK) .send();A
ChangeReportwithout a property that changed is not sent and prints an error. -
Limits. A directive may be 2047 bytes (
ALEX2ESP_MAX_DIRECTIVE), a report or the discovery object of one device 3071 (ALEX2ESP_MAX_MESSAGE). The second limit is the first plus 1024 unless it is set: an answer repeats thecorrelationTokenof its directive, which is most of a large directive, and adds 140 to 170 bytes per property, so the largest directive can be answered with six properties. Eight directives (ALEX2ESP_MAX_QUEUED_DIRECTIVES) of 8188 bytes together (ALEX2ESP_MAX_QUEUED_BYTES, four times the largest directive) may wait forloop(); one that finds no place is dropped with[Alex2ESP] error: directive of N bytes on <topic> dropped: ....-D<name>=<value>inbuild_flagschanges a limit.send()returnsfalsewhen the report was not sent: no session with the broker, the MQTT client or the heap cannot take it, or it is too large. Nothing is ever sent truncated, and nothing is dropped without a line on Serial. -
Serial output. Every line of the library starts with
[Alex2ESP], a problem with[Alex2ESP] error:.alexClient.setLogLevel(AlexaLogLevel::ERROR)leaves only the problems,AlexaLogLevel::NONEnothing; the default,AlexaLogLevel::INFO, adds the session, discovery and one line per directive ([Alex2ESP] ESP-01 <- Alexa.PowerController.TurnOn).AlexaLogLevel::DEBUG(sizes and free heap per message) has to be compiled in with-DALEX2ESP_LOG_MAX=3;-DALEX2ESP_LOG_MAX=0compiles every line out. The username, the password, the root topic and correlation tokens are never printed, at any level, so a log can be posted as it is: a topic appears without its root (ESP-01/alexaResponce), a directive under its endpoint id. A sketch that defines a macro namedDEBUG,ERRORorINFOcannot write the level of that name; it passes the number instead, for examplealexClient.setLogLevel(static_cast<AlexaLogLevel>(3))forDEBUG.
Boards that run 1.1.0 or older keep working: the backend still publishes the token on <root>/<endpointId>/alexaDirective_e and serves the HTTP routes they use.
Interface Types
The library describes these interfaces. addCapability() takes the row, AlexaInterfaces::PowerController, or the type as 1.x sketches write it, AlexaInterfaceType::POWER_CONTROLLER. A sketch links the rows it names and no others. The report helpers of 1.x, AddHealthProp and the others with a capital letter, stay as names of the ones in the table.
Row in AlexaInterfaces |
AlexaInterfaceType:: |
Version | Properties | Report helper |
|---|---|---|---|---|
EndpointHealth |
ENDPOINT_HEALTH |
3.1 | connectivity | addHealthProp |
PowerController |
POWER_CONTROLLER |
3 | powerState | addPowerControllerProp |
BrightnessController |
BRIGHTNESS_CONTROLLER |
3 | brightness | addBrightnessControllerProp |
ColorTemperatureController |
COLOR_TEMPERATURE_CONTROLLER |
3 | colorTemperatureInKelvin | addColorTemperatureControllerProp |
ColorController |
COLOR_CONTROLLER |
3 | color | addColorControllerProp(hue, saturation, brightness) |
PowerLevelController |
POWER_LEVEL_CONTROLLER |
3 | powerLevel | addPowerLevelControllerProp |
PercentageController |
PERCENTAGE_CONTROLLER |
3 | percentage | addPercentageControllerProp |
RangeController |
RANGE_CONTROLLER |
3 | rangeValue | addRangeControllerProp(instance, value) |
ModeController |
MODE_CONTROLLER |
3 | mode | addModeControllerProp(instance, mode) |
ToggleController |
TOGGLE_CONTROLLER |
3 | toggleState | addToggleControllerProp(instance, state) |
ThermostatController |
THERMOSTAT_CONTROLLER |
3.2 | targetSetpoint, lowerSetpoint, upperSetpoint, thermostatMode | addThermostatSetpointProp, addThermostatLowerSetpointProp, addThermostatUpperSetpointProp (both: addThermostatDualSetpointProp(lower, upper)), addThermostatModeProp |
TemperatureSensor |
TEMPERATURE_SENSOR |
3 | temperature | addTemperatureSensorProp(value, scale) |
HumiditySensor |
HUMIDITY_SENSOR |
3 | relativeHumidity | addHumiditySensorProp |
LockController |
LOCK_CONTROLLER |
3 | lockState | addLockControllerProp |
ContactSensor |
CONTACT_SENSOR |
3 | detectionState | addContactSensorProp |
MotionSensor |
MOTION_SENSOR |
3 | detectionState | addMotionSensorProp |
TimeHoldController |
TIME_HOLD_CONTROLLER |
3 | holdStartTime, holdEndTime | addProperty |
Speaker |
SPEAKER |
3 | volume, muted | addProperty |
PlaybackStateReporter |
PLAYBACK_STATE_REPORTER |
3 | playbackState | addProperty |
InputController |
INPUT_CONTROLLER |
3 | input | addProperty |
ChannelController |
CHANNEL_CONTROLLER |
3 | channel | addProperty |
InventoryLevelSensor |
INVENTORY_LEVEL_SENSOR |
3 | level | addProperty |
PlaybackController |
PLAYBACK_CONTROLLER |
3 | "properties": {} |
|
WakeOnLANController |
WAKE_ON_LAN_CONTROLLER |
3 | "properties": {} |
|
SceneController |
SCENE_CONTROLLER |
3 | no properties object |
|
DoorbellEventSource |
DOORBELL_EVENT_SOURCE |
3 | no properties object |
|
StepSpeaker |
STEP_SPEAKER |
3 | no properties object |
|
SimpleEventSource |
SIMPLE_EVENT_SOURCE |
1.0 | no properties object |
Every interface is announced with its version and properties; what else its discovery object needs (the configuration of a thermostat, the presets of a range) the sketch writes in the function it passes to setConfiguration(). A report helper is a function of its own, and a sketch links the ones it calls. A value outside of the range Alexa gives for its property (brightness, percentage, power level and humidity 0 to 100, color temperature 1000 to 10000, hue 0 to 360, saturation and brightness of a color 0 to 1) is reported as the nearest value of the range, with an error in the log. addProperty(row, place, value) adds a property that has no helper by its place in the row, 0 for the first: addProperty(AlexaInterfaces::Speaker, 1, muted) reports muted.
Every other value of AlexaInterfaceType (COOKING, LAUNCHER, SECURITY_PANEL_CONTROLLER, ...) names an interface the library has no row for. addCapability() adds nothing for it, prints [Alex2ESP] error: <endpointId>: capability not added: ... and returns nullptr.
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:
JsonDocument doc;
doc["namespace"] = "Alexa.PowerController";
doc["name"] = "powerState";
doc["value"] = "ON";
doc["uncertaintyInMilliseconds"] = 0;
AddContextProp(doc.as<JsonObject>())
AddContextProp sets timeOfSample to the current time when the object has none. (Until 1.1.0 a sketch wrote "{REPLACE_WITH_DATETIME}" there for the server to replace; the library now replaces that itself.)
Changelog
1.2.0
Behaviour changes:
- Directives arrive over MQTT. The library subscribes to
<root>/+/alexaDirective, where Alex2MQTT has always published the whole directive, instead of fetching it over HTTP with the token from<root>/<endpointId>/alexaDirective_e. The HTTP detour dates from 2024, when a directive larger than one TCP segment reached the MQTT callback in pieces; the pieces are now put together by their offset and the total length, in one heap block that lives untilloop()has parsed the directive.loop()no longer stalls for two HTTP round trips per directive. - Reports leave over MQTT.
send()publishes on<root>/<endpointId>/alexaResponceat once, where 1.1.0 queued the report for an HTTP POST from a laterloop(). It returnsfalsewhen there is no session with the broker, when the MQTT client or the heap cannot take the report, or when the report is over 3071 bytes (ALEX2ESP_MAX_MESSAGE, by default 1024 more than the largest directive, so that every directive that is accepted can be answered); each case prints its reason. The 5-slot send queue is gone. timeOfSampleis the board's own time in UTC, for example2026-09-28T13:05:09Z. 1.1.0 sent the placeholder{REPLACE_WITH_DATETIME}, which only the backend's HTTP route replaced.begin()starts SNTP (pool.ntp.org,time.nist.gov) and no longer connects itself:loop()opens the MQTT session once the clock is set, or after 5 s without an answer, so the session comes up a few seconds later than before.alexClient.setTimeSource(false)beforebegin()leaves the clock to the sketch.AddContextProp()fillstimeOfSamplein when the property has none or carries the old placeholder. The board needs an answer from an NTP server (DNS, outbound UDP port 123), which 1.1.0 did not: without one the reports carry a time in 1970, and the library says so when it connects and then at most once a minute while reports are sent.- A directive is parsed and handed to the sketch from
loop(), one per call, in the order of arrival. Up to eight directives of 8188 bytes together wait for it on the heap (ALEX2ESP_MAX_QUEUED_DIRECTIVES,ALEX2ESP_MAX_QUEUED_BYTES; 1.1.0 queued five tokens), because Alexa sends a group command as one directive per endpoint. A directive that finds no place is dropped, as is one over 2047 bytes (ALEX2ESP_MAX_DIRECTIVE); both print an error. - A directive that arrives twice is handled once: the Alex2MQTT broker currently delivers every message twice through a mirror. The repeat is recognised while it arrives, by a hash of its bytes, and takes no place among the waiting directives; a directive that comes again with other bytes is recognised by its
messageId. The last 16 directives are remembered; the repeat prints<endpointId>: repeated directive ignored. - The subscription delivers the directives of every endpoint of the account. Those for endpoints of another board are recognised by their topic and neither buffered nor parsed.
- Discovery is answered from
loop(), not inside the MQTT callback. A discovery object over 3071 bytes (ALEX2ESP_MAX_MESSAGE) is refused with an error; the other devices are still announced. getState()staysINITIALIZEDuntil the first connect, and becomesCONNECTEDwhen the broker has acknowledged both subscriptions (1.1.0: the first of them). A subscription that the broker refuses prints an error that names the topic (without its root) and what to check, the root topic passed tobegin(); the state staysSUBSCRIBING. When the MQTT client does not take a subscription, the session is closed and the next one subscribes again.- Serial output goes through one log with levels.
alexClient.setLogLevel()takesAlexaLogLevel::NONE,ERROR,INFO(the default) orDEBUG;-DALEX2ESP_LOG_MAX=<0..3>inbuild_flagssets the highest level that is compiled in (default 2,INFO). This replaces theAlex2ESP_DEBUGdefine insideAlexaUtils.cpp. 1.1.0 printed the memory figures for every MQTT message and the whole directive, correlation token included, for every directive; both are gone. A sketch or a build flag that definesDEBUG,ERRORorINFOas a macro (#define DEBUG 1,-DDEBUG) still compiles: the library sets those macros aside where it declares its levels and passes a level by its number everywhere else. - The session is opened again whatever ended it, and the reason is printed in words:
disconnected: the broker refused the username or the password, check the two passed to begin(); next attempt in 1 s. 1.1.0 reconnected every 5 s after a lost TCP connection only and printed nothing; a board with a wrong password, or one that met the broker while it was restarting, stayed offline until it was reset. The wait is 1 s at first and doubles with every attempt, up to 60 s; it is 1 s again after a session that lasted a minute, so a session that the broker accepts and closes at once does not bring the board back every second. Nothing is tried while Wi-Fi is down, and the first connect waits for Wi-Fi as well (waiting for Wi-Fi: ...is printed once). Loss of Wi-Fi printsWi-Fi is down, waiting for it, once per loss. A connect that has no answer after 30 s counts as failed and printsdisconnected: the broker did not answer within 30 s. The MQTT keep-alive is 30 s (1.1.0: the 15 s of the MQTT client). getDevice()beforebegin()prints an error: the device would have no root topic. A secondbegin()is ignored with an error.- A report is sent through the client that created its device. An
AlexaDeviceor anAlexaStatusMessagethat a sketch constructs itself has no client:send()returnsfalseand printsreport for <endpointId> not sent: its device was not created by getDevice(), where 1.1.0 queued the report. UsegetDevice()andbuildStatusMessage(). Both constructors take the client as an optional last argument, anAlexaTransport*. - New:
Alex2ESP::setServer(host, port)beforebegin(), for a broker other than the default; the host is not copied.Alex2ESP::setLogLevel(),Alex2ESP::setTimeSource(),AlexaDevice::hasEndpointId(),AlexaLog,AlexaSendResult, andAlexaTransport, the interface a device sends and stamps its reports through.Alex2ESPimplements it withpublish(topic, document), which sends aJsonDocumenton the session of the library under the same checks as a report and returns anAlexaSendResult, andtimestamp(buffer, size), which writes the current time astimeOfSamplehas it. - Removed: the queues and buffers of
AlexaUtils(enqueue,dequeue,dequeueVals,enqueueReceive,dequeueReceive,isQueueEmpty,isQueueFull,isReceiveQueueEmpty,isReceiveQueueFull,receivePayload,nextMessageId) and itslog/logln, which printed nothing unless the library was edited;AlexaUtils::printMemoryInfo()stays.MAX_STATUS_REPORT_SIZE(the limit isALEX2ESP_MAX_MESSAGE). The library no longer includesESP8266HTTPClient. - Interfaces are described by rows in program memory,
AlexaInterfaces::PowerControllerand 27 more (see Interface Types), in place of theswitchtables ofAlexaInterfaceUtils, whose strings took RAM in every sketch.addCapability()takes a row or, as before, anAlexaInterfaceType; a sketch links only the rows it names. The discovery objects of the five examples are the same byte for byte. - Versions and properties follow the interface pages of Alexa:
EndpointHealthis announced as 3.1 (1.1.0: 3.3) andThermostatControlleras 3.2 with its four properties (1.1.0: 3, none).Speaker,StepSpeaker,PlaybackStateReporter,InventoryLevelSensorandWakeOnLANControllerare announced as 3 (1.1.0: 1),SimpleEventSourceas 1.0. The interfaces that 1.1.0 announced with an empty list of properties have their properties (lockState,detectionState,mode,rangeValue,percentage, ...).SceneController,DoorbellEventSource,StepSpeakerandSimpleEventSourceare announced without apropertiesobject. - New:
AlexaInterfaceType::HUMIDITY_SENSORwith its row,AlexaInterfaces::HumiditySensor.AlexaDevice::getInterfaceType(namespace). addCapability()with a type that has no row adds nothing, prints an error and returnsnullptr. 1.1.0 announced such an interface as version 1 without properties.- The handler of
Eventgets the type of the capability of its device that the directive names, andAlexaInterfaceType::UNKNOWNfor a namespace the device has no capability for. 1.1.0 looked the namespace up among all interfaces. - Removed:
AlexaInterfaceType::AUTHORIZATION_CONTROLLERandAUTOMOTIVE_VEHICLE_DATA, which Alexa has withdrawn.AlexaInterfaceUtils(toString,fromString,getVersion,getProps): a row has the namespace, the version and the properties,alexaInterfaceRow(type)gives the row of a type. The constructor ofAlexaInterfacetakes a row.DisplayCategoryandAlexaInterfaceTypeare one byte wide. - New:
AlexaCapability, whichaddCapability()returns.addCapability(AlexaInterfaces::RangeController, "Blind.Lift")takes the instance of an interface that a device may have several of, and a device can have several capabilities of one such interface.addFriendlyAsset(),setConfiguration(),addStateMapping()andsetNonControllable()are new; every setter returns the capability.matches(namespace, instance)tells whether a directive is for the capability.AlexaInterfaceandAlexaActionsstay as names ofAlexaCapabilityandAlexaAction, so a 1.x sketch compiles as it is. - New:
AlexaDevice::onDirective(handler). The handler,void handler(AlexaDirective& d), gets every directive of its device with the device (d.device), the capability it is for (d.capability,d.type), itsns,name,instance,correlationTokenandpayload, and builds the answer withd.response(),d.stateReport(),d.error(type, message),d.deferred(seconds)or, for a scene,d.sceneStarted()andd.sceneStopped(); one function can serve several devices.registerEvent()stays for the handlers of 1.x, which a device calls when it has no handler ofonDirective().AlexaDevice::findCapability(namespace, instance). - New kinds of messages beside
ResponseandStateReport:ErrorResponse(AlexaErrorTypehas the types of Alexa,asErrorResponse(type, message)turns an answer into one),DeferredResponseand the answer that follows it (sendAsync(), on<root>/<endpointId>/deferredResponse),ChangeReport(AlexaDevice::changeReport(cause), on<root>/changeReport), the events of a scene, andDoorbellPress(AlexaDevice::doorbellPress(), orevent(row, name)for another interface, on<root>/event).AlexaDevice::response(token)andstateReport(token)build an answer outside of the handler.payload()gives the payload of a message,getKind()its kind. - The report helpers begin with a small letter:
addHealthProp,addPowerControllerProp,addBrightnessControllerProp,addColorTemperatureControllerProp,addToggleControllerProp(instance, state),addTemperatureSensorProp(value, scale),addContextProp. The names of 1.x stay, with their order of arguments. - New report helpers, one for every property of the interfaces a device on this board is likely to have:
addColorControllerProp,addPowerLevelControllerProp,addPercentageControllerProp,addRangeControllerProp,addModeControllerProp,addLockControllerProp(AlexaLockState),addContactSensorProp,addMotionSensorProp,addHumiditySensorProp,addThermostatSetpointProp,addThermostatLowerSetpointProp,addThermostatUpperSetpointProp,addThermostatDualSetpointPropandaddThermostatModeProp(AlexaThermostatMode);addProperty(row, place, value)for a property without one. See Interface Types. - A helper reports a value outside of the range of its property as the nearest value of the range and prints an error; 1.1.0 sent the value as it was. The brightness and the color temperature are taken as
int(1.1.0:unsigned int), so that a negative number is reported as the lowest value. - New:
AlexaAssetsandAlexaUnitsinsrc/AlexaResources.h, the 103 asset ids of the catalog of Alexa as strings in program memory.AlexaStatehas the statesEcoOn,EcoOff,Low,Empty,Full,DoneandStuckbesideOpenandClosed.addStateMapping()takes a number of any type,addStateMapping({AlexaState::Closed}, 0). - New:
AlexaCapability::isValid(),falseafter a setter has refused what it was given. A capability that is not valid is left out of discovery with an error; before, it was announced without what had been refused. - A temperature is reported in the scale it is given in:
AddTemperatureSensorProp(TemperatureSensorScale::FAHRENHEIT, 69)reports 69FAHRENHEIT, where 1.1.0 reported 20.56CELSIUS.TemperatureSensorScale::KELVINis new. messageIdis a UUID of version 4 from the random source of the hardware. 1.1.0 sent 37 characters fromrand(), seeded with the time in seconds: two messages of one second had the same id.- A
ChangeReportor an event that a handler sends is not taken for the answer to its directive. - A directive that nothing answers is answered by the library with the ErrorResponse
INVALID_DIRECTIVE: when the device has no handler for it, and when it names a capability that the device does not have and the handler sent nothing. 1.1.0 left both to the timeout, after which Alexa says that the device does not respond. A handler that sends nothing for a capability of its device prints an error; nothing is sent for it. - Devices are a linked list, and a device holds its capabilities in an array of 8 pointers (
ALEX2ESP_MAX_CAPABILITIES); both were astd::deque. A ninth capability is refused with an error andaddCapability()returnsnullptr.getDevice()returnsnullptrwith an error when the heap has no room for the device. AnAlexaDevicecannot be copied. A directive whose topic names a device of the board and whoseendpointIddoes not is ignored with a line atDEBUG(it was an error). - A capability takes 116 bytes of heap and holds 3 friendly names, 4 action mappings and 2 state mappings (
ALEX2ESP_MAX_FRIENDLY_NAMES,ALEX2ESP_MAX_ACTION_MAPPINGS,ALEX2ESP_MAX_STATE_MAPPINGS); one more is refused with an error. The instance and the text of a friendly name are copied, as before. The locale and the directive name and payload of anActionMappingare kept as pointers, where 1.1.0 copied them: pass literals. AnActionMappingannounces its actions in the order ofAlexaAction. - What Alexa does not accept is refused with an error that says what to change.
addCapability(row)for an interface with instances and no instance, or with an instance for an interface without, adds nothing and returnsnullptr. A capability with instances that has no instance or no friendly name when the device is announced (a 1.x sketch sets both afteraddCapability(type)) is left out of discovery. Action and state mappings are taken byRangeController,ModeControllerandToggleControlleronly. PlaybackControllerandWakeOnLANControllerare announced with"properties": {}, as their pages show them.- Removed:
AlexaInterface::getJSON()(toJson()adds the capability to the capabilities of its endpoint) andgetProps()(the row has the properties),ActionMapping::getJSON()and theStringandstd::vectormembers ofActionMapping, the classFriendlyName.
Memory: examples/basicLight.cpp for a D1 mini takes 30,520 bytes of static RAM (1.1.0: 52,768) and 333,389 bytes of flash (1.1.0: 350,885), as PlatformIO reports them (espressif8266 4.2.1, Arduino core 3.1.2). 18,260 bytes of the static RAM were the five 2 KB queue slots, three more 2 KB buffers and the two HTTP clients; 3,720 were the names, versions and properties of all interfaces and the names of the display categories, which are in flash now. SNTP and the time stamp are 1.8 KB of the flash figure. The instance, names, configuration and mappings of a capability are 2.0 KB of it, the directive handler and the ErrorResponse 0.9 KB, the kinds of messages and the UUID 1.3 KB. A kind that a sketch does not send and a report helper that it does not call are not linked.
Tests: pio test -e native in the repository runs 130 host tests: 52 of the receive and publish logic (reassembly of fragments, the directives that wait for loop(), repeated directives, the size limits, a heap without room, the wait between reconnects, topics, time stamps, the message id), 35 of discovery (the discovery object of every example against what 1.1.0 announced, the 28 rows against the interface pages, a type without a row, the discovery objects of a range, a mode and a toggle controller, a scene and a doorbell, what a capability refuses and that it is left out afterwards, the states and the asset ids), 11 of dispatch (the device and the capability a directive reaches, one handler for two devices, the handlers of 1.x, a capability more than a device holds, the answers to a directive for a capability the device lacks and to one without a handler) and 32 of the messages (every kind against the JSON and the topic it is published with, every report helper against the JSON of its property, values outside of their range, the names of 1.x, the scale of a temperature, what send() and sendAsync() refuse). No board is needed.
Packaging: library.json, library.properties and the softwareVersion and firmwareVersion that a device reports in discovery say 1.2.0; the library has the number in one place, ALEX2ESP_VERSION in src/AlexaVersion.h. The platformio.ini of the repository is for the host tests; a sketch does not need it.
Boards that run 1.1.0 are not affected: the backend keeps the token topic and the HTTP routes.
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 fromloop()as the client's queue drains, for up to 5 s. 1.0.0 pushed them one perloop()through a 5-slot HTTP queue and silently dropped the sixth device onwards, which the backend then removed from Alexa. AlexaStatusMessage::send()returnsbool: 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:
semanticsis emitted only when a capability has action mappings; anActionMappingdirective payload is emitted as a JSON object (parsed from the text you pass) or omitted when empty, instead of the string"{}". Response/StateReportevents include the required"payload": {}.- Devices and capabilities are stored in
std::deque; pointers fromgetDevice()/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.DirectiveReceivedno longer prints "No event registered" when the sketch has not registered it.- Removed:
Alex2ESP::messageSplitAndSend(declared, never defined) andAlexaStatusMessage::setEndpointId(did nothing). - Reported
softwareVersion/firmwareVersionin the discovery attributes are now1.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. pio test -e native in the repository root runs the host tests (no board needed); they have to pass.
License
This project is licensed under the MIT License. See the LICENSE file for details.