// Host tests of discovery: the object that each example announces on /discover_r, compared byte by byte // with what library 1.1.0 sent for the same sketch, and what the descriptor rows of src/AlexaInterfaces.cpp hold. // The devices are built with the calls of the example's setup(). // pio test -e native #include #include #include #include #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(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(200))); } 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); return UNITY_END(); }