From 65ddf85814bc7269767b6371c11d5aeb84937009 Mon Sep 17 00:00:00 2001 From: Jojo-1000 <33495614+Jojo-1000@users.noreply.github.com> Date: Fri, 21 Aug 2020 20:02:07 +0200 Subject: [PATCH] Implement searching for new sensors and lights. --- include/hueplusplus/Bridge.h | 6 +++--- include/hueplusplus/NewDeviceList.h | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ include/hueplusplus/ResourceList.h | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------ include/hueplusplus/SensorList.h | 2 +- src/Bridge.cpp | 2 +- src/CMakeLists.txt | 2 +- src/NewDeviceList.cpp | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/TimePattern.cpp | 27 +++++++++++++++++---------- test/CMakeLists.txt | 2 +- test/test_NewDeviceList.cpp | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/test_ResourceList.cpp | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------- 11 files changed, 346 insertions(+), 46 deletions(-) create mode 100644 include/hueplusplus/NewDeviceList.h create mode 100644 src/NewDeviceList.cpp create mode 100644 test/test_NewDeviceList.cpp diff --git a/include/hueplusplus/Bridge.h b/include/hueplusplus/Bridge.h index 993749d..bc6d794 100644 --- a/include/hueplusplus/Bridge.h +++ b/include/hueplusplus/Bridge.h @@ -129,10 +129,10 @@ class Bridge friend class BridgeFinder; public: - using LightList = ResourceList; + using LightList = SearchableResourceList; using GroupList = GroupResourceList; - using ScheduleList = CreateableResourceList; - using SceneList = CreateableResourceList; + using ScheduleList = CreateableResourceList, CreateSchedule>; + using SceneList = CreateableResourceList, CreateScene>; public: //! \brief Constructor of Bridge class diff --git a/include/hueplusplus/NewDeviceList.h b/include/hueplusplus/NewDeviceList.h new file mode 100644 index 0000000..715485e --- /dev/null +++ b/include/hueplusplus/NewDeviceList.h @@ -0,0 +1,69 @@ +/** + \file NewDeviceList.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_HUEPLUSPLUS_NEW_DEVICE_LIST_H +#define INCLUDE_HUEPLUSPLUS_NEW_DEVICE_LIST_H + +#include +#include + +#include "TimePattern.h" + +#include "json/json.hpp" + +namespace hueplusplus +{ +//! \brief List of new devices found during the last scan +class NewDeviceList +{ +public: + //! \brief Construct from data + NewDeviceList(const std::string& lastScan, const std::map& devices); + + //! \brief Get a map of id and name of new devices + const std::map& getNewDevices() const; + + //! \brief Get whether a last scan time is available + //! + //! This can be false if there was no scan since the last restart + //! or if the scan is still running. + bool hasLastScanTime() const; + //! \brief Get whether scan is currently active + //! + //! When scan is active, no last scan time is available + bool isScanActive(); + //! \brief Get time when last scan was completed + //! \throws HueException when no time is available or timestamp is invalid + //! \note Must only be called when \ref hasLastScanTime() is true. + time::AbsoluteTime getLastScanTime() const; + + //! \brief Parse from json response + //! \throws std::invalid_argument when json is invalid. + //! \throws nlohmann::json::exception when json is invalid. + static NewDeviceList parse(const nlohmann::json& json); + +private: + std::string lastScan; + std::map devices; +}; +} // namespace hueplusplus + +#endif diff --git a/include/hueplusplus/ResourceList.h b/include/hueplusplus/ResourceList.h index f1df811..7378270 100644 --- a/include/hueplusplus/ResourceList.h +++ b/include/hueplusplus/ResourceList.h @@ -29,21 +29,24 @@ #include "APICache.h" #include "HueException.h" +#include "NewDeviceList.h" #include "Utils.h" namespace hueplusplus { //! \brief Handles a list of a certain API resource //! \tparam Resource Resource type that is in the list -//! \tparam IdType Type of the resource id. int or std::string +//! \tparam IdT Type of the resource id. int or std::string //! //! The resources are assumed to be in an object with ids as keys. //! The Resource class needs a constructor that accepts \c id, HueCommandAPI and \c refreshDuration; //! otherwise a factory function needs to be provided that takes \c id and the JSON state. -template +template class ResourceList { public: + using ResourceType = Resource; + using IdType = IdT; static_assert(std::is_integral::value || std::is_same::value, "IdType must be integral or string"); @@ -236,16 +239,59 @@ protected: std::map resources; }; -//! \brief Handles a ResourceList where Resources can be added by the user +//! \brief Handles a ResourceList of physical devices which can be searched for //! \tparam Resource Resource type that is in the list -//! \tparam IdType Type of the resource id. int or std::string +template +class SearchableResourceList : public ResourceList +{ +public: + using ResourceList::ResourceList; + + //! \brief Start search for new devices + //! \param deviceIds Serial numbers of the devices to search for (max. 10) + //! + //! Takes more than 40s. If many devices were found a second search command might be necessary. + void search(const std::vector& deviceIds = {}) + { + std::string requestPath = this->path; + // Remove trailing slash + requestPath.pop_back(); + if (deviceIds.empty()) + { + this->stateCache.getCommandAPI().POSTRequest( + requestPath, nlohmann::json::object(), FileInfo {__FILE__, __LINE__, __func__}); + } + else + { + this->stateCache.getCommandAPI().POSTRequest( + requestPath, nlohmann::json {{"deviceid", deviceIds}}, FileInfo {__FILE__, __LINE__, __func__}); + } + } + + //! \brief Get devices found in last search + NewDeviceList getNewDevices() const + { + nlohmann::json response = this->stateCache.getCommandAPI().GETRequest( + this->path + "new", nlohmann::json::object(), FileInfo {__FILE__, __LINE__, __func__}); + return NewDeviceList::parse(response); + } + +protected: + //! \brief Protected defaulted move constructor + SearchableResourceList(SearchableResourceList&&) = default; + //! \brief Protected defaulted move assignment + SearchableResourceList& operator=(SearchableResourceList&&) = default; +}; + +//! \brief Handles a ResourceList where Resources can be added by the user +//! \tparam BaseResourceList Base resource list type (ResourceList or SearchableResourceList). //! \tparam CreateType Type that provides parameters for creation. //! Must have a const getRequest() function returning the JSON for the POST request. -template -class CreateableResourceList : public ResourceList +template +class CreateableResourceList : public BaseResourceList { public: - using ResourceList::ResourceList; + using BaseResourceList::BaseResourceList; //! \brief Create a new resource //! \param params Parameters for the new resource @@ -255,7 +301,7 @@ public: //! \throws HueAPIResponseException when response contains an error //! \throws nlohmann::json::parse_error when response could not be parsed //! \throws std::invalid_argument when IdType is int and std::stoi fails - IdType create(const CreateType& params) + typename BaseResourceList::IdType create(const CreateType& params) { std::string requestPath = this->path; // Remove slash @@ -273,8 +319,9 @@ public: this->stateCache.refresh(); return this->maybeStoi(idStr); } - return IdType {}; + return BaseResourceList::IdType {}; } + protected: //! \brief Protected defaulted move constructor CreateableResourceList(CreateableResourceList&&) = default; @@ -287,10 +334,12 @@ protected: //! \tparam CreateType Type that provides parameters for creation. //! Must have a const getRequest() function returning the JSON for the POST request. template -class GroupResourceList : public CreateableResourceList +class GroupResourceList : public CreateableResourceList, CreateType> { + using Base = CreateableResourceList, CreateType>; + public: - using CreateableResourceList::CreateableResourceList; + using Base::Base; //! \brief Get group, specially handles group 0 //! \see ResourceList::get Resource& get(const int& id) @@ -311,7 +360,7 @@ public: } //! \brief Get group, specially handles group 0 //! \see ResourceList::exists - bool exists(int id) const { return id == 0 || CreateableResourceList::exists(id); } + bool exists(int id) const { return id == 0 || Base::exists(id); } protected: //! \brief Protected defaulted move constructor diff --git a/include/hueplusplus/SensorList.h b/include/hueplusplus/SensorList.h index 0a1a7f3..0d7aeb9 100644 --- a/include/hueplusplus/SensorList.h +++ b/include/hueplusplus/SensorList.h @@ -30,7 +30,7 @@ namespace hueplusplus //! \brief Handles a list of Sensor%s with type specific getters //! //! Allows to directly get the requested sensor type or all sensors of a given type. -class SensorList : public CreateableResourceList +class SensorList : public CreateableResourceList, CreateSensor> { public: using CreateableResourceList::CreateableResourceList; diff --git a/src/Bridge.cpp b/src/Bridge.cpp index 9ed505b..2db5b6b 100644 --- a/src/Bridge.cpp +++ b/src/Bridge.cpp @@ -292,7 +292,7 @@ void Bridge::setHttpHandler(std::shared_ptr handler) { http_handler = handler; stateCache = std::make_shared("", HueCommandAPI(ip, port, username, handler), refreshDuration); - lightList = ResourceList(stateCache, "lights", refreshDuration, + lightList = LightList(stateCache, "lights", refreshDuration, [factory = LightFactory(stateCache->getCommandAPI(), refreshDuration)]( int id, const nlohmann::json& state) mutable { return factory.createLight(state, id); }); groupList = GroupList(stateCache, "groups", refreshDuration); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e627326..b32b4de 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,7 +22,7 @@ set(hueplusplus_SOURCES StateTransaction.cpp TimePattern.cpp UPnP.cpp - Utils.cpp "ZLLSensors.cpp" "CLIPSensors.cpp") + Utils.cpp "ZLLSensors.cpp" "CLIPSensors.cpp" "NewDeviceList.cpp") # on windows we want to compile the WinHttpHandler if(WIN32) diff --git a/src/NewDeviceList.cpp b/src/NewDeviceList.cpp new file mode 100644 index 0000000..bb681d2 --- /dev/null +++ b/src/NewDeviceList.cpp @@ -0,0 +1,63 @@ +/** + \file NewDeviceList.cpp + 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 . +**/ + +#include + +namespace hueplusplus +{ +NewDeviceList::NewDeviceList(const std::string& lastScan, const std::map& devices) + : lastScan(lastScan), devices(devices) +{ } +const std::map& NewDeviceList::getNewDevices() const +{ + return devices; +} +bool NewDeviceList::hasLastScanTime() const +{ + return !lastScan.empty() && lastScan != "none" && lastScan != "active"; +} +bool NewDeviceList::isScanActive() +{ + return lastScan == "active"; +} +time::AbsoluteTime NewDeviceList::getLastScanTime() const +{ + return time::AbsoluteTime::parseUTC(lastScan); // UTC? not clear in docs +} +NewDeviceList NewDeviceList::parse(const nlohmann::json& json) +{ + std::map devices; + std::string lastScan; + for (auto it = json.begin(); it != json.end(); ++it) + { + if (it.key() == "lastscan") + { + lastScan = it.value().get(); + } + else + { + int id = std::stoi(it.key()); + devices.emplace(id, it.value().at("name").get()); + } + } + return NewDeviceList(lastScan, devices); +} +} // namespace hueplusplus \ No newline at end of file diff --git a/src/TimePattern.cpp b/src/TimePattern.cpp index 4c54e3d..be9ef87 100644 --- a/src/TimePattern.cpp +++ b/src/TimePattern.cpp @@ -33,16 +33,23 @@ namespace { std::tm timestampToTm(const std::string& timestamp) { - std::tm tm {}; - tm.tm_year = std::stoi(timestamp.substr(0, 4)) - 1900; - tm.tm_mon = std::stoi(timestamp.substr(5, 2)) - 1; - tm.tm_mday = std::stoi(timestamp.substr(8, 2)); - tm.tm_hour = std::stoi(timestamp.substr(11, 2)); - tm.tm_min = std::stoi(timestamp.substr(14, 2)); - tm.tm_sec = std::stoi(timestamp.substr(17, 2)); - // Auto detect daylight savings time - tm.tm_isdst = -1; - return tm; + try + { + std::tm tm {}; + tm.tm_year = std::stoi(timestamp.substr(0, 4)) - 1900; + tm.tm_mon = std::stoi(timestamp.substr(5, 2)) - 1; + tm.tm_mday = std::stoi(timestamp.substr(8, 2)); + tm.tm_hour = std::stoi(timestamp.substr(11, 2)); + tm.tm_min = std::stoi(timestamp.substr(14, 2)); + tm.tm_sec = std::stoi(timestamp.substr(17, 2)); + // Auto detect daylight savings time + tm.tm_isdst = -1; + return tm; + } + catch (const std::invalid_argument& e) + { + throw HueException(CURRENT_FILE_INFO, std::string("Invalid argument: ") + e.what()); + } } } // namespace diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ff95aa4..52d7cd8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -54,7 +54,7 @@ set(TEST_SOURCES test_SimpleColorHueStrategy.cpp test_SimpleColorTemperatureStrategy.cpp test_StateTransaction.cpp - test_TimePattern.cpp) + test_TimePattern.cpp "test_NewDeviceList.cpp") set(HuePlusPlus_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/include") diff --git a/test/test_NewDeviceList.cpp b/test/test_NewDeviceList.cpp new file mode 100644 index 0000000..2b8d74a --- /dev/null +++ b/test/test_NewDeviceList.cpp @@ -0,0 +1,75 @@ +/** + \file test_NewDeviceList.cpp + 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 . +**/ + +#include +#include + +#include + +using namespace hueplusplus; +using namespace testing; + +TEST(NewDeviceList, Constructor) +{ + { + NewDeviceList list("none", {}); + EXPECT_TRUE(list.getNewDevices().empty()); + EXPECT_FALSE(list.hasLastScanTime()); + EXPECT_FALSE(list.isScanActive()); + EXPECT_THROW(list.getLastScanTime(), HueException); + } + { + const std::map devices = {{1, "a"}, {2, "b"}, {3, "c"}}; + NewDeviceList list("active", devices); + EXPECT_FALSE(list.hasLastScanTime()); + EXPECT_TRUE(list.isScanActive()); + EXPECT_EQ(devices, list.getNewDevices()); + EXPECT_THROW(list.getLastScanTime(), HueException); + } + { + const std::string timestamp = "2020-03-01T00:10:00"; + NewDeviceList list(timestamp, {}); + EXPECT_TRUE(list.hasLastScanTime()); + EXPECT_FALSE(list.isScanActive()); + EXPECT_EQ(time::AbsoluteTime::parseUTC(timestamp).getBaseTime(), list.getLastScanTime().getBaseTime()); + } +} + +TEST(NewDeviceList, parse) +{ + { + NewDeviceList list = NewDeviceList::parse({}); + EXPECT_FALSE(list.hasLastScanTime()); + EXPECT_FALSE(list.isScanActive()); + EXPECT_TRUE(list.getNewDevices().empty()); + EXPECT_THROW(list.getLastScanTime(), HueException); + } + { + const std::map devices = {{1, "a"}, {2, "b"}, {3, "c"}}; + const std::string timestamp = "2020-03-01T00:10:00"; + NewDeviceList list = NewDeviceList::parse( + {{"1", {{"name", "a"}}}, {"2", {{"name", "b"}}}, {"3", {{"name", "c"}}}, {"lastscan", timestamp}}); + EXPECT_TRUE(list.hasLastScanTime()); + EXPECT_FALSE(list.isScanActive()); + EXPECT_EQ(time::AbsoluteTime::parseUTC(timestamp).getBaseTime(), list.getLastScanTime().getBaseTime()); + EXPECT_EQ(devices, list.getNewDevices()); + } +} \ No newline at end of file diff --git a/test/test_ResourceList.cpp b/test/test_ResourceList.cpp index a3d529c..2748ba8 100644 --- a/test/test_ResourceList.cpp +++ b/test/test_ResourceList.cpp @@ -32,9 +32,9 @@ using namespace testing; class TestResource { public: - TestResource(int id, HueCommandAPI api, std::chrono::steady_clock::duration refreshDuration) : id(id) {} + TestResource(int id, HueCommandAPI api, std::chrono::steady_clock::duration refreshDuration) : id(id) { } - void refresh(bool force = false) {} + void refresh(bool force = false) { } public: int id; @@ -42,15 +42,15 @@ public: class TestResourceFactory { public: - void refresh(bool force = false) {} + void refresh(bool force = false) { } }; class TestStringResource { public: TestStringResource(const std::string& id, HueCommandAPI api, std::chrono::steady_clock::duration refreshDuration) : id(id) - {} - void refresh(bool force = false) {} + { } + void refresh(bool force = false) { } public: std::string id; @@ -149,12 +149,12 @@ TEST(ResourceList, get) TestStringResource& r2 = list.get(id); EXPECT_EQ(id, r2.id); } - + { ResourceList list(commands, path, std::chrono::steady_clock::duration::max()); const int id = 2; - const nlohmann::json response = { {std::to_string(id), {{"resource", "state"}}} }; + const nlohmann::json response = {{std::to_string(id), {{"resource", "state"}}}}; EXPECT_CALL(*handler, GETJson("/api/" + getBridgeUsername() + path, nlohmann::json::object(), getBridgeIp(), getBridgePort())) .WillOnce(Return(response)); @@ -162,10 +162,10 @@ TEST(ResourceList, get) } { ResourceList list(commands, path, std::chrono::steady_clock::duration::max(), - [](int, const nlohmann::json&) {return TestResourceFactory(); }); + [](int, const nlohmann::json&) { return TestResourceFactory(); }); const int id = 2; - const nlohmann::json response = { {std::to_string(id), {{"resource", "state"}}} }; + const nlohmann::json response = {{std::to_string(id), {{"resource", "state"}}}}; EXPECT_CALL(*handler, GETJson("/api/" + getBridgeUsername() + path, nlohmann::json::object(), getBridgeIp(), getBridgePort())) .WillOnce(Return(response)); @@ -238,22 +238,59 @@ TEST(ResourceList, remove) EXPECT_FALSE(list.remove(id)); } +TEST(SearchableResourceList, search) +{ + auto handler = std::make_shared(); + HueCommandAPI commands(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler); + + const std::string path = "/resources"; + SearchableResourceList list(commands, path, std::chrono::steady_clock::duration::max()); + const nlohmann::json response = {{{"success", {{path, "Searching for new devices"}}}}}; + EXPECT_CALL(*handler, + POSTJson("/api/" + getBridgeUsername() + path, nlohmann::json::object(), getBridgeIp(), getBridgePort())) + .WillOnce(Return(response)); + list.search(); + + EXPECT_CALL(*handler, + POSTJson("/api/" + getBridgeUsername() + path, nlohmann::json({{"deviceid", {"abcd", "def", "fgh"}}}), + getBridgeIp(), getBridgePort())) + .WillOnce(Return(response)); + list.search({"abcd", "def", "fgh"}); +} + +TEST(SearchableResourceList, getNewDevices) +{ + auto handler = std::make_shared(); + HueCommandAPI commands(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler); + + const std::string path = "/resources"; + SearchableResourceList list(commands, path, std::chrono::steady_clock::duration::max()); + const nlohmann::json response = {{"lastscan", "active"}, {"1", {{"name", "A"}}}}; + EXPECT_CALL(*handler, + GETJson( + "/api/" + getBridgeUsername() + path + "/new", nlohmann::json::object(), getBridgeIp(), getBridgePort())) + .WillOnce(Return(response)); + NewDeviceList newDevices = list.getNewDevices(); + EXPECT_TRUE(newDevices.isScanActive()); + EXPECT_THAT(newDevices.getNewDevices(), ElementsAre(std::make_pair(1, "A"))); +} + TEST(CreateableResourceList, create) { auto handler = std::make_shared(); HueCommandAPI commands(getBridgeIp(), getBridgePort(), getBridgeUsername(), handler); const std::string path = "/resources"; - const nlohmann::json response = { {{"success", {{"id", path + "/2"}}}} }; - const nlohmann::json request = { {"name", "bla"} }; - CreateableResourceList list(commands, path, std::chrono::steady_clock::duration::max()); - EXPECT_CALL(*handler, POSTJson( - "/api/" + getBridgeUsername() + path, request, getBridgeIp(), getBridgePort())) + const nlohmann::json response = {{{"success", {{"id", path + "/2"}}}}}; + const nlohmann::json request = {{"name", "bla"}}; + CreateableResourceList, TestCreateType> list( + commands, path, std::chrono::steady_clock::duration::max()); + EXPECT_CALL(*handler, POSTJson("/api/" + getBridgeUsername() + path, request, getBridgeIp(), getBridgePort())) .WillOnce(Return(response)) .WillOnce(Return(nlohmann::json())); - EXPECT_CALL(*handler, GETJson( - "/api/" + getBridgeUsername() + path, _, getBridgeIp(), getBridgePort())) - .Times(AnyNumber()).WillRepeatedly(Return(nlohmann::json::object())); + EXPECT_CALL(*handler, GETJson("/api/" + getBridgeUsername() + path, _, getBridgeIp(), getBridgePort())) + .Times(AnyNumber()) + .WillRepeatedly(Return(nlohmann::json::object())); TestCreateType params; EXPECT_CALL(params, getRequest()).Times(2).WillRepeatedly(Return(request)); EXPECT_EQ(2, list.create(params)); -- libgit2 0.21.4