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.

393 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. #if NTP_LEGACY_SUPPORT
  168. NtpCalendarWeekday _schGetWeekday(time_t timestamp, int daybefore) {
  169. if (daybefore > 0) {
  170. timestamp = timestamp - ((hour(timestamp) * SECS_PER_HOUR) + ((minute(timestamp) + 1) * SECS_PER_MIN) + second(timestamp) + (daybefore * SECS_PER_DAY));
  171. }
  172. // XXX: no
  173. time_t utc_timestamp = ntpLocal2UTC(timestamp);
  174. return NtpCalendarWeekday {
  175. weekday(timestamp), hour(timestamp), minute(timestamp),
  176. weekday(utc_timestamp), hour(utc_timestamp), minute(utc_timestamp)
  177. };
  178. }
  179. #else
  180. constexpr time_t secondsPerMinute = 60;
  181. constexpr time_t secondsPerHour = 3600;
  182. constexpr time_t secondsPerDay = secondsPerHour * 24;
  183. NtpCalendarWeekday _schGetWeekday(time_t timestamp, int daybefore) {
  184. tm utc_time;
  185. tm local_time;
  186. gmtime_r(&timestamp, &utc_time);
  187. if (daybefore > 0) {
  188. timestamp = timestamp - ((utc_time.tm_hour * secondsPerHour) + ((utc_time.tm_min + 1) * secondsPerMinute) + utc_time.tm_sec + (daybefore * secondsPerDay));
  189. gmtime_r(&timestamp, &utc_time);
  190. localtime_r(&timestamp, &local_time);
  191. } else {
  192. localtime_r(&timestamp, &local_time);
  193. }
  194. // TimeLib sunday is 1 instead of 0
  195. return NtpCalendarWeekday {
  196. local_time.tm_wday + 1, local_time.tm_hour, local_time.tm_min,
  197. utc_time.tm_wday + 1, utc_time.tm_hour, utc_time.tm_min
  198. };
  199. }
  200. #endif
  201. // If daybefore and target is -1, check with current timestamp
  202. // Otherwise, modify it by moving 'daybefore' days back and only use the 'target' id
  203. void _schCheck(int target, int daybefore) {
  204. time_t timestamp = now();
  205. auto calendar_weekday = _schGetWeekday(timestamp, daybefore);
  206. int minimum_restore_time = -(60 * 24);
  207. int saved_action = -1;
  208. int saved_sch = -1;
  209. // Check schedules
  210. for (unsigned char i = 0; i < scheduler::build::max(); i++) {
  211. int sch_switch = getSetting({"schSwitch", i}, SchedulerDummySwitchId);
  212. if (sch_switch == SchedulerDummySwitchId) break;
  213. // Skip disabled schedules
  214. if (!getSetting({"schEnabled", i}, false)) continue;
  215. // Get the datetime used for the calculation
  216. const bool sch_utc = getSetting({"schUTC", i}, false);
  217. String sch_weekdays = getSetting({"schWDs", i}, SCHEDULER_WEEKDAYS);
  218. if (_schIsThisWeekday(sch_utc ? calendar_weekday.utc_wday : calendar_weekday.local_wday, sch_weekdays)) {
  219. int sch_hour = getSetting({"schHour", i}, 0);
  220. int sch_minute = getSetting({"schMinute", i}, 0);
  221. int sch_action = getSetting({"schAction", i}, 0);
  222. int sch_type = getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH);
  223. int minutes_to_trigger = _schMinutesLeft(
  224. sch_utc ? calendar_weekday.utc_hour : calendar_weekday.local_hour,
  225. sch_utc ? calendar_weekday.utc_minute : calendar_weekday.local_minute,
  226. sch_hour, sch_minute
  227. );
  228. if (sch_type == SCHEDULER_TYPE_SWITCH && sch_switch == target && sch_action != 2 && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
  229. minimum_restore_time = minutes_to_trigger;
  230. saved_action = sch_action;
  231. saved_sch = i;
  232. }
  233. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  234. if (SCHEDULER_TYPE_DIM == sch_type && sch_switch == target && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
  235. minimum_restore_time = minutes_to_trigger;
  236. saved_action = sch_action;
  237. saved_sch = i;
  238. }
  239. #endif
  240. #if CURTAIN_SUPPORT == 1
  241. if (SCHEDULER_TYPE_CURTAIN == sch_type && sch_switch == target && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
  242. minimum_restore_time = minutes_to_trigger;
  243. saved_action = sch_action;
  244. saved_sch = i;
  245. }
  246. #endif
  247. if (minutes_to_trigger == 0 && target == -1) {
  248. _schAction(i, sch_action, sch_switch);
  249. DEBUG_MSG_P(PSTR("[SCH] Schedule #%u TRIGGERED!!\n"), i);
  250. // Show minutes to trigger every 15 minutes
  251. // or every minute if less than 15 minutes to scheduled time.
  252. // This only works for schedules on this same day.
  253. // For instance, if your scheduler is set for 00:01 you will only
  254. // get one notification before the trigger (at 00:00)
  255. } else if (minutes_to_trigger > 0 && target == -1) {
  256. #if DEBUG_SUPPORT
  257. if ((minutes_to_trigger % 15 == 0) || (minutes_to_trigger < 15)) {
  258. DEBUG_MSG_P(
  259. PSTR("[SCH] %d minutes to trigger schedule #%u\n"),
  260. minutes_to_trigger, i
  261. );
  262. }
  263. #endif
  264. }
  265. }
  266. }
  267. if (daybefore >= 0 && daybefore < 7 && minimum_restore_time == -(60 * 24) && saved_action == -1) {
  268. _schCheck(target, ++daybefore);
  269. return;
  270. }
  271. if (minimum_restore_time != -(60 * 24) && saved_action != -1 && saved_sch != -1) {
  272. _schAction(saved_sch, saved_action, target);
  273. }
  274. }
  275. // -----------------------------------------------------------------------------
  276. void schSetup() {
  277. _schConfigure();
  278. #if WEB_SUPPORT
  279. wsRegister()
  280. .onVisible(_schWebSocketOnVisible)
  281. .onConnected(_schWebSocketOnConnected)
  282. .onKeyCheck(_schWebSocketOnKeyCheck);
  283. #endif
  284. static bool restore_once = true;
  285. ntpOnTick([](NtpTick tick) {
  286. switch (tick) {
  287. case NtpTick::EveryHour:
  288. return;
  289. case NtpTick::EveryMinute:
  290. if (restore_once) {
  291. auto targets = schedulableCount();
  292. for (size_t i = 0; i < targets; i++) {
  293. if (getSetting({"schRestore", i}, scheduler::build::restoreLast())) {
  294. _schCheck(i, 0);
  295. }
  296. }
  297. }
  298. restore_once = false;
  299. }
  300. _schCheck(-1, -1);
  301. });
  302. espurnaRegisterReload(_schConfigure);
  303. }
  304. #endif // SCHEDULER_SUPPORT