diff --git a/include/hueplusplus/Rule.h b/include/hueplusplus/Rule.h index 391b616..07d6f94 100644 --- a/include/hueplusplus/Rule.h +++ b/include/hueplusplus/Rule.h @@ -141,6 +141,10 @@ private: class CreateRule { public: + //! \brief Construct with necessary parameters + //! \param conditions Conditions for the rule. Must not be empty + //! \param actions Actions for the rule. Must not be empty + CreateRule(const std::vector& conditions, const std::vector& actions); //! \brief Set name //! \see Rule::setName CreateRule& setName(const std::string& name); @@ -149,13 +153,6 @@ public: //! \see Rule::setEnabled CreateRule& setStatus(bool enabled); - //! \brief Set conditions - //! \see Rule::setConditions - CreateRule& setConditions(const std::vector& conditions); - //! \brief Set actions - //! \see Rule::setActions - CreateRule& setActions(const std::vector& actions); - //! \brief Get request to create the rule. //! \returns JSON request for a POST to create the new rule. nlohmann::json getRequest() const; diff --git a/include/hueplusplus/TimePattern.h b/include/hueplusplus/TimePattern.h index 80002ae..b204734 100644 --- a/include/hueplusplus/TimePattern.h +++ b/include/hueplusplus/TimePattern.h @@ -85,7 +85,7 @@ public: //! \brief Get formatted string as expected by Hue API //! \returns Timestamp in the format - //! YYYY-MM-DDThh:mm:ss + //! YYYY-MM-DDThh:mm:ss in local timezone std::string toString() const; //! \brief Parse AbsoluteTime from formatted string in local timezone diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8894f80..6a7bca2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,9 +1,11 @@ set(hueplusplus_SOURCES + Action.cpp APICache.cpp BaseDevice.cpp BaseHttpHandler.cpp Bridge.cpp BridgeConfig.cpp + CLIPSensors.cpp ColorUnits.cpp ExtendedColorHueStrategy.cpp ExtendedColorTemperatureStrategy.cpp @@ -13,6 +15,8 @@ set(hueplusplus_SOURCES HueException.cpp Light.cpp ModelPictures.cpp + NewDeviceList.cpp + Rule.cpp Scene.cpp Schedule.cpp Sensor.cpp @@ -22,7 +26,8 @@ set(hueplusplus_SOURCES StateTransaction.cpp TimePattern.cpp UPnP.cpp - Utils.cpp "ZLLSensors.cpp" "CLIPSensors.cpp" "NewDeviceList.cpp" "Action.cpp") + Utils.cpp + ZLLSensors.cpp) # on windows we want to compile the WinHttpHandler if(WIN32) diff --git a/src/Rule.cpp b/src/Rule.cpp index 49ff5d1..de328d1 100644 --- a/src/Rule.cpp +++ b/src/Rule.cpp @@ -120,7 +120,7 @@ Condition Condition::parse(const nlohmann::json& json) { op = Operator::notIn; } - else + else if(opStr != "eq") { throw HueException(CURRENT_FILE_INFO, "Unknown condition operator: " + opStr); } @@ -129,8 +129,10 @@ Condition Condition::parse(const nlohmann::json& json) } Rule::Rule(int id, const HueCommandAPI& commands, std::chrono::steady_clock::duration refreshDuration) - : id(id), state("/rules/" + id, commands, refreshDuration) -{ } + : id(id), state("/rules/" + std::to_string(id), commands, refreshDuration) +{ + state.refresh(); +} void Rule::refresh(bool force) { @@ -163,12 +165,17 @@ void Rule::setName(const std::string& name) time::AbsoluteTime Rule::getCreated() const { - return time::AbsoluteTime::parseUTC(state.getValue().at("creationtime").get()); + return time::AbsoluteTime::parseUTC(state.getValue().at("created").get()); } time::AbsoluteTime Rule::getLastTriggered() const { - return time::AbsoluteTime::parseUTC(state.getValue().at("lasttriggered").get()); + const std::string lasttriggered = state.getValue().value("lasttriggered", "none"); + if (lasttriggered.empty() || lasttriggered == "none") + { + return time::AbsoluteTime(std::chrono::system_clock::time_point(std::chrono::seconds(0))); + } + return time::AbsoluteTime::parseUTC(lasttriggered); } int Rule::getTimesTriggered() const @@ -184,6 +191,7 @@ bool Rule::isEnabled() const void Rule::setEnabled(bool enabled) { sendPutRequest({{"status", enabled ? "enabled" : "disabled"}}, CURRENT_FILE_INFO); + refresh(true); } std::string Rule::getOwner() const @@ -222,6 +230,7 @@ void Rule::setConditions(const std::vector& conditions) } sendPutRequest({{"conditions", json}}, CURRENT_FILE_INFO); + refresh(true); } void Rule::setActions(const std::vector& actions) @@ -233,26 +242,15 @@ void Rule::setActions(const std::vector& actions) } sendPutRequest({{"actions", json}}, CURRENT_FILE_INFO); + refresh(true); } nlohmann::json Rule::sendPutRequest(const nlohmann::json& request, FileInfo fileInfo) { - return state.getCommandAPI().PUTRequest("/groups/" + std::to_string(id), request, std::move(fileInfo)); -} - -CreateRule& CreateRule::setName(const std::string& name) -{ - request["name"] = name; - return *this; + return state.getCommandAPI().PUTRequest("/rules/" + std::to_string(id), request, std::move(fileInfo)); } -CreateRule& CreateRule::setStatus(bool enabled) -{ - request["status"] = enabled ? "enabled" : "disabled"; - return *this; -} - -CreateRule& CreateRule::setConditions(const std::vector& conditions) +CreateRule::CreateRule(const std::vector& conditions, const std::vector& actions) { nlohmann::json conditionsJson; for (const Condition& c : conditions) @@ -260,17 +258,23 @@ CreateRule& CreateRule::setConditions(const std::vector& conditions) conditionsJson.push_back(c.toJson()); } request["conditions"] = conditionsJson; - return *this; -} - -CreateRule& CreateRule::setActions(const std::vector& actions) -{ nlohmann::json actionsJson; for (const Action& a : actions) { actionsJson.push_back(a.toJson()); } request["actions"] = actionsJson; +} + +CreateRule& CreateRule::setName(const std::string& name) +{ + request["name"] = name; + return *this; +} + +CreateRule& CreateRule::setStatus(bool enabled) +{ + request["status"] = enabled ? "enabled" : "disabled"; return *this; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3a6776f..eee426e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -30,6 +30,7 @@ target_compile_features(gtest PUBLIC cxx_std_14) # define all test sources set(TEST_SOURCES + test_Action.cpp test_APICache.cpp test_BaseDevice.cpp test_BaseHttpHandler.cpp @@ -44,8 +45,10 @@ set(TEST_SOURCES test_Light.cpp test_LightFactory.cpp test_Main.cpp + test_NewDeviceList.cpp test_UPnP.cpp test_ResourceList.cpp + test_Rule.cpp test_Scene.cpp test_Schedule.cpp test_Sensor.cpp @@ -54,7 +57,7 @@ set(TEST_SOURCES test_SimpleColorHueStrategy.cpp test_SimpleColorTemperatureStrategy.cpp test_StateTransaction.cpp - test_TimePattern.cpp "test_NewDeviceList.cpp" "test_Action.cpp") + test_TimePattern.cpp) set(HuePlusPlus_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/include") diff --git a/test/test_Rule.cpp b/test/test_Rule.cpp new file mode 100644 index 0000000..e78d9bd --- /dev/null +++ b/test/test_Rule.cpp @@ -0,0 +1,270 @@ +/** + \file test_Rule.cpp + Copyright Notice\n + Copyright (C) 2020 Jan Rogall - developer\n + + This file is part of hueplusplus. + + hueplusplus is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + hueplusplus is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with hueplusplus. If not, see . +**/ + +#include + +#include + +#include "testhelper.h" + +#include "mocks/mock_HttpHandler.h" + +using namespace hueplusplus; +using namespace testing; + +TEST(Condition, Constructor) +{ + const std::string address = "/api/abcd/test"; + const std::string value = "test value"; + Condition condition(address, Condition::Operator::eq, value); + EXPECT_EQ(address, condition.getAddress()); + EXPECT_EQ(Condition::Operator::eq, condition.getOperator()); + EXPECT_EQ(value, condition.getValue()); +} + +TEST(Condition, toJson) +{ + Condition condition("/abcd", Condition::Operator::lt, "3"); + EXPECT_EQ(nlohmann::json({{"address", "/abcd"}, {"operator", "lt"}, {"value", "3"}}), condition.toJson()); +} + +TEST(Condition, parse) +{ + Condition condition = Condition::parse(nlohmann::json({{"address", "/abcd"}, {"operator", "lt"}, {"value", "3"}})); + EXPECT_EQ("/abcd", condition.getAddress()); + EXPECT_EQ(Condition::Operator::lt, condition.getOperator()); + EXPECT_EQ("3", condition.getValue()); + EXPECT_THROW(Condition::parse(nlohmann::json({{"address", "/abcd"}, {"operator", "something"}, {"value", "3"}})), + HueException); +} + +TEST(Condition, operatorString) +{ + using Op = Condition::Operator; + std::map values = {{Op::eq, "eq"}, {Op::gt, "gt"}, {Op::lt, "lt"}, {Op::dx, "dx"}, + {Op::ddx, "ddx"}, {Op::stable, "stable"}, {Op::notStable, "not stable"}, {Op::in, "in"}, {Op::notIn, "not in"}}; + + for (const auto& pair : values) + { + Condition c("", pair.first, ""); + // Check that correct string is + EXPECT_EQ(pair.second, c.toJson().at("operator")); + EXPECT_EQ(pair.first, + Condition::parse(nlohmann::json {{"address", "/abcd"}, {"operator", pair.second}, {"value", "3"}}) + .getOperator()); + } +} + +class RuleTest : public Test +{ +protected: + std::shared_ptr handler; + HueCommandAPI commands; + nlohmann::json ruleState; + + RuleTest() + : handler(std::make_shared()), + commands(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler), + ruleState({{"name", "Rule 1"}, {"owner", "testOwner"}, {"created", "2020-06-01T10:00:00"}, + {"lasttriggered", "none"}, {"timestriggered", 0}, {"status", "enabled"}, + {"conditions", {{{"address", "testAddress"}, {"operator", "eq"}, {"value", "10"}}}}, + {"actions", {{{"address", "testAction"}, {"method", "PUT"}, {"body", {}}}}}}) + { } + + void expectGetState(int id) + { + EXPECT_CALL(*handler, + GETJson("/api/" + getBridgeUsername() + "/rules/" + std::to_string(id), _, getBridgeIp(), getBridgePort())) + .WillOnce(Return(ruleState)); + } + + Rule getRule(int id = 1) + { + expectGetState(id); + return Rule(id, commands, std::chrono::steady_clock::duration::max()); + } +}; + +TEST_F(RuleTest, getName) +{ + const std::string name = "Rule name"; + ruleState["name"] = name; + const Rule rule = getRule(); + EXPECT_EQ(name, rule.getName()); +} + +TEST_F(RuleTest, setName) +{ + Rule rule = getRule(); + const std::string name = "Test rule"; + nlohmann::json request = {{"name", name}}; + nlohmann::json response = {{"success", {"/rules/1/name", name}}}; + EXPECT_CALL(*handler, PUTJson("/api/" + getBridgeUsername() + "/rules/1", request, getBridgeIp(), getBridgePort())) + .WillOnce(Return(response)); + expectGetState(1); + rule.setName(name); +} + +TEST_F(RuleTest, getCreated) +{ + const std::string timestamp = "2020-06-01T10:00:00"; + ruleState["created"] = timestamp; + const Rule rule = getRule(); + EXPECT_EQ(time::AbsoluteTime::parseUTC(timestamp).getBaseTime(), rule.getCreated().getBaseTime()); +} + +TEST_F(RuleTest, getLastTriggered) +{ + const std::string timestamp = "2020-06-01T10:00:00"; + ruleState["lasttriggered"] = timestamp; + const Rule rule = getRule(); + EXPECT_EQ(time::AbsoluteTime::parseUTC(timestamp).getBaseTime(), rule.getLastTriggered().getBaseTime()); + ruleState["lasttriggered"] = "none"; + const Rule rule2 = getRule(); + EXPECT_EQ(std::chrono::system_clock::time_point(std::chrono::seconds(0)), rule2.getLastTriggered().getBaseTime()); +} + +TEST_F(RuleTest, getTimesTriggered) +{ + const int times = 20; + ruleState["timestriggered"] = times; + EXPECT_EQ(times, getRule().getTimesTriggered()); +} + +TEST_F(RuleTest, isEnabled) +{ + ruleState["status"] = "enabled"; + EXPECT_TRUE(getRule().isEnabled()); + ruleState["status"] = "disabled"; + EXPECT_FALSE(getRule().isEnabled()); +} + +TEST_F(RuleTest, setEnabled) +{ + Rule rule = getRule(); + { + nlohmann::json request = {{"status", "enabled"}}; + nlohmann::json response = {{"success", {"/rules/1/status", "enabled"}}}; + EXPECT_CALL( + *handler, PUTJson("/api/" + getBridgeUsername() + "/rules/1", request, getBridgeIp(), getBridgePort())) + .WillOnce(Return(response)); + expectGetState(1); + rule.setEnabled(true); + } + { + nlohmann::json request = {{"status", "disabled"}}; + nlohmann::json response = {{"success", {"/rules/1/status", "disabled"}}}; + EXPECT_CALL( + *handler, PUTJson("/api/" + getBridgeUsername() + "/rules/1", request, getBridgeIp(), getBridgePort())) + .WillOnce(Return(response)); + expectGetState(1); + rule.setEnabled(false); + } +} + +TEST_F(RuleTest, getOwner) +{ + const std::string owner = "testowner"; + ruleState["owner"] = owner; + EXPECT_EQ(owner, getRule().getOwner()); +} + +TEST_F(RuleTest, getConditions) +{ + std::vector conditions + = {Condition("/a/b/c", Condition::Operator::eq, "12"), Condition("/d/c", Condition::Operator::dx, "")}; + ruleState["conditions"] = {conditions[0].toJson(), conditions[1].toJson()}; + const std::vector result = getRule().getConditions(); + ASSERT_EQ(2, result.size()); + EXPECT_EQ(conditions[0].toJson(), result[0].toJson()); + EXPECT_EQ(conditions[1].toJson(), result[1].toJson()); +} + +TEST_F(RuleTest, getActions) +{ + nlohmann::json action0 {{"address", "/a/b"}, {"method", "PUT"}, {"body", {{"value", "test"}}}}; + nlohmann::json action1 {{"address", "/c/d"}, {"method", "POST"}, {"body", {{"32", 1}}}}; + + ruleState["actions"] = {action0, action1}; + const std::vector result = getRule().getActions(); + ASSERT_EQ(2, result.size()); + EXPECT_EQ(action0, result[0].toJson()); + EXPECT_EQ(action1, result[1].toJson()); +} + +TEST_F(RuleTest, setConditions) +{ + std::vector conditions + = {Condition("/a/b/c", Condition::Operator::eq, "12"), Condition("/d/c", Condition::Operator::dx, "")}; + const nlohmann::json request = {{"conditions", {conditions[0].toJson(), conditions[1].toJson()}}}; + + Rule rule = getRule(); + EXPECT_CALL(*handler, PUTJson("/api/" + getBridgeUsername() + "/rules/1", request, getBridgeIp(), getBridgePort())); + expectGetState(1); + rule.setConditions(conditions); +} + +TEST_F(RuleTest, setActions) +{ + using hueplusplus::Action; + nlohmann::json action0 {{"address", "/a/b"}, {"method", "PUT"}, {"body", {{"value", "test"}}}}; + nlohmann::json action1 {{"address", "/c/d"}, {"method", "POST"}, {"body", {{"32", 1}}}}; + const nlohmann::json request = {{"actions", {action0, action1}}}; + + const std::vector actions = {Action(action0), Action(action1)}; + + Rule rule = getRule(); + EXPECT_CALL(*handler, PUTJson("/api/" + getBridgeUsername() + "/rules/1", request, getBridgeIp(), getBridgePort())); + expectGetState(1); + rule.setActions(actions); +} + +TEST(CreateRule, setName) +{ + const std::string name = "New rule"; + const nlohmann::json request = {{"conditions", {}}, {"actions", {}}, {"name", name}}; + EXPECT_EQ(request, CreateRule({}, {}).setName(name).getRequest()); +} + +TEST(CreateRule, setStatus) +{ + { + const nlohmann::json request = {{"conditions", {}}, {"actions", {}}, {"status", "enabled"}}; + EXPECT_EQ(request, CreateRule({}, {}).setStatus(true).getRequest()); + } + { + const nlohmann::json request = {{"conditions", {}}, {"actions", {}}, {"status", "disabled"}}; + EXPECT_EQ(request, CreateRule({}, {}).setStatus(false).getRequest()); + } +} + +TEST(CreateRule, Constructor) +{ + using hueplusplus::Action; + std::vector conditions + = {Condition("/a/b/c", Condition::Operator::eq, "12"), Condition("/d/c", Condition::Operator::dx, "")}; + nlohmann::json action0 {{"address", "/a/b"}, {"method", "PUT"}, {"body", {{"value", "test"}}}}; + nlohmann::json action1 {{"address", "/c/d"}, {"method", "POST"}, {"body", {{"32", 1}}}}; + const std::vector actions = {Action(action0), Action(action1)}; + const nlohmann::json request + = {{"conditions", {conditions[0].toJson(), conditions[1].toJson()}}, {"actions", {action0, action1}}}; + EXPECT_EQ(request, CreateRule(conditions, actions).getRequest()); +}