diff --git a/include/hueplusplus/APICache.h b/include/hueplusplus/APICache.h index c1ffbf4..c53a9a5 100644 --- a/include/hueplusplus/APICache.h +++ b/include/hueplusplus/APICache.h @@ -34,6 +34,7 @@ namespace hueplusplus class APICache { public: + APICache(std::shared_ptr baseCache, const std::string& subEntry); //! \brief Constructs APICache //! \param path URL appended after username, may be empty. //! \param commands HueCommandAPI for making API requests. @@ -54,12 +55,14 @@ public: //! \throws nlohmann::json::parse_error when response could not be parsed nlohmann::json& getValue(); //! \brief Get cached value, does not refresh. + //! \throws HueException when no previous request was cached const nlohmann::json& getValue() const; //! \brief Get duration between refreshes. std::chrono::steady_clock::duration getRefreshDuration() const; private: + std::shared_ptr base; std::string path; HueCommandAPI commands; std::chrono::steady_clock::duration refreshDuration; diff --git a/include/hueplusplus/Hue.h b/include/hueplusplus/Hue.h index 3afd94b..98f602d 100644 --- a/include/hueplusplus/Hue.h +++ b/include/hueplusplus/Hue.h @@ -136,6 +136,8 @@ public: Hue(const std::string& ip, const int port, const std::string& username, std::shared_ptr handler, std::chrono::steady_clock::duration refreshDuration = std::chrono::seconds(10)); + void refresh(); + //! \name Configuration ///@{ @@ -362,6 +364,7 @@ private: HueCommandAPI commands; //!< A HueCommandAPI that is used to communicate with the bridge std::chrono::steady_clock::duration refreshDuration; + std::shared_ptr stateCache; ResourceList lights; CreateableResourceList groups; CreateableResourceList schedules; diff --git a/include/hueplusplus/ResourceList.h b/include/hueplusplus/ResourceList.h index fbbdfc4..dd7ad69 100644 --- a/include/hueplusplus/ResourceList.h +++ b/include/hueplusplus/ResourceList.h @@ -36,6 +36,10 @@ template class ResourceList { public: + ResourceList(const HueCommandAPI& commands, const std::string& path, std::shared_ptr baseCache, + const std::string& cacheEntry, const std::function& factory = nullptr) + : commands(commands), stateCache(baseCache, cacheEntry), path(path + '/'), factory(factory) + {} ResourceList(const HueCommandAPI& commands, const std::string& path, std::chrono::steady_clock::duration refreshDuration, const std::function& factory = nullptr) @@ -97,7 +101,7 @@ public: 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; + bool success = utils::safeGetMember(result, 0, "success") == requestPath + " deleted"; auto it = resources.find(id); if (success && it != resources.end()) { @@ -113,7 +117,7 @@ protected: Resource construct(const IdType& id, const nlohmann::json& state) { - return construct(id, state, std::is_constructible {}); + return construct(id, state, std::is_constructible {}); } private: diff --git a/src/APICache.cpp b/src/APICache.cpp index 35a3b1b..70ac025 100644 --- a/src/APICache.cpp +++ b/src/APICache.cpp @@ -23,46 +23,95 @@ #include "hueplusplus/HueExceptionMacro.h" -hueplusplus::APICache::APICache( - const std::string& path, const HueCommandAPI& commands, std::chrono::steady_clock::duration refresh) +namespace hueplusplus +{ +APICache::APICache(std::shared_ptr baseCache, const std::string& subEntry) + : base(baseCache), + path(subEntry), + commands(baseCache->commands), + refreshDuration(baseCache->refreshDuration), + lastRefresh(baseCache->lastRefresh) +{} + +APICache::APICache(const std::string& path, const HueCommandAPI& commands, std::chrono::steady_clock::duration refresh) : path(path), commands(commands), refreshDuration(refresh), lastRefresh(std::chrono::steady_clock::duration::zero()) {} -void hueplusplus::APICache::refresh() +void APICache::refresh() { - value = commands.GETRequest(path, nlohmann::json::object(), CURRENT_FILE_INFO); - lastRefresh = std::chrono::steady_clock::now(); + if (base) + { + base->refresh(); + } + else + { + value = commands.GETRequest(path, nlohmann::json::object(), CURRENT_FILE_INFO); + lastRefresh = std::chrono::steady_clock::now(); + } } -nlohmann::json& hueplusplus::APICache::getValue() +nlohmann::json& APICache::getValue() { - using clock = std::chrono::steady_clock; - // Explicitly check for zero in case refreshDuration is duration::max() - // Negative duration causes overflow check to overflow itself - if (lastRefresh.time_since_epoch().count() == 0 || refreshDuration.count() < 0) + if (base) { - // No value set yet - refresh(); + nlohmann::json& baseState = base->getValue(); + auto pos = baseState.find(path); + if (pos != baseState.end()) + { + return *pos; + } + else + { + throw HueException(CURRENT_FILE_INFO, "Child path not present in base cache"); + } } - // Check if nextRefresh would overflow (assumes lastRefresh is not negative, which it should not be). - // If addition would overflow, do not refresh - else if (clock::duration::max() - refreshDuration > lastRefresh.time_since_epoch()) + else { - clock::time_point nextRefresh = lastRefresh + refreshDuration; - if (clock::now() >= nextRefresh) + using clock = std::chrono::steady_clock; + // Explicitly check for zero in case refreshDuration is duration::max() + // Negative duration causes overflow check to overflow itself + if (lastRefresh.time_since_epoch().count() == 0 || refreshDuration.count() < 0) { + // No value set yet refresh(); } + // Check if nextRefresh would overflow (assumes lastRefresh is not negative, which it should not be). + // If addition would overflow, do not refresh + else if (clock::duration::max() - refreshDuration > lastRefresh.time_since_epoch()) + { + clock::time_point nextRefresh = lastRefresh + refreshDuration; + if (clock::now() >= nextRefresh) + { + refresh(); + } + } + return value; } - return value; } -const nlohmann::json& hueplusplus::APICache::getValue() const +const nlohmann::json& APICache::getValue() const { - return value; + if (base) + { + // Make const reference to not refresh + const APICache& b = *base; + return b.getValue().at(path); + } + else + { + if (lastRefresh.time_since_epoch().count() == 0) + { + // No value has been requested yet + throw HueException(CURRENT_FILE_INFO, + "Tried to call const getValue(), but no value was cached. " + "Call refresh() or non-const getValue() first."); + } + return value; + } } -std::chrono::steady_clock::duration hueplusplus::APICache::getRefreshDuration() const +std::chrono::steady_clock::duration APICache::getRefreshDuration() const { return refreshDuration; } +} // namespace hueplusplus diff --git a/src/Hue.cpp b/src/Hue.cpp index 26d99a7..f4a0ae4 100644 --- a/src/Hue.cpp +++ b/src/Hue.cpp @@ -137,13 +137,19 @@ Hue::Hue(const std::string& ip, const int port, const std::string& username, http_handler(std::move(handler)), commands(ip, port, username, http_handler), refreshDuration(refreshDuration), - lights(commands, "/lights", refreshDuration, + stateCache(std::make_shared("", commands, refreshDuration)), + lights(commands, "/lights", stateCache, "lights", [factory = HueLightFactory(commands, refreshDuration)]( int id, const nlohmann::json& state) mutable { return factory.createLight(state, id); }), - groups(commands, "/groups", refreshDuration), - schedules(commands, "/schedules", refreshDuration) + groups(commands, "/groups", stateCache, "groups"), + schedules(commands, "/schedules", stateCache, "schedules") {} +void Hue::refresh() +{ + stateCache->refresh(); +} + std::string Hue::getBridgeIP() const { return ip; @@ -422,10 +428,11 @@ void Hue::setHttpHandler(std::shared_ptr handler) { http_handler = handler; commands = HueCommandAPI(ip, port, username, handler); - lights = ResourceList(commands, "/lights", refreshDuration, + stateCache = std::make_shared("", commands, refreshDuration); + lights = ResourceList(commands, "/lights", stateCache, "lights", [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); + groups = CreateableResourceList(commands, "/groups", stateCache, "groups"); + schedules = CreateableResourceList(commands, "/schedules", stateCache, "schedules"); } } // namespace hueplusplus diff --git a/test/test_APICache.cpp b/test/test_APICache.cpp index ef1e1c7..45cd007 100644 --- a/test/test_APICache.cpp +++ b/test/test_APICache.cpp @@ -110,7 +110,19 @@ TEST(APICache, getValue) EXPECT_EQ(value, cache.getValue()); Mock::VerifyAndClearExpectations(handler.get()); } - // No refresh with const + // Only refresh once + { + std::string path = "/test/abc"; + APICache cache(path, commands, std::chrono::seconds(0)); + nlohmann::json value = { {"a", "b"} }; + EXPECT_CALL(*handler, + GETJson("/api/" + getBridgeUsername() + path, nlohmann::json::object(), getBridgeIp(), getBridgePort())) + .WillOnce(Return(value)); + EXPECT_EQ(value, cache.getValue()); + EXPECT_EQ(value, Const(cache).getValue()); + Mock::VerifyAndClearExpectations(handler.get()); + } + // No refresh with const throws exception { std::string path = "/test/abc"; const APICache cache(path, commands, std::chrono::steady_clock::duration::max()); @@ -118,7 +130,7 @@ TEST(APICache, getValue) EXPECT_CALL(*handler, GETJson("/api/" + getBridgeUsername() + path, nlohmann::json::object(), getBridgeIp(), getBridgePort())) .Times(0); - EXPECT_EQ(nullptr, cache.getValue()); + EXPECT_THROW(cache.getValue(), HueException); Mock::VerifyAndClearExpectations(handler.get()); } } \ No newline at end of file diff --git a/test/test_Hue.cpp b/test/test_Hue.cpp index 3630c4d..13b5bb8 100644 --- a/test/test_Hue.cpp +++ b/test/test_Hue.cpp @@ -379,15 +379,13 @@ TEST(Hue, lightExists) *handler, GETJson("/api/" + getBridgeUsername(), nlohmann::json::object(), getBridgeIp(), getBridgePort())) .Times(AtLeast(1)) .WillRepeatedly(Return(hue_bridge_state)); - EXPECT_CALL(*handler, - GETJson("/api/" + getBridgeUsername() + "/lights/1", nlohmann::json::object(), getBridgeIp(), getBridgePort())) - .Times(AtLeast(1)) - .WillRepeatedly(Return(hue_bridge_state["lights"]["1"])); Hue test_bridge(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler); - EXPECT_EQ(true, Const(test_bridge).lightExists(1)); - EXPECT_EQ(false, Const(test_bridge).lightExists(2)); + test_bridge.refresh(); + + EXPECT_TRUE(Const(test_bridge).lightExists(1)); + EXPECT_FALSE(Const(test_bridge).lightExists(2)); } TEST(Hue, getGroup) @@ -492,13 +490,11 @@ TEST(Hue, groupExists) *handler, GETJson("/api/" + getBridgeUsername(), nlohmann::json::object(), getBridgeIp(), getBridgePort())) .Times(AtLeast(1)) .WillRepeatedly(Return(hue_bridge_state)); - EXPECT_CALL(*handler, - GETJson("/api/" + getBridgeUsername() + "/groups/1", nlohmann::json::object(), getBridgeIp(), getBridgePort())) - .Times(AtLeast(1)) - .WillRepeatedly(Return(hue_bridge_state["groups"]["1"])); Hue test_bridge(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler); + test_bridge.refresh(); + EXPECT_EQ(true, Const(test_bridge).groupExists(1)); EXPECT_EQ(false, Const(test_bridge).groupExists(2)); }