An interface without properties says beside its name what Alexa has to know of it; the library left both out. With a board and Alexa on 2026-09-28 the discovery of a doorbell was accepted (202), the endpoint was not listed, and its DoorbellPress was answered with 500 INTERNAL_SERVICE_EXCEPTION. DoorbellEventSource now always carries "proactivelyReported": true. SceneController carries "supportsDeactivation", false until the sketch calls setSupportsDeactivation(true); on another interface the call is refused and logged. The Scene example says that it can be undone. 148 host tests. Light: static RAM 30,616 B, flash 332,705 B (+140 B). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
297 lines
14 KiB
C++
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)->setSupportsDeactivation(true);
|
|
scene.addCapability(AlexaInterfaces::EndpointHealth);
|
|
|
|
assertDiscovery(endpoint("esp-scene", "Movie Night", "SCENE_TRIGGER",
|
|
list({"{\"interface\":\"Alexa.SceneController\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"supportsDeactivation\":true}",
|
|
HEALTH})),
|
|
scene);
|
|
}
|
|
|
|
void test_doorbell_discovery(void)
|
|
{
|
|
AlexaDevice doorbell("Front Door Bell", "root", "esp-doorbell");
|
|
doorbell.setDisplayCategory(DisplayCategory::DOORBELL);
|
|
doorbell.addCapability(AlexaInterfaces::DoorbellEventSource);
|
|
doorbell.addCapability(AlexaInterfaces::EndpointHealth);
|
|
|
|
assertDiscovery(
|
|
endpoint("esp-doorbell", "Front Door Bell", "DOORBELL",
|
|
list({"{\"interface\":\"Alexa.DoorbellEventSource\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"proactivelyReported\":true}",
|
|
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();
|
|
}
|