From 03a5432cbe8373dd23849e05fb40d35c289b00a7 Mon Sep 17 00:00:00 2001 From: M-Factory Date: Tue, 11 Nov 2025 23:16:17 +0900 Subject: [PATCH] Added automatic dimming based on sunrise/sunset times from the weather API --- ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino | 284 ++++++++++++++---- ESPTimeCast_ESP32/data/index.html | 257 +++++++++++----- ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino | 306 +++++++++++++++----- ESPTimeCast_ESP8266/data/index.html | 257 +++++++++++----- 4 files changed, 818 insertions(+), 286 deletions(-) diff --git a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino index 2db5216..861966c 100644 --- a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino +++ b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino @@ -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() : true; showWeatherDescription = doc["showWeatherDescription"] | false; + // --- Dimming settings --- + if (doc["dimmingEnabled"].is()) { + dimmingEnabled = doc["dimmingEnabled"].as(); + } else { + String de = doc["dimmingEnabled"].as(); + dimmingEnabled = (de == "true" || de == "1" || de == "on"); + } + String de = doc["dimmingEnabled"].as(); 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()) { + dimBrightness = doc["dimBrightness"].as(); + } else { + String val = doc["dimBrightness"].as(); + if (val.equalsIgnoreCase("off")) dimBrightness = -1; + else dimBrightness = val.toInt(); + } + + // --- Automatic dimming --- + if (doc.containsKey("autoDimmingEnabled")) { + if (doc["autoDimmingEnabled"].is()) { + autoDimmingEnabled = doc["autoDimmingEnabled"].as(); + } else { + String val = doc["autoDimmingEnabled"].as(); + 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 sunsetUtc = sys[F("sunset")].as(); + + // 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); } diff --git a/ESPTimeCast_ESP32/data/index.html b/ESPTimeCast_ESP32/data/index.html index 4b8fcc6..9cd1a5c 100644 --- a/ESPTimeCast_ESP32/data/index.html +++ b/ESPTimeCast_ESP32/data/index.html @@ -586,29 +586,47 @@ textarea::placeholder { oninput="brightnessValue.textContent = (this.value == -1 ? 'Off' : this.value); setBrightnessLive(this.value);">


- -
-
- - -
+ +
+ Requires a valid OpenWeather API key. +
-
- - -
-
- - + + + +
+
+ + +
+ +
+ + +
+
+ + + +


- +
- +
@@ -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); +}); diff --git a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino index e4bf3ff..42dbf70 100644 --- a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino +++ b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino @@ -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() : true; showWeatherDescription = doc["showWeatherDescription"] | false; - String de = doc["dimmingEnabled"].as(); - dimmingEnabled = (de == "true" || de == "on" || de == "1"); + // --- Dimming settings --- + if (doc["dimmingEnabled"].is()) { + dimmingEnabled = doc["dimmingEnabled"].as(); + } else { + String de = doc["dimmingEnabled"].as(); + 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()) { + dimBrightness = doc["dimBrightness"].as(); + } else { + String val = doc["dimBrightness"].as(); + if (val.equalsIgnoreCase("off")) dimBrightness = -1; + else dimBrightness = val.toInt(); + } + + // --- Automatic dimming --- + if (doc.containsKey("autoDimmingEnabled")) { + if (doc["autoDimmingEnabled"].is()) { + autoDimmingEnabled = doc["autoDimmingEnabled"].as(); + } else { + String val = doc["autoDimmingEnabled"].as(); + 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 sunsetUtc = sys[F("sunset")].as(); + + // 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; diff --git a/ESPTimeCast_ESP8266/data/index.html b/ESPTimeCast_ESP8266/data/index.html index 4b8fcc6..9cd1a5c 100644 --- a/ESPTimeCast_ESP8266/data/index.html +++ b/ESPTimeCast_ESP8266/data/index.html @@ -586,29 +586,47 @@ textarea::placeholder { oninput="brightnessValue.textContent = (this.value == -1 ? 'Off' : this.value); setBrightnessLive(this.value);">


- -
-
- - -
+ +
+ Requires a valid OpenWeather API key. +
-
- - -
-
- - + + + +
+
+ + +
+ +
+ + +
+
+ + + +


- +
- +
@@ -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); +});