Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions wled00/data/settings_time.htm
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ <h3>Clock</h3>
Countdown Goal:<br>
Date:&nbsp;<nowrap>20<input name="CY" class="xs" type="number" min="0" max="99" required>-<input name="CI" class="xs" type="number" min="1" max="12" required>-<input name="CD" class="xs" type="number" min="1" max="31" required></nowrap><br>
Time:&nbsp;<nowrap><input name="CH" class="xs" type="number" min="0" max="23" required>:<input name="CM" class="xs" type="number" min="0" max="59" required>:<input name="CS" class="xs" type="number" min="0" max="59" required></nowrap><br>
<h3>Upload Schedule JSON</h3>
<input type="file" name="scheduleFile" id="scheduleFile" accept=".json">
<input type="button" value="Upload" onclick="uploadFile(d.Sf.scheduleFile, '/schedule.json');">
<br>
<a class="btn lnk" id="bckschedule" href="/schedule.json" download="schedule">Backup schedule</a><br>
<h3>Macro presets</h3>
<b>Macros have moved!</b><br>
<i>Presets now also can be used as macros to save both JSON and HTTP API commands.<br>
Expand Down
111 changes: 111 additions & 0 deletions wled00/schedule.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// schedule.cpp


#include "schedule.h"
#include <WLED.h>
#include <time.h>

#define SCHEDULE_FILE "/schedule.json"

ScheduleEvent scheduleEvents[MAX_SCHEDULE_EVENTS];
uint8_t numScheduleEvents = 0;

bool isTodayInRange(uint8_t sm, uint8_t sd, uint8_t em, uint8_t ed, uint8_t cm, uint8_t cd)
{
if (sm < em || (sm == em && sd <= ed))
{
return (cm > sm || (cm == sm && cd >= sd)) &&
(cm < em || (cm == em && cd <= ed));
}
else
{
return (cm > sm || (cm == sm && cd >= sd)) ||
(cm < em || (cm == em && cd <= ed));
}
}


// Checks the schedule and applies any events that match the current time and date.

void checkSchedule() {
static int lastMinute = -1;

time_t now = localTime;
if (now < 100000) return;

struct tm* timeinfo = localtime(&now);
int thisMinute = timeinfo->tm_min + timeinfo->tm_hour * 60;

if (thisMinute == lastMinute) return;
lastMinute = thisMinute;


uint8_t cm = timeinfo->tm_mon + 1; // months since Jan (0-11)
uint8_t cd = timeinfo->tm_mday;
uint8_t wday = timeinfo->tm_wday; // days since Sunday (0-6)
uint8_t hr = timeinfo->tm_hour;
uint8_t min = timeinfo->tm_min;

DEBUG_PRINTF_P(PSTR("[Schedule] Checking schedule at %02u:%02u\n"), hr, min);

for (uint8_t i = 0; i < numScheduleEvents; i++)
{
const ScheduleEvent &e = scheduleEvents[i];
if (e.hour != hr || e.minute != min)
continue;

bool match = false;
if (e.repeatMask && ((e.repeatMask >> wday) & 0x01))
match = true;
if (e.startMonth)
{
if (isTodayInRange(e.startMonth, e.startDay, e.endMonth, e.endDay, cm, cd))
match = true;
}

if (match)
{
applyPreset(e.presetId);
DEBUG_PRINTF_P(PSTR("[Schedule] Applying preset %u at %02u:%02u\n"), e.presetId, hr, min);
}
DEBUG_PRINTF_P(PSTR("[Schedule] Checked event %u: match=%d\n"), i, match);
}
}

void loadSchedule()
{
if (!WLED_FS.exists(SCHEDULE_FILE)) return;

requestJSONBufferLock(); //Prevent concurrent JSON access

File file = WLED_FS.open(SCHEDULE_FILE, "r");
if (!file) {
releaseJSONBufferLock();
return;
}

DynamicJsonDocument doc(4096);
DeserializationError error = deserializeJson(doc, file);
file.close(); // ✅ Close file before releasing lock

if (error) {
DEBUG_PRINTF_P(PSTR("[Schedule] JSON parse failed: %s\n"), error.c_str());
releaseJSONBufferLock();
return;
}

numScheduleEvents = 0;
for (JsonObject e : doc.as<JsonArray>()) {
if (numScheduleEvents >= MAX_SCHEDULE_EVENTS) break;

scheduleEvents[numScheduleEvents++] = {
(uint8_t)e["sm"].as<int>(), (uint8_t)e["sd"].as<int>(), // start month, day
(uint8_t)e["em"].as<int>(), (uint8_t)e["ed"].as<int>(), // end month, day
(uint8_t)e["r"].as<int>(), (uint8_t)e["h"].as<int>(), // repeat mask, hour
(uint8_t)e["m"].as<int>(), (uint8_t)e["p"].as<int>() // minute, preset
};
}

DEBUG_PRINTF_P(PSTR("[Schedule] Loaded %u schedule entries from schedule.json\n"), numScheduleEvents);
releaseJSONBufferLock(); // Done safely
}
20 changes: 20 additions & 0 deletions wled00/schedule.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// schedule.h
#pragma once

#include <stdint.h>

#define MAX_SCHEDULE_EVENTS 32

struct ScheduleEvent {
uint8_t startMonth;
uint8_t startDay;
uint8_t endMonth;
uint8_t endDay;
uint8_t repeatMask;
uint8_t hour;
uint8_t minute;
uint8_t presetId;
};

void loadSchedule();
void checkSchedule();
3 changes: 3 additions & 0 deletions wled00/wled.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ void WLED::loop()
#endif

handleTime();
checkSchedule();
#ifndef WLED_DISABLE_INFRARED
handleIR(); // 2nd call to function needed for ESP32 to return valid results -- should be good for ESP8266, too
#endif
Expand Down Expand Up @@ -526,6 +527,8 @@ void WLED::setup()
#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET)
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 1); //enable brownout detector
#endif

loadSchedule();
}

void WLED::beginStrip()
Expand Down
2 changes: 2 additions & 0 deletions wled00/wled.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@

#include <cstddef>
#include <vector>
#include "schedule.h"


// Library inclusions.
#include <Arduino.h>
Expand Down
54 changes: 42 additions & 12 deletions wled00/wled_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,28 +179,58 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename,
if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg));
return;
}

String finalname = filename;
if (!index) {
String finalname = filename;
if (finalname.charAt(0) != '/') {
finalname = '/' + finalname; // prepend slash if missing
}

request->_tempFile = WLED_FS.open(finalname, "w");
DEBUG_PRINTF_P(PSTR("Uploading %s\n"), finalname.c_str());
if (finalname.equals(FPSTR(getPresetsFileName()))) presetsModifiedTime = toki.second();
// Special case: schedule.json upload uses temp file
if (finalname.equals(F("/schedule.json"))) {
request->_tempFile = WLED_FS.open("/schedule.json.tmp", "w");
DEBUG_PRINTLN(F("Uploading to /schedule.json.tmp"));
} else {
request->_tempFile = WLED_FS.open(finalname, "w");
DEBUG_PRINTF_P(PSTR("Uploading %s\n"), finalname.c_str());

if (finalname.equals(FPSTR(getPresetsFileName()))) {
presetsModifiedTime = toki.second();
}
}
}
if (len) {
request->_tempFile.write(data,len);

// Write chunk
if (len && request->_tempFile) {
request->_tempFile.write(data, len);
}

// Finalize upload
if (isFinal) {
request->_tempFile.close();
if (filename.indexOf(F("cfg.json")) >= 0) { // check for filename with or without slash
doReboot = true;
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting..."));
if (request->_tempFile) request->_tempFile.close();

if (finalname.equals(F("/schedule.json"))) {
// Atomically replace old file
WLED_FS.remove("/schedule.json");
WLED_FS.rename("/schedule.json.tmp", "/schedule.json");

// Apply new schedule immediately
loadSchedule();

request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Schedule uploaded and applied."));
DEBUG_PRINTLN(F("[Schedule] Upload complete and applied."));
} else {
if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) loadCustomPalettes();
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!"));
if (filename.indexOf(F("cfg.json")) >= 0) {
doReboot = true;
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting..."));
} else {
if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) {
loadCustomPalettes();
}
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!"));
}
}

cacheInvalidate++;
}
}
Expand Down