Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
79 changes: 41 additions & 38 deletions wled00/cfg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -694,37 +694,35 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
CJSON(macroCountdown, cntdwn["macro"]);
setCountdown();

// Load timers into new vector-based system
clearTimers();

JsonArray timers = tm["ins"];
uint8_t it = 0;
for (JsonObject timer : timers) {
if (it > 9) break;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Where do you handle legacy configuration? Consider upgrade path.

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) {
// Extract timer data from JSON
uint8_t hour = timer[F("hour")] | 0;
int8_t minute = timer["min"] | 0;
uint8_t preset = timer["macro"] | 0;

// Handle weekdays and enabled state
uint8_t weekdays = timer[F("dow")] | 0;
weekdays <<= 1; // shift to make room for enabled bit
bool enabled = timer["en"] | false;
if (enabled) weekdays |= 0x01; // set enabled bit

// Handle date range (only for regular timers)
uint8_t monthStart = 1, monthEnd = 12, dayStart = 1, dayEnd = 31;
if (hour < 254) { // regular timer
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
monthStart = start["mon"] | 1;
dayStart = start["day"] | 1;
monthEnd = end["mon"] | 12;
dayEnd = end["day"] | 31;
}
it++;

// Add timer to vector system
addTimer(preset, hour, minute, weekdays, monthStart, monthEnd, dayStart, dayEnd);
}

JsonObject ota = doc["ota"];
Expand Down Expand Up @@ -1194,21 +1192,26 @@ void serializeConfig(JsonObject root) {

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)
// Access the global timers vector from ntp.cpp
for (const auto& timer : ::timers) {
// Skip completely empty timers (but save sunrise/sunset even if preset=0)
if (timer.preset == 0 && timer.hour < 254 && timer.minute == 0) continue;

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) {
timers_ins0["en"] = timer.isEnabled();
timers_ins0[F("hour")] = timer.hour;
timers_ins0["min"] = timer.minute;
timers_ins0["macro"] = timer.preset;
timers_ins0[F("dow")] = timer.weekdays >> 1; // remove enabled bit

// Add date range for regular timers only
if (timer.isRegular()) {
JsonObject start = timers_ins0.createNestedObject("start");
start["mon"] = (timerMonth[i] >> 4) & 0xF;
start["day"] = timerDay[i];
start["mon"] = timer.monthStart;
start["day"] = timer.dayStart;
JsonObject end = timers_ins0.createNestedObject("end");
end["mon"] = timerMonth[i] & 0xF;
end["day"] = timerDayEnd[i];
end["mon"] = timer.monthEnd;
end["day"] = timer.dayEnd;
}
}

Expand Down
154 changes: 96 additions & 58 deletions wled00/data/settings_time.htm
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
<script>
var el=false;
var ms=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
var maxTimePresets = 16;
var currentPresetCount = 0;

function S() {
getLoc();
loadJS(getURL('/settings/s.js?p=5'), false, ()=>{BTa();}, ()=>{
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 expand(o,i)
Expand All @@ -24,63 +28,93 @@
o.innerHTML = t.style.display==="none" ? "&#128197;" : "&#x2715;";
}
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>`;
function BTa() {
var ih = "<thead><tr><th>En.</th><th>Hour</th><th>Minute</th><th>Preset</th><th></th></tr></thead><tbody>";

// Start with at least 1 timer row
currentPresetCount = Math.max(1, currentPresetCount);

for (var i = 0; i < currentPresetCount; i++) {
ih += generatePresetRow(i);
}
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;

ih += "</tbody>";
gId("TMT").innerHTML = ih;
updateButtonStates();
}
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 generatePresetRow(i) {
var ih = "";

ih += `<tr><td><input name="W${i}" id="W${i}" type="hidden"><input id="W${i}0" type="checkbox" checked></td>`;
ih += `<td><input name="H${i}" class="xs" type="number" min="0" max="23"></td>`;
ih += `<td><input name="N${i}" class="xs" type="number" min="0" max="59"></td>`;
ih += `<td><input name="T${i}" class="s" type="number" min="0" max="250"></td>`;
ih += `<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>`;
ih += `<div style="text-align:center;">Run on weekdays<br>`;
ih += `<table style="margin:10px auto;"><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 (var j=1;j<8;j++) {
ih += `<td><input id="W${i}${j}" type="checkbox" checked></td>`;
}
ih += "</tr></table>";

ih += `from <select name="M${i}">`;
for (var 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" value="1"> to <select name="P${i}">`;
for (var j=0;j<12;j++) {
ih += `<option value="${j+1}" ${j==11?'selected':''}>${ms[j]}</option>`;
}
ih += `</select><input name="E${i}" class="xs" type="number" min="1" max="31" value="31">`;
ih += "</div><hr></div></td></tr>";
return ih;
}
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];

function addTimePreset() {
if (currentPresetCount >= maxTimePresets) return;

var table = gId("TMT");
var tbody = table.querySelector("tbody");
var newRowHTML = generatePresetRow(currentPresetCount);
tbody.insertAdjacentHTML("beforeend", newRowHTML);

currentPresetCount++;
updateButtonStates();
}

function removeLastPreset() {
if (currentPresetCount <= 1) return;

// Remove the last 2 rows (main + detail) from tbody
var table = gId("TMT");
var tbody = table.querySelector("tbody");
var rows = tbody.rows;
if (rows.length >= 2) {
tbody.deleteRow(rows.length - 1);
tbody.deleteRow(rows.length - 1);
}

currentPresetCount--;
updateButtonStates();
}

function updateButtonStates() {
gId("+").style.display = (currentPresetCount<maxTimePresets) ? "inline":"none";
gId("-").style.display = (currentPresetCount>1) ? "inline":"none";
}

function Wd() {
a = [];
for (i=0; i<currentPresetCount; i++) {
a[i] = 0;
if (gId("W"+i+"0").checked) a[i] |= 0x01;
for (j=1; j<=7; j++) {
if (gId("W"+i+j).checked) a[i] |= (1 << j);
}
gId("W"+i).value = a[i];
}
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 @@ -201,8 +235,12 @@ <h3>Button actions</h3>
</table>
<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>
<div style="display: inline-block;">
<table id="TMT" style="min-width:330px; margin: 0 auto;"></table>
<div style="text-align: center; margin: 10px 0;">
<button type="button" id="+" onclick="addTimePreset()">+</button>
<button type="button" id="-" onclick="removeLastPreset()">-</button>
</div>
</div>
<hr>
<button type="button" onclick="B()">Back</button><button type="submit">Save</button>
Expand Down
40 changes: 40 additions & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,46 @@ void setCountdown();
byte weekdayMondayFirst();
void checkTimers();
void calculateSunriseAndSunset();

// Timer management functions
void addTimer(uint8_t preset, uint8_t hour, int8_t minute, uint8_t weekdays, uint8_t monthStart, uint8_t monthEnd, uint8_t dayStart, uint8_t dayEnd);
void clearTimers();
void syncTimersToArrays();
uint8_t getTimerCount();
uint8_t getRegularTimerCount();
bool hasSunriseTimer();
bool hasSunsetTimer();

// Timer constants
const uint8_t maxTimePresets = 16;

// Timer special hour values
const uint8_t TIMER_HOUR_SUNRISE = 255; // Special value for sunrise timer
const uint8_t TIMER_HOUR_SUNSET = 254; // Special value for sunset timer

// External timer declarations
Copy link
Collaborator

Choose a reason for hiding this comment

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

These should go into .cpp file.

struct Timer {
uint8_t preset;
uint8_t hour;
int8_t minute;
uint8_t weekdays;
uint8_t monthStart, monthEnd;
uint8_t dayStart, dayEnd;

// Constructor
Timer(uint8_t p = 0, uint8_t h = 0, int8_t m = 0, uint8_t w = 0,
uint8_t ms = 1, uint8_t me = 12, uint8_t ds = 1, uint8_t de = 31)
: preset(p), hour(h), minute(m), weekdays(w),
monthStart(ms), monthEnd(me), dayStart(ds), dayEnd(de) {}

bool isEnabled() const { return weekdays != 0; } // Timer is enabled if any weekday bit is set
bool isSunrise() const { return hour == TIMER_HOUR_SUNRISE; }
bool isSunset() const { return hour == TIMER_HOUR_SUNSET; }
bool isRegular() const { return hour < TIMER_HOUR_SUNSET; }
};

extern std::vector<Timer> timers;

void setTimeFromAPI(uint32_t timein);

//overlay.cpp
Expand Down
Loading