Alex2ESP firmware — imported with full history from github.com/chaos511/Alex2ESP (2026-09-18)
Find a file
David f9c787a67a Sizes of the examples measured again after the doorbell and scene change
All 17 sketches rebuilt for d1_mini at 50467ce, 0 warnings. Static RAM is
unchanged; flash grew by 140 to 352 bytes per sketch (Light 332,705, was
332,565). The tables of the readme and of examples/README.md and the line
of the changelog carry the new numbers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 22:38:57 +00:00
examples Sizes of the examples measured again after the doorbell and scene change 2026-09-28 22:38:57 +00:00
src Announce a doorbell with proactivelyReported and a scene with supportsDeactivation 2026-09-28 22:20:34 +00:00
test Announce a doorbell with proactivelyReported and a scene with supportsDeactivation 2026-09-28 22:20:34 +00:00
.gitattributes 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. 2026-09-28 04:58:38 +00:00
.gitignore Drop the HTTP fallback: directives arrive on <root>/<id>/alexaDirective and reports leave over MQTT 2026-09-28 14:31:57 +00:00
CHANGELOG.md Sizes of the examples measured again after the doorbell and scene change 2026-09-28 22:38:57 +00:00
keywords.txt Announce a doorbell with proactivelyReported and a scene with supportsDeactivation 2026-09-28 22:20:34 +00:00
library.json Examples of 1.x move to examples/legacy, one sketch folder each 2026-09-28 21:40:25 +00:00
library.properties Version 1.2.0 in library.json, library.properties and the discovery attributes 2026-09-28 16:02:52 +00:00
LICENSE 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. 2026-09-28 04:58:38 +00:00
platformio.ini AlexaStatusMessage: DeferredResponse, ChangeReport, ErrorResponse, scene and doorbell events, UUID-shaped messageId 2026-09-28 20:35:56 +00:00
readme.md Sizes of the examples measured again after the doorbell and scene change 2026-09-28 22:38:57 +00:00

Alex2ESP

Alex2ESP is an Arduino/PlatformIO library that makes an ESP8266 a set of Amazon Alexa smart home devices. The sketch declares devices and their capabilities (a lamp with PowerController, a blind with a RangeController); the library announces them to Alexa, hands every directive to a handler of the sketch and sends the answers and reports. It talks to Alexa through the Alex2MQTT skill, over MQTT only.

One of the most popular libraries for controlling ESP devices with Alexa is FauxmoESP. It works without an account by emulating a light bulb, so it is limited to the commands of one. Alex2ESP needs the Alex2MQTT account and announces the device as what it is.


What it needs

  • An Alex2MQTT account. Log in with Amazon at https://alex2mqtt.stormysdream.club/. The page shows the three credentials a sketch passes to begin(): the MQTT user name, the MQTT password and the root topic.
  • An ESP8266 board with the Arduino core 3.x.
  • Two libraries: AsyncMqttClient 0.9.x (which needs ESPAsyncTCP) and ArduinoJson 7.
  • The network: outbound TCP port 1883 to the broker, and DNS and outbound UDP port 123 for the time of day (pool.ntp.org, time.nist.gov).

Supported boards

Board State
ESP8266 Developed and measured on a Wemos D1 mini (PlatformIO, espressif8266 4.2.1, Arduino core 3.1.2). Every figure in this readme is from that board or that build.
ESP32 Untested. The sources have #ifdef branches for it, but the library has never been compiled for an ESP32 or run on one. The examples include ESP8266WiFi.h, and library.json and library.properties name the ESP8266 only.

Quick start: PlatformIO

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.

[env:d1_mini]
platform = espressif8266
board = d1_mini
framework = arduino
monitor_speed = 74880
lib_deps =
    https://git.stormysdream.club/platformio/Alex2ESP.git#v2.0.0
    marvinroger/AsyncMqttClient@^0.9.0
    bblanchon/ArduinoJson@^7

#v2.0.0 pins the release; without it PlatformIO takes main. 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.

Copy examples/Light/Light.ino into src/ of the project, fill in the five constants at its top and upload.

Quick start: Arduino IDE

  1. Download the repository as a ZIP (https://git.stormysdream.club/platformio/Alex2ESP/archive/v2.0.0.zip) and add it with Sketch -> Include Library -> Add .ZIP Library.
  2. Install AsyncMqttClient, ArduinoJson (7.x) and ESP Async TCP (by ESP32Async) from the Library Manager.
  3. Install the ESP8266 board package and select your board (for example LOLIN(WEMOS) D1 R2 & mini).
  4. Open File -> Examples -> Alex2ESP -> Light, fill in the five constants at its top and upload.

The examples are built with PlatformIO. They are laid out as the Arduino IDE expects them, one folder per sketch, but they have not been built with the IDE yet.

The Light example

examples/Light/Light.ino, complete:

// Light: a lamp that Alexa switches on and off (Alexa.PowerController).
//
// The lamp is the on-board LED. One handler gets every directive of the device: it answers ReportState with the
// state of the lamp, carries out TurnOn and TurnOff, and refuses anything else.
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <Alex2ESP.h>

// Wi-Fi and the credentials of your Alex2MQTT account
const char *WIFI_SSID = "";
const char *WIFI_PASSWORD = "";

const char *ALEXA_USERNAME = "";
const char *ALEXA_PASSWORD = "";
const char *ALEXA_ROOT_TOPIC = "";

// The on-board LED: GPIO2 on a Wemos D1 mini, lit when the pin is low
#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif

Alex2ESP alexa;
bool lampOn = false;

// Joins the Wi-Fi network and returns after 30 s at the latest. Without a connection the sketch carries on: the
// ESP8266 keeps trying, and alexa.loop() opens the MQTT session once Wi-Fi is up.
void connectWiFi()
{
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.printf("\n[WIFI] Connecting to \"%s\"\n", WIFI_SSID);

  unsigned long started = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - started < 30000)
  {
    delay(100);
  }

  if (WiFi.status() == WL_CONNECTED)
  {
    Serial.printf("[WIFI] Connected, IP address %s\n", WiFi.localIP().toString().c_str());
  }
  else
  {
    Serial.printf("[WIFI] No connection after 30 s (status %d): check WIFI_SSID and WIFI_PASSWORD. Still trying.\n",
                  WiFi.status());
  }
}

// The state of the lamp: what the answer to a directive and the answer to ReportState carry
void sendState(AlexaStatusMessage message)
{
  message.addHealthProp(EndpointHealth::OK)
      .addPowerControllerProp(lampOn ? PowerController::ON : PowerController::OFF)
      .send();
}

void onDirective(AlexaDirective &directive)
{
  if (directive.isReportState())
  {
    sendState(directive.stateReport());
    return;
  }

  if (directive.type == AlexaInterfaceType::POWER_CONTROLLER && (directive.is("TurnOn") || directive.is("TurnOff")))
  {
    lampOn = directive.is("TurnOn");
    digitalWrite(LED_BUILTIN, lampOn ? LOW : HIGH);
    sendState(directive.response());
    return;
  }

  directive.error(AlexaErrorType::INVALID_DIRECTIVE, "This lamp switches on and off").send();
}

void setup()
{
  Serial.begin(74880);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);

  connectWiFi();

  // MQTT user name, MQTT password, root topic
  alexa.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);

  // The name Alexa shows, and the id of the endpoint: every device of an account has its own
  AlexaDevice *lamp = alexa.getDevice("Desk Lamp", "esp-light");
  lamp->setDisplayCategory(DisplayCategory::LIGHT);
  lamp->addCapability(AlexaInterfaces::PowerController);
  lamp->addCapability(AlexaInterfaces::EndpointHealth);
  lamp->onDirective(onDirective);
}

void loop()
{
  alexa.loop();
}

After the upload, let Alexa discover devices. The serial monitor shows the session, the discovery and one line per directive.

The other examples, one folder per kind of device: DimmableLight, ColorTemperatureLight, ColorLight, TemperatureSensor, ContactSensor, Blind, Thermostat, Lock, Scene, Doorbell and MultiDevice. examples/README.md says what each does. The examples of 1.x are in examples/legacy/.


Writing a sketch

A sketch has one Alex2ESP, calls begin(username, password, rootTopic) once and loop() from its own loop(). getDevice(name, endpointId) creates a device, addCapability() gives it an interface, onDirective() its handler. The code blocks of this section are excerpts of the examples; the file is named under each.

Devices and capabilities

getDevice() is called after begin(). The endpoint id is what Alexa knows the device by: two boards with the same endpoint id on one account are one device to Alexa. The pointers returned by getDevice() and addCapability() stay valid for the lifetime of the client. getDevice() returns nullptr, with an error in the log, when the heap has no room for another device. The examples use the pointer unchecked, because they create their devices in setup(); a sketch that creates devices by the dozen, or later than setup(), checks it.

A device holds 8 capabilities. Every device of the examples has EndpointHealth and reports connectivity with its state.

The handler

onDirective() registers a function that gets every directive of its device, ReportState included. AlexaDirective has:

Member What it is
device the device the directive is for
capability, type the capability of the device with the namespace and the instance of the directive; nullptr and AlexaInterfaceType::UNKNOWN when the device has none, and for ReportState
ns, name, instance "Alexa.PowerController", "TurnOn", "Blind.Lift" ("" for an interface without instances)
correlationToken what the answer has to repeat; the library does that
payload the payload of the directive, directive.payload["rangeValue"]
is(name), isReportState() compare the name of the directive
response(), stateReport(), error(type, message), deferred(seconds), sceneStarted(), sceneStopped() build the answer; send() sends it

The texts are those of the directive and valid until the handler returns. The backend waits 7 s for the answer to a directive, so the handler answers before it returns.

One function can serve several devices; the directive says which one it is for:

void onDirective(AlexaDirective &directive)
{
  int lamp = directive.device == lamps[1] ? 1 : 0;

  if (directive.isReportState())
  {
    sendState(directive.stateReport(), lamp);
    return;
  }

examples/MultiDevice/MultiDevice.ino

What the library does when the handler sends nothing:

Case Answer Log
the device has no handler ErrorResponse INVALID_DIRECTIVE ... refused as INVALID_DIRECTIVE: the device has no handler, give it one with onDirective()
the directive names a capability the device does not have ErrorResponse INVALID_DIRECTIVE ... refused as INVALID_DIRECTIVE: the device has no such capability, and its handler sent no answer
the directive is for a capability of the device none; Alexa reports that the device does not respond ... the handler sent no answer to ...

Capabilities with an instance

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; the setters of the capability return the capability, so they can be chained. What the discovery object needs beside the names (the range of a value, the modes) the sketch writes in the function it passes to setConfiguration().

// The "configuration" of the capability in discovery: the range of the value and its unit
void liftConfiguration(JsonObject configuration, void *)
{
  JsonObject range = configuration["supportedRange"].to<JsonObject>();
  range["minimumValue"] = 0;
  range["maximumValue"] = 100;
  range["precision"] = 1;
  configuration["unitOfMeasure"] = FPSTR(AlexaUnits::Percent);
}
  blind->addCapability(AlexaInterfaces::RangeController, LIFT)
      ->addFriendlyAsset(AlexaAssets::Setting_Opening)
      .setConfiguration(liftConfiguration)
      .addActionMapping(ActionMapping({AlexaAction::Close}, "SetRangeValue", "{\"rangeValue\":0}"))
      .addActionMapping(ActionMapping({AlexaAction::Open}, "SetRangeValue", "{\"rangeValue\":100}"))
      .addActionMapping(ActionMapping({AlexaAction::Lower}, "AdjustRangeValue",
                                      "{\"rangeValueDelta\":-10,\"rangeValueDeltaDefault\":false}"))
      .addActionMapping(ActionMapping({AlexaAction::Raise}, "AdjustRangeValue",
                                      "{\"rangeValueDelta\":10,\"rangeValueDeltaDefault\":false}"))
      .addStateMapping({AlexaState::Closed}, 0)
      .addStateMapping({AlexaState::Open}, 1, 100);

examples/Blind/Blind.ino

  • An ActionMapping maps words of Alexa ("open", "close", "raise", "lower") to a directive: the actions, the name of the directive and its payload as JSON text. The Alexa documentation lists the actions. Action and state mappings are taken by the three generic controllers only.
  • 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. addFriendlyName("Lift", "en-US") adds a name as text. An id that the sketch does not name takes no flash.
  • A capability holds 3 names, 4 action mappings and 2 state mappings. 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.
  • 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, for an interface the library has no row for, and for a ninth capability: check the pointer when the instance is not a literal.
  • setProactivelyReported(true) announces that the sketch sends ChangeReports for the capability, setNonControllable(true) that it reports its state and takes no directives.

Reports

response() answers a directive that changed something, stateReport() answers ReportState. Both take the properties of the device, one helper per property (see Interface Types), and are sent with send(). Every property carries the board's UTC time as timeOfSample; the last argument of a helper is the time since the value was read, in milliseconds:

    directive.stateReport()
        .addHealthProp(EndpointHealth::OK)
        .addTemperatureSensorProp(temperature, TemperatureSensorScale::CELSIUS, millis() - lastRead)
        .send();

examples/TemperatureSensor/TemperatureSensor.ino

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.

send() returns false when the message was not sent: no session with the broker, the MQTT client or the heap cannot take it, or it is over ALEX2ESP_MAX_MESSAGE. Each case prints its reason. Nothing is sent truncated.

Change reports

A ChangeReport tells Alexa that a property changed without a directive: a door opened, somebody pressed the switch on the lamp. The capability is announced with setProactivelyReported(true). The properties added before context() are those that changed, the ones after it the others. A ChangeReport without a property that changed is not sent and prints an error.

    contact->changeReport(AlexaCause::PHYSICAL_INTERACTION)
        .addContactSensorProp(isOpen)
        .context()
        .addHealthProp(EndpointHealth::OK)
        .send();

examples/ContactSensor/ContactSensor.ino

A sketch that reports a value that drifts limits how often it does so, and keeps what it reported only when the report left:

  bool sent = sensor->changeReport(AlexaCause::PERIODIC_POLL)
                  .addTemperatureSensorProp(temperature)
                  .context()
                  .addHealthProp(EndpointHealth::OK)
                  .send();
  if (sent)
  {
    reported = temperature;
    everReported = true;
    lastReport = millis();
  }

examples/TemperatureSensor/TemperatureSensor.ino

Deferred answers

A device that takes longer than the 7 s answers with a DeferredResponse, keeps a copy of the correlation token and sends the answer itself when it is done, with sendAsync(). Alexa accepts a deferred answer for LockController and WakeOnLANController only.

  goal = directive.is("Lock") ? AlexaLockState::LOCKED : AlexaLockState::UNLOCKED;
  token = directive.correlationToken; // a copy: the text of the directive ends with the handler
  moving = true;
  movingSince = millis();
  digitalWrite(BOLT_PIN, goal == AlexaLockState::LOCKED ? HIGH : LOW);

  directive.deferred(5).send();
  if (moving && millis() - movingSince >= BOLT_MS)
  {
    moving = false;
    state = digitalRead(BLOCKED_PIN) == LOW ? AlexaLockState::JAMMED : goal;
    doorLock->response(token).addHealthProp(EndpointHealth::OK).addLockControllerProp(state).sendAsync();
  }

examples/Lock/Lock.ino: the handler, and loop()

Errors

error(type, message) answers a directive that was not carried out. AlexaErrorType has the types of Alexa; the message is for the log of the skill. What a type carries beside it goes into payload():

    int position = directive.payload["rangeValue"] | -1;
    if (position < 0 || position > 100)
    {
      AlexaStatusMessage error = directive.error(AlexaErrorType::VALUE_OUT_OF_RANGE, "The blind takes 0 to 100");
      error.payload()["validRange"]["minimumValue"] = 0;
      error.payload()["validRange"]["maximumValue"] = 100;
      error.send();
      return;
    }

examples/Blind/Blind.ino

The types of a thermostat (REQUESTED_SETPOINTS_TOO_CLOSE, THERMOSTAT_IS_OFF, ...) are sent in the namespace Alexa.ThermostatController; examples/Thermostat/Thermostat.ino sends THERMOSTAT_IS_OFF, DUAL_SETPOINTS_UNSUPPORTED, REQUESTED_SETPOINTS_TOO_CLOSE and UNSUPPORTED_THERMOSTAT_MODE.

Scenes

A scene has no state to report. Activate is answered with sceneStarted() in place of a Response, Deactivate with sceneStopped():

  if (directive.is("Activate"))
  {
    setScene(true);
    directive.sceneStarted().send();
  }
  else if (directive.is("Deactivate"))
  {
    setScene(false);
    directive.sceneStopped().send();
  }

examples/Scene/Scene.ino

Doorbells

A doorbell sends an event and takes no directives:

    if (pressed)
    {
      Serial.println("[DOORBELL] pressed");
      doorbell->doorbellPress().send();
    }

examples/Doorbell/Doorbell.ino

device->event(row, name) builds the event of another interface.


Interface Types

The library describes 28 interfaces, each with a row in program memory (src/AlexaInterfaces.cpp). 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, and the report helpers it calls and no others.

The last column says whether Alexa discovered and drove the interface from this library, on a Wemos D1 mini with a real Alexa account through the public Alex2MQTT service, on 2026-09-28. "no" means not tried, not that it failed.

Row in AlexaInterfaces Version Properties Report helper Example Driven through Alexa
EndpointHealth 3.1 connectivity addHealthProp all not checked on its own
PowerController 3 powerState addPowerControllerProp Light yes
BrightnessController 3 brightness addBrightnessControllerProp DimmableLight yes
ColorTemperatureController 3 colorTemperatureInKelvin addColorTemperatureControllerProp ColorTemperatureLight yes
ColorController 3 color addColorControllerProp(hue, saturation, brightness) ColorLight no
PowerLevelController 3 powerLevel addPowerLevelControllerProp no
PercentageController 3 percentage addPercentageControllerProp no
RangeController 3 rangeValue addRangeControllerProp(instance, value) Blind yes, one instance
ModeController 3 mode addModeControllerProp(instance, mode) yes, one instance
ToggleController 3 toggleState addToggleControllerProp(instance, state) yes, one instance
ThermostatController 3.2 targetSetpoint, lowerSetpoint, upperSetpoint, thermostatMode addThermostatSetpointProp, addThermostatLowerSetpointProp, addThermostatUpperSetpointProp (both: addThermostatDualSetpointProp(lower, upper)), addThermostatModeProp Thermostat no
TemperatureSensor 3 temperature addTemperatureSensorProp(value, scale) TemperatureSensor no
HumiditySensor 3 relativeHumidity addHumiditySensorProp no
LockController 3 lockState addLockControllerProp Lock no
ContactSensor 3 detectionState addContactSensorProp ContactSensor no
MotionSensor 3 detectionState addMotionSensorProp no
TimeHoldController 3 holdStartTime, holdEndTime addProperty no
Speaker 3 volume, muted addProperty no
PlaybackStateReporter 3 playbackState addProperty no
InputController 3 input addProperty no
ChannelController 3 channel addProperty no
InventoryLevelSensor 3 level addProperty no
PlaybackController 3 "properties": {} no
WakeOnLANController 3 "properties": {} no
SceneController 3 no properties object Scene no
DoorbellEventSource 3 no properties object Doorbell no
StepSpeaker 3 no properties object no
SimpleEventSource 1.0 no properties object no

The six interfaces with "yes" were driven with another sketch than the examples, which have not run on a board yet. The directives were answered within 0.2 to 0.4 s, and a directive that was delivered twice was answered once. Voice commands have not been tried.

The Alex2MQTT service itself has carried more than that. With its Node.js client, alex2node 1.5.2, on the same day: Power, Brightness, Color, ColorTemperature, Percentage, PowerLevel, Thermostat, Range, Mode, Toggle, Lock and Scene, deferred responses, ErrorResponse, ReportState, and ChangeReports for contact, motion, lock, temperature and power. So the way from the broker to Alexa is tried for these; what this library sends for them is checked by the host tests only.

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. addContextProp(object) adds a property that the sketch has written as JSON.

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.


The MQTT contract

Everything goes over MQTT, to port 1883 of alex2mqtt.stormysdream.club or of the broker named with setServer(). The library makes no HTTP requests. It subscribes with QoS 1 and publishes with QoS 0.

Topic Direction What
<root>/discover in the request to announce the devices
<root>/discover_r out one discovery object per device, one message each
<root>/<endpointId>/alexaDirective in the directive as JSON; subscribed as <root>/+/alexaDirective
<root>/<endpointId>/alexaResponce out Response, StateReport, ErrorResponse, DeferredResponse, ActivationStarted, DeactivationStarted
<root>/<endpointId>/deferredResponse out the answer after a DeferredResponse (sendAsync())
<root>/changeReport out ChangeReport
<root>/event out DoorbellPress and the events of event(row, name)

Discovery. 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 next loop(). 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 discovery deferred at <endpointId> and sends the rest from loop() as the queue drains, for up to 5 s after the request.

Directives. A directive larger than one TCP segment arrives in fragments, which are put together in one heap block that exists until loop() has parsed it. Every call of loop() handles one directive, in the order of arrival.

Sizes.

Limit Default Build flag
largest directive 2047 bytes ALEX2ESP_MAX_DIRECTIVE
largest message sent: a report, or the discovery object of one device 3071 bytes, the largest directive plus 1024 ALEX2ESP_MAX_MESSAGE
directives that wait for loop() 8 ALEX2ESP_MAX_QUEUED_DIRECTIVES
bytes they take together 8188, four times the largest directive ALEX2ESP_MAX_QUEUED_BYTES

Alex2MQTT publishes directives of 600 to 900 bytes; most of that is the correlation token. An answer repeats the token and adds 140 to 170 bytes per property, so the largest directive can be answered with six properties. A directive over the limit, and one that finds no place among the waiting ones, is dropped with an error in the log.

The queue. Alexa sends a group command ("turn off the kitchen") as one directive per endpoint, and they arrive faster than a busy sketch calls loop(). Up to eight wait for it.

Duplicates. 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 in the queue; a directive that comes again with other bytes is recognised by its messageId. The last 16 directives are remembered.

Other boards. 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.

Messages. Every message has a messageId of its own, a UUID of version 4 from the random source of the hardware.

Boards with 1.x. 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.


Session and log lines

begin() starts SNTP 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 from a time server. getState() is Alex2ESPState::CONNECTED when the broker has acknowledged both subscriptions.

A session that ends or cannot be opened is opened again, whatever ended it, for as long as Wi-Fi is up:

wait before the next attempt 1 s, then 2 s, 4 s, ... up to 60 s
the wait starts at 1 s again after a session that lasted a minute
an attempt without an answer given up after 30 s
MQTT keep-alive 30 s
while Wi-Fi is down nothing is tried; the session is opened when Wi-Fi is back

Tried on a D1 mini against a scripted broker (2026-09-28): the waits of 1 to 60 s, a refused password, a broker that does not answer, and loss of Wi-Fi.

Every line of the library starts with [Alex2ESP], a problem with [Alex2ESP] error:. The user name, 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-light/alexaResponce), a directive under its endpoint id.

Line What it means What to do
connected to <host>, subscribing the broker accepted the session
ready: N device(s) both subscriptions are acknowledged
discovery: N of M device(s) announced the answer to a discovery request
esp-light <- Alexa.PowerController.TurnOn a directive was handed to the handler
waiting for Wi-Fi: the sketch has to join a network with WiFi.begin() loop() runs and Wi-Fi is not up; printed once check the SSID and the password of the sketch
error: Wi-Fi is down, waiting for it the link was lost; printed once per loss nothing: the session is opened again when Wi-Fi is back
error: disconnected: the broker refused the username or the password, ...; next attempt in N s the first two arguments of begin() are not those of the account copy them again from the Alex2MQTT page. This is the reason to look for when a board never shows up in Alexa
error: disconnected: the broker cannot be reached or the connection was lost; ... no TCP connection, or it ended check the network and port 1883; the library tries again
error: disconnected: the broker did not answer within 30 s; ... the attempt was given up as above
error: the broker refused the subscription to <root>/discover: check the root topic passed to begin() the root topic is not the one of the account; the state stays SUBSCRIBING correct the third argument of begin()
error: the clock is not set, reports carry a time in 1970: ... no answer from a time server; printed when the session opens and at most once a minute while reports are sent allow DNS and outbound UDP port 123, or set the clock in the sketch and call setTimeSource(false)
discovery deferred at <endpointId> the heap is short, the rest is sent from loop() nothing
error: discovery gave up: N device(s) not announced those devices missed this answer; on the backend's proactive discovery that can remove them from Alexa until the next one fewer devices on the board, or more free heap
error: device <endpointId> not announced: its discovery object has N bytes, ... the object is over ALEX2ESP_MAX_MESSAGE fewer capabilities or mappings on the device, or a higher limit
error: <endpointId>: directive of N bytes dropped: ... over ALEX2ESP_MAX_DIRECTIVE, no place in the queue, or no memory call loop() more often, or raise the limit
<endpointId>: repeated directive ignored the directive arrived a second time nothing
error: <endpointId>: the handler sent no answer to ... the handler returned without send() answer with response() or error()
error: <topic>: N bytes not sent, ... send() returned false: no session, the MQTT client or the heap is full, or the message is over ALEX2ESP_MAX_MESSAGE

Memory

Static RAM and flash as PlatformIO reports them for d1_mini (espressif8266 4.2.1, Arduino core 3.1.2, AsyncMqttClient 0.9.0, ArduinoJson 7.4.3), built from the examples as committed, with empty credentials and the default log level. A D1 mini has 81,920 bytes of RAM; what is not static is the heap.

Sketch Static RAM (bytes) Flash (bytes)
Light 30,616 332,705
DimmableLight 30,692 336,905
ColorTemperatureLight 30,808 337,629
ColorLight 30,748 338,797
TemperatureSensor 30,572 333,353
ContactSensor 30,584 333,017
Blind 30,860 336,665
Thermostat 31,044 337,657
Lock 30,648 333,597
Scene 30,640 333,629
Doorbell 30,584 333,121
MultiDevice 30,672 332,865
legacy/basicLight 30,520 334,129
legacy/lightWithBrightness 30,648 338,069
legacy/lightWithColorTemp 30,828 338,793
legacy/tempSensor 30,412 332,921
legacy/blindControl 30,568 335,417

For comparison: basicLight with 1.1.0 took 52,768 bytes of static RAM and 350,885 of flash. A sketch with Wi-Fi and Serial and nothing else takes 28,152 and 269,771 with the same toolchain, one that also uses the two dependencies 29,000 and 299,401.

Devices and capabilities are on the heap, a capability with 116 bytes. MultiDevice, with two devices, has 56 bytes of static RAM more than Light: the arrays of the sketch.

What a capability costs

Measured on the prototype of the 2.0 design, not on the release: a lamp with PowerController and EndpointHealth (29,412 bytes of static RAM, 329,293 of flash) plus one capability as a sketch uses it, with its addCapability(), the handling of its directives and its report helper in the Response and the StateReport. The figures of the release differ by some bytes; the table shows what is cheap and what is not.

+ capability Static RAM Flash
TemperatureSensor, report only +0 +356
ContactSensor with its ChangeReport +0 +368
ToggleController, instance and name +32 +620
LockController with the deferred answer +24 +984
SceneController +48 +1,000
ColorTemperatureController +88 +1,280
ColorController +48 +1,316
BrightnessController, PowerLevelController, PercentageController, each +56 +1,376
ModeController, two modes with their names +116 +2,640
RangeController, range, one action and one state mapping +136 +2,872
ThermostatController with TemperatureSensor +204 +3,112

An interface that the sketch does not name costs nothing: the ESP8266 core links with --gc-sections, in PlatformIO and in the Arduino IDE, and every row is an object of its own. In the examples the step from Light to DimmableLight is larger than the table says (+4,200 bytes of flash) because the sketch also begins to use analogWrite().

Build flags that change the figures

Flag Effect, measured on legacy/basicLight (30,520 / 333,989)
-DALEX2ESP_LOG_MAX=0 no log lines compiled in: 30,496 / 327,953
-DALEX2ESP_LOG_MAX=3 the DEBUG lines compiled in: 30,520 / 334,477
-DALEX2ESP_MAX_CAPABILITIES=<n> 4 bytes of heap per place in every device
-DALEX2ESP_MAX_FRIENDLY_NAMES=<n>, -DALEX2ESP_MAX_ACTION_MAPPINGS=<n>, -DALEX2ESP_MAX_STATE_MAPPINGS=<n> 8 or 12 bytes of heap per place in every capability

Configuration

  // Another broker than alex2mqtt.stormysdream.club:1883. The text is not copied: a literal, or a buffer that stays
  alexa.setServer("broker.example.org", 1883);

  // The sketch sets the clock itself; the library then leaves SNTP alone
  alexa.setTimeSource(false);

  // NONE, ERROR, INFO (the default) or DEBUG; DEBUG needs -DALEX2ESP_LOG_MAX=3
  alexa.setLogLevel(AlexaLogLevel::DEBUG);

  // Where the lines go: any Print, Serial unless changed
  AlexaLog::setOutput(&Serial);

  alexa.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);

test/readme/Configuration/Configuration.ino

Call When What
setServer(host, port) before begin() another broker, for example your own instance of Alex2MQTT. After begin() it is ignored with an error
setTimeSource(false) before begin() for a sketch with its own configTime() or an RTC
setLogLevel(level) any time ERROR leaves only the problems, NONE nothing; INFO adds the session, discovery and one line per directive; DEBUG adds sizes and free heap per message
AlexaLog::setOutput(print) any time where the lines go; Serial unless changed, nullptr for nowhere

The user name and the password passed to begin() are not copied either.

The limits are build flags, set for the whole project in platformio.ini:

build_flags =
    -DALEX2ESP_LOG_MAX=3
    -DALEX2ESP_MAX_CAPABILITIES=12
    -DALEX2ESP_MAX_DIRECTIVE=3071
Flag Default Range
ALEX2ESP_LOG_MAX 2 (INFO) 0 to 3: the highest level that is compiled in
ALEX2ESP_MAX_DIRECTIVE 2047
ALEX2ESP_MAX_MESSAGE ALEX2ESP_MAX_DIRECTIVE + 1024 under ALEX2ESP_MAX_DIRECTIVE + 300 the answer of a lamp to the largest directive is refused
ALEX2ESP_MAX_QUEUED_DIRECTIVES 8 1 to 64
ALEX2ESP_MAX_QUEUED_BYTES 4 x ALEX2ESP_MAX_DIRECTIVE ALEX2ESP_MAX_DIRECTIVE or more
ALEX2ESP_MAX_CAPABILITIES 8 1 to 100
ALEX2ESP_MAX_FRIENDLY_NAMES 3 1 to 255
ALEX2ESP_MAX_ACTION_MAPPINGS 4 1 to 255
ALEX2ESP_MAX_STATE_MAPPINGS 2 1 to 255

The Arduino IDE has no flags per sketch and builds with the defaults. setLogLevel() with a level that is not compiled in prints an error that names the flag.

A sketch that defines a macro named DEBUG, ERROR or INFO cannot write the level of that name; it passes the number instead, alexa.setLogLevel(static_cast<AlexaLogLevel>(3)) for DEBUG.


Migrating from 1.x

The five examples of 1.x compile against 2.0 as they are, without a warning; they are in examples/legacy/. registerEvent(), buildStatusMessage(), the helpers with a capital letter (AddHealthProp, AddPowerControllerProp, ...), addCapability(AlexaInterfaceType::...), AlexaInterface and AlexaActions stay:

  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();
  });

examples/legacy/basicLight/basicLight.ino

A device with a handler of onDirective() does not call the handlers of registerEvent().

What changes for a 1.x sketch that is compiled against 2.0:

1.x 2.0 What to do
directives fetched and reports posted over HTTP both over MQTT; the HTTP fallback is removed nothing. The library no longer includes ESP8266HTTPClient: a sketch that uses it includes it itself
timeOfSample filled in by the backend the board's own time the board has to reach a time server (DNS, UDP port 123), or the sketch sets the clock and calls setTimeSource(false)
the session opens in begin() loop() opens it when the clock is set, at most 5 s later nothing
reconnect every 5 s after a lost TCP connection only, without a line after every end of a session, with the reason and a growing wait nothing
EndpointHealth announced as 3.3, ThermostatController as 3 without properties, Speaker, StepSpeaker, PlaybackStateReporter, InventoryLevelSensor and WakeOnLANController as 1 3.1, 3.2 with four properties, and 3, as the interface pages of Alexa have them; the interfaces that were announced with an empty list of properties have their properties let Alexa discover the devices again
AlexaInterfaceType::AUTHORIZATION_CONTROLLER, AUTOMOTIVE_VEHICLE_DATA removed; Alexa has withdrawn both remove the capability
addCapability() with a type the library has no description of announced it as version 1 without properties adds nothing, prints an error and returns nullptr check the pointer; announce the interface when the library has a row for it
AddTemperatureSensorProp(TemperatureSensorScale::FAHRENHEIT, 69) reported 20.56 CELSIUS reports 69 FAHRENHEIT nothing
a value outside of its range was sent as it was the nearest value of the range, with an error nothing
the handler of Event got the type of the namespace the type of the capability of its device, UNKNOWN when the device has none add the capability
a second registerEvent() for a name was ignored replaces the handler
the locale and the directive name and payload of an ActionMapping were copied kept as pointers pass literals
any number of capabilities, names and mappings 8 capabilities per device, 3 names, 4 action and 2 state mappings per capability build flags, see Configuration
a directive nobody answered ran into the timeout the library answers INVALID_DIRECTIVE when the device has no handler for it
an AlexaDevice or AlexaStatusMessage the sketch constructed itself could send send() returns false: it has no client use getDevice() and buildStatusMessage()
#define Alex2ESP_DEBUG in AlexaUtils.cpp setLogLevel() and -DALEX2ESP_LOG_MAX
AlexaInterfaceUtils, the queue functions of AlexaUtils, AlexaInterface::getJSON() and getProps(), ActionMapping::getJSON(), FriendlyName, MAX_STATUS_REPORT_SIZE removed alexaInterfaceRow(type) gives the row of a type, with its namespace, version and properties; the limit is ALEX2ESP_MAX_MESSAGE

CHANGELOG.md has every change. A sketch written for 1.0.0 also meets the change of 1.1.0: begin() takes the user name, the password and the root topic, in that order.


Limits

  • ESP8266 only. The library has never been compiled for an ESP32.
  • The session is MQTT over TCP on port 1883, without TLS.
  • Reports are published with QoS 0 and sent once. An event or a change while there is no session with the broker is not sent later; send() returns false and the sketch decides.
  • Tried with Alexa from this library: the six interfaces marked in Interface Types. Not tried from this library: ColorController, ThermostatController, LockController and the deferred answer, SceneController, the ErrorResponse, the ChangeReports, DoorbellPress and every interface without an example. Voice commands have not been tried.
  • The examples are compiled, with 0 warnings, and what each announces in discovery is checked by the host tests. They have not run on a board and have not been built with the Arduino IDE.
  • The library keeps no state of the devices. What a device is set to is a variable of the sketch and starts from its initial value after a reset.
  • 28 of the interfaces of Alexa have a row. The others cannot be announced.
  • The Arduino IDE builds with the default limits.

Tests

pio test -e native in the repository runs 148 host tests: 52 of the receive and publish logic, 36 of discovery, 16 of dispatch, 32 of the messages and 12 of the examples (what each announces in discovery). No board and no broker is needed. The platformio.ini of the repository is for these tests; a sketch does not need it.

Contributing

Feel free to submit pull requests or issues for feature requests and bug fixes. The host tests have to pass, and the examples have to build without a warning.

License

This project is licensed under the MIT License. See the LICENSE file for details.