// 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, 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 #include #include #include #include "AlexaDevice.h" #include "AlexaLog.h" #include "AlexaResources.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\",\"supportsDeactivation\":false}"), 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))); } 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(context); JsonObject supportedRange = configuration["supportedRange"].to(); 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(); for (const char *position : {"Up", "Down"}) { JsonObject mode = supportedModes.add(); mode["value"] = std::string("Position.") + position; JsonObject name = mode["modeResources"]["friendlyNames"].add(); 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); } // alexa-scenecontroller.html: supportsDeactivation says whether Alexa may send Deactivate void test_scene_that_can_be_undone_says_so_and_no_other_interface_can(void) { AlexaDevice device("Movie Night", "root", "ESP-01"); device.setDisplayCategory(DisplayCategory::SCENE_TRIGGER); device.addCapability(AlexaInterfaces::SceneController)->setSupportsDeactivation(true); assertDiscovery(endpoint("Movie Night", "SCENE_TRIGGER", "{\"interface\":\"Alexa.SceneController\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"supportsDeactivation\":true}"), device); AlexaDevice lamp("Lamp", "root", "ESP-02"); AlexaCapability *power = lamp.addCapability(AlexaInterfaces::PowerController); TEST_ASSERT_TRUE(power->isValid()); power->setSupportsDeactivation(true); TEST_ASSERT_FALSE_MESSAGE(power->isValid(), "a call the interface cannot take is a refused call"); } void test_doorbell_is_announced_without_properties(void) { AlexaDevice device("Front Door", "root", "ESP-01"); device.setDisplayCategory(DisplayCategory::DOORBELL); device.addCapability(AlexaInterfaces::DoorbellEventSource); assertDiscovery(endpoint("Front Door", "DOORBELL", "{\"interface\":\"Alexa.DoorbellEventSource\",\"version\":\"3\",\"type\":\"AlexaInterface\",\"proactivelyReported\":true}"), 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"); AlexaCapability *power = device.addCapability(AlexaInterfaces::PowerController); power->addActionMapping(ActionMapping({AlexaAction::Open}, "TurnOn")); AlexaCapability *brightness = device.addCapability(AlexaInterfaces::BrightnessController); brightness->addStateMapping({AlexaState::Open}, "ON"); assertLogged(log, "error: Alexa.PowerController: action mapping not added: " "only RangeController, ModeController and ToggleController take one"); assertLogged(log, "error: Alexa.BrightnessController: state mapping not added: " "only RangeController, ModeController and ToggleController take one"); TEST_ASSERT_FALSE(power->isValid()); TEST_ASSERT_FALSE(brightness->isValid()); } 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); } TEST_ASSERT_NOT_NULL(strstr(announcedBy(device).c_str(), "Alexa.RangeController")); TEST_ASSERT_TRUE(lift->isValid()); 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(AlexaAssets::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_FALSE(lift->isValid()); } // One capability for every call that a setter refuses: each has to make its capability invalid void test_capability_is_valid_until_a_setter_refuses_what_it_is_given(void) { AlexaDevice device("Bedroom Blinds", "root", "ESP-01"); AlexaCapability *valid = device.addCapability(AlexaInterfaces::ToggleController, "Blind.Valid"); valid->addFriendlyName("Blind", "en-US") .addFriendlyAsset(AlexaAssets::DeviceName_Shade) .setNonControllable(true) .addActionMapping(ActionMapping({AlexaAction::Close}, "TurnOn")) .addStateMapping({AlexaState::Closed}, "ON"); TEST_ASSERT_TRUE(valid->isValid()); TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.A")->addFriendlyName("", "en-US").isValid()); TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.B")->addFriendlyName("Blind", nullptr).isValid()); TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.C")->addFriendlyAsset(nullptr).isValid()); TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.D")->addActionMapping(ActionMapping({}, "TurnOn")).isValid()); TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.E")->addActionMapping(ActionMapping({AlexaAction::Open}, "")).isValid()); TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.F")->addStateMapping({}, "ON").isValid()); TEST_ASSERT_FALSE(device.addCapability(AlexaInterfaces::ToggleController, "Blind.G") ->addStateMapping({AlexaState::Open}, static_cast(nullptr)) .isValid()); } void test_capability_that_is_not_valid_is_left_out_of_discovery_with_an_error(void) { CapturedLog log; AlexaLog::setOutput(&log); AlexaDevice device("Bedroom Blinds", "root", "ESP-01"); device.addCapability(AlexaInterfaces::PowerController); // The name that was refused is followed by one that is taken: the capability could be announced, as half // of what the sketch asked for device.addCapability(AlexaInterfaces::RangeController, "Blind.Lift") ->addFriendlyName("Lift", nullptr) .addFriendlyAsset(AlexaAssets::Setting_Opening); assertDiscovery(endpoint("Bedroom Blinds", "OTHER", POWER), device); assertLogged(log, "error: ESP-01: Alexa.RangeController Blind.Lift left out of discovery: " "a call that set it up was refused, correct what the error of that call names"); } void test_state_mapping_takes_a_number_of_any_type(void) { AlexaDevice device("Bedroom Blinds", "root", "ESP-01"); AlexaCapability *lift = device.addCapability(AlexaInterfaces::RangeController, "Blind.Lift"); lift->addFriendlyAsset(AlexaAssets::Setting_Opening).addStateMapping({AlexaState::Closed}, 0).addStateMapping({AlexaState::Open}, 1, 100); AlexaCapability *tilt = device.addCapability(AlexaInterfaces::RangeController, "Blind.Tilt"); tilt->addFriendlyAsset(AlexaAssets::Setting_Direction).addStateMapping({AlexaState::Closed}, 0.0).addStateMapping({AlexaState::Open}, 90u); TEST_ASSERT_TRUE(lift->isValid()); TEST_ASSERT_TRUE(tilt->isValid()); assertDiscovery( endpoint("Bedroom Blinds", "OTHER", "{\"interface\":\"Alexa.RangeController\",\"version\":\"3\",\"type\":\"AlexaInterface\"," "\"properties\":{\"retrievable\":true,\"proactivelyReported\":false," "\"supported\":[{\"name\":\"rangeValue\"}]}," "\"semantics\":{\"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\"}}]}}," "{\"interface\":\"Alexa.RangeController\",\"version\":\"3\",\"type\":\"AlexaInterface\"," "\"properties\":{\"retrievable\":true,\"proactivelyReported\":false," "\"supported\":[{\"name\":\"rangeValue\"}]}," "\"semantics\":{\"stateMappings\":[" "{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.Closed\"],\"value\":0}," "{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.Open\"],\"value\":90}]}," "\"instance\":\"Blind.Tilt\"," "\"capabilityResources\":{\"friendlyNames\":[" "{\"@type\":\"asset\",\"value\":{\"assetId\":\"Alexa.Setting.Direction\"}}]}}"), device); } void test_states_of_a_hunch_and_of_an_announcement_are_announced(void) { AlexaDevice device("Washer", "root", "ESP-01"); device.addCapability(AlexaInterfaces::ToggleController, "Washer.Eco") ->addFriendlyName("Eco", "en-US") .addStateMapping({AlexaState::EcoOn}, "ON") .addStateMapping({AlexaState::EcoOff}, "OFF"); device.addCapability(AlexaInterfaces::ModeController, "Washer.Cycle") ->addFriendlyAsset(AlexaAssets::Setting_WashCycle) .addStateMapping({AlexaState::Stuck, AlexaState::Done}, "Cycle.End"); device.addCapability(AlexaInterfaces::RangeController, "Washer.Detergent") ->addFriendlyName("Detergent", "en-US") .addStateMapping({AlexaState::Empty, AlexaState::Low}, 0, 10) .addStateMapping({AlexaState::Full}, 100); const std::string announced = announcedBy(device); TEST_ASSERT_NOT_NULL(strstr(announced.c_str(), "\"stateMappings\":[{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.EcoOn\"],\"value\":\"ON\"}," "{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.EcoOff\"],\"value\":\"OFF\"}]")); TEST_ASSERT_NOT_NULL(strstr(announced.c_str(), "\"stateMappings\":[{\"@type\":\"StatesToValue\"," "\"states\":[\"Alexa.States.Done\",\"Alexa.States.Stuck\"],\"value\":\"Cycle.End\"}]")); TEST_ASSERT_NOT_NULL(strstr(announced.c_str(), "\"stateMappings\":[{\"@type\":\"StatesToRange\",\"states\":[\"Alexa.States.Low\",\"Alexa.States.Empty\"]," "\"range\":{\"minimumValue\":0,\"maximumValue\":10}}," "{\"@type\":\"StatesToValue\",\"states\":[\"Alexa.States.Full\"],\"value\":100}]")); } void test_assets_and_units_have_the_ids_of_alexa(void) { TEST_ASSERT_EQUAL_STRING("Alexa.Setting.Opening", AlexaAssets::Setting_Opening); TEST_ASSERT_EQUAL_STRING("Alexa.Setting.2GGuestWiFi", AlexaAssets::Setting_2GGuestWiFi); TEST_ASSERT_EQUAL_STRING("Alexa.DeviceName.SpaceHeater", AlexaAssets::DeviceName_SpaceHeater); TEST_ASSERT_EQUAL_STRING("Alexa.Value.QuickWash", AlexaAssets::Value_QuickWash); TEST_ASSERT_EQUAL_STRING("Alexa.Button.EighthButton", AlexaAssets::Button_EighthButton); TEST_ASSERT_EQUAL_STRING("Alexa.Gestures.DoubleTap", AlexaAssets::Gesture_DoubleTap); TEST_ASSERT_EQUAL_STRING("Alexa.Shower.RainHead", AlexaAssets::Shower_RainHead); TEST_ASSERT_EQUAL_STRING("Alexa.Actions.Vacuum", AlexaAssets::Actions_Vacuum); TEST_ASSERT_EQUAL_STRING("Alexa.Unit.Percent", AlexaUnits::Percent); TEST_ASSERT_EQUAL_STRING("Alexa.Unit.Temperature.Celsius", AlexaUnits::Temperature_Celsius); TEST_ASSERT_EQUAL_STRING("Alexa.Unit.Volume.CubicMeters", AlexaUnits::Volume_CubicMeters); } 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.States.EcoOn", alexaStateName(AlexaState::EcoOn)); TEST_ASSERT_EQUAL_STRING("Alexa.States.Stuck", alexaStateName(AlexaState::Stuck)); 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_scene_that_can_be_undone_says_so_and_no_other_interface_can); 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); RUN_TEST(test_capability_is_valid_until_a_setter_refuses_what_it_is_given); RUN_TEST(test_capability_that_is_not_valid_is_left_out_of_discovery_with_an_error); RUN_TEST(test_state_mapping_takes_a_number_of_any_type); RUN_TEST(test_states_of_a_hunch_and_of_an_announcement_are_announced); RUN_TEST(test_assets_and_units_have_the_ids_of_alexa); return UNITY_END(); }