Mirror of espurna firmware for wireless switches and more
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.
 
 
 
 
 
 

1468 lines
31 KiB

/*
SCHEDULER MODULE
Copyright (C) 2017 by faina09
Adapted by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2019-2024 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
*/
#include "espurna.h"
#if SCHEDULER_SUPPORT
#include "api.h"
#include "curtain_kingart.h"
#include "datetime.h"
#include "mqtt.h"
#include "ntp.h"
#include "ntp_timelib.h"
#include "scheduler.h"
#include "types.h"
#include "ws.h"
#if TERMINAL_SUPPORT == 0
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
#include "light.h"
#endif
#if RELAY_SUPPORT
#include "relay.h"
#endif
#endif
#include "libs/EphemeralPrint.h"
#include "libs/PrintString.h"
// -----------------------------------------------------------------------------
#include "scheduler_common.ipp"
#include "scheduler_time.re.ipp"
#if SCHEDULER_SUN_SUPPORT
#include "scheduler_sun.ipp"
#endif
namespace espurna {
namespace scheduler {
// TODO recurrent in addition to calendar?
enum class Type : int {
Unknown = 0,
Disabled,
Calendar,
};
namespace v1 {
enum class Type : int {
None = 0,
Relay,
Channel,
Curtain,
};
} // namespace v1
namespace {
bool initial { true };
#if SCHEDULER_SUN_SUPPORT
namespace sun {
// scheduler itself has minutes precision, while seconds are used in debug and calculations
struct Event {
datetime::Minutes minutes{ -1 };
datetime::Seconds seconds{ -1 };
};
datetime::Seconds event_seconds(const Event& event) {
return std::chrono::duration_cast<datetime::Seconds>(event.minutes) + event.seconds;
}
bool event_valid(const Event& event) {
return (event.minutes > datetime::Minutes::zero())
&& (event.seconds > datetime::Seconds::zero());
}
struct EventMatch {
datetime::Date date;
TimeMatch time;
Event last;
};
struct Match {
EventMatch rising;
EventMatch setting;
};
Location location;
Match match;
void setup();
void reset() {
match.rising = EventMatch{};
match.setting = EventMatch{};
}
} // namespace sun
#endif
namespace build {
constexpr size_t max() {
return SCHEDULER_MAX_SCHEDULES;
}
constexpr Type type() {
return Type::Unknown;
}
constexpr bool restore() {
return 1 == SCHEDULER_RESTORE;
}
constexpr int restoreDays() {
return SCHEDULER_RESTORE_DAYS;
}
#if SCHEDULER_SUN_SUPPORT
constexpr double latitude() {
return SCHEDULER_LATITUDE;
}
constexpr double longitude() {
return SCHEDULER_LONGITUDE;
}
constexpr double altitude() {
return SCHEDULER_ALTITUDE;
}
#endif
} // namespace build
namespace settings {
namespace internal {
using espurna::settings::options::Enumeration;
STRING_VIEW_INLINE(Unknown, "unknown");
STRING_VIEW_INLINE(Disabled, "disabled");
STRING_VIEW_INLINE(Calendar, "calendar");
static constexpr std::array<Enumeration<Type>, 3> Types PROGMEM {
{{Type::Unknown, Unknown},
{Type::Disabled, Disabled},
{Type::Calendar, Calendar}}
};
namespace v1 {
STRING_VIEW_INLINE(None, "none");
STRING_VIEW_INLINE(Relay, "relay");
STRING_VIEW_INLINE(Channel, "channel");
STRING_VIEW_INLINE(Curtain, "curtain");
static constexpr std::array<Enumeration<scheduler::v1::Type>, 4> Types PROGMEM {
{{scheduler::v1::Type::None, None},
{scheduler::v1::Type::Relay, Relay},
{scheduler::v1::Type::Channel, Channel},
{scheduler::v1::Type::Curtain, Curtain}}
};
} // namespace v1
} // namespace internal
} // namespace settings
} // namespace
} // namespace scheduler
namespace settings {
namespace internal {
template <>
scheduler::Type convert(const String& value) {
return convert(scheduler::settings::internal::Types, value, scheduler::Type::Unknown);
}
String serialize(scheduler::Type type) {
return serialize(scheduler::settings::internal::Types, type);
}
template <>
scheduler::v1::Type convert(const String& value) {
return convert(scheduler::settings::internal::v1::Types, value, scheduler::v1::Type::None);
}
String serialize(scheduler::v1::Type type) {
return serialize(scheduler::settings::internal::v1::Types, type);
}
} // namespace internal
} // namespace settings
} // namespace espurna
namespace espurna {
namespace scheduler {
namespace {
bool tryParseId(StringView value, size_t& out) {
return ::tryParseId(value, build::max(), out);
}
namespace settings {
STRING_VIEW_INLINE(Prefix, "sch");
namespace keys {
#if SCHEDULER_SUN_SUPPORT
STRING_VIEW_INLINE(Latitude, "schLat");
STRING_VIEW_INLINE(Longitude, "schLong");
STRING_VIEW_INLINE(Altitude, "schAlt");
#endif
STRING_VIEW_INLINE(Days, "schRstrDays");
STRING_VIEW_INLINE(Type, "schType");
STRING_VIEW_INLINE(Restore, "schRestore");
STRING_VIEW_INLINE(Time, "schTime");
STRING_VIEW_INLINE(Action, "schAction");
} // namespace keys
#if SCHEDULER_SUN_SUPPORT
double latitude() {
return getSetting(keys::Latitude, build::latitude());
}
double longitude() {
return getSetting(keys::Longitude, build::longitude());
}
double altitude() {
return getSetting(keys::Altitude, build::altitude());
}
#endif
int restoreDays() {
return getSetting(keys::Days, build::restoreDays());
}
Type type(size_t index) {
return getSetting({keys::Type, index}, build::type());
}
bool restore(size_t index) {
return getSetting({keys::Restore, index}, build::restore());
}
String time(size_t index) {
return getSetting({keys::Time, index});
}
String action(size_t index) {
return getSetting({keys::Action, index});
}
namespace internal {
#define ID_VALUE(NAME, FUNC)\
String NAME (size_t id) {\
return espurna::settings::internal::serialize(FUNC(id));\
}
ID_VALUE(type, settings::type)
ID_VALUE(restore, settings::restore)
#undef ID_VALUE
#define EXACT_VALUE(NAME, FUNC)\
String NAME () {\
return espurna::settings::internal::serialize(FUNC());\
}
EXACT_VALUE(restoreDays, settings::restoreDays);
#if SCHEDULER_SUN_SUPPORT
EXACT_VALUE(latitude, settings::latitude);
EXACT_VALUE(longitude, settings::longitude);
EXACT_VALUE(altitude, settings::altitude);
#endif
#undef EXACT_VALUE
} // namespace internal
static constexpr espurna::settings::query::Setting Settings[] PROGMEM {
{keys::Days, internal::restoreDays},
#if SCHEDULER_SUN_SUPPORT
{keys::Latitude, internal::latitude},
{keys::Longitude, internal::longitude},
{keys::Altitude, internal::altitude},
#endif
};
static constexpr espurna::settings::query::IndexedSetting IndexedSettings[] PROGMEM {
{keys::Type, internal::type},
{keys::Restore, internal::restore},
{keys::Action, settings::action},
{keys::Time, settings::time},
};
struct Parsed {
bool date { false };
bool weekdays { false };
bool time { false };
};
Schedule schedule(size_t index) {
Schedule out;
bool parsed_date { false };
bool parsed_weekdays { false };
bool parsed_time { false };
const auto time = settings::time(index);
auto split = SplitStringView(time);
while (split.next()) {
auto elem = split.current();
// most expected order, starting with date
if (!parsed_date && ((parsed_date = parse_date(out.date, elem)))) {
continue;
}
// then weekdays
if (!parsed_weekdays && ((parsed_weekdays = parse_weekdays(out.weekdays, elem)))) {
continue;
}
// then time
if (!parsed_time && ((parsed_time = parse_time(out.time, elem)))) {
continue;
}
// and keyword is always at the end. forcibly stop the parsing, regardless of the state
if (parse_time_keyword(out.time, elem)) {
if (want_utc(out.time)) {
break;
}
#if SCHEDULER_SUN_SUPPORT
// do not want both time and sun{rise,set}
if (want_sunrise_sunset(out.time)) {
parsed_time = !parsed_time;
}
#endif
break;
}
}
out.ok = parsed_date
|| parsed_weekdays
|| parsed_time;
return out;
}
size_t count() {
size_t out { 0 };
for (size_t index = 0; index < build::max(); ++index) {
const auto type = settings::type(index);
if (type == Type::Unknown) {
break;
}
++out;
}
return out;
}
void gc(size_t total) {
DEBUG_MSG_P(PSTR("[SCH] Registered %zu schedule(s)\n"), total);
for (size_t index = total; index < build::max(); ++index) {
for (auto setting : IndexedSettings) {
delSetting({setting.prefix(), index});
}
}
}
bool checkSamePrefix(StringView key) {
return key.startsWith(settings::Prefix);
}
espurna::settings::query::Result findFrom(StringView key) {
return espurna::settings::query::findFrom(Settings, key);
}
void setup() {
::settingsRegisterQueryHandler({
.check = checkSamePrefix,
.get = findFrom,
});
}
} // namespace settings
namespace v1 {
using scheduler::v1::Type;
namespace settings {
namespace keys {
STRING_VIEW_INLINE(Enabled, "schEnabled");
STRING_VIEW_INLINE(Switch, "schSwitch");
STRING_VIEW_INLINE(Target, "schTarget");
STRING_VIEW_INLINE(Hour, "schHour");
STRING_VIEW_INLINE(Minute, "schMinute");
STRING_VIEW_INLINE(Weekdays, "schWDs");
STRING_VIEW_INLINE(UTC, "schUTC");
static constexpr std::array<StringView, 5> List {
Enabled,
Target,
Hour,
Minute,
Weekdays,
};
} // namespace keys
STRING_VIEW_INLINE(DefaultWeekdays, "1,2,3,4,5,6,7");
bool enabled(size_t index) {
return getSetting({keys::Enabled, index}, false);
}
Type type(size_t index) {
return getSetting({espurna::scheduler::settings::keys::Type, index}, Type::None);
}
int target(size_t index) {
return getSetting({keys::Target, index}, 0);
}
int action(size_t index) {
using namespace espurna::scheduler::settings::keys;
return getSetting({Action, index}, 0);
}
int hour(size_t index) {
return getSetting({keys::Hour, index}, 0);
}
int minute(size_t index) {
return getSetting({keys::Minute, index}, 0);
}
String weekdays(size_t index) {
return getSetting({keys::Weekdays, index}, DefaultWeekdays);
}
bool utc(size_t index) {
return getSetting({keys::UTC, index}, false);
}
} // namespace settings
String convert_time(const String& weekdays, int hour, int minute, bool utc) {
String out;
// implicit mon..sun already by default
if (weekdays != settings::DefaultWeekdays) {
out += weekdays;
out += ' ';
}
if (hour < 10) {
out += '0';
}
out += String(hour, 10);
out += ':';
if (minute < 10) {
out += '0';
}
out += String(minute, 10);
if (utc) {
out += STRING_VIEW(" UTC");
}
return out;
}
String convert_action(Type type, int target, int action) {
String out;
StringView prefix;
switch (type) {
case Type::None:
break;
case Type::Relay:
{
STRING_VIEW_INLINE(Relay, "relay");
prefix = Relay;
break;
}
case Type::Channel:
{
STRING_VIEW_INLINE(Channel, "channel");
prefix = Channel;
break;
}
case Type::Curtain:
{
STRING_VIEW_INLINE(Curtain, "curtain");
prefix = Curtain;
break;
}
}
if (prefix.length()) {
out += prefix.toString()
+ ' ';
out += String(target, 10)
+ ' '
+ String(action, 10);
}
return out;
}
String convert_type(bool enabled, Type type) {
auto out = scheduler::Type::Unknown;
switch (type) {
case Type::None:
break;
case Type::Relay:
case Type::Channel:
case Type::Curtain:
out = scheduler::Type::Calendar;
break;
}
if (!enabled && (out != scheduler::Type::Unknown)) {
out = scheduler::Type::Disabled;
}
return ::espurna::settings::internal::serialize(out);
}
void migrate() {
for (size_t index = 0; index < build::max(); ++index) {
const auto type = settings::type(index);
setSetting({scheduler::settings::keys::Type, index},
convert_type(settings::enabled(index), type));
setSetting({scheduler::settings::keys::Time, index},
convert_time(settings::weekdays(index),
settings::hour(index),
settings::minute(index),
settings::utc(index)));
setSetting({scheduler::settings::keys::Action, index},
convert_action(type,
settings::target(index),
settings::action(index)));
for (auto& key : settings::keys::List) {
delSetting({key, index});
}
}
}
} // namespace v1
namespace settings {
void migrate(int version) {
if (version < 6) {
moveSettings(
v1::settings::keys::Switch.toString(),
v1::settings::keys::Target.toString());
}
if (version < 15) {
v1::migrate();
}
}
} // namespace settings
#if SCHEDULER_SUN_SUPPORT
namespace sun {
STRING_VIEW_INLINE(Module, "sun");
void setup() {
location.latitude = settings::latitude();
location.longitude = settings::longitude();
location.altitude = settings::altitude();
}
EventMatch* find_event_match(const TimeMatch& m) {
if (want_sunrise(m)) {
return &match.rising;
} else if (want_sunset(m)) {
return &match.setting;
}
return nullptr;
}
EventMatch* find_event_match(const Schedule& schedule) {
return find_event_match(schedule.time);
}
tm time_point_from_seconds(datetime::Seconds seconds) {
tm out{};
time_t timestamp{ seconds.count() };
gmtime_r(&timestamp, &out);
return out;
}
Event make_invalid_event() {
Event out;
out.seconds = datetime::Seconds{ -1 };
out.minutes = datetime::Minutes{ -1 };
return out;
}
Event make_event(datetime::Seconds seconds) {
Event out;
out.seconds = seconds;
out.minutes =
std::chrono::duration_cast<datetime::Minutes>(out.seconds);
out.seconds -= out.minutes;
return out;
}
datetime::Date date_point(const tm& time_point) {
datetime::Date out;
out.year = time_point.tm_year + 1900;
out.month = time_point.tm_mon + 1;
out.day = time_point.tm_mday;
return out;
}
TimeMatch time_match(const tm& time_point) {
TimeMatch out;
out.hour[time_point.tm_hour] = true;
out.minute[time_point.tm_min] = true;
out.flags = FlagUtc;
return out;
}
void update_event_match(EventMatch& match, datetime::Seconds seconds) {
if (seconds <= datetime::Seconds::zero()) {
match.last = make_invalid_event();
return;
}
const auto time_point = time_point_from_seconds(seconds);
match.date = date_point(time_point);
match.time = time_match(time_point);
match.last = make_event(seconds);
}
void update_schedule_from(Schedule& schedule, const EventMatch& match) {
schedule.date.day[match.date.day] = true;
schedule.date.month[match.date.month] = true;
schedule.date.year = match.date.year;
schedule.time = match.time;
}
bool update_schedule(Schedule& schedule) {
// if not sun{rise,set} schedule, keep it as-is
const auto* selected = sun::find_event_match(schedule);
if (nullptr == selected) {
return false;
}
// in case calculation failed, no use here
if (!event_valid((*selected).last)) {
return false;
}
// make sure event can actually trigger with this date spec
if (::espurna::scheduler::match(schedule.date, (*selected).date)) {
update_schedule_from(schedule, *selected);
return true;
}
return false;
}
bool needs_update(datetime::Minutes minutes) {
return ((match.rising.last.minutes < minutes)
|| (match.setting.last.minutes < minutes));
}
template <typename T>
void delta_compare(tm& out, datetime::Minutes, T);
void update(datetime::Minutes minutes, const tm& today) {
const auto result = sun::sunrise_sunset(location, today);
update_event_match(match.rising, result.sunrise);
update_event_match(match.setting, result.sunset);
}
template <typename T>
void update(datetime::Minutes minutes, const tm& today, T compare) {
auto result = sun::sunrise_sunset(location, today);
if ((result.sunrise.count() < 0) || (result.sunset.count() < 0)) {
return;
}
if (compare(minutes, result.sunrise) || compare(minutes, result.sunset)) {
tm tmp;
std::memcpy(&tmp, &today, sizeof(tmp));
delta_compare(tmp, minutes, compare);
const auto other = sun::sunrise_sunset(location, tmp);
if ((other.sunrise.count() < 0) || (other.sunset.count() < 0)) {
return;
}
if (compare(minutes, result.sunrise)) {
result.sunrise = other.sunrise;
}
if (compare(minutes, result.sunset)) {
result.sunset = other.sunset;
}
}
update_event_match(match.rising, result.sunrise);
update_event_match(match.setting, result.sunset);
}
template <typename T>
void update(time_t timestamp, const tm& today, T&& compare) {
update(datetime::Seconds{ timestamp }, today, std::forward<T>(compare));
}
String format_match(const EventMatch& match) {
return datetime::format_local(event_seconds(match.last).count());
}
// check() needs current or future events, discard timestamps in the past
// std::greater is type-fixed, make sure minutes vs. seconds actually works
struct CheckCompare {
bool operator()(const datetime::Minutes& lhs, const datetime::Seconds& rhs) {
return lhs > rhs;
}
};
template <>
void delta_compare(tm& out, datetime::Minutes minutes, CheckCompare) {
datetime::delta_utc(
out, datetime::Seconds{ minutes },
datetime::Days{ 1 });
}
void update_after(const datetime::Context& ctx) {
const auto seconds = datetime::Seconds{ ctx.timestamp };
const auto minutes =
std::chrono::duration_cast<datetime::Minutes>(seconds);
if (!needs_update(minutes)) {
return;
}
update(minutes, ctx.utc, CheckCompare{});
if (match.rising.last.minutes.count() > 0) {
DEBUG_MSG_P(PSTR("[SCH] Sunrise at %s\n"),
format_match(match.rising).c_str());
}
if (match.setting.last.minutes.count() > 0) {
DEBUG_MSG_P(PSTR("[SCH] Sunset at %s\n"),
format_match(match.setting).c_str());
}
}
} // namespace sun
#endif
// -----------------------------------------------------------------------------
#if TERMINAL_SUPPORT
namespace terminal {
#if SCHEDULER_SUN_SUPPORT
namespace internal {
String sunrise_sunset(const sun::EventMatch& match) {
if (match.last.minutes > datetime::Minutes::zero()) {
return sun::format_match(match);
}
return STRING_VIEW("value not set").toString();
}
void format_output(::terminal::CommandContext& ctx, const String& prefix, const String& value) {
ctx.output.printf_P(PSTR("%s%s%s\n"),
prefix.c_str(),
value.length()
? PSTR(" at ")
: " ",
value.c_str());
}
void dump_sunrise_sunset(::terminal::CommandContext& ctx) {
format_output(ctx,
STRING_VIEW("Sunrise").toString(),
sunrise_sunset(sun::match.rising));
format_output(ctx,
STRING_VIEW("Sunset").toString(),
sunrise_sunset(sun::match.setting));
}
} // namespace internal
#endif
PROGMEM_STRING(Dump, "SCHEDULE");
void dump(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 2) {
#if SCHEDULER_SUN_SUPPORT
internal::dump_sunrise_sunset(ctx);
#endif
settingsDump(ctx, settings::Settings);
return;
}
size_t id;
if (!tryParseId(ctx.argv[1], id)) {
terminalError(ctx, F("Invalid ID"));
return;
}
settingsDump(ctx, settings::IndexedSettings, id);
terminalOK(ctx);
}
static constexpr ::terminal::Command Commands[] PROGMEM {
{Dump, dump},
};
void setup() {
espurna::terminal::add(Commands);
}
} // namespace terminal
#endif
// -----------------------------------------------------------------------------
#if API_SUPPORT
namespace api {
namespace keys {
STRING_VIEW_INLINE(Type, "type");
STRING_VIEW_INLINE(Restore, "restore");
STRING_VIEW_INLINE(Time, "time");
STRING_VIEW_INLINE(Action, "action");
} // namespace keys
using espurna::settings::internal::serialize;
using espurna::settings::internal::convert;
struct Schedule {
size_t id;
Type type;
int restore;
String time;
String action;
};
void print(JsonObject& root, const Schedule& schedule) {
root[keys::Type] = serialize(schedule.type);
root[keys::Restore] = (1 == schedule.restore);
root[keys::Action] = schedule.action;
root[keys::Time] = schedule.time;
}
template <typename T>
bool set_typed(T& out, JsonObject& root, StringView key) {
auto value = root[key];
if (value.success()) {
out = value.as<T>();
return true;
}
return false;
}
template <>
bool set_typed<Type>(Type& out, JsonObject& root, StringView key) {
auto value = root[key];
if (!value.success()) {
return false;
}
auto type = convert<Type>(value.as<String>());
if (type != Type::Unknown) {
out = type;
return true;
}
return false;
}
template
bool set_typed<String>(String&, JsonObject&, StringView);
template
bool set_typed<bool>(bool&, JsonObject&, StringView);
void update_from(const Schedule& schedule) {
setSetting({keys::Type, schedule.id}, serialize(schedule.type));
setSetting({keys::Time, schedule.id}, schedule.time);
setSetting({keys::Action, schedule.id}, schedule.action);
if (schedule.restore != -1) {
setSetting({keys::Restore, schedule.id}, serialize(1 == schedule.restore));
}
}
bool set(JsonObject& root, const size_t id) {
Schedule out;
out.restore = -1;
// always need type, time and action
if (!set_typed(out.type, root, keys::Type)) {
return false;
}
if (!set_typed(out.time, root, keys::Time)) {
return false;
}
if (!set_typed(out.action, root, keys::Action)) {
return false;
}
// optional restore flag
bool restore;
if (set_typed(restore, root, keys::Restore)) {
out.restore = restore ? 1 : 0;
}
update_from(out);
return true;
}
Schedule make_schedule(size_t id) {
Schedule out;
out.type = settings::type(id);
if (out.type != Type::Unknown) {
out.id = id;
out.restore = settings::restore(id) ? 1 : 0;
out.time = settings::time(id);
out.action = settings::action(id);
}
return out;
}
namespace schedules {
bool get(ApiRequest&, JsonObject& root) {
JsonArray& out = root.createNestedArray("schedules");
for (size_t id = 0; id < build::max(); ++id) {
const auto sch = make_schedule(id);
if (sch.type == Type::Unknown) {
break;
}
auto& root = out.createNestedObject();
print(root, sch);
}
return true;
}
bool set(ApiRequest&, JsonObject& root) {
size_t id = 0;
while (hasSetting({settings::keys::Type, id})) {
++id;
}
if (id < build::max()) {
return api::set(root, id);
}
return false;
}
} // namespace schedules
namespace schedule {
bool get(ApiRequest& req, JsonObject& root) {
const auto param = req.wildcard(0);
size_t id;
if (tryParseId(param, id)) {
const auto sch = make_schedule(id);
if (sch.type == Type::Unknown) {
return false;
}
print(root, sch);
return true;
}
return false;
}
bool set(ApiRequest& req, JsonObject& root) {
const auto param = req.wildcard(0);
size_t id;
if (tryParseId(param, id)) {
return api::set(root, id);
}
return false;
}
} // namespace schedule
void setup() {
apiRegister(F(MQTT_TOPIC_SCHEDULE), schedules::get, schedules::set);
apiRegister(F(MQTT_TOPIC_SCHEDULE "/+"), schedule::get, schedule::set);
}
} // namespace api
#endif // API_SUPPORT
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
namespace web {
bool onKey(StringView key, const JsonVariant&) {
return key.startsWith(settings::Prefix);
}
void onVisible(JsonObject& root) {
wsPayloadModule(root, settings::Prefix);
#if SCHEDULER_SUN_SUPPORT
wsPayloadModule(root, sun::Module);
#endif
for (const auto& pair : settings::Settings) {
root[pair.key()] = pair.value();
}
}
void onConnected(JsonObject& root){
espurna::web::ws::EnumerableConfig config{ root, STRING_VIEW("schConfig") };
config(STRING_VIEW("schedules"), settings::count(), settings::IndexedSettings);
auto& schedules = config.root();
schedules["max"] = build::max();
}
void setup() {
wsRegister()
.onVisible(onVisible)
.onConnected(onConnected)
.onKeyCheck(onKey);
}
} // namespace web
#endif
// When terminal is disabled, still allow minimum set of actions that we available in v1
#if TERMINAL_SUPPORT == 0
namespace terminal_stub {
#if RELAY_SUPPORT
namespace relay {
void action(SplitStringView split) {
if (!split.next()) {
return;
}
size_t id;
if (!::tryParseId(split.current(), relayCount(), id)) {
return;
}
if (!split.next()) {
return;
}
const auto status = relayParsePayload(split.current());
switch (status) {
case PayloadStatus::Unknown:
break;
case PayloadStatus::Off:
case PayloadStatus::On:
relayStatus(id, (status == PayloadStatus::On));
break;
case PayloadStatus::Toggle:
relayToggle(id);
break;
}
}
} // namespace relay
#endif
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
namespace light {
void action(SplitStringView split) {
if (!split.next()) {
return;
}
size_t id;
if (!tryParseId(split.current(), lightChannels(), id)) {
return;
}
if (!split.next()) {
return;
}
const auto convert = ::espurna::settings::internal::convert<long>;
lightChannel(id, convert(split.current().toString()));
lightUpdate();
}
} // namespace light
#endif
#if CURTAIN_SUPPORT
namespace curtain {
void action(SplitStringView split) {
if (!split.next()) {
return;
}
size_t id;
if (!tryParseId(split.current(), curtainCount(), id)) {
return;
}
if (!split.next()) {
return;
}
const auto convert = ::espurna::settings::internal::convert<int>;
curtainUpdate(id, convert(split.current().toString()));
}
} // namespace curtain
#endif
void parse_action(String action) {
auto split = SplitStringView{ action };
if (!split.next()) {
return;
}
auto current = split.current();
#if RELAY_SUPPORT
if (current == STRING_VIEW("relay")) {
relay::action(split);
return;
}
#endif
#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE
if (current == STRING_VIEW("channel")) {
light::action(split);
return;
}
#endif
#if CURTAIN_SUPPORT
if (current == STRING_VIEW("curtain")) {
curtain::action(split);
return;
}
#endif
DEBUG_MSG_P(PSTR("[SCH] Unknown action: %s\n"), action.c_str());
}
} // namespace terminal_stub
using terminal_stub::parse_action;
#else
void parse_action(String action) {
if (!action.endsWith("\r\n") && !action.endsWith("\n")) {
action.concat('\n');
}
static EphemeralPrint output;
PrintString error(64);
if (!espurna::terminal::api_find_and_call(action, output, error)) {
DEBUG_MSG_P(PSTR("[SCH] %s\n"), error.c_str());
}
}
#endif
namespace restore {
[[gnu::used]]
void Context::init() {
#if SCHEDULER_SUN_SUPPORT
const auto seconds = datetime::Seconds{ this->current.timestamp };
const auto minutes =
std::chrono::duration_cast<datetime::Minutes>(seconds);
sun::update(minutes, this->current.utc);
#endif
}
[[gnu::used]]
void Context::init_delta() {
#if SCHEDULER_SUN_SUPPORT
init();
for (auto& pending : this->pending) {
// additional logic in handle_delta. keeps as pending when current value does not pass date match()
pending.schedule.ok =
sun::update_schedule(pending.schedule);
}
#endif
}
[[gnu::used]]
void Context::destroy() {
#if SCHEDULER_SUN_SUPPORT
sun::reset();
#endif
}
// otherwise, there are pending results that need extra days to check
void run_delta(Context& ctx) {
if (!ctx.pending.size()) {
return;
}
const auto days = settings::restoreDays();
for (int day = 0; day < days; ++day) {
if (!ctx.next()) {
break;
}
for (auto it = ctx.pending.begin(); it != ctx.pending.end();) {
if (handle_delta(ctx, *it)) {
it = ctx.pending.erase(it);
} else {
it = std::next(it);
}
}
}
}
// if schedule was due earlier today, make sure this gets checked first
void run_today(Context& ctx) {
for (size_t index = 0; index < build::max(); ++index) {
switch (settings::type(index)) {
case Type::Unknown:
return;
case Type::Disabled:
continue;
case Type::Calendar:
break;
}
if (!settings::restore(index)) {
continue;
}
auto schedule = settings::schedule(index);
if (!schedule.ok) {
continue;
}
#if SCHEDULER_SUN_SUPPORT
if (!sun::update_schedule(schedule)) {
context_pending(ctx, index, schedule);
continue;
}
#endif
handle_today(ctx, index, schedule);
}
}
void sort(Context& ctx) {
std::sort(
ctx.results.begin(),
ctx.results.end(),
[](const Result& lhs, const Result& rhs) {
return lhs.offset < rhs.offset;
});
}
void run(const datetime::Context& base) {
Context ctx{ base };
run_today(ctx);
run_delta(ctx);
sort(ctx);
for (auto& result : ctx.results) {
const auto action = settings::action(result.index);
DEBUG_MSG_P(PSTR("[SCH] Restoring #%zu => %s (%sm)\n"),
result.index, action.c_str(),
String(result.offset.count(), 10).c_str());
parse_action(action);
}
}
} // namespace restore
void check(const datetime::Context& ctx) {
for (size_t index = 0; index < build::max(); ++index) {
switch (settings::type(index)) {
case Type::Unknown:
return;
case Type::Disabled:
continue;
case Type::Calendar:
break;
}
auto schedule = settings::schedule(index);
if (!schedule.ok) {
continue;
}
#if SCHEDULER_SUN_SUPPORT
if (!sun::update_schedule(schedule)) {
continue;
}
#endif
const auto& time = select_time(ctx, schedule);
if (!match(schedule.date, time)) {
continue;
}
if (!match(schedule.weekdays, time)) {
continue;
}
if (!match(schedule.time, time)) {
continue;
}
DEBUG_MSG_P(PSTR("[SCH] Action #%zu triggered\n"), index);
parse_action(settings::action(index));
}
}
void tick(NtpTick tick) {
if (tick != NtpTick::EveryMinute) {
return;
}
auto ctx = datetime::make_context(now());
if (initial) {
initial = false;
settings::gc(settings::count());
restore::run(ctx);
}
#if SCHEDULER_SUN_SUPPORT
sun::update_after(ctx);
#endif
check(ctx);
}
void setup() {
migrateVersion(scheduler::settings::migrate);
settings::setup();
#if SCHEDULER_SUN_SUPPORT
sun::setup();
#endif
#if TERMINAL_SUPPORT
terminal::setup();
#endif
#if WEB_SUPPORT
web::setup();
#endif
#if API_SUPPORT
api::setup();
#endif
ntpOnTick(tick);
}
} // namespace
} // namespace scheduler
} // namespace espurna
// -----------------------------------------------------------------------------
void schSetup() {
espurna::scheduler::setup();
}
#endif // SCHEDULER_SUPPORT