Report helpers for every tier-1 property, asset ids, AlexaCapability::isValid()
AlexaStatusMessage gets a helper for color, percentage, power level, range, mode, lock state, contact, motion, humidity and the thermostat (target, lower and upper setpoint, mode), and addProperty(row, place, value) for a property without one. Each is a function of its own on one shared builder; a value outside of the range of its property is reported as the nearest of the range with an error. AlexaResources.h holds the 103 asset ids of resources-and-assets.html (AlexaAssets, AlexaUnits); AlexaState has the seven states that were missing; addStateMapping() takes a number of any type. A setter that refuses its argument makes isValid() false, and the capability is left out of discovery with an error instead of being announced without what was refused. basicLight: static RAM 30,520 B (was 30,540), flash 333,389 B (was 334,981); 130 host tests (was 112). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
3215457f91
commit
77156ffbe5
10 changed files with 995 additions and 100 deletions
|
|
@ -9,6 +9,7 @@
|
|||
#include <string>
|
||||
#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
|
||||
|
|
@ -552,15 +553,17 @@ void test_semantics_on_an_interface_that_takes_none_are_refused_with_an_error(vo
|
|||
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");
|
||||
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.PowerController: state mapping not added: "
|
||||
assertLogged(log, "error: Alexa.BrightnessController: state mapping not added: "
|
||||
"only RangeController, ModeController and ToggleController take one");
|
||||
assertDiscovery(endpoint("Test Lamp", "OTHER", POWER), device);
|
||||
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)
|
||||
|
|
@ -581,11 +584,12 @@ void test_more_names_and_mappings_than_a_capability_holds_are_refused_with_an_er
|
|||
{
|
||||
lift->addStateMapping({AlexaState::Open}, 100.0f);
|
||||
}
|
||||
const std::string full = announcedBy(device);
|
||||
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(PSTR("Alexa.Setting.Opening"))
|
||||
.addFriendlyAsset(AlexaAssets::Setting_Opening)
|
||||
.addActionMapping(ActionMapping({AlexaAction::Close}, "SetRangeValue", "{\"rangeValue\":0}"))
|
||||
.addStateMapping({AlexaState::Closed}, 0.0f);
|
||||
|
||||
|
|
@ -596,7 +600,125 @@ void test_more_names_and_mappings_than_a_capability_holds_are_refused_with_an_er
|
|||
"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());
|
||||
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<const char *>(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)
|
||||
|
|
@ -620,6 +742,8 @@ 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());
|
||||
}
|
||||
|
||||
|
|
@ -658,5 +782,10 @@ int main(int, char **)
|
|||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -493,6 +493,272 @@ void test_message_is_sent_once(void)
|
|||
TEST_ASSERT_EQUAL_STRING("null", transport.sent[1].json.c_str()); // the bridge refuses it as empty
|
||||
}
|
||||
|
||||
// The properties of the message as it was published, without what is around them
|
||||
static std::string propertiesOf(const FakeTransport::Message &message)
|
||||
{
|
||||
JsonDocument doc;
|
||||
TEST_ASSERT_TRUE(deserializeJson(doc, message.json) == DeserializationError::Ok);
|
||||
std::string text;
|
||||
serializeJson(doc["context"]["properties"], text);
|
||||
return text;
|
||||
}
|
||||
|
||||
#define PROPERTY(ns, name, value) "{\"namespace\":\"" ns "\",\"name\":\"" name "\",\"value\":" value SAMPLED
|
||||
#define PROPERTY_OF(ns, name, instance, value) \
|
||||
"{\"namespace\":\"" ns "\",\"name\":\"" name "\",\"instance\":\"" instance "\",\"value\":" value SAMPLED
|
||||
|
||||
void test_numbers_of_a_light_are_reported_as_they_are(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||
|
||||
lamp.response(TOKEN_OF_THE_DIRECTIVE)
|
||||
.addBrightnessControllerProp(40)
|
||||
.addPercentageControllerProp(75)
|
||||
.addPowerLevelControllerProp(0)
|
||||
.addColorTemperatureControllerProp(10000)
|
||||
.send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"[" PROPERTY("Alexa.BrightnessController", "brightness", "40") ","
|
||||
PROPERTY("Alexa.PercentageController", "percentage", "75") ","
|
||||
PROPERTY("Alexa.PowerLevelController", "powerLevel", "0") ","
|
||||
PROPERTY("Alexa.ColorTemperatureController", "colorTemperatureInKelvin", "10000") "]",
|
||||
propertiesOf(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_color_is_reported_as_hue_saturation_and_brightness(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||
|
||||
lamp.response(TOKEN_OF_THE_DIRECTIVE).addColorControllerProp(350.5f, 0.75f, 0.5f).send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"[" PROPERTY("Alexa.ColorController", "color", "{\"hue\":350.5,\"saturation\":0.75,\"brightness\":0.5}") "]",
|
||||
propertiesOf(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_generic_controllers_report_with_their_instance(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice fan("Fan", "root", "ESP-01", &transport);
|
||||
|
||||
fan.response(TOKEN_OF_THE_DIRECTIVE)
|
||||
.addRangeControllerProp("Fan.Speed", 7.5f)
|
||||
.addRangeControllerProp("Fan.Angle", 100)
|
||||
.addModeControllerProp("Fan.Direction", "Direction.Forward")
|
||||
.addToggleControllerProp("Fan.Oscillate", PowerController::ON)
|
||||
.send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"[" PROPERTY_OF("Alexa.RangeController", "rangeValue", "Fan.Speed", "7.5") ","
|
||||
PROPERTY_OF("Alexa.RangeController", "rangeValue", "Fan.Angle", "100") ","
|
||||
PROPERTY_OF("Alexa.ModeController", "mode", "Fan.Direction", "\"Direction.Forward\"") ","
|
||||
PROPERTY_OF("Alexa.ToggleController", "toggleState", "Fan.Oscillate", "\"ON\"") "]",
|
||||
propertiesOf(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_lock_reports_each_of_its_states(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice lock("Lock", "root", "ESP-01", &transport);
|
||||
|
||||
lock.response(TOKEN_OF_THE_DIRECTIVE)
|
||||
.addLockControllerProp(AlexaLockState::LOCKED)
|
||||
.addLockControllerProp(AlexaLockState::UNLOCKED)
|
||||
.addLockControllerProp(AlexaLockState::JAMMED, 2000)
|
||||
.send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"[" PROPERTY("Alexa.LockController", "lockState", "\"LOCKED\"") ","
|
||||
PROPERTY("Alexa.LockController", "lockState", "\"UNLOCKED\"") ","
|
||||
"{\"namespace\":\"Alexa.LockController\",\"name\":\"lockState\",\"value\":\"JAMMED\","
|
||||
"\"timeOfSample\":\"2026-09-28T13:05:09Z\",\"uncertaintyInMilliseconds\":2000}]",
|
||||
propertiesOf(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_contact_and_motion_report_what_they_detect(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice door("Door", "root", "ESP-01", &transport);
|
||||
|
||||
door.response(TOKEN_OF_THE_DIRECTIVE)
|
||||
.addContactSensorProp(true)
|
||||
.addContactSensorProp(false)
|
||||
.addMotionSensorProp(true)
|
||||
.addMotionSensorProp(false)
|
||||
.send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"[" PROPERTY("Alexa.ContactSensor", "detectionState", "\"DETECTED\"") ","
|
||||
PROPERTY("Alexa.ContactSensor", "detectionState", "\"NOT_DETECTED\"") ","
|
||||
PROPERTY("Alexa.MotionSensor", "detectionState", "\"DETECTED\"") ","
|
||||
PROPERTY("Alexa.MotionSensor", "detectionState", "\"NOT_DETECTED\"") "]",
|
||||
propertiesOf(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_humidity_is_reported_inside_an_object(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice sensor("Sensor", "root", "ESP-01", &transport);
|
||||
|
||||
sensor.response(TOKEN_OF_THE_DIRECTIVE).addHumiditySensorProp(45.5f).send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"[" PROPERTY("Alexa.HumiditySensor", "relativeHumidity", "{\"value\":45.5}") "]",
|
||||
propertiesOf(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_health_reports_a_device_that_cannot_be_reached(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice sensor("Sensor", "root", "ESP-01", &transport);
|
||||
|
||||
sensor.response(TOKEN_OF_THE_DIRECTIVE).addHealthProp(EndpointHealth::OK).addHealthProp(EndpointHealth::UNREACHABLE).send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"[" HEALTH_OK "," PROPERTY("Alexa.EndpointHealth", "connectivity", "{\"value\":\"UNREACHABLE\"}") "]",
|
||||
propertiesOf(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_thermostat_with_one_setpoint_reports_the_target_and_the_mode(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice thermostat("Thermostat", "root", "ESP-01", &transport);
|
||||
|
||||
thermostat.response(TOKEN_OF_THE_DIRECTIVE)
|
||||
.addThermostatSetpointProp(21.5f)
|
||||
.addThermostatModeProp(AlexaThermostatMode::HEAT)
|
||||
.send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"[" PROPERTY("Alexa.ThermostatController", "targetSetpoint", "{\"value\":21.5,\"scale\":\"CELSIUS\"}") ","
|
||||
PROPERTY("Alexa.ThermostatController", "thermostatMode", "\"HEAT\"") "]",
|
||||
propertiesOf(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
void test_thermostat_with_two_setpoints_reports_the_lower_and_the_upper(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice thermostat("Thermostat", "root", "ESP-01", &transport);
|
||||
|
||||
thermostat.response(TOKEN_OF_THE_DIRECTIVE)
|
||||
.addThermostatDualSetpointProp(68, 76, TemperatureSensorScale::FAHRENHEIT)
|
||||
.addThermostatModeProp(AlexaThermostatMode::AUTO)
|
||||
.send();
|
||||
thermostat.response(TOKEN_OF_THE_DIRECTIVE)
|
||||
.addThermostatLowerSetpointProp(68, TemperatureSensorScale::FAHRENHEIT)
|
||||
.addThermostatUpperSetpointProp(76, TemperatureSensorScale::FAHRENHEIT)
|
||||
.addThermostatModeProp(AlexaThermostatMode::AUTO)
|
||||
.send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"[" PROPERTY("Alexa.ThermostatController", "lowerSetpoint", "{\"value\":68,\"scale\":\"FAHRENHEIT\"}") ","
|
||||
PROPERTY("Alexa.ThermostatController", "upperSetpoint", "{\"value\":76,\"scale\":\"FAHRENHEIT\"}") ","
|
||||
PROPERTY("Alexa.ThermostatController", "thermostatMode", "\"AUTO\"") "]",
|
||||
propertiesOf(transport.sent[0]).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(propertiesOf(transport.sent[0]).c_str(), propertiesOf(transport.sent[1]).c_str());
|
||||
}
|
||||
|
||||
void test_every_thermostat_mode_has_the_name_of_alexa(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice thermostat("Thermostat", "root", "ESP-01", &transport);
|
||||
const AlexaThermostatMode modes[] = {AlexaThermostatMode::AUTO, AlexaThermostatMode::COOL, AlexaThermostatMode::ECO,
|
||||
AlexaThermostatMode::EM_HEAT, AlexaThermostatMode::HEAT, AlexaThermostatMode::OFF};
|
||||
const char *const names[] = {"AUTO", "COOL", "ECO", "EM_HEAT", "HEAT", "OFF"};
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
thermostat.response(TOKEN_OF_THE_DIRECTIVE).addThermostatModeProp(modes[i]).send();
|
||||
JsonDocument report;
|
||||
TEST_ASSERT_TRUE(deserializeJson(report, transport.sent[i].json) == DeserializationError::Ok);
|
||||
TEST_ASSERT_EQUAL_STRING(names[i], report["context"]["properties"][0]["value"] | "");
|
||||
}
|
||||
}
|
||||
|
||||
void test_value_outside_of_its_range_is_reported_as_the_nearest_with_an_error(void)
|
||||
{
|
||||
CapturedLog log;
|
||||
AlexaLog::setOutput(&log);
|
||||
FakeTransport transport;
|
||||
AlexaDevice lamp("Lamp", "root", "ESP-01", &transport);
|
||||
|
||||
lamp.response(TOKEN_OF_THE_DIRECTIVE)
|
||||
.addBrightnessControllerProp(120)
|
||||
.addPercentageControllerProp(-5)
|
||||
.addColorTemperatureControllerProp(500)
|
||||
.addHumiditySensorProp(100.5f)
|
||||
.addColorControllerProp(400, -0.25f, 0.5f)
|
||||
.send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"[" PROPERTY("Alexa.BrightnessController", "brightness", "100") ","
|
||||
PROPERTY("Alexa.PercentageController", "percentage", "0") ","
|
||||
PROPERTY("Alexa.ColorTemperatureController", "colorTemperatureInKelvin", "1000") ","
|
||||
PROPERTY("Alexa.HumiditySensor", "relativeHumidity", "{\"value\":100}") ","
|
||||
PROPERTY("Alexa.ColorController", "color", "{\"hue\":360,\"saturation\":0,\"brightness\":0.5}") "]",
|
||||
propertiesOf(transport.sent[0]).c_str());
|
||||
assertLogged(log, "error: ESP-01: Alexa.BrightnessController reported as 100: "
|
||||
"the value was outside of 0 to 100, the range Alexa gives for it");
|
||||
assertLogged(log, "error: ESP-01: Alexa.PercentageController reported as 0: the value was outside of 0 to 100");
|
||||
assertLogged(log, "error: ESP-01: Alexa.ColorTemperatureController reported as 1000: the value was outside of 1000 to 10000");
|
||||
assertLogged(log, "error: ESP-01: Alexa.HumiditySensor reported as 100: the value was outside of 0 to 100");
|
||||
assertLogged(log, "error: ESP-01: Alexa.ColorController reported as 360: the value was outside of 0 to 360");
|
||||
assertLogged(log, "error: ESP-01: Alexa.ColorController reported as 0: the value was outside of 0 to 1");
|
||||
}
|
||||
|
||||
void test_property_without_a_function_is_added_by_its_place_in_the_row(void)
|
||||
{
|
||||
CapturedLog log;
|
||||
AlexaLog::setOutput(&log);
|
||||
FakeTransport transport;
|
||||
AlexaDevice speaker("Speaker", "root", "ESP-01", &transport);
|
||||
JsonDocument values;
|
||||
values["volume"] = 40;
|
||||
values["muted"] = false;
|
||||
values["level"]["@type"] = "Percentage";
|
||||
values["level"]["value"] = 60;
|
||||
|
||||
speaker.response(TOKEN_OF_THE_DIRECTIVE)
|
||||
.addProperty(AlexaInterfaces::Speaker, 0, values["volume"])
|
||||
.addProperty(AlexaInterfaces::Speaker, 1, values["muted"], 250)
|
||||
.addProperty(AlexaInterfaces::InventoryLevelSensor, 0, values["level"], 0, "Speaker.Battery")
|
||||
.addProperty(AlexaInterfaces::Speaker, 2, values["volume"])
|
||||
.addProperty(AlexaInterfaces::SceneController, 0, values["volume"])
|
||||
.send();
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"[" PROPERTY("Alexa.Speaker", "volume", "40") ","
|
||||
"{\"namespace\":\"Alexa.Speaker\",\"name\":\"muted\",\"value\":false,"
|
||||
"\"timeOfSample\":\"2026-09-28T13:05:09Z\",\"uncertaintyInMilliseconds\":250},"
|
||||
PROPERTY_OF("Alexa.InventoryLevelSensor", "level", "Speaker.Battery", "{\"@type\":\"Percentage\",\"value\":60}") "]",
|
||||
propertiesOf(transport.sent[0]).c_str());
|
||||
assertLogged(log, "error: ESP-01: property 2 of Alexa.Speaker not added: the interface has 2, the first of them is 0");
|
||||
assertLogged(log, "error: ESP-01: property 0 of Alexa.SceneController not added: the interface has 0");
|
||||
}
|
||||
|
||||
void test_change_report_takes_the_properties_of_every_function(void)
|
||||
{
|
||||
FakeTransport transport;
|
||||
AlexaDevice door("Door", "root", "ESP-01", &transport);
|
||||
|
||||
TEST_ASSERT_TRUE(door.changeReport(AlexaCause::PHYSICAL_INTERACTION)
|
||||
.addContactSensorProp(true)
|
||||
.context()
|
||||
.addLockControllerProp(AlexaLockState::UNLOCKED)
|
||||
.addHealthProp(EndpointHealth::OK)
|
||||
.send());
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"event\":{" HEADER_OF("Alexa", "ChangeReport") "}" ENDPOINT
|
||||
",\"payload\":{\"change\":{\"cause\":{\"type\":\"PHYSICAL_INTERACTION\"},\"properties\":["
|
||||
PROPERTY("Alexa.ContactSensor", "detectionState", "\"DETECTED\"") "]}}},"
|
||||
"\"context\":{\"properties\":[" PROPERTY("Alexa.LockController", "lockState", "\"UNLOCKED\"") "," HEALTH_OK "]}}",
|
||||
published(transport.sent[0]).c_str());
|
||||
}
|
||||
|
||||
int main(int, char **)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
|
|
@ -515,5 +781,19 @@ int main(int, char **)
|
|||
RUN_TEST(test_every_message_has_an_id_of_its_own);
|
||||
RUN_TEST(test_change_report_of_a_handler_is_not_its_answer);
|
||||
RUN_TEST(test_message_is_sent_once);
|
||||
|
||||
RUN_TEST(test_numbers_of_a_light_are_reported_as_they_are);
|
||||
RUN_TEST(test_color_is_reported_as_hue_saturation_and_brightness);
|
||||
RUN_TEST(test_generic_controllers_report_with_their_instance);
|
||||
RUN_TEST(test_lock_reports_each_of_its_states);
|
||||
RUN_TEST(test_contact_and_motion_report_what_they_detect);
|
||||
RUN_TEST(test_humidity_is_reported_inside_an_object);
|
||||
RUN_TEST(test_health_reports_a_device_that_cannot_be_reached);
|
||||
RUN_TEST(test_thermostat_with_one_setpoint_reports_the_target_and_the_mode);
|
||||
RUN_TEST(test_thermostat_with_two_setpoints_reports_the_lower_and_the_upper);
|
||||
RUN_TEST(test_every_thermostat_mode_has_the_name_of_alexa);
|
||||
RUN_TEST(test_value_outside_of_its_range_is_reported_as_the_nearest_with_an_error);
|
||||
RUN_TEST(test_property_without_a_function_is_added_by_its_place_in_the_row);
|
||||
RUN_TEST(test_change_report_takes_the_properties_of_every_function);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue