/* * web_server.cpp - Web server handlers * TJ-56-654 Weather Clock v1.9.2 */ #include "globals.h" #include #include // Dummy function for compatibility void ICACHE_FLASH_ATTR displaySegments(const uint8_t segments[]) { // Not used with OLED } // PROGMEM templates for handleRoot() const char ROOT_HTML_HEADER[] PROGMEM = "" "" "" "TJ-56-654 Clock v" FIRMWARE_VERSION "" "" "" "" "
" "

TJ-56-654 NTP Clock v" FIRMWARE_VERSION "

" "
--:--:--
"; const char ROOT_HTML_FOOTER[] PROGMEM = "Configuration" "Debug Info" "Firmware Update" "Status (JSON)" "" "
"; void ICACHE_FLASH_ATTR handleRoot() { char buf[150]; server.setContentLength(CONTENT_LENGTH_UNKNOWN); server.send(200, "text/html", ""); server.sendContent_P(ROOT_HTML_HEADER); if (lastError != "") { snprintf_P(buf, sizeof(buf), PSTR("
Error: %s
"), lastError.c_str()); server.sendContent(buf); } snprintf_P(buf, sizeof(buf), PSTR("
WiFi: %s
"), WiFi.SSID().c_str()); server.sendContent(buf); snprintf_P(buf, sizeof(buf), PSTR("
IP: %s
"), WiFi.localIP().toString().c_str()); server.sendContent(buf); snprintf_P(buf, sizeof(buf), PSTR("
Hostname: %s.local
"), config.hostname); server.sendContent(buf); snprintf_P(buf, sizeof(buf), PSTR("
Uptime: %lu seconds
"), millis()/1000); server.sendContent(buf); if (!timeClient.isTimeSet()) { snprintf_P(buf, sizeof(buf), PSTR("
NTP not synced yet
Attempts: %d | Success: %d
"), ntpAttempts, ntpSuccesses); server.sendContent(buf); } server.sendContent_P(ROOT_HTML_FOOTER); server.sendContent(""); } // PROGMEM templates for handleDebug() const char DEBUG_HTML_HEADER[] PROGMEM = "" "" "" "Debug Info" "" "" "
" "

Debug Information

" "

Network

";

const char DEBUG_HTML_FOOTER[] PROGMEM =
  "

Actions

" "" "" "" "" "" "" "
"; void ICACHE_FLASH_ATTR handleDebug() { char buf[200]; server.setContentLength(CONTENT_LENGTH_UNKNOWN); server.send(200, "text/html", ""); server.sendContent_P(DEBUG_HTML_HEADER); snprintf_P(buf, sizeof(buf), PSTR("SSID: %s\nIP: %s\nGateway: %s\nDNS: %s\nRSSI: %d dBm\nHostname: %s\n"), WiFi.SSID().c_str(), WiFi.localIP().toString().c_str(), WiFi.gatewayIP().toString().c_str(), WiFi.dnsIP().toString().c_str(), WiFi.RSSI(), config.hostname); server.sendContent(buf); server.sendContent_P(PSTR("

Internet Connectivity

Status: "));
  server.sendContent_P(internetConnected ? PSTR("Connected\n
") : PSTR("Not connected\n")); server.sendContent_P(PSTR("

NTP

"));
  snprintf_P(buf, sizeof(buf), PSTR("Server: %s\nUpdate interval: %u seconds\nSynced: "), config.ntp_server, config.ntp_interval);
  server.sendContent(buf);
  server.sendContent_P(timeClient.isTimeSet() ? PSTR("Yes\n") : PSTR("No\n"));

  snprintf_P(buf, sizeof(buf), PSTR("UTC time: %s\n"), timeClient.getFormattedTime().c_str());
  server.sendContent(buf);

  if (timeClient.isTimeSet()) {
    unsigned long epochTime = timeClient.getEpochTime();
    unsigned long localTime = epochTime + getTotalOffset(epochTime);
    snprintf_P(buf, sizeof(buf), PSTR("Local time: %02d:%02d:%02d\n"), (int)((localTime/3600)%24), (int)((localTime/60)%60), (int)(localTime%60));
    server.sendContent(buf);
  }

  snprintf_P(buf, sizeof(buf), PSTR("Attempts: %d\nSuccesses: %d\nLast error: %s\n
"), ntpAttempts, ntpSuccesses, lastError.c_str()); server.sendContent(buf); server.sendContent_P(PSTR("

Timezone & DST

"));
  snprintf_P(buf, sizeof(buf), PSTR("Base offset: %.1f hours (%ld seconds)\nDST enabled: %s\n"),
    config.timezone_offset/3600.0, config.timezone_offset, config.dst_enabled ? "Yes" : "No");
  server.sendContent(buf);

  if (config.dst_enabled && timeClient.isTimeSet()) {
    unsigned long epochTime = timeClient.getEpochTime();
    bool inDST = isDST(epochTime);
    snprintf_P(buf, sizeof(buf), PSTR("DST active now: %s\nTotal offset: %.1f hours\n"),
      inDST ? "Yes (+1 hour)" : "No", getTotalOffset(epochTime)/3600.0);
    server.sendContent(buf);
  }

  snprintf_P(buf, sizeof(buf), PSTR("Time format: %s\n
"), config.hour_format_24 ? "24-hour" : "12-hour (AM/PM)"); server.sendContent(buf); server.sendContent_P(PSTR("

Weather

"));
  snprintf_P(buf, sizeof(buf), PSTR("Enabled: %s\nValid data: %s\n"),
    config.weather_enabled ? "Yes" : "No",
    weather.valid ? "Yes" : "No");
  server.sendContent(buf);

  if (weather.valid) {
    snprintf_P(buf, sizeof(buf), PSTR("Temperature: %.1f C\nWeather code: %d\nWind speed: %.1f km/h\nLast update: %lu sec ago\n"),
      weather.temperature, weather.weathercode, weather.windspeed, weather.lastUpdate/1000);
    server.sendContent(buf);
  }

  snprintf_P(buf, sizeof(buf), PSTR("City: %s\nLocation: %.6f, %.6f\nUpdate interval: %u seconds\n
"), config.city_name, config.latitude, config.longitude, config.weather_interval); server.sendContent(buf); server.sendContent_P(PSTR("

Sunrise/Sunset

"));
  snprintf_P(buf, sizeof(buf), PSTR("Enabled: %s\n"), config.show_sunrise_sunset ? "Yes" : "No");
  server.sendContent(buf);

  if (sunTimes.lastDay != -1) {
    snprintf_P(buf, sizeof(buf), PSTR("Data available\nSunrise: %s (%d min)\nSunset: %s (%d min)\nLast update day: %d\n
"), sunTimes.sunrise, sunTimes.sunriseMinutes, sunTimes.sunset, sunTimes.sunsetMinutes, sunTimes.lastDay); } else { snprintf_P(buf, sizeof(buf), PSTR("No data\n")); } server.sendContent(buf); server.sendContent_P(PSTR("

Display

"));
  const char* modeStr = (displayMode == 0) ? " (Time)" : (displayMode == 1) ? " (Weather)" : " (Sun times)";
  snprintf_P(buf, sizeof(buf), PSTR("Current mode: %d%s\nRotation interval: %u seconds\nBrightness: %d (0-7)\nShow weather: %s\nShow sun times: %s\nNTP synced: %s\n
"), displayMode, modeStr, config.display_rotation_sec, config.brightness, config.show_weather ? "Yes" : "No", config.show_sunrise_sunset ? "Yes" : "No", timeClient.isTimeSet() ? "Yes" : "No"); server.sendContent(buf); server.sendContent_P(PSTR("

System

"));
  snprintf_P(buf, sizeof(buf), PSTR("Uptime: %lu seconds\nFree heap: %u bytes\nChip ID: %X\nFlash size: %u bytes\nSDK version: %s\n
"), millis()/1000, ESP.getFreeHeap(), ESP.getChipId(), ESP.getFlashChipSize(), ESP.getSdkVersion()); server.sendContent(buf); server.sendContent_P(DEBUG_HTML_FOOTER); server.sendContent(""); } void ICACHE_FLASH_ATTR handleTestNTP() { testInternetConnectivity(); updateNTPTime(); server.sendHeader("Location", "/debug"); server.send(303); } void ICACHE_FLASH_ATTR handleTestDisplay() { uint8_t data[] = {0xFF, 0xFF, 0xFF, 0xFF}; displaySegments(data); delay(3000); server.sendHeader("Location", "/debug"); server.send(303); } // PROGMEM templates for handleConfig() const char CONFIG_HTML_HEADER[] PROGMEM = "" "" "" "Configuration" "" "" "
" "

Configuration

" "
"; const char CONFIG_HTML_FOOTER[] PROGMEM = "" "
" "

Back to Home

" "
"; void ICACHE_FLASH_ATTR handleConfig() { char buf[150]; server.setContentLength(CONTENT_LENGTH_UNKNOWN); server.send(200, "text/html", ""); server.sendContent_P(CONFIG_HTML_HEADER); snprintf_P(buf, sizeof(buf), PSTR(""), config.ssid); server.sendContent(buf); snprintf_P(buf, sizeof(buf), PSTR(""), config.password); server.sendContent(buf); snprintf_P(buf, sizeof(buf), PSTR(""), config.timezone_offset); server.sendContent(buf); snprintf_P(buf, sizeof(buf), PSTR(""), config.brightness); server.sendContent(buf); snprintf_P(buf, sizeof(buf), PSTR(""), config.hostname); server.sendContent(buf); server.sendContent_P(PSTR("

Weather Settings

")); snprintf_P(buf, sizeof(buf), PSTR(""), config.city_name); server.sendContent(buf); snprintf_P(buf, sizeof(buf), PSTR(""), config.latitude); server.sendContent(buf); snprintf_P(buf, sizeof(buf), PSTR(""), config.longitude); server.sendContent(buf); snprintf_P(buf, sizeof(buf), PSTR(""), config.weather_interval); server.sendContent(buf); server.sendContent_P(PSTR("

Display Settings

")); snprintf_P(buf, sizeof(buf), PSTR(""), config.display_rotation_sec); server.sendContent(buf); server.sendContent_P(CONFIG_HTML_FOOTER); server.sendContent(""); } void ICACHE_FLASH_ATTR handleConfigSave() { if (server.hasArg("ssid")) { safeStringCopy(server.arg("ssid"), config.ssid, sizeof(config.ssid)); } if (server.hasArg("password")) { safeStringCopy(server.arg("password"), config.password, sizeof(config.password)); } if (server.hasArg("timezone")) { config.timezone_offset = server.arg("timezone").toInt(); } if (server.hasArg("brightness")) { config.brightness = server.arg("brightness").toInt(); } if (server.hasArg("hostname")) { safeStringCopy(server.arg("hostname"), config.hostname, sizeof(config.hostname)); } if (server.hasArg("city_name")) { safeStringCopy(server.arg("city_name"), config.city_name, sizeof(config.city_name)); } if (server.hasArg("latitude")) { config.latitude = server.arg("latitude").toFloat(); } if (server.hasArg("longitude")) { config.longitude = server.arg("longitude").toFloat(); } if (server.hasArg("weather_interval")) { config.weather_interval = server.arg("weather_interval").toInt(); } if (server.hasArg("display_rotation_sec")) { config.display_rotation_sec = server.arg("display_rotation_sec").toInt(); } if (server.hasArg("display_orientation")) { config.display_orientation = server.arg("display_orientation").toInt(); display.setRotation(config.display_orientation); } 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(); } void ICACHE_FLASH_ATTR handleAPITime() { String json = "{"; json += "\"time\":\"" + timeClient.getFormattedTime() + "\","; json += "\"hours\":" + String(timeClient.getHours()) + ","; json += "\"minutes\":" + String(timeClient.getMinutes()) + ","; json += "\"seconds\":" + String(timeClient.getSeconds()) + ","; json += "\"epoch\":" + String(timeClient.getEpochTime()); json += "}"; server.send(200, "application/json", json); } void ICACHE_FLASH_ATTR handleAPIStatus() { String json = "{"; json += "\"wifi\":{"; json += "\"ssid\":\"" + String(WiFi.SSID()) + "\","; json += "\"ip\":\"" + WiFi.localIP().toString() + "\","; json += "\"rssi\":" + String(WiFi.RSSI()) + ","; json += "\"hostname\":\"" + String(config.hostname) + "\""; json += "},"; json += "\"time\":{"; json += "\"current\":\"" + timeClient.getFormattedTime() + "\","; json += "\"timezone_offset\":" + String(config.timezone_offset) + ","; json += "\"ntp_synced\":" + String(timeClient.isTimeSet() ? "true" : "false"); json += "},"; json += "\"system\":{"; json += "\"uptime\":" + String(millis() / 1000) + ","; json += "\"free_heap\":" + String(ESP.getFreeHeap()) + ","; json += "\"chip_id\":\"" + String(ESP.getChipId(), HEX) + "\""; json += "}"; json += "}"; server.send(200, "application/json", json); } void ICACHE_FLASH_ATTR handleAPIDebug() { String json = "{"; json += "\"internet_connected\":" + String(internetConnected ? "true" : "false") + ","; json += "\"ntp_attempts\":" + String(ntpAttempts) + ","; json += "\"ntp_successes\":" + String(ntpSuccesses) + ","; json += "\"last_error\":\"" + lastError + "\","; json += "\"gateway\":\"" + WiFi.gatewayIP().toString() + "\","; json += "\"dns\":\"" + WiFi.dnsIP().toString() + "\""; json += "}"; server.send(200, "application/json", json); } void ICACHE_FLASH_ATTR handleAPIWeather() { String json = "{"; json += "\"enabled\":" + String(config.weather_enabled ? "true" : "false") + ","; json += "\"valid\":" + String(weather.valid ? "true" : "false") + ","; json += "\"temperature\":" + String(weather.temperature, 1) + ","; json += "\"weathercode\":" + String(weather.weathercode) + ","; json += "\"windspeed\":" + String(weather.windspeed, 1) + ","; json += "\"last_update\":" + String(weather.lastUpdate) + ","; json += "\"sunrise\":\"" + String(sunTimes.sunrise) + "\","; json += "\"sunset\":\"" + String(sunTimes.sunset) + "\","; json += "\"sunrise_minutes\":" + String(sunTimes.sunriseMinutes) + ","; json += "\"sunset_minutes\":" + String(sunTimes.sunsetMinutes); json += "}"; server.send(200, "application/json", json); } void ICACHE_FLASH_ATTR handleAPIConfigExport() { String json = "{"; json += "\"firmware_version\":\"" FIRMWARE_VERSION "\","; json += "\"magic\":\"0x" + String(config.magic, HEX) + "\","; json += "\"ssid\":\"" + String(config.ssid) + "\","; json += "\"password\":\"" + String(config.password) + "\","; json += "\"timezone_offset\":" + String(config.timezone_offset) + ","; json += "\"dst_enabled\":" + String(config.dst_enabled ? "true" : "false") + ","; json += "\"brightness\":" + String(config.brightness) + ","; json += "\"ntp_server\":\"" + String(config.ntp_server) + "\","; json += "\"ntp_interval\":" + String(config.ntp_interval) + ","; json += "\"hour_format_24\":" + String(config.hour_format_24 ? "true" : "false") + ","; json += "\"hostname\":\"" + String(config.hostname) + "\","; json += "\"latitude\":" + String(config.latitude, 6) + ","; json += "\"longitude\":" + String(config.longitude, 6) + ","; json += "\"city_name\":\"" + String(config.city_name) + "\","; json += "\"weather_enabled\":" + String(config.weather_enabled ? "true" : "false") + ","; json += "\"weather_interval\":" + String(config.weather_interval) + ","; json += "\"display_rotation_sec\":" + String(config.display_rotation_sec) + ","; json += "\"show_weather\":" + String(config.show_weather ? "true" : "false") + ","; json += "\"show_sunrise_sunset\":" + String(config.show_sunrise_sunset ? "true" : "false"); json += "}"; server.sendHeader("Content-Disposition", "attachment; filename=clock-config.json"); server.send(200, "application/json", json); } void ICACHE_FLASH_ATTR handleAPIConfigImport() { if (!server.hasArg("plain")) { server.send(400, "text/plain", "No config data received"); return; } String body = server.arg("plain"); Serial.println("Received config: " + body); // Parse various fields (simplified parsing) int pos; pos = body.indexOf("\"ssid\":\""); if (pos >= 0) { int start = pos + 8; int end = body.indexOf("\"", start); if (end > start) { safeStringCopy(body.substring(start, end), config.ssid, sizeof(config.ssid)); } } pos = body.indexOf("\"password\":\""); if (pos >= 0) { int start = pos + 12; int end = body.indexOf("\"", start); if (end > start) { safeStringCopy(body.substring(start, end), config.password, sizeof(config.password)); } } pos = body.indexOf("\"timezone_offset\":"); if (pos >= 0) { int start = pos + 18; int end = body.indexOf(",", start); if (end < 0) end = body.indexOf("}", start); if (end > start) { config.timezone_offset = body.substring(start, end).toInt(); } } pos = body.indexOf("\"brightness\":"); if (pos >= 0) { int start = pos + 13; int end = body.indexOf(",", start); if (end < 0) end = body.indexOf("}", start); if (end > start) { config.brightness = body.substring(start, end).toInt(); } } pos = body.indexOf("\"hostname\":\""); if (pos >= 0) { int start = pos + 12; int end = body.indexOf("\"", start); if (end > start) { safeStringCopy(body.substring(start, end), config.hostname, sizeof(config.hostname)); } } pos = body.indexOf("\"city_name\":\""); if (pos >= 0) { int start = pos + 13; int end = body.indexOf("\"", start); if (end > start) { safeStringCopy(body.substring(start, end), config.city_name, sizeof(config.city_name)); } } pos = body.indexOf("\"latitude\":"); if (pos >= 0) { int start = pos + 11; int end = body.indexOf(",", start); if (end < 0) end = body.indexOf("}", start); if (end > start) { config.latitude = body.substring(start, end).toFloat(); } } pos = body.indexOf("\"longitude\":"); if (pos >= 0) { int start = pos + 12; int end = body.indexOf(",", start); if (end < 0) end = body.indexOf("}", start); if (end > start) { config.longitude = body.substring(start, end).toFloat(); } } config.magic = CONFIG_MAGIC; saveConfig(); server.send(200, "application/json", "{\"status\":\"ok\",\"message\":\"Config imported and saved. Reboot recommended.\"}"); } void ICACHE_FLASH_ATTR handleEEPROMClear() { EEPROM.begin(512); for (int i = 0; i < 512; i++) { EEPROM.write(i, 0xFF); } EEPROM.commit(); EEPROM.end(); Serial.println("EEPROM cleared!"); server.send(200, "application/json", "{\"status\":\"ok\",\"message\":\"EEPROM cleared, device will reboot\"}"); delay(1000); ESP.restart(); } void ICACHE_FLASH_ATTR handleReboot() { Serial.println("Reboot requested via web interface"); server.send(200, "application/json", "{\"status\":\"ok\",\"message\":\"Device rebooting...\"}"); delay(1000); ESP.restart(); } void ICACHE_FLASH_ATTR handleI2CScan() { String json = "{\"i2c_scan\":{\"devices\":["; int deviceCount = 0; for (uint8_t address = 0x08; address <= 0x77; address++) { Wire.beginTransmission(address); uint8_t error = Wire.endTransmission(); if (error == 0) { if (deviceCount > 0) json += ","; json += "{\"address\":\"0x"; if (address < 16) json += "0"; json += String(address, HEX); json += "\",\"decimal\":" + String(address) + "}"; deviceCount++; } delay(1); } json += "],\"count\":" + String(deviceCount); json += ",\"oled_test\":{"; Wire.beginTransmission(0x3C); json += "\"0x3C\":\"" + String(Wire.endTransmission() == 0 ? "FOUND" : "not found") + "\","; Wire.beginTransmission(0x3D); json += "\"0x3D\":\"" + String(Wire.endTransmission() == 0 ? "FOUND" : "not found") + "\""; json += "}}}"; Serial.println("I2C Scan results: " + json); server.send(200, "application/json", json); } // Setup web server void ICACHE_FLASH_ATTR setupWebServer() { httpUpdater.setup(&server, "/update", "admin", "admin"); server.on("/", HTTP_GET, handleRoot); server.on("/config", HTTP_GET, handleConfig); server.on("/config", HTTP_POST, handleConfigSave); server.on("/debug", HTTP_GET, handleDebug); server.on("/test-ntp", HTTP_GET, handleTestNTP); server.on("/test-display", HTTP_GET, handleTestDisplay); server.on("/api/time", HTTP_GET, handleAPITime); server.on("/api/status", HTTP_GET, handleAPIStatus); server.on("/api/debug", HTTP_GET, handleAPIDebug); server.on("/api/weather", HTTP_GET, handleAPIWeather); server.on("/api/config", HTTP_GET, handleAPIConfigExport); server.on("/api/config", HTTP_POST, handleAPIConfigImport); server.on("/api/eeprom-clear", HTTP_POST, handleEEPROMClear); server.on("/api/reboot", HTTP_POST, handleReboot); server.on("/api/i2c-scan", HTTP_GET, handleI2CScan); server.begin(); Serial.println("Web server started"); if (MDNS.begin(config.hostname)) { Serial.printf("mDNS responder started: %s.local\n", config.hostname); MDNS.addService("http", "tcp", 80); } }