mirror of
https://github.com/mfactory-osaka/ESPTimeCast.git
synced 2026-02-19 11:54:56 -05:00
Added automatic dimming based on sunrise/sunset times from the weather API
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user