Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
213 changes: 173 additions & 40 deletions wled00/data/settings_time.htm
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<script>
var el=false;
var ms=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
var maxTimePresets = 16; // Maximum number of time-controlled presets
var currentPresetCount = 3; // Current number of presets (sunrise + sunset + 1 permanent time preset)
function S() {
getLoc();
loadJS(getURL('/settings/s.js?p=5'), false, ()=>{BTa();}, ()=>{
Expand All @@ -27,60 +29,183 @@
function BTa()
{
var ih="<thead><tr><th>En.</th><th>Hour</th><th>Minute</th><th>Preset</th><th></th></tr></thead>";
for (i=0;i<8;i++) {
ih+=`<tr><td><input name="W${i}" id="W${i}" type="hidden"><input id="W${i}0" type="checkbox"></td>
<td><input name="H${i}" class="xs" type="number" min="0" max="24"></td>
<td><input name="N${i}" class="xs" type="number" min="0" max="59"></td>
// Generate sunrise preset (0)
ih += generatePresetRow(0, true, "Sunrise");
// Generate sunset preset (1)
ih += generatePresetRow(1, true, "Sunset");
// Generate one permanent time preset (2)
ih += generatePresetRow(2, false, null, false);
// Generate any additional dynamic presets (starting from 3)
for (i=3; i<currentPresetCount; i++) {
ih += generatePresetRow(i, false, null, true);
}
gId("TMT").innerHTML=ih;
}

function generatePresetRow(i, isSunEvent = false, sunType = null, isDynamic = false) {
var ih = "";
var hourInput = isSunEvent ?
`${sunType}<input name="H${i}" value="255" type="hidden">` :
`<input name="H${i}" class="xs" type="number" min="0" max="24">`;

ih += `<tr id="preset-row-${i}"><td><input name="W${i}" id="W${i}" type="hidden"><input id="W${i}0" type="checkbox"></td>
<td>${hourInput}</td>
<td><input name="N${i}" class="xs" type="number" min="${isSunEvent ? '-59' : '0'}" max="59"></td>
<td><input name="T${i}" class="s" type="number" min="0" max="250"></td>
<td><div id="CB${i}" onclick="expand(this,${i})" class="cal">&#128197;</div></td></tr>`;
ih+=`<tr><td colspan=5><div id="WD${i}" style="display:none;background-color:#444;"><hr>Run on weekdays`;
ih+=`<table><tr><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th><th>S</th></tr><tr>`
for (j=1;j<8;j++) ih+=`<td><input id="W${i}${j}" type="checkbox"></td>`;
ih+=`</tr></table>from <select name="M${i}">`;
for (j=0;j<12;j++) ih+=`<option value="${j+1}">${ms[j]}</option>`;
ih+=`</select><input name="D${i}" class="xs" type="number" min="1" max="31"></input> to <select name="P${i}">`;
for (j=0;j<12;j++) ih+=`<option value="${j+1}">${ms[j]}</option>`;
ih+=`</select><input name="E${i}" class="xs" type="number" min="1" max="31"></input>
<hr></div></td></tr>`;

ih += `<tr id="preset-detail-${i}"><td colspan=5><div id="WD${i}" style="display:none;background-color:#444;"><hr>`;

if (!isSunEvent) {
ih += "Run on weekdays";
}

ih += `<table><tr><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th><th>S</th></tr><tr>`;
for (j=1;j<8;j++) ih += `<td><input id="W${i}${j}" type="checkbox"></td>`;
ih += "</tr></table>";

if (!isSunEvent) {
ih += `from <select name="M${i}">`;
for (j=0;j<12;j++) ih += `<option value="${j+1}">${ms[j]}</option>`;
ih += `</select><input name="D${i}" class="xs" type="number" min="1" max="31"></input> to <select name="P${i}">`;
for (j=0;j<12;j++) ih += `<option value="${j+1}">${ms[j]}</option>`;
ih += `</select><input name="E${i}" class="xs" type="number" min="1" max="31"></input>`;
}

ih += "<hr></div></td></tr>";
return ih;
}

function addTimePreset() {
if (currentPresetCount >= maxTimePresets) {
alert(`Maximum of ${maxTimePresets} time presets allowed.`);
return;
}

var table = gId("TMT");
var newRowHTML = generatePresetRow(currentPresetCount, false, null, true);
table.insertAdjacentHTML("beforeend", newRowHTML);

currentPresetCount++;
updateButtonStates();
}

function removePreset(index) {
if (index < 3) {
alert("Cannot remove built-in presets (Sunrise, Sunset, and first time preset).");
return;
}

// Remove the preset rows
var presetRow = gId(`preset-row-${index}`);
var detailRow = gId(`preset-detail-${index}`);

if (presetRow) presetRow.remove();
if (detailRow) detailRow.remove();

// Reindex remaining presets
reindexPresets();
currentPresetCount--;
updateButtonStates();
}

function removeLastPreset() {
if (currentPresetCount <= 3) {
alert("Cannot remove built-in presets. Only dynamic presets (3+) can be removed.");
return;
}

// Find the highest index dynamic preset and remove it
var lastIndex = currentPresetCount - 1;
removePreset(lastIndex);
}

function reindexPresets() {
var table = gId("TMT");
var rows = table.querySelectorAll("tr[id^='preset-row-']");
var newIndex = 3;

rows.forEach((row) => {
var oldIndex = row.id.split('-')[2];
if (parseInt(oldIndex) >= 3) {
updatePresetRowIndex(row, oldIndex, newIndex);
var detailRow = gId(`preset-detail-${oldIndex}`);
if (detailRow) {
updatePresetDetailRowIndex(detailRow, oldIndex, newIndex);
}
newIndex++;
}
});
}

function updatePresetRowIndex(row, oldIndex, newIndex) {
row.id = `preset-row-${newIndex}`;
row.innerHTML = row.innerHTML
.replace(new RegExp(`name="W${oldIndex}"`, 'g'), `name="W${newIndex}"`)
.replace(new RegExp(`id="W${oldIndex}`, 'g'), `id="W${newIndex}`)
.replace(new RegExp(`name="H${oldIndex}"`, 'g'), `name="H${newIndex}"`)
.replace(new RegExp(`name="N${oldIndex}"`, 'g'), `name="N${newIndex}"`)
.replace(new RegExp(`name="T${oldIndex}"`, 'g'), `name="T${newIndex}"`)
.replace(new RegExp(`id="CB${oldIndex}"`, 'g'), `id="CB${newIndex}"`)
.replace(new RegExp(`expand\(this,${oldIndex}\)`, 'g'), `expand(this,${newIndex})`)
.replace(new RegExp(`removePreset\(${oldIndex}\)`, 'g'), `removePreset(${newIndex})`);
}

function updatePresetDetailRowIndex(row, oldIndex, newIndex) {
row.id = `preset-detail-${newIndex}`;
row.innerHTML = row.innerHTML
.replace(new RegExp(`id="WD${oldIndex}"`, 'g'), `id="WD${newIndex}"`)
.replace(new RegExp(`id="W${oldIndex}`, 'g'), `id="W${newIndex}`)
.replace(new RegExp(`name="M${oldIndex}"`, 'g'), `name="M${newIndex}"`)
.replace(new RegExp(`name="D${oldIndex}"`, 'g'), `name="D${newIndex}"`)
.replace(new RegExp(`name="P${oldIndex}"`, 'g'), `name="P${newIndex}"`)
.replace(new RegExp(`name="E${oldIndex}"`, 'g'), `name="E${newIndex}"`);
}

function updateButtonStates() {
var addBtn = gId("addPresetBtn");
var removeBtn = gId("removePresetBtn");

if (addBtn) {
addBtn.disabled = currentPresetCount >= maxTimePresets;
}

if (removeBtn) {
removeBtn.disabled = currentPresetCount <= 3;
}
ih+=`<tr><td><input name="W8" id="W8" type="hidden"><input id="W80" type="checkbox"></td>
<td>Sunrise<input name="H8" value="255" type="hidden"></td>
<td><input name="N8" class="xs" type="number" min="-59" max="59"></td>
<td><input name="T8" class="s" type="number" min="0" max="250"></td>
<td><div id="CB8" onclick="expand(this,8)" class="cal">&#128197;</div></td></tr><tr><td colspan=5>`;
ih+=`<div id="WD8" style="display:none;background-color:#444;"><hr><table><tr><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th><th>S</th></tr><tr>`;
for (j=1;j<8;j++) ih+=`<td><input id="W8${j}" type="checkbox"></td>`;
ih+="</tr></table><hr></div></td></tr>";
ih+=`<tr><td><input name="W9" id="W9" type="hidden"><input id="W90" type="checkbox"></td>
<td>Sunset<input name="H9" value="255" type="hidden"></td>
<td><input name="N9" class="xs" type="number" min="-59" max="59"></td>
<td><input name="T9" class="s" type="number" min="0" max="250"></td>
<td><div id="CB9" onclick="expand(this,9)" class="cal">&#128197;</div></td></tr><tr><td colspan=5>`;
ih+=`<div id="WD9" style="display:none;background-color:#444;"><hr><table><tr><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th><th>S</th></tr><tr>`;
for (j=1;j<8;j++) ih+=`<td><input id="W9${j}" type="checkbox"></td>`;
ih+="</tr></table><hr></div></td></tr>";
gId("TMT").innerHTML=ih;
}
function FC()
{
for(i=0;i<10;i++)
for(i=0;i<currentPresetCount;i++)
{
let wd = gId("W"+i).value;
let wd = gId("W"+i);
if (!wd) continue; // Skip if preset doesn't exist
wd = wd.value;
for(j=0;j<8;j++) {
gId("W"+i+j).checked=wd>>j&1;
let checkbox = gId("W"+i+j);
if (checkbox) checkbox.checked=wd>>j&1;
}
if ((wd&254) != 254 || (i<8 && (gN("M"+i).value != 1 || gN("D"+i).value != 1 || gN("P"+i).value != 12 || gN("E"+i).value != 31))) {
expand(gId("CB"+i),i); //expand macros with custom DOW or date range set
if ((wd&254) != 254 || (i<8 && (gN("M"+i) && gN("D"+i) && gN("P"+i) && gN("E"+i) &&
(gN("M"+i).value != 1 || gN("D"+i).value != 1 || gN("P"+i).value != 12 || gN("E"+i).value != 31)))) {
let cbElement = gId("CB"+i);
if (cbElement) expand(cbElement,i); //expand macros with custom DOW or date range set
}
}
updateAddButtonState();
}
function Wd()
{
a = [0,0,0,0,0,0,0,0,0,0];
for (i=0; i<10; i++) {
a = [];
for (i=0; i<currentPresetCount; i++) {
a[i] = 0;
m=1;
for(j=0;j<8;j++) { a[i]+=gId(("W"+i)+j).checked*m; m*=2;}
gId("W"+i).value=a[i];
for (j=0; j<8; j++) {
let checkbox = gId("W"+i+j);
if (checkbox && checkbox.checked) a[i]+=m;
m*=2;
}
let hiddenInput = gId("W"+i);
if (hiddenInput) hiddenInput.value = a[i];
}
Comment on lines 177 to 209
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Good adaptation to dynamic presets with minor issue.

The FC() and Wd() functions are correctly updated to handle dynamic presets with proper null checks. However, line 194 calls updateAddButtonState() which doesn't exist - should this be updateButtonStates()?

-updateAddButtonState();
+updateButtonStates();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function FC()
{
for(i=0;i<10;i++)
for(i=0;i<currentPresetCount;i++)
{
let wd = gId("W"+i).value;
let wd = gId("W"+i);
if (!wd) continue; // Skip if preset doesn't exist
wd = wd.value;
for(j=0;j<8;j++) {
gId("W"+i+j).checked=wd>>j&1;
let checkbox = gId("W"+i+j);
if (checkbox) checkbox.checked=wd>>j&1;
}
if ((wd&254) != 254 || (i<8 && (gN("M"+i).value != 1 || gN("D"+i).value != 1 || gN("P"+i).value != 12 || gN("E"+i).value != 31))) {
expand(gId("CB"+i),i); //expand macros with custom DOW or date range set
if ((wd&254) != 254 || (i<8 && (gN("M"+i) && gN("D"+i) && gN("P"+i) && gN("E"+i) &&
(gN("M"+i).value != 1 || gN("D"+i).value != 1 || gN("P"+i).value != 12 || gN("E"+i).value != 31)))) {
let cbElement = gId("CB"+i);
if (cbElement) expand(cbElement,i); //expand macros with custom DOW or date range set
}
}
updateAddButtonState();
}
function Wd()
{
a = [0,0,0,0,0,0,0,0,0,0];
for (i=0; i<10; i++) {
a = [];
for (i=0; i<currentPresetCount; i++) {
a[i] = 0;
m=1;
for(j=0;j<8;j++) { a[i]+=gId(("W"+i)+j).checked*m; m*=2;}
gId("W"+i).value=a[i];
for (j=0; j<8; j++) {
let checkbox = gId("W"+i+j);
if (checkbox && checkbox.checked) a[i]+=m;
m*=2;
}
let hiddenInput = gId("W"+i);
if (hiddenInput) hiddenInput.value = a[i];
}
function FC()
{
for(i=0;i<currentPresetCount;i++)
{
let wd = gId("W"+i);
if (!wd) continue; // Skip if preset doesn't exist
wd = wd.value;
for(j=0;j<8;j++) {
let checkbox = gId("W"+i+j);
if (checkbox) checkbox.checked=wd>>j&1;
}
if ((wd&254) != 254 || (i<8 && (gN("M"+i) && gN("D"+i) && gN("P"+i) && gN("E"+i) &&
(gN("M"+i).value != 1 || gN("D"+i).value != 1 || gN("P"+i).value != 12 || gN("E"+i).value != 31))))
{
let cbElement = gId("CB"+i);
if (cbElement) expand(cbElement,i); //expand macros with custom DOW or date range set
}
}
- updateAddButtonState();
+ updateButtonStates();
}
function Wd()
{
a = [];
for (i=0; i<currentPresetCount; i++) {
a[i] = 0;
m=1;
for (j=0; j<8; j++) {
let checkbox = gId("W"+i+j);
if (checkbox && checkbox.checked) a[i]+=m;
m*=2;
}
let hiddenInput = gId("W"+i);
if (hiddenInput) hiddenInput.value = a[i];
}
🤖 Prompt for AI Agents
In wled00/data/settings_time.htm around lines 177 to 209, the function FC()
calls updateAddButtonState() on line 194, but this function is not defined.
Replace this call with updateButtonStates() to fix the undefined function error
and ensure the correct button state update logic is executed.

if (d.Sf.LTR.value==="S") { d.Sf.LT.value = -1*parseFloat(d.Sf.LT.value); }
if (d.Sf.LNR.value==="W") { d.Sf.LN.value = -1*parseFloat(d.Sf.LN.value); }
Expand Down Expand Up @@ -178,6 +303,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 All @@ -202,7 +332,10 @@ <h3>Button actions</h3>
<a href="https://kno.wled.ge/features/macros/#analog-button" target="_blank">Analog Button setup</a>
<h3>Time-controlled presets</h3>
<div style="display: inline-block">
<table id="TMT" style="min-width:330px;"></table>
<table id="TMT" style="min-width:380px; margin: 0 auto;"></table>
<br>
<button type="button" id="addPresetBtn" onclick="addTimePreset()">+</button>
<button type="button" id="removePresetBtn" onclick="removeLastPreset()" style="margin-left: 5px;">-</button>
</div>
<hr>
<button type="button" onclick="B()">Back</button><button type="submit">Save</button>
Expand Down
141 changes: 141 additions & 0 deletions wled00/schedule.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// schedule.cpp
// Handles reading, parsing, and checking the preset schedule from schedule.json

#include "schedule.h"
#include "wled.h"
#include <time.h>
#include <vector>

#define SCHEDULE_FILE "/schedule.json"
#define SCHEDULE_JSON_BUFFER_SIZE 4096

// Array to hold scheduled events, max size defined in schedule.h
std::vector<ScheduleEvent> scheduleEvents;

// 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 (size_t i = 0; i < scheduleEvents.size(); 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(SCHEDULE_JSON_BUFFER_SIZE);
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;
}

scheduleEvents.clear();
for (JsonObject e : doc.as<JsonArray>()) {

// 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"), (uint16_t)scheduleEvents.size());
continue;
}

scheduleEvents.push_back({
(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"), (uint16_t)scheduleEvents.size());

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

return true;
}
Loading