Commit 12b9174988a50e8f002b8207bb453f5112ef6c62
Committed by
Moritz Wirger
1 parent
972c34a0
Add possible baseCache to APICache to share cached state.
Showing
7 changed files
with
115 additions
and
41 deletions
include/hueplusplus/APICache.h
| @@ -34,6 +34,7 @@ namespace hueplusplus | @@ -34,6 +34,7 @@ namespace hueplusplus | ||
| 34 | class APICache | 34 | class APICache |
| 35 | { | 35 | { |
| 36 | public: | 36 | public: |
| 37 | + APICache(std::shared_ptr<APICache> baseCache, const std::string& subEntry); | ||
| 37 | //! \brief Constructs APICache | 38 | //! \brief Constructs APICache |
| 38 | //! \param path URL appended after username, may be empty. | 39 | //! \param path URL appended after username, may be empty. |
| 39 | //! \param commands HueCommandAPI for making API requests. | 40 | //! \param commands HueCommandAPI for making API requests. |
| @@ -54,12 +55,14 @@ public: | @@ -54,12 +55,14 @@ public: | ||
| 54 | //! \throws nlohmann::json::parse_error when response could not be parsed | 55 | //! \throws nlohmann::json::parse_error when response could not be parsed |
| 55 | nlohmann::json& getValue(); | 56 | nlohmann::json& getValue(); |
| 56 | //! \brief Get cached value, does not refresh. | 57 | //! \brief Get cached value, does not refresh. |
| 58 | + //! \throws HueException when no previous request was cached | ||
| 57 | const nlohmann::json& getValue() const; | 59 | const nlohmann::json& getValue() const; |
| 58 | 60 | ||
| 59 | //! \brief Get duration between refreshes. | 61 | //! \brief Get duration between refreshes. |
| 60 | std::chrono::steady_clock::duration getRefreshDuration() const; | 62 | std::chrono::steady_clock::duration getRefreshDuration() const; |
| 61 | 63 | ||
| 62 | private: | 64 | private: |
| 65 | + std::shared_ptr<APICache> base; | ||
| 63 | std::string path; | 66 | std::string path; |
| 64 | HueCommandAPI commands; | 67 | HueCommandAPI commands; |
| 65 | std::chrono::steady_clock::duration refreshDuration; | 68 | std::chrono::steady_clock::duration refreshDuration; |
include/hueplusplus/Hue.h
| @@ -136,6 +136,8 @@ public: | @@ -136,6 +136,8 @@ public: | ||
| 136 | Hue(const std::string& ip, const int port, const std::string& username, std::shared_ptr<const IHttpHandler> handler, | 136 | Hue(const std::string& ip, const int port, const std::string& username, std::shared_ptr<const IHttpHandler> handler, |
| 137 | std::chrono::steady_clock::duration refreshDuration = std::chrono::seconds(10)); | 137 | std::chrono::steady_clock::duration refreshDuration = std::chrono::seconds(10)); |
| 138 | 138 | ||
| 139 | + void refresh(); | ||
| 140 | + | ||
| 139 | //! \name Configuration | 141 | //! \name Configuration |
| 140 | ///@{ | 142 | ///@{ |
| 141 | 143 | ||
| @@ -362,6 +364,7 @@ private: | @@ -362,6 +364,7 @@ private: | ||
| 362 | HueCommandAPI commands; //!< A HueCommandAPI that is used to communicate with the bridge | 364 | HueCommandAPI commands; //!< A HueCommandAPI that is used to communicate with the bridge |
| 363 | std::chrono::steady_clock::duration refreshDuration; | 365 | std::chrono::steady_clock::duration refreshDuration; |
| 364 | 366 | ||
| 367 | + std::shared_ptr<APICache> stateCache; | ||
| 365 | ResourceList<HueLight, int> lights; | 368 | ResourceList<HueLight, int> lights; |
| 366 | CreateableResourceList<Group, int, CreateGroup> groups; | 369 | CreateableResourceList<Group, int, CreateGroup> groups; |
| 367 | CreateableResourceList<Schedule, int, CreateSchedule> schedules; | 370 | CreateableResourceList<Schedule, int, CreateSchedule> schedules; |
include/hueplusplus/ResourceList.h
| @@ -36,6 +36,10 @@ template <typename Resource, typename IdType> | @@ -36,6 +36,10 @@ template <typename Resource, typename IdType> | ||
| 36 | class ResourceList | 36 | class ResourceList |
| 37 | { | 37 | { |
| 38 | public: | 38 | public: |
| 39 | + ResourceList(const HueCommandAPI& commands, const std::string& path, std::shared_ptr<APICache> baseCache, | ||
| 40 | + const std::string& cacheEntry, const std::function<Resource(int, const nlohmann::json&)>& factory = nullptr) | ||
| 41 | + : commands(commands), stateCache(baseCache, cacheEntry), path(path + '/'), factory(factory) | ||
| 42 | + {} | ||
| 39 | ResourceList(const HueCommandAPI& commands, const std::string& path, | 43 | ResourceList(const HueCommandAPI& commands, const std::string& path, |
| 40 | std::chrono::steady_clock::duration refreshDuration, | 44 | std::chrono::steady_clock::duration refreshDuration, |
| 41 | const std::function<Resource(int, const nlohmann::json&)>& factory = nullptr) | 45 | const std::function<Resource(int, const nlohmann::json&)>& factory = nullptr) |
| @@ -97,7 +101,7 @@ public: | @@ -97,7 +101,7 @@ public: | ||
| 97 | std::string requestPath = path + maybeToString(id); | 101 | std::string requestPath = path + maybeToString(id); |
| 98 | nlohmann::json result | 102 | nlohmann::json result |
| 99 | = commands.DELETERequest(requestPath, nlohmann::json::object(), FileInfo {__FILE__, __LINE__, __func__}); | 103 | = commands.DELETERequest(requestPath, nlohmann::json::object(), FileInfo {__FILE__, __LINE__, __func__}); |
| 100 | - bool success = utils::safeGetMember(result, 0, "success") == requestPath; | 104 | + bool success = utils::safeGetMember(result, 0, "success") == requestPath + " deleted"; |
| 101 | auto it = resources.find(id); | 105 | auto it = resources.find(id); |
| 102 | if (success && it != resources.end()) | 106 | if (success && it != resources.end()) |
| 103 | { | 107 | { |
| @@ -113,7 +117,7 @@ protected: | @@ -113,7 +117,7 @@ protected: | ||
| 113 | 117 | ||
| 114 | Resource construct(const IdType& id, const nlohmann::json& state) | 118 | Resource construct(const IdType& id, const nlohmann::json& state) |
| 115 | { | 119 | { |
| 116 | - return construct(id, state, std::is_constructible<Resource, IdType, HueCommandAPI, nlohmann::json> {}); | 120 | + return construct(id, state, std::is_constructible<Resource, IdType, HueCommandAPI, std::chrono::steady_clock::duration> {}); |
| 117 | } | 121 | } |
| 118 | 122 | ||
| 119 | private: | 123 | private: |
src/APICache.cpp
| @@ -23,46 +23,95 @@ | @@ -23,46 +23,95 @@ | ||
| 23 | 23 | ||
| 24 | #include "hueplusplus/HueExceptionMacro.h" | 24 | #include "hueplusplus/HueExceptionMacro.h" |
| 25 | 25 | ||
| 26 | -hueplusplus::APICache::APICache( | ||
| 27 | - const std::string& path, const HueCommandAPI& commands, std::chrono::steady_clock::duration refresh) | 26 | +namespace hueplusplus |
| 27 | +{ | ||
| 28 | +APICache::APICache(std::shared_ptr<APICache> baseCache, const std::string& subEntry) | ||
| 29 | + : base(baseCache), | ||
| 30 | + path(subEntry), | ||
| 31 | + commands(baseCache->commands), | ||
| 32 | + refreshDuration(baseCache->refreshDuration), | ||
| 33 | + lastRefresh(baseCache->lastRefresh) | ||
| 34 | +{} | ||
| 35 | + | ||
| 36 | +APICache::APICache(const std::string& path, const HueCommandAPI& commands, std::chrono::steady_clock::duration refresh) | ||
| 28 | : path(path), commands(commands), refreshDuration(refresh), lastRefresh(std::chrono::steady_clock::duration::zero()) | 37 | : path(path), commands(commands), refreshDuration(refresh), lastRefresh(std::chrono::steady_clock::duration::zero()) |
| 29 | {} | 38 | {} |
| 30 | 39 | ||
| 31 | -void hueplusplus::APICache::refresh() | 40 | +void APICache::refresh() |
| 32 | { | 41 | { |
| 33 | - value = commands.GETRequest(path, nlohmann::json::object(), CURRENT_FILE_INFO); | ||
| 34 | - lastRefresh = std::chrono::steady_clock::now(); | 42 | + if (base) |
| 43 | + { | ||
| 44 | + base->refresh(); | ||
| 45 | + } | ||
| 46 | + else | ||
| 47 | + { | ||
| 48 | + value = commands.GETRequest(path, nlohmann::json::object(), CURRENT_FILE_INFO); | ||
| 49 | + lastRefresh = std::chrono::steady_clock::now(); | ||
| 50 | + } | ||
| 35 | } | 51 | } |
| 36 | 52 | ||
| 37 | -nlohmann::json& hueplusplus::APICache::getValue() | 53 | +nlohmann::json& APICache::getValue() |
| 38 | { | 54 | { |
| 39 | - using clock = std::chrono::steady_clock; | ||
| 40 | - // Explicitly check for zero in case refreshDuration is duration::max() | ||
| 41 | - // Negative duration causes overflow check to overflow itself | ||
| 42 | - if (lastRefresh.time_since_epoch().count() == 0 || refreshDuration.count() < 0) | 55 | + if (base) |
| 43 | { | 56 | { |
| 44 | - // No value set yet | ||
| 45 | - refresh(); | 57 | + nlohmann::json& baseState = base->getValue(); |
| 58 | + auto pos = baseState.find(path); | ||
| 59 | + if (pos != baseState.end()) | ||
| 60 | + { | ||
| 61 | + return *pos; | ||
| 62 | + } | ||
| 63 | + else | ||
| 64 | + { | ||
| 65 | + throw HueException(CURRENT_FILE_INFO, "Child path not present in base cache"); | ||
| 66 | + } | ||
| 46 | } | 67 | } |
| 47 | - // Check if nextRefresh would overflow (assumes lastRefresh is not negative, which it should not be). | ||
| 48 | - // If addition would overflow, do not refresh | ||
| 49 | - else if (clock::duration::max() - refreshDuration > lastRefresh.time_since_epoch()) | 68 | + else |
| 50 | { | 69 | { |
| 51 | - clock::time_point nextRefresh = lastRefresh + refreshDuration; | ||
| 52 | - if (clock::now() >= nextRefresh) | 70 | + using clock = std::chrono::steady_clock; |
| 71 | + // Explicitly check for zero in case refreshDuration is duration::max() | ||
| 72 | + // Negative duration causes overflow check to overflow itself | ||
| 73 | + if (lastRefresh.time_since_epoch().count() == 0 || refreshDuration.count() < 0) | ||
| 53 | { | 74 | { |
| 75 | + // No value set yet | ||
| 54 | refresh(); | 76 | refresh(); |
| 55 | } | 77 | } |
| 78 | + // Check if nextRefresh would overflow (assumes lastRefresh is not negative, which it should not be). | ||
| 79 | + // If addition would overflow, do not refresh | ||
| 80 | + else if (clock::duration::max() - refreshDuration > lastRefresh.time_since_epoch()) | ||
| 81 | + { | ||
| 82 | + clock::time_point nextRefresh = lastRefresh + refreshDuration; | ||
| 83 | + if (clock::now() >= nextRefresh) | ||
| 84 | + { | ||
| 85 | + refresh(); | ||
| 86 | + } | ||
| 87 | + } | ||
| 88 | + return value; | ||
| 56 | } | 89 | } |
| 57 | - return value; | ||
| 58 | } | 90 | } |
| 59 | 91 | ||
| 60 | -const nlohmann::json& hueplusplus::APICache::getValue() const | 92 | +const nlohmann::json& APICache::getValue() const |
| 61 | { | 93 | { |
| 62 | - return value; | 94 | + if (base) |
| 95 | + { | ||
| 96 | + // Make const reference to not refresh | ||
| 97 | + const APICache& b = *base; | ||
| 98 | + return b.getValue().at(path); | ||
| 99 | + } | ||
| 100 | + else | ||
| 101 | + { | ||
| 102 | + if (lastRefresh.time_since_epoch().count() == 0) | ||
| 103 | + { | ||
| 104 | + // No value has been requested yet | ||
| 105 | + throw HueException(CURRENT_FILE_INFO, | ||
| 106 | + "Tried to call const getValue(), but no value was cached. " | ||
| 107 | + "Call refresh() or non-const getValue() first."); | ||
| 108 | + } | ||
| 109 | + return value; | ||
| 110 | + } | ||
| 63 | } | 111 | } |
| 64 | 112 | ||
| 65 | -std::chrono::steady_clock::duration hueplusplus::APICache::getRefreshDuration() const | 113 | +std::chrono::steady_clock::duration APICache::getRefreshDuration() const |
| 66 | { | 114 | { |
| 67 | return refreshDuration; | 115 | return refreshDuration; |
| 68 | } | 116 | } |
| 117 | +} // namespace hueplusplus |
src/Hue.cpp
| @@ -137,13 +137,19 @@ Hue::Hue(const std::string& ip, const int port, const std::string& username, | @@ -137,13 +137,19 @@ Hue::Hue(const std::string& ip, const int port, const std::string& username, | ||
| 137 | http_handler(std::move(handler)), | 137 | http_handler(std::move(handler)), |
| 138 | commands(ip, port, username, http_handler), | 138 | commands(ip, port, username, http_handler), |
| 139 | refreshDuration(refreshDuration), | 139 | refreshDuration(refreshDuration), |
| 140 | - lights(commands, "/lights", refreshDuration, | 140 | + stateCache(std::make_shared<APICache>("", commands, refreshDuration)), |
| 141 | + lights(commands, "/lights", stateCache, "lights", | ||
| 141 | [factory = HueLightFactory(commands, refreshDuration)]( | 142 | [factory = HueLightFactory(commands, refreshDuration)]( |
| 142 | int id, const nlohmann::json& state) mutable { return factory.createLight(state, id); }), | 143 | int id, const nlohmann::json& state) mutable { return factory.createLight(state, id); }), |
| 143 | - groups(commands, "/groups", refreshDuration), | ||
| 144 | - schedules(commands, "/schedules", refreshDuration) | 144 | + groups(commands, "/groups", stateCache, "groups"), |
| 145 | + schedules(commands, "/schedules", stateCache, "schedules") | ||
| 145 | {} | 146 | {} |
| 146 | 147 | ||
| 148 | +void Hue::refresh() | ||
| 149 | +{ | ||
| 150 | + stateCache->refresh(); | ||
| 151 | +} | ||
| 152 | + | ||
| 147 | std::string Hue::getBridgeIP() const | 153 | std::string Hue::getBridgeIP() const |
| 148 | { | 154 | { |
| 149 | return ip; | 155 | return ip; |
| @@ -422,10 +428,11 @@ void Hue::setHttpHandler(std::shared_ptr<const IHttpHandler> handler) | @@ -422,10 +428,11 @@ void Hue::setHttpHandler(std::shared_ptr<const IHttpHandler> handler) | ||
| 422 | { | 428 | { |
| 423 | http_handler = handler; | 429 | http_handler = handler; |
| 424 | commands = HueCommandAPI(ip, port, username, handler); | 430 | commands = HueCommandAPI(ip, port, username, handler); |
| 425 | - lights = ResourceList<HueLight, int>(commands, "/lights", refreshDuration, | 431 | + stateCache = std::make_shared<APICache>("", commands, refreshDuration); |
| 432 | + lights = ResourceList<HueLight, int>(commands, "/lights", stateCache, "lights", | ||
| 426 | [factory = HueLightFactory(commands, refreshDuration)]( | 433 | [factory = HueLightFactory(commands, refreshDuration)]( |
| 427 | int id, const nlohmann::json& state) mutable { return factory.createLight(state, id); }); | 434 | int id, const nlohmann::json& state) mutable { return factory.createLight(state, id); }); |
| 428 | - groups = CreateableResourceList<Group, int, CreateGroup>(commands, "/groups", refreshDuration); | ||
| 429 | - schedules = CreateableResourceList<Schedule, int, CreateSchedule>(commands, "/schedules", refreshDuration); | 435 | + groups = CreateableResourceList<Group, int, CreateGroup>(commands, "/groups", stateCache, "groups"); |
| 436 | + schedules = CreateableResourceList<Schedule, int, CreateSchedule>(commands, "/schedules", stateCache, "schedules"); | ||
| 430 | } | 437 | } |
| 431 | } // namespace hueplusplus | 438 | } // namespace hueplusplus |
test/test_APICache.cpp
| @@ -110,7 +110,19 @@ TEST(APICache, getValue) | @@ -110,7 +110,19 @@ TEST(APICache, getValue) | ||
| 110 | EXPECT_EQ(value, cache.getValue()); | 110 | EXPECT_EQ(value, cache.getValue()); |
| 111 | Mock::VerifyAndClearExpectations(handler.get()); | 111 | Mock::VerifyAndClearExpectations(handler.get()); |
| 112 | } | 112 | } |
| 113 | - // No refresh with const | 113 | + // Only refresh once |
| 114 | + { | ||
| 115 | + std::string path = "/test/abc"; | ||
| 116 | + APICache cache(path, commands, std::chrono::seconds(0)); | ||
| 117 | + nlohmann::json value = { {"a", "b"} }; | ||
| 118 | + EXPECT_CALL(*handler, | ||
| 119 | + GETJson("/api/" + getBridgeUsername() + path, nlohmann::json::object(), getBridgeIp(), getBridgePort())) | ||
| 120 | + .WillOnce(Return(value)); | ||
| 121 | + EXPECT_EQ(value, cache.getValue()); | ||
| 122 | + EXPECT_EQ(value, Const(cache).getValue()); | ||
| 123 | + Mock::VerifyAndClearExpectations(handler.get()); | ||
| 124 | + } | ||
| 125 | + // No refresh with const throws exception | ||
| 114 | { | 126 | { |
| 115 | std::string path = "/test/abc"; | 127 | std::string path = "/test/abc"; |
| 116 | const APICache cache(path, commands, std::chrono::steady_clock::duration::max()); | 128 | const APICache cache(path, commands, std::chrono::steady_clock::duration::max()); |
| @@ -118,7 +130,7 @@ TEST(APICache, getValue) | @@ -118,7 +130,7 @@ TEST(APICache, getValue) | ||
| 118 | EXPECT_CALL(*handler, | 130 | EXPECT_CALL(*handler, |
| 119 | GETJson("/api/" + getBridgeUsername() + path, nlohmann::json::object(), getBridgeIp(), getBridgePort())) | 131 | GETJson("/api/" + getBridgeUsername() + path, nlohmann::json::object(), getBridgeIp(), getBridgePort())) |
| 120 | .Times(0); | 132 | .Times(0); |
| 121 | - EXPECT_EQ(nullptr, cache.getValue()); | 133 | + EXPECT_THROW(cache.getValue(), HueException); |
| 122 | Mock::VerifyAndClearExpectations(handler.get()); | 134 | Mock::VerifyAndClearExpectations(handler.get()); |
| 123 | } | 135 | } |
| 124 | } | 136 | } |
| 125 | \ No newline at end of file | 137 | \ No newline at end of file |
test/test_Hue.cpp
| @@ -379,15 +379,13 @@ TEST(Hue, lightExists) | @@ -379,15 +379,13 @@ TEST(Hue, lightExists) | ||
| 379 | *handler, GETJson("/api/" + getBridgeUsername(), nlohmann::json::object(), getBridgeIp(), getBridgePort())) | 379 | *handler, GETJson("/api/" + getBridgeUsername(), nlohmann::json::object(), getBridgeIp(), getBridgePort())) |
| 380 | .Times(AtLeast(1)) | 380 | .Times(AtLeast(1)) |
| 381 | .WillRepeatedly(Return(hue_bridge_state)); | 381 | .WillRepeatedly(Return(hue_bridge_state)); |
| 382 | - EXPECT_CALL(*handler, | ||
| 383 | - GETJson("/api/" + getBridgeUsername() + "/lights/1", nlohmann::json::object(), getBridgeIp(), getBridgePort())) | ||
| 384 | - .Times(AtLeast(1)) | ||
| 385 | - .WillRepeatedly(Return(hue_bridge_state["lights"]["1"])); | ||
| 386 | 382 | ||
| 387 | Hue test_bridge(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler); | 383 | Hue test_bridge(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler); |
| 388 | 384 | ||
| 389 | - EXPECT_EQ(true, Const(test_bridge).lightExists(1)); | ||
| 390 | - EXPECT_EQ(false, Const(test_bridge).lightExists(2)); | 385 | + test_bridge.refresh(); |
| 386 | + | ||
| 387 | + EXPECT_TRUE(Const(test_bridge).lightExists(1)); | ||
| 388 | + EXPECT_FALSE(Const(test_bridge).lightExists(2)); | ||
| 391 | } | 389 | } |
| 392 | 390 | ||
| 393 | TEST(Hue, getGroup) | 391 | TEST(Hue, getGroup) |
| @@ -492,13 +490,11 @@ TEST(Hue, groupExists) | @@ -492,13 +490,11 @@ TEST(Hue, groupExists) | ||
| 492 | *handler, GETJson("/api/" + getBridgeUsername(), nlohmann::json::object(), getBridgeIp(), getBridgePort())) | 490 | *handler, GETJson("/api/" + getBridgeUsername(), nlohmann::json::object(), getBridgeIp(), getBridgePort())) |
| 493 | .Times(AtLeast(1)) | 491 | .Times(AtLeast(1)) |
| 494 | .WillRepeatedly(Return(hue_bridge_state)); | 492 | .WillRepeatedly(Return(hue_bridge_state)); |
| 495 | - EXPECT_CALL(*handler, | ||
| 496 | - GETJson("/api/" + getBridgeUsername() + "/groups/1", nlohmann::json::object(), getBridgeIp(), getBridgePort())) | ||
| 497 | - .Times(AtLeast(1)) | ||
| 498 | - .WillRepeatedly(Return(hue_bridge_state["groups"]["1"])); | ||
| 499 | 493 | ||
| 500 | Hue test_bridge(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler); | 494 | Hue test_bridge(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler); |
| 501 | 495 | ||
| 496 | + test_bridge.refresh(); | ||
| 497 | + | ||
| 502 | EXPECT_EQ(true, Const(test_bridge).groupExists(1)); | 498 | EXPECT_EQ(true, Const(test_bridge).groupExists(1)); |
| 503 | EXPECT_EQ(false, Const(test_bridge).groupExists(2)); | 499 | EXPECT_EQ(false, Const(test_bridge).groupExists(2)); |
| 504 | } | 500 | } |