Files
WLED/wled00/data/settings_time.htm
Benjam Welker 4a6ff64519 add more macro/timer slots (#5140)
* convert to vectors

* Update wled00/cfg.cpp

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* UI fixes

converted preset entry to select box for ease of use

* no eeprom compatibility

* tighten up code

* fix sunrise/sunset issue

* fix issue with sunrise/sunset/regular selector

* preset dropdowns

* sort before use

* delete timer when preset = 0

* truncate long names and add sort options

* html updates

* fix save bug

* make boxes a bit darker

* add fallback if localStorage is empty

* remove sort option

* code rabbit suggested fixes

* remove css

* restyling

* common

* dry preset loading

* revert missing const

* update deep sleep usermod with new timer struct

* tabs

* Revert "tabs"

This reverts commit fdc01f2f5d.

* remove unneeded clamp checks

* leave index alone

* remove useless check

* fix up await functions

* only fetch localstorage

* inline single line function

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-29 13:01:24 +02:00

391 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>Time Settings</title>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
var el=false;
var ms=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
var timerCount = 0;
var maxTimers;
var presets = {};
var sortedPresetOptions = '';
const TIMER_SUNRISE = 255;
const TIMER_SUNSET = 254;
const TIMER_WEEKDAYS_ALL = 255;
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function S() {
getLoc();
populatePresets(true);
loadJS(getURL('/settings/s.js?p=5'), false, ()=>{BTa();}, ()=>{
updLatLon();
Cs();
});
if (loc) d.Sf.action = getURL('/settings/time');
}
function populatePresets(fromls) {
let json = null;
if (fromls) {
try {
json = JSON.parse(localStorage.getItem("wledP"));
} catch (e) {}
}
if (!json) {
showToast("Please visit the main UI first to load presets", true);
return;
}
onPresetsLoaded(json);
}
function onPresetsLoaded(json) {
presets = json;
delete presets["0"];
bSPO();
pMP();
rTOPO();
rBPO();
}
function bSPO() { // buildSortedPresetOptions
sortedPresetOptions = '';
for (var p of Object.entries(presets)) {
var id = p[0];
var name = p[1].n || ("Preset " + id);
sortedPresetOptions += `<option value="${id}">${id}: ${name}</option>`;
}
}
function sPSV(sel, value, attrName) { // setPresetSelectValue
var v = (value === undefined || value === null || value === "") ? "0" : String(value);
sel.setAttribute(attrName, v);
sel.value = v;
if (sel.value === "") sel.value = "0";
}
function rPS(sel, presetOpts, attrName) { // refreshPresetSelect
var currentVal = sel.value;
var initialVal = sel.getAttribute(attrName);
var val = currentVal;
if ((val === "" || val === "0") && initialVal !== null && initialVal !== "" && initialVal !== "0") {
val = initialVal;
}
if (val === "") val = "0";
sel.innerHTML = presetOpts;
sel.value = val;
if (sel.value === "" && initialVal !== null && initialVal !== "") sel.value = initialVal;
if (sel.value === "") sel.value = "0";
}
function expand(o,i)
{
var t = gId("WD"+i);
t.style.display = t.style.display!=="none" ? "none" : "";
o.innerHTML = t.style.display==="none" ? "&#128197;" : "&#x2715;";
}
function Cs() { gId("cac").style.display=(gN("OL").checked)?"block":"none"; }
function BTa()
{
timerCount = 0;
gId("TMT").innerHTML = "<thead><tr><th>En.</th><th>Type</th><th>Hour</th><th>Minute</th><th></th></tr></thead>";
}
function addTimerRow(hour, minute, preset, weekdays, monthStart, dayStart, monthEnd, dayEnd) {
if (timerCount >= maxTimers) return;
var i = timerCount++;
var isSunrise = (hour === TIMER_SUNRISE);
var isSunset = (hour === TIMER_SUNSET);
var isSpecial = isSunrise || isSunset;
if (hour === undefined) hour = 0;
if (minute === undefined) minute = 0;
if (preset === undefined) preset = 0;
if (weekdays === undefined) weekdays = TIMER_WEEKDAYS_ALL;
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 container = gId("TMT");
var hourVal = isSpecial ? 0 : hour;
var presetOpts = '<option value="0">Delete Timer</option>' + sortedPresetOptions;
var weekdayTable = '<table class="tw"><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++) weekdayTable += `<td><input id="W${i}${j}" type="checkbox" ${(dow>>(j-1))&1?'checked':''}></td>`;
weekdayTable += '</tr></table>';
var dateRange = `<div id="DR${i}" class="tdr"><span class="tdf">from <select name="M${i}">`;
for (j=0;j<12;j++) dateRange += `<option value="${j+1}" ${monthStart==j+1?'selected':''}>${ms[j]}</option>`;
dateRange += `</select><input name="D${i}" class="xs" type="number" min="1" max="31" value="${dayStart}"></span><span class="tdf">to <select name="P${i}">`;
for (j=0;j<12;j++) dateRange += `<option value="${j+1}" ${monthEnd==j+1?'selected':''}>${ms[j]}</option>`;
dateRange += `</select><input name="E${i}" class="xs" type="number" min="1" max="31" value="${dayEnd}"></span></div>`;
var timerMain = `
<tr>
<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="s" onchange="TT(${i})">
<option value="0" ${!isSpecial ? "selected" : ""}>Regular</option>
<option value="${TIMER_SUNRISE}" ${isSunrise ? "selected" : ""}>Sunrise</option>
<option value="${TIMER_SUNSET}" ${isSunset ? "selected" : ""}>Sunset</option>
</select><br>
</td><td>
<input ${isSpecial ? "" : 'name="H'+i+'"'} id="H${i}" class="s" type="number" min="0" max="24" value="${hourVal}" ${isSpecial ? "disabled" : ""}>
</td>
<td><input name="N${i}" id="N${i}" class="l" type="number" min="${isSpecial ? -120 : 0}" max="${isSpecial ? 120 : 59}" value="${minute}"></td>
<td><div id="CB${i}" onclick="expand(this,${i})" class="cal">&#128197;</div></td>
</tr><tr>
<td colspan="5">
<select name="T${i}" id="T${i}" class="s">${presetOpts}</select>
</td>
</tr><tr><td colspan="5"><hr></td></tr>
`;
var timerExpanded = `
<tr>
<td colspan="5">
<div id="WD${i}" style="display:none;background-color:#444;">
<hr>Run on weekdays
${weekdayTable}
${dateRange}
<hr>
</div>
</td>
</tr>
`;
container.insertAdjacentHTML("beforeend", timerMain + timerExpanded);
var timerPresetSel = gId("T"+i);
sPSV(timerPresetSel, preset, "data-preset");
}
function TT(i) {
var sel = gId("TS"+i);
var hour = gId("H"+i);
var min = gId("N"+i);
var isSpecial = sel.value != 0;
hour.disabled = isSpecial;
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 pMP() { // 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.getAttribute("data-preset-value");
if (val === null || val === "") val = inp.value;
if (inp.tagName === "SELECT") {
sPSV(inp, val, "data-preset-value");
rPS(inp, presetOpts, "data-preset-value");
} else {
var sel = document.createElement('select');
sel.name = f;
sel.className = inp.className;
sel.required = inp.required;
sPSV(sel, val, "data-preset-value");
rPS(sel, presetOpts, "data-preset-value");
inp.parentNode.replaceChild(sel, inp);
}
}
}
function rTOPO() { // refreshTimerPresetOptions
var presetOpts = '<option value="0">Delete Timer</option>' + sortedPresetOptions;
for (var i=0; i<timerCount; i++) {
var sel = gId("T"+i);
if (!sel) continue;
rPS(sel, presetOpts, "data-preset");
}
}
function rBPO() { // refreshButtonPresetOptions
var presetOpts = '<option value="0">Default Action</option>' + sortedPresetOptions;
var container = gId("macros");
if (!container) return;
var sels = container.querySelectorAll('select[name^="MP"],select[name^="ML"],select[name^="MD"]');
for (var sel of sels) {
rPS(sel, presetOpts, "data-preset");
}
}
function Wd()
{
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, remove disabled, and set value for sunrise/sunset before submission
hour.setAttribute("name", "H"+i);
hour.disabled = false;
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 b = String.fromCharCode((i<10?48:55)+i);
var presetOpts = '<option value="0">Default Action</option>' + sortedPresetOptions;
var buttonBlock = document.createElement('div');
buttonBlock.className = 'bb';
buttonBlock.innerHTML = `
<div class="bh">Button ${i}</div>
<div class="bs">
<div class="ba">
<label>Push/Switch</label>
<select name="MP${b}" class="s" required>${presetOpts}</select>
</div>
<div class="ba">
<label>Short (on→off)</label>
<select name="ML${b}" class="s" required>${presetOpts}</select>
</div>
<div class="ba">
<label>Long (off→on)</label>
<select name="MD${b}" class="s" required>${presetOpts}</select>
</div>
</div>
<hr style="width:100%;margin:8px 0 0 0;">
`;
var buttonSels = buttonBlock.querySelectorAll("select");
var buttonVals = [String(p), String(l), String(d)];
for (var si=0; si<buttonSels.length; si++) {
sPSV(buttonSels[si], buttonVals[si], "data-preset");
}
gId("macros").appendChild(buttonBlock);
}
function getLatLon() {
if (!el) {
window.addEventListener("message", (event) => {
if (event.origin !== "https://locate.wled.me") return;
if (event.data instanceof Object) {
d.Sf.LT.value = event.data.lat;
d.Sf.LN.value = event.data.lon;
updLatLon();
}
}, false);
el = true;
}
window.open("https://locate.wled.me","_blank");
}
function updLatLon(i) {
if (parseFloat(d.Sf.LT.value)<0) { d.Sf.LTR.value = "S"; d.Sf.LT.value = -1*parseFloat(d.Sf.LT.value); } else d.Sf.LTR.value = "N";
if (parseFloat(d.Sf.LN.value)<0) { d.Sf.LNR.value = "W"; d.Sf.LN.value = -1*parseFloat(d.Sf.LN.value); } else d.Sf.LNR.value = "E";
}
</script>
</head>
<body>
<form id="form_s" name="Sf" method="post" onsubmit="Wd()">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/settings/#time-settings')">?</button></div>
<button type="button" onclick="B()">Back</button><button type="submit">Save</button><hr>
</div>
<h2>Time setup</h2>
<div class="sec">
Get time from NTP server: <input type="checkbox" name="NT"><br>
<input type="text" name="NS" maxlength="32"><br>
Use 24h format: <input type="checkbox" name="CF"><br>
Time zone:
<select name="TZ">
<option value="0" selected>GMT(UTC)</option>
<option value="1">GMT/BST</option>
<option value="2">CET/CEST</option>
<option value="3">EET/EEST</option>
<option value="4">US-EST/EDT</option>
<option value="5">US-CST/CDT</option>
<option value="6">US-MST/MDT</option>
<option value="7">US-AZ</option>
<option value="8">US-PST/PDT</option>
<option value="9">CST (AWST, PHST)</option>
<option value="10">JST (KST)</option>
<option value="11">AEST/AEDT</option>
<option value="12">NZST/NZDT</option>
<option value="13">North Korea</option>
<option value="14">IST (India)</option>
<option value="15">CA-Saskatchewan</option>
<option value="16">ACST</option>
<option value="17">ACST/ACDT</option>
<option value="18">HST (Hawaii)</option>
<option value="19">NOVT (Novosibirsk)</option>
<option value="20">AKST/AKDT (Anchorage)</option>
<option value="21">MX-CST</option>
<option value="22">PKT (Pakistan)</option>
<option value="23">BRT (Brasília)</option>
<option value="24">AWST (Perth)</option>
</select><br>
UTC offset: <input name="UO" type="number" min="-65500" max="65500" required> seconds (max. 18 hours)<br>
Current local time is <span class="times">unknown</span>.<br>
Latitude: <select name="LTR"><option value="N">N</option><option value="S">S</option></select><input name="LT" type="number" class="xl" min="0" max="66.6" step="0.01"><br>
Longitude: <select name="LNR"><option value="E">E</option><option value="W">W</option></select><input name="LN" type="number" class="xl" min="0" max="180" step="0.01"><br>
<button type="button" id="locbtn" onclick="getLatLon()">Get location</button>
<div><i>(opens new tab, only works in browser)</i></div>
<div id="sun" class="times"></div>
</div>
<div class="sec">
<h3>Clock</h3>
Analog Clock overlay: <input type="checkbox" name="OL" onchange="Cs()"><br>
<div id="cac">
First LED: <input name="O1" type="number" min="0" max="1024" required> Last LED: <input name="O2" type="number" min="0" max="1024" required><br>
12h LED: <input name="OM" type="number" min="0" max="1024" required><br>
Show 5min marks: <input type="checkbox" name="O5"><br>
Seconds (as trail): <input type="checkbox" name="OS"><br>
Show clock overlay only if all LEDs are solid black: <input type="checkbox" name="OB"><br>
</div>
Countdown Mode: <input type="checkbox" name="CE"><br>
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>
</div>
<hr>
<h2>Macro Presets</h2>
<i>Presets can be used as macros for both JSON and HTTP API commands.<br>
Enter the preset ID below.</i>
<i>Use 0 for the default action instead of a preset</i><br>
<a href="https://kno.wled.ge/interfaces/json-api/" target="_blank">JSON API</a><br>
<a href="https://kno.wled.ge/interfaces/http-api/" target="_blank">HTTP API</a><br>
<div class="sec">
<h3>Timer & Alexa Presets</h3>
Countdown-Over Preset: <input name="MC" class="m" type="number" min="0" max="250" required><br>
Timed-Light-Over Presets: <input name="MN" class="m" type="number" min="0" max="250" required><br>
Alexa On/Off Preset: <input name="A0" class="m" type="number" min="0" max="250" required> <input name="A1" class="m" type="number" min="0" max="250" required><br>
</div>
<div class="sec">
<h3>Button Action Presets</h3>
<div id="macros"></div>
<a href="https://kno.wled.ge/features/macros/#analog-button" target="_blank">Analog Button setup</a>
</div>
<div class="sec">
<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>
</div>
<hr>
<button type="button" onclick="B()">Back</button><button type="submit">Save</button>
</form>
</body>
</html>