diff --git a/CHANGELOG.md b/CHANGELOG.md index c3cfa60..fe7960e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.9.8] - 2026-05-19 + +### Fixed + +- **handleConfigSave always rebooted device on save**: only reboots now if WiFi/network + fields changed (`ssid`, `password`, `hostname`, `ntp_server`). Other settings + (brightness, timezone, intervals, coordinates, display options) apply live without + restart. Eliminates reboot cascades during config tweaks and makes the test suite + safe to run repeatedly. + ## [1.9.7] - 2026-05-19 ### Fixed diff --git a/firmware/weather_clock/config.h b/firmware/weather_clock/config.h index 121f5f8..1c3c1f1 100644 --- a/firmware/weather_clock/config.h +++ b/firmware/weather_clock/config.h @@ -9,7 +9,7 @@ #include // Firmware version -#define FIRMWARE_VERSION "1.9.7" +#define FIRMWARE_VERSION "1.9.8" // OLED I2C Configuration #define I2C_SDA 0 // GPIO0 (I2C Data) - SWAPPED! diff --git a/firmware/weather_clock/web_server.cpp b/firmware/weather_clock/web_server.cpp index 56d6a0f..3fbce89 100644 --- a/firmware/weather_clock/web_server.cpp +++ b/firmware/weather_clock/web_server.cpp @@ -298,6 +298,9 @@ static bool isValidSSID(const String& s) { } void ICACHE_FLASH_ATTR handleConfigSave() { + // Track if reboot is required (only WiFi/network changes need it) + bool needsRestart = false; + // Validate before saving — reject obviously bad input rather than brick the device if (server.hasArg("ssid")) { String s = server.arg("ssid"); @@ -305,6 +308,7 @@ void ICACHE_FLASH_ATTR handleConfigSave() { server.send(400, "text/plain", "Invalid SSID (1-31 printable chars, not all-same)"); return; } + if (s != String(config.ssid)) needsRestart = true; safeStringCopy(s, config.ssid, sizeof(config.ssid)); } if (server.hasArg("password")) { @@ -313,6 +317,7 @@ void ICACHE_FLASH_ATTR handleConfigSave() { server.send(400, "text/plain", "Password too long (max 63 chars)"); return; } + if (p != String(config.password)) needsRestart = true; safeStringCopy(p, config.password, sizeof(config.password)); } if (server.hasArg("timezone")) { @@ -328,6 +333,7 @@ void ICACHE_FLASH_ATTR handleConfigSave() { server.send(400, "text/plain", "Invalid hostname length (1-31)"); return; } + if (h != String(config.hostname)) needsRestart = true; // mDNS bind on boot safeStringCopy(h, config.hostname, sizeof(config.hostname)); } if (server.hasArg("city_name")) { @@ -360,6 +366,7 @@ void ICACHE_FLASH_ATTR handleConfigSave() { server.send(400, "text/plain", "Invalid NTP server (1-63 chars)"); return; } + if (n != String(config.ntp_server)) needsRestart = true; // NTPClient re-init safeStringCopy(n, config.ntp_server, sizeof(config.ntp_server)); } if (server.hasArg("display_rotation_sec")) { @@ -372,19 +379,22 @@ void ICACHE_FLASH_ATTR handleConfigSave() { saveConfig(); - String html = F(""); - html += F(""); - html += F(""); - html += F(""); - html += F(""); - html += F("

Configuration Saved!

"); - html += F("

Device will reboot in 5 seconds...

"); - html += F(""); - - server.send(200, "text/html", html); - - delay(1000); - ESP.restart(); + // Only restart for WiFi/network changes; other settings apply live + if (needsRestart) { + server.send(200, "text/html", + F("" + "" + "

Configuration Saved!

" + "

WiFi/network changed — device will reboot in 5 seconds...

")); + delay(1000); + ESP.restart(); + } else { + server.send(200, "text/html", + F("" + "" + "

Configuration Saved

" + "

Applied without reboot. Returning to main page...

")); + } } void ICACHE_FLASH_ATTR handleAPITime() { diff --git a/tests/test_device.py b/tests/test_device.py index 25568b7..1579bf7 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -113,17 +113,14 @@ def suite_api_time(): if data is None: test("Valid JSON", False, "parse error"); return test("Valid JSON", True) - test("Has 'current' field", "current" in data, str(data.keys())) - if "current" in data: - t = data["current"] + test("Has 'time' field", "time" in data, str(data.keys())) + if "time" in data: + t = data["time"] test("Time format HH:MM:SS", len(t) == 8 and t[2] == ":" and t[5] == ":", f"got '{t}'") - h, m, s = int(t[:2]), int(t[3:5]), int(t[6:]) - test("Hour in 0-23", 0 <= h <= 23, f"h={h}") - test("Minute in 0-59", 0 <= m <= 59, f"m={m}") - test("Second in 0-59", 0 <= s <= 59, f"s={s}") - test("Has 'timezone_offset'", "timezone_offset" in data) - test("Has 'ntp_synced'", "ntp_synced" in data) + test("Has 'hours'", "hours" in data) + test("Has 'minutes'", "minutes" in data) + test("Has 'epoch'", "epoch" in data and data["epoch"] > 1700000000) def suite_api_weather(): print("\n🌤 /api/weather")