diff --git a/include/hueplusplus/TimePattern.h b/include/hueplusplus/TimePattern.h new file mode 100644 index 0000000..d1183a5 --- /dev/null +++ b/include/hueplusplus/TimePattern.h @@ -0,0 +1,198 @@ +/** + \file TimePattern.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_TIME_PATTERN +#define INCLUDE_HUEPLUSPLUS_TIME_PATTERN + +#include +#include + +namespace hueplusplus +{ +namespace time +{ +std::string timepointToTimestamp(std::chrono::system_clock::time_point time); +std::chrono::system_clock::time_point parseTimestamp(const std::string& timestamp); + +std::string durationTo_hh_mm_ss(std::chrono::system_clock::duration duration); +std::chrono::system_clock::duration parseDuration(const std::string& hourMinSec); + +class AbsoluteTime +{ + using clock = std::chrono::system_clock; + +public: + explicit AbsoluteTime(clock::time_point baseTime, clock::duration variation = std::chrono::seconds(0)); + + clock::time_point getBaseTime(); + clock::duration getRandomVariation(); + + std::string toString(); + +private: + clock::time_point base; + clock::duration variation; +}; +class Weekdays +{ +public: + Weekdays() : bitmask(0) {} + explicit Weekdays(int num) : bitmask(1 << num) {} + + bool isNone() const; + bool isAll() const; + bool isMonday() const; + bool isTuesday() const; + bool isWednesday() const; + bool isThursday() const; + bool isFriday() const; + bool isSaturday() const; + bool isSunday() const; + + Weekdays unionWith(Weekdays other) const; + Weekdays operator|(Weekdays other) const { return unionWith(other); } + + std::string toString() const; + + static Weekdays none(); + static Weekdays all(); + static Weekdays monday(); + static Weekdays tuesday(); + static Weekdays wednesday(); + static Weekdays thursday(); + static Weekdays friday(); + static Weekdays saturday(); + static Weekdays sunday(); + + static Weekdays parse(const std::string& s); +private: + int bitmask; +}; +class RecurringTime +{ + using clock = std::chrono::system_clock; + +public: + explicit RecurringTime(clock::duration daytime, Weekdays days, clock::duration variation = std::chrono::seconds(0)); + + clock::duration getDaytime() const; + clock::duration getRandomVariation() const; + Weekdays getWeekdays() const; + + std::string toString() const; + +private: + clock::duration time; + clock::duration variation; + Weekdays days; +}; +class TimeInterval +{ + using clock = std::chrono::system_clock; + +public: + TimeInterval(clock::duration start, clock::duration end, Weekdays days = Weekdays::all()); + + clock::duration getStartTime() const; + clock::duration getEndTime() const; + Weekdays getWeekdays() const; + + std::string toString() const; + +private: + clock::duration start; + clock::duration end; + Weekdays days; +}; +class Timer +{ + using clock = std::chrono::system_clock; + +public: + Timer(clock::duration duration, clock::duration variation = std::chrono::seconds(0)); + Timer(clock::duration duration, int numExecutions, clock::duration variation = std::chrono::seconds(0)); + + bool isRecurring() const; + int getNumberOfExecutions() const; + clock::duration getExpiryTime() const; + clock::duration getRandomVariation() const; + + std::string toString() const; + +private: + clock::duration expires; + clock::duration variation; + int numExecutions; +}; + +class TimePattern +{ +public: + enum class Type + { + undefined, + absolute, + recurring, + interval, + timer + }; + + TimePattern(); + ~TimePattern(); + explicit TimePattern(const AbsoluteTime& absolute); + explicit TimePattern(const RecurringTime& recurring); + explicit TimePattern(const TimeInterval& interval); + explicit TimePattern(const Timer& timer); + + TimePattern(const TimePattern& other); + + TimePattern& operator=(const TimePattern& other); + + Type getType() const; + + AbsoluteTime asAbsolute() const; + + RecurringTime asRecurring() const; + + TimeInterval asInterval() const; + + Timer asTimer() const; + + static TimePattern parse(const std::string& s); + +private: + void destroy(); + +private: + Type type; + union + { + nullptr_t undefined; + AbsoluteTime absolute; + RecurringTime recurring; + TimeInterval interval; + Timer timer; + }; +}; +} // namespace time +} // namespace hueplusplus + +#endif \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 74238fc..486f6ba 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,9 +12,10 @@ set(hueplusplus_SOURCES SimpleBrightnessStrategy.cpp SimpleColorHueStrategy.cpp SimpleColorTemperatureStrategy.cpp + StateTransaction.cpp + "TimePattern.cpp" UPnP.cpp - Utils.cpp - StateTransaction.cpp) + Utils.cpp) # on windows we want to compile the WinHttpHandler if(WIN32) diff --git a/src/TimePattern.cpp b/src/TimePattern.cpp new file mode 100644 index 0000000..b8c5cba --- /dev/null +++ b/src/TimePattern.cpp @@ -0,0 +1,545 @@ +/** + \file TimePattern.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 + +namespace hueplusplus +{ +namespace time +{ + +using clock = std::chrono::system_clock; +std::string timepointToTimestamp(clock::time_point time) +{ + using namespace std::chrono; + std::time_t ctime = clock::to_time_t(time); + + std::tm localtime = *std::localtime(&ctime); + char buf[32]; + + std::size_t result = std::strftime(buf, sizeof(buf), "%FT%T", &localtime); + if (result == 0) + { + throw HueException(CURRENT_FILE_INFO, "strftime failed"); + } + return std::string(buf); +} + +clock::time_point parseTimestamp(const std::string& timestamp) +{ + std::tm tm; + tm.tm_year = std::stoi(timestamp.substr(0, 4)); + tm.tm_mon = std::stoi(timestamp.substr(5, 2)); + 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; + std::time_t ctime = std::mktime(&tm); + return clock::from_time_t(ctime); +} + +std::string durationTo_hh_mm_ss(clock::duration duration) +{ + using namespace std::chrono; + if (duration > hours(24)) + { + throw HueException(CURRENT_FILE_INFO, "Duration parameter longer than 1 day"); + } + int numH = static_cast(duration_cast(duration).count()); + duration -= hours(numH); + int numM = static_cast(duration_cast(duration).count()); + duration -= minutes(numM); + int numS = static_cast(duration_cast(duration).count()); + + char result[9]; + std::sprintf(result, "%02d:%02d:%02d", numH, numM, numS); + return std::string(result); +} + +clock::duration parseDuration(const std::string& s) +{ + using namespace std::chrono; + return hours(std::stoi(s.substr(0, 2))) + minutes(std::stoi(s.substr(3, 2))) + seconds(std::stoi(s.substr(7, 2))); +} + +AbsoluteTime::AbsoluteTime(clock::time_point baseTime, clock::duration variation) : base(baseTime), variation(variation) +{} + +clock::time_point AbsoluteTime::getBaseTime() +{ + return base; +} +clock::duration AbsoluteTime::getRandomVariation() +{ + return variation; +} +std::string AbsoluteTime::toString() +{ + std::string result = timepointToTimestamp(base); + if (variation.count() != 0) + { + result.push_back('A'); + result.append(durationTo_hh_mm_ss(variation)); + } + return result; +} + +bool Weekdays::isNone() const +{ + return bitmask == 0; +} + +bool Weekdays::isAll() const +{ + // Check all 7 bits are set + return bitmask == (1 << 7) - 1; +} + +bool Weekdays::isMonday() const +{ + return (bitmask & 1) != 0; +} + +bool Weekdays::isTuesday() const +{ + return (bitmask & 2) != 0; +} + +bool Weekdays::isWednesday() const +{ + return (bitmask & 4) != 0; +} + +bool Weekdays::isThursday() const +{ + return (bitmask & 8) != 0; +} + +bool Weekdays::isFriday() const +{ + return (bitmask & 16) != 0; +} + +bool Weekdays::isSaturday() const +{ + return (bitmask & 32) != 0; +} + +bool Weekdays::isSunday() const +{ + return (bitmask & 64) != 0; +} + +std::string Weekdays::toString() const +{ + std::string result = std::to_string(bitmask); + if (result.size() < 3) + { + result.insert(0, 3 - result.size(), '0'); + } + return result; +} + +Weekdays Weekdays::unionWith(Weekdays other) const +{ + other.bitmask |= bitmask; + return other; +} + +Weekdays Weekdays::none() +{ + return Weekdays(); +} + +Weekdays Weekdays::all() +{ + Weekdays result; + result.bitmask = (1 << 7) - 1; + return result; +} + +Weekdays Weekdays::monday() +{ + return Weekdays(0); +} + +Weekdays Weekdays::tuesday() +{ + return Weekdays(1); +} + +Weekdays Weekdays::wednesday() +{ + return Weekdays(2); +} + +Weekdays Weekdays::thursday() +{ + return Weekdays(3); +} + +Weekdays Weekdays::friday() +{ + return Weekdays(4); +} + +Weekdays Weekdays::saturday() +{ + return Weekdays(5); +} + +Weekdays Weekdays::sunday() +{ + return Weekdays(6); +} + +Weekdays Weekdays::parse(const std::string& s) +{ + Weekdays result; + result.bitmask = std::stoi(s); + return result; +} + +RecurringTime::RecurringTime(clock::duration daytime, Weekdays days, clock::duration variation) + : time(daytime), days(days), variation(variation) +{} + +clock::duration RecurringTime::getDaytime() const +{ + return time; +} + +clock::duration RecurringTime::getRandomVariation() const +{ + return variation; +} + +Weekdays RecurringTime::getWeekdays() const +{ + return days; +} + +std::string RecurringTime::toString() const +{ + std::string result = "W"; + result.append(days.toString()); + result.append("/"); + result.append(durationTo_hh_mm_ss(time)); + if (variation.count() != 0) + { + result.push_back('A'); + result.append(durationTo_hh_mm_ss(variation)); + } + return std::string(); +} + +TimeInterval::TimeInterval(clock::duration start, clock::duration end, Weekdays days) + : start(start), end(end), days(days) +{} + +clock::duration TimeInterval::getStartTime() const +{ + return start; +} + +clock::duration TimeInterval::getEndTime() const +{ + return end; +} + +Weekdays TimeInterval::getWeekdays() const +{ + return days; +} + +std::string TimeInterval::toString() const +{ + std::string result; + if (!days.isAll()) + { + result.append("W"); + result.append(days.toString()); + result.append("/"); + } + result.push_back('T'); + result.append(durationTo_hh_mm_ss(start)); + result.append("/T"); + result.append(durationTo_hh_mm_ss(end)); + + return result; +} + +Timer::Timer(clock::duration duration, clock::duration variation) + : expires(duration), numExecutions(1), variation(variation) +{} + +Timer::Timer(clock::duration duration, int numExecutions, clock::duration variation) + : expires(duration), numExecutions(numExecutions), variation(variation) +{} + +bool Timer::isRecurring() const +{ + return numExecutions != 1; +} + +int Timer::getNumberOfExecutions() const +{ + return numExecutions; +} + +clock::duration Timer::getExpiryTime() const +{ + return expires; +} + +clock::duration Timer::getRandomVariation() const +{ + return variation; +} + +std::string Timer::toString() const +{ + std::string result; + if (numExecutions != 1) + { + result.push_back('R'); + if (numExecutions != 0) + { + std::string s = std::to_string(numExecutions); + // Pad to two digits + if (s.size() < 2) + { + result.push_back('0'); + } + result.append(s); + } + result.push_back('/'); + } + result.append("PT"); + result.append(durationTo_hh_mm_ss(expires)); + if (variation.count() != 0) + { + result.push_back('A'); + result.append(durationTo_hh_mm_ss(variation)); + } + return result; +} + +TimePattern::TimePattern() : type(Type::undefined), undefined(nullptr) {} + +TimePattern::~TimePattern() +{ + destroy(); +} + +TimePattern::TimePattern(const AbsoluteTime& absolute) : type(Type::absolute) +{ + new (&this->absolute) AbsoluteTime(absolute); +} + +TimePattern::TimePattern(const RecurringTime& recurring) : type(Type::recurring) +{ + new (&this->recurring) RecurringTime(recurring); +} + +TimePattern::TimePattern(const TimeInterval& interval) : type(Type::interval) +{ + new (&this->interval) TimeInterval(interval); +} + +TimePattern::TimePattern(const Timer& timer) : type(Type::timer) +{ + new (&this->timer) Timer(timer); +} + +TimePattern::TimePattern(const TimePattern& other) : type(Type::undefined), undefined(nullptr) +{ + *this = other; +} + +TimePattern& TimePattern::operator=(const TimePattern& other) +{ + if (this == &other) + { + return *this; + } + destroy(); + try + { + type = other.type; + switch (type) + { + case Type::undefined: + undefined = nullptr; + break; + case Type::absolute: + new (&absolute) AbsoluteTime(other.absolute); + break; + case Type::recurring: + new (&recurring) RecurringTime(other.recurring); + break; + case Type::interval: + new (&interval) TimeInterval(other.interval); + break; + case Type::timer: + new (&timer) Timer(other.timer); + break; + } + } + catch (...) + { + // Catch any throws from constructors to stay in valid state + type = Type::undefined; + undefined = nullptr; + throw; + } + return *this; +} + +TimePattern::Type TimePattern::getType() const +{ + return type; +} + +AbsoluteTime TimePattern::asAbsolute() const +{ + return absolute; +} + +RecurringTime TimePattern::asRecurring() const +{ + return recurring; +} + +TimeInterval TimePattern::asInterval() const +{ + return interval; +} + +Timer TimePattern::asTimer() const +{ + return timer; +} + +TimePattern TimePattern::parse(const std::string& s) +{ + if (s.empty() || s == "none") + { + return TimePattern(); + } + else if (std::isdigit(s.front())) + { + // Absolute time + clock::time_point time = parseTimestamp(s); + clock::duration variation {0}; + if (s.size() > 19 && s[19] == 'A') + { + // Random variation + variation = parseDuration(s.substr(20)); + } + return TimePattern(AbsoluteTime(time, variation)); + } + else if (s.front() == 'R' || s.front() == 'P') + { + // (Recurring) timer + int numRepetitions = 1; + if (s.front() == 'R') + { + if (s.at(1) == '/') + { + // Infinite + numRepetitions = -1; + } + else + { + numRepetitions = std::stoi(s.substr(1, 2)); + } + } + std::size_t start = s.find('T') + 1; + std::size_t randomStart = s.find('A'); + clock::duration expires = parseDuration(s.substr(start, randomStart - start)); + clock::duration variance = std::chrono::seconds(0); + if (randomStart != std::string::npos) + { + variance = parseDuration(s.substr(randomStart)); + } + return TimePattern(Timer(expires, numRepetitions, variance)); + } + else if (s.front() == 'W' && std::count(s.begin(), s.end(), '/') == 1) + { + // Recurring time + Weekdays days = Weekdays::parse(s.substr(1, 3)); + clock::duration time = parseDuration(s.substr(6)); + clock::duration variation {0}; + if (s.size() > 14) + { + variation = parseDuration(s.substr(15)); + } + return TimePattern(RecurringTime(time, days, variation)); + } + else if (s.front() == 'T' || s.front() == 'W') + { + Weekdays days = Weekdays::all(); + if (s.front() == 'W') + { + // Time interval with weekdays + days = Weekdays::parse(s.substr(1, 3)); + } + // Time interval + std::size_t start = s.find('T') + 1; + std::size_t end = s.find('/', start); + clock::duration startTime = parseDuration(s.substr(start, end - start)); + clock::duration endTime = parseDuration(s.substr(end + 2)); + return TimePattern(TimeInterval(startTime, endTime, days)); + } + throw HueException(CURRENT_FILE_INFO, "Unable to parse time string: " + s); +} + +void TimePattern::destroy() +{ + switch (type) + { + case Type::absolute: + absolute.~AbsoluteTime(); + break; + case Type::recurring: + recurring.~RecurringTime(); + break; + case Type::interval: + interval.~TimeInterval(); + break; + case Type::timer: + timer.~Timer(); + break; + default: + break; + } + type = Type::undefined; + undefined = nullptr; +} + +} // namespace time +} // namespace hueplusplus diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c251d86..a498282 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -44,7 +44,8 @@ set(TEST_SOURCES test_SimpleColorHueStrategy.cpp test_SimpleColorTemperatureStrategy.cpp test_UPnP.cpp - test_StateTransaction.cpp) + test_StateTransaction.cpp + test_TimePattern.cpp) set(HuePlusPlus_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/include") diff --git a/test/test_TimePattern.cpp b/test/test_TimePattern.cpp new file mode 100644 index 0000000..5aede3d --- /dev/null +++ b/test/test_TimePattern.cpp @@ -0,0 +1,130 @@ +/** + \file test_TimePattern.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 + +using namespace hueplusplus::time; +using std::chrono::system_clock; +using namespace std::chrono_literals; + +TEST(AbsoluteTime, Constructor) +{ + system_clock::time_point now = system_clock::now(); + { + AbsoluteTime time(now); + EXPECT_EQ(now, time.getBaseTime()); + EXPECT_EQ(0s, time.getRandomVariation()); + } + system_clock::duration variation = 4h + 2min; + { + AbsoluteTime time(now, variation); + EXPECT_EQ(now, time.getBaseTime()); + EXPECT_EQ(variation, time.getRandomVariation()); + } +} + +TEST(AbsoluteTime, toString) +{ + std::tm time {}; + time.tm_year = 2020 - 1900; + time.tm_mon = 2; + time.tm_mday = 3; + time.tm_hour = 20; + time.tm_min = 53; + time.tm_sec = 3; + std::time_t ctime = std::mktime(&time); + const system_clock::time_point timePoint = system_clock::from_time_t(ctime); + + EXPECT_EQ("2020-03-03T20:53:03", AbsoluteTime(timePoint).toString()); + + const system_clock::duration noVariation = 0s; + EXPECT_EQ("2020-03-03T20:53:03", AbsoluteTime(timePoint, noVariation).toString()); + + const system_clock::duration variation = 1h + 2min + 1s; + EXPECT_EQ("2020-03-03T20:53:03A01:02:01", AbsoluteTime(timePoint, variation).toString()); +} + +TEST(Weekdays, Constructor) +{ + EXPECT_TRUE(Weekdays().isNone()); + EXPECT_TRUE(Weekdays(0).isMonday()); + EXPECT_TRUE(Weekdays(6).isSunday()); +} + +TEST(Weekdays, isXXX) +{ + Weekdays none = Weekdays::none(); + EXPECT_TRUE(none.isNone()); + EXPECT_FALSE(none.isAll()); + EXPECT_FALSE(none.isMonday()); + EXPECT_FALSE(none.isTuesday()); + EXPECT_FALSE(none.isWednesday()); + EXPECT_FALSE(none.isThursday()); + EXPECT_FALSE(none.isFriday()); + EXPECT_FALSE(none.isSaturday()); + EXPECT_FALSE(none.isSunday()); + + Weekdays all = Weekdays::all(); + EXPECT_FALSE(all.isNone()); + EXPECT_TRUE(all.isAll()); + EXPECT_TRUE(all.isMonday()); + EXPECT_TRUE(all.isTuesday()); + EXPECT_TRUE(all.isWednesday()); + EXPECT_TRUE(all.isThursday()); + EXPECT_TRUE(all.isFriday()); + EXPECT_TRUE(all.isSaturday()); + EXPECT_TRUE(all.isSunday()); + + // Test that for all days, only their own isXXX function is true + std::vector days {Weekdays::monday(), Weekdays::tuesday(), Weekdays::wednesday(), Weekdays::thursday(), + Weekdays::friday(), Weekdays::saturday(), Weekdays::sunday()}; + using BoolGetter = bool (Weekdays::*)() const; + std::vector getters {&Weekdays::isMonday, &Weekdays::isTuesday, &Weekdays::isWednesday, + &Weekdays::isThursday, &Weekdays::isFriday, &Weekdays::isSaturday, &Weekdays::isSunday}; + for (int i = 0; i < days.size(); ++i) + { + Weekdays day = days[i]; + EXPECT_FALSE(day.isNone()); + EXPECT_FALSE(day.isAll()); + for (int j = 0; j < getters.size(); ++j) + { + EXPECT_EQ(j == i, (day.*getters[j])()) << "on Day " << i << ": getter for day " << j << " has wrong result"; + } + } +} + +TEST(Weekdays, unionWith) +{ + Weekdays day = Weekdays::monday().unionWith(Weekdays::saturday()); + EXPECT_TRUE(day.isMonday()); + EXPECT_TRUE(day.isSaturday()); + + day = Weekdays::monday() | Weekdays::tuesday() | Weekdays::all(); + EXPECT_TRUE(day.isAll()); +} + +TEST(Weekdays, toString) +{ + EXPECT_EQ("001", Weekdays(0).toString()); + EXPECT_EQ("064", Weekdays(6).toString()); + EXPECT_EQ("112", (Weekdays(6) | Weekdays(5) | Weekdays(4)).toString()); +}