Files
TimbreESP32/lib/timbre_core/Scheduler.cpp
Angel Ivan 2a9eabce14 Fist commit
2026-10-02 12:08:21 -06:00

124 lines
4.1 KiB
C++
Executable File

#include "Scheduler.h"
#include "ScheduleValidator.h"
#include <limits>
namespace timbre_core {
void Scheduler::reset() noexcept {
hasLastKey_ = false;
lastKey_ = LocalMinuteKey{};
}
bool Scheduler::isHoliday(const AppState& state, std::uint8_t month,
std::uint8_t day) noexcept {
for (std::size_t i = 0; i < state.holidays.size(); ++i) {
const Holiday& holiday = state.holidays[i];
if (holiday.enabled && holiday.month == month && holiday.day == day) {
return true;
}
}
return false;
}
bool Scheduler::scheduleMatches(const Schedule& schedule, const AppState& state,
const CivilDateTime& localTime) noexcept {
if (!schedule.enabled || schedule.minuteOfDay != localTime.minuteOfDay()) {
return false;
}
const Profile* activeProfile = state.profile(state.activeProfileId);
if (activeProfile == nullptr || !activeProfile->enabled ||
schedule.profileId != state.activeProfileId) {
return false;
}
const std::uint8_t weekday = CivilTime::weekdayForDate(localTime.year, localTime.month,
localTime.day);
if ((schedule.weekdayMask & static_cast<std::uint8_t>(1u << weekday)) == 0) {
return false;
}
if (isHoliday(state, localTime.month, localTime.day)) {
return false;
}
const Pattern* pattern = state.pattern(schedule.patternId);
return pattern != nullptr && isPatternId(schedule.patternId) &&
ScheduleValidator::validatePattern(*pattern).valid;
}
ScheduleDecision Scheduler::poll(const AppState& state,
const CivilDateTime& localTime,
bool firstObservation) {
ScheduleDecision decision;
decision.firstObservation = firstObservation;
if (!localTime.valid()) {
decision.invalidTime = true;
return decision;
}
const LocalMinuteKey currentKey = LocalMinuteKey::from(localTime);
const std::int64_t currentSerial = currentKey.serial();
if (currentSerial == std::numeric_limits<std::int64_t>::min()) {
decision.invalidTime = true;
return decision;
}
if (hasLastKey_) {
const std::int64_t previousSerial = lastKey_.serial();
if (currentSerial <= previousSerial) {
decision.alreadyProcessed = true;
return decision;
}
const std::int64_t distance = currentSerial - previousSerial;
decision.missedMinutes = distance > 1 ? static_cast<std::uint64_t>(distance - 1) : 0;
}
// Consume the current key before evaluating matches. Therefore a failed
// lookup (holiday, disabled profile, etc.) cannot be replayed later.
lastKey_ = currentKey;
hasLastKey_ = true;
if (firstObservation) {
return decision;
}
if (isHoliday(state, localTime.month, localTime.day)) {
decision.blockedByHoliday = true;
return decision;
}
const Schedule* selected = nullptr;
for (std::size_t i = 0; i < state.schedules.size(); ++i) {
const Schedule& candidate = state.schedules[i];
if (!scheduleMatches(candidate, state, localTime)) {
continue;
}
// Stable IDs, rather than array positions, define deterministic
// precedence when two schedules intentionally share a minute.
if (selected == nullptr || candidate.id < selected->id) {
selected = &candidate;
}
}
if (selected != nullptr) {
decision.due = true;
decision.scheduleId = selected->id;
decision.patternId = selected->patternId;
}
return decision;
}
ScheduleDecision Scheduler::poll(const AppState& state,
SessionClock& clock,
std::int64_t utcEpochSeconds) {
const SessionClock::Observation observation = clock.observe(utcEpochSeconds);
if (!observation.valid) {
ScheduleDecision decision;
decision.invalidTime = true;
return decision;
}
return poll(state, observation.local, observation.firstObservation);
}
} // namespace timbre_core