Files
project-vesper/vesper/src/TimeKeeper/TimeKeeper.cpp

823 lines
30 KiB
C++

#include "TimeKeeper.hpp"
#include "../OutputManager/OutputManager.hpp"
#include "../ConfigManager/ConfigManager.hpp"
#include "../Networking/Networking.hpp"
#include "../Player/Player.hpp" // 🔥 Include for Player class definition
#include "SD.h"
#include <time.h>
void Timekeeper::begin() {
LOG_INFO("Timekeeper initialized - clock outputs managed by ConfigManager");
// Initialize RTC
if (!rtc.begin()) {
LOG_ERROR("Couldn't find RTC");
// Continue anyway, but log the error
} else {
LOG_INFO("RTC initialized successfully");
// Check if RTC lost power
if (!rtc.isrunning()) {
LOG_WARNING("RTC is NOT running! Setting time...");
// Set to compile time as fallback
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
}
// Load today's events
loadTodaysEvents();
// Create SINGLE consolidated task (saves 8KB RAM!)
xTaskCreatePinnedToCore(mainTimekeeperTask, "TimeKeeper", 4096, this, 2, &mainTaskHandle, 1);
LOG_INFO("TimeKeeper initialized with SIMPLE sync approach (like your Arduino code)");
}
void Timekeeper::setOutputManager(OutputManager* outputManager) {
_outputManager = outputManager;
LOG_INFO("Timekeeper connected to OutputManager - CLEAN ARCHITECTURE!");
}
void Timekeeper::setConfigManager(ConfigManager* configManager) {
_configManager = configManager;
LOG_INFO("Timekeeper connected to ConfigManager");
}
void Timekeeper::setNetworking(Networking* networking) {
_networking = networking;
LOG_INFO("Timekeeper connected to Networking");
}
void Timekeeper::setPlayer(Player* player) {
_player = player;
LOG_INFO("Timekeeper connected to Player for playback coordination");
}
void Timekeeper::interruptActiveAlert() {
if (alertInProgress.load()) {
LOG_INFO("⚡ ALERT INTERRUPTED by user playback - marking as complete");
alertInProgress.store(false);
// Alert will stop naturally on next check in fireAlertBell loop
}
}
void Timekeeper::setRelayWriteFunction(void (*func)(int, int)) {
relayWriteFunc = func;
LOG_WARNING("Using LEGACY relay function - consider upgrading to OutputManager");
}
void Timekeeper::setClockOutputs(int relay1, int relay2) {
LOG_WARNING("⚠️ setClockOutputs() is DEPRECATED! Use ConfigManager.setClockOutput1/2() instead");
LOG_WARNING("⚠️ Clock outputs should be configured via MQTT/WebSocket commands");
// For backward compatibility, still set the config if ConfigManager is available
if (_configManager) {
_configManager->setClockOutput1(relay1);
_configManager->setClockOutput2(relay2);
LOG_INFO("Clock outputs updated via legacy method: C1=%d, C2=%d", relay1, relay2);
} else {
LOG_ERROR("ConfigManager not available - cannot set clock outputs");
}
}
void Timekeeper::setTime(unsigned long timestamp) {
if (!rtc.begin()) {
LOG_ERROR("RTC not available - cannot set time");
return;
}
// Get timezone configuration
auto& timeConfig = _configManager->getTimeConfig();
// Apply timezone offset to UTC timestamp
long totalOffset = timeConfig.gmtOffsetSec + timeConfig.daylightOffsetSec;
unsigned long localTimestamp = timestamp + totalOffset;
// Convert local timestamp to DateTime object
DateTime newTime(localTimestamp);
// Set the RTC with local time
rtc.adjust(newTime);
LOG_INFO("RTC time set to LOCAL: %04d-%02d-%02d %02d:%02d:%02d (UTC timestamp: %lu + %ld offset = %lu)",
newTime.year(), newTime.month(), newTime.day(),
newTime.hour(), newTime.minute(), newTime.second(),
timestamp, totalOffset, localTimestamp);
// Reload today's events since the date might have changed
loadTodaysEvents();
}
void Timekeeper::setTimeWithLocalTimestamp(unsigned long localTimestamp) {
if (!rtc.begin()) {
LOG_ERROR("RTC not available - cannot set time");
return;
}
// Convert local timestamp directly to DateTime object (no timezone conversion needed)
DateTime newTime(localTimestamp);
// Set the RTC with local time
rtc.adjust(newTime);
LOG_INFO("RTC time set to LOCAL: %04d-%02d-%02d %02d:%02d:%02d (local timestamp: %lu)",
newTime.year(), newTime.month(), newTime.day(),
newTime.hour(), newTime.minute(), newTime.second(),
localTimestamp);
// Reload today's events since the date might have changed
loadTodaysEvents();
}
unsigned long Timekeeper::getTime() {
if (!rtc.isrunning()) {
LOG_ERROR("RTC not running - cannot get time");
return 0;
}
DateTime now = rtc.now();
unsigned long timestamp = now.unixtime();
LOG_DEBUG("Current RTC time: %04d-%02d-%02d %02d:%02d:%02d (timestamp: %lu)",
now.year(), now.month(), now.day(),
now.hour(), now.minute(), now.second(),
timestamp);
return timestamp;
}
void Timekeeper::syncTimeWithNTP() {
// Check if we have network connection and required dependencies
if (!_networking || !_configManager) {
LOG_WARNING("Cannot sync time: Networking or ConfigManager not set - using RTC time");
return;
}
if (!_networking->isConnected()) {
LOG_INFO("No network connection - skipping NTP sync, using RTC time");
return;
}
LOG_INFO("⏰ Starting non-blocking NTP sync...");
// Get config from ConfigManager
auto& timeConfig = _configManager->getTimeConfig();
// Configure NTP with settings from config
configTime(timeConfig.gmtOffsetSec, timeConfig.daylightOffsetSec, timeConfig.ntpServer.c_str());
// 🔥 NON-BLOCKING: Try to get time with reasonable timeout for network response
struct tm timeInfo;
if (getLocalTime(&timeInfo, 5000)) { // 5 second timeout for NTP response
// Success! Update RTC with synchronized time
rtc.adjust(DateTime(timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec));
LOG_INFO("✅ NTP sync successful: %04d-%02d-%02d %02d:%02d:%02d",
timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
// Reload today's events since the time might have changed significantly
loadTodaysEvents();
} else {
// No internet or NTP server unreachable - this is NORMAL for local networks
LOG_INFO("⚠️ NTP sync skipped (no internet) - using RTC time. This is normal for local networks.");
}
}
// ════════════════════════════════════════════════════════════════════════════
// CONSOLIDATED TimeKeeper Task - SIMPLE approach like your Arduino code
// ════════════════════════════════════════════════════════════════════════════
void Timekeeper::mainTimekeeperTask(void* parameter) {
Timekeeper* keeper = static_cast<Timekeeper*>(parameter);
LOG_INFO("🕒 SIMPLE TimeKeeper task started - based on your Arduino code approach");
unsigned long lastRtcCheck = 0;
unsigned long lastScheduleCheck = 0;
while (true) {
unsigned long now = millis();
// 🕐 SIMPLE PHYSICAL CLOCK SYNC (check every loop, fire if needed - your Arduino approach)
keeper->checkAndSyncPhysicalClock();
// 🔔 ALERT MANAGEMENT (every second for precise timing)
keeper->checkClockAlerts();
// 💡 BACKLIGHT AUTOMATION (every 10 seconds)
if (now - lastRtcCheck >= 10000) {
keeper->checkBacklightAutomation();
}
// 📅 SCHEDULE CHECK (every second)
if (now - lastScheduleCheck >= 1000) {
keeper->checkScheduledEvents();
lastScheduleCheck = now;
}
// 🔧 RTC MAINTENANCE (every 10 seconds)
if (now - lastRtcCheck >= 10000) {
// RTC health check
DateTime rtcNow = keeper->rtc.now();
if (keeper->rtc.isrunning()) {
// Check for midnight - reload events for new day
if (rtcNow.hour() == 0 && rtcNow.minute() == 0 && rtcNow.second() < 10) {
LOG_INFO("🌙 Midnight detected - reloading events");
keeper->loadTodaysEvents();
keeper->loadNextDayEvents();
}
// Hourly maintenance
if (rtcNow.minute() == 0 && rtcNow.second() < 10) {
LOG_DEBUG("🕐 Hourly check at %02d:00", rtcNow.hour());
}
} else {
static uint8_t rtcWarningCounter = 0;
if (rtcWarningCounter++ % 6 == 0) { // Log every minute
LOG_WARNING("⚠️ RTC not running!");
}
}
lastRtcCheck = now;
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// ════════════════════════════════════════════════════════════════════════════
// SIMPLE CLOCK SYNC IMPLEMENTATION - Based on your Arduino code
// ════════════════════════════════════════════════════════════════════════════
void Timekeeper::checkAndSyncPhysicalClock() {
// Check if clock is enabled in config
if (!_configManager || !_configManager->getClockEnabled()) {
return; // Clock is disabled - skip all clock functionality
}
// Check if there is any Time Difference between the Physical Clock and the Actual Time and if yes, run the Motor
if (!_outputManager || !rtc.isrunning() || clockUpdatesPaused) {
return;
}
// Get current real time (your updateCurrentTime() equivalent)
DateTime now = rtc.now();
int8_t realHour = now.hour() % 12; // Convert to 12-hour format for clock face
if (realHour == 0) realHour = 12; // 12 AM/PM shows as 12, not 0
int8_t realMinute = now.minute();
// Get physical clock state (your clock.hour/clock.minute equivalent)
int8_t physicalHour = _configManager->getPhysicalClockHour();
int8_t physicalMinute = _configManager->getPhysicalClockMinute();
// Calculate time difference (your exact logic!)
int16_t timeDifference = (realHour * 60 + realMinute) - (physicalHour * 60 + physicalMinute);
LOG_VERBOSE("⏰ CHECK: Real time %02d:%02d vs Physical %02d:%02d - DIFF: %d mins",
realHour, realMinute, physicalHour, physicalMinute, timeDifference);
// Handle 12-hour rollover (if negative, add 12 hours)
if (timeDifference < 0) {
timeDifference += 12 * 60; // Add 12 hours to handle rollover
LOG_VERBOSE("⏰ DIFF: Adjusted for rollover, new difference %d minutes", timeDifference);
}
// If there's a difference, advance the clock by one minute (your runMotor equivalent)
if (timeDifference >= 1) {
advancePhysicalClockOneMinute();
LOG_DEBUG("⏰ SYNC: Advanced physical clock by 1 minute to %02d:%02d (real: %02d:%02d, diff: %lu mins)",
_configManager->getPhysicalClockHour(), _configManager->getPhysicalClockMinute(),
realHour, realMinute, timeDifference);
}
}
void Timekeeper::advancePhysicalClockOneMinute() {
const auto& clockConfig = _configManager->getClockConfig();
if (clockConfig.c1output == 255 || clockConfig.c2output == 255) {
return;
}
bool useC1 = _configManager->getNextOutputIsC1();
uint8_t outputToFire = useC1 ? (clockConfig.c1output - 1) : (clockConfig.c2output - 1);
LOG_DEBUG("🔥 ADVANCE: Firing %s (output %d) for %dms",
useC1 ? "C1" : "C2", outputToFire + 1, clockConfig.pulseDuration);
_outputManager->fireOutputForDuration(outputToFire, clockConfig.pulseDuration);
vTaskDelay(pdMS_TO_TICKS(clockConfig.pulseDuration + clockConfig.pauseDuration)); // cool-off motor
_configManager->setNextOutputIsC1(!useC1);
updatePhysicalClockTime();
}
void Timekeeper::updatePhysicalClockTime() {
uint8_t currentHour = _configManager->getPhysicalClockHour();
uint8_t currentMinute = _configManager->getPhysicalClockMinute();
currentMinute++;
if (currentMinute >= 60) {
currentMinute = 0;
currentHour++;
if (currentHour > 12) { // 12-hour clock (your code used 24, but clock face is 12)
currentHour = 1;
}
}
_configManager->setPhysicalClockHour(currentHour);
_configManager->setPhysicalClockMinute(currentMinute);
_configManager->setLastSyncTime(millis() / 1000);
_configManager->saveClockState();
LOG_DEBUG("📅 STATE: Physical clock advanced to %d:%02d", currentHour, currentMinute);
}
// ════════════════════════════════════════════════════════════════════════════
// EVENT MANAGEMENT
// ════════════════════════════════════════════════════════════════════════════
void Timekeeper::loadTodaysEvents() {
// Clear existing events
todaysEvents.clear();
// Get current date/time from RTC
DateTime now = rtc.now();
if (!rtc.isrunning()) {
LOG_ERROR("RTC not running - cannot load events");
return;
}
int currentYear = now.year();
int currentMonth = now.month();
int currentDay = now.day();
int currentDayOfWeek = now.dayOfTheWeek(); // 0=Sunday, 1=Monday, etc.
LOG_INFO("Loading events for: %04d-%02d-%02d (day %d)",
currentYear, currentMonth, currentDay, currentDayOfWeek);
// Open and parse events file
File file = SD.open("/events/events.json");
if (!file) {
LOG_ERROR("Failed to open events.json");
return;
}
// Use static allocation instead of dynamic to avoid heap fragmentation
static StaticJsonDocument<8192> doc;
doc.clear();
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error) {
LOG_ERROR("JSON parsing failed: %s", error.c_str());
return;
}
JsonArray events = doc["events"];
int eventsLoaded = 0;
for (JsonObject event : events) {
if (!event["enabled"].as<bool>()) {
continue; // Skip disabled events
}
String type = event["type"].as<String>();
bool shouldAdd = false;
if (type == "single") {
// Check if event date matches today
String eventDateTime = event["datetime"].as<String>();
if (isSameDate(eventDateTime, currentYear, currentMonth, currentDay)) {
shouldAdd = true;
}
}
else if (type == "weekly") {
// Check if today's day of week is in the event's days
JsonArray daysOfWeek = event["days_of_week"];
for (JsonVariant dayVar : daysOfWeek) {
int day = dayVar.as<int>();
if (day == currentDayOfWeek) {
shouldAdd = true;
break;
}
}
}
else if (type == "monthly") {
// Check if today's date is in the event's days of month
JsonArray daysOfMonth = event["days_of_month"];
for (JsonVariant dayVar : daysOfMonth) {
int day = dayVar.as<int>();
if (day == currentDay) {
shouldAdd = true;
break;
}
}
}
if (shouldAdd) {
addToTodaysSchedule(event);
eventsLoaded++;
}
}
// Sort events by time
sortEventsByTime();
LOG_INFO("Loaded %d events for today", eventsLoaded);
}
bool Timekeeper::isSameDate(String eventDateTime, int year, int month, int day) {
// Parse "2025-12-25T09:00:00" format
if (eventDateTime.length() < 10) return false;
int eventYear = eventDateTime.substring(0, 4).toInt();
int eventMonth = eventDateTime.substring(5, 7).toInt();
int eventDay = eventDateTime.substring(8, 10).toInt();
return (eventYear == year && eventMonth == month && eventDay == day);
}
void Timekeeper::addToTodaysSchedule(JsonObject event) {
ScheduledEvent schedEvent;
// Extract time based on event type
if (event["type"] == "single") {
// Extract time from datetime: "2025-12-25T09:00:00" -> "09:00:00"
String datetime = event["datetime"].as<String>();
if (datetime.length() >= 19) {
schedEvent.timeStr = datetime.substring(11, 19);
}
} else {
// Weekly/Monthly events have separate time field
schedEvent.timeStr = event["time"].as<String>();
}
// Store the entire event data (create a copy)
schedEvent.eventData = event;
todaysEvents.push_back(schedEvent);
LOG_DEBUG("Added event '%s' at %s",
event["name"].as<String>().c_str(),
schedEvent.timeStr.c_str());
}
void Timekeeper::sortEventsByTime() {
std::sort(todaysEvents.begin(), todaysEvents.end(),
[](const ScheduledEvent& a, const ScheduledEvent& b) {
return a.timeStr < b.timeStr;
});
}
String Timekeeper::getCurrentTimeString() {
DateTime now = rtc.now();
if (!rtc.isrunning()) {
return "00:00:00";
}
char timeStr[9];
sprintf(timeStr, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
return String(timeStr);
}
void Timekeeper::checkScheduledEvents() {
String currentTime = getCurrentTimeString();
// Only check the seconds part for exact matching
String currentTimeMinute = currentTime.substring(0, 5); // "HH:MM"
for (auto& event : todaysEvents) {
String eventTimeMinute = event.timeStr.substring(0, 5); // "HH:MM"
if (eventTimeMinute == currentTimeMinute) {
// Check if we haven't already triggered this event
if (!event.triggered) {
triggerEvent(event);
event.triggered = true;
}
}
// Reset trigger flag when we're past the minute
if (eventTimeMinute < currentTimeMinute) {
event.triggered = false;
}
}
}
void Timekeeper::triggerEvent(ScheduledEvent& event) {
JsonObject eventData = event.eventData;
LOG_INFO("TRIGGERING EVENT: %s at %s",
eventData["name"].as<String>().c_str(),
event.timeStr.c_str());
// Here you would trigger your melody playback
// You might want to call a function from your main program
// or send a message to another task
// Example of what you might do:
JsonObject melody = eventData["melody"];
String melodyUID = melody["uid"].as<String>();
String melodyName = melody["name"].as<String>();
LOG_INFO("Playing melody: %s (UID: %s)",
melodyName.c_str(), melodyUID.c_str());
// TODO: Add your melody trigger code here
// playMelody(melody);
}
void Timekeeper::loadNextDayEvents() {
// This function would load tomorrow's events for smooth midnight transition
// Implementation similar to loadTodaysEvents() but for tomorrow's date
LOG_DEBUG("Pre-loading tomorrow's events...");
// TODO: Implement if needed for smoother transitions
}
// ════════════════════════════════════════════════════════════════════════════
// CLOCK ALERTS IMPLEMENTATION
// ════════════════════════════════════════════════════════════════════════════
void Timekeeper::checkClockAlerts() {
// Check if clock is enabled in config
if (!_configManager || !_configManager->getClockEnabled()) {
return; // Clock is disabled - skip all alert functionality
}
// Check if we have required dependencies
if (!_outputManager || !rtc.isrunning()) {
return;
}
// 🔥 CRITICAL: Check if Player is busy - if so, SKIP alert completely
if (_player && _player->isPlaying) {
// Player is active (playing, paused, stopping, etc.) - skip alert entirely
// Mark this alert as processed to prevent it from firing when playback ends
DateTime now = rtc.now();
int currentMinute = now.minute();
if (currentMinute == 0) {
lastHour = now.hour(); // Mark hour as processed
} else if (currentMinute == 30) {
lastMinute = 30; // Mark half-hour as processed
} else if (currentMinute == 15 || currentMinute == 45) {
lastMinute = currentMinute; // Mark quarter-hour as processed
}
LOG_DEBUG("⏭️ SKIPPING clock alert - Player is busy (playing/paused)");
return;
}
// Get current time
DateTime now = rtc.now();
int currentHour = now.hour();
int currentMinute = now.minute();
int currentSecond = now.second();
// Only trigger alerts in first 30 seconds of the minute
// The lastHour/lastMinute tracking prevents duplicate triggers
if (currentSecond > 30) {
return;
}
// Get clock configuration
const auto& clockConfig = _configManager->getClockConfig();
// Check if alerts are disabled
if (clockConfig.alertType == "OFF") {
return;
}
// Check if we're in a silence period
if (isInSilencePeriod()) {
return;
}
// 🕐 HOURLY ALERTS (at xx:00)
if (currentMinute == 0 && currentHour != lastHour) {
triggerHourlyAlert(currentHour);
lastHour = currentHour;
}
// 🕕 HALF-HOUR ALERTS (at xx:30)
if (currentMinute == 30 && lastMinute != 30) {
if (clockConfig.halfBell != 255) { // 255 = disabled
LOG_INFO("🕕 Half-hour alert at %02d:30", currentHour);
fireAlertBell(clockConfig.halfBell, 1);
}
lastMinute = 30;
}
// 🕒 QUARTER-HOUR ALERTS (at xx:15 and xx:45)
if ((currentMinute == 15 || currentMinute == 45) && lastMinute != currentMinute) {
if (clockConfig.quarterBell != 255) { // 255 = disabled
LOG_INFO("🕒 Quarter-hour alert at %02d:%02d", currentHour, currentMinute);
fireAlertBell(clockConfig.quarterBell, 1);
}
lastMinute = currentMinute;
}
// Reset minute tracking for other minutes
if (currentMinute != 0 && currentMinute != 15 && currentMinute != 30 && currentMinute != 45) {
if (lastMinute != currentMinute) {
lastMinute = currentMinute;
}
}
}
void Timekeeper::triggerHourlyAlert(int hour) {
const auto& clockConfig = _configManager->getClockConfig();
// Check if hourly bell is configured
if (clockConfig.hourBell == 255) { // 255 = disabled
return;
}
if (clockConfig.alertType == "SINGLE") {
// Single ding for any hour
LOG_INFO("🕐 Hourly alert (SINGLE) at %02d:00", hour);
fireAlertBell(clockConfig.hourBell, 1);
}
else if (clockConfig.alertType == "HOURS") {
// Ring the number of times equal to the hour (1-12)
int bellCount = hour;
if (bellCount == 0) bellCount = 12; // Midnight = 12 bells
if (bellCount > 12) bellCount = bellCount - 12; // 24h to 12h conversion
LOG_INFO("🕐 Hourly alert (HOURS) at %02d:00 - %d rings", hour, bellCount);
fireAlertBell(clockConfig.hourBell, bellCount);
}
}
void Timekeeper::fireAlertBell(uint8_t bellNumber, int count) {
if (!_outputManager || bellNumber == 255) {
return;
}
const auto& clockConfig = _configManager->getClockConfig();
// Mark alert as in progress
alertInProgress.store(true);
for (int i = 0; i < count; i++) {
// 🔥 Check for interruption by user playback
if (!alertInProgress.load()) {
LOG_INFO("⚡ Alert interrupted at ring %d/%d - stopping immediately", i + 1, count);
return;
}
// Get bell duration from bell configuration
uint16_t bellDuration = _configManager->getBellDuration(bellNumber);
LOG_DEBUG("🔔 Alert bell #%d ring %d/%d (duration: %dms)",
bellNumber + 1, i + 1, count, bellDuration);
// Fire the bell using OutputManager
_outputManager->fireOutputForDuration(bellNumber, bellDuration);
// Wait between rings (only if there's more than one ring)
if (i < count - 1) {
vTaskDelay(pdMS_TO_TICKS(clockConfig.alertRingInterval));
}
}
// Mark alert as complete
alertInProgress.store(false);
}
void Timekeeper::checkBacklightAutomation() {
// Check if clock is enabled in config
if (!_configManager || !_configManager->getClockEnabled()) {
return; // Clock is disabled - skip all backlight functionality
}
// Check if we have required dependencies
if (!_outputManager || !rtc.isrunning()) {
return;
}
const auto& clockConfig = _configManager->getClockConfig();
// Check if backlight automation is enabled
if (!clockConfig.backlight || clockConfig.backlightOutput == 255) {
return; // Backlight automation disabled
}
// Get current time
DateTime now = rtc.now();
char currentTimeStr[6];
sprintf(currentTimeStr, "%02d:%02d", now.hour(), now.minute());
String currentTime = String(currentTimeStr);
// Check if it's time to turn backlight ON
if (currentTime == clockConfig.backlightOnTime && !backlightState) {
LOG_INFO("💡 Turning backlight ON at %s (output #%d)",
currentTime.c_str(), clockConfig.backlightOutput + 1);
_outputManager->fireOutput(clockConfig.backlightOutput);
backlightState = true;
}
// Check if it's time to turn backlight OFF
else if (currentTime == clockConfig.backlightOffTime && backlightState) {
LOG_INFO("💡 Turning backlight OFF at %s (output #%d)",
currentTime.c_str(), clockConfig.backlightOutput + 1);
_outputManager->extinguishOutput(clockConfig.backlightOutput);
backlightState = false;
}
}
bool Timekeeper::isInSilencePeriod() {
if (!_configManager || !rtc.isrunning()) {
return false;
}
const auto& clockConfig = _configManager->getClockConfig();
// Get current time
DateTime now = rtc.now();
char currentTimeStr[6];
sprintf(currentTimeStr, "%02d:%02d", now.hour(), now.minute());
String currentTime = String(currentTimeStr);
// Check daytime silence period
if (clockConfig.daytimeSilenceEnabled) {
bool inDaytime = isTimeInRange(currentTime, clockConfig.daytimeSilenceOnTime, clockConfig.daytimeSilenceOffTime);
LOG_DEBUG("🔇 Daytime silence check: current=%s, range=%s-%s, inRange=%s",
currentTime.c_str(), clockConfig.daytimeSilenceOnTime.c_str(),
clockConfig.daytimeSilenceOffTime.c_str(), inDaytime ? "YES" : "NO");
if (inDaytime) {
return true;
}
}
// Check nighttime silence period
if (clockConfig.nighttimeSilenceEnabled) {
bool inNighttime = isTimeInRange(currentTime, clockConfig.nighttimeSilenceOnTime, clockConfig.nighttimeSilenceOffTime);
LOG_DEBUG("🌙 Nighttime silence check: current=%s, range=%s-%s, inRange=%s",
currentTime.c_str(), clockConfig.nighttimeSilenceOnTime.c_str(),
clockConfig.nighttimeSilenceOffTime.c_str(), inNighttime ? "YES" : "NO");
if (inNighttime) {
return true;
}
}
return false;
}
bool Timekeeper::isTimeInRange(const String& currentTime, const String& startTime, const String& endTime) const {
// Handle the case where the time range crosses midnight (e.g., 22:00 to 07:00)
if (startTime > endTime) {
// Range crosses midnight - current time is in range if it's after start OR before end
return (currentTime >= startTime || currentTime <= endTime);
} else {
// Normal range - current time is in range if it's between start and end
return (currentTime >= startTime && currentTime <= endTime);
}
}
// ════════════════════════════════════════════════════════════════════════════
// HEALTH CHECK IMPLEMENTATION
// ════════════════════════════════════════════════════════════════════════════
bool Timekeeper::isHealthy() {
// Check if RTC is running
if (!rtc.isrunning()) {
LOG_DEBUG("TimeKeeper: Unhealthy - RTC not running");
return false;
}
// Check if main task is created and running
if (mainTaskHandle == NULL) {
LOG_DEBUG("TimeKeeper: Unhealthy - Main task not created");
return false;
}
// Check if task is still alive
eTaskState taskState = eTaskGetState(mainTaskHandle);
if (taskState == eDeleted || taskState == eInvalid) {
LOG_DEBUG("TimeKeeper: Unhealthy - Main task deleted or invalid");
return false;
}
// Check if required dependencies are set
if (!_configManager) {
LOG_DEBUG("TimeKeeper: Unhealthy - ConfigManager not set");
return false;
}
if (!_outputManager) {
LOG_DEBUG("TimeKeeper: Unhealthy - OutputManager not set");
return false;
}
// Check if time is reasonable (not stuck at epoch or way in the future)
DateTime now = rtc.now();
if (now.year() < 2020 || now.year() > 2100) {
LOG_DEBUG("TimeKeeper: Unhealthy - RTC time unreasonable: %d", now.year());
return false;
}
return true;
}