Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

363 lines
12 KiB

6 years ago
6 years ago
  1. /*
  2. SCHEDULER MODULE
  3. Copyright (C) 2017 by faina09
  4. Adapted by Xose Pérez <xose dot perez at gmail dot com>
  5. */
  6. #include "scheduler.h"
  7. #if SCHEDULER_SUPPORT
  8. #include "broker.h"
  9. #include "light.h"
  10. #include "ntp.h"
  11. #include "relay.h"
  12. #include "ws.h"
  13. #include "curtain_kingart.h"
  14. constexpr const int SchedulerDummySwitchId = 0xff;
  15. int _sch_restore = 0;
  16. unsigned char schedulableCount() {
  17. return relayCount()
  18. #ifdef CURTAIN_SUPPORT
  19. + curtainCount()
  20. #endif
  21. ;
  22. }
  23. // -----------------------------------------------------------------------------
  24. #if WEB_SUPPORT
  25. bool _schWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  26. return (strncmp(key, "sch", 3) == 0);
  27. }
  28. void _schWebSocketOnVisible(JsonObject& root) {
  29. if (!schedulableCount()) return;
  30. root["schVisible"] = 1;
  31. }
  32. void _schWebSocketOnConnected(JsonObject &root){
  33. if (!schedulableCount()) return;
  34. JsonObject &schedules = root.createNestedObject("schedules");
  35. schedules["max"] = SCHEDULER_MAX_SCHEDULES;
  36. JsonArray& enabled = schedules.createNestedArray("schEnabled");
  37. JsonArray& switch_ = schedules.createNestedArray("schSwitch");
  38. JsonArray& action = schedules.createNestedArray("schAction");
  39. JsonArray& type = schedules.createNestedArray("schType");
  40. JsonArray& hour = schedules.createNestedArray("schHour");
  41. JsonArray& minute = schedules.createNestedArray("schMinute");
  42. JsonArray& utc = schedules.createNestedArray("schUTC");
  43. JsonArray& weekdays = schedules.createNestedArray("schWDs");
  44. uint8_t size = 0;
  45. for (unsigned char i = 0; i < SCHEDULER_MAX_SCHEDULES; i++) {
  46. if (!getSetting({"schSwitch", i}).length()) break;
  47. ++size;
  48. enabled.add(getSetting({"schEnabled", i}, false) ? 1 : 0);
  49. utc.add(getSetting({"schUTC", i}, 0));
  50. switch_.add(getSetting({"schSwitch", i}, 0));
  51. action.add(getSetting({"schAction", i}, 0));
  52. type.add(getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH));
  53. hour.add(getSetting({"schHour", i}, 0));
  54. minute.add(getSetting({"schMinute", i}, 0));
  55. weekdays.add(getSetting({"schWDs", i}, SCHEDULER_WEEKDAYS));
  56. }
  57. schedules["size"] = size;
  58. schedules["start"] = 0;
  59. }
  60. #endif // WEB_SUPPORT
  61. // -----------------------------------------------------------------------------
  62. void _schConfigure() {
  63. bool delete_flag = false;
  64. for (unsigned char i = 0; i < SCHEDULER_MAX_SCHEDULES; i++) {
  65. int sch_switch = getSetting({"schSwitch", i}, SchedulerDummySwitchId);
  66. if (sch_switch == SchedulerDummySwitchId) delete_flag = true;
  67. if (delete_flag) {
  68. delSetting({"schEnabled", i});
  69. delSetting({"schSwitch", i});
  70. delSetting({"schAction", i});
  71. delSetting({"schHour", i});
  72. delSetting({"schMinute", i});
  73. delSetting({"schWDs", i});
  74. delSetting({"schType", i});
  75. delSetting({"schUTC", i});
  76. } else {
  77. #if DEBUG_SUPPORT
  78. bool sch_enabled = getSetting({"schEnabled", i}, false);
  79. int sch_action = getSetting({"schAction", i}, 0);
  80. int sch_hour = getSetting({"schHour", i}, 0);
  81. int sch_minute = getSetting({"schMinute", i}, 0);
  82. bool sch_utc = getSetting({"schUTC", i}, false);
  83. String sch_weekdays = getSetting({"schWDs", i}, SCHEDULER_WEEKDAYS);
  84. int type = getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH);
  85. const auto sch_type =
  86. (SCHEDULER_TYPE_SWITCH == type) ? "switch" :
  87. (SCHEDULER_TYPE_CURTAIN == type) ? "curtain" :
  88. (SCHEDULER_TYPE_DIM == type) ? "channel" : "unknown";
  89. DEBUG_MSG_P(
  90. PSTR("[SCH] Schedule #%d: %s #%d to %d at %02d:%02d %s on %s%s\n"),
  91. i, sch_type, sch_switch,
  92. sch_action, sch_hour, sch_minute, sch_utc ? "UTC" : "local time",
  93. (char *) sch_weekdays.c_str(),
  94. sch_enabled ? "" : " (disabled)"
  95. );
  96. #endif // DEBUG_SUPPORT
  97. }
  98. }
  99. }
  100. bool _schIsThisWeekday(int day, const String& weekdays){
  101. // Convert from Sunday to Monday as day 1
  102. int w = day - 1;
  103. if (0 == w) w = 7;
  104. char pch;
  105. char * p = (char *) weekdays.c_str();
  106. unsigned char position = 0;
  107. while ((pch = p[position++])) {
  108. if ((pch - '0') == w) return true;
  109. }
  110. return false;
  111. }
  112. int _schMinutesLeft(int current_hour, int current_minute, int schedule_hour, int schedule_minute) {
  113. return (schedule_hour - current_hour) * 60 + schedule_minute - current_minute;
  114. }
  115. void _schAction(unsigned char sch_id, int sch_action, int sch_switch) {
  116. const auto sch_type = getSetting({"schType", sch_id}, SCHEDULER_TYPE_SWITCH);
  117. if (SCHEDULER_TYPE_SWITCH == sch_type) {
  118. DEBUG_MSG_P(PSTR("[SCH] Switching switch %d to %d\n"), sch_switch, sch_action);
  119. if (sch_action == 2) {
  120. relayToggle(sch_switch);
  121. } else {
  122. relayStatus(sch_switch, sch_action);
  123. }
  124. }
  125. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  126. if (SCHEDULER_TYPE_DIM == sch_type) {
  127. DEBUG_MSG_P(PSTR("[SCH] Set channel %d value to %d\n"), sch_switch, sch_action);
  128. lightChannel(sch_switch, sch_action);
  129. lightUpdate(true, true);
  130. }
  131. #endif
  132. #if CURTAIN_SUPPORT == 1
  133. if (SCHEDULER_TYPE_CURTAIN == sch_type) {
  134. DEBUG_MSG_P(PSTR("[SCH] Set curtain %d value to %d\n"), sch_switch, sch_action);
  135. curtainSetPosition(sch_switch, sch_action);
  136. }
  137. #endif
  138. }
  139. #if NTP_LEGACY_SUPPORT
  140. NtpCalendarWeekday _schGetWeekday(time_t timestamp, int daybefore) {
  141. if (daybefore > 0) {
  142. timestamp = timestamp - ((hour(timestamp) * SECS_PER_HOUR) + ((minute(timestamp) + 1) * SECS_PER_MIN) + second(timestamp) + (daybefore * SECS_PER_DAY));
  143. }
  144. // XXX: no
  145. time_t utc_timestamp = ntpLocal2UTC(timestamp);
  146. return NtpCalendarWeekday {
  147. weekday(timestamp), hour(timestamp), minute(timestamp),
  148. weekday(utc_timestamp), hour(utc_timestamp), minute(utc_timestamp)
  149. };
  150. }
  151. #else
  152. constexpr time_t secondsPerMinute = 60;
  153. constexpr time_t secondsPerHour = 3600;
  154. constexpr time_t secondsPerDay = secondsPerHour * 24;
  155. NtpCalendarWeekday _schGetWeekday(time_t timestamp, int daybefore) {
  156. tm utc_time;
  157. tm local_time;
  158. gmtime_r(&timestamp, &utc_time);
  159. if (daybefore > 0) {
  160. timestamp = timestamp - ((utc_time.tm_hour * secondsPerHour) + ((utc_time.tm_min + 1) * secondsPerMinute) + utc_time.tm_sec + (daybefore * secondsPerDay));
  161. gmtime_r(&timestamp, &utc_time);
  162. localtime_r(&timestamp, &local_time);
  163. } else {
  164. localtime_r(&timestamp, &local_time);
  165. }
  166. // TimeLib sunday is 1 instead of 0
  167. return NtpCalendarWeekday {
  168. local_time.tm_wday + 1, local_time.tm_hour, local_time.tm_min,
  169. utc_time.tm_wday + 1, utc_time.tm_hour, utc_time.tm_min
  170. };
  171. }
  172. #endif
  173. // If daybefore and relay is -1, check with current timestamp
  174. // Otherwise, modify it by moving 'daybefore' days back and only use the 'relay' id
  175. void _schCheck(int relay, int daybefore) {
  176. time_t timestamp = now();
  177. auto calendar_weekday = _schGetWeekday(timestamp, daybefore);
  178. int minimum_restore_time = -(60 * 24);
  179. int saved_action = -1;
  180. int saved_sch = -1;
  181. // Check schedules
  182. for (unsigned char i = 0; i < SCHEDULER_MAX_SCHEDULES; i++) {
  183. int sch_switch = getSetting({"schSwitch", i}, SchedulerDummySwitchId);
  184. if (sch_switch == SchedulerDummySwitchId) break;
  185. // Skip disabled schedules
  186. if (!getSetting({"schEnabled", i}, false)) continue;
  187. // Get the datetime used for the calculation
  188. const bool sch_utc = getSetting({"schUTC", i}, false);
  189. String sch_weekdays = getSetting({"schWDs", i}, SCHEDULER_WEEKDAYS);
  190. if (_schIsThisWeekday(sch_utc ? calendar_weekday.utc_wday : calendar_weekday.local_wday, sch_weekdays)) {
  191. int sch_hour = getSetting({"schHour", i}, 0);
  192. int sch_minute = getSetting({"schMinute", i}, 0);
  193. int sch_action = getSetting({"schAction", i}, 0);
  194. int sch_type = getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH);
  195. int minutes_to_trigger = _schMinutesLeft(
  196. sch_utc ? calendar_weekday.utc_hour : calendar_weekday.local_hour,
  197. sch_utc ? calendar_weekday.utc_minute : calendar_weekday.local_minute,
  198. sch_hour, sch_minute
  199. );
  200. if (sch_type == SCHEDULER_TYPE_SWITCH && sch_switch == relay && sch_action != 2 && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
  201. minimum_restore_time = minutes_to_trigger;
  202. saved_action = sch_action;
  203. saved_sch = i;
  204. }
  205. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  206. if (SCHEDULER_TYPE_DIM == sch_type && sch_switch == relay && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
  207. minimum_restore_time = minutes_to_trigger;
  208. saved_action = sch_action;
  209. saved_sch = i;
  210. }
  211. #endif
  212. #if CURTAIN_SUPPORT == 1
  213. if (SCHEDULER_TYPE_CURTAIN == sch_type && sch_switch == relay && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
  214. minimum_restore_time = minutes_to_trigger;
  215. saved_action = sch_action;
  216. saved_sch = i;
  217. }
  218. #endif
  219. if (minutes_to_trigger == 0 && relay == -1) {
  220. _schAction(i, sch_action, sch_switch);
  221. DEBUG_MSG_P(PSTR("[SCH] Schedule #%d TRIGGERED!!\n"), i);
  222. // Show minutes to trigger every 15 minutes
  223. // or every minute if less than 15 minutes to scheduled time.
  224. // This only works for schedules on this same day.
  225. // For instance, if your scheduler is set for 00:01 you will only
  226. // get one notification before the trigger (at 00:00)
  227. } else if (minutes_to_trigger > 0 && relay == -1) {
  228. #if DEBUG_SUPPORT
  229. if ((minutes_to_trigger % 15 == 0) || (minutes_to_trigger < 15)) {
  230. DEBUG_MSG_P(
  231. PSTR("[SCH] %d minutes to trigger schedule #%d\n"),
  232. minutes_to_trigger, i
  233. );
  234. }
  235. #endif
  236. }
  237. }
  238. }
  239. if (daybefore >= 0 && daybefore < 7 && minimum_restore_time == -(60 * 24) && saved_action == -1) {
  240. _schCheck(relay, ++daybefore);
  241. return;
  242. }
  243. if (minimum_restore_time != -(60 * 24) && saved_action != -1 && saved_sch != -1) {
  244. _schAction(saved_sch, saved_action, relay);
  245. }
  246. }
  247. // -----------------------------------------------------------------------------
  248. void schSetup() {
  249. _schConfigure();
  250. #if WEB_SUPPORT
  251. wsRegister()
  252. .onVisible(_schWebSocketOnVisible)
  253. .onConnected(_schWebSocketOnConnected)
  254. .onKeyCheck(_schWebSocketOnKeyCheck);
  255. #endif
  256. NtpBroker::Register([](const NtpTick tick, time_t, const String&) {
  257. if (NtpTick::EveryMinute != tick) return;
  258. static bool restore_once = true;
  259. if (restore_once) {
  260. for (unsigned char i = 0; i < schedulableCount(); i++) {
  261. if (getSetting({"relayLastSch", i}, 1 == SCHEDULER_RESTORE_LAST_SCHEDULE)) {
  262. _schCheck(i, 0);
  263. }
  264. }
  265. restore_once = false;
  266. }
  267. _schCheck(-1, -1);
  268. });
  269. espurnaRegisterReload(_schConfigure);
  270. }
  271. #endif // SCHEDULER_SUPPORT