Examples as Arduino sketches: one folder per device type, each its own endpointId, Wi-Fi with a timeout
Light, DimmableLight, ColorTemperatureLight, ColorLight, TemperatureSensor, ContactSensor, Blind, Thermostat, Lock, Scene, Doorbell and MultiDevice on the 2.0 API: addCapability(row, instance), one onDirective() handler, the report helpers, ChangeReports from loop(), a deferred answer for the lock. Wi-Fi is waited for 30 s, then the sketch says what to check and carries on. examples/README.md has what each sketch does, its limits and the measured sizes for d1_mini: Light takes 30,616 bytes of static RAM and 332,565 of flash; all 17 sketches build with 0 warnings. test/test_examples compares the discovery object of every new sketch: 147 host tests (were 135). The sketches were compiled, not run on a board. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
7369883479
commit
69b4d59896
15 changed files with 2061 additions and 1 deletions
138
examples/Blind/Blind.ino
Normal file
138
examples/Blind/Blind.ino
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
// Blind: a roller blind that Alexa opens, closes and sets to a position (Alexa.RangeController with semantics).
|
||||||
|
//
|
||||||
|
// The capability is one instance of the RangeController, "Blind.Lift": 0 is closed, 100 is open. Its semantics
|
||||||
|
// tell Alexa what "open", "close", "raise" and "lower" mean for it, and which values count as open and closed.
|
||||||
|
// SetRangeValue outside of 0 to 100 is answered with VALUE_OUT_OF_RANGE; AdjustRangeValue stops at the ends.
|
||||||
|
//
|
||||||
|
// The blind of this sketch is a number. moveTo() is the place for the motor.
|
||||||
|
#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 = "";
|
||||||
|
|
||||||
|
const char LIFT[] = "Blind.Lift";
|
||||||
|
|
||||||
|
Alex2ESP alexa;
|
||||||
|
int lift = 0; // percent open
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveTo(int position)
|
||||||
|
{
|
||||||
|
lift = position;
|
||||||
|
Serial.printf("[BLIND] %d percent open\n", lift);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The state of the blind: what the answer to a directive and the answer to ReportState carry
|
||||||
|
void sendState(AlexaStatusMessage message)
|
||||||
|
{
|
||||||
|
message.addHealthProp(EndpointHealth::OK).addRangeControllerProp(LIFT, lift).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirective(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
if (directive.isReportState())
|
||||||
|
{
|
||||||
|
sendState(directive.stateReport());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directive.is("SetRangeValue"))
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
moveTo(position);
|
||||||
|
}
|
||||||
|
else if (directive.is("AdjustRangeValue"))
|
||||||
|
{
|
||||||
|
moveTo(constrain(lift + (directive.payload["rangeValueDelta"] | 0), 0, 100));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "This blind takes a position").send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendState(directive.response());
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(74880);
|
||||||
|
|
||||||
|
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 *blind = alexa.getDevice("Bedroom Blind", "esp-blind");
|
||||||
|
blind->setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
blind->addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
blind->onDirective(onDirective);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
alexa.loop();
|
||||||
|
}
|
||||||
154
examples/ColorLight/ColorLight.ino
Normal file
154
examples/ColorLight/ColorLight.ino
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
// ColorLight: an RGB lamp (Alexa.PowerController, Alexa.BrightnessController, Alexa.ColorController).
|
||||||
|
//
|
||||||
|
// Alexa names a color by hue (0 to 360 degrees), saturation and brightness (0 to 1). The sketch keeps the three,
|
||||||
|
// turns them into red, green and blue and drives three PWM outputs. SetColor sets the brightness too; SetBrightness
|
||||||
|
// changes the brightness and leaves hue and saturation alone.
|
||||||
|
#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 = "";
|
||||||
|
|
||||||
|
const uint8_t RED_PIN = 14; // D5 on a Wemos D1 mini, high = lit
|
||||||
|
const uint8_t GREEN_PIN = 12; // D6
|
||||||
|
const uint8_t BLUE_PIN = 13; // D7
|
||||||
|
|
||||||
|
Alex2ESP alexa;
|
||||||
|
bool lampOn = false;
|
||||||
|
float hue = 0; // degrees
|
||||||
|
float saturation = 0; // 0 to 1: 0 is white
|
||||||
|
int brightness = 100; // percent
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hue, saturation and brightness as the share of red, green and blue, 0 to 100 each
|
||||||
|
void writeLamp()
|
||||||
|
{
|
||||||
|
float value = lampOn ? brightness : 0;
|
||||||
|
float sector = hue / 60;
|
||||||
|
int whole = static_cast<int>(sector) % 6;
|
||||||
|
float part = sector - static_cast<int>(sector);
|
||||||
|
float low = value * (1 - saturation);
|
||||||
|
float falling = value * (1 - saturation * part);
|
||||||
|
float rising = value * (1 - saturation * (1 - part));
|
||||||
|
|
||||||
|
float red = value, green = value, blue = value;
|
||||||
|
switch (whole)
|
||||||
|
{
|
||||||
|
case 0: green = rising; blue = low; break;
|
||||||
|
case 1: red = falling; blue = low; break;
|
||||||
|
case 2: red = low; blue = rising; break;
|
||||||
|
case 3: red = low; green = falling; break;
|
||||||
|
case 4: red = rising; green = low; break;
|
||||||
|
default: green = low; blue = falling; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
analogWrite(RED_PIN, static_cast<int>(red + 0.5f));
|
||||||
|
analogWrite(GREEN_PIN, static_cast<int>(green + 0.5f));
|
||||||
|
analogWrite(BLUE_PIN, static_cast<int>(blue + 0.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
.addBrightnessControllerProp(brightness)
|
||||||
|
.addColorControllerProp(hue, saturation, brightness / 100.0f)
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirective(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
if (directive.isReportState())
|
||||||
|
{
|
||||||
|
sendState(directive.stateReport());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directive.is("TurnOn") || directive.is("TurnOff"))
|
||||||
|
{
|
||||||
|
lampOn = directive.is("TurnOn");
|
||||||
|
}
|
||||||
|
else if (directive.is("SetBrightness"))
|
||||||
|
{
|
||||||
|
brightness = constrain(directive.payload["brightness"] | brightness, 0, 100);
|
||||||
|
}
|
||||||
|
else if (directive.is("AdjustBrightness"))
|
||||||
|
{
|
||||||
|
brightness = constrain(brightness + (directive.payload["brightnessDelta"] | 0), 0, 100);
|
||||||
|
}
|
||||||
|
else if (directive.is("SetColor"))
|
||||||
|
{
|
||||||
|
JsonVariantConst color = directive.payload["color"];
|
||||||
|
hue = constrain(color["hue"] | hue, 0.0f, 360.0f);
|
||||||
|
saturation = constrain(color["saturation"] | saturation, 0.0f, 1.0f);
|
||||||
|
brightness = constrain(static_cast<int>((color["brightness"] | brightness / 100.0f) * 100 + 0.5f), 0, 100);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "This lamp switches, dims and changes its color").send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLamp();
|
||||||
|
sendState(directive.response());
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(74880);
|
||||||
|
pinMode(RED_PIN, OUTPUT);
|
||||||
|
pinMode(GREEN_PIN, OUTPUT);
|
||||||
|
pinMode(BLUE_PIN, OUTPUT);
|
||||||
|
analogWriteRange(100);
|
||||||
|
writeLamp();
|
||||||
|
|
||||||
|
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("Color Lamp", "esp-color-light");
|
||||||
|
lamp->setDisplayCategory(DisplayCategory::LIGHT);
|
||||||
|
lamp->addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp->addCapability(AlexaInterfaces::BrightnessController);
|
||||||
|
lamp->addCapability(AlexaInterfaces::ColorController);
|
||||||
|
lamp->addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
lamp->onDirective(onDirective);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
alexa.loop();
|
||||||
|
}
|
||||||
156
examples/ColorTemperatureLight/ColorTemperatureLight.ino
Normal file
156
examples/ColorTemperatureLight/ColorTemperatureLight.ino
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
// ColorTemperatureLight: a white lamp with a warm and a cold channel (Alexa.PowerController,
|
||||||
|
// Alexa.BrightnessController, Alexa.ColorTemperatureController).
|
||||||
|
//
|
||||||
|
// Two PWM outputs drive the two LED channels; the color temperature decides how the brightness is shared between
|
||||||
|
// them. The lamp covers 2200 K to 7000 K: a value outside of it is set to the nearest the lamp has, and the answer
|
||||||
|
// reports what was set. "Alexa, make the lamp warmer" and "cooler" move to the next of the five steps below.
|
||||||
|
#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 = "";
|
||||||
|
|
||||||
|
const uint8_t WARM_PIN = 5; // D1 on a Wemos D1 mini, high = lit
|
||||||
|
const uint8_t COLD_PIN = 4; // D2
|
||||||
|
|
||||||
|
// Warm white, soft white, white, daylight white, cool white: the values Alexa sends for these names
|
||||||
|
const int STEPS[] = {2200, 2700, 4000, 5500, 7000};
|
||||||
|
const int STEP_COUNT = sizeof(STEPS) / sizeof(STEPS[0]);
|
||||||
|
|
||||||
|
Alex2ESP alexa;
|
||||||
|
bool lampOn = false;
|
||||||
|
int brightness = 100; // percent
|
||||||
|
int kelvin = 2700;
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeLamp()
|
||||||
|
{
|
||||||
|
int level = lampOn ? brightness : 0;
|
||||||
|
int cold = map(kelvin, STEPS[0], STEPS[STEP_COUNT - 1], 0, level);
|
||||||
|
analogWrite(COLD_PIN, cold);
|
||||||
|
analogWrite(WARM_PIN, level - cold);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
.addBrightnessControllerProp(brightness)
|
||||||
|
.addColorTemperatureControllerProp(kelvin)
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirective(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
if (directive.isReportState())
|
||||||
|
{
|
||||||
|
sendState(directive.stateReport());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directive.is("TurnOn") || directive.is("TurnOff"))
|
||||||
|
{
|
||||||
|
lampOn = directive.is("TurnOn");
|
||||||
|
}
|
||||||
|
else if (directive.is("SetBrightness"))
|
||||||
|
{
|
||||||
|
brightness = constrain(directive.payload["brightness"] | brightness, 0, 100);
|
||||||
|
}
|
||||||
|
else if (directive.is("AdjustBrightness"))
|
||||||
|
{
|
||||||
|
brightness = constrain(brightness + (directive.payload["brightnessDelta"] | 0), 0, 100);
|
||||||
|
}
|
||||||
|
else if (directive.is("SetColorTemperature"))
|
||||||
|
{
|
||||||
|
kelvin = constrain(directive.payload["colorTemperatureInKelvin"] | kelvin, STEPS[0], STEPS[STEP_COUNT - 1]);
|
||||||
|
}
|
||||||
|
else if (directive.is("IncreaseColorTemperature"))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < STEP_COUNT; i++)
|
||||||
|
{
|
||||||
|
if (STEPS[i] > kelvin)
|
||||||
|
{
|
||||||
|
kelvin = STEPS[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (directive.is("DecreaseColorTemperature"))
|
||||||
|
{
|
||||||
|
for (int i = STEP_COUNT - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (STEPS[i] < kelvin)
|
||||||
|
{
|
||||||
|
kelvin = STEPS[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "This lamp switches, dims and changes its white").send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLamp();
|
||||||
|
sendState(directive.response());
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(74880);
|
||||||
|
pinMode(WARM_PIN, OUTPUT);
|
||||||
|
pinMode(COLD_PIN, OUTPUT);
|
||||||
|
analogWriteRange(100);
|
||||||
|
writeLamp();
|
||||||
|
|
||||||
|
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("White Lamp", "esp-white-light");
|
||||||
|
lamp->setDisplayCategory(DisplayCategory::LIGHT);
|
||||||
|
lamp->addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp->addCapability(AlexaInterfaces::BrightnessController);
|
||||||
|
lamp->addCapability(AlexaInterfaces::ColorTemperatureController);
|
||||||
|
lamp->addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
lamp->onDirective(onDirective);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
alexa.loop();
|
||||||
|
}
|
||||||
105
examples/ContactSensor/ContactSensor.ino
Normal file
105
examples/ContactSensor/ContactSensor.ino
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
// ContactSensor: a door or window contact that tells Alexa when it opens and closes (Alexa.ContactSensor).
|
||||||
|
//
|
||||||
|
// A reed switch connects the pin to ground while the door is closed; the internal pull-up makes the pin high when
|
||||||
|
// it opens. A level has to hold for 50 ms before it counts. Every change is sent as a ChangeReport; ReportState
|
||||||
|
// is answered with the state of the contact. A change while there is no session with the broker is not reported
|
||||||
|
// later: Alexa gets the state with its next ReportState.
|
||||||
|
#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 = "";
|
||||||
|
|
||||||
|
const uint8_t CONTACT_PIN = 14; // D5 on a Wemos D1 mini
|
||||||
|
const unsigned long DEBOUNCE_MS = 50;
|
||||||
|
|
||||||
|
Alex2ESP alexa;
|
||||||
|
AlexaDevice *contact;
|
||||||
|
|
||||||
|
bool isOpen = false;
|
||||||
|
bool lastLevel = false;
|
||||||
|
unsigned long lastEdge = 0;
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirective(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
if (directive.isReportState())
|
||||||
|
{
|
||||||
|
directive.stateReport().addHealthProp(EndpointHealth::OK).addContactSensorProp(isOpen).send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "A contact takes no directives").send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(74880);
|
||||||
|
pinMode(CONTACT_PIN, INPUT_PULLUP);
|
||||||
|
isOpen = lastLevel = digitalRead(CONTACT_PIN) == 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
|
||||||
|
contact = alexa.getDevice("Back Door", "esp-contact");
|
||||||
|
contact->setDisplayCategory(DisplayCategory::CONTACT_SENSOR);
|
||||||
|
contact->addCapability(AlexaInterfaces::ContactSensor)->setProactivelyReported(true);
|
||||||
|
contact->addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
contact->onDirective(onDirective);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
alexa.loop();
|
||||||
|
|
||||||
|
bool level = digitalRead(CONTACT_PIN) == HIGH;
|
||||||
|
if (level != lastLevel)
|
||||||
|
{
|
||||||
|
lastLevel = level;
|
||||||
|
lastEdge = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level != isOpen && millis() - lastEdge >= DEBOUNCE_MS)
|
||||||
|
{
|
||||||
|
isOpen = level;
|
||||||
|
Serial.println(isOpen ? "[CONTACT] open" : "[CONTACT] closed");
|
||||||
|
contact->changeReport(AlexaCause::PHYSICAL_INTERACTION)
|
||||||
|
.addContactSensorProp(isOpen)
|
||||||
|
.context()
|
||||||
|
.addHealthProp(EndpointHealth::OK)
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
}
|
||||||
120
examples/DimmableLight/DimmableLight.ino
Normal file
120
examples/DimmableLight/DimmableLight.ino
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
// DimmableLight: a lamp that Alexa switches and dims (Alexa.PowerController, Alexa.BrightnessController).
|
||||||
|
//
|
||||||
|
// The lamp is the on-board LED, dimmed with PWM. SetBrightness sets a value of 0 to 100, AdjustBrightness changes
|
||||||
|
// it by a difference; the result stays within 0 to 100.
|
||||||
|
#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;
|
||||||
|
int brightness = 100; // percent
|
||||||
|
|
||||||
|
// 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 pin is low for the part of the time the LED is lit
|
||||||
|
void writeLamp()
|
||||||
|
{
|
||||||
|
analogWrite(LED_BUILTIN, lampOn ? 100 - brightness : 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
.addBrightnessControllerProp(brightness)
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirective(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
if (directive.isReportState())
|
||||||
|
{
|
||||||
|
sendState(directive.stateReport());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directive.is("TurnOn") || directive.is("TurnOff"))
|
||||||
|
{
|
||||||
|
lampOn = directive.is("TurnOn");
|
||||||
|
}
|
||||||
|
else if (directive.is("SetBrightness"))
|
||||||
|
{
|
||||||
|
brightness = constrain(directive.payload["brightness"] | brightness, 0, 100);
|
||||||
|
}
|
||||||
|
else if (directive.is("AdjustBrightness"))
|
||||||
|
{
|
||||||
|
brightness = constrain(brightness + (directive.payload["brightnessDelta"] | 0), 0, 100);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "This lamp switches and dims").send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLamp();
|
||||||
|
sendState(directive.response());
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(74880);
|
||||||
|
pinMode(LED_BUILTIN, OUTPUT);
|
||||||
|
analogWriteRange(100);
|
||||||
|
writeLamp();
|
||||||
|
|
||||||
|
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("Dimmable Lamp", "esp-dimmable-light");
|
||||||
|
lamp->setDisplayCategory(DisplayCategory::LIGHT);
|
||||||
|
lamp->addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp->addCapability(AlexaInterfaces::BrightnessController);
|
||||||
|
lamp->addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
lamp->onDirective(onDirective);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
alexa.loop();
|
||||||
|
}
|
||||||
104
examples/Doorbell/Doorbell.ino
Normal file
104
examples/Doorbell/Doorbell.ino
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
// Doorbell: a button that tells Alexa when it is pressed (Alexa.DoorbellEventSource).
|
||||||
|
//
|
||||||
|
// The button connects the pin to ground; the internal pull-up makes the pin high while it is released. A level has
|
||||||
|
// to hold for 50 ms before it counts. Every press publishes a DoorbellPress event on <root>/event. A press while
|
||||||
|
// there is no session with the broker is lost: the library prints why it was not sent.
|
||||||
|
//
|
||||||
|
// This event has not been tried with Alexa yet.
|
||||||
|
#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 = "";
|
||||||
|
|
||||||
|
const uint8_t BUTTON_PIN = 14; // D5 on a Wemos D1 mini
|
||||||
|
const unsigned long DEBOUNCE_MS = 50;
|
||||||
|
|
||||||
|
Alex2ESP alexa;
|
||||||
|
AlexaDevice *doorbell;
|
||||||
|
|
||||||
|
bool pressed = false;
|
||||||
|
bool lastLevel = false;
|
||||||
|
unsigned long lastEdge = 0;
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirective(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
if (directive.isReportState())
|
||||||
|
{
|
||||||
|
directive.stateReport().addHealthProp(EndpointHealth::OK).send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "A doorbell takes no directives").send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(74880);
|
||||||
|
pinMode(BUTTON_PIN, INPUT_PULLUP);
|
||||||
|
|
||||||
|
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
|
||||||
|
doorbell = alexa.getDevice("Front Door Bell", "esp-doorbell");
|
||||||
|
doorbell->setDisplayCategory(DisplayCategory::DOORBELL);
|
||||||
|
doorbell->addCapability(AlexaInterfaces::DoorbellEventSource)->setProactivelyReported(true);
|
||||||
|
doorbell->addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
doorbell->onDirective(onDirective);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
alexa.loop();
|
||||||
|
|
||||||
|
bool level = digitalRead(BUTTON_PIN) == LOW;
|
||||||
|
if (level != lastLevel)
|
||||||
|
{
|
||||||
|
lastLevel = level;
|
||||||
|
lastEdge = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level != pressed && millis() - lastEdge >= DEBOUNCE_MS)
|
||||||
|
{
|
||||||
|
pressed = level;
|
||||||
|
if (pressed)
|
||||||
|
{
|
||||||
|
Serial.println("[DOORBELL] pressed");
|
||||||
|
doorbell->doorbellPress().send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
examples/Light/Light.ino
Normal file
99
examples/Light/Light.ino
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
117
examples/Lock/Lock.ino
Normal file
117
examples/Lock/Lock.ino
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
// Lock: a door lock whose bolt takes 3 s to move (Alexa.LockController), answered with a deferred response.
|
||||||
|
//
|
||||||
|
// Lock and Unlock start the bolt and are answered at once with a DeferredResponse that names 5 s. When the bolt
|
||||||
|
// has arrived, loop() sends the answer itself, with the correlationToken of the directive, which the sketch has
|
||||||
|
// kept. While the bolt moves, another Lock or Unlock is refused with ENDPOINT_BUSY.
|
||||||
|
//
|
||||||
|
// A switch from BLOCKED_PIN to ground stands for a bolt that cannot move: the answer then reports JAMMED.
|
||||||
|
#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 = "";
|
||||||
|
|
||||||
|
const uint8_t BOLT_PIN = 5; // D1 on a Wemos D1 mini: high = locked
|
||||||
|
const uint8_t BLOCKED_PIN = 14; // D5: low = the bolt is blocked
|
||||||
|
const unsigned long BOLT_MS = 3000;
|
||||||
|
|
||||||
|
Alex2ESP alexa;
|
||||||
|
AlexaDevice *doorLock;
|
||||||
|
|
||||||
|
AlexaLockState state = AlexaLockState::LOCKED;
|
||||||
|
AlexaLockState goal = AlexaLockState::LOCKED;
|
||||||
|
bool moving = false;
|
||||||
|
unsigned long movingSince = 0;
|
||||||
|
String token; // the correlationToken of the directive that is being carried out
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirective(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
if (directive.isReportState())
|
||||||
|
{
|
||||||
|
// While the bolt moves this is the state it left
|
||||||
|
directive.stateReport().addHealthProp(EndpointHealth::OK).addLockControllerProp(state).send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!directive.is("Lock") && !directive.is("Unlock"))
|
||||||
|
{
|
||||||
|
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "This lock locks and unlocks").send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (moving)
|
||||||
|
{
|
||||||
|
directive.error(AlexaErrorType::ENDPOINT_BUSY, "The bolt is moving").send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(74880);
|
||||||
|
pinMode(BOLT_PIN, OUTPUT);
|
||||||
|
pinMode(BLOCKED_PIN, INPUT_PULLUP);
|
||||||
|
digitalWrite(BOLT_PIN, 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
|
||||||
|
doorLock = alexa.getDevice("Front Door Lock", "esp-lock");
|
||||||
|
doorLock->setDisplayCategory(DisplayCategory::SMARTLOCK);
|
||||||
|
doorLock->addCapability(AlexaInterfaces::LockController);
|
||||||
|
doorLock->addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
doorLock->onDirective(onDirective);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
alexa.loop();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
106
examples/MultiDevice/MultiDevice.ino
Normal file
106
examples/MultiDevice/MultiDevice.ino
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
// MultiDevice: two lamps on one board, two Alexa endpoints, one handler (Alexa.PowerController).
|
||||||
|
//
|
||||||
|
// Every device has its own endpoint id and name. Both get the same handler; the directive says which device it
|
||||||
|
// is for.
|
||||||
|
#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 = "";
|
||||||
|
|
||||||
|
const int LAMP_COUNT = 2;
|
||||||
|
const uint8_t LAMP_PINS[LAMP_COUNT] = {5, 4}; // D1 and D2 on a Wemos D1 mini, high = lit
|
||||||
|
const char *const LAMP_NAMES[LAMP_COUNT] = {"Left Lamp", "Right Lamp"};
|
||||||
|
const char *const LAMP_IDS[LAMP_COUNT] = {"esp-multi-left", "esp-multi-right"};
|
||||||
|
|
||||||
|
Alex2ESP alexa;
|
||||||
|
AlexaDevice *lamps[LAMP_COUNT];
|
||||||
|
bool lampOn[LAMP_COUNT] = {false, 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 one lamp: what the answer to a directive and the answer to ReportState carry
|
||||||
|
void sendState(AlexaStatusMessage message, int lamp)
|
||||||
|
{
|
||||||
|
message.addHealthProp(EndpointHealth::OK)
|
||||||
|
.addPowerControllerProp(lampOn[lamp] ? PowerController::ON : PowerController::OFF)
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirective(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
int lamp = directive.device == lamps[1] ? 1 : 0;
|
||||||
|
|
||||||
|
if (directive.isReportState())
|
||||||
|
{
|
||||||
|
sendState(directive.stateReport(), lamp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directive.type == AlexaInterfaceType::POWER_CONTROLLER && (directive.is("TurnOn") || directive.is("TurnOff")))
|
||||||
|
{
|
||||||
|
lampOn[lamp] = directive.is("TurnOn");
|
||||||
|
digitalWrite(LAMP_PINS[lamp], lampOn[lamp] ? HIGH : LOW);
|
||||||
|
sendState(directive.response(), lamp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "This lamp switches on and off").send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(74880);
|
||||||
|
|
||||||
|
connectWiFi();
|
||||||
|
|
||||||
|
// MQTT user name, MQTT password, root topic
|
||||||
|
alexa.begin(ALEXA_USERNAME, ALEXA_PASSWORD, ALEXA_ROOT_TOPIC);
|
||||||
|
|
||||||
|
for (int i = 0; i < LAMP_COUNT; i++)
|
||||||
|
{
|
||||||
|
pinMode(LAMP_PINS[i], OUTPUT);
|
||||||
|
digitalWrite(LAMP_PINS[i], LOW);
|
||||||
|
|
||||||
|
// The name Alexa shows, and the id of the endpoint: every device of an account has its own
|
||||||
|
lamps[i] = alexa.getDevice(LAMP_NAMES[i], LAMP_IDS[i]);
|
||||||
|
lamps[i]->setDisplayCategory(DisplayCategory::LIGHT);
|
||||||
|
lamps[i]->addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamps[i]->addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
lamps[i]->onDirective(onDirective);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
alexa.loop();
|
||||||
|
}
|
||||||
114
examples/README.md
Normal file
114
examples/README.md
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
One folder per kind of device. Every sketch is complete: Wi-Fi, the MQTT session, one device (MultiDevice: two) with
|
||||||
|
its own endpoint id and name, and one handler for its directives.
|
||||||
|
|
||||||
|
| Sketch | Device | Interfaces | Endpoint id |
|
||||||
|
|---|---|---|---|
|
||||||
|
| [Light](Light/Light.ino) | a lamp, on and off | PowerController | `esp-light` |
|
||||||
|
| [DimmableLight](DimmableLight/DimmableLight.ino) | a lamp with PWM brightness | PowerController, BrightnessController | `esp-dimmable-light` |
|
||||||
|
| [ColorTemperatureLight](ColorTemperatureLight/ColorTemperatureLight.ino) | a lamp with a warm and a cold channel, 2200 K to 7000 K | PowerController, BrightnessController, ColorTemperatureController | `esp-white-light` |
|
||||||
|
| [ColorLight](ColorLight/ColorLight.ino) | an RGB lamp | PowerController, BrightnessController, ColorController | `esp-color-light` |
|
||||||
|
| [TemperatureSensor](TemperatureSensor/TemperatureSensor.ino) | a thermometer; a ChangeReport after a change of more than 0.5 degrees, at most one a minute | TemperatureSensor | `esp-temperature` |
|
||||||
|
| [ContactSensor](ContactSensor/ContactSensor.ino) | a door contact on a GPIO; a ChangeReport on every change | ContactSensor | `esp-contact` |
|
||||||
|
| [Blind](Blind/Blind.ino) | a blind with a position of 0 to 100 and the words open, close, raise, lower | RangeController `Blind.Lift` with semantics | `esp-blind` |
|
||||||
|
| [Thermostat](Thermostat/Thermostat.ino) | HEAT, COOL, AUTO and OFF, one setpoint or two, the errors of the thermostat | ThermostatController, TemperatureSensor | `esp-thermostat` |
|
||||||
|
| [Lock](Lock/Lock.ino) | a bolt that takes 3 s: DeferredResponse, then the answer from `loop()` | LockController | `esp-lock` |
|
||||||
|
| [Scene](Scene/Scene.ino) | a scene that switches two outputs | SceneController | `esp-scene` |
|
||||||
|
| [Doorbell](Doorbell/Doorbell.ino) | a button on a GPIO; DoorbellPress on every press | DoorbellEventSource | `esp-doorbell` |
|
||||||
|
| [MultiDevice](MultiDevice/MultiDevice.ino) | two lamps, two endpoints, one handler | PowerController | `esp-multi-left`, `esp-multi-right` |
|
||||||
|
|
||||||
|
Every device announces EndpointHealth as well and reports `connectivity` with its state.
|
||||||
|
|
||||||
|
The sketches in [`legacy/`](legacy/) are the examples of 1.x, unchanged. They use `registerEvent()` and the
|
||||||
|
`Add...Prop()` names and are kept to show that a 1.x sketch compiles against 2.0. New sketches start from the ones
|
||||||
|
above.
|
||||||
|
|
||||||
|
## Before flashing
|
||||||
|
|
||||||
|
Fill in the five constants at the top of the sketch: the Wi-Fi network and the MQTT user name, password and root
|
||||||
|
topic of your Alex2MQTT account.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
const char *WIFI_SSID = "";
|
||||||
|
const char *WIFI_PASSWORD = "";
|
||||||
|
|
||||||
|
const char *ALEXA_USERNAME = "";
|
||||||
|
const char *ALEXA_PASSWORD = "";
|
||||||
|
const char *ALEXA_ROOT_TOPIC = "";
|
||||||
|
```
|
||||||
|
|
||||||
|
Two boards with the same endpoint id on one account are one device to Alexa. Change the id and the name in
|
||||||
|
`getDevice()` when you flash a sketch a second time.
|
||||||
|
|
||||||
|
Each sketch names the pins it uses by GPIO number, with the label of the Wemos D1 mini in a comment. The serial
|
||||||
|
output is at 74880 baud.
|
||||||
|
|
||||||
|
## Wi-Fi
|
||||||
|
|
||||||
|
`connectWiFi()` waits for the network for 30 s at the most. Without a connection it prints the status and the two
|
||||||
|
constants to check, and the sketch carries on: the ESP8266 keeps trying to join, and `alexa.loop()` opens the MQTT
|
||||||
|
session once Wi-Fi is up.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Arduino IDE: a sketch is a folder with an `.ino` of the same name, which is how the IDE opens it.
|
||||||
|
|
||||||
|
PlatformIO: copy the `.ino` into `src/` of a project that has the library in `lib_deps`, or make the folder of the
|
||||||
|
sketch the source directory of the project (`src_dir`, or the environment variable of the same meaning). In a
|
||||||
|
project with a `d1_mini` environment and the library:
|
||||||
|
|
||||||
|
```
|
||||||
|
PLATFORMIO_SRC_DIR=/path/to/Alex2ESP/examples/Light pio run -e d1_mini
|
||||||
|
```
|
||||||
|
|
||||||
|
This is how the sizes below were built. The sketches define every function before it is used and include `Arduino.h`, so they are also valid as a `.cpp`
|
||||||
|
file.
|
||||||
|
|
||||||
|
## Size
|
||||||
|
|
||||||
|
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 this folder 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,565 |
|
||||||
|
| DimmableLight | 30,692 | 336,765 |
|
||||||
|
| ColorTemperatureLight | 30,808 | 337,489 |
|
||||||
|
| ColorLight | 30,748 | 338,657 |
|
||||||
|
| TemperatureSensor | 30,572 | 333,213 |
|
||||||
|
| ContactSensor | 30,584 | 332,861 |
|
||||||
|
| Blind | 30,860 | 336,525 |
|
||||||
|
| Thermostat | 31,044 | 337,517 |
|
||||||
|
| Lock | 30,648 | 333,441 |
|
||||||
|
| Scene | 30,640 | 333,277 |
|
||||||
|
| Doorbell | 30,584 | 333,013 |
|
||||||
|
| MultiDevice | 30,672 | 332,725 |
|
||||||
|
| legacy/basicLight | 30,520 | 333,989 |
|
||||||
|
| legacy/lightWithBrightness | 30,648 | 337,929 |
|
||||||
|
| legacy/lightWithColorTemp | 30,828 | 338,653 |
|
||||||
|
| legacy/tempSensor | 30,412 | 332,781 |
|
||||||
|
| legacy/blindControl | 30,568 | 335,261 |
|
||||||
|
|
||||||
|
## What has been tried
|
||||||
|
|
||||||
|
The sketches of this folder were compiled with PlatformIO, with 0 warnings. They have not been built with the
|
||||||
|
Arduino IDE and have not run on a board yet. What each announces
|
||||||
|
in discovery is checked by the host tests in `test/test_examples`.
|
||||||
|
|
||||||
|
With this library on a Wemos D1 mini, a real Alexa account and other sketches (2026-09-28), Alexa discovered and
|
||||||
|
drove PowerController, BrightnessController, ColorTemperatureController and one instance each of ToggleController,
|
||||||
|
RangeController and ModeController. Not tried with Alexa from this library: ColorController, ThermostatController,
|
||||||
|
LockController and the deferred answer, SceneController, the ChangeReports and DoorbellPress. Voice commands have
|
||||||
|
not been tried.
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
- Scene: the discovery object does not carry `supportsDeactivation`. The sketch answers `Deactivate`, but Alexa
|
||||||
|
may never send it.
|
||||||
|
- Doorbell, ContactSensor: an event or a change while there is no session with the broker is not sent later.
|
||||||
|
- Lock: the answer after the DeferredResponse is sent once. If the session is lost while the bolt moves, Alexa gets
|
||||||
|
the state with its next ReportState.
|
||||||
|
- Blind, Scene, Thermostat, the lamps: the state is kept in RAM and starts from the values in the sketch after a
|
||||||
|
reset.
|
||||||
104
examples/Scene/Scene.ino
Normal file
104
examples/Scene/Scene.ino
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
// Scene: something Alexa starts by name, "Alexa, turn on Movie Night" (Alexa.SceneController).
|
||||||
|
//
|
||||||
|
// A scene has no state to report. Activate is answered with ActivationStarted in place of a Response, Deactivate
|
||||||
|
// with DeactivationStarted. The scene of this sketch switches two outputs: the ceiling lamp off and the lamp
|
||||||
|
// behind the screen on.
|
||||||
|
//
|
||||||
|
// The discovery object of the library does not say that the scene can be deactivated (supportsDeactivation), so
|
||||||
|
// Alexa may never send Deactivate. The handler answers it all the same.
|
||||||
|
#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 = "";
|
||||||
|
|
||||||
|
const uint8_t CEILING_PIN = 5; // D1 on a Wemos D1 mini, high = on
|
||||||
|
const uint8_t SCREEN_PIN = 4; // D2
|
||||||
|
|
||||||
|
Alex2ESP alexa;
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setScene(bool active)
|
||||||
|
{
|
||||||
|
digitalWrite(CEILING_PIN, active ? LOW : HIGH);
|
||||||
|
digitalWrite(SCREEN_PIN, active ? HIGH : LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirective(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
if (directive.isReportState())
|
||||||
|
{
|
||||||
|
directive.stateReport().addHealthProp(EndpointHealth::OK).send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directive.is("Activate"))
|
||||||
|
{
|
||||||
|
setScene(true);
|
||||||
|
directive.sceneStarted().send();
|
||||||
|
}
|
||||||
|
else if (directive.is("Deactivate"))
|
||||||
|
{
|
||||||
|
setScene(false);
|
||||||
|
directive.sceneStopped().send();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "A scene is activated and deactivated").send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(74880);
|
||||||
|
pinMode(CEILING_PIN, OUTPUT);
|
||||||
|
pinMode(SCREEN_PIN, OUTPUT);
|
||||||
|
setScene(false);
|
||||||
|
|
||||||
|
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 *scene = alexa.getDevice("Movie Night", "esp-scene");
|
||||||
|
scene->setDisplayCategory(DisplayCategory::SCENE_TRIGGER);
|
||||||
|
scene->addCapability(AlexaInterfaces::SceneController);
|
||||||
|
scene->addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
scene->onDirective(onDirective);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
alexa.loop();
|
||||||
|
}
|
||||||
127
examples/TemperatureSensor/TemperatureSensor.ino
Normal file
127
examples/TemperatureSensor/TemperatureSensor.ino
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
// TemperatureSensor: a thermometer that Alexa reads, and that tells Alexa when the temperature changed
|
||||||
|
// (Alexa.TemperatureSensor).
|
||||||
|
//
|
||||||
|
// The sensor is a TMP36 on A0 of a Wemos D1 mini, whose A0 takes 0 to 3.2 V; for another sensor replace
|
||||||
|
// readTemperature(). The temperature is read every 2 s. A ChangeReport is sent when it differs by more than
|
||||||
|
// 0.5 degrees from what Alexa was told last, and at most once a minute.
|
||||||
|
#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 = "";
|
||||||
|
|
||||||
|
const unsigned long READ_EVERY_MS = 2000;
|
||||||
|
const unsigned long REPORT_EVERY_MS = 60000;
|
||||||
|
const float REPORT_DIFFERENCE = 0.5f; // degrees Celsius
|
||||||
|
|
||||||
|
Alex2ESP alexa;
|
||||||
|
AlexaDevice *sensor;
|
||||||
|
|
||||||
|
float temperature = 0; // degrees Celsius, as read last
|
||||||
|
float reported = 0; // what the last ChangeReport said
|
||||||
|
bool everReported = false;
|
||||||
|
unsigned long lastRead = 0;
|
||||||
|
unsigned long lastReport = 0;
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TMP36: 0.5 V at 0 degrees Celsius, 10 mV per degree
|
||||||
|
float readTemperature()
|
||||||
|
{
|
||||||
|
float volts = analogRead(A0) * 3.2f / 1023;
|
||||||
|
return (volts - 0.5f) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirective(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
if (directive.isReportState())
|
||||||
|
{
|
||||||
|
directive.stateReport()
|
||||||
|
.addHealthProp(EndpointHealth::OK)
|
||||||
|
.addTemperatureSensorProp(temperature, TemperatureSensorScale::CELSIUS, millis() - lastRead)
|
||||||
|
.send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "A thermometer takes no directives").send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reportChange()
|
||||||
|
{
|
||||||
|
bool due = !everReported || millis() - lastReport >= REPORT_EVERY_MS;
|
||||||
|
if (!due || fabs(temperature - reported) <= REPORT_DIFFERENCE || alexa.getState() != Alex2ESPState::CONNECTED)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sent = sensor->changeReport(AlexaCause::PERIODIC_POLL)
|
||||||
|
.addTemperatureSensorProp(temperature)
|
||||||
|
.context()
|
||||||
|
.addHealthProp(EndpointHealth::OK)
|
||||||
|
.send();
|
||||||
|
if (sent)
|
||||||
|
{
|
||||||
|
reported = temperature;
|
||||||
|
everReported = true;
|
||||||
|
lastReport = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(74880);
|
||||||
|
temperature = readTemperature();
|
||||||
|
|
||||||
|
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
|
||||||
|
sensor = alexa.getDevice("Room Temperature", "esp-temperature");
|
||||||
|
sensor->setDisplayCategory(DisplayCategory::TEMPERATURE_SENSOR);
|
||||||
|
sensor->addCapability(AlexaInterfaces::TemperatureSensor)->setProactivelyReported(true);
|
||||||
|
sensor->addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
sensor->onDirective(onDirective);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
alexa.loop();
|
||||||
|
|
||||||
|
if (millis() - lastRead >= READ_EVERY_MS)
|
||||||
|
{
|
||||||
|
lastRead = millis();
|
||||||
|
temperature = readTemperature();
|
||||||
|
reportChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
319
examples/Thermostat/Thermostat.ino
Normal file
319
examples/Thermostat/Thermostat.ino
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
// Thermostat: heating and cooling with a setpoint (Alexa.ThermostatController, Alexa.TemperatureSensor).
|
||||||
|
//
|
||||||
|
// Modes HEAT, COOL, AUTO and OFF. HEAT and COOL have one setpoint; AUTO has two, it heats below the lower and
|
||||||
|
// cools above the upper one. Setpoints are 10 to 30 degrees Celsius and, in AUTO, at least 2 degrees apart.
|
||||||
|
// A directive in Fahrenheit or Kelvin is converted; the thermostat reports in Celsius.
|
||||||
|
//
|
||||||
|
// What is refused, and with which error:
|
||||||
|
// a setpoint outside of 10 to 30 degrees TEMPERATURE_VALUE_OUT_OF_RANGE, with the range
|
||||||
|
// two setpoints closer than 2 degrees REQUESTED_SETPOINTS_TOO_CLOSE, with the distance
|
||||||
|
// two setpoints in HEAT or COOL DUAL_SETPOINTS_UNSUPPORTED
|
||||||
|
// a setpoint while the thermostat is OFF THERMOSTAT_IS_OFF
|
||||||
|
// a mode other than the four UNSUPPORTED_THERMOSTAT_MODE
|
||||||
|
//
|
||||||
|
// The sensor is a TMP36 on A0 of a Wemos D1 mini (A0 takes 0 to 3.2 V); two outputs switch the heating and the
|
||||||
|
// cooling.
|
||||||
|
#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 = "";
|
||||||
|
|
||||||
|
const uint8_t HEAT_PIN = 5; // D1 on a Wemos D1 mini, high = on
|
||||||
|
const uint8_t COOL_PIN = 4; // D2
|
||||||
|
|
||||||
|
const float LOWEST = 10; // degrees Celsius
|
||||||
|
const float HIGHEST = 30;
|
||||||
|
const float DISTANCE = 2; // between the two setpoints of AUTO
|
||||||
|
const float HYSTERESIS = 0.5f;
|
||||||
|
|
||||||
|
Alex2ESP alexa;
|
||||||
|
AlexaThermostatMode mode = AlexaThermostatMode::HEAT;
|
||||||
|
float target = 21; // HEAT and COOL
|
||||||
|
float lower = 19; // AUTO
|
||||||
|
float upper = 24;
|
||||||
|
float temperature = 0;
|
||||||
|
unsigned long lastRead = 0;
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TMP36: 0.5 V at 0 degrees Celsius, 10 mV per degree
|
||||||
|
float readTemperature()
|
||||||
|
{
|
||||||
|
float volts = analogRead(A0) * 3.2f / 1023;
|
||||||
|
return (volts - 0.5f) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A temperature of a directive, {"value": 70, "scale": "FAHRENHEIT"}, in degrees Celsius
|
||||||
|
float toCelsius(JsonVariantConst temperature)
|
||||||
|
{
|
||||||
|
float value = temperature["value"] | 0.0f;
|
||||||
|
const char *scale = temperature["scale"] | "CELSIUS";
|
||||||
|
if (strcmp(scale, "FAHRENHEIT") == 0)
|
||||||
|
{
|
||||||
|
return (value - 32) * 5 / 9;
|
||||||
|
}
|
||||||
|
return strcmp(scale, "KELVIN") == 0 ? value - 273.15f : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A difference between two temperatures: one degree Fahrenheit is 5/9 of a degree Celsius
|
||||||
|
float differenceInCelsius(JsonVariantConst difference)
|
||||||
|
{
|
||||||
|
float value = difference["value"] | 0.0f;
|
||||||
|
return strcmp(difference["scale"] | "CELSIUS", "FAHRENHEIT") == 0 ? value * 5 / 9 : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCelsius(JsonObject temperature, float value)
|
||||||
|
{
|
||||||
|
temperature["value"] = value;
|
||||||
|
temperature["scale"] = "CELSIUS";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool withinRange(float setpoint)
|
||||||
|
{
|
||||||
|
return setpoint >= LOWEST && setpoint <= HIGHEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendOutOfRange(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
AlexaStatusMessage error = directive.error(AlexaErrorType::TEMPERATURE_VALUE_OUT_OF_RANGE, "10 to 30 degrees Celsius");
|
||||||
|
JsonObject range = error.payload()["validRange"].to<JsonObject>();
|
||||||
|
setCelsius(range["minimumValue"].to<JsonObject>(), LOWEST);
|
||||||
|
setCelsius(range["maximumValue"].to<JsonObject>(), HIGHEST);
|
||||||
|
error.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The state of the thermostat: what the answer to a directive and the answer to ReportState carry
|
||||||
|
void sendState(AlexaStatusMessage message)
|
||||||
|
{
|
||||||
|
message.addHealthProp(EndpointHealth::OK)
|
||||||
|
.addTemperatureSensorProp(temperature, TemperatureSensorScale::CELSIUS, millis() - lastRead)
|
||||||
|
.addThermostatModeProp(mode);
|
||||||
|
if (mode == AlexaThermostatMode::AUTO)
|
||||||
|
{
|
||||||
|
message.addThermostatDualSetpointProp(lower, upper);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
message.addThermostatSetpointProp(target);
|
||||||
|
}
|
||||||
|
message.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes the setpoints when the thermostat can use them and returns true; sends the error and returns false when not.
|
||||||
|
// One setpoint in AUTO moves both and keeps their distance.
|
||||||
|
bool setSetpoints(AlexaDirective &directive, bool two, float first, float second)
|
||||||
|
{
|
||||||
|
if (mode == AlexaThermostatMode::OFF)
|
||||||
|
{
|
||||||
|
directive.error(AlexaErrorType::THERMOSTAT_IS_OFF, "Set a mode first").send();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (two && mode != AlexaThermostatMode::AUTO)
|
||||||
|
{
|
||||||
|
directive.error(AlexaErrorType::DUAL_SETPOINTS_UNSUPPORTED, "Two setpoints in AUTO only").send();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!two && mode == AlexaThermostatMode::AUTO)
|
||||||
|
{
|
||||||
|
float half = (upper - lower) / 2;
|
||||||
|
second = first + half;
|
||||||
|
first = first - half;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode != AlexaThermostatMode::AUTO)
|
||||||
|
{
|
||||||
|
if (!withinRange(first))
|
||||||
|
{
|
||||||
|
sendOutOfRange(directive);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
target = first;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!withinRange(first) || !withinRange(second))
|
||||||
|
{
|
||||||
|
sendOutOfRange(directive);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (second - first < DISTANCE)
|
||||||
|
{
|
||||||
|
AlexaStatusMessage error = directive.error(AlexaErrorType::REQUESTED_SETPOINTS_TOO_CLOSE, "At least 2 degrees apart");
|
||||||
|
setCelsius(error.payload()["minimumTemperatureDelta"].to<JsonObject>(), DISTANCE);
|
||||||
|
error.send();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lower = first;
|
||||||
|
upper = second;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool setMode(const char *name)
|
||||||
|
{
|
||||||
|
if (strcmp(name, "HEAT") == 0)
|
||||||
|
{
|
||||||
|
mode = AlexaThermostatMode::HEAT;
|
||||||
|
}
|
||||||
|
else if (strcmp(name, "COOL") == 0)
|
||||||
|
{
|
||||||
|
mode = AlexaThermostatMode::COOL;
|
||||||
|
}
|
||||||
|
else if (strcmp(name, "AUTO") == 0)
|
||||||
|
{
|
||||||
|
mode = AlexaThermostatMode::AUTO;
|
||||||
|
}
|
||||||
|
else if (strcmp(name, "OFF") == 0)
|
||||||
|
{
|
||||||
|
mode = AlexaThermostatMode::OFF;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDirective(AlexaDirective &directive)
|
||||||
|
{
|
||||||
|
if (directive.isReportState())
|
||||||
|
{
|
||||||
|
sendState(directive.stateReport());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObjectConst payload = directive.payload;
|
||||||
|
if (directive.is("SetTargetTemperature"))
|
||||||
|
{
|
||||||
|
bool two = !payload["lowerSetpoint"].isNull() && !payload["upperSetpoint"].isNull();
|
||||||
|
bool taken = two ? setSetpoints(directive, true, toCelsius(payload["lowerSetpoint"]), toCelsius(payload["upperSetpoint"]))
|
||||||
|
: setSetpoints(directive, false, toCelsius(payload["targetSetpoint"]), 0);
|
||||||
|
if (!taken)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (directive.is("AdjustTargetTemperature"))
|
||||||
|
{
|
||||||
|
float centre = mode == AlexaThermostatMode::AUTO ? (lower + upper) / 2 : target;
|
||||||
|
if (!setSetpoints(directive, false, centre + differenceInCelsius(payload["targetSetpointDelta"]), 0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (directive.is("SetThermostatMode"))
|
||||||
|
{
|
||||||
|
if (!setMode(payload["thermostatMode"]["value"] | ""))
|
||||||
|
{
|
||||||
|
directive.error(AlexaErrorType::UNSUPPORTED_THERMOSTAT_MODE, "HEAT, COOL, AUTO or OFF").send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
directive.error(AlexaErrorType::INVALID_DIRECTIVE, "This thermostat takes setpoints and modes").send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendState(directive.response());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "configuration" of the capability in discovery
|
||||||
|
void thermostatConfiguration(JsonObject configuration, void *)
|
||||||
|
{
|
||||||
|
JsonArray modes = configuration["supportedModes"].to<JsonArray>();
|
||||||
|
modes.add("HEAT");
|
||||||
|
modes.add("COOL");
|
||||||
|
modes.add("AUTO");
|
||||||
|
modes.add("OFF");
|
||||||
|
configuration["supportsScheduling"] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switches on below the setpoint and off above it, with half a degree between the two
|
||||||
|
void control()
|
||||||
|
{
|
||||||
|
bool heats = mode == AlexaThermostatMode::HEAT || mode == AlexaThermostatMode::AUTO;
|
||||||
|
bool cools = mode == AlexaThermostatMode::COOL || mode == AlexaThermostatMode::AUTO;
|
||||||
|
float heatBelow = mode == AlexaThermostatMode::AUTO ? lower : target;
|
||||||
|
float coolAbove = mode == AlexaThermostatMode::AUTO ? upper : target;
|
||||||
|
|
||||||
|
if (!heats || temperature >= heatBelow + HYSTERESIS)
|
||||||
|
{
|
||||||
|
digitalWrite(HEAT_PIN, LOW);
|
||||||
|
}
|
||||||
|
else if (temperature <= heatBelow - HYSTERESIS)
|
||||||
|
{
|
||||||
|
digitalWrite(HEAT_PIN, HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cools || temperature <= coolAbove - HYSTERESIS)
|
||||||
|
{
|
||||||
|
digitalWrite(COOL_PIN, LOW);
|
||||||
|
}
|
||||||
|
else if (temperature >= coolAbove + HYSTERESIS)
|
||||||
|
{
|
||||||
|
digitalWrite(COOL_PIN, HIGH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(74880);
|
||||||
|
pinMode(HEAT_PIN, OUTPUT);
|
||||||
|
pinMode(COOL_PIN, OUTPUT);
|
||||||
|
temperature = readTemperature();
|
||||||
|
|
||||||
|
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 *thermostat = alexa.getDevice("Hallway Thermostat", "esp-thermostat");
|
||||||
|
thermostat->setDisplayCategory(DisplayCategory::THERMOSTAT);
|
||||||
|
thermostat->addCapability(AlexaInterfaces::ThermostatController)->setConfiguration(thermostatConfiguration);
|
||||||
|
thermostat->addCapability(AlexaInterfaces::TemperatureSensor);
|
||||||
|
thermostat->addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
thermostat->onDirective(onDirective);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop()
|
||||||
|
{
|
||||||
|
alexa.loop();
|
||||||
|
|
||||||
|
if (millis() - lastRead >= 2000)
|
||||||
|
{
|
||||||
|
lastRead = millis();
|
||||||
|
temperature = readTemperature();
|
||||||
|
control();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -54,7 +54,7 @@ Then include the library in your project:
|
||||||
#include <Alex2ESP.h>
|
#include <Alex2ESP.h>
|
||||||
```
|
```
|
||||||
|
|
||||||
Complete sketches are in [`examples/legacy/`](examples/legacy/), one folder each: `basicLight`, `lightWithBrightness`, `lightWithColorTemp`, `tempSensor` and `blindControl`. Every example joins Wi-Fi with `WiFi.begin(WIFI_SSID, WIFI_PASSWORD)`; fill in the SSID, the password and your Alex2MQTT credentials before flashing.
|
Complete sketches are in [`examples/`](examples/), one folder per kind of device: `Light`, `DimmableLight`, `ColorTemperatureLight`, `ColorLight`, `TemperatureSensor`, `ContactSensor`, `Blind`, `Thermostat`, `Lock`, `Scene`, `Doorbell` and `MultiDevice`; [`examples/README.md`](examples/README.md) says what each does and what it takes of RAM and flash. The examples of 1.x are in [`examples/legacy/`](examples/legacy/). Every example joins Wi-Fi with `WiFi.begin(WIFI_SSID, WIFI_PASSWORD)`; fill in the SSID, the password and your Alex2MQTT credentials before flashing.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
297
test/test_examples/test_main.cpp
Normal file
297
test/test_examples/test_main.cpp
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
// Host tests of the example sketches of 2.0 (examples/<Name>/<Name>.ino): the object each of them announces on
|
||||||
|
// <root>/discover_r, compared byte by byte with the text below.
|
||||||
|
// The devices are built with the calls of the example's setup(). A change to the capabilities of an example is a
|
||||||
|
// change here. The examples of 1.x (examples/legacy) are in test/test_discovery.
|
||||||
|
// pio test -e native
|
||||||
|
#include <unity.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <string>
|
||||||
|
#include "AlexaDevice.h"
|
||||||
|
#include "AlexaLog.h"
|
||||||
|
#include "AlexaResources.h"
|
||||||
|
#include "AlexaVersion.h"
|
||||||
|
|
||||||
|
static std::string endpoint(const char *endpointId, const char *friendlyName, const char *displayCategory,
|
||||||
|
const std::string &capabilities)
|
||||||
|
{
|
||||||
|
return std::string("{\"endpointId\":\"") + endpointId + "\",\"friendlyName\":\"" + friendlyName +
|
||||||
|
"\",\"description\":\"Alexa2MQTT Default Device\",\"manufacturerName\":\"Alexa2MQTT\","
|
||||||
|
"\"displayCategories\":[\"" + displayCategory + "\"],"
|
||||||
|
"\"additionalAttributes\":{\"manufacturer\":\"Alexa2MQTT\",\"model\":\"Alexa2MQTT\","
|
||||||
|
"\"serialNumber\":\"ESP2Alex\",\"firmwareVersion\":\"" ALEX2ESP_VERSION "\","
|
||||||
|
"\"softwareVersion\":\"" ALEX2ESP_VERSION "\",\"customIdentifier\":\"ESP2Alex\"},"
|
||||||
|
"\"capabilities\":[" + capabilities + "]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// A capability with one property that is not reported unasked
|
||||||
|
#define CAPABILITY(interface, version, property) \
|
||||||
|
"{\"interface\":\"Alexa." interface "\",\"version\":\"" version "\",\"type\":\"AlexaInterface\"," \
|
||||||
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":[{\"name\":\"" property "\"}]}}"
|
||||||
|
|
||||||
|
// The same, reported unasked with a ChangeReport
|
||||||
|
#define REPORTING_CAPABILITY(interface, version, property) \
|
||||||
|
"{\"interface\":\"Alexa." interface "\",\"version\":\"" version "\",\"type\":\"AlexaInterface\"," \
|
||||||
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":true,\"supported\":[{\"name\":\"" property "\"}]}}"
|
||||||
|
|
||||||
|
static const char POWER[] = CAPABILITY("PowerController", "3", "powerState");
|
||||||
|
static const char BRIGHTNESS[] = CAPABILITY("BrightnessController", "3", "brightness");
|
||||||
|
static const char COLOR_TEMPERATURE[] = CAPABILITY("ColorTemperatureController", "3", "colorTemperatureInKelvin");
|
||||||
|
static const char COLOR[] = CAPABILITY("ColorController", "3", "color");
|
||||||
|
static const char TEMPERATURE[] = CAPABILITY("TemperatureSensor", "3", "temperature");
|
||||||
|
static const char LOCK[] = CAPABILITY("LockController", "3", "lockState");
|
||||||
|
static const char HEALTH[] = CAPABILITY("EndpointHealth", "3.1", "connectivity");
|
||||||
|
|
||||||
|
static std::string list(std::initializer_list<const char *> capabilities)
|
||||||
|
{
|
||||||
|
std::string text;
|
||||||
|
for (const char *capability : capabilities)
|
||||||
|
{
|
||||||
|
text += text.empty() ? "" : ",";
|
||||||
|
text += capability;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void assertDiscovery(const std::string &expected, const AlexaDevice &device)
|
||||||
|
{
|
||||||
|
std::string announced;
|
||||||
|
serializeJson(device.getDeviceJSON(), announced);
|
||||||
|
TEST_ASSERT_EQUAL_STRING(expected.c_str(), announced.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUp(void)
|
||||||
|
{
|
||||||
|
AlexaLog::setOutput(nullptr); // the lines about added capabilities are not what is tested
|
||||||
|
}
|
||||||
|
|
||||||
|
void tearDown(void) {}
|
||||||
|
|
||||||
|
void test_light_discovery(void)
|
||||||
|
{
|
||||||
|
AlexaDevice lamp("Desk Lamp", "root", "esp-light");
|
||||||
|
lamp.setDisplayCategory(DisplayCategory::LIGHT);
|
||||||
|
lamp.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp.addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
|
||||||
|
assertDiscovery(endpoint("esp-light", "Desk Lamp", "LIGHT", list({POWER, HEALTH})), lamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_dimmable_light_discovery(void)
|
||||||
|
{
|
||||||
|
AlexaDevice lamp("Dimmable Lamp", "root", "esp-dimmable-light");
|
||||||
|
lamp.setDisplayCategory(DisplayCategory::LIGHT);
|
||||||
|
lamp.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp.addCapability(AlexaInterfaces::BrightnessController);
|
||||||
|
lamp.addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
|
||||||
|
assertDiscovery(endpoint("esp-dimmable-light", "Dimmable Lamp", "LIGHT", list({POWER, BRIGHTNESS, HEALTH})), lamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_color_temperature_light_discovery(void)
|
||||||
|
{
|
||||||
|
AlexaDevice lamp("White Lamp", "root", "esp-white-light");
|
||||||
|
lamp.setDisplayCategory(DisplayCategory::LIGHT);
|
||||||
|
lamp.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp.addCapability(AlexaInterfaces::BrightnessController);
|
||||||
|
lamp.addCapability(AlexaInterfaces::ColorTemperatureController);
|
||||||
|
lamp.addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
|
||||||
|
assertDiscovery(endpoint("esp-white-light", "White Lamp", "LIGHT",
|
||||||
|
list({POWER, BRIGHTNESS, COLOR_TEMPERATURE, HEALTH})),
|
||||||
|
lamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_color_light_discovery(void)
|
||||||
|
{
|
||||||
|
AlexaDevice lamp("Color Lamp", "root", "esp-color-light");
|
||||||
|
lamp.setDisplayCategory(DisplayCategory::LIGHT);
|
||||||
|
lamp.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp.addCapability(AlexaInterfaces::BrightnessController);
|
||||||
|
lamp.addCapability(AlexaInterfaces::ColorController);
|
||||||
|
lamp.addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
|
||||||
|
assertDiscovery(endpoint("esp-color-light", "Color Lamp", "LIGHT", list({POWER, BRIGHTNESS, COLOR, HEALTH})), lamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_temperature_sensor_discovery(void)
|
||||||
|
{
|
||||||
|
AlexaDevice sensor("Room Temperature", "root", "esp-temperature");
|
||||||
|
sensor.setDisplayCategory(DisplayCategory::TEMPERATURE_SENSOR);
|
||||||
|
sensor.addCapability(AlexaInterfaces::TemperatureSensor)->setProactivelyReported(true);
|
||||||
|
sensor.addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
|
||||||
|
assertDiscovery(endpoint("esp-temperature", "Room Temperature", "TEMPERATURE_SENSOR",
|
||||||
|
list({REPORTING_CAPABILITY("TemperatureSensor", "3", "temperature"), HEALTH})),
|
||||||
|
sensor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_contact_sensor_discovery(void)
|
||||||
|
{
|
||||||
|
AlexaDevice contact("Back Door", "root", "esp-contact");
|
||||||
|
contact.setDisplayCategory(DisplayCategory::CONTACT_SENSOR);
|
||||||
|
contact.addCapability(AlexaInterfaces::ContactSensor)->setProactivelyReported(true);
|
||||||
|
contact.addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
|
||||||
|
assertDiscovery(endpoint("esp-contact", "Back Door", "CONTACT_SENSOR",
|
||||||
|
list({REPORTING_CAPABILITY("ContactSensor", "3", "detectionState"), HEALTH})),
|
||||||
|
contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_blind_discovery(void)
|
||||||
|
{
|
||||||
|
static const char LIFT[] = "Blind.Lift";
|
||||||
|
AlexaDevice blind("Bedroom Blind", "root", "esp-blind");
|
||||||
|
blind.setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
blind.addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
|
||||||
|
assertDiscovery(
|
||||||
|
endpoint("esp-blind", "Bedroom Blind", "INTERIOR_BLIND",
|
||||||
|
list({"{\"interface\":\"Alexa.RangeController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
||||||
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,"
|
||||||
|
"\"supported\":[{\"name\":\"rangeValue\"}]},"
|
||||||
|
"\"semantics\":{\"actionMappings\":["
|
||||||
|
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Close\"],"
|
||||||
|
"\"directive\":{\"name\":\"SetRangeValue\",\"payload\":{\"rangeValue\":0}}},"
|
||||||
|
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Open\"],"
|
||||||
|
"\"directive\":{\"name\":\"SetRangeValue\",\"payload\":{\"rangeValue\":100}}},"
|
||||||
|
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Lower\"],"
|
||||||
|
"\"directive\":{\"name\":\"AdjustRangeValue\","
|
||||||
|
"\"payload\":{\"rangeValueDelta\":-10,\"rangeValueDeltaDefault\":false}}},"
|
||||||
|
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Raise\"],"
|
||||||
|
"\"directive\":{\"name\":\"AdjustRangeValue\","
|
||||||
|
"\"payload\":{\"rangeValueDelta\":10,\"rangeValueDeltaDefault\":false}}}],"
|
||||||
|
"\"stateMappings\":["
|
||||||
|
"{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.Closed\"],\"value\":0},"
|
||||||
|
"{\"@type\":\"StatesToRange\",\"states\":[\"Alexa.States.Open\"],"
|
||||||
|
"\"range\":{\"minimumValue\":1,\"maximumValue\":100}}]},"
|
||||||
|
"\"instance\":\"Blind.Lift\","
|
||||||
|
"\"capabilityResources\":{\"friendlyNames\":["
|
||||||
|
"{\"@type\":\"asset\",\"value\":{\"assetId\":\"Alexa.Setting.Opening\"}}]},"
|
||||||
|
"\"configuration\":{\"supportedRange\":{\"minimumValue\":0,\"maximumValue\":100,\"precision\":1},"
|
||||||
|
"\"unitOfMeasure\":\"Alexa.Unit.Percent\"}}",
|
||||||
|
HEALTH})),
|
||||||
|
blind);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void thermostatConfiguration(JsonObject configuration, void *)
|
||||||
|
{
|
||||||
|
JsonArray modes = configuration["supportedModes"].to<JsonArray>();
|
||||||
|
modes.add("HEAT");
|
||||||
|
modes.add("COOL");
|
||||||
|
modes.add("AUTO");
|
||||||
|
modes.add("OFF");
|
||||||
|
configuration["supportsScheduling"] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_thermostat_discovery(void)
|
||||||
|
{
|
||||||
|
AlexaDevice thermostat("Hallway Thermostat", "root", "esp-thermostat");
|
||||||
|
thermostat.setDisplayCategory(DisplayCategory::THERMOSTAT);
|
||||||
|
thermostat.addCapability(AlexaInterfaces::ThermostatController)->setConfiguration(thermostatConfiguration);
|
||||||
|
thermostat.addCapability(AlexaInterfaces::TemperatureSensor);
|
||||||
|
thermostat.addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
|
||||||
|
assertDiscovery(
|
||||||
|
endpoint("esp-thermostat", "Hallway Thermostat", "THERMOSTAT",
|
||||||
|
list({"{\"interface\":\"Alexa.ThermostatController\",\"version\":\"3.2\",\"type\":\"AlexaInterface\","
|
||||||
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":["
|
||||||
|
"{\"name\":\"targetSetpoint\"},{\"name\":\"lowerSetpoint\"},{\"name\":\"upperSetpoint\"},"
|
||||||
|
"{\"name\":\"thermostatMode\"}]},"
|
||||||
|
"\"configuration\":{\"supportedModes\":[\"HEAT\",\"COOL\",\"AUTO\",\"OFF\"],"
|
||||||
|
"\"supportsScheduling\":false}}",
|
||||||
|
TEMPERATURE, HEALTH})),
|
||||||
|
thermostat);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_lock_discovery(void)
|
||||||
|
{
|
||||||
|
AlexaDevice doorLock("Front Door Lock", "root", "esp-lock");
|
||||||
|
doorLock.setDisplayCategory(DisplayCategory::SMARTLOCK);
|
||||||
|
doorLock.addCapability(AlexaInterfaces::LockController);
|
||||||
|
doorLock.addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
|
||||||
|
assertDiscovery(endpoint("esp-lock", "Front Door Lock", "SMARTLOCK", list({LOCK, HEALTH})), doorLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_scene_discovery(void)
|
||||||
|
{
|
||||||
|
AlexaDevice scene("Movie Night", "root", "esp-scene");
|
||||||
|
scene.setDisplayCategory(DisplayCategory::SCENE_TRIGGER);
|
||||||
|
scene.addCapability(AlexaInterfaces::SceneController);
|
||||||
|
scene.addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
|
||||||
|
assertDiscovery(endpoint("esp-scene", "Movie Night", "SCENE_TRIGGER",
|
||||||
|
list({"{\"interface\":\"Alexa.SceneController\",\"version\":\"3\",\"type\":\"AlexaInterface\"}",
|
||||||
|
HEALTH})),
|
||||||
|
scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_doorbell_discovery(void)
|
||||||
|
{
|
||||||
|
AlexaDevice doorbell("Front Door Bell", "root", "esp-doorbell");
|
||||||
|
doorbell.setDisplayCategory(DisplayCategory::DOORBELL);
|
||||||
|
doorbell.addCapability(AlexaInterfaces::DoorbellEventSource)->setProactivelyReported(true);
|
||||||
|
doorbell.addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
|
||||||
|
assertDiscovery(
|
||||||
|
endpoint("esp-doorbell", "Front Door Bell", "DOORBELL",
|
||||||
|
list({"{\"interface\":\"Alexa.DoorbellEventSource\",\"version\":\"3\",\"type\":\"AlexaInterface\"}",
|
||||||
|
HEALTH})),
|
||||||
|
doorbell);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_multi_device_discovery(void)
|
||||||
|
{
|
||||||
|
const char *const names[] = {"Left Lamp", "Right Lamp"};
|
||||||
|
const char *const ids[] = {"esp-multi-left", "esp-multi-right"};
|
||||||
|
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
AlexaDevice lamp(names[i], "root", ids[i]);
|
||||||
|
lamp.setDisplayCategory(DisplayCategory::LIGHT);
|
||||||
|
lamp.addCapability(AlexaInterfaces::PowerController);
|
||||||
|
lamp.addCapability(AlexaInterfaces::EndpointHealth);
|
||||||
|
|
||||||
|
assertDiscovery(endpoint(ids[i], names[i], "LIGHT", list({POWER, HEALTH})), lamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int, char **)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_light_discovery);
|
||||||
|
RUN_TEST(test_dimmable_light_discovery);
|
||||||
|
RUN_TEST(test_color_temperature_light_discovery);
|
||||||
|
RUN_TEST(test_color_light_discovery);
|
||||||
|
RUN_TEST(test_temperature_sensor_discovery);
|
||||||
|
RUN_TEST(test_contact_sensor_discovery);
|
||||||
|
RUN_TEST(test_blind_discovery);
|
||||||
|
RUN_TEST(test_thermostat_discovery);
|
||||||
|
RUN_TEST(test_lock_discovery);
|
||||||
|
RUN_TEST(test_scene_discovery);
|
||||||
|
RUN_TEST(test_doorbell_discovery);
|
||||||
|
RUN_TEST(test_multi_device_discovery);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue