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.

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