AlexaCapability replaces the header-only AlexaInterface as what a device holds: 116 bytes with fixed places for
3 names, 4 action mappings and 2 state mappings, no std::vector or std::string. AlexaInterface and AlexaActions
stay as 1.x names; the five examples compile unchanged and announce the same bytes.
addCapability(row, instance) adds several capabilities of one generic controller. An instanced interface without
an instance or a name, an instance or semantics on an interface that takes none, and a full store are refused
with an ERROR line that says what to change.
PlaybackController and WakeOnLANController announce "properties": {} (AIF_EMPTY_PROPERTIES).
basicLight on a D1 mini: static RAM 30,788 -> 30,672 B, flash 330,509 -> 332,541 B; host tests 61 -> 77.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
662 lines
33 KiB
C++
662 lines
33 KiB
C++
// Host tests of discovery: the object that each example announces on <root>/discover_r, compared byte by byte
|
|
// with what library 1.1.0 sent for the same sketch, what the descriptor rows of src/AlexaInterfaces.cpp hold, and
|
|
// what a capability announces and refuses (src/AlexaCapability.cpp).
|
|
// The devices are built with the calls of the example's setup().
|
|
// pio test -e native
|
|
#include <unity.h>
|
|
#include <ArduinoJson.h>
|
|
#include <string.h>
|
|
#include <string>
|
|
#include "AlexaDevice.h"
|
|
#include "AlexaLog.h"
|
|
#include "AlexaVersion.h"
|
|
|
|
// The endpoint id and the defaults are the same in every example; its name, category and capabilities are not
|
|
static std::string endpoint(const char *friendlyName, const char *displayCategory, const std::string &capabilities)
|
|
{
|
|
return std::string("{\"endpointId\":\"ESP-01\",\"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 + "]}";
|
|
}
|
|
|
|
static const char POWER[] =
|
|
"{\"interface\":\"Alexa.PowerController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":[{\"name\":\"powerState\"}]}}";
|
|
|
|
static const char BRIGHTNESS[] =
|
|
"{\"interface\":\"Alexa.BrightnessController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":[{\"name\":\"brightness\"}]}}";
|
|
|
|
static const char COLOR_TEMPERATURE[] =
|
|
"{\"interface\":\"Alexa.ColorTemperatureController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,"
|
|
"\"supported\":[{\"name\":\"colorTemperatureInKelvin\"}]}}";
|
|
|
|
static const char TEMPERATURE[] =
|
|
"{\"interface\":\"Alexa.TemperatureSensor\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":[{\"name\":\"temperature\"}]}}";
|
|
|
|
static const char BLIND_TOGGLE[] =
|
|
"{\"interface\":\"Alexa.ToggleController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":[{\"name\":\"toggleState\"}]},"
|
|
"\"semantics\":{\"actionMappings\":["
|
|
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Close\"],\"directive\":{\"name\":\"TurnOn\"}},"
|
|
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Open\"],\"directive\":{\"name\":\"TurnOff\"}}]},"
|
|
"\"instance\":\"ESP-01.Toggle\","
|
|
"\"capabilityResources\":{\"friendlyNames\":["
|
|
"{\"@type\":\"text\",\"value\":{\"text\":\"Bedroom Blinds\",\"locale\":\"en-US\"}}]}}";
|
|
|
|
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_basic_light_discovery(void)
|
|
{
|
|
AlexaDevice device("Test Lamp", "root", "ESP-01");
|
|
device.setDisplayCategory(DisplayCategory::LIGHT);
|
|
device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
|
|
|
|
assertDiscovery(endpoint("Test Lamp", "LIGHT", POWER), device);
|
|
}
|
|
|
|
void test_light_with_brightness_discovery(void)
|
|
{
|
|
AlexaDevice device("Test Lamp", "root", "ESP-01");
|
|
device.setDisplayCategory(DisplayCategory::LIGHT);
|
|
device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
|
|
device.addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER);
|
|
|
|
assertDiscovery(endpoint("Test Lamp", "LIGHT", std::string(POWER) + "," + BRIGHTNESS), device);
|
|
}
|
|
|
|
void test_light_with_color_temperature_discovery(void)
|
|
{
|
|
AlexaDevice device("Test Lamp ESP", "root", "ESP-01");
|
|
device.setDisplayCategory(DisplayCategory::LIGHT);
|
|
device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
|
|
device.addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER);
|
|
device.addCapability(AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER);
|
|
|
|
assertDiscovery(endpoint("Test Lamp ESP", "LIGHT", std::string(POWER) + "," + BRIGHTNESS + "," + COLOR_TEMPERATURE),
|
|
device);
|
|
}
|
|
|
|
void test_temperature_sensor_discovery(void)
|
|
{
|
|
AlexaDevice device("ESP Temp", "root", "ESP-01");
|
|
device.setDisplayCategory(DisplayCategory::TEMPERATURE_SENSOR);
|
|
device.addCapability(AlexaInterfaceType::TEMPERATURE_SENSOR);
|
|
|
|
assertDiscovery(endpoint("ESP Temp", "TEMPERATURE_SENSOR", TEMPERATURE), device);
|
|
}
|
|
|
|
void test_blind_control_discovery(void)
|
|
{
|
|
String deviceName = "Bedroom Blinds";
|
|
AlexaDevice device(deviceName, "root", "ESP-01");
|
|
device.setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
|
|
device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
|
|
AlexaInterface *toggleController = device.addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER);
|
|
toggleController->setInstance("ESP-01.Toggle");
|
|
toggleController->addFriendlyName(deviceName.c_str(), "en-US");
|
|
ActionMapping closeMapping({AlexaActions::Close}, "TurnOn");
|
|
ActionMapping openMapping({AlexaActions::Open}, "TurnOff");
|
|
toggleController->addActionMapping(closeMapping);
|
|
toggleController->addActionMapping(openMapping);
|
|
|
|
assertDiscovery(endpoint("Bedroom Blinds", "INTERIOR_BLIND", std::string(POWER) + "," + BLIND_TOGGLE), device);
|
|
}
|
|
|
|
// What the library prints, for the tests that expect an error
|
|
class CapturedLog : public Print
|
|
{
|
|
public:
|
|
std::string text;
|
|
|
|
size_t write(const uint8_t *buffer, size_t size) override
|
|
{
|
|
text.append(reinterpret_cast<const char *>(buffer), size);
|
|
return size;
|
|
}
|
|
};
|
|
|
|
struct ExpectedRow
|
|
{
|
|
const AlexaInterfaceDesc *row;
|
|
AlexaInterfaceType type;
|
|
const char *ns;
|
|
const char *version;
|
|
const char *properties; // separated by commas
|
|
};
|
|
|
|
// The interface pages of the Alexa documentation, as the design lists them
|
|
static const ExpectedRow EXPECTED_ROWS[] = {
|
|
{&AlexaInterfaces::EndpointHealth, AlexaInterfaceType::ENDPOINT_HEALTH, "Alexa.EndpointHealth", "3.1", "connectivity"},
|
|
{&AlexaInterfaces::PowerController, AlexaInterfaceType::POWER_CONTROLLER, "Alexa.PowerController", "3", "powerState"},
|
|
{&AlexaInterfaces::BrightnessController, AlexaInterfaceType::BRIGHTNESS_CONTROLLER, "Alexa.BrightnessController", "3", "brightness"},
|
|
{&AlexaInterfaces::ColorController, AlexaInterfaceType::COLOR_CONTROLLER, "Alexa.ColorController", "3", "color"},
|
|
{&AlexaInterfaces::ColorTemperatureController, AlexaInterfaceType::COLOR_TEMPERATURE_CONTROLLER, "Alexa.ColorTemperatureController", "3", "colorTemperatureInKelvin"},
|
|
{&AlexaInterfaces::PowerLevelController, AlexaInterfaceType::POWER_LEVEL_CONTROLLER, "Alexa.PowerLevelController", "3", "powerLevel"},
|
|
{&AlexaInterfaces::PercentageController, AlexaInterfaceType::PERCENTAGE_CONTROLLER, "Alexa.PercentageController", "3", "percentage"},
|
|
{&AlexaInterfaces::RangeController, AlexaInterfaceType::RANGE_CONTROLLER, "Alexa.RangeController", "3", "rangeValue"},
|
|
{&AlexaInterfaces::ModeController, AlexaInterfaceType::MODE_CONTROLLER, "Alexa.ModeController", "3", "mode"},
|
|
{&AlexaInterfaces::ToggleController, AlexaInterfaceType::TOGGLE_CONTROLLER, "Alexa.ToggleController", "3", "toggleState"},
|
|
{&AlexaInterfaces::ThermostatController, AlexaInterfaceType::THERMOSTAT_CONTROLLER, "Alexa.ThermostatController", "3.2", "targetSetpoint,lowerSetpoint,upperSetpoint,thermostatMode"},
|
|
{&AlexaInterfaces::TemperatureSensor, AlexaInterfaceType::TEMPERATURE_SENSOR, "Alexa.TemperatureSensor", "3", "temperature"},
|
|
{&AlexaInterfaces::HumiditySensor, AlexaInterfaceType::HUMIDITY_SENSOR, "Alexa.HumiditySensor", "3", "relativeHumidity"},
|
|
{&AlexaInterfaces::LockController, AlexaInterfaceType::LOCK_CONTROLLER, "Alexa.LockController", "3", "lockState"},
|
|
{&AlexaInterfaces::SceneController, AlexaInterfaceType::SCENE_CONTROLLER, "Alexa.SceneController", "3", ""},
|
|
{&AlexaInterfaces::ContactSensor, AlexaInterfaceType::CONTACT_SENSOR, "Alexa.ContactSensor", "3", "detectionState"},
|
|
{&AlexaInterfaces::MotionSensor, AlexaInterfaceType::MOTION_SENSOR, "Alexa.MotionSensor", "3", "detectionState"},
|
|
{&AlexaInterfaces::DoorbellEventSource, AlexaInterfaceType::DOORBELL_EVENT_SOURCE, "Alexa.DoorbellEventSource", "3", ""},
|
|
{&AlexaInterfaces::TimeHoldController, AlexaInterfaceType::TIME_HOLD_CONTROLLER, "Alexa.TimeHoldController", "3", "holdStartTime,holdEndTime"},
|
|
{&AlexaInterfaces::Speaker, AlexaInterfaceType::SPEAKER, "Alexa.Speaker", "3", "volume,muted"},
|
|
{&AlexaInterfaces::StepSpeaker, AlexaInterfaceType::STEP_SPEAKER, "Alexa.StepSpeaker", "3", ""},
|
|
{&AlexaInterfaces::PlaybackController, AlexaInterfaceType::PLAYBACK_CONTROLLER, "Alexa.PlaybackController", "3", ""},
|
|
{&AlexaInterfaces::PlaybackStateReporter, AlexaInterfaceType::PLAYBACK_STATE_REPORTER, "Alexa.PlaybackStateReporter", "3", "playbackState"},
|
|
{&AlexaInterfaces::InputController, AlexaInterfaceType::INPUT_CONTROLLER, "Alexa.InputController", "3", "input"},
|
|
{&AlexaInterfaces::ChannelController, AlexaInterfaceType::CHANNEL_CONTROLLER, "Alexa.ChannelController", "3", "channel"},
|
|
{&AlexaInterfaces::InventoryLevelSensor, AlexaInterfaceType::INVENTORY_LEVEL_SENSOR, "Alexa.InventoryLevelSensor", "3", "level"},
|
|
{&AlexaInterfaces::SimpleEventSource, AlexaInterfaceType::SIMPLE_EVENT_SOURCE, "Alexa.SimpleEventSource", "1.0", ""},
|
|
{&AlexaInterfaces::WakeOnLANController, AlexaInterfaceType::WAKE_ON_LAN_CONTROLLER, "Alexa.WakeOnLANController", "3", ""},
|
|
};
|
|
|
|
void test_rows_hold_the_namespace_version_and_properties_of_their_interface(void)
|
|
{
|
|
TEST_ASSERT_EQUAL_size_t(28, sizeof(EXPECTED_ROWS) / sizeof(EXPECTED_ROWS[0]));
|
|
for (const ExpectedRow &expected : EXPECTED_ROWS)
|
|
{
|
|
const AlexaInterfaceDesc &row = *expected.row;
|
|
TEST_ASSERT_EQUAL_STRING(expected.ns, row.ns);
|
|
TEST_ASSERT_EQUAL_STRING_MESSAGE(expected.version, row.version, expected.ns);
|
|
TEST_ASSERT_TRUE_MESSAGE(expected.type == row.interfaceType(), expected.ns);
|
|
|
|
std::string properties;
|
|
for (uint8_t i = 0; i < row.propertyCount(); i++)
|
|
{
|
|
properties += (i == 0 ? "" : ",");
|
|
properties += row.property(i);
|
|
}
|
|
TEST_ASSERT_EQUAL_STRING_MESSAGE(expected.properties, properties.c_str(), expected.ns);
|
|
}
|
|
}
|
|
|
|
void test_every_row_is_found_by_its_type(void)
|
|
{
|
|
for (const ExpectedRow &expected : EXPECTED_ROWS)
|
|
{
|
|
TEST_ASSERT_EQUAL_PTR_MESSAGE(expected.row, alexaInterfaceRow(expected.type), expected.ns);
|
|
}
|
|
TEST_ASSERT_NULL(alexaInterfaceRow(AlexaInterfaceType::COOKING));
|
|
TEST_ASSERT_NULL(alexaInterfaceRow(AlexaInterfaceType::UNKNOWN));
|
|
}
|
|
|
|
void test_generic_controllers_and_interfaces_without_properties_are_flagged(void)
|
|
{
|
|
TEST_ASSERT_TRUE(AlexaInterfaces::ToggleController.has(AIF_INSTANCED));
|
|
TEST_ASSERT_TRUE(AlexaInterfaces::RangeController.has(AIF_SEMANTICS));
|
|
TEST_ASSERT_TRUE(AlexaInterfaces::InventoryLevelSensor.has(AIF_INSTANCED));
|
|
TEST_ASSERT_FALSE(AlexaInterfaces::InventoryLevelSensor.has(AIF_SEMANTICS));
|
|
TEST_ASSERT_TRUE(AlexaInterfaces::SimpleEventSource.has(AIF_NO_PROPERTIES));
|
|
TEST_ASSERT_FALSE(AlexaInterfaces::PowerController.has(AIF_INSTANCED));
|
|
TEST_ASSERT_FALSE(AlexaInterfaces::PlaybackController.has(AIF_NO_PROPERTIES));
|
|
}
|
|
|
|
void test_capability_by_row_and_by_type_is_the_same_capability(void)
|
|
{
|
|
AlexaDevice device("Test Lamp", "root", "ESP-01");
|
|
AlexaInterface *byType = device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
|
|
AlexaInterface *byRow = device.addCapability(AlexaInterfaces::PowerController);
|
|
|
|
TEST_ASSERT_NOT_NULL(byType);
|
|
TEST_ASSERT_EQUAL_PTR(byType, byRow);
|
|
assertDiscovery(endpoint("Test Lamp", "OTHER", POWER), device);
|
|
}
|
|
|
|
void test_type_without_a_row_is_refused_with_an_error(void)
|
|
{
|
|
CapturedLog log;
|
|
AlexaLog::setOutput(&log);
|
|
AlexaDevice device("Oven", "root", "ESP-01");
|
|
|
|
TEST_ASSERT_NULL(device.addCapability(AlexaInterfaceType::COOKING));
|
|
|
|
TEST_ASSERT_NOT_NULL(strstr(log.text.c_str(), "[Alex2ESP] error: ESP-01: capability not added"));
|
|
assertDiscovery(endpoint("Oven", "OTHER", ""), device);
|
|
}
|
|
|
|
void test_scene_is_announced_without_properties(void)
|
|
{
|
|
AlexaDevice device("Movie Night", "root", "ESP-01");
|
|
device.setDisplayCategory(DisplayCategory::SCENE_TRIGGER);
|
|
device.addCapability(AlexaInterfaces::SceneController);
|
|
|
|
assertDiscovery(endpoint("Movie Night", "SCENE_TRIGGER",
|
|
"{\"interface\":\"Alexa.SceneController\",\"version\":\"3\",\"type\":\"AlexaInterface\"}"),
|
|
device);
|
|
}
|
|
|
|
void test_thermostat_announces_its_four_properties(void)
|
|
{
|
|
AlexaDevice device("Hallway", "root", "ESP-01");
|
|
device.setDisplayCategory(DisplayCategory::THERMOSTAT);
|
|
device.addCapability(AlexaInterfaceType::THERMOSTAT_CONTROLLER);
|
|
|
|
assertDiscovery(endpoint("Hallway", "THERMOSTAT",
|
|
"{\"interface\":\"Alexa.ThermostatController\",\"version\":\"3.2\",\"type\":\"AlexaInterface\","
|
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":["
|
|
"{\"name\":\"targetSetpoint\"},{\"name\":\"lowerSetpoint\"},{\"name\":\"upperSetpoint\"},"
|
|
"{\"name\":\"thermostatMode\"}]}}"),
|
|
device);
|
|
}
|
|
|
|
void test_directive_gets_the_type_of_the_capability_it_names(void)
|
|
{
|
|
AlexaDevice device("Test Lamp", "root", "ESP-01");
|
|
device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
|
|
device.addCapability(AlexaInterfaceType::BRIGHTNESS_CONTROLLER);
|
|
|
|
TEST_ASSERT_TRUE(AlexaInterfaceType::BRIGHTNESS_CONTROLLER == device.getInterfaceType("Alexa.BrightnessController"));
|
|
TEST_ASSERT_TRUE(AlexaInterfaceType::POWER_CONTROLLER == device.getInterfaceType("Alexa.PowerController"));
|
|
// An interface of the library that this device does not have, and no interface at all
|
|
TEST_ASSERT_TRUE(AlexaInterfaceType::UNKNOWN == device.getInterfaceType("Alexa.LockController"));
|
|
TEST_ASSERT_TRUE(AlexaInterfaceType::UNKNOWN == device.getInterfaceType("Alexa"));
|
|
}
|
|
|
|
void test_display_categories_have_the_name_of_their_enumerator(void)
|
|
{
|
|
TEST_ASSERT_EQUAL_STRING("ACTIVITY_TRIGGER", alexaDisplayCategoryName(DisplayCategory::ACTIVITY_TRIGGER));
|
|
TEST_ASSERT_EQUAL_STRING("OTHER", alexaDisplayCategoryName(DisplayCategory::OTHER));
|
|
TEST_ASSERT_EQUAL_STRING("WEARABLE", alexaDisplayCategoryName(DisplayCategory::WEARABLE));
|
|
TEST_ASSERT_EQUAL_STRING("TV", DisplayCategoryUtils::toString(DisplayCategory::TV).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("OTHER", alexaDisplayCategoryName(static_cast<DisplayCategory>(200)));
|
|
}
|
|
|
|
static std::string announcedBy(const AlexaDevice &device)
|
|
{
|
|
std::string announced;
|
|
serializeJson(device.getDeviceJSON(), announced);
|
|
return announced;
|
|
}
|
|
|
|
static void assertLogged(const CapturedLog &log, const char *expected)
|
|
{
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(log.text.c_str(), expected), log.text.c_str());
|
|
}
|
|
|
|
struct LiftRange
|
|
{
|
|
int minimum;
|
|
int maximum;
|
|
};
|
|
|
|
static void fillLiftConfiguration(JsonObject configuration, void *context)
|
|
{
|
|
const LiftRange *range = static_cast<const LiftRange *>(context);
|
|
JsonObject supportedRange = configuration["supportedRange"].to<JsonObject>();
|
|
supportedRange["minimumValue"] = range->minimum;
|
|
supportedRange["maximumValue"] = range->maximum;
|
|
supportedRange["precision"] = 1;
|
|
configuration["unitOfMeasure"] = "Alexa.Unit.Percent";
|
|
}
|
|
|
|
void test_range_controller_blind_is_announced_with_names_configuration_and_semantics(void)
|
|
{
|
|
static LiftRange range = {0, 100};
|
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
|
device.setDisplayCategory(DisplayCategory::INTERIOR_BLIND);
|
|
AlexaCapability *lift = device.addCapability(AlexaInterfaces::RangeController, "Blind.Lift");
|
|
TEST_ASSERT_NOT_NULL(lift);
|
|
lift->addFriendlyAsset(PSTR("Alexa.Setting.Opening"))
|
|
.addFriendlyName("Lift", "en-US")
|
|
.setProactivelyReported(true)
|
|
.setConfiguration(fillLiftConfiguration, &range)
|
|
.addActionMapping(ActionMapping({AlexaAction::Close, AlexaAction::Lower}, "SetRangeValue", "{\"rangeValue\":0}"))
|
|
.addActionMapping(ActionMapping({AlexaAction::Raise, AlexaAction::Open}, "SetRangeValue", "{\"rangeValue\":100}"))
|
|
.addStateMapping({AlexaState::Closed}, 0.0f)
|
|
.addStateMapping({AlexaState::Open}, 1.0f, 100.0f);
|
|
|
|
assertDiscovery(
|
|
endpoint("Bedroom Blinds", "INTERIOR_BLIND",
|
|
"{\"interface\":\"Alexa.RangeController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":true,"
|
|
"\"supported\":[{\"name\":\"rangeValue\"}]},"
|
|
"\"semantics\":{\"actionMappings\":["
|
|
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Close\",\"Alexa.Actions.Lower\"],"
|
|
"\"directive\":{\"name\":\"SetRangeValue\",\"payload\":{\"rangeValue\":0}}},"
|
|
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Open\",\"Alexa.Actions.Raise\"],"
|
|
"\"directive\":{\"name\":\"SetRangeValue\",\"payload\":{\"rangeValue\":100}}}],"
|
|
"\"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\"}},"
|
|
"{\"@type\":\"text\",\"value\":{\"text\":\"Lift\",\"locale\":\"en-US\"}}]},"
|
|
"\"configuration\":{\"supportedRange\":{\"minimumValue\":0,\"maximumValue\":100,\"precision\":1},"
|
|
"\"unitOfMeasure\":\"Alexa.Unit.Percent\"}}"),
|
|
device);
|
|
}
|
|
|
|
static void fillPositionConfiguration(JsonObject configuration, void *)
|
|
{
|
|
configuration["ordered"] = false;
|
|
JsonArray supportedModes = configuration["supportedModes"].to<JsonArray>();
|
|
for (const char *position : {"Up", "Down"})
|
|
{
|
|
JsonObject mode = supportedModes.add<JsonObject>();
|
|
mode["value"] = std::string("Position.") + position;
|
|
JsonObject name = mode["modeResources"]["friendlyNames"].add<JsonObject>();
|
|
name["@type"] = "text";
|
|
name["value"]["text"] = position;
|
|
name["value"]["locale"] = "en-US";
|
|
}
|
|
}
|
|
|
|
void test_mode_controller_is_announced_with_its_modes_and_text_states(void)
|
|
{
|
|
AlexaDevice device("Garage Door", "root", "ESP-01");
|
|
device.setDisplayCategory(DisplayCategory::GARAGE_DOOR);
|
|
device.addCapability(AlexaInterfaces::ModeController, "Door.Position")
|
|
->addFriendlyAsset(PSTR("Alexa.Setting.Mode"))
|
|
.setConfiguration(fillPositionConfiguration)
|
|
.addActionMapping(ActionMapping({AlexaAction::Open}, "SetMode", "{\"mode\":\"Position.Up\"}"))
|
|
.addStateMapping({AlexaState::Closed}, "Position.Down");
|
|
|
|
assertDiscovery(
|
|
endpoint("Garage Door", "GARAGE_DOOR",
|
|
"{\"interface\":\"Alexa.ModeController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
|
"\"properties\":{\"retrievable\":true,\"proactivelyReported\":false,\"supported\":[{\"name\":\"mode\"}]},"
|
|
"\"semantics\":{\"actionMappings\":["
|
|
"{\"@type\":\"ActionsToDirective\",\"actions\":[\"Alexa.Actions.Open\"],"
|
|
"\"directive\":{\"name\":\"SetMode\",\"payload\":{\"mode\":\"Position.Up\"}}}],"
|
|
"\"stateMappings\":["
|
|
"{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.Closed\"],\"value\":\"Position.Down\"}]},"
|
|
"\"instance\":\"Door.Position\","
|
|
"\"capabilityResources\":{\"friendlyNames\":["
|
|
"{\"@type\":\"asset\",\"value\":{\"assetId\":\"Alexa.Setting.Mode\"}}]},"
|
|
"\"configuration\":{\"ordered\":false,\"supportedModes\":["
|
|
"{\"value\":\"Position.Up\",\"modeResources\":{\"friendlyNames\":["
|
|
"{\"@type\":\"text\",\"value\":{\"text\":\"Up\",\"locale\":\"en-US\"}}]}},"
|
|
"{\"value\":\"Position.Down\",\"modeResources\":{\"friendlyNames\":["
|
|
"{\"@type\":\"text\",\"value\":{\"text\":\"Down\",\"locale\":\"en-US\"}}]}}]}}"),
|
|
device);
|
|
}
|
|
|
|
void test_toggle_controller_that_only_reports_is_announced_as_non_controllable(void)
|
|
{
|
|
AlexaDevice device("Fan", "root", "ESP-01");
|
|
device.setDisplayCategory(DisplayCategory::FAN);
|
|
device.addCapability(AlexaInterfaces::ToggleController, "Fan.Oscillate")
|
|
->addFriendlyName("Oscillate", "en-US")
|
|
.addFriendlyName("Schwenken", "de-DE")
|
|
.setRetrievable(false)
|
|
.setNonControllable(true);
|
|
|
|
assertDiscovery(
|
|
endpoint("Fan", "FAN",
|
|
"{\"interface\":\"Alexa.ToggleController\",\"version\":\"3\",\"type\":\"AlexaInterface\","
|
|
"\"properties\":{\"retrievable\":false,\"proactivelyReported\":false,"
|
|
"\"supported\":[{\"name\":\"toggleState\"}],\"nonControllable\":true},"
|
|
"\"instance\":\"Fan.Oscillate\","
|
|
"\"capabilityResources\":{\"friendlyNames\":["
|
|
"{\"@type\":\"text\",\"value\":{\"text\":\"Oscillate\",\"locale\":\"en-US\"}},"
|
|
"{\"@type\":\"text\",\"value\":{\"text\":\"Schwenken\",\"locale\":\"de-DE\"}}]}}"),
|
|
device);
|
|
}
|
|
|
|
void test_doorbell_is_announced_without_properties(void)
|
|
{
|
|
AlexaDevice device("Front Door", "root", "ESP-01");
|
|
device.setDisplayCategory(DisplayCategory::DOORBELL);
|
|
device.addCapability(AlexaInterfaces::DoorbellEventSource)->setProactivelyReported(true);
|
|
|
|
assertDiscovery(endpoint("Front Door", "DOORBELL",
|
|
"{\"interface\":\"Alexa.DoorbellEventSource\",\"version\":\"3\",\"type\":\"AlexaInterface\"}"),
|
|
device);
|
|
}
|
|
|
|
void test_playback_and_wake_on_lan_are_announced_with_empty_properties(void)
|
|
{
|
|
AlexaDevice device("Media PC", "root", "ESP-01");
|
|
device.setDisplayCategory(DisplayCategory::COMPUTER);
|
|
device.addCapability(AlexaInterfaces::PlaybackController);
|
|
device.addCapability(AlexaInterfaces::WakeOnLANController);
|
|
|
|
assertDiscovery(
|
|
endpoint("Media PC", "COMPUTER",
|
|
"{\"interface\":\"Alexa.PlaybackController\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"properties\":{}},"
|
|
"{\"interface\":\"Alexa.WakeOnLANController\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"properties\":{}}"),
|
|
device);
|
|
}
|
|
|
|
void test_instance_and_friendly_name_are_copied(void)
|
|
{
|
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
|
{
|
|
std::string instance = "Blind.Tilt";
|
|
std::string name = "Tilt";
|
|
device.addCapability(AlexaInterfaces::RangeController, instance.c_str())->addFriendlyName(name.c_str(), "en-US");
|
|
instance.assign("overwritten");
|
|
name.assign("gone");
|
|
}
|
|
|
|
const std::string announced = announcedBy(device);
|
|
TEST_ASSERT_NOT_NULL(strstr(announced.c_str(), "\"instance\":\"Blind.Tilt\""));
|
|
TEST_ASSERT_NOT_NULL(strstr(announced.c_str(), "\"text\":\"Tilt\""));
|
|
}
|
|
|
|
void test_instances_of_one_interface_are_capabilities_of_their_own(void)
|
|
{
|
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
|
AlexaCapability *lift = device.addCapability(AlexaInterfaces::RangeController, "Blind.Lift");
|
|
AlexaCapability *tilt = device.addCapability(AlexaInterfaces::RangeController, "Blind.Tilt");
|
|
|
|
TEST_ASSERT_NOT_NULL(lift);
|
|
TEST_ASSERT_NOT_NULL(tilt);
|
|
TEST_ASSERT_TRUE(lift != tilt);
|
|
TEST_ASSERT_EQUAL_PTR(tilt, device.addCapability(AlexaInterfaces::RangeController, "Blind.Tilt"));
|
|
TEST_ASSERT_EQUAL_STRING("Blind.Lift", lift->getInstance());
|
|
// As a 1.x sketch asks for the capability it added by its type
|
|
TEST_ASSERT_EQUAL_PTR(lift, device.addCapability(AlexaInterfaceType::RANGE_CONTROLLER));
|
|
}
|
|
|
|
void test_directive_matches_by_namespace_and_for_instanced_interfaces_by_instance(void)
|
|
{
|
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
|
AlexaCapability *power = device.addCapability(AlexaInterfaces::PowerController);
|
|
AlexaCapability *lift = device.addCapability(AlexaInterfaces::RangeController, "Blind.Lift");
|
|
|
|
TEST_ASSERT_TRUE(power->matches("Alexa.PowerController", nullptr));
|
|
TEST_ASSERT_TRUE(power->matches("Alexa.PowerController", "Blind.Lift"));
|
|
TEST_ASSERT_FALSE(power->matches("Alexa.RangeController", nullptr));
|
|
TEST_ASSERT_FALSE(power->matches(nullptr, nullptr));
|
|
TEST_ASSERT_TRUE(lift->matches("Alexa.RangeController", "Blind.Lift"));
|
|
TEST_ASSERT_FALSE(lift->matches("Alexa.RangeController", "Blind.Tilt"));
|
|
TEST_ASSERT_FALSE(lift->matches("Alexa.RangeController", nullptr));
|
|
}
|
|
|
|
void test_instanced_row_without_an_instance_is_refused_with_an_error(void)
|
|
{
|
|
CapturedLog log;
|
|
AlexaLog::setOutput(&log);
|
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
|
|
|
TEST_ASSERT_NULL(device.addCapability(AlexaInterfaces::RangeController));
|
|
TEST_ASSERT_NULL(device.addCapability(AlexaInterfaces::ModeController, ""));
|
|
|
|
assertLogged(log, "error: ESP-01: capability Alexa.RangeController not added: it needs an instance, "
|
|
"addCapability(row, \"Blind.Lift\")");
|
|
assertLogged(log, "error: ESP-01: capability Alexa.ModeController not added: it needs an instance");
|
|
assertDiscovery(endpoint("Bedroom Blinds", "OTHER", ""), device);
|
|
}
|
|
|
|
void test_instance_for_an_interface_without_instances_is_refused_with_an_error(void)
|
|
{
|
|
CapturedLog log;
|
|
AlexaLog::setOutput(&log);
|
|
AlexaDevice device("Test Lamp", "root", "ESP-01");
|
|
|
|
TEST_ASSERT_NULL(device.addCapability(AlexaInterfaces::PowerController, "Lamp.Power"));
|
|
|
|
assertLogged(log, "error: ESP-01: capability Alexa.PowerController Lamp.Power not added: "
|
|
"the interface takes no instance, leave it out");
|
|
assertDiscovery(endpoint("Test Lamp", "OTHER", ""), device);
|
|
}
|
|
|
|
void test_capability_added_by_type_without_an_instance_is_left_out_of_discovery(void)
|
|
{
|
|
CapturedLog log;
|
|
AlexaLog::setOutput(&log);
|
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
|
device.addCapability(AlexaInterfaceType::POWER_CONTROLLER);
|
|
AlexaInterface *toggle = device.addCapability(AlexaInterfaceType::TOGGLE_CONTROLLER);
|
|
TEST_ASSERT_NOT_NULL(toggle);
|
|
toggle->addFriendlyName("Bedroom Blinds", "en-US");
|
|
|
|
assertDiscovery(endpoint("Bedroom Blinds", "OTHER", POWER), device);
|
|
assertLogged(log, "error: ESP-01: Alexa.ToggleController left out of discovery: it has no instance, "
|
|
"call setInstance(\"Blind.Lift\") on it");
|
|
}
|
|
|
|
void test_instanced_capability_without_a_friendly_name_is_left_out_of_discovery(void)
|
|
{
|
|
CapturedLog log;
|
|
AlexaLog::setOutput(&log);
|
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
|
device.addCapability(AlexaInterfaces::PowerController);
|
|
device.addCapability(AlexaInterfaces::ModeController, "Blind.Mode");
|
|
|
|
assertDiscovery(endpoint("Bedroom Blinds", "OTHER", POWER), device);
|
|
assertLogged(log, "error: ESP-01: Alexa.ModeController Blind.Mode left out of discovery: it has no name, "
|
|
"call addFriendlyName(\"Lift\", \"en-US\") on it");
|
|
}
|
|
|
|
void test_semantics_on_an_interface_that_takes_none_are_refused_with_an_error(void)
|
|
{
|
|
CapturedLog log;
|
|
AlexaLog::setOutput(&log);
|
|
AlexaDevice device("Test Lamp", "root", "ESP-01");
|
|
device.addCapability(AlexaInterfaces::PowerController)
|
|
->addActionMapping(ActionMapping({AlexaAction::Open}, "TurnOn"))
|
|
.addStateMapping({AlexaState::Open}, "ON");
|
|
|
|
assertLogged(log, "error: Alexa.PowerController: action mapping not added: "
|
|
"only RangeController, ModeController and ToggleController take one");
|
|
assertLogged(log, "error: Alexa.PowerController: state mapping not added: "
|
|
"only RangeController, ModeController and ToggleController take one");
|
|
assertDiscovery(endpoint("Test Lamp", "OTHER", POWER), device);
|
|
}
|
|
|
|
void test_more_names_and_mappings_than_a_capability_holds_are_refused_with_an_error(void)
|
|
{
|
|
CapturedLog log;
|
|
AlexaLog::setOutput(&log);
|
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
|
AlexaCapability *lift = device.addCapability(AlexaInterfaces::RangeController, "Blind.Lift");
|
|
for (int i = 0; i < ALEX2ESP_MAX_FRIENDLY_NAMES; i++)
|
|
{
|
|
lift->addFriendlyName("Lift", "en-US");
|
|
}
|
|
for (int i = 0; i < ALEX2ESP_MAX_ACTION_MAPPINGS; i++)
|
|
{
|
|
lift->addActionMapping(ActionMapping({AlexaAction::Open}, "SetRangeValue", "{\"rangeValue\":100}"));
|
|
}
|
|
for (int i = 0; i < ALEX2ESP_MAX_STATE_MAPPINGS; i++)
|
|
{
|
|
lift->addStateMapping({AlexaState::Open}, 100.0f);
|
|
}
|
|
const std::string full = announcedBy(device);
|
|
TEST_ASSERT_EQUAL_STRING("", log.text.c_str() + strlen("[Alex2ESP] ESP-01: capability Alexa.RangeController Blind.Lift added\r\n"));
|
|
|
|
lift->addFriendlyName("Shade", "en-US")
|
|
.addFriendlyAsset(PSTR("Alexa.Setting.Opening"))
|
|
.addActionMapping(ActionMapping({AlexaAction::Close}, "SetRangeValue", "{\"rangeValue\":0}"))
|
|
.addStateMapping({AlexaState::Closed}, 0.0f);
|
|
|
|
assertLogged(log, "error: Alexa.RangeController Blind.Lift: friendly name \"Shade\" not added: "
|
|
"it has 3 already (ALEX2ESP_MAX_FRIENDLY_NAMES)");
|
|
assertLogged(log, "error: Alexa.RangeController Blind.Lift: asset not added: it has 3 names already");
|
|
assertLogged(log, "error: Alexa.RangeController Blind.Lift: action mapping to SetRangeValue not added: "
|
|
"it has 4 already (ALEX2ESP_MAX_ACTION_MAPPINGS)");
|
|
assertLogged(log, "error: Alexa.RangeController Blind.Lift: state mapping not added: "
|
|
"it has 2 already (ALEX2ESP_MAX_STATE_MAPPINGS)");
|
|
TEST_ASSERT_EQUAL_STRING(full.c_str(), announcedBy(device).c_str());
|
|
}
|
|
|
|
void test_action_mapping_payload_that_is_no_object_is_left_out_with_an_error(void)
|
|
{
|
|
CapturedLog log;
|
|
AlexaLog::setOutput(&log);
|
|
AlexaDevice device("Bedroom Blinds", "root", "ESP-01");
|
|
device.addCapability(AlexaInterfaces::ToggleController, "Blind.Toggle")
|
|
->addFriendlyName("Blind", "en-US")
|
|
.addActionMapping(ActionMapping({AlexaAction::Close}, "TurnOn", "rangeValue: 0"));
|
|
|
|
const std::string announced = announcedBy(device);
|
|
|
|
TEST_ASSERT_NOT_NULL(strstr(announced.c_str(), "\"directive\":{\"name\":\"TurnOn\"}"));
|
|
assertLogged(log, "error: Alexa.ToggleController Blind.Toggle: payload of the action mapping to TurnOn left out: "
|
|
"it is not the JSON text of an object");
|
|
}
|
|
|
|
void test_actions_and_states_have_the_names_of_alexa(void)
|
|
{
|
|
TEST_ASSERT_EQUAL_STRING("Alexa.Actions.Open", alexaActionName(AlexaAction::Open));
|
|
TEST_ASSERT_EQUAL_STRING("Alexa.Actions.SetEcoOff", alexaActionName(AlexaAction::SetEcoOff));
|
|
TEST_ASSERT_EQUAL_STRING("Alexa.States.Closed", alexaStateName(AlexaState::Closed));
|
|
TEST_ASSERT_EQUAL_STRING("Alexa.Actions.Lower", AlexaActionsUtils::toString(AlexaActions::Lower).c_str());
|
|
}
|
|
|
|
int main(int, char **)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_basic_light_discovery);
|
|
RUN_TEST(test_light_with_brightness_discovery);
|
|
RUN_TEST(test_light_with_color_temperature_discovery);
|
|
RUN_TEST(test_temperature_sensor_discovery);
|
|
RUN_TEST(test_blind_control_discovery);
|
|
|
|
RUN_TEST(test_rows_hold_the_namespace_version_and_properties_of_their_interface);
|
|
RUN_TEST(test_every_row_is_found_by_its_type);
|
|
RUN_TEST(test_generic_controllers_and_interfaces_without_properties_are_flagged);
|
|
RUN_TEST(test_capability_by_row_and_by_type_is_the_same_capability);
|
|
RUN_TEST(test_type_without_a_row_is_refused_with_an_error);
|
|
RUN_TEST(test_scene_is_announced_without_properties);
|
|
RUN_TEST(test_thermostat_announces_its_four_properties);
|
|
RUN_TEST(test_directive_gets_the_type_of_the_capability_it_names);
|
|
RUN_TEST(test_display_categories_have_the_name_of_their_enumerator);
|
|
|
|
RUN_TEST(test_range_controller_blind_is_announced_with_names_configuration_and_semantics);
|
|
RUN_TEST(test_mode_controller_is_announced_with_its_modes_and_text_states);
|
|
RUN_TEST(test_toggle_controller_that_only_reports_is_announced_as_non_controllable);
|
|
RUN_TEST(test_doorbell_is_announced_without_properties);
|
|
RUN_TEST(test_playback_and_wake_on_lan_are_announced_with_empty_properties);
|
|
RUN_TEST(test_instance_and_friendly_name_are_copied);
|
|
RUN_TEST(test_instances_of_one_interface_are_capabilities_of_their_own);
|
|
RUN_TEST(test_directive_matches_by_namespace_and_for_instanced_interfaces_by_instance);
|
|
RUN_TEST(test_instanced_row_without_an_instance_is_refused_with_an_error);
|
|
RUN_TEST(test_instance_for_an_interface_without_instances_is_refused_with_an_error);
|
|
RUN_TEST(test_capability_added_by_type_without_an_instance_is_left_out_of_discovery);
|
|
RUN_TEST(test_instanced_capability_without_a_friendly_name_is_left_out_of_discovery);
|
|
RUN_TEST(test_semantics_on_an_interface_that_takes_none_are_refused_with_an_error);
|
|
RUN_TEST(test_more_names_and_mappings_than_a_capability_holds_are_refused_with_an_error);
|
|
RUN_TEST(test_action_mapping_payload_that_is_no_object_is_left_out_with_an_error);
|
|
RUN_TEST(test_actions_and_states_have_the_names_of_alexa);
|
|
return UNITY_END();
|
|
}
|