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.

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