Using the
Alex2ESP
library, version 2.0, for the ESP8266 (Arduino / PlatformIO; it
has not been tried on an ESP32). Add the library and its two
dependencies to platformio.ini (the repo is not in the
PlatformIO registry yet):
[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
The library's examples/Light/Light.ino: one lamp
that answers TurnOn, TurnOff and ReportState. Fill in your Wi-Fi
SSID/password and the three values from
/MQTT_Credentials; the serial
monitor runs at 74880 baud. Directives and answers travel over
MQTT only.
// 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();
}
All twelve example sketches on Forgejo →
Using the
Alex2Node
library, version 2.0 (Node.js 18 or later):
npm install git+https://git.stormysdream.club/apps/Alex2Node.git#v2.0.0.
Give the script the three values from
/MQTT_Credentials in its
environment:
export ALEX2MQTT_USERNAME=amzn1.account.… # MQTT Username
export ALEX2MQTT_PASSWORD=… # MQTT Password (64 hex characters)
export ALEX2MQTT_ROOT_TOPIC=… # MQTT Root Topic
Then run the library's examples/lamp.js: a lamp that
switches and dims. The library answers discovery for you, checks
what Alexa sends, and reports the lamp's state with every answer
and on ReportState. Programs written for 1.x keep running on 2.0.
// A lamp that switches and dims: Alexa.PowerController and Alexa.BrightnessController.
//
// ALEX2MQTT_USERNAME=... ALEX2MQTT_PASSWORD=... ALEX2MQTT_ROOT_TOPIC=... node examples/lamp.js
//
// "alex2node" is the installed package; inside this checkout it resolves to the built dist/.
const { Alex2MQTT, PowerController, BrightnessController } = require("alex2node");
const missing = ["ALEX2MQTT_USERNAME", "ALEX2MQTT_PASSWORD", "ALEX2MQTT_ROOT_TOPIC"].filter((name) => !process.env[name]);
if (missing.length > 0) {
console.error(`${missing.join(", ")} not set. Set the MQTT user name, the password and the root topic of your Alex2MQTT account.`);
process.exit(1);
}
const { ALEX2MQTT_USERNAME, ALEX2MQTT_PASSWORD, ALEX2MQTT_ROOT_TOPIC } = process.env;
const bridge = new Alex2MQTT(ALEX2MQTT_USERNAME, ALEX2MQTT_PASSWORD, ALEX2MQTT_ROOT_TOPIC);
bridge.on("connect", () => console.log("connected, discover the devices in the Alexa app"));
bridge.on("error", (err) => console.error("bridge:", err.message));
// The lamp itself. Replace it with what drives yours.
const lamp = { on: false, brightness: 100 };
const device = bridge.addDevice({ endpointId: "desk-lamp", name: "Desk Lamp", categories: ["LIGHT"] });
const power = device.add(PowerController);
const brightness = device.add(BrightnessController);
// The whole state: the answer to ReportState, and the context of every ctx.respond()
device.state((s) => s
.set(power, "powerState", lamp.on ? "ON" : "OFF")
.set(brightness, "brightness", lamp.brightness)
.health("OK"));
power.on("TurnOn", (ctx) => {
lamp.on = true;
return ctx.respond();
});
power.on("TurnOff", (ctx) => {
lamp.on = false;
return ctx.respond();
});
// Both brightness directives turn a lamp on that is off
brightness.on("SetBrightness", (ctx) => {
lamp.brightness = ctx.payload.brightness;
lamp.on = true;
return ctx.respond();
});
brightness.on("AdjustBrightness", (ctx) => {
lamp.brightness = Math.min(100, Math.max(0, lamp.brightness + ctx.payload.brightnessDelta));
lamp.on = true;
return ctx.respond();
});
bridge.connect();
All ten examples on Forgejo →
No SDK required — anything that speaks MQTT can be a device. The
topic contract, relative to your root topic:
-
<root>/discover — Alexa asks every device to
report itself; publish your complete device list on
<root>/discover_r.
-
<root>/<endpointId>/alexaDirective — a
directive for one device (TurnOn, TurnOff, ReportState, …).
-
<root>/<endpointId>/alexaDirective_e — a
short token published with every directive, for Alex2ESP 1.x,
which exchanges it for the directive over HTTP:
GET /Alex2ESP/<token> with Basic auth = MQTT
username:password. Alex2ESP 2.0 and plain MQTT clients ignore it.
-
<root>/<endpointId>/alexaResponce —
publish your reply here (spelling kept for compatibility). Reply
within 7 s or Alexa reports the device as not responding.
-
<root>/<endpointId>/deferredResponse —
for a slow device: reply on alexaResponce within 7 s with
"name": "DeferredResponse" (optional
payload.estimatedDeferralInSeconds), then publish the
real Response/StateReport (with context) here within
10 s; the bridge forwards it to Alexa with your account token.
-
<root>/changeReport — push a state change
without being asked (a wall switch was flipped). The bridge adds
your account token and forwards it to Alexa; the capability must
have been discovered with "proactivelyReported": true.
-
<root>/event — an event that is not a state: a
doorbell was pressed (DoorbellPress), a button was
pushed. Publish the event with its endpoint; the
bridge adds your account token and forwards it. At most 30 a
minute and 16 kB each.
Connect and subscribe with the credentials from
/MQTT_Credentials:
# Connect with the username / password / root topic from /MQTT_Credentials
# and watch every topic under your root:
mosquitto_sub -h alexa2mqtt.stormysdream.club -p 1883 \
-u "$MQTT_USER" -P "$MQTT_PASS" \
-t "$ROOT_TOPIC/#" -v
Discovery
When Alexa starts discovery, this payload arrives on
<root>/discover:
{
"namespace": "Alexa.Discovery",
"name": "Discover",
"payloadVersion": "3"
}
Answer on <root>/discover_r with your devices —
one object or an array; each entry needs endpointId
and friendlyName, otherwise the bridge publishes
{"status":"Error",...} back on
discover_r:
# Answer within 1 s on <root>/discover_r with your COMPLETE current device list
# (that is what an Alexa-initiated discovery returns); answers up to 5 s later are still recorded
# and reach Alexa on the next Devices-page discovery or periodic sweep (AddOrUpdateReport).
# (A discovery is a full sync. On a discovery started from the Devices page or the
# periodic sweep (every 15 min), endpoints you no longer list are removed from Alexa
# - never on an empty answer, and (with four or more devices) never more than half of them at once.)
mosquitto_pub -h alexa2mqtt.stormysdream.club -p 1883 \
-u "$MQTT_USER" -P "$MQTT_PASS" \
-t "$ROOT_TOPIC/discover_r" -m '[
{
"endpointId": "sample-bulb-01",
"friendlyName": "Livingroom lamp",
"description": "Virtual smart light bulb",
"manufacturerName": "Smart Device Company",
"displayCategories": ["LIGHT"],
"capabilities": [
{
"interface": "Alexa.PowerController",
"version": "3",
"type": "AlexaInterface",
"properties": {
"supported": [ { "name": "powerState" } ],
"retrievable": true,
"proactivelyReported": true
}
}
]
}
]'
Anything supported by the v3 Alexa interface should work through this
skill. For more details, see the
Alexa Discovery documentation.
Directives & responses
A directive such as ReportState (or TurnOn / TurnOff)
arrives on
<root>/<endpointId>/alexaDirective:
{
"header": {
"namespace": "Alexa",
"name": "ReportState",
"messageId": "unique-identifier",
"correlationToken": "correlation-token",
"payloadVersion": "3"
},
"endpoint": {
"endpointId": "sample-bulb-01",
"cookie": {}
},
"payload": {}
}
A TurnOn arrives the same way with
"namespace": "Alexa.PowerController", "name": "TurnOn";
copy correlationToken into your reply. (The
endpoint.scope bearer token is stripped before
publishing.)
Reply on <root>/<endpointId>/alexaResponce
with the device's state. Use a real ISO-8601
timeOfSample; the bridge does not fill it in for MQTT
clients:
# Reply on <root>/<endpointId>/alexaResponce (spelling kept for compatibility):
mosquitto_pub -h alexa2mqtt.stormysdream.club -p 1883 \
-u "$MQTT_USER" -P "$MQTT_PASS" \
-t "$ROOT_TOPIC/sample-bulb-01/alexaResponce" -m '{
"event": {
"header": {
"namespace": "Alexa",
"name": "StateReport",
"messageId": "unique-identifier",
"correlationToken": "correlation-token",
"payloadVersion": "3"
},
"endpoint": { "endpointId": "sample-bulb-01" },
"payload": {}
},
"context": {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": "OFF",
"timeOfSample": "2022-02-03T16:20:50.52Z",
"uncertaintyInMilliseconds": 0
},
{
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": { "value": "OK" },
"timeOfSample": "2022-02-03T16:20:00.00Z",
"uncertaintyInMilliseconds": 0
}
]
}
}'
For more information on what to include in the response, see the
Alexa State Report documentation.
Proactive state
When the device changes on its own, publish an
Alexa.ChangeReport on <root>/changeReport
and the bridge forwards it to Alexa:
mosquitto_pub -h alexa2mqtt.stormysdream.club -p 1883 \
-u "$MQTT_USER" -P "$MQTT_PASS" \
-t "$ROOT_TOPIC/changeReport" -m '{
"event": {
"header": { "namespace": "Alexa", "name": "ChangeReport", "messageId": "unique-identifier", "payloadVersion": "3" },
"endpoint": { "endpointId": "sample-bulb-01" },
"payload": {
"change": {
"cause": { "type": "PHYSICAL_INTERACTION" },
"properties": [
{ "namespace": "Alexa.PowerController", "name": "powerState", "value": "OFF",
"timeOfSample": "2026-09-28T00:51:04.033Z", "uncertaintyInMilliseconds": 0 }
]
}
}
},
"context": {
"properties": [
{ "namespace": "Alexa.EndpointHealth", "name": "connectivity", "value": { "value": "OK" },
"timeOfSample": "2026-09-28T00:51:04.033Z", "uncertaintyInMilliseconds": 0 }
]
}
}'