Alex2ESP/test/test_examples/test_main.cpp
David 69b4d59896 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>
2026-09-28 21:41:51 +00:00

297 lines
14 KiB
C++

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