Added automatic dimming based on sunrise/sunset times from the weather API

This commit is contained in:
M-Factory
2025-11-11 23:16:17 +09:00
parent 8bbb89ba6d
commit 03a5432cbe
4 changed files with 818 additions and 286 deletions

View File

@@ -71,7 +71,12 @@ int dimStartHour = 18; // 6pm default
int dimStartMinute = 0;
int dimEndHour = 8; // 8am default
int dimEndMinute = 0;
int dimBrightness = 2; // Dimming level (0-15)
int dimBrightness = 2; // Dimming level (0-15)
bool autoDimmingEnabled = false; // true if using sunrise/sunset
int sunriseHour = 6;
int sunriseMinute = 0;
int sunsetHour = 18;
int sunsetMinute = 0;
//Countdown Globals - NEW
bool countdownEnabled = false;
@@ -226,6 +231,13 @@ void loadConfig() {
doc[F("dimBrightness")] = dimBrightness;
doc[F("showWeatherDescription")] = showWeatherDescription;
// --- Automatic dimming defaults ---
doc[F("autoDimmingEnabled")] = autoDimmingEnabled;
doc[F("sunriseHour")] = sunriseHour;
doc[F("sunriseMinute")] = sunriseMinute;
doc[F("sunsetHour")] = sunsetHour;
doc[F("sunsetMinute")] = sunsetMinute;
// Add countdown defaults when creating a new config.json
JsonObject countdownObj = doc.createNestedObject("countdown");
countdownObj["enabled"] = false;
@@ -287,6 +299,14 @@ void loadConfig() {
colonBlinkEnabled = doc.containsKey("colonBlinkEnabled") ? doc["colonBlinkEnabled"].as<bool>() : true;
showWeatherDescription = doc["showWeatherDescription"] | false;
// --- Dimming settings ---
if (doc["dimmingEnabled"].is<bool>()) {
dimmingEnabled = doc["dimmingEnabled"].as<bool>();
} else {
String de = doc["dimmingEnabled"].as<String>();
dimmingEnabled = (de == "true" || de == "1" || de == "on");
}
String de = doc["dimmingEnabled"].as<String>();
dimmingEnabled = (de == "true" || de == "on" || de == "1");
@@ -296,6 +316,32 @@ void loadConfig() {
dimEndMinute = doc["dimEndMinute"] | 0;
dimBrightness = doc["dimBrightness"] | 0;
// safely handle both numeric or string "Off" for dimBrightness
if (doc["dimBrightness"].is<int>()) {
dimBrightness = doc["dimBrightness"].as<int>();
} else {
String val = doc["dimBrightness"].as<String>();
if (val.equalsIgnoreCase("off")) dimBrightness = -1;
else dimBrightness = val.toInt();
}
// --- Automatic dimming ---
if (doc.containsKey("autoDimmingEnabled")) {
if (doc["autoDimmingEnabled"].is<bool>()) {
autoDimmingEnabled = doc["autoDimmingEnabled"].as<bool>();
} else {
String val = doc["autoDimmingEnabled"].as<String>();
autoDimmingEnabled = (val == "true" || val == "1" || val == "on");
}
} else {
autoDimmingEnabled = false; // default if key missing
}
sunriseHour = doc["sunriseHour"] | 6;
sunriseMinute = doc["sunriseMinute"] | 0;
sunsetHour = doc["sunsetHour"] | 18;
sunsetMinute = doc["sunsetMinute"] | 0;
strlcpy(ntpServer1, doc["ntpServer1"] | "pool.ntp.org", sizeof(ntpServer1));
strlcpy(ntpServer2, doc["ntpServer2"] | "time.nist.gov", sizeof(ntpServer2));
@@ -577,18 +623,45 @@ void printConfigToSerial() {
Serial.println(ntpServer1);
Serial.print(F("NTP Server 2: "));
Serial.println(ntpServer2);
Serial.print(F("Dimming Enabled: "));
Serial.println(dimmingEnabled);
Serial.print(F("Dimming Start Hour: "));
Serial.println(dimStartHour);
Serial.print(F("Dimming Start Minute: "));
Serial.println(dimStartMinute);
Serial.print(F("Dimming End Hour: "));
Serial.println(dimEndHour);
Serial.print(F("Dimming End Minute: "));
Serial.println(dimEndMinute);
Serial.print(F("Dimming Brightness: "));
Serial.println(dimBrightness);
// ---------------------------------------------------------------------------
// DIMMING SECTION
// ---------------------------------------------------------------------------
Serial.print(F("Automatic Dimming: "));
Serial.println(autoDimmingEnabled ? "Enabled" : "Disabled");
Serial.print(F("Custom Dimming: "));
Serial.println(dimmingEnabled ? "Enabled" : "Disabled");
if (autoDimmingEnabled) {
// --- Automatic (Sunrise/Sunset) dimming mode ---
if ((sunriseHour == 6 && sunriseMinute == 0) && (sunsetHour == 18 && sunsetMinute == 0)) {
Serial.println(F("Automatic Dimming Schedule: Sunrise/Sunset Data not available yet (waiting for weather update)"));
} else {
Serial.printf("Automatic Dimming Schedule: Sunrise: %02d:%02d → Sunset: %02d:%02d\n",
sunriseHour, sunriseMinute, sunsetHour, sunsetMinute);
time_t now_time = time(nullptr);
struct tm localTime;
localtime_r(&now_time, &localTime);
int curTotal = localTime.tm_hour * 60 + localTime.tm_min;
int startTotal = sunsetHour * 60 + sunsetMinute;
int endTotal = sunriseHour * 60 + sunriseMinute;
bool autoActive = (startTotal < endTotal)
? (curTotal >= startTotal && curTotal < endTotal)
: (curTotal >= startTotal || curTotal < endTotal);
Serial.printf("Current Auto-Dimming Status: %s\n", autoActive ? "ACTIVE" : "Inactive");
Serial.printf("Dimming Brightness (night): %d\n", dimBrightness);
}
} else {
// --- Manual (Custom Schedule) dimming mode ---
Serial.printf("Custom Dimming Schedule: %02d:%02d → %02d:%02d\n",
dimStartHour, dimStartMinute, dimEndHour, dimEndMinute);
Serial.printf("Dimming Brightness: %d\n", dimBrightness);
}
Serial.print(F("Countdown Enabled: "));
Serial.println(countdownEnabled ? "Yes" : "No");
Serial.print(F("Countdown Target Timestamp: "));
@@ -599,12 +672,14 @@ void printConfigToSerial() {
Serial.println(isDramaticCountdown ? "Yes" : "No");
Serial.print(F("Custom Message: "));
Serial.println(customMessage);
Serial.print(F("Total Runtime: "));
if (totalUptimeSeconds > 0) {
Serial.println(formatUptime(totalUptimeSeconds));
} else {
Serial.println(F("No runtime recorded yet."));
}
Serial.println(F("========================================"));
Serial.println();
}
@@ -687,8 +762,10 @@ void setupWebServer() {
else if (n == "dimStartMinute") doc[n] = v.toInt();
else if (n == "dimEndHour") doc[n] = v.toInt();
else if (n == "dimEndMinute") doc[n] = v.toInt();
else if (n == "dimBrightness") doc[n] = v.toInt();
else if (n == "showWeatherDescription") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "dimBrightness") {
if (v == "Off" || v == "off") doc[n] = -1;
else doc[n] = v.toInt();
} else if (n == "showWeatherDescription") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "dimmingEnabled") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "weatherUnits") doc[n] = v;
@@ -1711,10 +1788,93 @@ void fetchWeather() {
weatherDescription = normalizeWeatherDescription(detailedDesc);
Serial.printf("[WEATHER] Description used: %s\n", weatherDescription.c_str());
// -----------------------------------------
// Sunrise/Sunset for Auto Dimming (local time)
// -----------------------------------------
if (doc.containsKey(F("sys"))) {
JsonObject sys = doc[F("sys")];
if (sys.containsKey(F("sunrise")) && sys.containsKey(F("sunset"))) {
// OWM gives UTC timestamps
time_t sunriseUtc = sys[F("sunrise")].as<time_t>();
time_t sunsetUtc = sys[F("sunset")].as<time_t>();
// Get local timezone offset (in seconds)
long tzOffset = 0;
struct tm local_tm;
time_t now = time(nullptr);
if (localtime_r(&now, &local_tm)) {
tzOffset = mktime(&local_tm) - now;
}
// Convert UTC → local
time_t sunriseLocal = sunriseUtc + tzOffset;
time_t sunsetLocal = sunsetUtc + tzOffset;
// Break into hour/minute
struct tm tmSunrise, tmSunset;
localtime_r(&sunriseLocal, &tmSunrise);
localtime_r(&sunsetLocal, &tmSunset);
sunriseHour = tmSunrise.tm_hour;
sunriseMinute = tmSunrise.tm_min;
sunsetHour = tmSunset.tm_hour;
sunsetMinute = tmSunset.tm_min;
Serial.printf("[WEATHER] Adjusted Sunrise/Sunset (local): %02d:%02d | %02d:%02d\n",
sunriseHour, sunriseMinute, sunsetHour, sunsetMinute);
} else {
Serial.println(F("[WEATHER] Sunrise/Sunset not found in JSON."));
}
} else {
Serial.println(F("[WEATHER] 'sys' object not found in JSON payload."));
}
weatherFetched = true;
// -----------------------------------------
// Save updated sunrise/sunset to config.json
// -----------------------------------------
if (autoDimmingEnabled && sunriseHour >= 0 && sunsetHour >= 0) {
File configFile = LittleFS.open("/config.json", "r");
DynamicJsonDocument doc(1024);
if (configFile) {
DeserializationError error = deserializeJson(doc, configFile);
configFile.close();
if (!error) {
// Check if ANY value has changed
bool valuesChanged = (doc["sunriseHour"] != sunriseHour || doc["sunriseMinute"] != sunriseMinute || doc["sunsetHour"] != sunsetHour || doc["sunsetMinute"] != sunsetMinute);
if (valuesChanged) { // Only write if a change occurred
doc["sunriseHour"] = sunriseHour;
doc["sunriseMinute"] = sunriseMinute;
doc["sunsetHour"] = sunsetHour;
doc["sunsetMinute"] = sunsetMinute;
File f = LittleFS.open("/config.json", "w");
if (f) {
serializeJsonPretty(doc, f);
f.close();
Serial.println(F("[WEATHER] SAVED NEW sunrise/sunset to config.json (Values changed)"));
} else {
Serial.println(F("[WEATHER] Failed to write updated sunrise/sunset to config.json"));
}
} else {
Serial.println(F("[WEATHER] Sunrise/Sunset unchanged, skipping config save."));
}
// --- END MODIFIED COMPARISON LOGIC ---
} else {
Serial.println(F("[WEATHER] JSON parse error when saving updated sunrise/sunset"));
}
}
}
} else {
Serial.printf("[WEATHER] HTTP GET failed, error code: %d, reason: %s\n", httpCode, http.errorToString(httpCode).c_str());
Serial.printf("[WEATHER] HTTP GET failed, error code: %d, reason: %s\n",
httpCode, http.errorToString(httpCode).c_str());
weatherAvailable = false;
weatherFetched = false;
}
@@ -2081,65 +2241,69 @@ void loop() {
}
// Dimming
// -----------------------------
// Dimming (auto + manual)
// -----------------------------
time_t now_time = time(nullptr);
struct tm timeinfo;
localtime_r(&now_time, &timeinfo);
int curHour = timeinfo.tm_hour;
int curMinute = timeinfo.tm_min;
int curTotal = curHour * 60 + curMinute;
int startTotal = dimStartHour * 60 + dimStartMinute;
int endTotal = dimEndHour * 60 + dimEndMinute;
bool isDimmingActive = false;
if (dimmingEnabled) {
// Determine if dimming is active (overnight-aware)
// -----------------------------
// Determine dimming start/end
// -----------------------------
int startTotal, endTotal;
bool dimActive = false;
if (autoDimmingEnabled) {
startTotal = sunsetHour * 60 + sunsetMinute;
endTotal = sunriseHour * 60 + sunriseMinute;
} else if (dimmingEnabled) {
startTotal = dimStartHour * 60 + dimStartMinute;
endTotal = dimEndHour * 60 + dimEndMinute;
} else {
startTotal = endTotal = -1; // not used
}
// -----------------------------
// Check if dimming should be active
// -----------------------------
if (autoDimmingEnabled || dimmingEnabled) {
if (startTotal < endTotal) {
isDimmingActive = (curTotal >= startTotal && curTotal < endTotal);
dimActive = (curTotal >= startTotal && curTotal < endTotal);
} else {
isDimmingActive = (curTotal >= startTotal || curTotal < endTotal);
dimActive = (curTotal >= startTotal || curTotal < endTotal); // overnight
}
}
int targetBrightness = isDimmingActive ? dimBrightness : brightness;
// -----------------------------
// Apply brightness / display on-off
// -----------------------------
int targetBrightness;
if (dimActive) targetBrightness = dimBrightness;
else targetBrightness = brightness;
if (targetBrightness == -1) {
if (!displayOff) {
Serial.println(F("[DISPLAY] Turning display OFF (dimming -1)"));
P.displayShutdown(true);
P.displayClear();
displayOff = true;
displayOffByDimming = true;
displayOffByBrightness = false;
}
} else {
if (displayOff && displayOffByDimming) {
Serial.println(F("[DISPLAY] Waking display (dimming end)"));
P.displayShutdown(false);
displayOff = false;
displayOffByDimming = false;
}
P.setIntensity(targetBrightness);
if (targetBrightness == -1) {
if (!displayOff) {
Serial.println(F("[DISPLAY] Turning display OFF (dimming -1)"));
P.displayShutdown(true);
P.displayClear();
displayOff = true;
displayOffByDimming = dimActive;
displayOffByBrightness = !dimActive;
}
} else {
// Dimming disabled: just obey brightness slider
if (brightness == -1) {
if (!displayOff) {
Serial.println(F("[DISPLAY] Turning display OFF (brightness -1)"));
P.displayShutdown(true);
P.displayClear();
displayOff = true;
displayOffByBrightness = true;
displayOffByDimming = false;
}
} else {
if (displayOff && displayOffByBrightness) {
Serial.println(F("[DISPLAY] Waking display (brightness changed)"));
P.displayShutdown(false);
displayOff = false;
displayOffByBrightness = false;
}
P.setIntensity(brightness);
if (displayOff && ((dimActive && displayOffByDimming) || (!dimActive && displayOffByBrightness))) {
Serial.println(F("[DISPLAY] Waking display (dimming end)"));
P.displayShutdown(false);
displayOff = false;
displayOffByDimming = false;
displayOffByBrightness = false;
}
P.setIntensity(targetBrightness);
}

View File

@@ -586,29 +586,47 @@ textarea::placeholder {
oninput="brightnessValue.textContent = (this.value == -1 ? 'Off' : this.value); setBrightnessLive(this.value);">
<br><br><br>
<label style="display: flex; align-items: center; justify-content: space-between;">
<span style="margin-right: 0.5em;">Enable Dimming:</span>
<span class="toggle-switch">
<input type="checkbox" id="dimmingEnabled" name="dimmingEnabled">
<span class="toggle-slider"></span>
</span>
</label>
<div class="form-row two-col">
<div>
<label for="dimStartTime">Dimming Start Time:</label>
<input type="time" id="dimStartTime" value="18:00">
</div>
<label style="display: flex; align-items: center; justify-content: space-between; margin-top: 1rem;">
<span style="margin-right: 0.5em;">Automatic Dimming:</span>
<span class="toggle-switch">
<input type="checkbox" id="autoDimmingEnabled" name="autoDimmingEnabled">
<span class="toggle-slider"></span>
</span>
</label>
<div id="autoDimmingNote" class="small">
Requires a valid OpenWeather API key.
</div>
<div>
<label for="dimEndTime">Dimming End Time:</label>
<input type="time" id="dimEndTime" value="08:00">
</div>
</div>
<label style="margin-top: 1.75rem;" for="dimBrightness">Dimming Brightness: <span id="dimmingBrightnessValue">2</span></label>
<input style="width: 100%;" type="range" min="-1" max="15" name="dimming_brightness" id="dimBrightness" value="2"
oninput="dimmingBrightnessValue.textContent = (this.value == -1 ? 'Off' : this.value);">
<label style="display: flex; align-items: center; justify-content: space-between; margin-top: 0.75rem;">
<span style="margin-right: 0.5em;">Custom Dimming:</span>
<span class="toggle-switch">
<input type="checkbox" id="dimmingEnabled" name="dimmingEnabled">
<span class="toggle-slider"></span>
</span>
</label>
<div class="form-row two-col">
<div>
<label for="dimStartTime">Start Time:</label>
<input type="time" id="dimStartTime" value="18:00">
</div>
<div>
<label for="dimEndTime">End Time:</label>
<input type="time" id="dimEndTime" value="08:00">
</div>
</div>
<label style="margin-top: 1.75rem;" for="dimBrightness">
Dimming Brightness: <span id="dimmingBrightnessValue">2</span>
</label>
<input style="width: 100%;" type="range" min="-1" max="15"
name="dimming_brightness" id="dimBrightness" value="2"
oninput="dimmingBrightnessValue.textContent = (this.value == -1 ? 'Off' : this.value);">
<br><br><br>
<div class="form-group">
<label style="display: flex; align-items: center; justify-content: space-between;">
@@ -630,11 +648,11 @@ textarea::placeholder {
</div>
<div class="form-row two-col">
<div class="form-group">
<label for="countdownDate">Countdown Date:</label>
<label for="countdownDate">Date:</label>
<input type="date" id="countdownDate" name="countdownDate" class="form-control">
</div>
<div class="form-group">
<label for="countdownTime">Countdown Time:</label>
<label for="countdownTime">Time:</label>
<input type="time" id="countdownTime" name="countdownTime" class="form-control">
</div>
</div>
@@ -735,11 +753,12 @@ window.onload = function () {
const apiInput = document.getElementById('openWeatherApiKey');
if (data.openWeatherApiKey && data.openWeatherApiKey.trim() !== '') {
apiInput.value = MASK;
hasSavedKey = true; // mark it as having a saved key
hasSavedKey = true;
} else {
apiInput.value = '';
hasSavedKey = false;
}
document.getElementById('openWeatherCity').value = data.openWeatherCity || '';
document.getElementById('openWeatherCountry').value = data.openWeatherCountry || '';
document.getElementById('weatherUnits').checked = (data.weatherUnits === "imperial");
@@ -760,86 +779,95 @@ window.onload = function () {
document.getElementById('colonBlinkEnabled').checked = !!data.colonBlinkEnabled;
document.getElementById('showWeatherDescription').checked = !!data.showWeatherDescription;
// Dimming controls
// --- Dimming Controls ---
const autoDimmingEl = document.getElementById('autoDimmingEnabled');
const dimmingEnabledEl = document.getElementById('dimmingEnabled');
const isDimming = (data.dimmingEnabled === true || data.dimmingEnabled === "true" || data.dimmingEnabled === 1);
dimmingEnabledEl.checked = isDimming;
// Defer field enabling until checkbox state is rendered
setTimeout(() => {
setDimmingFieldsEnabled(dimmingEnabledEl.checked);
}, 0);
dimmingEnabledEl.addEventListener('change', function () {
setDimmingFieldsEnabled(this.checked);
const apiInputEl = document.getElementById('openWeatherApiKey');
// Evaluate flags from config.json
const isAutoDimming = (data.autoDimmingEnabled === true || data.autoDimmingEnabled === "true" || data.autoDimmingEnabled === 1);
const isCustomDimming = (data.dimmingEnabled === true || data.dimmingEnabled === "true" || data.dimmingEnabled === 1);
// Set toggle states
autoDimmingEl.checked = isAutoDimming;
dimmingEnabledEl.checked = isCustomDimming;
// Apply initial dimming state
setDimmingFieldsEnabled();
// Attach listeners (mutually exclusive + API dependency)
if (apiInputEl) apiInputEl.addEventListener('input', setDimmingFieldsEnabled);
autoDimmingEl.addEventListener('change', () => {
if (autoDimmingEl.checked) dimmingEnabledEl.checked = false;
setDimmingFieldsEnabled();
});
dimmingEnabledEl.addEventListener('change', () => {
if (dimmingEnabledEl.checked) autoDimmingEl.checked = false;
setDimmingFieldsEnabled();
});
// Set field values from config
document.getElementById('dimStartTime').value =
(data.dimStartHour !== undefined ? String(data.dimStartHour).padStart(2, '0') : "18") + ":" +
(data.dimStartMinute !== undefined ? String(data.dimStartMinute).padStart(2, '0') : "00");
(data.dimStartHour !== undefined ? String(data.dimStartHour).padStart(2, '0') : "18") + ":" +
(data.dimStartMinute !== undefined ? String(data.dimStartMinute).padStart(2, '0') : "00");
document.getElementById('dimEndTime').value =
(data.dimEndHour !== undefined ? String(data.dimEndHour).padStart(2, '0') : "08") + ":" +
(data.dimEndMinute !== undefined ? String(data.dimEndMinute).padStart(2, '0') : "00");
(data.dimEndHour !== undefined ? String(data.dimEndHour).padStart(2, '0') : "08") + ":" +
(data.dimEndMinute !== undefined ? String(data.dimEndMinute).padStart(2, '0') : "00");
document.getElementById('dimBrightness').value = (data.dimBrightness !== undefined ? data.dimBrightness : 2);
// Then update the span's text content with that value
document.getElementById('dimmingBrightnessValue').textContent = (document.getElementById('dimBrightness').value == -1 ? 'Off' : document.getElementById('dimBrightness').value);
setDimmingFieldsEnabled(!!data.dimmingEnabled);
document.getElementById('dimmingBrightnessValue').textContent =
(document.getElementById('dimBrightness').value == -1 ? 'Off' : document.getElementById('dimBrightness').value);
// --- Populate Countdown Fields ---
document.getElementById('isDramaticCountdown').checked = !!(data.countdown && data.countdown.isDramaticCountdown);
const countdownEnabledEl = document.getElementById('countdownEnabled'); // Get reference
countdownEnabledEl.checked = !!(data.countdown && data.countdown.enabled);
if (data.countdown && data.countdown.targetTimestamp) {
// Convert Unix timestamp (seconds) to milliseconds for JavaScript Date object
const targetDate = new Date(data.countdown.targetTimestamp * 1000);
const year = targetDate.getFullYear();
// Month is 0-indexed in JS, so add 1
const month = (targetDate.getMonth() + 1).toString().padStart(2, '0');
const day = targetDate.getDate().toString().padStart(2, '0');
const hours = targetDate.getHours().toString().padStart(2, '0');
const minutes = targetDate.getMinutes().toString().padStart(2, '0');
document.getElementById('countdownDate').value = `${year}-${month}-${day}`;
document.getElementById('countdownTime').value = `${hours}:${minutes}`;
const targetDate = new Date(data.countdown.targetTimestamp * 1000);
const year = targetDate.getFullYear();
const month = (targetDate.getMonth() + 1).toString().padStart(2, '0');
const day = targetDate.getDate().toString().padStart(2, '0');
const hours = targetDate.getHours().toString().padStart(2, '0');
const minutes = targetDate.getMinutes().toString().padStart(2, '0');
document.getElementById('countdownDate').value = `${year}-${month}-${day}`;
document.getElementById('countdownTime').value = `${hours}:${minutes}`;
} else {
// Clear fields if no target timestamp is set
document.getElementById('countdownDate').value = '';
document.getElementById('countdownTime').value = '';
document.getElementById('countdownDate').value = '';
document.getElementById('countdownTime').value = '';
}
// Countdown Label Input Validation
const countdownLabelInput = document.getElementById('countdownLabel');
countdownLabelInput.addEventListener('input', function() {
// Convert to uppercase and remove any characters that are not A-Z or space
// Note: The `maxlength` attribute in HTML handles the length limit.
this.value = this.value.toUpperCase().replace(/[^A-Z0-9 :!'\-.,_\+%\/?]/g, '');
});
// Set initial value for countdownLabel from config.json (apply validation on load too)
if (data.countdown && data.countdown.label) {
countdownLabelInput.value = data.countdown.label.toUpperCase().replace(/[^A-Z0-9 :!'\-.,_\+%\/?]/g, '');
countdownLabelInput.value = data.countdown.label.toUpperCase().replace(/[^A-Z0-9 :!'\-.,_\+%\/?]/g, '');
} else {
countdownLabelInput.value = '';
countdownLabelInput.value = '';
}
// Countdown Toggle Event Listener & Field Enabling
// If you're using onchange="setCountdownEnabled(this.checked)" directly in HTML,
// you would add setCountdownFieldsEnabled(this.checked) there as well.
// If you are using addEventListener, keep this:
// Countdown Toggle Event Listener & Field Enabling
countdownEnabledEl.addEventListener('change', function() {
setCountdownEnabled(this.checked); // Sends command to ESP
setCountdownFieldsEnabled(this.checked); // Enables/disables local fields
setCountdownEnabled(this.checked);
setCountdownFieldsEnabled(this.checked);
});
const dramaticCountdownEl = document.getElementById('isDramaticCountdown');
dramaticCountdownEl.addEventListener('change', function () {
setIsDramaticCountdown(this.checked);
setIsDramaticCountdown(this.checked);
});
// Set initial state of fields when page loads
setCountdownFieldsEnabled(countdownEnabledEl.checked);
if (data.customMessage !== undefined) {
document.getElementById('customMessage').value = data.customMessage;
}
if (data.customMessage !== undefined) {
document.getElementById('customMessage').value = data.customMessage;
}
// Auto-detect browser's timezone if not set in config
if (!data.timeZone) {
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (
tz &&
document.getElementById('timeZone').querySelector(`[value="${tz}"]`)
) {
if (tz && document.getElementById('timeZone').querySelector(`[value="${tz}"]`)) {
document.getElementById('timeZone').value = tz;
} else {
document.getElementById('timeZone').value = '';
@@ -903,10 +931,21 @@ async function submitConfig(event) {
formData.set('showHumidity', document.getElementById('showHumidity').checked ? 'on' : '');
formData.set('colonBlinkEnabled', document.getElementById('colonBlinkEnabled').checked ? 'on' : '');
//dimming
formData.set('dimmingEnabled', document.getElementById('dimmingEnabled').checked ? 'true' : 'false');
// --- Dimming ---
const autoDimmingChecked = document.getElementById('autoDimmingEnabled').checked;
const customDimmingChecked = document.getElementById('dimmingEnabled').checked;
// Mutual exclusivity (if both checked somehow, keep auto as priority)
if (autoDimmingChecked && customDimmingChecked) {
formData.set('autoDimmingEnabled', 'true');
formData.set('dimmingEnabled', 'false');
} else {
formData.set('autoDimmingEnabled', autoDimmingChecked ? 'true' : 'false');
formData.set('dimmingEnabled', customDimmingChecked ? 'true' : 'false');
}
const dimStart = document.getElementById('dimStartTime').value; // "18:45"
const dimEnd = document.getElementById('dimEndTime').value; // "08:30"
const dimEnd = document.getElementById('dimEndTime').value; // "08:30"
// Parse hour and minute
if (dimStart) {
@@ -1367,8 +1406,6 @@ async function getLocation() {
}
}
// --- OpenWeather API Key field UX ---
const MASK_LENGTH = 32;
const MASK = '*'.repeat(MASK_LENGTH);
@@ -1410,6 +1447,7 @@ apiInput.addEventListener('blur', () => {
hasSavedKey = false; // user cleared the key
apiInput.dataset.clearing = 'false';
apiInput.value = ''; // leave blank
setDimmingFieldsEnabled();
}
}
});
@@ -1527,6 +1565,71 @@ function clearCustomMessage() {
});
}
// --- Dimming Controls Logic (The correct version) ---
function setDimmingFieldsEnabled() {
const apiKeyField = document.getElementById('openWeatherApiKey');
const autoDimming = document.getElementById('autoDimmingEnabled');
const dimmingEnabled = document.getElementById('dimmingEnabled');
const dimStart = document.getElementById('dimStartTime');
const dimEnd = document.getElementById('dimEndTime');
const dimBrightness = document.getElementById('dimBrightness');
const noteEl = document.getElementById('autoDimmingNote');
if (!apiKeyField || !autoDimming || !dimmingEnabled) return;
const currentApiKeyInput = apiKeyField.value.trim();
// Checks if a key is saved (hasSavedKey) OR if the user is currently typing a new one.
const isKeyPresent = hasSavedKey || (currentApiKeyInput !== '' && currentApiKeyInput !== MASK);
// --- 1. Control Auto Dimming based on Key Presence ---
// Meets requirement: "when page load after autodim has been saved to json,
// if user removes the api key (masked) the toggle auto dim toggle should get disabled"
if (!isKeyPresent) {
autoDimming.checked = false;
autoDimming.disabled = true;
if (noteEl) noteEl.style.display = 'block';
} else {
autoDimming.disabled = false;
if (noteEl) noteEl.style.display = 'none';
}
// Custom Dimming toggle is always enabled (since it's not key-dependent)
dimmingEnabled.disabled = false;
// --- 2. Control Dependent Fields based on Active Mode ---
const isAutoDimmingActive = autoDimming.checked && isKeyPresent; // Auto is only active if checked AND key is present
const isCustomDimmingActive = dimmingEnabled.checked;
const isDimmingActive = isAutoDimmingActive || isCustomDimmingActive; // Brightness slider logic
// BRIGHTNESS SLIDER: Enabled if EITHER mode is active.
if (dimBrightness) {
dimBrightness.disabled = !isDimmingActive;
}
// START/END TIME FIELDS: Enabled ONLY if Custom Dimming is checked (key not needed).
const isCustomTimeEnabled = dimmingEnabled.checked;
if (dimStart) {
dimStart.disabled = !isCustomTimeEnabled;
}
if (dimEnd) {
dimEnd.disabled = !isCustomTimeEnabled;
}
}
window.addEventListener('DOMContentLoaded', () => {
const apiKeyEl = document.getElementById('openWeatherApiKey');
const autoEl = document.getElementById('autoDimmingEnabled');
const dimEl = document.getElementById('dimmingEnabled');
if (apiKeyEl) {
apiKeyEl.addEventListener('input', setDimmingFieldsEnabled);
apiKeyEl.addEventListener('change', setDimmingFieldsEnabled);
}
if (autoEl) autoEl.addEventListener('change', setDimmingFieldsEnabled);
if (dimEl) dimEl.addEventListener('change', setDimmingFieldsEnabled);
});
</script>
</body>

View File

@@ -28,8 +28,8 @@ MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);
AsyncWebServer server(80);
// --- Global Scroll Speed Settings ---
const int GENERAL_SCROLL_SPEED = 85; // Default: Adjust this for Weather Description and Countdown Label (e.g., 50 for faster, 200 for slower)
const int IP_SCROLL_SPEED = 115; // Default: Adjust this for the IP Address display (slower for readability)
const int GENERAL_SCROLL_SPEED = 85; // Default: Adjust this for Weather Description and Countdown Label (e.g., 50 for faster, 200 for slower)
const int IP_SCROLL_SPEED = 115; // Default: Adjust this for the IP Address display (slower for readability)
int messageScrollSpeed = 85; // default fallback
// --- Nightscout setting ---
@@ -61,7 +61,7 @@ bool colonBlinkEnabled = true;
char ntpServer1[64] = "pool.ntp.org";
char ntpServer2[256] = "time.nist.gov";
char customMessage[121] = "";
char lastPersistentMessage[128] = "";
char lastPersistentMessage[128] = "";
// Dimming
bool dimmingEnabled = false;
@@ -71,7 +71,13 @@ int dimStartHour = 18; // 6pm default
int dimStartMinute = 0;
int dimEndHour = 8; // 8am default
int dimEndMinute = 0;
int dimBrightness = 2; // Dimming level (0-15)
int dimBrightness = 2; // Dimming level (0-15)
bool autoDimmingEnabled = false; // true if using sunrise/sunset
int sunriseHour = 6;
int sunriseMinute = 0;
int sunsetHour = 18;
int sunsetMinute = 0;
//Countdown Globals - NEW
bool countdownEnabled = false;
@@ -225,6 +231,13 @@ void loadConfig() {
doc[F("dimBrightness")] = dimBrightness;
doc[F("showWeatherDescription")] = showWeatherDescription;
// --- Automatic dimming defaults ---
doc[F("autoDimmingEnabled")] = autoDimmingEnabled;
doc[F("sunriseHour")] = sunriseHour;
doc[F("sunriseMinute")] = sunriseMinute;
doc[F("sunsetHour")] = sunsetHour;
doc[F("sunsetMinute")] = sunsetMinute;
// Add countdown defaults when creating a new config.json
JsonObject countdownObj = doc.createNestedObject("countdown");
countdownObj["enabled"] = false;
@@ -286,14 +299,44 @@ void loadConfig() {
colonBlinkEnabled = doc.containsKey("colonBlinkEnabled") ? doc["colonBlinkEnabled"].as<bool>() : true;
showWeatherDescription = doc["showWeatherDescription"] | false;
String de = doc["dimmingEnabled"].as<String>();
dimmingEnabled = (de == "true" || de == "on" || de == "1");
// --- Dimming settings ---
if (doc["dimmingEnabled"].is<bool>()) {
dimmingEnabled = doc["dimmingEnabled"].as<bool>();
} else {
String de = doc["dimmingEnabled"].as<String>();
dimmingEnabled = (de == "true" || de == "1" || de == "on");
}
dimStartHour = doc["dimStartHour"] | 18;
dimStartMinute = doc["dimStartMinute"] | 0;
dimEndHour = doc["dimEndHour"] | 8;
dimEndMinute = doc["dimEndMinute"] | 0;
dimBrightness = doc["dimBrightness"] | 0;
// safely handle both numeric or string "Off" for dimBrightness
if (doc["dimBrightness"].is<int>()) {
dimBrightness = doc["dimBrightness"].as<int>();
} else {
String val = doc["dimBrightness"].as<String>();
if (val.equalsIgnoreCase("off")) dimBrightness = -1;
else dimBrightness = val.toInt();
}
// --- Automatic dimming ---
if (doc.containsKey("autoDimmingEnabled")) {
if (doc["autoDimmingEnabled"].is<bool>()) {
autoDimmingEnabled = doc["autoDimmingEnabled"].as<bool>();
} else {
String val = doc["autoDimmingEnabled"].as<String>();
autoDimmingEnabled = (val == "true" || val == "1" || val == "on");
}
} else {
autoDimmingEnabled = false; // default if key missing
}
sunriseHour = doc["sunriseHour"] | 6;
sunriseMinute = doc["sunriseMinute"] | 0;
sunsetHour = doc["sunsetHour"] | 18;
sunsetMinute = doc["sunsetMinute"] | 0;
strlcpy(ntpServer1, doc["ntpServer1"] | "pool.ntp.org", sizeof(ntpServer1));
strlcpy(ntpServer2, doc["ntpServer2"] | "time.nist.gov", sizeof(ntpServer2));
@@ -577,18 +620,45 @@ void printConfigToSerial() {
Serial.println(ntpServer1);
Serial.print(F("NTP Server 2: "));
Serial.println(ntpServer2);
Serial.print(F("Dimming Enabled: "));
Serial.println(dimmingEnabled);
Serial.print(F("Dimming Start Hour: "));
Serial.println(dimStartHour);
Serial.print(F("Dimming Start Minute: "));
Serial.println(dimStartMinute);
Serial.print(F("Dimming End Hour: "));
Serial.println(dimEndHour);
Serial.print(F("Dimming End Minute: "));
Serial.println(dimEndMinute);
Serial.print(F("Dimming Brightness: "));
Serial.println(dimBrightness);
// ---------------------------------------------------------------------------
// DIMMING SECTION
// ---------------------------------------------------------------------------
Serial.print(F("Automatic Dimming: "));
Serial.println(autoDimmingEnabled ? "Enabled" : "Disabled");
Serial.print(F("Custom Dimming: "));
Serial.println(dimmingEnabled ? "Enabled" : "Disabled");
if (autoDimmingEnabled) {
// --- Automatic (Sunrise/Sunset) dimming mode ---
if ((sunriseHour == 6 && sunriseMinute == 0) && (sunsetHour == 18 && sunsetMinute == 0)) {
Serial.println(F("Automatic Dimming Schedule: Sunrise/Sunset Data not available yet (waiting for weather update)"));
} else {
Serial.printf("Automatic Dimming Schedule: Sunrise: %02d:%02d → Sunset: %02d:%02d\n",
sunriseHour, sunriseMinute, sunsetHour, sunsetMinute);
time_t now_time = time(nullptr);
struct tm localTime;
localtime_r(&now_time, &localTime);
int curTotal = localTime.tm_hour * 60 + localTime.tm_min;
int startTotal = sunsetHour * 60 + sunsetMinute;
int endTotal = sunriseHour * 60 + sunriseMinute;
bool autoActive = (startTotal < endTotal)
? (curTotal >= startTotal && curTotal < endTotal)
: (curTotal >= startTotal || curTotal < endTotal);
Serial.printf("Current Auto-Dimming Status: %s\n", autoActive ? "ACTIVE" : "Inactive");
Serial.printf("Dimming Brightness (night): %d\n", dimBrightness);
}
} else {
// --- Manual (Custom Schedule) dimming mode ---
Serial.printf("Custom Dimming Schedule: %02d:%02d → %02d:%02d\n",
dimStartHour, dimStartMinute, dimEndHour, dimEndMinute);
Serial.printf("Dimming Brightness: %d\n", dimBrightness);
}
Serial.print(F("Countdown Enabled: "));
Serial.println(countdownEnabled ? "Yes" : "No");
Serial.print(F("Countdown Target Timestamp: "));
@@ -599,16 +669,19 @@ void printConfigToSerial() {
Serial.println(isDramaticCountdown ? "Yes" : "No");
Serial.print(F("Custom Message: "));
Serial.println(customMessage);
Serial.print(F("Total Runtime: "));
if (totalUptimeSeconds > 0) {
Serial.println(formatUptime(totalUptimeSeconds));
} else {
Serial.println(F("No runtime recorded yet."));
}
Serial.println(F("========================================"));
Serial.println();
}
// -----------------------------------------------------------------------------
// Web Server and Captive Portal
// -----------------------------------------------------------------------------
@@ -686,8 +759,10 @@ void setupWebServer() {
else if (n == "dimStartMinute") doc[n] = v.toInt();
else if (n == "dimEndHour") doc[n] = v.toInt();
else if (n == "dimEndMinute") doc[n] = v.toInt();
else if (n == "dimBrightness") doc[n] = v.toInt();
else if (n == "showWeatherDescription") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "dimBrightness") {
if (v == "Off" || v == "off") doc[n] = -1;
else doc[n] = v.toInt();
} else if (n == "showWeatherDescription") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "dimmingEnabled") doc[n] = (v == "true" || v == "on" || v == "1");
else if (n == "weatherUnits") doc[n] = v;
@@ -1709,10 +1784,93 @@ void fetchWeather() {
weatherDescription = normalizeWeatherDescription(detailedDesc);
Serial.printf("[WEATHER] Description used: %s\n", weatherDescription.c_str());
// -----------------------------------------
// Sunrise/Sunset for Auto Dimming (local time)
// -----------------------------------------
if (doc.containsKey(F("sys"))) {
JsonObject sys = doc[F("sys")];
if (sys.containsKey(F("sunrise")) && sys.containsKey(F("sunset"))) {
// OWM gives UTC timestamps
time_t sunriseUtc = sys[F("sunrise")].as<time_t>();
time_t sunsetUtc = sys[F("sunset")].as<time_t>();
// Get local timezone offset (in seconds)
long tzOffset = 0;
struct tm local_tm;
time_t now = time(nullptr);
if (localtime_r(&now, &local_tm)) {
tzOffset = mktime(&local_tm) - now;
}
// Convert UTC → local
time_t sunriseLocal = sunriseUtc + tzOffset;
time_t sunsetLocal = sunsetUtc + tzOffset;
// Break into hour/minute
struct tm tmSunrise, tmSunset;
localtime_r(&sunriseLocal, &tmSunrise);
localtime_r(&sunsetLocal, &tmSunset);
sunriseHour = tmSunrise.tm_hour;
sunriseMinute = tmSunrise.tm_min;
sunsetHour = tmSunset.tm_hour;
sunsetMinute = tmSunset.tm_min;
Serial.printf("[WEATHER] Adjusted Sunrise/Sunset (local): %02d:%02d | %02d:%02d\n",
sunriseHour, sunriseMinute, sunsetHour, sunsetMinute);
} else {
Serial.println(F("[WEATHER] Sunrise/Sunset not found in JSON."));
}
} else {
Serial.println(F("[WEATHER] 'sys' object not found in JSON payload."));
}
weatherFetched = true;
// -----------------------------------------
// Save updated sunrise/sunset to config.json
// -----------------------------------------
if (autoDimmingEnabled && sunriseHour >= 0 && sunsetHour >= 0) {
File configFile = LittleFS.open("/config.json", "r");
DynamicJsonDocument doc(1024);
if (configFile) {
DeserializationError error = deserializeJson(doc, configFile);
configFile.close();
if (!error) {
// Check if ANY value has changed
bool valuesChanged = (doc["sunriseHour"] != sunriseHour || doc["sunriseMinute"] != sunriseMinute || doc["sunsetHour"] != sunsetHour || doc["sunsetMinute"] != sunsetMinute);
if (valuesChanged) { // Only write if a change occurred
doc["sunriseHour"] = sunriseHour;
doc["sunriseMinute"] = sunriseMinute;
doc["sunsetHour"] = sunsetHour;
doc["sunsetMinute"] = sunsetMinute;
File f = LittleFS.open("/config.json", "w");
if (f) {
serializeJsonPretty(doc, f);
f.close();
Serial.println(F("[WEATHER] SAVED NEW sunrise/sunset to config.json (Values changed)"));
} else {
Serial.println(F("[WEATHER] Failed to write updated sunrise/sunset to config.json"));
}
} else {
Serial.println(F("[WEATHER] Sunrise/Sunset unchanged, skipping config save."));
}
// --- END MODIFIED COMPARISON LOGIC ---
} else {
Serial.println(F("[WEATHER] JSON parse error when saving updated sunrise/sunset"));
}
}
}
} else {
Serial.printf("[WEATHER] HTTP GET failed, error code: %d, reason: %s\n", httpCode, http.errorToString(httpCode).c_str());
Serial.printf("[WEATHER] HTTP GET failed, error code: %d, reason: %s\n",
httpCode, http.errorToString(httpCode).c_str());
weatherAvailable = false;
weatherFetched = false;
}
@@ -1720,7 +1878,6 @@ void fetchWeather() {
http.end();
}
void loadUptime() {
if (LittleFS.exists("/uptime.dat")) {
File f = LittleFS.open("/uptime.dat", "r");
@@ -2077,68 +2234,73 @@ void loop() {
return;
}
// Dimming
// -----------------------------
// Dimming (auto + manual)
// -----------------------------
time_t now_time = time(nullptr);
struct tm timeinfo;
localtime_r(&now_time, &timeinfo);
int curHour = timeinfo.tm_hour;
int curMinute = timeinfo.tm_min;
int curTotal = curHour * 60 + curMinute;
int startTotal = dimStartHour * 60 + dimStartMinute;
int endTotal = dimEndHour * 60 + dimEndMinute;
bool isDimmingActive = false;
if (dimmingEnabled) {
// Determine if dimming is active (overnight-aware)
if (startTotal < endTotal) {
isDimmingActive = (curTotal >= startTotal && curTotal < endTotal);
} else {
isDimmingActive = (curTotal >= startTotal || curTotal < endTotal);
}
// -----------------------------
// Determine dimming start/end
// -----------------------------
int startTotal, endTotal;
bool dimActive = false;
int targetBrightness = isDimmingActive ? dimBrightness : brightness;
if (targetBrightness == -1) {
if (!displayOff) {
Serial.println(F("[DISPLAY] Turning display OFF (dimming -1)"));
P.displayShutdown(true);
P.displayClear();
displayOff = true;
displayOffByDimming = true;
displayOffByBrightness = false;
}
} else {
if (displayOff && displayOffByDimming) {
Serial.println(F("[DISPLAY] Waking display (dimming end)"));
P.displayShutdown(false);
displayOff = false;
displayOffByDimming = false;
}
P.setIntensity(targetBrightness);
}
if (autoDimmingEnabled) {
startTotal = sunsetHour * 60 + sunsetMinute;
endTotal = sunriseHour * 60 + sunriseMinute;
} else if (dimmingEnabled) {
startTotal = dimStartHour * 60 + dimStartMinute;
endTotal = dimEndHour * 60 + dimEndMinute;
} else {
// Dimming disabled: just obey brightness slider
if (brightness == -1) {
if (!displayOff) {
Serial.println(F("[DISPLAY] Turning display OFF (brightness -1)"));
P.displayShutdown(true);
P.displayClear();
displayOff = true;
displayOffByBrightness = true;
displayOffByDimming = false;
}
startTotal = endTotal = -1; // not used
}
// -----------------------------
// Check if dimming should be active
// -----------------------------
if (autoDimmingEnabled || dimmingEnabled) {
if (startTotal < endTotal) {
dimActive = (curTotal >= startTotal && curTotal < endTotal);
} else {
if (displayOff && displayOffByBrightness) {
Serial.println(F("[DISPLAY] Waking display (brightness changed)"));
P.displayShutdown(false);
displayOff = false;
displayOffByBrightness = false;
}
P.setIntensity(brightness);
dimActive = (curTotal >= startTotal || curTotal < endTotal); // overnight
}
}
// -----------------------------
// Apply brightness / display on-off
// -----------------------------
int targetBrightness;
if (dimActive) targetBrightness = dimBrightness;
else targetBrightness = brightness;
if (targetBrightness == -1) {
if (!displayOff) {
Serial.println(F("[DISPLAY] Turning display OFF (dimming -1)"));
P.displayShutdown(true);
P.displayClear();
displayOff = true;
displayOffByDimming = dimActive;
displayOffByBrightness = !dimActive;
}
} else {
if (displayOff && ((dimActive && displayOffByDimming) || (!dimActive && displayOffByBrightness))) {
Serial.println(F("[DISPLAY] Waking display (dimming end)"));
P.displayShutdown(false);
displayOff = false;
displayOffByDimming = false;
displayOffByBrightness = false;
}
P.setIntensity(targetBrightness);
}
// --- IMMEDIATE COUNTDOWN FINISH TRIGGER ---
if (countdownEnabled && !countdownFinished && ntpSyncSuccessful && countdownTargetTimestamp > 0 && now_time >= countdownTargetTimestamp) {
countdownFinished = true;

View File

@@ -586,29 +586,47 @@ textarea::placeholder {
oninput="brightnessValue.textContent = (this.value == -1 ? 'Off' : this.value); setBrightnessLive(this.value);">
<br><br><br>
<label style="display: flex; align-items: center; justify-content: space-between;">
<span style="margin-right: 0.5em;">Enable Dimming:</span>
<span class="toggle-switch">
<input type="checkbox" id="dimmingEnabled" name="dimmingEnabled">
<span class="toggle-slider"></span>
</span>
</label>
<div class="form-row two-col">
<div>
<label for="dimStartTime">Dimming Start Time:</label>
<input type="time" id="dimStartTime" value="18:00">
</div>
<label style="display: flex; align-items: center; justify-content: space-between; margin-top: 1rem;">
<span style="margin-right: 0.5em;">Automatic Dimming:</span>
<span class="toggle-switch">
<input type="checkbox" id="autoDimmingEnabled" name="autoDimmingEnabled">
<span class="toggle-slider"></span>
</span>
</label>
<div id="autoDimmingNote" class="small">
Requires a valid OpenWeather API key.
</div>
<div>
<label for="dimEndTime">Dimming End Time:</label>
<input type="time" id="dimEndTime" value="08:00">
</div>
</div>
<label style="margin-top: 1.75rem;" for="dimBrightness">Dimming Brightness: <span id="dimmingBrightnessValue">2</span></label>
<input style="width: 100%;" type="range" min="-1" max="15" name="dimming_brightness" id="dimBrightness" value="2"
oninput="dimmingBrightnessValue.textContent = (this.value == -1 ? 'Off' : this.value);">
<label style="display: flex; align-items: center; justify-content: space-between; margin-top: 0.75rem;">
<span style="margin-right: 0.5em;">Custom Dimming:</span>
<span class="toggle-switch">
<input type="checkbox" id="dimmingEnabled" name="dimmingEnabled">
<span class="toggle-slider"></span>
</span>
</label>
<div class="form-row two-col">
<div>
<label for="dimStartTime">Start Time:</label>
<input type="time" id="dimStartTime" value="18:00">
</div>
<div>
<label for="dimEndTime">End Time:</label>
<input type="time" id="dimEndTime" value="08:00">
</div>
</div>
<label style="margin-top: 1.75rem;" for="dimBrightness">
Dimming Brightness: <span id="dimmingBrightnessValue">2</span>
</label>
<input style="width: 100%;" type="range" min="-1" max="15"
name="dimming_brightness" id="dimBrightness" value="2"
oninput="dimmingBrightnessValue.textContent = (this.value == -1 ? 'Off' : this.value);">
<br><br><br>
<div class="form-group">
<label style="display: flex; align-items: center; justify-content: space-between;">
@@ -630,11 +648,11 @@ textarea::placeholder {
</div>
<div class="form-row two-col">
<div class="form-group">
<label for="countdownDate">Countdown Date:</label>
<label for="countdownDate">Date:</label>
<input type="date" id="countdownDate" name="countdownDate" class="form-control">
</div>
<div class="form-group">
<label for="countdownTime">Countdown Time:</label>
<label for="countdownTime">Time:</label>
<input type="time" id="countdownTime" name="countdownTime" class="form-control">
</div>
</div>
@@ -735,11 +753,12 @@ window.onload = function () {
const apiInput = document.getElementById('openWeatherApiKey');
if (data.openWeatherApiKey && data.openWeatherApiKey.trim() !== '') {
apiInput.value = MASK;
hasSavedKey = true; // mark it as having a saved key
hasSavedKey = true;
} else {
apiInput.value = '';
hasSavedKey = false;
}
document.getElementById('openWeatherCity').value = data.openWeatherCity || '';
document.getElementById('openWeatherCountry').value = data.openWeatherCountry || '';
document.getElementById('weatherUnits').checked = (data.weatherUnits === "imperial");
@@ -760,86 +779,95 @@ window.onload = function () {
document.getElementById('colonBlinkEnabled').checked = !!data.colonBlinkEnabled;
document.getElementById('showWeatherDescription').checked = !!data.showWeatherDescription;
// Dimming controls
// --- Dimming Controls ---
const autoDimmingEl = document.getElementById('autoDimmingEnabled');
const dimmingEnabledEl = document.getElementById('dimmingEnabled');
const isDimming = (data.dimmingEnabled === true || data.dimmingEnabled === "true" || data.dimmingEnabled === 1);
dimmingEnabledEl.checked = isDimming;
// Defer field enabling until checkbox state is rendered
setTimeout(() => {
setDimmingFieldsEnabled(dimmingEnabledEl.checked);
}, 0);
dimmingEnabledEl.addEventListener('change', function () {
setDimmingFieldsEnabled(this.checked);
const apiInputEl = document.getElementById('openWeatherApiKey');
// Evaluate flags from config.json
const isAutoDimming = (data.autoDimmingEnabled === true || data.autoDimmingEnabled === "true" || data.autoDimmingEnabled === 1);
const isCustomDimming = (data.dimmingEnabled === true || data.dimmingEnabled === "true" || data.dimmingEnabled === 1);
// Set toggle states
autoDimmingEl.checked = isAutoDimming;
dimmingEnabledEl.checked = isCustomDimming;
// Apply initial dimming state
setDimmingFieldsEnabled();
// Attach listeners (mutually exclusive + API dependency)
if (apiInputEl) apiInputEl.addEventListener('input', setDimmingFieldsEnabled);
autoDimmingEl.addEventListener('change', () => {
if (autoDimmingEl.checked) dimmingEnabledEl.checked = false;
setDimmingFieldsEnabled();
});
dimmingEnabledEl.addEventListener('change', () => {
if (dimmingEnabledEl.checked) autoDimmingEl.checked = false;
setDimmingFieldsEnabled();
});
// Set field values from config
document.getElementById('dimStartTime').value =
(data.dimStartHour !== undefined ? String(data.dimStartHour).padStart(2, '0') : "18") + ":" +
(data.dimStartMinute !== undefined ? String(data.dimStartMinute).padStart(2, '0') : "00");
(data.dimStartHour !== undefined ? String(data.dimStartHour).padStart(2, '0') : "18") + ":" +
(data.dimStartMinute !== undefined ? String(data.dimStartMinute).padStart(2, '0') : "00");
document.getElementById('dimEndTime').value =
(data.dimEndHour !== undefined ? String(data.dimEndHour).padStart(2, '0') : "08") + ":" +
(data.dimEndMinute !== undefined ? String(data.dimEndMinute).padStart(2, '0') : "00");
(data.dimEndHour !== undefined ? String(data.dimEndHour).padStart(2, '0') : "08") + ":" +
(data.dimEndMinute !== undefined ? String(data.dimEndMinute).padStart(2, '0') : "00");
document.getElementById('dimBrightness').value = (data.dimBrightness !== undefined ? data.dimBrightness : 2);
// Then update the span's text content with that value
document.getElementById('dimmingBrightnessValue').textContent = (document.getElementById('dimBrightness').value == -1 ? 'Off' : document.getElementById('dimBrightness').value);
setDimmingFieldsEnabled(!!data.dimmingEnabled);
document.getElementById('dimmingBrightnessValue').textContent =
(document.getElementById('dimBrightness').value == -1 ? 'Off' : document.getElementById('dimBrightness').value);
// --- Populate Countdown Fields ---
document.getElementById('isDramaticCountdown').checked = !!(data.countdown && data.countdown.isDramaticCountdown);
const countdownEnabledEl = document.getElementById('countdownEnabled'); // Get reference
countdownEnabledEl.checked = !!(data.countdown && data.countdown.enabled);
if (data.countdown && data.countdown.targetTimestamp) {
// Convert Unix timestamp (seconds) to milliseconds for JavaScript Date object
const targetDate = new Date(data.countdown.targetTimestamp * 1000);
const year = targetDate.getFullYear();
// Month is 0-indexed in JS, so add 1
const month = (targetDate.getMonth() + 1).toString().padStart(2, '0');
const day = targetDate.getDate().toString().padStart(2, '0');
const hours = targetDate.getHours().toString().padStart(2, '0');
const minutes = targetDate.getMinutes().toString().padStart(2, '0');
document.getElementById('countdownDate').value = `${year}-${month}-${day}`;
document.getElementById('countdownTime').value = `${hours}:${minutes}`;
const targetDate = new Date(data.countdown.targetTimestamp * 1000);
const year = targetDate.getFullYear();
const month = (targetDate.getMonth() + 1).toString().padStart(2, '0');
const day = targetDate.getDate().toString().padStart(2, '0');
const hours = targetDate.getHours().toString().padStart(2, '0');
const minutes = targetDate.getMinutes().toString().padStart(2, '0');
document.getElementById('countdownDate').value = `${year}-${month}-${day}`;
document.getElementById('countdownTime').value = `${hours}:${minutes}`;
} else {
// Clear fields if no target timestamp is set
document.getElementById('countdownDate').value = '';
document.getElementById('countdownTime').value = '';
document.getElementById('countdownDate').value = '';
document.getElementById('countdownTime').value = '';
}
// Countdown Label Input Validation
const countdownLabelInput = document.getElementById('countdownLabel');
countdownLabelInput.addEventListener('input', function() {
// Convert to uppercase and remove any characters that are not A-Z or space
// Note: The `maxlength` attribute in HTML handles the length limit.
this.value = this.value.toUpperCase().replace(/[^A-Z0-9 :!'\-.,_\+%\/?]/g, '');
});
// Set initial value for countdownLabel from config.json (apply validation on load too)
if (data.countdown && data.countdown.label) {
countdownLabelInput.value = data.countdown.label.toUpperCase().replace(/[^A-Z0-9 :!'\-.,_\+%\/?]/g, '');
countdownLabelInput.value = data.countdown.label.toUpperCase().replace(/[^A-Z0-9 :!'\-.,_\+%\/?]/g, '');
} else {
countdownLabelInput.value = '';
countdownLabelInput.value = '';
}
// Countdown Toggle Event Listener & Field Enabling
// If you're using onchange="setCountdownEnabled(this.checked)" directly in HTML,
// you would add setCountdownFieldsEnabled(this.checked) there as well.
// If you are using addEventListener, keep this:
// Countdown Toggle Event Listener & Field Enabling
countdownEnabledEl.addEventListener('change', function() {
setCountdownEnabled(this.checked); // Sends command to ESP
setCountdownFieldsEnabled(this.checked); // Enables/disables local fields
setCountdownEnabled(this.checked);
setCountdownFieldsEnabled(this.checked);
});
const dramaticCountdownEl = document.getElementById('isDramaticCountdown');
dramaticCountdownEl.addEventListener('change', function () {
setIsDramaticCountdown(this.checked);
setIsDramaticCountdown(this.checked);
});
// Set initial state of fields when page loads
setCountdownFieldsEnabled(countdownEnabledEl.checked);
if (data.customMessage !== undefined) {
document.getElementById('customMessage').value = data.customMessage;
}
if (data.customMessage !== undefined) {
document.getElementById('customMessage').value = data.customMessage;
}
// Auto-detect browser's timezone if not set in config
if (!data.timeZone) {
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (
tz &&
document.getElementById('timeZone').querySelector(`[value="${tz}"]`)
) {
if (tz && document.getElementById('timeZone').querySelector(`[value="${tz}"]`)) {
document.getElementById('timeZone').value = tz;
} else {
document.getElementById('timeZone').value = '';
@@ -903,10 +931,21 @@ async function submitConfig(event) {
formData.set('showHumidity', document.getElementById('showHumidity').checked ? 'on' : '');
formData.set('colonBlinkEnabled', document.getElementById('colonBlinkEnabled').checked ? 'on' : '');
//dimming
formData.set('dimmingEnabled', document.getElementById('dimmingEnabled').checked ? 'true' : 'false');
// --- Dimming ---
const autoDimmingChecked = document.getElementById('autoDimmingEnabled').checked;
const customDimmingChecked = document.getElementById('dimmingEnabled').checked;
// Mutual exclusivity (if both checked somehow, keep auto as priority)
if (autoDimmingChecked && customDimmingChecked) {
formData.set('autoDimmingEnabled', 'true');
formData.set('dimmingEnabled', 'false');
} else {
formData.set('autoDimmingEnabled', autoDimmingChecked ? 'true' : 'false');
formData.set('dimmingEnabled', customDimmingChecked ? 'true' : 'false');
}
const dimStart = document.getElementById('dimStartTime').value; // "18:45"
const dimEnd = document.getElementById('dimEndTime').value; // "08:30"
const dimEnd = document.getElementById('dimEndTime').value; // "08:30"
// Parse hour and minute
if (dimStart) {
@@ -1367,8 +1406,6 @@ async function getLocation() {
}
}
// --- OpenWeather API Key field UX ---
const MASK_LENGTH = 32;
const MASK = '*'.repeat(MASK_LENGTH);
@@ -1410,6 +1447,7 @@ apiInput.addEventListener('blur', () => {
hasSavedKey = false; // user cleared the key
apiInput.dataset.clearing = 'false';
apiInput.value = ''; // leave blank
setDimmingFieldsEnabled();
}
}
});
@@ -1527,6 +1565,71 @@ function clearCustomMessage() {
});
}
// --- Dimming Controls Logic (The correct version) ---
function setDimmingFieldsEnabled() {
const apiKeyField = document.getElementById('openWeatherApiKey');
const autoDimming = document.getElementById('autoDimmingEnabled');
const dimmingEnabled = document.getElementById('dimmingEnabled');
const dimStart = document.getElementById('dimStartTime');
const dimEnd = document.getElementById('dimEndTime');
const dimBrightness = document.getElementById('dimBrightness');
const noteEl = document.getElementById('autoDimmingNote');
if (!apiKeyField || !autoDimming || !dimmingEnabled) return;
const currentApiKeyInput = apiKeyField.value.trim();
// Checks if a key is saved (hasSavedKey) OR if the user is currently typing a new one.
const isKeyPresent = hasSavedKey || (currentApiKeyInput !== '' && currentApiKeyInput !== MASK);
// --- 1. Control Auto Dimming based on Key Presence ---
// Meets requirement: "when page load after autodim has been saved to json,
// if user removes the api key (masked) the toggle auto dim toggle should get disabled"
if (!isKeyPresent) {
autoDimming.checked = false;
autoDimming.disabled = true;
if (noteEl) noteEl.style.display = 'block';
} else {
autoDimming.disabled = false;
if (noteEl) noteEl.style.display = 'none';
}
// Custom Dimming toggle is always enabled (since it's not key-dependent)
dimmingEnabled.disabled = false;
// --- 2. Control Dependent Fields based on Active Mode ---
const isAutoDimmingActive = autoDimming.checked && isKeyPresent; // Auto is only active if checked AND key is present
const isCustomDimmingActive = dimmingEnabled.checked;
const isDimmingActive = isAutoDimmingActive || isCustomDimmingActive; // Brightness slider logic
// BRIGHTNESS SLIDER: Enabled if EITHER mode is active.
if (dimBrightness) {
dimBrightness.disabled = !isDimmingActive;
}
// START/END TIME FIELDS: Enabled ONLY if Custom Dimming is checked (key not needed).
const isCustomTimeEnabled = dimmingEnabled.checked;
if (dimStart) {
dimStart.disabled = !isCustomTimeEnabled;
}
if (dimEnd) {
dimEnd.disabled = !isCustomTimeEnabled;
}
}
window.addEventListener('DOMContentLoaded', () => {
const apiKeyEl = document.getElementById('openWeatherApiKey');
const autoEl = document.getElementById('autoDimmingEnabled');
const dimEl = document.getElementById('dimmingEnabled');
if (apiKeyEl) {
apiKeyEl.addEventListener('input', setDimmingFieldsEnabled);
apiKeyEl.addEventListener('change', setDimmingFieldsEnabled);
}
if (autoEl) autoEl.addEventListener('change', setDimmingFieldsEnabled);
if (dimEl) dimEl.addEventListener('change', setDimmingFieldsEnabled);
});
</script>
</body>