Skip to content
Draft
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
83 changes: 36 additions & 47 deletions wled00/cfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -679,37 +679,28 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
CJSON(macroCountdown, cntdwn["macro"]);
setCountdown();

JsonArray timers = tm["ins"];
uint8_t it = 0;
for (JsonObject timer : timers) {
if (it > 9) break;
if (it<8 && timer[F("hour")]==255) it=8; // hour==255 -> sunrise/sunset
CJSON(timerHours[it], timer[F("hour")]);
CJSON(timerMinutes[it], timer["min"]);
CJSON(timerMacro[it], timer["macro"]);

byte dowPrev = timerWeekday[it];
//note: act is currently only 0 or 1.
//the reason we are not using bool is that the on-disk type in 0.11.0 was already int
int actPrev = timerWeekday[it] & 0x01;
CJSON(timerWeekday[it], timer[F("dow")]);
if (timerWeekday[it] != dowPrev) { //present in JSON
timerWeekday[it] <<= 1; //add active bit
int act = timer["en"] | actPrev;
if (act) timerWeekday[it]++;
}
if (it<8) {
JsonObject start = timer["start"];
byte startm = start["mon"];
if (startm) timerMonth[it] = (startm << 4);
CJSON(timerDay[it], start["day"]);
JsonObject end = timer["end"];
CJSON(timerDayEnd[it], end["day"]);
byte endm = end["mon"];
if (startm) timerMonth[it] += endm & 0x0F;
if (!(timerMonth[it] & 0x0F)) timerMonth[it] += 12; //default end month to 12
JsonArray timersArray = tm["ins"];
if (!timersArray.isNull()) {
clearTimers();
for (JsonObject timer : timersArray) {
uint8_t h = timer[F("hour")] | 0;
int8_t m = timer[F("min")] | 0;
uint8_t p = timer[F("macro")] | 0;
uint8_t dow = timer[F("dow")] | 127;
uint8_t wd = (dow << 1) | ((timer[F("en")] | 0) ? 1 : 0);
uint8_t ms = 1, me = 12, ds = 1, de = 31;
JsonObject start = timer[F("start")];
if (!start.isNull()) {
ms = start[F("mon")] | 1;
ds = start[F("day")] | 1;
}
JsonObject end = timer[F("end")];
if (!end.isNull()) {
me = end[F("mon")] | 12;
de = end[F("day")] | 31;
}
addTimer(p, h, m, wd, ms, me, ds, de);
}
it++;
}

JsonObject ota = doc["ota"];
Expand Down Expand Up @@ -1206,23 +1197,21 @@ void serializeConfig(JsonObject root) {
cntdwn["macro"] = macroCountdown;

JsonArray timers_ins = timers.createNestedArray("ins");

for (unsigned i = 0; i < 10; i++) {
if (timerMacro[i] == 0 && timerHours[i] == 0 && timerMinutes[i] == 0) continue; // sunrise/sunset get saved always (timerHours=255)
JsonObject timers_ins0 = timers_ins.createNestedObject();
timers_ins0["en"] = (timerWeekday[i] & 0x01);
timers_ins0[F("hour")] = timerHours[i];
timers_ins0["min"] = timerMinutes[i];
timers_ins0["macro"] = timerMacro[i];
timers_ins0[F("dow")] = timerWeekday[i] >> 1;
if (i<8) {
JsonObject start = timers_ins0.createNestedObject("start");
start["mon"] = (timerMonth[i] >> 4) & 0xF;
start["day"] = timerDay[i];
JsonObject end = timers_ins0.createNestedObject("end");
end["mon"] = timerMonth[i] & 0xF;
end["day"] = timerDayEnd[i];
}
for (size_t i = 0; i < ::timers.size(); i++) {
const Timer& t = ::timers[i];
if (t.preset == 0 && t.hour == 0 && t.minute == 0) continue;
JsonObject ti = timers_ins.createNestedObject();
ti[F("en")] = t.isEnabled() ? 1 : 0;
ti[F("hour")] = t.hour;
ti[F("min")] = t.minute;
ti[F("macro")] = t.preset;
ti[F("dow")] = t.weekdays >> 1;
JsonObject start = ti.createNestedObject(F("start"));
start[F("mon")] = t.monthStart;
start[F("day")] = t.dayStart;
JsonObject end = ti.createNestedObject(F("end"));
end[F("mon")] = t.monthEnd;
end[F("day")] = t.dayEnd;
}

JsonObject ota = root.createNestedObject("ota");
Expand Down
2 changes: 2 additions & 0 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_C
#define WLED_MAX_DIGITAL_CHANNELS 3
#define WLED_MAX_ANALOG_CHANNELS 5
#define WLED_MIN_VIRTUAL_BUSSES 3 // no longer used for bus creation but used to distinguish S2/S3 in UI
#define WLED_MAX_TIMERS 16 // reduced limit for ESP8266 due to memory constraints
#else
#if !defined(LEDC_CHANNEL_MAX) || !defined(LEDC_SPEED_MODE_MAX)
#include "driver/ledc.h" // needed for analog/LEDC channel counts
Expand All @@ -81,6 +82,7 @@ constexpr size_t FIXED_PALETTE_COUNT = DYNAMIC_PALETTE_COUNT + FASTLED_PALETTE_C
//#define WLED_MAX_ANALOG_CHANNELS 16
#define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI
#endif
#define WLED_MAX_TIMERS 64 // maximum number of timers
#endif
// WLED_MAX_BUSSES was used to define the size of busses[] array which is no longer needed
// instead it will help determine max number of buses that can be defined at compile time
Expand Down
181 changes: 125 additions & 56 deletions wled00/data/settings_time.htm
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,41 @@
<script>
var el=false;
var ms=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
var timerCount = 0;
var maxTimers = 64;
var presets = {};
var sortedPresetOptions = '';
function S() {
getLoc();
loadPresets();
loadJS(getURL('/settings/s.js?p=5'), false, ()=>{BTa();}, ()=>{
updLatLon();
Cs();
FC();
}); // If we set async false, file is loaded and executed, then next statement is processed
});
if (loc) d.Sf.action = getURL('/settings/time');
}
function loadPresets() {
try {
var p = localStorage.getItem("wledP");
if (p) presets = JSON.parse(p);
} catch(e) {}
buildSortedPresetOptions();
}
function buildSortedPresetOptions() {
var arr = Object.entries(presets).sort((a,b)=>{
var nameA = (a[1].n || ("Preset " + a[0])).toLowerCase();
var nameB = (b[1].n || ("Preset " + b[0])).toLowerCase();
return nameA.localeCompare(nameB);
});
sortedPresetOptions = '';
for (var p of arr) {
if (p[0]=="0") continue;
var id = parseInt(p[0]);
var name = p[1].n || ("Preset " + id);
sortedPresetOptions += `<option value="${id}">${id}: ${name}</option>`;
}
}
function expand(o,i)
{
var t = gId("WD"+i);
Expand All @@ -26,79 +52,121 @@
function Cs() { gId("cac").style.display=(gN("OL").checked)?"block":"none"; }
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>
<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><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>";
var ih="<thead><tr><th>En.</th><th>Type/Hour</th><th>Minute</th><th>Preset</th><th></th></tr></thead><tbody id=\"timerRows\"></tbody>";
gId("TMT").innerHTML=ih;
}
function FC()
{
for(i=0;i<10;i++)
{
let wd = gId("W"+i).value;
for(j=0;j<8;j++) {
gId("W"+i+j).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
function addTimerRow(hour, minute, preset, weekdays, monthStart, dayStart, monthEnd, dayEnd) {
if (timerCount >= maxTimers) return;
var i = timerCount++;
var isSunrise = (hour === 255);
var isSunset = (hour === 254);
var isSpecial = isSunrise || isSunset;
Comment on lines +61 to +63
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace magic numbers 255 and 254 with named constants.

The special timer type values 255 (sunrise) and 254 (sunset) are magic numbers repeated throughout the code (lines 104-106, 134-136, 206, 225, 397-398). These should be defined as named constants at the top of the script for maintainability.

Based on learnings, add named constants:

+	var TIMER_TYPE_SUNRISE = 255;
+	var TIMER_TYPE_SUNSET = 254;
 	var pJson = {}; // Store presets
 	var presetsLoaded = false;

Then replace all occurrences:

-		var isSunrise = (hour === 255);
-		var isSunset = (hour === 254);
+		var isSunrise = (hour === TIMER_TYPE_SUNRISE);
+		var isSunset = (hour === TIMER_TYPE_SUNSET);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In wled00/data/settings_time.htm around lines 104-106 (and also update
occurrences at 134-136, 206, 225, 397-398), the code uses magic numbers 255 and
254 to indicate sunrise and sunset; define named constants (e.g., const
TIMER_SUNRISE = 255; const TIMER_SUNSET = 254;) near the top of the script and
replace all literal uses of 255 and 254 throughout the file with those
constants, ensuring any boolean checks (isSunrise/isSunset/isSpecial) and
comparisons use the new names and that no other numeric meanings are changed.

if (hour === undefined) hour = 0;
if (minute === undefined) minute = 0;
if (preset === undefined) preset = 0;
if (weekdays === undefined) weekdays = 255;
if (monthStart === undefined) monthStart = 1;
if (dayStart === undefined) dayStart = 1;
if (monthEnd === undefined) monthEnd = 12;
if (dayEnd === undefined) dayEnd = 31;
var enabled = weekdays & 1;
var dow = weekdays >> 1;
var tbody = gId("timerRows");
var tr = tbody.insertRow();
var hourVal = isSpecial ? 0 : hour;
var presetOpts = '<option value="0">Delete Timer</option>' + sortedPresetOptions.replace(new RegExp(`value="${preset}"`, 'g'), `value="${preset}" selected`);
tr.innerHTML = `<td><input name="W${i}" id="W${i}" type="hidden"><input id="W${i}0" type="checkbox" ${enabled?'checked':''}></td>
<td><select id="TS${i}" class="xs" onchange="TT(${i})"><option value="0" ${!isSpecial?'selected':''}>Regular</option><option value="255" ${isSunrise?'selected':''}>Sunrise</option><option value="254" ${isSunset?'selected':''}>Sunset</option></select><input ${isSpecial?'':'name="H'+i+'"'} id="H${i}" class="xs" type="number" min="0" max="24" value="${hourVal}" style="display:${isSpecial?'none':'inline'}"></td>
<td><input name="N${i}" id="N${i}" class="xs" type="number" min="${isSpecial?-120:0}" max="${isSpecial?120:59}" value="${minute}"></td>
<td><select name="T${i}" id="T${i}" class="s">${presetOpts}</select></td>
<td><div id="CB${i}" onclick="expand(this,${i})" class="cal">&#128197;</div></td>`;
tr = tbody.insertRow();
var ih = `<div id="WD${i}" style="display:none;background-color:#444;"><hr>Run on weekdays<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" ${(dow>>(j-1))&1?'checked':''}></td>`;
ih += `</tr></table>`;
ih += `<div id="DR${i}">from <select name="M${i}">`;
for (j=0;j<12;j++) ih += `<option value="${j+1}" ${monthStart==j+1?'selected':''}>${ms[j]}</option>`;
ih += `</select><input name="D${i}" class="xs" type="number" min="1" max="31" value="${dayStart}"> to <select name="P${i}">`;
for (j=0;j<12;j++) ih += `<option value="${j+1}" ${monthEnd==j+1?'selected':''}>${ms[j]}</option>`;
ih += `</select><input name="E${i}" class="xs" type="number" min="1" max="31" value="${dayEnd}"></div>`;
ih += `<hr></div>`;
tr.innerHTML = `<td colspan=5>${ih}</td>`;
}
function TT(i) {
var sel = gId("TS"+i);
var hour = gId("H"+i);
var min = gId("N"+i);
var isSpecial = sel.value != 0;
hour.style.display = isSpecial ? "none" : "inline";
if (isSpecial) {
// Save current hour value before switching to sunrise/sunset
hour.setAttribute("data-regular-hour", hour.value);
hour.removeAttribute("name");
min.min = -120;
min.max = 120;
} else {
// Restore saved hour value when switching back to regular
var savedHour = hour.getAttribute("data-regular-hour");
if (savedHour !== null && savedHour !== "") {
hour.value = savedHour;
}
hour.setAttribute("name", "H"+i);
min.min = 0;
min.max = 59;
if (min.value < 0 || min.value > 59) min.value = 0;
}
}
function FC() {
populateMacroPresets();
}
function populateMacroPresets() {
var presetOpts = '<option value="0">Default Action</option>' + sortedPresetOptions;
var fields = ['A0','A1','MC','MN'];
for (var f of fields) {
var inp = gN(f);
if (!inp) continue;
var val = inp.value || 0;
var sel = document.createElement('select');
sel.name = f;
sel.className = inp.className;
sel.required = inp.required;
sel.innerHTML = presetOpts;
sel.value = val;
inp.parentNode.replaceChild(sel, inp);
}
}
function Wd()
{
a = [0,0,0,0,0,0,0,0,0,0];
for (i=0; i<10; i++) {
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 (i=0; i<timerCount; i++) {
var m=1, val=0;
for(j=0;j<8;j++) { val+=gId(("W"+i)+j).checked*m; m*=2;}
gId("W"+i).value=val;
var sel = gId("TS"+i);
var hour = gId("H"+i);
if (sel && sel.value != 0) {
// Re-add name attribute and set value for sunrise/sunset before submission
hour.setAttribute("name", "H"+i);
hour.value = sel.value;
}
}
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); }
}
function addRow(i,p,l,d) {
var t = gId("macros"); // table
var rCnt = t.rows.length; // get the number of rows.
var tr = t.insertRow(rCnt); // table row.
var t = gId("macros");
var rCnt = t.rows.length;
var tr = t.insertRow(rCnt);
var b = String.fromCharCode((i<10?48:55)+i);
var td = document.createElement('td'); // TABLE DEFINITION.
var presetOpts = '<option value="0">Default Action</option>' + sortedPresetOptions;
var td = document.createElement('td');
td = tr.insertCell(0);
td.innerHTML = `Button ${i}:`;
td = tr.insertCell(1);
td.innerHTML = `<input name="MP${b}" type="number" class="s" min="0" max="250" value="${p}" required>`;
td.innerHTML = `<select name="MP${b}" class="s" required>${presetOpts.replace(`value="${p}"`, `value="${p}" selected`)}</select>`;
td = tr.insertCell(2);
td.innerHTML = `<input name="ML${b}" type="number" class="s" min="0" max="250" value="${l}" required>`;
td.innerHTML = `<select name="ML${b}" class="s" required>${presetOpts.replace(`value="${l}"`, `value="${l}" selected`)}</select>`;
td = tr.insertCell(3);
td.innerHTML = `<input name="MD${b}" type="number" class="s" min="0" max="250" value="${d}" required>`;
td.innerHTML = `<select name="MD${b}" class="s" required>${presetOpts.replace(`value="${d}"`, `value="${d}" selected`)}</select>`;
}
function getLatLon() {
if (!el) {
Expand Down Expand Up @@ -203,6 +271,7 @@ <h3>Button actions</h3>
<h3>Time-controlled presets</h3>
<div style="display: inline-block">
<table id="TMT" style="min-width:330px;"></table>
<button type="button" onclick="addTimerRow()">Add Timer</button>
</div>
<hr>
<button type="button" onclick="B()">Back</button><button type="submit">Save</button>
Expand Down
28 changes: 28 additions & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,34 @@ void checkTimers();
void calculateSunriseAndSunset();
void setTimeFromAPI(uint32_t timein);

const uint8_t TH_SUNRISE = 255;
const uint8_t TH_SUNSET = 254;

struct Timer {
uint8_t preset;
uint8_t hour;
int8_t minute;
uint8_t weekdays;
uint8_t monthStart;
uint8_t monthEnd;
uint8_t dayStart;
uint8_t dayEnd;
inline bool isEnabled() const { return (weekdays & 0x01) && (preset != 0); }
inline bool isSunrise() const { return hour == TH_SUNRISE; }
inline bool isSunset() const { return hour == TH_SUNSET; }
inline bool isRegular() const { return hour < TH_SUNSET; }
Timer() : preset(0), hour(0), minute(0), weekdays(255), monthStart(1), monthEnd(12), dayStart(1), dayEnd(31) {}
Timer(uint8_t p, uint8_t h, int8_t m, uint8_t wd, uint8_t ms = 1, uint8_t me = 12, uint8_t ds = 1, uint8_t de = 31)
: preset(p), hour(h), minute(m), weekdays(wd), monthStart(ms), monthEnd(me), dayStart(ds), dayEnd(de) {}
};

void addTimer(uint8_t preset, uint8_t hour, int8_t minute, uint8_t weekdays,
uint8_t monthStart = 1, uint8_t monthEnd = 12, uint8_t dayStart = 1, uint8_t dayEnd = 31);
void removeTimer(size_t index);
void clearTimers();
size_t getTimerCount();
void compactTimers();

//overlay.cpp
void handleOverlayDraw();
void _overlayAnalogCountdown();
Expand Down
Loading