diff --git a/examples/ArduinoIoTCloud-Schedule/ArduinoIoTCloud-Schedule.ino b/examples/ArduinoIoTCloud-Schedule/ArduinoIoTCloud-Schedule.ino new file mode 100644 index 000000000..c035079e4 --- /dev/null +++ b/examples/ArduinoIoTCloud-Schedule/ArduinoIoTCloud-Schedule.ino @@ -0,0 +1,230 @@ +/* + This sketch demonstrates how to use the cloud schedule variable type. + + This sketch is compatible with the following boards: + - MKR 1000 + - MKR WIFI 1010 + - MKR GSM 1400 + - MKR NB 1500 + - MKR WAN 1300/1310 + - Nano 33 IoT + - ESP 8266 +*/ + +#include "arduino_secrets.h" +#include "thingProperties.h" + +#if defined(ESP32) +static int const LED_BUILTIN = 2; +#endif + +void setup() { + /* Initialize the serial port and wait up to 5 seconds for a connection */ + Serial.begin(9600); + for(unsigned long const serialBeginTime = millis(); !Serial && (millis() - serialBeginTime > 5000); ) { } + + /* Configure LED pin as an output */ + pinMode(LED_BUILTIN, OUTPUT); + + /* This function takes care of connecting your sketch variables to the ArduinoIoTCloud object */ + initProperties(); + + /* Initialize Arduino IoT Cloud library */ + ArduinoCloud.begin(ArduinoIoTPreferredConnection); + + setDebugMessageLevel(DBG_INFO); + ArduinoCloud.printDebugInfo(); + + /* Setup one shot schedule example */ + setupOneShotSchedule(); + + /* Setup per minute schedule example */ + setupMinuteSchedule(); + + /* Setup hourly schedule example */ + setupHourlySchedule(); + + /* Setup daily schedule example */ + setupDailySchedule(); + + /* Setup weekly schedule example */ + setupWeeklySchedule(); + + /* Setup monthly schedule example */ + setupMonthlySchedule(); + + /* Setup yearly schedule example */ + setupYearlySchedule(); +} + + /* Setup a schedule with an active period of 5 minutes that doesn't repeat + * Starting from 2021 11 01 17:00:00 + * Until 2021 11 02 17:00:00 + */ +void setupOneShotSchedule() { + + ScheduleTimeType startingFrom = TimeService::getTimeFromString("2021 Nov 01 17:00:00"); + ScheduleTimeType until = startingFrom + ( DAYS * 1 ); + ScheduleTimeType activePeriod = MINUTES * 5; + + /* Warning: there is no cross check between until and activePeriod */ + ScheduleConfigurationType scheduleConfiguration = Schedule::createOneShotScheduleConfiguration(); + + oneShot = Schedule(startingFrom, until, activePeriod, scheduleConfiguration); +} + + /* Setup a schedule with an active period of 15 seconds that repeats each minute + * Starting from 2021 11 01 17:00:00 + * Until 2021 11 02 17:00:00 + */ +void setupMinuteSchedule() { + + ScheduleTimeType startingFrom = TimeService::getTimeFromString("2021 Nov 01 17:00:00"); + ScheduleTimeType until = startingFrom + ( DAYS * 1 ); + ScheduleTimeType activePeriod = SECONDS * 15; + unsigned int repetitionPeriod = 1; + + /* Warning: there is no cross check between repetitionPeriod and activePeriod */ + ScheduleConfigurationType scheduleConfiguration = Schedule::createFixedDeltaScheduleConfiguration(ScheduleUnit::Minutes, repetitionPeriod); + + minute = Schedule(startingFrom, until, activePeriod, scheduleConfiguration); +} + +/* Setup a schedule with an active period of 20 minutes that repeats each hour + * Starting from 2021 11 01 17:00:00 + * Until 2021 11 15 13:00:00 + */ +void setupHourlySchedule() { + + ScheduleTimeType startingFrom = TimeService::getTimeFromString("2021 Nov 01 17:00:00"); + ScheduleTimeType until = TimeService::getTimeFromString("2021 Nov 15 13:00:00"); + ScheduleTimeType activePeriod = MINUTES * 20; + unsigned int repetitionPeriod = 1; + + /* Warning: there is no cross check between repetitionPeriod and activePeriod */ + ScheduleConfigurationType scheduleConfiguration = Schedule::createFixedDeltaScheduleConfiguration(ScheduleUnit::Hours, repetitionPeriod); + + hourly = Schedule(startingFrom, until, activePeriod, scheduleConfiguration); +} + +/* Setup a schedule with an active period of 2 hours that repeats each day + * Starting from 2021 11 01 17:00:00 + * Until 2021 11 15 13:00:00 + */ +void setupDailySchedule() { + + ScheduleTimeType startingFrom = TimeService::getTimeFromString("2021 Nov 01 17:00:00"); + ScheduleTimeType until = TimeService::getTimeFromString("2021 Nov 15 13:00:00"); + ScheduleTimeType activePeriod = HOURS * 2; + unsigned int repetitionPeriod = 1; + + /* Warning: there is no cross check between repetitionPeriod and activePeriod */ + ScheduleConfigurationType scheduleConfiguration = Schedule::createFixedDeltaScheduleConfiguration(ScheduleUnit::Days, repetitionPeriod); + + daily = Schedule(startingFrom, until, activePeriod, scheduleConfiguration); +} + +/* Setup a schedule with an active period of 3 minutes with a weekly configuration + * Starting from 2021 11 01 17:00:00 + * Until 2021 11 31 17:00:00 + * Weekly configuration + * Sunday -> Inactive + * Monday -> Active + * Tuesday -> Inactive + * Wednesday -> Active + * Thursday -> Inactive + * Friday -> Active + * Saturday -> Inactive + */ +void setupWeeklySchedule() { + + unsigned int startingFrom = TimeService::getTimeFromString("2021 Nov 01 17:00:00"); + unsigned int until = startingFrom + ( DAYS * 30 ); + unsigned int executionPeriod = MINUTES * 3; + + ScheduleWeeklyMask WeeklyMask = { + ScheduleState::Inactive, /* Sunday */ + ScheduleState::Active, /* Monday */ + ScheduleState::Inactive, /* Tuesday */ + ScheduleState::Active, /* Wednesday */ + ScheduleState::Inactive, /* Thursday */ + ScheduleState::Active, /* Friday */ + ScheduleState::Inactive, /* Saturday */ + }; + + ScheduleConfigurationType scheduleConfiguration = Schedule::createWeeklyScheduleConfiguration(WeeklyMask); + + weekly = Schedule(startingFrom, until, executionPeriod, scheduleConfiguration); +} + +/* Setup a schedule with an active period of 1 day that repeats each third day of the month + * Starting from 2021 11 01 17:00:00 + * Until 2022 11 15 13:00:00 + */ +void setupMonthlySchedule() { + + ScheduleTimeType startingFrom = TimeService::getTimeFromString("2021 Nov 01 17:00:00"); + ScheduleTimeType until = TimeService::getTimeFromString("2021 Nov 15 13:00:00"); + ScheduleTimeType activePeriod = DAYS * 1; + int dayOfMonth = 3; + + ScheduleConfigurationType scheduleConfiguration = Schedule::createMonthlyScheduleConfiguration(dayOfMonth); + + monthly = Schedule(startingFrom, until, activePeriod, scheduleConfiguration); +} + + +/* Setup a schedule with an active period of 2 days that repeats each year on November 6th + * Starting from 2021 11 06 17:00:00 + * Until 2041 11 15 13:00:00 + */ +void setupYearlySchedule() { + + ScheduleTimeType startingFrom = TimeService::getTimeFromString("2021 Nov 06 17:00:00"); + ScheduleTimeType until = TimeService::getTimeFromString("2041 Nov 06 13:00:00"); + ScheduleTimeType activePeriod = DAYS * 2; + int dayOfMonth = 6; + + ScheduleConfigurationType scheduleConfiguration = Schedule::createYearlyScheduleConfiguration(ScheduleMonth::Nov, dayOfMonth); + + yearly = Schedule(startingFrom, until, activePeriod, scheduleConfiguration); +} + +void loop() { + ArduinoCloud.update(); + + /* Print a message when the oneShot schedule is active */ + if(oneShot.isActive()) { + Serial.println("One shot schedule is active"); + } + + /* Print a message when the per minute schedule is active */ + if(minute.isActive()) { + Serial.println("Per minute schedule is active"); + } + + /* Print a message when the hourly schedule is active */ + if(hourly.isActive()) { + Serial.println("Hourly schedule is active"); + } + + /* Print a message when the daily schedule is active */ + if(daily.isActive()) { + Serial.println("Daily schedule is active"); + } + + /* Activate LED when the weekly schedule is active */ + digitalWrite(LED_BUILTIN, weekly.isActive()); + + /* Print a message when the monthly schedule is active */ + if(monthly.isActive()) { + Serial.println("Monthly schedule is active"); + } + + /* Print a message when the yearly schedule is active */ + if(yearly.isActive()) { + Serial.println("Yearly schedule is active"); + } + +} + diff --git a/examples/ArduinoIoTCloud-Schedule/arduino_secrets.h b/examples/ArduinoIoTCloud-Schedule/arduino_secrets.h new file mode 100644 index 000000000..fc0b0661e --- /dev/null +++ b/examples/ArduinoIoTCloud-Schedule/arduino_secrets.h @@ -0,0 +1,34 @@ +#include + +/* MKR1000, MKR WiFi 1010 */ +#if defined(BOARD_HAS_WIFI) + #define SECRET_SSID "YOUR_WIFI_NETWORK_NAME" + #define SECRET_PASS "YOUR_WIFI_PASSWORD" +#endif + +/* ESP8266 */ +#if defined(BOARD_ESP8266) + #define SECRET_DEVICE_KEY "my-device-password" +#endif + +/* MKR GSM 1400 */ +#if defined(BOARD_HAS_GSM) + #define SECRET_PIN "" + #define SECRET_APN "" + #define SECRET_LOGIN "" + #define SECRET_PASS "" +#endif + +/* MKR WAN 1300/1310 */ +#if defined(BOARD_HAS_LORA) + #define SECRET_APP_EUI "" + #define SECRET_APP_KEY "" +#endif + +/* MKR NB 1500 */ +#if defined(BOARD_HAS_NB) + #define SECRET_PIN "" + #define SECRET_APN "" + #define SECRET_LOGIN "" + #define SECRET_PASS "" +#endif diff --git a/examples/ArduinoIoTCloud-Schedule/thingProperties.h b/examples/ArduinoIoTCloud-Schedule/thingProperties.h new file mode 100644 index 000000000..a426978ce --- /dev/null +++ b/examples/ArduinoIoTCloud-Schedule/thingProperties.h @@ -0,0 +1,46 @@ +#include +#include + +#define THING_ID "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +#define BOARD_ID "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +void onSwitchButtonChange(); + +bool switchButton; +CloudSchedule oneShot; +CloudSchedule minute; +CloudSchedule hourly; +CloudSchedule daily; +CloudSchedule weekly; +CloudSchedule monthly; +CloudSchedule yearly; + +void initProperties() { +#if defined(BOARD_ESP8266) + ArduinoCloud.setBoardId(BOARD_ID); + ArduinoCloud.setSecretDeviceKey(SECRET_DEVICE_KEY); +#endif + ArduinoCloud.setThingId(THING_ID); +#if defined(BOARD_HAS_WIFI) || defined(BOARD_HAS_GSM) || defined(BOARD_HAS_NB) + ArduinoCloud.addProperty(switchButton, WRITE, ON_CHANGE); + ArduinoCloud.addProperty(oneShot, READWRITE, ON_CHANGE); + ArduinoCloud.addProperty(minute, READWRITE, ON_CHANGE); + ArduinoCloud.addProperty(hourly, READWRITE, ON_CHANGE); + ArduinoCloud.addProperty(daily, READWRITE, ON_CHANGE); + ArduinoCloud.addProperty(weekly, READWRITE, ON_CHANGE); + ArduinoCloud.addProperty(monthly, READWRITE, ON_CHANGE); + ArduinoCloud.addProperty(yearly, READWRITE, ON_CHANGE); +#elif defined(BOARD_HAS_LORA) + ArduinoCloud.addProperty(switchButton, 1, WRITE, ON_CHANGE; +#endif +} + +#if defined(BOARD_HAS_WIFI) + WiFiConnectionHandler ArduinoIoTPreferredConnection(SECRET_SSID, SECRET_PASS); +#elif defined(BOARD_HAS_GSM) + GSMConnectionHandler ArduinoIoTPreferredConnection(SECRET_PIN, SECRET_APN, SECRET_LOGIN, SECRET_PASS); +#elif defined(BOARD_HAS_LORA) + LoRaConnectionHandler ArduinoIoTPreferredConnection(SECRET_APP_EUI, SECRET_APP_KEY, _lora_band::EU868, NULL, _lora_class::CLASS_A); +#elif defined(BOARD_HAS_NB) + NBConnectionHandler ArduinoIoTPreferredConnection(SECRET_PIN, SECRET_APN, SECRET_LOGIN, SECRET_PASS); +#endif diff --git a/extras/test/CMakeLists.txt b/extras/test/CMakeLists.txt index dd7b3e990..b707b5254 100644 --- a/extras/test/CMakeLists.txt +++ b/extras/test/CMakeLists.txt @@ -12,6 +12,7 @@ include_directories(include) include_directories(../../src) include_directories(../../src/cbor) include_directories(../../src/property) +include_directories(../../src/utility/time) include_directories(external/catch/v2.12.1/include) include_directories(external/fakeit/v2.0.5/include) @@ -32,6 +33,7 @@ set(TEST_SRCS src/test_callback.cpp src/test_CloudColor.cpp src/test_CloudLocation.cpp + src/test_CloudSchedule.cpp src/test_decode.cpp src/test_encode.cpp src/test_publishEvery.cpp diff --git a/extras/test/include/Arduino_ConnectionHandler.h b/extras/test/include/Arduino_ConnectionHandler.h new file mode 100644 index 000000000..babc86f08 --- /dev/null +++ b/extras/test/include/Arduino_ConnectionHandler.h @@ -0,0 +1,14 @@ +/* + Copyright (c) 2019 Arduino. All rights reserved. +*/ + +#ifndef TEST_ARDUINO_CONNECTION_HANDLER_H_ +#define TEST_ARDUINO_CONNECTION_HANDLER_H_ + +/****************************************************************************** + TYPEDEF + ******************************************************************************/ + +typedef void ConnectionHandler; + +#endif /* TEST_ARDUINO_CONNECTION_HANDLER_H_ */ diff --git a/extras/test/src/test_CloudSchedule.cpp b/extras/test/src/test_CloudSchedule.cpp new file mode 100644 index 000000000..53b582aef --- /dev/null +++ b/extras/test/src/test_CloudSchedule.cpp @@ -0,0 +1,509 @@ +/* + Copyright (c) 2021 Arduino. All rights reserved. +*/ + +/************************************************************************************** + INCLUDE + **************************************************************************************/ + +#include + +#include + +unsigned long time_now = 1; + +/************************************************************************************** + * TimeService Fake CTOR/DTOR + **************************************************************************************/ + +TimeService::TimeService() {} + +/************************************************************************************** + * TimeService Fake Methods + **************************************************************************************/ + +unsigned long TimeService::getLocalTime() {return time_now;} + +/************************************************************************************** + TEST CODE + **************************************************************************************/ + +SCENARIO("Tesing cloud type 'Schedule' Ctor", "[Schedule::Schedule]") +{ + WHEN("A Schedule(0,0,0,0) is being instantiated") + { + Schedule schedule(0,0,0,0); + THEN("The member variable 'frm' should be 0") { + REQUIRE(schedule.frm == 0); + } + THEN("The member variable 'to' should be 0") { + REQUIRE(schedule.to == 0); + } + THEN("The member variable 'len' should be 0") { + REQUIRE(schedule.len == 0); + } + THEN("The member variable 'msk' should be 0") { + REQUIRE(schedule.msk == 0); + } + } +} + +/**************************************************************************************/ + +SCENARIO("Setup a schedule that repeats each 20 minutes and test isActive Method", "[Schedule::isActive]") +{ + Schedule schedule( 1633305600, /* Start 4/10/2021 00:00:00 */ + 1633651200, /* End 8/10/2021 00:00:00 */ + 600, /* Duration 00:10:00 */ + 1140850708 /* Minutes */ + /* Repeats 00:20:00 */ + ); + + WHEN("Time is 4/10/2021 00:00:00") + { + time_now = 1633305600; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 4/10/2021 00:10:00") + { + time_now = 1633306200; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 4/10/2021 00:10:01") + { + time_now = 1633306201; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 4/10/2021 00:19:59") + { + time_now = 1633306799; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 4/10/2021 00:20:00") + { + time_now = 1633306800; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 7/10/2021 23:45:00") + { + time_now = 1633650300; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 7/10/2021 23:55:00") + { + time_now = 1633650900; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 8/10/2021 00:00:00") + { + time_now = 1633651200; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 8/10/2021 00:05:00") + { + time_now = 1633651500; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } +} + +/**************************************************************************************/ + +SCENARIO("Setup a weekly schedule and test isActive Method", "[Schedule::isActive]") +{ + Schedule schedule( 1633305600, /* Start 4/10/2021 00:00:00 */ + 1633651200, /* End 8/10/2021 00:00:00 */ + 600, /* Duration 00:10:00 */ + 134217798 /* Weekly */ + /* Daymask 1000110 */ + ); + + WHEN("Time is 4/10/2021 00:05:00") + { + time_now = 1633305900; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 4/10/2021 00:25:00") + { + time_now = 1633307100; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 5/10/2021 00:05:00") + { + time_now = 1633392300; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 5/10/2021 00:25:00") + { + time_now = 1633393500; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 6/10/2021 00:05:00") + { + time_now = 1633478700; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 7/10/2021 00:05:00") + { + time_now = 1633565100; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } +} + +/**************************************************************************************/ + +SCENARIO("Setup a monthly schedule and test isActive Method", "[Schedule::isActive]") +{ + Schedule schedule( 1633305600, /* Start 4/10/2021 00:00:00 */ + 1664841600, /* End 4/10/2022 00:00:00 */ + 600, /* Duration 00:10:00 */ + 201326598 /* Monthly */ + /* Day of month 6 */ + ); + + WHEN("Time is 6/09/2021 00:05:00") + { + time_now = 1630886700; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 6/10/2021 00:05:00") + { + time_now = 1633478700; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/10/2021 00:25:00") + { + time_now = 1633479900; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 6/11/2021 00:05:00") + { + time_now = 1636157100; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/11/2021 00:25:00") + { + time_now = 1636158300; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 7/11/2021 00:05:00") + { + time_now = 1636243500; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 6/12/2021 00:05:00") + { + time_now = 1638749100; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/01/2022 00:05:00") + { + time_now = 1641427500; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/02/2022 00:05:00") + { + time_now = 1644105900; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/03/2022 00:05:00") + { + time_now = 1646525100; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/04/2022 00:05:00") + { + time_now = 1649203500; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/05/2022 00:05:00") + { + time_now = 1651795500; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/06/2022 00:05:00") + { + time_now = 1654473900; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/07/2022 00:05:00") + { + time_now = 1657065900; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/08/2022 00:05:00") + { + time_now = 1659744300; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/09/2022 00:05:00") + { + time_now = 1662422700; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/10/2022 00:05:00") + { + time_now = 1665014700; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } +} + +/**************************************************************************************/ + +SCENARIO("Setup a yearly schedule and test isActive Method", "[Schedule::isActive]") +{ + Schedule schedule( 1633305600, /* Start 4/10/2021 00:00:00 */ + 1759536000, /* End 4/10/2025 00:00:00 */ + 600, /* Duration 00:10:00 */ + 268438022 /* Yearly */ + /* Month 11, Day of month 6 */ + ); + + WHEN("Time is 6/11/2020 00:05:00") + { + time_now = 1604621100; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 6/11/2021 00:05:00") + { + time_now = 1636157100; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/11/2021 00:25:00") + { + time_now = 1636158300; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 6/11/2022 00:05:00") + { + time_now = 1667693100; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/11/2022 00:25:00") + { + time_now = 1667694300; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 7/11/2021 00:05:00") + { + time_now = 1636243500; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 6/11/2023 00:05:00") + { + time_now = 1699229100; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/11/2024 00:05:00") + { + time_now = 1730851500; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/11/2025 00:05:00") + { + time_now = 1762387500; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } +} + +/**************************************************************************************/ + +SCENARIO("Setup a one shot schedule and test isActive Method", "[Schedule::isActive]") +{ + Schedule schedule( 1636156800, /* Start 6/11/2021 00:00:00 */ + 1636243199, /* End 6/11/2021 23:59:59 */ + 600, /* Duration 00:10:00 */ + 0 /* One shot */ + ); + + WHEN("Time is 6/11/2020 00:05:00") + { + time_now = 1604621100; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 6/11/2021 00:05:00") + { + time_now = 1636157100; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 6/11/2021 00:25:00") + { + time_now = 1636158300; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 6/11/2022 00:05:00") + { + time_now = 1667693100; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 7/11/2021 00:05:00") + { + time_now = 1636243500; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 7/11/2021 00:15:00") + { + time_now = 1636244100; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 6/11/2021 00:11:00") + { + time_now = 1636157460; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } + + WHEN("Time is 6/11/2021 00:01:00") + { + time_now = 1636156860; + THEN("Schedule must be active") { + REQUIRE(schedule.isActive() == true); + } + } + + WHEN("Time is 7/11/2021 00:11:00") + { + time_now = 1636243860; + THEN("Schedule must be inactive") { + REQUIRE(schedule.isActive() == false); + } + } +} + diff --git a/extras/test/src/test_decode.cpp b/extras/test/src/test_decode.cpp index c95d4afe9..fab8c18d4 100644 --- a/extras/test/src/test_decode.cpp +++ b/extras/test/src/test_decode.cpp @@ -143,7 +143,7 @@ SCENARIO("Arduino Cloud Properties are decoded", "[ArduinoCloudThing::decode]") CloudLocation location_test = CloudLocation(0, 1); addPropertyToContainer(property_container, location_test, "test", Permission::ReadWrite); - /* [{0: "test:lat", 3: 2},{0: "test:lon", 3: 3}] = 82 A2 00 68 74 65 73 74 3A 6C 61 74 02 02 A2 00 68 74 65 73 74 3A 6C 6F 6E 02 03*/ + /* [{0: "test:lat", 2: 2},{0: "test:lon", 2: 3}] = 82 A2 00 68 74 65 73 74 3A 6C 61 74 02 02 A2 00 68 74 65 73 74 3A 6C 6F 6E 02 03*/ uint8_t const payload[] = { 0x82, 0xA2, 0x00, 0x68, 0x74, 0x65, 0x73, 0x74, 0x3A, 0x6C, 0x61, 0x74, 0x02, 0x02, 0xA2, 0x00, 0x68, 0x74, 0x65, 0x73, 0x74, 0x3A, 0x6C, 0x6F, 0x6E, 0x02, 0x03 }; CBORDecoder::decode(property_container, payload, sizeof(payload) / sizeof(uint8_t)); Location location_compare = Location(2, 3); @@ -400,6 +400,32 @@ SCENARIO("Arduino Cloud Properties are decoded", "[ArduinoCloudThing::decode]") /************************************************************************************/ + WHEN("A Schedule property is changed via CBOR message") + { + PropertyContainer property_container; + + CloudSchedule schedule_test = CloudSchedule(0, 0, 0, 0); + addPropertyToContainer(property_container, schedule_test, "test", Permission::ReadWrite); + + /* [{0: "test:frm", 2: 1633305600}, {0: "test:to", 2: 1633651200}, {0: "test:len", 2: 600}, {0: "test:msk", 2: 1140850708}] + = 84 A2 00 68 74 65 73 74 3A 66 72 6D 02 1A 61 5A 44 00 A2 00 67 74 65 73 74 3A 74 6F 02 1A 61 5F 8A 00 A2 00 68 74 65 73 74 3A 6C 65 6E 02 19 02 58 A2 00 68 74 65 73 74 3A 6D 73 6B 02 1A 44 00 00 14 + */ + uint8_t const payload[] = {0x84, 0xA2, 0x00, 0x68, 0x74, 0x65, 0x73, 0x74, 0x3A, 0x66, 0x72, 0x6D, 0x02, 0x1A, 0x61, 0x5A, 0x44, 0x00, 0xA2, 0x00, 0x67, 0x74, 0x65, 0x73, 0x74, 0x3A, 0x74, 0x6F, 0x02, 0x1A, 0x61, 0x5F, 0x8A, 0x00, 0xA2, 0x00, 0x68, 0x74, 0x65, 0x73, 0x74, 0x3A, 0x6C, 0x65, 0x6E, 0x02, 0x19, 0x02, 0x58, 0xA2, 0x00, 0x68, 0x74, 0x65, 0x73, 0x74, 0x3A, 0x6D, 0x73, 0x6B, 0x02, 0x1A, 0x44, 0x00, 0x00, 0x14}; + CBORDecoder::decode(property_container, payload, sizeof(payload) / sizeof(uint8_t)); + + Schedule schedule_compare = Schedule(1633305600, 1633651200, 600, 1140850708); + Schedule value_schedule_test = schedule_test.getValue(); + + bool verify = (value_schedule_test == schedule_compare); + REQUIRE(verify); + REQUIRE(value_schedule_test.frm == schedule_compare.frm); + REQUIRE(value_schedule_test.to == schedule_compare.to); + REQUIRE(value_schedule_test.len == schedule_compare.len); + REQUIRE(value_schedule_test.msk == schedule_compare.msk); + } + + /************************************************************************************/ + WHEN("Multiple properties is changed via CBOR message") { WHEN("Multiple properties of different type are changed via CBOR message") diff --git a/extras/test/src/test_encode.cpp b/extras/test/src/test_encode.cpp index 8a4904fc6..f0667166b 100644 --- a/extras/test/src/test_encode.cpp +++ b/extras/test/src/test_encode.cpp @@ -322,6 +322,24 @@ SCENARIO("Arduino Cloud Properties are encoded", "[ArduinoCloudThing::encode-1]" /************************************************************************************/ + WHEN("A 'Schedule' property is added") + { + PropertyContainer property_container; + cbor::encode(property_container); + + CloudSchedule schedule_test = CloudSchedule(1633305600, 1633651200, 600, 1140850708); + addPropertyToContainer(property_container, schedule_test, "test", Permission::ReadWrite); + + /* [{0: "test:frm", 2: 1633305600}, {0: "test:to", 2: 1633651200}, {0: "test:len", 2: 600}, {0: "test:msk", 2: 1140850708}] + = 9F A2 00 68 74 65 73 74 3A 66 72 6D 02 1A 61 5A 44 00 A2 00 67 74 65 73 74 3A 74 6F 02 1A 61 5F 8A 00 A2 00 68 74 65 73 74 3A 6C 65 6E 02 19 02 58 A2 00 68 74 65 73 74 3A 6D 73 6B 02 1A 44 00 00 14 FF + */ + std::vector const expected = {0x9F, 0xA2, 0x00, 0x68, 0x74, 0x65, 0x73, 0x74, 0x3A, 0x66, 0x72, 0x6D, 0x02, 0x1A, 0x61, 0x5A, 0x44, 0x00, 0xA2, 0x00, 0x67, 0x74, 0x65, 0x73, 0x74, 0x3A, 0x74, 0x6F, 0x02, 0x1A, 0x61, 0x5F, 0x8A, 0x00, 0xA2, 0x00, 0x68, 0x74, 0x65, 0x73, 0x74, 0x3A, 0x6C, 0x65, 0x6E, 0x02, 0x19, 0x02, 0x58, 0xA2, 0x00, 0x68, 0x74, 0x65, 0x73, 0x74, 0x3A, 0x6D, 0x73, 0x6B, 0x02, 0x1A, 0x44, 0x00, 0x00, 0x14, 0xFF }; + std::vector const actual = cbor::encode(property_container); + REQUIRE(actual == expected); + } + + /************************************************************************************/ + WHEN("Multiple properties are added") { PropertyContainer property_container; diff --git a/extras/test/src/util/PropertyTestUtil.cpp b/extras/test/src/util/PropertyTestUtil.cpp index 35717a385..ca71cd765 100644 --- a/extras/test/src/util/PropertyTestUtil.cpp +++ b/extras/test/src/util/PropertyTestUtil.cpp @@ -7,6 +7,8 @@ **************************************************************************************/ #include +#include +#include /************************************************************************************** FUNCTION DEFINITION @@ -16,3 +18,8 @@ unsigned long getTime() { return 0; } + +TimeService & ArduinoIoTCloudTimeService() { + static TimeService _timeService_instance; + return _timeService_instance; +} diff --git a/src/ArduinoIoTCloud.cpp b/src/ArduinoIoTCloud.cpp index 5b5637eb3..b62a91990 100644 --- a/src/ArduinoIoTCloud.cpp +++ b/src/ArduinoIoTCloud.cpp @@ -21,6 +21,22 @@ #include +/****************************************************************************** + CTOR/DTOR + ******************************************************************************/ + +ArduinoIoTCloudClass::ArduinoIoTCloudClass() +: _connection{nullptr} +, _time_service(ArduinoIoTCloudTimeService()) +, _tz_offset{0} +, _tz_dst_until{0} +, _thing_id{""} +, _device_id{""} +, _cloud_event_callback{nullptr} +{ + +} + /****************************************************************************** * PUBLIC MEMBER FUNCTIONS ******************************************************************************/ diff --git a/src/ArduinoIoTCloud.h b/src/ArduinoIoTCloud.h index 7c1f9679c..ae8ac1a86 100644 --- a/src/ArduinoIoTCloud.h +++ b/src/ArduinoIoTCloud.h @@ -80,6 +80,7 @@ class ArduinoIoTCloudClass { public: + ArduinoIoTCloudClass(); virtual ~ArduinoIoTCloudClass() { } @@ -97,7 +98,9 @@ class ArduinoIoTCloudClass inline ConnectionHandler * getConnection() { return _connection; } - inline unsigned long getInternalTime() { return _time_service.getTime(); } + inline unsigned long getInternalTime() { return _time_service.getTime(); } + inline unsigned long getLocalTime() { return _time_service.getLocalTime(); } + inline void updateInternalTimezoneInfo() { _time_service.setTimeZoneData(_tz_offset, _tz_dst_until); } void addCallback(ArduinoIoTCloudEvent const event, OnCloudEventCallback callback); @@ -143,17 +146,19 @@ class ArduinoIoTCloudClass protected: - ConnectionHandler * _connection = nullptr; + ConnectionHandler * _connection; PropertyContainer _property_container; - TimeService _time_service; + TimeService & _time_service; + int _tz_offset; + unsigned int _tz_dst_until; void execCloudEventCallback(ArduinoIoTCloudEvent const event); private: - String _thing_id = ""; - String _device_id = ""; - OnCloudEventCallback _cloud_event_callback[3] = {nullptr}; + String _thing_id; + String _device_id; + OnCloudEventCallback _cloud_event_callback[3]; }; #ifdef HAS_TCP diff --git a/src/ArduinoIoTCloudTCP.cpp b/src/ArduinoIoTCloudTCP.cpp index 4d4f86b9d..11245ea85 100644 --- a/src/ArduinoIoTCloudTCP.cpp +++ b/src/ArduinoIoTCloudTCP.cpp @@ -64,6 +64,11 @@ extern "C" unsigned long getTime() return ArduinoCloud.getInternalTime(); } +extern "C" void updateTimezoneInfo() +{ + ArduinoCloud.updateInternalTimezoneInfo(); +} + /****************************************************************************** CTOR/DTOR ******************************************************************************/ @@ -243,6 +248,9 @@ int ArduinoIoTCloudTCP::begin(bool const enable_watchdog, String brokerAddress, addPropertyReal(_ota_req, "OTA_REQ", Permission::ReadWrite).onSync(CLOUD_WINS); #endif /* OTA_ENABLED */ + addPropertyReal(_tz_offset, "tz_offset", Permission::ReadWrite).onSync(CLOUD_WINS).onUpdate(updateTimezoneInfo); + addPropertyReal(_tz_dst_until, "tz_dst_until", Permission::ReadWrite).onSync(CLOUD_WINS).onUpdate(updateTimezoneInfo); + #if OTA_STORAGE_PORTENTA_QSPI #define BOOTLOADER_ADDR (0x8000000) uint32_t bootloader_data_offset = 0x1F000; @@ -527,7 +535,12 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_Connected() */ sendPropertiesToCloud(); - return State::Connected; + unsigned long const internal_posix_time = _time_service.getTime(); + if(internal_posix_time < _tz_dst_until) { + return State::Connected; + } else { + return State::RequestLastValues; + } } } @@ -555,6 +568,7 @@ void ArduinoIoTCloudTCP::handleMessage(int length) DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s [%d] last values received", __FUNCTION__, millis()); CBORDecoder::decode(_property_container, (uint8_t*)bytes, length, true); sendPropertiesToCloud(); + _time_service.setTimeZoneData(_tz_offset, _tz_dst_until); execCloudEventCallback(ArduinoIoTCloudEvent::SYNC); _last_sync_request_cnt = 0; _last_sync_request_tick = 0; diff --git a/src/property/PropertyContainer.h b/src/property/PropertyContainer.h index fc038f583..8c254875e 100644 --- a/src/property/PropertyContainer.h +++ b/src/property/PropertyContainer.h @@ -40,6 +40,7 @@ #include "types/CloudUnsignedInt.h" #include "types/CloudString.h" #include "types/CloudLocation.h" +#include "types/CloudSchedule.h" #include "types/CloudColor.h" #include "types/CloudWrapperBase.h" diff --git a/src/property/types/CloudSchedule.h b/src/property/types/CloudSchedule.h new file mode 100644 index 000000000..93090c507 --- /dev/null +++ b/src/property/types/CloudSchedule.h @@ -0,0 +1,435 @@ +// +// This file is part of ArduinoCloudThing +// +// Copyright 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of ArduinoCloudThing. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to modify or +// otherwise use the software for commercial activities involving the Arduino +// software without disclosing the source code of your own applications. To purchase +// a commercial license, send an email to license@arduino.cc. +// + +#ifndef CLOUDSCHEDULE_H_ +#define CLOUDSCHEDULE_H_ + +/****************************************************************************** + INCLUDE + ******************************************************************************/ + +#include +#include "../Property.h" +#include "../../AIoTC_Const.h" +#include "utility/time/TimeService.h" +#include + +/****************************************************************************** + * DEFINE + ******************************************************************************/ +#define SCHEDULE_UNIT_MASK 0xC0000000 +#define SCHEDULE_UNIT_SHIFT 30 + +#define SCHEDULE_TYPE_MASK 0x3C000000 +#define SCHEDULE_TYPE_SHIFT 26 + +#define SCHEDULE_MONTH_MASK 0x0000FF00 +#define SCHEDULE_MONTH_SHIFT 8 + +#define SCHEDULE_REP_MASK 0x03FFFFFF +#define SCHEDULE_WEEK_MASK 0x000000FF +#define SCHEDULE_DAY_MASK 0x000000FF + +#define SCHEDULE_ONE_SHOT 0xFFFFFFFF + +/****************************************************************************** + ENUM + ******************************************************************************/ +enum class ScheduleUnit : int { + Seconds = 0, + Minutes = 1, + Hours = 2, + Days = 3 +}; + +enum class ScheduleType : int { + OneShot = 0, + FixedDelta = 1, + Weekly = 2, + Monthly = 3, + Yearly = 4 +}; + +enum class ScheduleMonth : int { + Jan = 0, + Feb = 1, + Mar = 2, + Apr = 3, + May = 4, + Jun = 5, + Jul = 6, + Aug = 7, + Sep = 8, + Oct = 9, + Nov = 10, + Dec = 11 +}; + +enum class ScheduleWeekDay : int { + Sun = 0, + Mon = 1, + Tue = 2, + Wed = 3, + Thu = 4, + Fri = 5, + Sat = 6 +}; + +enum class ScheduleState : int { + Inactive = 0, + Active = 1 +}; + +/****************************************************************************** + * TYPEDEF + ******************************************************************************/ +typedef struct ScheduleWeeklyMask { + ScheduleState& operator[](ScheduleWeekDay i) { return day[static_cast(i)];} + ScheduleState day[7]; +}ScheduleWeeklyMask; + +typedef unsigned int ScheduleTimeType; +typedef unsigned int ScheduleConfigurationType; + +/****************************************************************************** + CLASS DECLARATION + ******************************************************************************/ +class Schedule { + public: + ScheduleTimeType frm, to, len, msk; + Schedule(ScheduleTimeType s, ScheduleTimeType e, ScheduleTimeType d, ScheduleConfigurationType m): frm(s), to(e), len(d), msk(m) {} + + bool isActive() { + + ScheduleTimeType now = _schedule_time_service.getLocalTime(); + + if(checkTimeValid(now)) { + /* We have to wait RTC configuration and Timezone setting from the cloud */ + + if(checkSchedulePeriod(now, frm, to)) { + /* We are in the schedule range */ + + if(checkScheduleMask(now, msk)) { + + /* We can assume now that the schedule is always repeating with fixed delta */ + ScheduleTimeType delta = getScheduleDelta(msk); + if ( ( (std::max(now , frm) - std::min(now , frm)) % delta ) <= len ) { + return true; + } + } + } + } + return false; + } + + static ScheduleConfigurationType createOneShotScheduleConfiguration() { + return 0; + } + + static ScheduleConfigurationType createFixedDeltaScheduleConfiguration(ScheduleUnit unit, unsigned int delta) { + int temp_unit = static_cast(unit); + int temp_type = static_cast(ScheduleType::FixedDelta); + unsigned int temp_delta = delta; + + if (temp_delta > SCHEDULE_REP_MASK) { + temp_delta = SCHEDULE_REP_MASK; + } + return (temp_unit << SCHEDULE_UNIT_SHIFT) | (temp_type << SCHEDULE_TYPE_SHIFT) | temp_delta; + } + + static ScheduleConfigurationType createWeeklyScheduleConfiguration(ScheduleWeeklyMask weekMask) { + unsigned int temp_week = 0; + int temp_type = static_cast(ScheduleType::Weekly); + + for(int i = 0; i<7; i++) { + if(weekMask[static_cast(i)] == ScheduleState::Active) { + temp_week |= 1 << i; + } + } + return (temp_type << SCHEDULE_TYPE_SHIFT) | temp_week; + } + + static ScheduleConfigurationType createMonthlyScheduleConfiguration(int dayOfTheMonth) { + int temp_day = dayOfTheMonth; + int temp_type = static_cast(ScheduleType::Monthly); + + if(temp_day < 1) { + temp_day = 1; + } + + if(temp_day > 31) { + temp_day = 31; + } + return (temp_type << SCHEDULE_TYPE_SHIFT) | temp_day; + } + + static ScheduleConfigurationType createYearlyScheduleConfiguration(ScheduleMonth month, int dayOfTheMonth) { + unsigned int temp_day = createMonthlyScheduleConfiguration(dayOfTheMonth); + int temp_month = static_cast(month); + int temp_type = static_cast(ScheduleType::Yearly); + + return (temp_type << SCHEDULE_TYPE_SHIFT) | (temp_month << SCHEDULE_MONTH_SHIFT)| temp_day; + } + + Schedule& operator=(Schedule & aSchedule) { + frm = aSchedule.frm; + to = aSchedule.to; + len = aSchedule.len; + msk = aSchedule.msk; + return *this; + } + + bool operator==(Schedule & aSchedule) { + return frm == aSchedule.frm && to == aSchedule.to && len == aSchedule.len && msk == aSchedule.msk; + } + + bool operator!=(Schedule & aSchedule) { + return !(operator==(aSchedule)); + } + private: + TimeService & _schedule_time_service = ArduinoIoTCloudTimeService(); + + ScheduleUnit getScheduleUnit(ScheduleConfigurationType msk) { + return static_cast((msk & SCHEDULE_UNIT_MASK) >> SCHEDULE_UNIT_SHIFT); + } + + ScheduleType getScheduleType(ScheduleConfigurationType msk) { + return static_cast((msk & SCHEDULE_TYPE_MASK) >> SCHEDULE_TYPE_SHIFT); + } + + unsigned int getScheduleRepetition(ScheduleConfigurationType msk) { + return (msk & SCHEDULE_REP_MASK); + } + + unsigned int getScheduleWeekMask(ScheduleConfigurationType msk) { + return (msk & SCHEDULE_WEEK_MASK); + } + + unsigned int getScheduleDay(ScheduleConfigurationType msk) { + return (msk & SCHEDULE_DAY_MASK); + } + + unsigned int getScheduleMonth(ScheduleConfigurationType msk) { + return ((msk & SCHEDULE_MONTH_MASK) >> SCHEDULE_MONTH_SHIFT); + } + + bool isScheduleOneShot(ScheduleConfigurationType msk) { + return (getScheduleType(msk) == ScheduleType::OneShot) ? true : false ; + } + + bool isScheduleFixed(ScheduleConfigurationType msk) { + return (getScheduleType(msk) == ScheduleType::FixedDelta) ? true : false ; + } + + bool isScheduleWeekly(ScheduleConfigurationType msk) { + return (getScheduleType(msk) == ScheduleType::Weekly) ? true : false ; + } + + bool isScheduleMonthly(ScheduleConfigurationType msk) { + return (getScheduleType(msk) == ScheduleType::Monthly) ? true : false ; + } + + bool isScheduleYearly(ScheduleConfigurationType msk) { + return (getScheduleType(msk) == ScheduleType::Yearly) ? true : false ; + } + + bool isScheduleInSeconds(ScheduleConfigurationType msk) { + if(isScheduleFixed(msk)) { + return (getScheduleUnit(msk) == ScheduleUnit::Seconds) ? true : false ; + } else { + return false; + } + } + + bool isScheduleInMinutes(ScheduleConfigurationType msk) { + if(isScheduleFixed(msk)) { + return (getScheduleUnit(msk) == ScheduleUnit::Minutes) ? true : false ; + } else { + return false; + } + } + + bool isScheduleInHours(ScheduleConfigurationType msk) { + if(isScheduleFixed(msk)) { + return (getScheduleUnit(msk) == ScheduleUnit::Hours) ? true : false ; + } else { + return false; + } + } + + bool isScheduleInDays(ScheduleConfigurationType msk) { + if(isScheduleFixed(msk)) { + return (getScheduleUnit(msk) == ScheduleUnit::Days) ? true : false ; + } else { + return false; + } + } + + unsigned int getCurrentDayMask(time_t time) { + struct tm * ptm; + ptm = gmtime (&time); + + return 1 << ptm->tm_wday; + } + + unsigned int getCurrentDay(time_t time) { + struct tm * ptm; + ptm = gmtime (&time); + + return ptm->tm_mday; + } + + unsigned int getCurrentMonth(time_t time) { + struct tm * ptm; + ptm = gmtime (&time); + + return ptm->tm_mon; + } + + bool checkTimeValid(ScheduleTimeType now) { + return (now != 0); + } + + bool checkSchedulePeriod(ScheduleTimeType now, ScheduleTimeType frm, ScheduleTimeType to) { + /* Check if current time is inside the schedule period. If 'to' is equal to + * 0 the schedule has no end. + */ + if(now >= frm && (now < to || to == 0)) { + return true; + } else { + return false; + } + } + + bool checkScheduleMask(ScheduleTimeType now, ScheduleConfigurationType msk) { + if(isScheduleFixed(msk) || isScheduleOneShot(msk)) { + return true; + } + + if(isScheduleWeekly(msk)) { + unsigned int currentDayMask = getCurrentDayMask(now); + unsigned int scheduleMask = getScheduleWeekMask(msk); + + if((currentDayMask & scheduleMask) != 0) { + return true; + } + } + + if(isScheduleMonthly(msk)) { + unsigned int currentDay = getCurrentDay(now); + unsigned int scheduleDay = getScheduleDay(msk); + + if(currentDay == scheduleDay) { + return true; + } + } + + if(isScheduleYearly(msk)) { + unsigned int currentDay = getCurrentDay(now); + unsigned int scheduleDay = getScheduleDay(msk); + unsigned int currentMonth = getCurrentMonth(now); + unsigned int scheduleMonth = getScheduleMonth(msk); + + if((currentDay == scheduleDay) && (currentMonth == scheduleMonth)) { + return true; + } + } + + return false; + } + + ScheduleTimeType getScheduleDelta(ScheduleConfigurationType msk) { + if(isScheduleInSeconds(msk)) { + return SECONDS * getScheduleRepetition(msk); + } + + if(isScheduleInMinutes(msk)) { + return MINUTES * getScheduleRepetition(msk); + } + + if(isScheduleInHours(msk)) { + return HOURS * getScheduleRepetition(msk); + } + + if(isScheduleInDays(msk)) { + return DAYS * getScheduleRepetition(msk); + } + + if(isScheduleWeekly(msk) || isScheduleMonthly(msk) || isScheduleYearly(msk)) { + return DAYS; + } + + return SCHEDULE_ONE_SHOT; + } +}; + +class CloudSchedule : public Property { + private: + Schedule _value, + _cloud_value; + public: + CloudSchedule() : _value(0, 0, 0, 0), _cloud_value(0, 0, 0, 0) {} + CloudSchedule(unsigned int frm, unsigned int to, unsigned int len, unsigned int msk) : _value(frm, to, len, msk), _cloud_value(frm, to, len, msk) {} + + virtual bool isDifferentFromCloud() { + + return _value != _cloud_value; + } + + CloudSchedule& operator=(Schedule aSchedule) { + _value.frm = aSchedule.frm; + _value.to = aSchedule.to; + _value.len = aSchedule.len; + _value.msk = aSchedule.msk; + updateLocalTimestamp(); + return *this; + } + + Schedule getCloudValue() { + return _cloud_value; + } + + Schedule getValue() { + return _value; + } + + bool isActive() { + return _value.isActive(); + } + + virtual void fromCloudToLocal() { + _value = _cloud_value; + } + virtual void fromLocalToCloud() { + _cloud_value = _value; + } + virtual CborError appendAttributesToCloud() { + CHECK_CBOR(appendAttribute(_value.frm)); + CHECK_CBOR(appendAttribute(_value.to)); + CHECK_CBOR(appendAttribute(_value.len)); + CHECK_CBOR(appendAttribute(_value.msk)); + return CborNoError; + } + virtual void setAttributesFromCloud() { + setAttribute(_cloud_value.frm); + setAttribute(_cloud_value.to); + setAttribute(_cloud_value.len); + setAttribute(_cloud_value.msk); + } +}; + +#endif /* CLOUDSCHEDULE_H_ */ diff --git a/src/utility/time/TimeService.cpp b/src/utility/time/TimeService.cpp index dcd15f83b..112745d06 100644 --- a/src/utility/time/TimeService.cpp +++ b/src/utility/time/TimeService.cpp @@ -44,6 +44,7 @@ time_t cvt_time(char const * time); **************************************************************************************/ static time_t const EPOCH_AT_COMPILE_TIME = cvt_time(__DATE__); +static time_t const EPOCH = 0; /************************************************************************************** * CTOR/DTOR @@ -54,6 +55,9 @@ TimeService::TimeService() #if defined (ARDUINO_ARCH_SAMD) || defined (ARDUINO_ARCH_MBED) , _is_rtc_configured(false) #endif +, _is_tz_configured(false) +, _timezone_offset(0) +, _timezone_dst_until(0) { } @@ -75,15 +79,25 @@ unsigned long TimeService::getTime() #ifdef ARDUINO_ARCH_SAMD if(!_is_rtc_configured) { - rtc.setEpoch(getRemoteTime()); - _is_rtc_configured = true; + unsigned long utc = getRemoteTime(); + if(EPOCH_AT_COMPILE_TIME != utc) + { + rtc.setEpoch(utc); + _is_rtc_configured = true; + } + return utc; } return rtc.getEpoch(); #elif ARDUINO_ARCH_MBED if(!_is_rtc_configured) { - set_time(getRemoteTime()); - _is_rtc_configured = true; + unsigned long utc = getRemoteTime(); + if(EPOCH_AT_COMPILE_TIME != utc) + { + set_time(utc); + _is_rtc_configured = true; + } + return utc; } return time(NULL); #else @@ -91,6 +105,89 @@ unsigned long TimeService::getTime() #endif } +void TimeService::setTimeZoneData(long offset, unsigned long dst_until) +{ + if(_timezone_offset != offset) + DEBUG_DEBUG("ArduinoIoTCloudTCP::%s tz_offset: [%d]", __FUNCTION__, offset); + _timezone_offset = offset; + + if(_timezone_dst_until != dst_until) + DEBUG_DEBUG("ArduinoIoTCloudTCP::%s tz_dst_unitl: [%ul]", __FUNCTION__, dst_until); + _timezone_dst_until = dst_until; + + _is_tz_configured = true; +} + +unsigned long TimeService::getLocalTime() +{ + unsigned long utc = getTime(); + if(_is_tz_configured) { + return utc + _timezone_offset; + } else { + return EPOCH; + } +} + +unsigned long TimeService::getTimeFromString(const String& input) +{ + struct tm t = + { + 0 /* tm_sec */, + 0 /* tm_min */, + 0 /* tm_hour */, + 0 /* tm_mday */, + 0 /* tm_mon */, + 0 /* tm_year */, + 0 /* tm_wday */, + 0 /* tm_yday */, + 0 /* tm_isdst */ + }; + + char s_month[16]; + int month, day, year, hour, min, sec; + static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec"; + static const int expected_length = 20; + static const int expected_parameters = 6; + + if(input == nullptr || input.length() != expected_length) + { + DEBUG_ERROR("ArduinoIoTCloudTCP::%s invalid input length", __FUNCTION__); + return 0; + } + + int scanned_parameters = sscanf(input.c_str(), "%d %s %d %d:%d:%d", &year, s_month, &day, &hour, &min, &sec); + + if(scanned_parameters != expected_parameters) + { + DEBUG_ERROR("ArduinoIoTCloudTCP::%s invalid input parameters number", __FUNCTION__); + return 0; + } + + char * s_month_position = strstr(month_names, s_month); + + if(s_month_position == nullptr || strlen(s_month) != 3) { + DEBUG_ERROR("ArduinoIoTCloudTCP::%s invalid month name, use %s", __FUNCTION__, month_names); + return 0; + } + + month = (s_month_position - month_names) / 3; + + if(month < 0 || month > 11 || day < 1 || day > 31 || year < 1900 || hour < 0 || + hour > 24 || min < 0 || min > 60 || sec < 0 || sec > 60) { + DEBUG_ERROR("ArduinoIoTCloudTCP::%s invalid date values", __FUNCTION__); + return 0; + } + + t.tm_mon = month; + t.tm_mday = day; + t.tm_year = year - 1900; + t.tm_hour = hour; + t.tm_min = min; + t.tm_sec = sec; + t.tm_isdst = -1; + + return mktime(&t); +} /************************************************************************************** * PRIVATE MEMBER FUNCTIONS **************************************************************************************/ @@ -170,3 +267,8 @@ time_t cvt_time(char const * time) return mktime(&t); } + +TimeService & ArduinoIoTCloudTimeService() { + static TimeService _timeService_instance; + return _timeService_instance; +} diff --git a/src/utility/time/TimeService.h b/src/utility/time/TimeService.h index ae8c12f35..fbe81986d 100644 --- a/src/utility/time/TimeService.h +++ b/src/utility/time/TimeService.h @@ -46,6 +46,12 @@ class TimeService void begin (ConnectionHandler * con_hdl); unsigned long getTime(); + unsigned long getLocalTime(); + void setTimeZoneData(long offset, unsigned long valid_until); + /* Helper function to convert an input String into a UNIX timestamp. + * The input String format must be as follow "2021 Nov 01 17:00:00" + */ + static unsigned long getTimeFromString(const String& input); private: @@ -53,10 +59,15 @@ class TimeService #if defined (ARDUINO_ARCH_SAMD) || defined (ARDUINO_ARCH_MBED) bool _is_rtc_configured; #endif + bool _is_tz_configured; + long _timezone_offset; + unsigned long _timezone_dst_until; unsigned long getRemoteTime(); static bool isTimeValid(unsigned long const time); }; +TimeService & ArduinoIoTCloudTimeService(); + #endif /* ARDUINO_IOT_CLOUD_TIME_SERVICE_H_ */