diff --git a/include/hueplusplus/Hue.h b/include/hueplusplus/Hue.h index 5107080..3afd94b 100644 --- a/include/hueplusplus/Hue.h +++ b/include/hueplusplus/Hue.h @@ -42,6 +42,8 @@ #include "json/json.hpp" +#include "ResourceList.h" + //! \brief Namespace for the hueplusplus library namespace hueplusplus { @@ -140,12 +142,12 @@ public: //! \brief Function to get the ip address of the hue bridge //! //! \return string containing ip - std::string getBridgeIP(); + std::string getBridgeIP() const; //! \brief Function to get the port of the hue bridge //! //! \return integer containing port - int getBridgePort(); + int getBridgePort() const; //! \brief Send a username request to the Hue bridge. //! @@ -161,7 +163,7 @@ public: //! \brief Function that returns the username //! //! \return The username used for API access - std::string getUsername(); + std::string getUsername() const; //! \brief Function to set the ip address of this class representing a bridge //! @@ -337,7 +339,7 @@ public: //! because Philips provides different file types. \param id Id of a light to //! get the picture of \return String that either contains the filename of the //! picture of the light or if it was not found an empty string - std::string getPictureOfLight(int id) const; + std::string getPictureOfLight(int id); //! \brief Const function that returns the picture name of a given model id //! @@ -354,15 +356,16 @@ private: //!< like "192.168.2.1" std::string username; //!< Username that is ussed to access the hue bridge int port; - std::map lights; //!< Maps ids to HueLights that are controlled by this bridge - std::map groups; //!< Maps ids to Groups - std::map schedules; //!< Maps ids to Schedules std::shared_ptr http_handler; //!< A IHttpHandler that is used to communicate with the //!< bridge HueCommandAPI commands; //!< A HueCommandAPI that is used to communicate with the bridge - APICache stateCache; //!< The state of the hue bridge as it is returned from it - HueLightFactory lightFactory; + std::chrono::steady_clock::duration refreshDuration; + + ResourceList lights; + CreateableResourceList groups; + CreateableResourceList schedules; + }; } // namespace hueplusplus diff --git a/include/hueplusplus/ResourceList.h b/include/hueplusplus/ResourceList.h new file mode 100644 index 0000000..fbbdfc4 --- /dev/null +++ b/include/hueplusplus/ResourceList.h @@ -0,0 +1,188 @@ +/** + \file ResourceList.h + 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 . +**/ + +#ifndef INCLUDE_RESOURCE_LIST_H +#define INCLUDE_RESOURCE_LIST_H + +#include +#include +#include +#include + +#include "APICache.h" +#include "HueException.h" + +namespace hueplusplus +{ +template +class ResourceList +{ +public: + ResourceList(const HueCommandAPI& commands, const std::string& path, + std::chrono::steady_clock::duration refreshDuration, + const std::function& factory = nullptr) + : commands(commands), stateCache(path, commands, refreshDuration), path(path + '/'), factory(factory) + {} + + ResourceList(const ResourceList&) = delete; + ResourceList(ResourceList&&) = default; + ResourceList& operator=(const ResourceList&) = delete; + ResourceList& operator=(ResourceList&&) = default; + + void refresh() { stateCache.refresh(); } + + std::vector> getAll() + { + nlohmann::json state = stateCache.getValue(); + for (auto it = state.begin(); it != state.end(); ++it) + { + get(maybeStoi(it.key())); + } + std::vector> result; + result.reserve(state.size()); + for (auto& entry : resources) + { + result.emplace_back(entry.second); + } + return result; + } + + Resource& get(const IdType& id) + { + auto pos = resources.find(id); + if (pos != resources.end()) + { + pos->second.refresh(); + return pos->second; + } + const nlohmann::json& state = stateCache.getValue(); + std::string key = maybeToString(id); + if (!state.count(key)) + { + throw HueException(FileInfo {__FILE__, __LINE__, __func__}, "Resource id is not valid"); + } + return resources.emplace(id, construct(id, state[key])).first->second; + } + + bool exists(const IdType& id) const + { + auto pos = resources.find(id); + if (pos != resources.end()) + { + return true; + } + return stateCache.getValue().count(maybeToString(id)) != 0; + } + + bool remove(const IdType& id) + { + std::string requestPath = path + maybeToString(id); + nlohmann::json result + = commands.DELETERequest(requestPath, nlohmann::json::object(), FileInfo {__FILE__, __LINE__, __func__}); + bool success = utils::safeGetMember(result, 0, "success") == requestPath; + auto it = resources.find(id); + if (success && it != resources.end()) + { + resources.erase(it); + } + return success; + } + +protected: + static IdType maybeStoi(const std::string& key) { return maybeStoi(key, std::is_integral {}); } + + static std::string maybeToString(const IdType& id) { return maybeToString(id, std::is_integral {}); } + + Resource construct(const IdType& id, const nlohmann::json& state) + { + return construct(id, state, std::is_constructible {}); + } + +private: + // Resource is constructable + Resource construct(const IdType& id, const nlohmann::json& state, std::true_type) + { + if (factory) + { + return factory(id, state); + } + else + { + return Resource(id, commands, stateCache.getRefreshDuration()); + } + } + // Resource is not constructable + Resource construct(const IdType& id, const nlohmann::json& state, std::false_type) + { + if (!factory) + { + throw HueException(FileInfo {__FILE__, __LINE__, __func__}, + "Resource is not constructable with default parameters, but no factory given"); + } + return factory(id, state); + } + +private: + static IdType maybeStoi(const std::string& key, std::true_type) { return std::stoi(key); } + static IdType maybeStoi(const std::string& key, std::false_type) { return key; } + static std::string maybeToString(IdType id, std::true_type) { return std::to_string(id); } + static std::string maybeToString(const IdType& id, std::false_type) { return id; } + +protected: + std::function factory; + + HueCommandAPI commands; + APICache stateCache; + std::string path; + + std::map resources; +}; + +template +class CreateableResourceList : public ResourceList +{ +public: + using ResourceList::ResourceList; + + IdType create(const CreateType& params) + { + std::string requestPath = path; + // Remove leading slash + requestPath.pop_back(); + nlohmann::json response + = commands.POSTRequest(requestPath, params.getRequest(), FileInfo {__FILE__, __LINE__, __func__}); + nlohmann::json id = utils::safeGetMember(response, 0, "success", "id"); + if (id.is_string()) + { + std::string idStr = id.get(); + if (idStr.find(path) == 0) + { + idStr.erase(0, path.size()); + } + stateCache.refresh(); + return maybeStoi(idStr); + } + return IdType {}; + } +}; +} // namespace hueplusplus + +#endif diff --git a/src/Hue.cpp b/src/Hue.cpp index e071565..26d99a7 100644 --- a/src/Hue.cpp +++ b/src/Hue.cpp @@ -132,20 +132,24 @@ std::string HueFinder::ParseDescription(const std::string& description) Hue::Hue(const std::string& ip, const int port, const std::string& username, std::shared_ptr handler, std::chrono::steady_clock::duration refreshDuration) : ip(ip), - port(port), username(username), + port(port), http_handler(std::move(handler)), commands(ip, port, username, http_handler), - stateCache("", commands, refreshDuration), - lightFactory(commands, refreshDuration) + refreshDuration(refreshDuration), + lights(commands, "/lights", refreshDuration, + [factory = HueLightFactory(commands, refreshDuration)]( + int id, const nlohmann::json& state) mutable { return factory.createLight(state, id); }), + groups(commands, "/groups", refreshDuration), + schedules(commands, "/schedules", refreshDuration) {} -std::string Hue::getBridgeIP() +std::string Hue::getBridgeIP() const { return ip; } -int Hue::getBridgePort() +int Hue::getBridgePort() const { return port; } @@ -200,7 +204,7 @@ std::string Hue::requestUsername() return username; } -std::string Hue::getUsername() +std::string Hue::getUsername() const { return username; } @@ -217,242 +221,75 @@ void Hue::setPort(const int port) HueLight& Hue::getLight(int id) { - auto pos = lights.find(id); - if (pos != lights.end()) - { - pos->second.refresh(); - return pos->second; - } - const nlohmann::json& lightsCache = stateCache.getValue()["lights"]; - if (!lightsCache.count(std::to_string(id))) - { - std::cerr << "Error in Hue getLight(): light with id " << id << " is not valid\n"; - throw HueException(CURRENT_FILE_INFO, "Light id is not valid"); - } - auto light = lightFactory.createLight(lightsCache[std::to_string(id)], id); - lights.emplace(id, light); - return lights.find(id)->second; + return lights.get(id); } bool Hue::removeLight(int id) { - nlohmann::json result - = commands.DELETERequest("/lights/" + std::to_string(id), nlohmann::json::object(), CURRENT_FILE_INFO); - bool success = utils::safeGetMember(result, 0, "success") == "/lights/" + std::to_string(id) + " deleted"; - if (success && lights.count(id) != 0) - { - lights.erase(id); - } - return success; + return lights.remove(id); } std::vector> Hue::getAllLights() { - // No reference because getLight may invalidate it - nlohmann::json lightsState = stateCache.getValue()["lights"]; - for (auto it = lightsState.begin(); it != lightsState.end(); ++it) - { - getLight(std::stoi(it.key())); - } - std::vector> result; - for (auto& entry : lights) - { - result.emplace_back(entry.second); - } - return result; + return lights.getAll(); } std::vector> Hue::getAllGroups() { - nlohmann::json groupsState = stateCache.getValue().at("groups"); - for (auto it = groupsState.begin(); it != groupsState.end(); ++it) - { - getGroup(std::stoi(it.key())); - } - std::vector> result; - result.reserve(result.size()); - for (auto& entry : groups) - { - result.emplace_back(entry.second); - } - return result; + return groups.getAll(); } Group& Hue::getGroup(int id) { - auto pos = groups.find(id); - if (pos != groups.end()) - { - pos->second.refresh(); - return pos->second; - } - const nlohmann::json& groupsCache = stateCache.getValue()["groups"]; - if (!groupsCache.count(std::to_string(id))) - { - std::cerr << "Error in Hue getGroup(): group with id " << id << " is not valid\n"; - throw HueException(CURRENT_FILE_INFO, "Group id is not valid"); - } - return groups.emplace(id, Group(id, commands, stateCache.getRefreshDuration())).first->second; + return groups.get(id); } bool Hue::removeGroup(int id) { - nlohmann::json result - = commands.DELETERequest("/groups/" + std::to_string(id), nlohmann::json::object(), CURRENT_FILE_INFO); - bool success = utils::safeGetMember(result, 0, "success") == "/groups/" + std::to_string(id) + " deleted"; - if (success && groups.count(id) != 0) - { - groups.erase(id); - } - return success; -} - -bool Hue::groupExists(int id) -{ - auto pos = lights.find(id); - if (pos != lights.end()) - { - return true; - } - if (stateCache.getValue()["groups"].count(std::to_string(id))) - { - return true; - } - return false; + return groups.remove(id); } bool Hue::groupExists(int id) const { - auto pos = lights.find(id); - if (pos != lights.end()) - { - return true; - } - if (stateCache.getValue()["groups"].count(std::to_string(id))) - { - return true; - } - return false; + return groups.exists(id); } int Hue::createGroup(const CreateGroup& params) { - nlohmann::json response = commands.POSTRequest("/groups", params.getRequest(), CURRENT_FILE_INFO); - nlohmann::json id = utils::safeGetMember(response, 0, "success", "id"); - if (id.is_string()) - { - std::string idStr = id.get(); - // Sometimes the response can be /groups/? - if (idStr.find("/groups/") == 0) - { - idStr.erase(0, 8); - } - stateCache.refresh(); - return std::stoi(idStr); - } - return 0; -} - -bool Hue::lightExists(int id) -{ - auto pos = lights.find(id); - if (pos != lights.end()) - { - return true; - } - if (stateCache.getValue()["lights"].count(std::to_string(id))) - { - return true; - } - return false; + return groups.create(params); } bool Hue::lightExists(int id) const { - auto pos = lights.find(id); - if (pos != lights.end()) - { - return true; - } - if (stateCache.getValue()["lights"].count(std::to_string(id))) - { - return true; - } - return false; + return lights.exists(id); } std::vector> Hue::getAllSchedules() { - nlohmann::json schedulesState = stateCache.getValue().at("schedules"); - for (auto it = schedulesState.begin(); it != schedulesState.end(); ++it) - { - getSchedule(std::stoi(it.key())); - } - std::vector> result; - result.reserve(result.size()); - for (auto& entry : schedules) - { - result.emplace_back(entry.second); - } - return result; + return schedules.getAll(); } Schedule& Hue::getSchedule(int id) { - auto pos = schedules.find(id); - if (pos != schedules.end()) - { - pos->second.refresh(); - return pos->second; - } - const nlohmann::json& schedulesCache = stateCache.getValue()["schedules"]; - if (!schedulesCache.count(std::to_string(id))) - { - std::cerr << "Error in Hue getSchedule(): schedule with id " << id << " is not valid\n"; - throw HueException(CURRENT_FILE_INFO, "Schedule id is not valid"); - } - return schedules.emplace(id, Schedule(id, commands, stateCache.getRefreshDuration())).first->second; + return schedules.get(id); } bool Hue::scheduleExists(int id) const { - auto pos = schedules.find(id); - if (pos != schedules.end()) - { - return true; - } - if (stateCache.getValue()["schedules"].count(std::to_string(id))) - { - return true; - } - return false; + return schedules.exists(id); } int Hue::createSchedule(const CreateSchedule& params) { - nlohmann::json response = commands.POSTRequest("/schedules", params.getRequest(), CURRENT_FILE_INFO); - nlohmann::json id = utils::safeGetMember(response, 0, "success", "id"); - if (id.is_string()) - { - std::string idStr = id.get(); - // Sometimes the response can be /groups/? - if (idStr.find("/schedules/") == 0) - { - idStr.erase(0, 11); - } - stateCache.refresh(); - return std::stoi(idStr); - } - return 0; + return schedules.create(params); } -std::string Hue::getPictureOfLight(int id) const +std::string Hue::getPictureOfLight(int id) { std::string ret = ""; - auto pos = lights.find(id); - if (pos != lights.end()) + if (lights.exists(id)) { - ret = getPictureOfModel(pos->second.getModelId()); + ret = getPictureOfModel(lights.get(id).getModelId()); } return ret; } @@ -585,7 +422,10 @@ void Hue::setHttpHandler(std::shared_ptr handler) { http_handler = handler; commands = HueCommandAPI(ip, port, username, handler); - stateCache = APICache("", commands, stateCache.getRefreshDuration()); - lightFactory = HueLightFactory(commands, stateCache.getRefreshDuration()); + lights = ResourceList(commands, "/lights", refreshDuration, + [factory = HueLightFactory(commands, refreshDuration)]( + int id, const nlohmann::json& state) mutable { return factory.createLight(state, id); }); + groups = CreateableResourceList(commands, "/groups", refreshDuration); + schedules = CreateableResourceList(commands, "/schedules", refreshDuration); } } // namespace hueplusplus diff --git a/test/test_Hue.cpp b/test/test_Hue.cpp index 7b5ce28..3630c4d 100644 --- a/test/test_Hue.cpp +++ b/test/test_Hue.cpp @@ -116,15 +116,6 @@ TEST_F(HueFinderTest, GetBridge) EXPECT_EQ(test_bridge.getBridgePort(), getBridgePort()) << "Bridge Port not matching"; EXPECT_EQ(test_bridge.getUsername(), getBridgeUsername()) << "Bridge username not matching"; - // Verify that username is correctly set in api requests - nlohmann::json hue_bridge_state {{"lights", {}}}; - EXPECT_CALL( - *handler, GETJson("/api/" + getBridgeUsername(), nlohmann::json::object(), getBridgeIp(), getBridgePort())) - .Times(1) - .WillOnce(Return(hue_bridge_state)); - - test_bridge.getAllLights(); - Mock::VerifyAndClearExpectations(handler.get()); } @@ -395,17 +386,8 @@ TEST(Hue, lightExists) Hue test_bridge(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler); - EXPECT_EQ(true, test_bridge.lightExists(1)); - EXPECT_EQ(false, test_bridge.lightExists(2)); - - const Hue const_test_bridge1 = test_bridge; - EXPECT_EQ(true, const_test_bridge1.lightExists(1)); - EXPECT_EQ(false, const_test_bridge1.lightExists(2)); - - test_bridge.getLight(1); - const Hue const_test_bridge2 = test_bridge; - EXPECT_EQ(true, test_bridge.lightExists(1)); - EXPECT_EQ(true, const_test_bridge2.lightExists(1)); + EXPECT_EQ(true, Const(test_bridge).lightExists(1)); + EXPECT_EQ(false, Const(test_bridge).lightExists(2)); } TEST(Hue, getGroup) @@ -517,17 +499,8 @@ TEST(Hue, groupExists) Hue test_bridge(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler); - EXPECT_EQ(true, test_bridge.groupExists(1)); - EXPECT_EQ(false, test_bridge.groupExists(2)); - - const Hue const_test_bridge1 = test_bridge; - EXPECT_EQ(true, const_test_bridge1.groupExists(1)); - EXPECT_EQ(false, const_test_bridge1.groupExists(2)); - - test_bridge.getGroup(1); - const Hue const_test_bridge2 = test_bridge; - EXPECT_EQ(true, test_bridge.groupExists(1)); - EXPECT_EQ(true, const_test_bridge2.groupExists(1)); + EXPECT_EQ(true, Const(test_bridge).groupExists(1)); + EXPECT_EQ(false, Const(test_bridge).groupExists(2)); } TEST(Hue, getAllGroups)