Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
147 changes: 147 additions & 0 deletions wled00/schedule.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// schedule.cpp
// Handles reading, parsing, and checking the preset schedule from schedule.json

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

#define SCHEDULE_FILE "/schedule.json"

// Array to hold scheduled events, max size defined in schedule.h
ScheduleEvent scheduleEvents[MAX_SCHEDULE_EVENTS];
uint8_t numScheduleEvents = 0; // Current count of loaded schedule entries

// Helper function to check if current date (cm, cd) is within the event's start and end date range
bool isTodayInRange(uint8_t sm, uint8_t sd, uint8_t em, uint8_t ed, uint8_t cm, uint8_t cd)
{
// Handles ranges that wrap over the year end, e.g., Dec to Jan
if (sm < em || (sm == em && sd <= ed))
{
// Normal range within a year
return (cm > sm || (cm == sm && cd >= sd)) &&
(cm < em || (cm == em && cd <= ed));
}
else
{
// Range wraps year-end (e.g., Nov 20 to Feb 10)
return (cm > sm || (cm == sm && cd >= sd)) ||
(cm < em || (cm == em && cd <= ed));
}
}

// Checks current time against schedule entries and applies matching presets
void checkSchedule() {
static int lastMinute = -1; // To avoid multiple triggers within the same minute

time_t now = localTime;
if (now < 100000) return; // Invalid or uninitialized time guard

struct tm* timeinfo = localtime(&now);

int thisMinute = timeinfo->tm_min + timeinfo->tm_hour * 60;
if (thisMinute == lastMinute) return; // Already checked this minute
lastMinute = thisMinute;

// Extract date/time components for easier use
uint8_t cm = timeinfo->tm_mon + 1; // Month [1-12]
uint8_t cd = timeinfo->tm_mday; // Day of month [1-31]
uint8_t wday = timeinfo->tm_wday; // Weekday [0-6], Sunday=0
uint8_t hr = timeinfo->tm_hour; // Hour [0-23]
uint8_t min = timeinfo->tm_min; // Minute [0-59]

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

// Iterate through all scheduled events
for (uint8_t i = 0; i < numScheduleEvents; i++)
{
const ScheduleEvent &e = scheduleEvents[i];

// Skip if hour or minute doesn't match current time
if (e.hour != hr || e.minute != min)
continue;

bool match = false;

// Check if repeat mask matches current weekday (bitmask with Sunday=LSB)
if (e.repeatMask && ((e.repeatMask >> wday) & 0x01))
match = true;

// Otherwise check if current date is within start and end date range
if (e.startMonth)
{
if (isTodayInRange(e.startMonth, e.startDay, e.endMonth, e.endDay, cm, cd))
match = true;
}

// If match, apply preset and print debug
if (match)
{
applyPreset(e.presetId);
DEBUG_PRINTF_P(PSTR("[Schedule] Applying preset %u at %02u:%02u\n"), e.presetId, hr, min);
}
}
}

// Loads schedule events from the schedule JSON file
// Returns true if successful, false on error or missing file
bool loadSchedule() {
if (!WLED_FS.exists(SCHEDULE_FILE)) return false;

// Acquire JSON buffer lock to prevent concurrent access
if (!requestJSONBufferLock(7)) return false;

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

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

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

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

// Read and validate fields with type safety
int sm = e["sm"].as<int>();
int sd = e["sd"].as<int>();
int em = e["em"].as<int>();
int ed = e["ed"].as<int>();
int r = e["r"].as<int>();
int h = e["h"].as<int>();
int m = e["m"].as<int>();
int p = e["p"].as<int>();

// Validate ranges to prevent bad data
if (sm < 1 || sm > 12 || em < 1 || em > 12 ||
sd < 1 || sd > 31 || ed < 1 || ed > 31 ||
h < 0 || h > 23 || m < 0 || m > 59 ||
r < 0 || r > 127|| p < 1 || p > 250) {
DEBUG_PRINTF_P(PSTR("[Schedule] Invalid values in event %u, skipping\n"), numScheduleEvents);
continue;
}

// Store event in the array
scheduleEvents[numScheduleEvents++] = {
(uint8_t)sm, (uint8_t)sd,
(uint8_t)em, (uint8_t)ed,
(uint8_t)r, (uint8_t)h,
(uint8_t)m, (uint8_t)p
};
}

DEBUG_PRINTF_P(PSTR("[Schedule] Loaded %u schedule entries from schedule.json\n"), numScheduleEvents);

// Release JSON buffer lock after finishing
releaseJSONBufferLock();

return true;
}
27 changes: 27 additions & 0 deletions wled00/schedule.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// schedule.h
// Defines the schedule event data structure and declares schedule-related functions

#pragma once

#include <stdint.h>

// Maximum number of schedule events supported
#define MAX_SCHEDULE_EVENTS 32

// Structure representing one scheduled event
struct ScheduleEvent {
uint8_t startMonth; // Starting month of the schedule event (1-12)
uint8_t startDay; // Starting day of the schedule event (1-31)
uint8_t endMonth; // Ending month of the schedule event (1-12)
uint8_t endDay; // Ending day of the schedule event (1-31)
uint8_t repeatMask; // Bitmask indicating repeat days of the week (bit0=Sunday ... bit6=Saturday)
uint8_t hour; // Hour of day when event triggers (0-23)
uint8_t minute; // Minute of hour when event triggers (0-59)
uint8_t presetId; // Preset number to apply when event triggers (1-250)
};

// Loads the schedule from the JSON file; returns true if successful
bool loadSchedule();

// Checks current time against schedule and applies any matching preset
void checkSchedule();
4 changes: 4 additions & 0 deletions wled00/wled.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "wled.h"
#include "wled_ethernet.h"
#include <Arduino.h>
#include "schedule.h"

#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DISABLE_BROWNOUT_DET)
#include "soc/soc.h"
Expand Down Expand Up @@ -54,6 +55,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 @@ -414,6 +416,8 @@ void WLED::setup()
#endif
updateFSInfo();

loadSchedule(); // Load schedule from file on startup

// generate module IDs must be done before AP setup
escapedMac = WiFi.macAddress();
escapedMac.replace(":", "");
Expand Down
109 changes: 90 additions & 19 deletions wled00/wled_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "html_pxmagic.h"
#endif
#include "html_cpal.h"
#include "schedule.h"

// define flash strings once (saves flash memory)
static const char s_redirecting[] PROGMEM = "Redirecting...";
Expand Down Expand Up @@ -174,33 +175,103 @@ static String msgProcessor(const String& var)
return String();
}

static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool isFinal) {
if (!correctPIN) {
if (isFinal) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg));
static void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool isFinal)
{
if (!correctPIN)
{
if (isFinal)
request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg));
return;
}
if (!index) {
String finalname = filename;
if (finalname.charAt(0) != '/') {

String finalname = filename;
if (!index)
{
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)
{
size_t written = request->_tempFile.write(data, len);
if (written != len)
{
DEBUG_PRINTLN(F("File write error during upload"));
request->_tempFile.close();
request->_tempFile = File(); // invalidate file handle
// Consider sending error response early
}
}
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..."));
} else {
if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) loadCustomPalettes();
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!"));

// Finalize upload
if (isFinal)
{
if (request->_tempFile)
request->_tempFile.close();

if (finalname.equals(F("/schedule.json")))
{
// Atomically replace old file
// First try rename (which overwrites on most filesystems)
if (!WLED_FS.rename("/schedule.json.tmp", "/schedule.json"))
{
// If rename failed, try remove then rename
WLED_FS.remove("/schedule.json");
if (!WLED_FS.rename("/schedule.json.tmp", "/schedule.json"))
{
DEBUG_PRINTLN(F("[Schedule] Failed to replace schedule file"));
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Failed to save schedule file."));
return;
}
}
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Schedule uploaded and applied."));
DEBUG_PRINTLN(F("[Schedule] Upload complete and applied."));

// Apply new schedule immediately
if (!loadSchedule())
{
DEBUG_PRINTLN(F("[Schedule] Failed to load new schedule"));
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Schedule uploaded but failed to load."));
return;
}
}
else
{
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