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.

365 lines
12 KiB

providers: relays, lights and buttons refactoring (#2414) - gpio module now tracks the known providers (right now, hardware and mcp expander) - refactored relay struct to use 'Provider' implementing setup,notify,change,boot instead of just BasePin actions - refactored button module to use gpio provider instead of referencing types itself - removed dual & stm code from buttons, migrate both to relay module - added status notify and change callbacks for relayStatus (i.e. 'notify' when relay status was called, but not changed. and 'changed' when it did) - relays runtime configuration keys - relay command now shows configured relays and current & target statuses - refactor the code using relayStatus(0, blah) under LIGHT_PROVIDER check to use lightState instead - remove rfbridge code form relay module. implement through a basic state listener in the rfbridge module, depend on RELAY_SUPPORT - allow to bind rf codes to real relays - drop tuya-specific lights provider, remove tuya code from relays and lights modules - integrate tuya via relay listeners and providers, use lights custom provider - implement channel transitions for tuya. disabled by default, and transition time and step are overridden to 2000 + 100. needs to be set to some value below the total time (i.e. `total transition time / step time == number of steps`, we need to figure out a correct time that serial comms could handle) - lights custom provider (global, not per-pin) and state listeners - remove lights code from relay module. implement through providers & listeners in the lights module, depend on RELAY_SUPPORT - lights per-channel relay provider (unused atm), depend on RELAY_SUPPORT - refactored channel transition - calculate step only once, make sure time + step values are sane, generate quick transitions with very small delay (10ms hardcoded) for transitions during OFF state i.e. we no longer waste 500ms (or whatever transition time is set to) on boot doing nothing - transition time + step parameter for the lightUpdate - report mask parameter for the lightUpdate - minor fixes across the board resolve #2222
3 years ago
6 years ago
providers: relays, lights and buttons refactoring (#2414) - gpio module now tracks the known providers (right now, hardware and mcp expander) - refactored relay struct to use 'Provider' implementing setup,notify,change,boot instead of just BasePin actions - refactored button module to use gpio provider instead of referencing types itself - removed dual & stm code from buttons, migrate both to relay module - added status notify and change callbacks for relayStatus (i.e. 'notify' when relay status was called, but not changed. and 'changed' when it did) - relays runtime configuration keys - relay command now shows configured relays and current & target statuses - refactor the code using relayStatus(0, blah) under LIGHT_PROVIDER check to use lightState instead - remove rfbridge code form relay module. implement through a basic state listener in the rfbridge module, depend on RELAY_SUPPORT - allow to bind rf codes to real relays - drop tuya-specific lights provider, remove tuya code from relays and lights modules - integrate tuya via relay listeners and providers, use lights custom provider - implement channel transitions for tuya. disabled by default, and transition time and step are overridden to 2000 + 100. needs to be set to some value below the total time (i.e. `total transition time / step time == number of steps`, we need to figure out a correct time that serial comms could handle) - lights custom provider (global, not per-pin) and state listeners - remove lights code from relay module. implement through providers & listeners in the lights module, depend on RELAY_SUPPORT - lights per-channel relay provider (unused atm), depend on RELAY_SUPPORT - refactored channel transition - calculate step only once, make sure time + step values are sane, generate quick transitions with very small delay (10ms hardcoded) for transitions during OFF state i.e. we no longer waste 500ms (or whatever transition time is set to) on boot doing nothing - transition time + step parameter for the lightUpdate - report mask parameter for the lightUpdate - minor fixes across the board resolve #2222
3 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. int _sch_restore = 0;
  17. unsigned char schedulableCount() {
  18. return relayCount()
  19. #ifdef CURTAIN_SUPPORT
  20. + curtainCount()
  21. #endif
  22. ;
  23. }
  24. // -----------------------------------------------------------------------------
  25. #if WEB_SUPPORT
  26. bool _schWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  27. return (strncmp(key, "sch", 3) == 0);
  28. }
  29. void _schWebSocketOnVisible(JsonObject& root) {
  30. if (!schedulableCount()) return;
  31. root["schVisible"] = 1;
  32. }
  33. void _schWebSocketOnConnected(JsonObject &root){
  34. if (!schedulableCount()) return;
  35. JsonObject &schedules = root.createNestedObject("schedules");
  36. schedules["max"] = SCHEDULER_MAX_SCHEDULES;
  37. JsonArray& enabled = schedules.createNestedArray("schEnabled");
  38. JsonArray& switch_ = schedules.createNestedArray("schSwitch");
  39. JsonArray& action = schedules.createNestedArray("schAction");
  40. JsonArray& type = schedules.createNestedArray("schType");
  41. JsonArray& hour = schedules.createNestedArray("schHour");
  42. JsonArray& minute = schedules.createNestedArray("schMinute");
  43. JsonArray& utc = schedules.createNestedArray("schUTC");
  44. JsonArray& weekdays = schedules.createNestedArray("schWDs");
  45. uint8_t size = 0;
  46. for (unsigned char i = 0; i < SCHEDULER_MAX_SCHEDULES; i++) {
  47. if (!getSetting({"schSwitch", i}).length()) break;
  48. ++size;
  49. enabled.add(getSetting({"schEnabled", i}, false) ? 1 : 0);
  50. utc.add(getSetting({"schUTC", i}, 0));
  51. switch_.add(getSetting({"schSwitch", i}, 0));
  52. action.add(getSetting({"schAction", i}, 0));
  53. type.add(getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH));
  54. hour.add(getSetting({"schHour", i}, 0));
  55. minute.add(getSetting({"schMinute", i}, 0));
  56. weekdays.add(getSetting({"schWDs", i}, SCHEDULER_WEEKDAYS));
  57. }
  58. schedules["size"] = size;
  59. schedules["start"] = 0;
  60. }
  61. #endif // WEB_SUPPORT
  62. // -----------------------------------------------------------------------------
  63. void _schConfigure() {
  64. bool delete_flag = false;
  65. for (unsigned char i = 0; i < SCHEDULER_MAX_SCHEDULES; i++) {
  66. int sch_switch = getSetting({"schSwitch", i}, SchedulerDummySwitchId);
  67. if (sch_switch == SchedulerDummySwitchId) delete_flag = true;
  68. if (delete_flag) {
  69. delSetting({"schEnabled", i});
  70. delSetting({"schSwitch", i});
  71. delSetting({"schAction", i});
  72. delSetting({"schHour", i});
  73. delSetting({"schMinute", i});
  74. delSetting({"schWDs", i});
  75. delSetting({"schType", i});
  76. delSetting({"schUTC", i});
  77. } else {
  78. #if DEBUG_SUPPORT
  79. bool sch_enabled = getSetting({"schEnabled", i}, false);
  80. int sch_action = getSetting({"schAction", i}, 0);
  81. int sch_hour = getSetting({"schHour", i}, 0);
  82. int sch_minute = getSetting({"schMinute", i}, 0);
  83. bool sch_utc = getSetting({"schUTC", i}, false);
  84. String sch_weekdays = getSetting({"schWDs", i}, SCHEDULER_WEEKDAYS);
  85. int type = getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH);
  86. const auto sch_type =
  87. (SCHEDULER_TYPE_SWITCH == type) ? "switch" :
  88. (SCHEDULER_TYPE_CURTAIN == type) ? "curtain" :
  89. (SCHEDULER_TYPE_DIM == type) ? "channel" : "unknown";
  90. DEBUG_MSG_P(
  91. PSTR("[SCH] Schedule #%d: %s #%d to %d at %02d:%02d %s on %s%s\n"),
  92. i, sch_type, sch_switch,
  93. sch_action, sch_hour, sch_minute, sch_utc ? "UTC" : "local time",
  94. sch_weekdays.c_str(),
  95. sch_enabled ? "" : " (disabled)"
  96. );
  97. #endif // DEBUG_SUPPORT
  98. }
  99. }
  100. }
  101. bool _schIsThisWeekday(int day, const String& weekdays){
  102. // Convert from Sunday to Monday as day 1
  103. int w = day - 1;
  104. if (0 == w) w = 7;
  105. char pch;
  106. char * p = (char *) weekdays.c_str();
  107. unsigned char position = 0;
  108. while ((pch = p[position++])) {
  109. if ((pch - '0') == w) return true;
  110. }
  111. return false;
  112. }
  113. int _schMinutesLeft(int current_hour, int current_minute, int schedule_hour, int schedule_minute) {
  114. return (schedule_hour - current_hour) * 60 + schedule_minute - current_minute;
  115. }
  116. void _schAction(unsigned char sch_id, int sch_action, int sch_switch) {
  117. const auto sch_type = getSetting({"schType", sch_id}, SCHEDULER_TYPE_SWITCH);
  118. if (SCHEDULER_TYPE_SWITCH == sch_type) {
  119. DEBUG_MSG_P(PSTR("[SCH] Switching switch %d to %d\n"), sch_switch, sch_action);
  120. if (sch_action == 2) {
  121. relayToggle(sch_switch);
  122. } else {
  123. relayStatus(sch_switch, sch_action);
  124. }
  125. }
  126. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  127. if (SCHEDULER_TYPE_DIM == sch_type) {
  128. DEBUG_MSG_P(PSTR("[SCH] Set channel %d value to %d\n"), sch_switch, sch_action);
  129. lightChannel(sch_switch, sch_action);
  130. lightUpdate();
  131. }
  132. #endif
  133. #if CURTAIN_SUPPORT == 1
  134. if (SCHEDULER_TYPE_CURTAIN == sch_type) {
  135. DEBUG_MSG_P(PSTR("[SCH] Set curtain %d value to %d\n"), sch_switch, sch_action);
  136. curtainSetPosition(sch_switch, sch_action);
  137. }
  138. #endif
  139. }
  140. #if NTP_LEGACY_SUPPORT
  141. NtpCalendarWeekday _schGetWeekday(time_t timestamp, int daybefore) {
  142. if (daybefore > 0) {
  143. timestamp = timestamp - ((hour(timestamp) * SECS_PER_HOUR) + ((minute(timestamp) + 1) * SECS_PER_MIN) + second(timestamp) + (daybefore * SECS_PER_DAY));
  144. }
  145. // XXX: no
  146. time_t utc_timestamp = ntpLocal2UTC(timestamp);
  147. return NtpCalendarWeekday {
  148. weekday(timestamp), hour(timestamp), minute(timestamp),
  149. weekday(utc_timestamp), hour(utc_timestamp), minute(utc_timestamp)
  150. };
  151. }
  152. #else
  153. constexpr time_t secondsPerMinute = 60;
  154. constexpr time_t secondsPerHour = 3600;
  155. constexpr time_t secondsPerDay = secondsPerHour * 24;
  156. NtpCalendarWeekday _schGetWeekday(time_t timestamp, int daybefore) {
  157. tm utc_time;
  158. tm local_time;
  159. gmtime_r(&timestamp, &utc_time);
  160. if (daybefore > 0) {
  161. timestamp = timestamp - ((utc_time.tm_hour * secondsPerHour) + ((utc_time.tm_min + 1) * secondsPerMinute) + utc_time.tm_sec + (daybefore * secondsPerDay));
  162. gmtime_r(&timestamp, &utc_time);
  163. localtime_r(&timestamp, &local_time);
  164. } else {
  165. localtime_r(&timestamp, &local_time);
  166. }
  167. // TimeLib sunday is 1 instead of 0
  168. return NtpCalendarWeekday {
  169. local_time.tm_wday + 1, local_time.tm_hour, local_time.tm_min,
  170. utc_time.tm_wday + 1, utc_time.tm_hour, utc_time.tm_min
  171. };
  172. }
  173. #endif
  174. // If daybefore and relay is -1, check with current timestamp
  175. // Otherwise, modify it by moving 'daybefore' days back and only use the 'relay' id
  176. void _schCheck(int relay, int daybefore) {
  177. time_t timestamp = now();
  178. auto calendar_weekday = _schGetWeekday(timestamp, daybefore);
  179. int minimum_restore_time = -(60 * 24);
  180. int saved_action = -1;
  181. int saved_sch = -1;
  182. // Check schedules
  183. for (unsigned char i = 0; i < SCHEDULER_MAX_SCHEDULES; i++) {
  184. int sch_switch = getSetting({"schSwitch", i}, SchedulerDummySwitchId);
  185. if (sch_switch == SchedulerDummySwitchId) break;
  186. // Skip disabled schedules
  187. if (!getSetting({"schEnabled", i}, false)) continue;
  188. // Get the datetime used for the calculation
  189. const bool sch_utc = getSetting({"schUTC", i}, false);
  190. String sch_weekdays = getSetting({"schWDs", i}, SCHEDULER_WEEKDAYS);
  191. if (_schIsThisWeekday(sch_utc ? calendar_weekday.utc_wday : calendar_weekday.local_wday, sch_weekdays)) {
  192. int sch_hour = getSetting({"schHour", i}, 0);
  193. int sch_minute = getSetting({"schMinute", i}, 0);
  194. int sch_action = getSetting({"schAction", i}, 0);
  195. int sch_type = getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH);
  196. int minutes_to_trigger = _schMinutesLeft(
  197. sch_utc ? calendar_weekday.utc_hour : calendar_weekday.local_hour,
  198. sch_utc ? calendar_weekday.utc_minute : calendar_weekday.local_minute,
  199. sch_hour, sch_minute
  200. );
  201. if (sch_type == SCHEDULER_TYPE_SWITCH && sch_switch == relay && sch_action != 2 && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
  202. minimum_restore_time = minutes_to_trigger;
  203. saved_action = sch_action;
  204. saved_sch = i;
  205. }
  206. #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
  207. if (SCHEDULER_TYPE_DIM == sch_type && sch_switch == relay && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
  208. minimum_restore_time = minutes_to_trigger;
  209. saved_action = sch_action;
  210. saved_sch = i;
  211. }
  212. #endif
  213. #if CURTAIN_SUPPORT == 1
  214. if (SCHEDULER_TYPE_CURTAIN == sch_type && sch_switch == relay && 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. #endif
  220. if (minutes_to_trigger == 0 && relay == -1) {
  221. _schAction(i, sch_action, sch_switch);
  222. DEBUG_MSG_P(PSTR("[SCH] Schedule #%d TRIGGERED!!\n"), i);
  223. // Show minutes to trigger every 15 minutes
  224. // or every minute if less than 15 minutes to scheduled time.
  225. // This only works for schedules on this same day.
  226. // For instance, if your scheduler is set for 00:01 you will only
  227. // get one notification before the trigger (at 00:00)
  228. } else if (minutes_to_trigger > 0 && relay == -1) {
  229. #if DEBUG_SUPPORT
  230. if ((minutes_to_trigger % 15 == 0) || (minutes_to_trigger < 15)) {
  231. DEBUG_MSG_P(
  232. PSTR("[SCH] %d minutes to trigger schedule #%d\n"),
  233. minutes_to_trigger, i
  234. );
  235. }
  236. #endif
  237. }
  238. }
  239. }
  240. if (daybefore >= 0 && daybefore < 7 && minimum_restore_time == -(60 * 24) && saved_action == -1) {
  241. _schCheck(relay, ++daybefore);
  242. return;
  243. }
  244. if (minimum_restore_time != -(60 * 24) && saved_action != -1 && saved_sch != -1) {
  245. _schAction(saved_sch, saved_action, relay);
  246. }
  247. }
  248. // -----------------------------------------------------------------------------
  249. void schSetup() {
  250. _schConfigure();
  251. #if WEB_SUPPORT
  252. wsRegister()
  253. .onVisible(_schWebSocketOnVisible)
  254. .onConnected(_schWebSocketOnConnected)
  255. .onKeyCheck(_schWebSocketOnKeyCheck);
  256. #endif
  257. static bool restore_once = true;
  258. NtpBroker::Register([](NtpTick tick, time_t, const String&) {
  259. if (NtpTick::EveryMinute != tick) {
  260. return;
  261. }
  262. if (restore_once) {
  263. for (unsigned char i = 0; i < schedulableCount(); i++) {
  264. if (getSetting({"relayLastSch", i}, 1 == SCHEDULER_RESTORE_LAST_SCHEDULE)) {
  265. _schCheck(i, 0);
  266. }
  267. }
  268. restore_once = false;
  269. }
  270. _schCheck(-1, -1);
  271. });
  272. espurnaRegisterReload(_schConfigure);
  273. }
  274. #endif // SCHEDULER_SUPPORT