Commit 972c34a0f5455fa8ad4bdea2c8d6f99043911108

Authored by Jojo-1000
Committed by Moritz Wirger
1 parent dfbcb0ae

Add ResourceList to simplify handling of HueLight, Group and Schedule.

include/hueplusplus/Hue.h
... ... @@ -42,6 +42,8 @@
42 42  
43 43 #include "json/json.hpp"
44 44  
  45 +#include "ResourceList.h"
  46 +
45 47 //! \brief Namespace for the hueplusplus library
46 48 namespace hueplusplus
47 49 {
... ... @@ -140,12 +142,12 @@ public:
140 142 //! \brief Function to get the ip address of the hue bridge
141 143 //!
142 144 //! \return string containing ip
143   - std::string getBridgeIP();
  145 + std::string getBridgeIP() const;
144 146  
145 147 //! \brief Function to get the port of the hue bridge
146 148 //!
147 149 //! \return integer containing port
148   - int getBridgePort();
  150 + int getBridgePort() const;
149 151  
150 152 //! \brief Send a username request to the Hue bridge.
151 153 //!
... ... @@ -161,7 +163,7 @@ public:
161 163 //! \brief Function that returns the username
162 164 //!
163 165 //! \return The username used for API access
164   - std::string getUsername();
  166 + std::string getUsername() const;
165 167  
166 168 //! \brief Function to set the ip address of this class representing a bridge
167 169 //!
... ... @@ -337,7 +339,7 @@ public:
337 339 //! because Philips provides different file types. \param id Id of a light to
338 340 //! get the picture of \return String that either contains the filename of the
339 341 //! picture of the light or if it was not found an empty string
340   - std::string getPictureOfLight(int id) const;
  342 + std::string getPictureOfLight(int id);
341 343  
342 344 //! \brief Const function that returns the picture name of a given model id
343 345 //!
... ... @@ -354,15 +356,16 @@ private:
354 356 //!< like "192.168.2.1"
355 357 std::string username; //!< Username that is ussed to access the hue bridge
356 358 int port;
357   - std::map<int, HueLight> lights; //!< Maps ids to HueLights that are controlled by this bridge
358   - std::map<int, Group> groups; //!< Maps ids to Groups
359   - std::map<int, Schedule> schedules; //!< Maps ids to Schedules
360 359  
361 360 std::shared_ptr<const IHttpHandler> http_handler; //!< A IHttpHandler that is used to communicate with the
362 361 //!< bridge
363 362 HueCommandAPI commands; //!< A HueCommandAPI that is used to communicate with the bridge
364   - APICache stateCache; //!< The state of the hue bridge as it is returned from it
365   - HueLightFactory lightFactory;
  363 + std::chrono::steady_clock::duration refreshDuration;
  364 +
  365 + ResourceList<HueLight, int> lights;
  366 + CreateableResourceList<Group, int, CreateGroup> groups;
  367 + CreateableResourceList<Schedule, int, CreateSchedule> schedules;
  368 +
366 369 };
367 370 } // namespace hueplusplus
368 371  
... ...
include/hueplusplus/ResourceList.h 0 → 100644
  1 +/**
  2 + \file ResourceList.h
  3 + Copyright Notice\n
  4 + Copyright (C) 2020 Jan Rogall - developer\n
  5 +
  6 + This file is part of hueplusplus.
  7 +
  8 + hueplusplus is free software: you can redistribute it and/or modify
  9 + it under the terms of the GNU Lesser General Public License as published by
  10 + the Free Software Foundation, either version 3 of the License, or
  11 + (at your option) any later version.
  12 +
  13 + hueplusplus is distributed in the hope that it will be useful,
  14 + but WITHOUT ANY WARRANTY; without even the implied warranty of
  15 + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16 + GNU Lesser General Public License for more details.
  17 +
  18 + You should have received a copy of the GNU Lesser General Public License
  19 + along with hueplusplus. If not, see <http://www.gnu.org/licenses/>.
  20 +**/
  21 +
  22 +#ifndef INCLUDE_RESOURCE_LIST_H
  23 +#define INCLUDE_RESOURCE_LIST_H
  24 +
  25 +#include <functional>
  26 +#include <map>
  27 +#include <string>
  28 +#include <vector>
  29 +
  30 +#include "APICache.h"
  31 +#include "HueException.h"
  32 +
  33 +namespace hueplusplus
  34 +{
  35 +template <typename Resource, typename IdType>
  36 +class ResourceList
  37 +{
  38 +public:
  39 + ResourceList(const HueCommandAPI& commands, const std::string& path,
  40 + std::chrono::steady_clock::duration refreshDuration,
  41 + const std::function<Resource(int, const nlohmann::json&)>& factory = nullptr)
  42 + : commands(commands), stateCache(path, commands, refreshDuration), path(path + '/'), factory(factory)
  43 + {}
  44 +
  45 + ResourceList(const ResourceList&) = delete;
  46 + ResourceList(ResourceList&&) = default;
  47 + ResourceList& operator=(const ResourceList&) = delete;
  48 + ResourceList& operator=(ResourceList&&) = default;
  49 +
  50 + void refresh() { stateCache.refresh(); }
  51 +
  52 + std::vector<std::reference_wrapper<Resource>> getAll()
  53 + {
  54 + nlohmann::json state = stateCache.getValue();
  55 + for (auto it = state.begin(); it != state.end(); ++it)
  56 + {
  57 + get(maybeStoi(it.key()));
  58 + }
  59 + std::vector<std::reference_wrapper<Resource>> result;
  60 + result.reserve(state.size());
  61 + for (auto& entry : resources)
  62 + {
  63 + result.emplace_back(entry.second);
  64 + }
  65 + return result;
  66 + }
  67 +
  68 + Resource& get(const IdType& id)
  69 + {
  70 + auto pos = resources.find(id);
  71 + if (pos != resources.end())
  72 + {
  73 + pos->second.refresh();
  74 + return pos->second;
  75 + }
  76 + const nlohmann::json& state = stateCache.getValue();
  77 + std::string key = maybeToString(id);
  78 + if (!state.count(key))
  79 + {
  80 + throw HueException(FileInfo {__FILE__, __LINE__, __func__}, "Resource id is not valid");
  81 + }
  82 + return resources.emplace(id, construct(id, state[key])).first->second;
  83 + }
  84 +
  85 + bool exists(const IdType& id) const
  86 + {
  87 + auto pos = resources.find(id);
  88 + if (pos != resources.end())
  89 + {
  90 + return true;
  91 + }
  92 + return stateCache.getValue().count(maybeToString(id)) != 0;
  93 + }
  94 +
  95 + bool remove(const IdType& id)
  96 + {
  97 + std::string requestPath = path + maybeToString(id);
  98 + nlohmann::json result
  99 + = commands.DELETERequest(requestPath, nlohmann::json::object(), FileInfo {__FILE__, __LINE__, __func__});
  100 + bool success = utils::safeGetMember(result, 0, "success") == requestPath;
  101 + auto it = resources.find(id);
  102 + if (success && it != resources.end())
  103 + {
  104 + resources.erase(it);
  105 + }
  106 + return success;
  107 + }
  108 +
  109 +protected:
  110 + static IdType maybeStoi(const std::string& key) { return maybeStoi(key, std::is_integral<IdType> {}); }
  111 +
  112 + static std::string maybeToString(const IdType& id) { return maybeToString(id, std::is_integral<IdType> {}); }
  113 +
  114 + Resource construct(const IdType& id, const nlohmann::json& state)
  115 + {
  116 + return construct(id, state, std::is_constructible<Resource, IdType, HueCommandAPI, nlohmann::json> {});
  117 + }
  118 +
  119 +private:
  120 + // Resource is constructable
  121 + Resource construct(const IdType& id, const nlohmann::json& state, std::true_type)
  122 + {
  123 + if (factory)
  124 + {
  125 + return factory(id, state);
  126 + }
  127 + else
  128 + {
  129 + return Resource(id, commands, stateCache.getRefreshDuration());
  130 + }
  131 + }
  132 + // Resource is not constructable
  133 + Resource construct(const IdType& id, const nlohmann::json& state, std::false_type)
  134 + {
  135 + if (!factory)
  136 + {
  137 + throw HueException(FileInfo {__FILE__, __LINE__, __func__},
  138 + "Resource is not constructable with default parameters, but no factory given");
  139 + }
  140 + return factory(id, state);
  141 + }
  142 +
  143 +private:
  144 + static IdType maybeStoi(const std::string& key, std::true_type) { return std::stoi(key); }
  145 + static IdType maybeStoi(const std::string& key, std::false_type) { return key; }
  146 + static std::string maybeToString(IdType id, std::true_type) { return std::to_string(id); }
  147 + static std::string maybeToString(const IdType& id, std::false_type) { return id; }
  148 +
  149 +protected:
  150 + std::function<Resource(int, const nlohmann::json&)> factory;
  151 +
  152 + HueCommandAPI commands;
  153 + APICache stateCache;
  154 + std::string path;
  155 +
  156 + std::map<IdType, Resource> resources;
  157 +};
  158 +
  159 +template <typename Resource, typename IdType, typename CreateType>
  160 +class CreateableResourceList : public ResourceList<Resource, IdType>
  161 +{
  162 +public:
  163 + using ResourceList::ResourceList;
  164 +
  165 + IdType create(const CreateType& params)
  166 + {
  167 + std::string requestPath = path;
  168 + // Remove leading slash
  169 + requestPath.pop_back();
  170 + nlohmann::json response
  171 + = commands.POSTRequest(requestPath, params.getRequest(), FileInfo {__FILE__, __LINE__, __func__});
  172 + nlohmann::json id = utils::safeGetMember(response, 0, "success", "id");
  173 + if (id.is_string())
  174 + {
  175 + std::string idStr = id.get<std::string>();
  176 + if (idStr.find(path) == 0)
  177 + {
  178 + idStr.erase(0, path.size());
  179 + }
  180 + stateCache.refresh();
  181 + return maybeStoi(idStr);
  182 + }
  183 + return IdType {};
  184 + }
  185 +};
  186 +} // namespace hueplusplus
  187 +
  188 +#endif
... ...
src/Hue.cpp
... ... @@ -132,20 +132,24 @@ std::string HueFinder::ParseDescription(const std::string&amp; description)
132 132 Hue::Hue(const std::string& ip, const int port, const std::string& username,
133 133 std::shared_ptr<const IHttpHandler> handler, std::chrono::steady_clock::duration refreshDuration)
134 134 : ip(ip),
135   - port(port),
136 135 username(username),
  136 + port(port),
137 137 http_handler(std::move(handler)),
138 138 commands(ip, port, username, http_handler),
139   - stateCache("", commands, refreshDuration),
140   - lightFactory(commands, refreshDuration)
  139 + refreshDuration(refreshDuration),
  140 + lights(commands, "/lights", refreshDuration,
  141 + [factory = HueLightFactory(commands, refreshDuration)](
  142 + int id, const nlohmann::json& state) mutable { return factory.createLight(state, id); }),
  143 + groups(commands, "/groups", refreshDuration),
  144 + schedules(commands, "/schedules", refreshDuration)
141 145 {}
142 146  
143   -std::string Hue::getBridgeIP()
  147 +std::string Hue::getBridgeIP() const
144 148 {
145 149 return ip;
146 150 }
147 151  
148   -int Hue::getBridgePort()
  152 +int Hue::getBridgePort() const
149 153 {
150 154 return port;
151 155 }
... ... @@ -200,7 +204,7 @@ std::string Hue::requestUsername()
200 204 return username;
201 205 }
202 206  
203   -std::string Hue::getUsername()
  207 +std::string Hue::getUsername() const
204 208 {
205 209 return username;
206 210 }
... ... @@ -217,242 +221,75 @@ void Hue::setPort(const int port)
217 221  
218 222 HueLight& Hue::getLight(int id)
219 223 {
220   - auto pos = lights.find(id);
221   - if (pos != lights.end())
222   - {
223   - pos->second.refresh();
224   - return pos->second;
225   - }
226   - const nlohmann::json& lightsCache = stateCache.getValue()["lights"];
227   - if (!lightsCache.count(std::to_string(id)))
228   - {
229   - std::cerr << "Error in Hue getLight(): light with id " << id << " is not valid\n";
230   - throw HueException(CURRENT_FILE_INFO, "Light id is not valid");
231   - }
232   - auto light = lightFactory.createLight(lightsCache[std::to_string(id)], id);
233   - lights.emplace(id, light);
234   - return lights.find(id)->second;
  224 + return lights.get(id);
235 225 }
236 226  
237 227 bool Hue::removeLight(int id)
238 228 {
239   - nlohmann::json result
240   - = commands.DELETERequest("/lights/" + std::to_string(id), nlohmann::json::object(), CURRENT_FILE_INFO);
241   - bool success = utils::safeGetMember(result, 0, "success") == "/lights/" + std::to_string(id) + " deleted";
242   - if (success && lights.count(id) != 0)
243   - {
244   - lights.erase(id);
245   - }
246   - return success;
  229 + return lights.remove(id);
247 230 }
248 231  
249 232 std::vector<std::reference_wrapper<HueLight>> Hue::getAllLights()
250 233 {
251   - // No reference because getLight may invalidate it
252   - nlohmann::json lightsState = stateCache.getValue()["lights"];
253   - for (auto it = lightsState.begin(); it != lightsState.end(); ++it)
254   - {
255   - getLight(std::stoi(it.key()));
256   - }
257   - std::vector<std::reference_wrapper<HueLight>> result;
258   - for (auto& entry : lights)
259   - {
260   - result.emplace_back(entry.second);
261   - }
262   - return result;
  234 + return lights.getAll();
263 235 }
264 236  
265 237 std::vector<std::reference_wrapper<Group>> Hue::getAllGroups()
266 238 {
267   - nlohmann::json groupsState = stateCache.getValue().at("groups");
268   - for (auto it = groupsState.begin(); it != groupsState.end(); ++it)
269   - {
270   - getGroup(std::stoi(it.key()));
271   - }
272   - std::vector<std::reference_wrapper<Group>> result;
273   - result.reserve(result.size());
274   - for (auto& entry : groups)
275   - {
276   - result.emplace_back(entry.second);
277   - }
278   - return result;
  239 + return groups.getAll();
279 240 }
280 241  
281 242 Group& Hue::getGroup(int id)
282 243 {
283   - auto pos = groups.find(id);
284   - if (pos != groups.end())
285   - {
286   - pos->second.refresh();
287   - return pos->second;
288   - }
289   - const nlohmann::json& groupsCache = stateCache.getValue()["groups"];
290   - if (!groupsCache.count(std::to_string(id)))
291   - {
292   - std::cerr << "Error in Hue getGroup(): group with id " << id << " is not valid\n";
293   - throw HueException(CURRENT_FILE_INFO, "Group id is not valid");
294   - }
295   - return groups.emplace(id, Group(id, commands, stateCache.getRefreshDuration())).first->second;
  244 + return groups.get(id);
296 245 }
297 246  
298 247 bool Hue::removeGroup(int id)
299 248 {
300   - nlohmann::json result
301   - = commands.DELETERequest("/groups/" + std::to_string(id), nlohmann::json::object(), CURRENT_FILE_INFO);
302   - bool success = utils::safeGetMember(result, 0, "success") == "/groups/" + std::to_string(id) + " deleted";
303   - if (success && groups.count(id) != 0)
304   - {
305   - groups.erase(id);
306   - }
307   - return success;
308   -}
309   -
310   -bool Hue::groupExists(int id)
311   -{
312   - auto pos = lights.find(id);
313   - if (pos != lights.end())
314   - {
315   - return true;
316   - }
317   - if (stateCache.getValue()["groups"].count(std::to_string(id)))
318   - {
319   - return true;
320   - }
321   - return false;
  249 + return groups.remove(id);
322 250 }
323 251  
324 252 bool Hue::groupExists(int id) const
325 253 {
326   - auto pos = lights.find(id);
327   - if (pos != lights.end())
328   - {
329   - return true;
330   - }
331   - if (stateCache.getValue()["groups"].count(std::to_string(id)))
332   - {
333   - return true;
334   - }
335   - return false;
  254 + return groups.exists(id);
336 255 }
337 256  
338 257 int Hue::createGroup(const CreateGroup& params)
339 258 {
340   - nlohmann::json response = commands.POSTRequest("/groups", params.getRequest(), CURRENT_FILE_INFO);
341   - nlohmann::json id = utils::safeGetMember(response, 0, "success", "id");
342   - if (id.is_string())
343   - {
344   - std::string idStr = id.get<std::string>();
345   - // Sometimes the response can be /groups/<id>?
346   - if (idStr.find("/groups/") == 0)
347   - {
348   - idStr.erase(0, 8);
349   - }
350   - stateCache.refresh();
351   - return std::stoi(idStr);
352   - }
353   - return 0;
354   -}
355   -
356   -bool Hue::lightExists(int id)
357   -{
358   - auto pos = lights.find(id);
359   - if (pos != lights.end())
360   - {
361   - return true;
362   - }
363   - if (stateCache.getValue()["lights"].count(std::to_string(id)))
364   - {
365   - return true;
366   - }
367   - return false;
  259 + return groups.create(params);
368 260 }
369 261  
370 262 bool Hue::lightExists(int id) const
371 263 {
372   - auto pos = lights.find(id);
373   - if (pos != lights.end())
374   - {
375   - return true;
376   - }
377   - if (stateCache.getValue()["lights"].count(std::to_string(id)))
378   - {
379   - return true;
380   - }
381   - return false;
  264 + return lights.exists(id);
382 265 }
383 266  
384 267 std::vector<std::reference_wrapper<Schedule>> Hue::getAllSchedules()
385 268 {
386   - nlohmann::json schedulesState = stateCache.getValue().at("schedules");
387   - for (auto it = schedulesState.begin(); it != schedulesState.end(); ++it)
388   - {
389   - getSchedule(std::stoi(it.key()));
390   - }
391   - std::vector<std::reference_wrapper<Schedule>> result;
392   - result.reserve(result.size());
393   - for (auto& entry : schedules)
394   - {
395   - result.emplace_back(entry.second);
396   - }
397   - return result;
  269 + return schedules.getAll();
398 270 }
399 271  
400 272 Schedule& Hue::getSchedule(int id)
401 273 {
402   - auto pos = schedules.find(id);
403   - if (pos != schedules.end())
404   - {
405   - pos->second.refresh();
406   - return pos->second;
407   - }
408   - const nlohmann::json& schedulesCache = stateCache.getValue()["schedules"];
409   - if (!schedulesCache.count(std::to_string(id)))
410   - {
411   - std::cerr << "Error in Hue getSchedule(): schedule with id " << id << " is not valid\n";
412   - throw HueException(CURRENT_FILE_INFO, "Schedule id is not valid");
413   - }
414   - return schedules.emplace(id, Schedule(id, commands, stateCache.getRefreshDuration())).first->second;
  274 + return schedules.get(id);
415 275 }
416 276  
417 277 bool Hue::scheduleExists(int id) const
418 278 {
419   - auto pos = schedules.find(id);
420   - if (pos != schedules.end())
421   - {
422   - return true;
423   - }
424   - if (stateCache.getValue()["schedules"].count(std::to_string(id)))
425   - {
426   - return true;
427   - }
428   - return false;
  279 + return schedules.exists(id);
429 280 }
430 281  
431 282 int Hue::createSchedule(const CreateSchedule& params)
432 283 {
433   - nlohmann::json response = commands.POSTRequest("/schedules", params.getRequest(), CURRENT_FILE_INFO);
434   - nlohmann::json id = utils::safeGetMember(response, 0, "success", "id");
435   - if (id.is_string())
436   - {
437   - std::string idStr = id.get<std::string>();
438   - // Sometimes the response can be /groups/<id>?
439   - if (idStr.find("/schedules/") == 0)
440   - {
441   - idStr.erase(0, 11);
442   - }
443   - stateCache.refresh();
444   - return std::stoi(idStr);
445   - }
446   - return 0;
  284 + return schedules.create(params);
447 285 }
448 286  
449   -std::string Hue::getPictureOfLight(int id) const
  287 +std::string Hue::getPictureOfLight(int id)
450 288 {
451 289 std::string ret = "";
452   - auto pos = lights.find(id);
453   - if (pos != lights.end())
  290 + if (lights.exists(id))
454 291 {
455   - ret = getPictureOfModel(pos->second.getModelId());
  292 + ret = getPictureOfModel(lights.get(id).getModelId());
456 293 }
457 294 return ret;
458 295 }
... ... @@ -585,7 +422,10 @@ void Hue::setHttpHandler(std::shared_ptr&lt;const IHttpHandler&gt; handler)
585 422 {
586 423 http_handler = handler;
587 424 commands = HueCommandAPI(ip, port, username, handler);
588   - stateCache = APICache("", commands, stateCache.getRefreshDuration());
589   - lightFactory = HueLightFactory(commands, stateCache.getRefreshDuration());
  425 + lights = ResourceList<HueLight, int>(commands, "/lights", refreshDuration,
  426 + [factory = HueLightFactory(commands, refreshDuration)](
  427 + 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);
590 430 }
591 431 } // namespace hueplusplus
... ...
test/test_Hue.cpp
... ... @@ -116,15 +116,6 @@ TEST_F(HueFinderTest, GetBridge)
116 116 EXPECT_EQ(test_bridge.getBridgePort(), getBridgePort()) << "Bridge Port not matching";
117 117 EXPECT_EQ(test_bridge.getUsername(), getBridgeUsername()) << "Bridge username not matching";
118 118  
119   - // Verify that username is correctly set in api requests
120   - nlohmann::json hue_bridge_state {{"lights", {}}};
121   - EXPECT_CALL(
122   - *handler, GETJson("/api/" + getBridgeUsername(), nlohmann::json::object(), getBridgeIp(), getBridgePort()))
123   - .Times(1)
124   - .WillOnce(Return(hue_bridge_state));
125   -
126   - test_bridge.getAllLights();
127   -
128 119 Mock::VerifyAndClearExpectations(handler.get());
129 120 }
130 121  
... ... @@ -395,17 +386,8 @@ TEST(Hue, lightExists)
395 386  
396 387 Hue test_bridge(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler);
397 388  
398   - EXPECT_EQ(true, test_bridge.lightExists(1));
399   - EXPECT_EQ(false, test_bridge.lightExists(2));
400   -
401   - const Hue const_test_bridge1 = test_bridge;
402   - EXPECT_EQ(true, const_test_bridge1.lightExists(1));
403   - EXPECT_EQ(false, const_test_bridge1.lightExists(2));
404   -
405   - test_bridge.getLight(1);
406   - const Hue const_test_bridge2 = test_bridge;
407   - EXPECT_EQ(true, test_bridge.lightExists(1));
408   - EXPECT_EQ(true, const_test_bridge2.lightExists(1));
  389 + EXPECT_EQ(true, Const(test_bridge).lightExists(1));
  390 + EXPECT_EQ(false, Const(test_bridge).lightExists(2));
409 391 }
410 392  
411 393 TEST(Hue, getGroup)
... ... @@ -517,17 +499,8 @@ TEST(Hue, groupExists)
517 499  
518 500 Hue test_bridge(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler);
519 501  
520   - EXPECT_EQ(true, test_bridge.groupExists(1));
521   - EXPECT_EQ(false, test_bridge.groupExists(2));
522   -
523   - const Hue const_test_bridge1 = test_bridge;
524   - EXPECT_EQ(true, const_test_bridge1.groupExists(1));
525   - EXPECT_EQ(false, const_test_bridge1.groupExists(2));
526   -
527   - test_bridge.getGroup(1);
528   - const Hue const_test_bridge2 = test_bridge;
529   - EXPECT_EQ(true, test_bridge.groupExists(1));
530   - EXPECT_EQ(true, const_test_bridge2.groupExists(1));
  502 + EXPECT_EQ(true, Const(test_bridge).groupExists(1));
  503 + EXPECT_EQ(false, Const(test_bridge).groupExists(2));
531 504 }
532 505  
533 506 TEST(Hue, getAllGroups)
... ...