- /*
-
- SCHEDULER MODULE
-
- Copyright (C) 2017 by faina09
- Adapted by Xose Pérez <xose dot perez at gmail dot com>
-
- */
-
- #include "scheduler.h"
-
- #if SCHEDULER_SUPPORT
-
- #include "light.h"
- #include "ntp.h"
- #include "ntp_timelib.h"
- #include "relay.h"
- #include "ws.h"
- #include "curtain_kingart.h"
-
- constexpr int SchedulerDummySwitchId { 0xff };
-
- #if RELAY_SUPPORT
-
- size_t schedulableCount() {
- return relayCount();
- }
-
- #elif CURTAIN_SUPPORT
-
- size_t schedulableCount() {
- return curtainCount()
- }
-
- #endif
-
- // -----------------------------------------------------------------------------
-
- namespace scheduler {
- namespace build {
-
- constexpr size_t max() {
- return SCHEDULER_MAX_SCHEDULES;
- }
-
- constexpr int defaultType() {
- return SCHEDULER_TYPE_SWITCH;
- }
-
- constexpr const char* const weekdays() {
- return SCHEDULER_WEEKDAYS;
- }
-
- constexpr bool restoreLast() {
- return 1 == SCHEDULER_RESTORE_LAST_SCHEDULE;
- }
-
- } // namespace build
- } // namespace scheduler
-
- // -----------------------------------------------------------------------------
-
- #if WEB_SUPPORT
-
- bool _schWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
- return (strncmp(key, "sch", 3) == 0);
- }
-
- void _schWebSocketOnVisible(JsonObject& root) {
- if (!schedulableCount()) return;
- root["schVisible"] = 1;
- }
-
- void _schWebSocketOnConnected(JsonObject &root){
-
- if (!schedulableCount()) return;
-
- JsonObject& config = root.createNestedObject("schConfig");
- config["max"] = scheduler::build::max();
-
- {
- static constexpr const char* const schema_keys[] PROGMEM = {
- "schEnabled",
- "schRestore",
- "schType",
- "schSwitch",
- "schAction",
- "schHour",
- "schMinute",
- "schUTC",
- "schWDs"
- };
-
- JsonArray& schema = config.createNestedArray("schema");
- schema.copyFrom(schema_keys, sizeof(schema_keys) / sizeof(*schema_keys));
- }
-
- uint8_t size = 0;
-
- JsonArray& schedules = config.createNestedArray("schedules");
-
- for (size_t id = 0; id < scheduler::build::max(); ++id) {
- auto schedulerSwitch = getSetting({"schSwitch", id});
- if (!schedulerSwitch.length()) {
- break;
- }
-
- JsonArray& entry = schedules.createNestedArray();
- ++size;
-
- entry.add(getSetting({"schEnabled", id}, false) ? 1 : 0);
- entry.add(getSetting({"schRestore", id}, scheduler::build::restoreLast()) ? 1 : 0);
-
- entry.add(getSetting({"schType", id}, scheduler::build::defaultType()));
- entry.add(schedulerSwitch);
- entry.add(getSetting({"schAction", id}, 0));
-
- entry.add(getSetting({"schHour", id}, 0));
- entry.add(getSetting({"schMinute", id}, 0));
-
- entry.add(getSetting({"schWDs", id}, scheduler::build::weekdays()));
- entry.add(getSetting({"schUTC", id}, 0));
- }
-
- config["size"] = size;
- config["start"] = 0;
-
- }
-
- #endif // WEB_SUPPORT
-
- // -----------------------------------------------------------------------------
-
- void _schConfigure() {
- for (unsigned char i = 0; i < scheduler::build::max(); i++) {
- int sch_switch = getSetting({"schSwitch", i}, SchedulerDummySwitchId);
- if (sch_switch == SchedulerDummySwitchId) {
- delSetting({"schEnabled", i});
- delSetting({"schRestore", i});
- delSetting({"schSwitch", i});
- delSetting({"schAction", i});
- delSetting({"schHour", i});
- delSetting({"schMinute", i});
- delSetting({"schWDs", i});
- delSetting({"schType", i});
- delSetting({"schUTC", i});
- } else {
- #if DEBUG_SUPPORT
- bool sch_enabled = getSetting({"schEnabled", i}, false);
- int sch_action = getSetting({"schAction", i}, 0);
- int sch_hour = getSetting({"schHour", i}, 0);
- int sch_minute = getSetting({"schMinute", i}, 0);
- bool sch_utc = getSetting({"schUTC", i}, false);
- String sch_weekdays = getSetting({"schWDs", i}, SCHEDULER_WEEKDAYS);
-
- int type = getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH);
- const auto sch_type =
- (SCHEDULER_TYPE_SWITCH == type) ? "switch" :
- (SCHEDULER_TYPE_CURTAIN == type) ? "curtain" :
- (SCHEDULER_TYPE_DIM == type) ? "channel" : "unknown";
-
- DEBUG_MSG_P(
- PSTR("[SCH] Schedule #%d: %s #%d to %d at %02d:%02d %s on %s%s\n"),
- i, sch_type, sch_switch,
- sch_action, sch_hour, sch_minute, sch_utc ? "UTC" : "local time",
- sch_weekdays.c_str(),
- sch_enabled ? "" : " (disabled)"
- );
- #endif // DEBUG_SUPPORT
- }
- }
- }
-
- bool _schIsThisWeekday(int day, const String& weekdays){
-
- // Convert from Sunday to Monday as day 1
- int w = day - 1;
- if (0 == w) w = 7;
-
- char pch;
- char * p = (char *) weekdays.c_str();
- unsigned char position = 0;
- while ((pch = p[position++])) {
- if ((pch - '0') == w) return true;
- }
- return false;
-
- }
-
- int _schMinutesLeft(int current_hour, int current_minute, int schedule_hour, int schedule_minute) {
- return (schedule_hour - current_hour) * 60 + schedule_minute - current_minute;
- }
-
- void _schAction(unsigned char sch_id, int sch_action, int sch_switch) {
- const auto sch_type = getSetting({"schType", sch_id}, SCHEDULER_TYPE_SWITCH);
-
- if (SCHEDULER_TYPE_SWITCH == sch_type) {
- DEBUG_MSG_P(PSTR("[SCH] Switching switch %d to %d\n"), sch_switch, sch_action);
- if (sch_action == 2) {
- relayToggle(sch_switch);
- } else {
- relayStatus(sch_switch, sch_action);
- }
- #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
- } else if (SCHEDULER_TYPE_DIM == sch_type) {
- DEBUG_MSG_P(PSTR("[SCH] Set channel %d value to %d\n"), sch_switch, sch_action);
- lightChannel(sch_switch, sch_action);
- lightUpdate();
- #elif CURTAIN_SUPPORT
- } else if (SCHEDULER_TYPE_CURTAIN == sch_type) {
- DEBUG_MSG_P(PSTR("[SCH] Set curtain %d value to %d\n"), sch_switch, sch_action);
- curtainSetPosition(sch_switch, sch_action);
- #endif
- }
- }
-
- constexpr time_t secondsPerMinute = 60;
- constexpr time_t secondsPerHour = 3600;
- constexpr time_t secondsPerDay = secondsPerHour * 24;
-
- NtpCalendarWeekday _schGetWeekday(time_t timestamp, int daybefore) {
- tm utc_time;
- tm local_time;
-
- gmtime_r(×tamp, &utc_time);
- if (daybefore > 0) {
- timestamp = timestamp - ((utc_time.tm_hour * secondsPerHour) + ((utc_time.tm_min + 1) * secondsPerMinute) + utc_time.tm_sec + (daybefore * secondsPerDay));
- gmtime_r(×tamp, &utc_time);
- localtime_r(×tamp, &local_time);
- } else {
- localtime_r(×tamp, &local_time);
- }
-
- // TimeLib sunday is 1 instead of 0
- return NtpCalendarWeekday {
- local_time.tm_wday + 1, local_time.tm_hour, local_time.tm_min,
- utc_time.tm_wday + 1, utc_time.tm_hour, utc_time.tm_min
- };
- }
-
- // If daybefore and target is -1, check with current timestamp
- // Otherwise, modify it by moving 'daybefore' days back and only use the 'target' id
- void _schCheck(int target, int daybefore) {
- time_t timestamp = now();
- auto calendar_weekday = _schGetWeekday(timestamp, daybefore);
-
- int minimum_restore_time = -(60 * 24);
- int saved_action = -1;
- int saved_sch = -1;
-
- // Check schedules
- for (unsigned char i = 0; i < scheduler::build::max(); i++) {
-
- int sch_switch = getSetting({"schSwitch", i}, SchedulerDummySwitchId);
- if (sch_switch == SchedulerDummySwitchId) break;
-
- // Skip disabled schedules
- if (!getSetting({"schEnabled", i}, false)) continue;
-
- // Get the datetime used for the calculation
- const bool sch_utc = getSetting({"schUTC", i}, false);
-
- String sch_weekdays = getSetting({"schWDs", i}, SCHEDULER_WEEKDAYS);
- if (_schIsThisWeekday(sch_utc ? calendar_weekday.utc_wday : calendar_weekday.local_wday, sch_weekdays)) {
-
- int sch_hour = getSetting({"schHour", i}, 0);
- int sch_minute = getSetting({"schMinute", i}, 0);
- int sch_action = getSetting({"schAction", i}, 0);
- int sch_type = getSetting({"schType", i}, SCHEDULER_TYPE_SWITCH);
-
- int minutes_to_trigger = _schMinutesLeft(
- sch_utc ? calendar_weekday.utc_hour : calendar_weekday.local_hour,
- sch_utc ? calendar_weekday.utc_minute : calendar_weekday.local_minute,
- sch_hour, sch_minute
- );
-
- if (sch_type == SCHEDULER_TYPE_SWITCH && sch_switch == target && sch_action != 2 && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
- minimum_restore_time = minutes_to_trigger;
- saved_action = sch_action;
- saved_sch = i;
- }
-
- #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
- if (SCHEDULER_TYPE_DIM == sch_type && sch_switch == target && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
- minimum_restore_time = minutes_to_trigger;
- saved_action = sch_action;
- saved_sch = i;
- }
- #endif
-
- #if CURTAIN_SUPPORT == 1
- if (SCHEDULER_TYPE_CURTAIN == sch_type && sch_switch == target && minutes_to_trigger < 0 && minutes_to_trigger > minimum_restore_time) {
- minimum_restore_time = minutes_to_trigger;
- saved_action = sch_action;
- saved_sch = i;
- }
- #endif
-
- if (minutes_to_trigger == 0 && target == -1) {
-
- _schAction(i, sch_action, sch_switch);
- DEBUG_MSG_P(PSTR("[SCH] Schedule #%u TRIGGERED!!\n"), i);
-
- // Show minutes to trigger every 15 minutes
- // or every minute if less than 15 minutes to scheduled time.
- // This only works for schedules on this same day.
- // For instance, if your scheduler is set for 00:01 you will only
- // get one notification before the trigger (at 00:00)
- } else if (minutes_to_trigger > 0 && target == -1) {
-
- #if DEBUG_SUPPORT
- if ((minutes_to_trigger % 15 == 0) || (minutes_to_trigger < 15)) {
- DEBUG_MSG_P(
- PSTR("[SCH] %d minutes to trigger schedule #%u\n"),
- minutes_to_trigger, i
- );
- }
- #endif
-
- }
-
- }
-
- }
-
- if (daybefore >= 0 && daybefore < 7 && minimum_restore_time == -(60 * 24) && saved_action == -1) {
- _schCheck(target, ++daybefore);
- return;
- }
-
- if (minimum_restore_time != -(60 * 24) && saved_action != -1 && saved_sch != -1) {
- _schAction(saved_sch, saved_action, target);
- }
-
- }
-
- // -----------------------------------------------------------------------------
-
- void schSetup() {
-
- _schConfigure();
-
- #if WEB_SUPPORT
- wsRegister()
- .onVisible(_schWebSocketOnVisible)
- .onConnected(_schWebSocketOnConnected)
- .onKeyCheck(_schWebSocketOnKeyCheck);
- #endif
-
- static bool restore_once = true;
- ntpOnTick([](NtpTick tick) {
- switch (tick) {
- case NtpTick::EveryHour:
- return;
- case NtpTick::EveryMinute:
- if (restore_once) {
- auto targets = schedulableCount();
- for (size_t i = 0; i < targets; i++) {
- if (getSetting({"schRestore", i}, scheduler::build::restoreLast())) {
- _schCheck(i, 0);
- }
- }
- }
- restore_once = false;
- }
- _schCheck(-1, -1);
- });
-
- espurnaRegisterReload(_schConfigure);
-
- }
-
- #endif // SCHEDULER_SUPPORT
|