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 | 34 | class APICache |
| 35 | 35 | { |
| 36 | 36 | public: |
| 37 | + APICache(std::shared_ptr<APICache> baseCache, const std::string& subEntry); | |
| 37 | 38 | //! \brief Constructs APICache |
| 38 | 39 | //! \param path URL appended after username, may be empty. |
| 39 | 40 | //! \param commands HueCommandAPI for making API requests. |
| ... | ... | @@ -54,12 +55,14 @@ public: |
| 54 | 55 | //! \throws nlohmann::json::parse_error when response could not be parsed |
| 55 | 56 | nlohmann::json& getValue(); |
| 56 | 57 | //! \brief Get cached value, does not refresh. |
| 58 | + //! \throws HueException when no previous request was cached | |
| 57 | 59 | const nlohmann::json& getValue() const; |
| 58 | 60 | |
| 59 | 61 | //! \brief Get duration between refreshes. |
| 60 | 62 | std::chrono::steady_clock::duration getRefreshDuration() const; |
| 61 | 63 | |
| 62 | 64 | private: |
| 65 | + std::shared_ptr<APICache> base; | |
| 63 | 66 | std::string path; |
| 64 | 67 | HueCommandAPI commands; |
| 65 | 68 | std::chrono::steady_clock::duration refreshDuration; | ... | ... |
include/hueplusplus/Hue.h
| ... | ... | @@ -136,6 +136,8 @@ public: |
| 136 | 136 | Hue(const std::string& ip, const int port, const std::string& username, std::shared_ptr<const IHttpHandler> handler, |
| 137 | 137 | std::chrono::steady_clock::duration refreshDuration = std::chrono::seconds(10)); |
| 138 | 138 | |
| 139 | + void refresh(); | |
| 140 | + | |
| 139 | 141 | //! \name Configuration |
| 140 | 142 | ///@{ |
| 141 | 143 | |
| ... | ... | @@ -362,6 +364,7 @@ private: |
| 362 | 364 | HueCommandAPI commands; //!< A HueCommandAPI that is used to communicate with the bridge |
| 363 | 365 | std::chrono::steady_clock::duration refreshDuration; |
| 364 | 366 | |
| 367 | + std::shared_ptr<APICache> stateCache; | |
| 365 | 368 | ResourceList<HueLight, int> lights; |
| 366 | 369 | CreateableResourceList<Group, int, CreateGroup> groups; |
| 367 | 370 | CreateableResourceList<Schedule, int, CreateSchedule> schedules; | ... | ... |
include/hueplusplus/ResourceList.h
| ... | ... | @@ -36,6 +36,10 @@ template <typename Resource, typename IdType> |
| 36 | 36 | class ResourceList |
| 37 | 37 | { |
| 38 | 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 | 43 | ResourceList(const HueCommandAPI& commands, const std::string& path, |
| 40 | 44 | std::chrono::steady_clock::duration refreshDuration, |
| 41 | 45 | const std::function<Resource(int, const nlohmann::json&)>& factory = nullptr) |
| ... | ... | @@ -97,7 +101,7 @@ public: |
| 97 | 101 | std::string requestPath = path + maybeToString(id); |
| 98 | 102 | nlohmann::json result |
| 99 | 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 | 105 | auto it = resources.find(id); |
| 102 | 106 | if (success && it != resources.end()) |
| 103 | 107 | { |
| ... | ... | @@ -113,7 +117,7 @@ protected: |
| 113 | 117 | |
| 114 | 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 | 123 | private: | ... | ... |
src/APICache.cpp
| ... | ... | @@ -23,46 +23,95 @@ |
| 23 | 23 | |
| 24 | 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 | 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 | 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 | 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 | 137 | http_handler(std::move(handler)), |
| 138 | 138 | commands(ip, port, username, http_handler), |
| 139 | 139 | refreshDuration(refreshDuration), |
| 140 | - lights(commands, "/lights", refreshDuration, | |
| 140 | + stateCache(std::make_shared<APICache>("", commands, refreshDuration)), | |
| 141 | + lights(commands, "/lights", stateCache, "lights", | |
| 141 | 142 | [factory = HueLightFactory(commands, refreshDuration)]( |
| 142 | 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 | 153 | std::string Hue::getBridgeIP() const |
| 148 | 154 | { |
| 149 | 155 | return ip; |
| ... | ... | @@ -422,10 +428,11 @@ void Hue::setHttpHandler(std::shared_ptr<const IHttpHandler> handler) |
| 422 | 428 | { |
| 423 | 429 | http_handler = handler; |
| 424 | 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 | 433 | [factory = HueLightFactory(commands, refreshDuration)]( |
| 427 | 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 | 438 | } // namespace hueplusplus | ... | ... |
test/test_APICache.cpp
| ... | ... | @@ -110,7 +110,19 @@ TEST(APICache, getValue) |
| 110 | 110 | EXPECT_EQ(value, cache.getValue()); |
| 111 | 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 | 127 | std::string path = "/test/abc"; |
| 116 | 128 | const APICache cache(path, commands, std::chrono::steady_clock::duration::max()); |
| ... | ... | @@ -118,7 +130,7 @@ TEST(APICache, getValue) |
| 118 | 130 | EXPECT_CALL(*handler, |
| 119 | 131 | GETJson("/api/" + getBridgeUsername() + path, nlohmann::json::object(), getBridgeIp(), getBridgePort())) |
| 120 | 132 | .Times(0); |
| 121 | - EXPECT_EQ(nullptr, cache.getValue()); | |
| 133 | + EXPECT_THROW(cache.getValue(), HueException); | |
| 122 | 134 | Mock::VerifyAndClearExpectations(handler.get()); |
| 123 | 135 | } |
| 124 | 136 | } |
| 125 | 137 | \ No newline at end of file | ... | ... |
test/test_Hue.cpp
| ... | ... | @@ -379,15 +379,13 @@ TEST(Hue, lightExists) |
| 379 | 379 | *handler, GETJson("/api/" + getBridgeUsername(), nlohmann::json::object(), getBridgeIp(), getBridgePort())) |
| 380 | 380 | .Times(AtLeast(1)) |
| 381 | 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 | 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 | 391 | TEST(Hue, getGroup) |
| ... | ... | @@ -492,13 +490,11 @@ TEST(Hue, groupExists) |
| 492 | 490 | *handler, GETJson("/api/" + getBridgeUsername(), nlohmann::json::object(), getBridgeIp(), getBridgePort())) |
| 493 | 491 | .Times(AtLeast(1)) |
| 494 | 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 | 494 | Hue test_bridge(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler); |
| 501 | 495 | |
| 496 | + test_bridge.refresh(); | |
| 497 | + | |
| 502 | 498 | EXPECT_EQ(true, Const(test_bridge).groupExists(1)); |
| 503 | 499 | EXPECT_EQ(false, Const(test_bridge).groupExists(2)); |
| 504 | 500 | } | ... | ... |