From 32bed6682ca8adb175ec26987a961386bb60fa4a Mon Sep 17 00:00:00 2001 From: Kyle Phillips Date: Thu, 15 Jan 2026 21:57:13 +1100 Subject: [PATCH 1/5] Apply most recent time-controlled preset on boot I use WLED bulbs that are controlled by a wall switch, so they power cycle frequently. I wanted them to automatically switch to red light after 8pm to reduce melatonin suppression before sleep. The existing time-controlled presets only trigger at the exact scheduled minute, not when powering on after that time has passed. This change adds a one-time check after NTP sync that finds and applies the most recent preset that should have triggered today. Changes: - Add getTimerMinuteOfDay() helper to reduce duplication between checkTimers() and the new boot-time logic - Add applyBootTimerPreset() called after first NTP sync - Add "Apply scheduled preset on boot" checkbox in Time settings UI - Setting is enabled by default and persisted in config Addresses #2546 --- wled00/cfg.cpp | 2 + wled00/data/settings_time.htm | 2 + wled00/fcn_declare.h | 1 + wled00/ntp.cpp | 117 ++++++++++++++++++++++++---------- wled00/set.cpp | 1 + wled00/wled.h | 2 + wled00/xml.cpp | 1 + 7 files changed, 91 insertions(+), 35 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index ff491faffd..420065037e 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -712,6 +712,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } it++; } + CJSON(applyTimerOnBoot, tm["aob"]); JsonObject ota = doc["ota"]; const char* pwd = ota["psk"]; //normally not present due to security @@ -1220,6 +1221,7 @@ void serializeConfig(JsonObject root) { end["day"] = timerDayEnd[i]; } } + timers["aob"] = applyTimerOnBoot; JsonObject ota = root.createNestedObject("ota"); ota[F("lock")] = otaLock; diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index 11f3c47d9d..3e5644daff 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -210,6 +210,8 @@

Button actions

Analog Button setup

Time-controlled presets

+ Apply scheduled preset on boot:
+ When enabled, if you power on after a scheduled time, that preset will be applied automatically.

diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 84b5595df7..436eccd0d2 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -202,6 +202,7 @@ void setCountdown(); byte weekdayMondayFirst(); bool isTodayInDateRange(byte monthStart, byte dayStart, byte monthEnd, byte dayEnd); void checkTimers(); +void applyBootTimerPreset(); void calculateSunriseAndSunset(); void setTimeFromAPI(uint32_t timein); diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index abad5c3c9d..00809d025b 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -292,6 +292,8 @@ bool checkNTPResponse() // if time changed re-calculate sunrise/sunset updateLocalTime(); calculateSunriseAndSunset(); + // Apply most recent timer preset on first NTP sync after boot + applyBootTimerPreset(); return true; } @@ -376,6 +378,39 @@ bool isTodayInDateRange(byte monthStart, byte dayStart, byte monthEnd, byte dayE return (m == monthStart && d >= dayStart && d <= dayEnd); //just the designated days this month } +/* + * Returns the minute-of-day (0-1439) for timer i if it's valid for today, or -1 if invalid. + * Validates: preset assigned, timer enabled, weekday matches, date range (for timers 0-7). + * For "every hour" timers (hour=24), returns the current hour's scheduled minute. + * For sunrise/sunset timers (8/9), returns the calculated trigger time. + */ +static int getTimerMinuteOfDay(unsigned i) +{ + if (i > 9) return -1; + if (timerMacro[i] == 0) return -1; + if (!(timerWeekday[i] & 0x01)) return -1; // not enabled + if (!((timerWeekday[i] >> weekdayMondayFirst()) & 0x01)) return -1; // wrong weekday + + if (i < 8) { + // Standard timer (0-7) + if (!isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i])) return -1; + if (timerHours[i] == 24) { + return hour(localTime) * 60 + timerMinutes[i]; // "every hour" at this minute + } + return timerHours[i] * 60 + timerMinutes[i]; + } else if (i == 8) { + // Sunrise timer + if (!sunrise) return -1; + time_t t = sunrise + timerMinutes[8] * 60; + return hour(t) * 60 + minute(t); + } else { + // Sunset timer (i == 9) + if (!sunset) return -1; + time_t t = sunset + timerMinutes[9] * 60; + return hour(t) * 60 + minute(t); + } +} + void checkTimers() { if (lastTimerMinute != minute(localTime)) //only check once a new minute begins @@ -385,49 +420,61 @@ void checkTimers() // re-calculate sunrise and sunset just after midnight if (!hour(localTime) && minute(localTime)==1) calculateSunriseAndSunset(); + int currentMinuteOfDay = hour(localTime) * 60 + minute(localTime); DEBUG_PRINTF_P(PSTR("Local time: %02d:%02d\n"), hour(localTime), minute(localTime)); - for (unsigned i = 0; i < 8; i++) + + for (unsigned i = 0; i < 10; i++) { - if (timerMacro[i] != 0 - && (timerWeekday[i] & 0x01) //timer is enabled - && (timerHours[i] == hour(localTime) || timerHours[i] == 24) //if hour is set to 24, activate every hour - && timerMinutes[i] == minute(localTime) - && ((timerWeekday[i] >> weekdayMondayFirst()) & 0x01) //timer should activate at current day of week - && isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i]) - ) - { + int timerMinute = getTimerMinuteOfDay(i); + if (timerMinute == currentMinuteOfDay) { applyPreset(timerMacro[i]); + if (i == 8) DEBUG_PRINTF_P(PSTR("Sunrise macro %d triggered."), timerMacro[8]); + if (i == 9) DEBUG_PRINTF_P(PSTR("Sunset macro %d triggered."), timerMacro[9]); } } - // sunrise macro - if (sunrise) { - time_t tmp = sunrise + timerMinutes[8]*60; // NOTE: may not be ok - DEBUG_PRINTF_P(PSTR("Trigger time: %02d:%02d\n"), hour(tmp), minute(tmp)); - if (timerMacro[8] != 0 - && hour(tmp) == hour(localTime) - && minute(tmp) == minute(localTime) - && (timerWeekday[8] & 0x01) //timer is enabled - && ((timerWeekday[8] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week - { - applyPreset(timerMacro[8]); - DEBUG_PRINTF_P(PSTR("Sunrise macro %d triggered."),timerMacro[8]); - } + } +} + +/* + * Apply the most recent time-controlled preset that should have triggered today. + * Called once after NTP sync on boot to handle powering on after a scheduled time. + */ +void applyBootTimerPreset() +{ + if (bootTimerApplied || !applyTimerOnBoot) return; + bootTimerApplied = true; + + int currentMinuteOfDay = hour(localTime) * 60 + minute(localTime); + int latestTimerMinute = -1; + byte latestPreset = 0; + + DEBUG_PRINTLN(F("Checking for boot timer preset...")); + + for (unsigned i = 0; i < 10; i++) + { + int timerMinute = getTimerMinuteOfDay(i); + if (timerMinute < 0) continue; + + // For "every hour" timers, find the most recent past occurrence + if (i < 8 && timerHours[i] == 24 && timerMinute > currentMinuteOfDay) { + timerMinute -= 60; + if (timerMinute < 0) continue; // would be yesterday, skip } - // sunset macro - if (sunset) { - time_t tmp = sunset + timerMinutes[9]*60; // NOTE: may not be ok - DEBUG_PRINTF_P(PSTR("Trigger time: %02d:%02d\n"), hour(tmp), minute(tmp)); - if (timerMacro[9] != 0 - && hour(tmp) == hour(localTime) - && minute(tmp) == minute(localTime) - && (timerWeekday[9] & 0x01) //timer is enabled - && ((timerWeekday[9] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week - { - applyPreset(timerMacro[9]); - DEBUG_PRINTF_P(PSTR("Sunset macro %d triggered."),timerMacro[9]); - } + + // Only consider timers that should have already triggered today + if (timerMinute <= currentMinuteOfDay && timerMinute > latestTimerMinute) { + latestTimerMinute = timerMinute; + latestPreset = timerMacro[i]; } } + + if (latestPreset > 0) { + DEBUG_PRINTF_P(PSTR("Applying boot timer preset %d (scheduled for %02d:%02d)\n"), + latestPreset, latestTimerMinute / 60, latestTimerMinute % 60); + applyPreset(latestPreset); + } else { + DEBUG_PRINTLN(F("No applicable boot timer preset found.")); + } } #define ZENITH -0.83 diff --git a/wled00/set.cpp b/wled00/set.cpp index db8b30bac8..f7618f9625 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -568,6 +568,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) timerDayEnd[i] = request->arg(k).toInt(); } } + applyTimerOnBoot = request->hasArg(F("TB")); } //SECURITY diff --git a/wled00/wled.h b/wled00/wled.h index 66b33740d6..dc5b645b8f 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -815,6 +815,8 @@ WLED_GLOBAL bool countdownOverTriggered _INIT(true); //timer WLED_GLOBAL byte lastTimerMinute _INIT(0); +WLED_GLOBAL bool bootTimerApplied _INIT(false); // whether boot-time timer check has been performed +WLED_GLOBAL bool applyTimerOnBoot _INIT(true); // apply most recent scheduled timer preset on boot (after NTP sync) WLED_GLOBAL byte timerHours[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); WLED_GLOBAL int8_t timerMinutes[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); WLED_GLOBAL byte timerMacro[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 194256d82e..d1e519ddd3 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -601,6 +601,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) k[0] = 'E'; printSetFormValue(settingsScript,k,timerDayEnd[i]); } } + printSetFormCheckbox(settingsScript,PSTR("TB"),applyTimerOnBoot); } if (subPage == SUBPAGE_SEC) From e5ea6cecb0a6f1cc555f01c4374482216c44a1a9 Mon Sep 17 00:00:00 2001 From: Kyle Phillips Date: Thu, 15 Jan 2026 23:06:42 +1100 Subject: [PATCH 2/5] Use elapsedSecsToday() and clarify -1 sentinel value --- wled00/ntp.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index 00809d025b..c2db07cbf1 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -420,12 +420,12 @@ void checkTimers() // re-calculate sunrise and sunset just after midnight if (!hour(localTime) && minute(localTime)==1) calculateSunriseAndSunset(); - int currentMinuteOfDay = hour(localTime) * 60 + minute(localTime); + int currentMinuteOfDay = elapsedSecsToday(localTime) / 60; DEBUG_PRINTF_P(PSTR("Local time: %02d:%02d\n"), hour(localTime), minute(localTime)); for (unsigned i = 0; i < 10; i++) { - int timerMinute = getTimerMinuteOfDay(i); + int timerMinute = getTimerMinuteOfDay(i); // returns -1 if timer not valid for today if (timerMinute == currentMinuteOfDay) { applyPreset(timerMacro[i]); if (i == 8) DEBUG_PRINTF_P(PSTR("Sunrise macro %d triggered."), timerMacro[8]); @@ -444,7 +444,7 @@ void applyBootTimerPreset() if (bootTimerApplied || !applyTimerOnBoot) return; bootTimerApplied = true; - int currentMinuteOfDay = hour(localTime) * 60 + minute(localTime); + int currentMinuteOfDay = elapsedSecsToday(localTime) / 60; int latestTimerMinute = -1; byte latestPreset = 0; @@ -452,7 +452,7 @@ void applyBootTimerPreset() for (unsigned i = 0; i < 10; i++) { - int timerMinute = getTimerMinuteOfDay(i); + int timerMinute = getTimerMinuteOfDay(i); // returns -1 if timer not valid for today if (timerMinute < 0) continue; // For "every hour" timers, find the most recent past occurrence From 5dc42c58947b477f68752d878ffbfd2d1da8dc31 Mon Sep 17 00:00:00 2001 From: Kyle Phillips Date: Thu, 15 Jan 2026 23:33:39 +1100 Subject: [PATCH 3/5] Fix tie-breaking: highest index wins on same-minute timers Match checkTimers() behavior where all matching presets are applied in order and the last one sticks. Changed > to >= and added comment explaining the design choice. --- wled00/ntp.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index c2db07cbf1..cc225310ec 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -461,8 +461,10 @@ void applyBootTimerPreset() if (timerMinute < 0) continue; // would be yesterday, skip } - // Only consider timers that should have already triggered today - if (timerMinute <= currentMinuteOfDay && timerMinute > latestTimerMinute) { + // Only consider timers that should have already triggered today. + // Use >= so that if multiple timers share the same minute, the highest index wins, + // matching checkTimers() behavior where all are applied and the last one sticks. + if (timerMinute <= currentMinuteOfDay && timerMinute >= latestTimerMinute) { latestTimerMinute = timerMinute; latestPreset = timerMacro[i]; } From b24e7d82c893fc38e07ab59b682af2f9727a3bd0 Mon Sep 17 00:00:00 2001 From: Kyle Phillips Date: Fri, 16 Jan 2026 08:05:18 +1100 Subject: [PATCH 4/5] Disable applyTimerOnBoot by default for backwards compatibility --- wled00/wled.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/wled.h b/wled00/wled.h index dc5b645b8f..25a5ebc20f 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -816,7 +816,7 @@ WLED_GLOBAL bool countdownOverTriggered _INIT(true); //timer WLED_GLOBAL byte lastTimerMinute _INIT(0); WLED_GLOBAL bool bootTimerApplied _INIT(false); // whether boot-time timer check has been performed -WLED_GLOBAL bool applyTimerOnBoot _INIT(true); // apply most recent scheduled timer preset on boot (after NTP sync) +WLED_GLOBAL bool applyTimerOnBoot _INIT(false); // apply most recent scheduled timer preset on boot (after NTP sync) WLED_GLOBAL byte timerHours[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); WLED_GLOBAL int8_t timerMinutes[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); WLED_GLOBAL byte timerMacro[] _INIT_N(({ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 })); From a9b0598754399964af01a798f23860a078551792 Mon Sep 17 00:00:00 2001 From: Kyle Phillips Date: Sat, 17 Jan 2026 09:50:24 +1100 Subject: [PATCH 5/5] Clarify comment at NTP sync success path Update comment from "if time changed" to "NTP sync succeeded" for better clarity about when this code executes. --- wled00/ntp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index cc225310ec..7c6557f1c1 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -289,10 +289,10 @@ bool checkNTPResponse() #endif if (countdownTime - toki.second() > 0) countdownOverTriggered = false; - // if time changed re-calculate sunrise/sunset + + // NTP sync succeeded updateLocalTime(); calculateSunriseAndSunset(); - // Apply most recent timer preset on first NTP sync after boot applyBootTimerPreset(); return true; }