From b3a80c162adbb99e1268c7a2457d013aef0720ad Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 2 Apr 2026 20:19:19 +0200 Subject: [PATCH] Review changes --- lib/Weather/WeatherClient.cpp | 56 ++++- lib/Weather/WeatherClient.h | 1 + lib/Weather/WeatherData.h | 1 + lib/Weather/WeatherIcons.cpp | 238 +----------------- lib/Weather/WeatherIcons.h | 6 +- lib/Weather/WeatherSettingsStore.cpp | 10 + lib/Weather/WeatherSettingsStore.h | 4 +- scripts/convert_icon.py | 58 +---- scripts/generate_weather_icons.py | 54 +--- scripts/svg_utils.py | 53 ++++ src/activities/home/HomeActivity.cpp | 2 +- src/activities/weather/WeatherActivity.cpp | 73 +++--- src/activities/weather/WeatherActivity.h | 2 + .../weather/WeatherSettingsActivity.cpp | 32 +-- .../weather/WeatherSettingsActivity.h | 2 +- src/network/HttpDownloader.cpp | 30 --- 16 files changed, 176 insertions(+), 446 deletions(-) create mode 100644 scripts/svg_utils.py diff --git a/lib/Weather/WeatherClient.cpp b/lib/Weather/WeatherClient.cpp index da2fb4dc..68ad4e56 100644 --- a/lib/Weather/WeatherClient.cpp +++ b/lib/Weather/WeatherClient.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "../../src/network/HttpDownloader.h" @@ -12,9 +13,7 @@ namespace { constexpr char WEATHER_CACHE_FILE[] = "/.crosspoint/weather_cache.json"; std::string buildForecastUrl(const WeatherSettingsStore& settings) { - // Open-Meteo supports plain HTTP. Using HTTP here avoids TLS handshake - // instability observed on some ESP32-C3 builds. - std::string url = "http://api.open-meteo.com/v1/forecast?"; + std::string url = "https://api.open-meteo.com/v1/forecast?"; url += "latitude=" + std::to_string(settings.getLatitude()); url += "&longitude=" + std::to_string(settings.getLongitude()); url += @@ -33,8 +32,33 @@ std::string buildForecastUrl(const WeatherSettingsStore& settings) { url += "&forecast_hours=48"; return url; } + +std::string buildRequestSignature(const WeatherSettingsStore& settings) { + char latitude[24]; + char longitude[24]; + snprintf(latitude, sizeof(latitude), "%.6f", settings.getLatitude()); + snprintf(longitude, sizeof(longitude), "%.6f", settings.getLongitude()); + + std::string signature = "lat="; + signature += latitude; + signature += "|lon="; + signature += longitude; + signature += "|temp="; + signature += settings.getTempUnitParam(); + signature += "|wind="; + signature += settings.getWindUnitParam(); + signature += "|precip="; + signature += settings.getPrecipUnitParam(); + signature += "|days="; + signature += std::to_string(settings.getForecastDays()); + return signature; +} } // namespace +std::string WeatherClient::buildRequestSignature(const WeatherSettingsStore& settings) { + return ::buildRequestSignature(settings); +} + WeatherData WeatherClient::getWeather(const WeatherSettingsStore& settings, bool forceRefresh) { if (!settings.hasLocation()) { WeatherData data; @@ -42,19 +66,26 @@ WeatherData WeatherClient::getWeather(const WeatherSettingsStore& settings, bool return data; } + const std::string requestSignature = buildRequestSignature(settings); + if (!forceRefresh) { WeatherData cached; if (loadCache(cached) && cached.valid) { - time_t now; - time(&now); - if (now - cached.fetchedAt < CACHE_TTL_SECONDS) { - LOG_DBG("WEA", "Using cached weather data (age: %ld s)", (long)(now - cached.fetchedAt)); + if (cached.requestSignature != requestSignature) { + LOG_DBG("WEA", "Ignoring cache with mismatched request signature"); + Storage.remove(WEATHER_CACHE_FILE); + } else { + time_t now; + time(&now); + if (now - cached.fetchedAt < CACHE_TTL_SECONDS) { + LOG_DBG("WEA", "Using cached weather data (age: %ld s)", (long)(now - cached.fetchedAt)); + return cached; + } + LOG_DBG("WEA", "Cache expired (age: %ld s)", (long)(now - cached.fetchedAt)); + // Cache-first behavior: return stale cache and let the caller decide + // whether/when to perform a network refresh. return cached; } - LOG_DBG("WEA", "Cache expired (age: %ld s)", (long)(now - cached.fetchedAt)); - // Cache-first behavior: return stale cache and let the caller decide - // whether/when to perform a network refresh. - return cached; } LOG_DBG("WEA", "No cache available; caller should establish network and force refresh"); @@ -68,6 +99,7 @@ WeatherData WeatherClient::getWeather(const WeatherSettingsStore& settings, bool WeatherData WeatherClient::fetchFromApi(const WeatherSettingsStore& settings) { WeatherData data; + data.requestSignature = buildRequestSignature(settings); std::string url = buildForecastUrl(settings); LOG_DBG("WEA", "fetchFromApi[1] start"); LOG_DBG("WEA", "fetchFromApi[2] url length=%zu", url.size()); @@ -188,6 +220,7 @@ bool WeatherClient::saveCache(const WeatherData& data) { Storage.mkdir("/.crosspoint"); JsonDocument doc; + doc["requestSignature"] = data.requestSignature; doc["fetchedAt"] = data.fetchedAt; doc["timezone"] = data.timezone; doc["utcOffsetSeconds"] = data.utcOffsetSeconds; @@ -253,6 +286,7 @@ bool WeatherClient::loadCache(WeatherData& data) { } data.fetchedAt = doc["fetchedAt"] | (time_t)0; + data.requestSignature = doc["requestSignature"] | std::string(""); data.timezone = doc["timezone"] | std::string(""); data.utcOffsetSeconds = doc["utcOffsetSeconds"] | 0; diff --git a/lib/Weather/WeatherClient.h b/lib/Weather/WeatherClient.h index f2ae6e19..a0de4818 100644 --- a/lib/Weather/WeatherClient.h +++ b/lib/Weather/WeatherClient.h @@ -17,6 +17,7 @@ class WeatherClient { private: static WeatherData fetchFromApi(const WeatherSettingsStore& settings); static bool parseWeatherJson(const std::string& json, WeatherData& data); + static std::string buildRequestSignature(const WeatherSettingsStore& settings); static bool saveCache(const WeatherData& data); static bool loadCache(WeatherData& data); diff --git a/lib/Weather/WeatherData.h b/lib/Weather/WeatherData.h index b593865c..1410f9d7 100644 --- a/lib/Weather/WeatherData.h +++ b/lib/Weather/WeatherData.h @@ -40,6 +40,7 @@ struct WeatherData { CurrentWeather current; std::vector daily; std::vector hourly; + std::string requestSignature; std::string timezone; int utcOffsetSeconds = 0; time_t fetchedAt = 0; diff --git a/lib/Weather/WeatherIcons.cpp b/lib/Weather/WeatherIcons.cpp index 64236d7d..ad652b71 100644 --- a/lib/Weather/WeatherIcons.cpp +++ b/lib/Weather/WeatherIcons.cpp @@ -4,180 +4,7 @@ // Weather icon glyph source attribution: // https://github.com/erikflowers/weather-icons -// 64x64 monochrome icon bitmaps in this file are adapted from that icon set. - -// ============================================================================ -// 64x64 Large Weather Icons (1-bit, MSB-first, row-major) -// Each row = 64 pixels = 8 bytes. Total = 64 * 8 = 512 bytes per icon. -// Pixel ON (1) = white/clear, Pixel OFF (0) = black/drawn. -// These are "inverted" bitmaps - 0 bits are drawn as black pixels. -// ============================================================================ - -// Clear Day - Sun with rays -// clang-format off -static const uint8_t ICON_CLEAR_DAY_48[] = { - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, - 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, - 0xFF,0x8F,0xFE,0x7F,0xF1,0xFF, 0xFF,0x1F,0xFE,0x7F,0xF8,0xFF, 0xFF,0x3F,0x80,0x01,0xFC,0xFF, - 0xFF,0x7F,0x00,0x00,0xFE,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF, - 0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, - 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xE0,0xC0,0x00,0x00,0x03,0x07, - 0xE0,0x80,0x00,0x00,0x01,0x07, 0xF0,0x80,0x00,0x00,0x01,0x0F, 0xF8,0x80,0x00,0x00,0x01,0x1F, - 0xFC,0x80,0x00,0x00,0x01,0x3F, 0xFC,0x80,0x00,0x00,0x01,0x3F, 0xF8,0x80,0x00,0x00,0x01,0x1F, - 0xF0,0x80,0x00,0x00,0x01,0x0F, 0xE0,0x80,0x00,0x00,0x01,0x07, 0xE0,0xC0,0x00,0x00,0x03,0x07, - 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, - 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF, - 0xFF,0xFC,0x00,0x00,0x3F,0xFF, 0xFF,0x7F,0x00,0x00,0xFE,0xFF, 0xFF,0x3F,0x80,0x01,0xFC,0xFF, - 0xFF,0x1F,0xFE,0x7F,0xF8,0xFF, 0xFF,0x8F,0xFE,0x7F,0xF1,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, - 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, - 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, -}; - -// Clear Night - Moon crescent -static const uint8_t ICON_CLEAR_NIGHT_48[] = { - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFC,0x0F,0xFF,0xFF, 0xFF,0xFF,0xF0,0x03,0xFF,0xFF, - 0xFF,0xFF,0xE0,0x01,0xFF,0xFF, 0xFF,0xFF,0xC0,0x00,0xFF,0xFF, 0xFF,0xFF,0x80,0x00,0x7F,0xFF, - 0xFF,0xFF,0x00,0x00,0x3F,0xFF, 0xFF,0xFF,0x00,0x00,0x3F,0xFF, 0xFF,0xFE,0x00,0x00,0x1F,0xFF, - 0xFF,0xFE,0x00,0x00,0x3F,0xFF, 0xFF,0xFC,0x00,0x00,0x7F,0xFF, 0xFF,0xFC,0x00,0x00,0xFF,0xFF, - 0xFF,0xFC,0x00,0x01,0xFF,0xFF, 0xFF,0xF8,0x00,0x03,0xFF,0xFF, 0xFF,0xF8,0x00,0x07,0xFF,0xFF, - 0xFF,0xF8,0x00,0x07,0xFF,0xFF, 0xFF,0xF8,0x00,0x0F,0xFF,0xFF, 0xFF,0xF8,0x00,0x0F,0xFF,0xFF, - 0xFF,0xF8,0x00,0x0F,0xFF,0xFF, 0xFF,0xF8,0x00,0x0F,0xFF,0xFF, 0xFF,0xF8,0x00,0x0F,0xFF,0xFF, - 0xFF,0xF8,0x00,0x0F,0xFF,0xFF, 0xFF,0xF8,0x00,0x07,0xFF,0xFF, 0xFF,0xF8,0x00,0x07,0xFF,0xFF, - 0xFF,0xF8,0x00,0x03,0xFF,0xFF, 0xFF,0xFC,0x00,0x01,0xFF,0xFF, 0xFF,0xFC,0x00,0x00,0xFF,0xFF, - 0xFF,0xFC,0x00,0x00,0x7F,0xFF, 0xFF,0xFE,0x00,0x00,0x3F,0xFF, 0xFF,0xFE,0x00,0x00,0x1F,0xFF, - 0xFF,0xFF,0x00,0x00,0x1F,0xFF, 0xFF,0xFF,0x00,0x00,0x3F,0xFF, 0xFF,0xFF,0x80,0x00,0x7F,0xFF, - 0xFF,0xFF,0xC0,0x00,0xFF,0xFF, 0xFF,0xFF,0xE0,0x01,0xFF,0xFF, 0xFF,0xFF,0xF0,0x03,0xFF,0xFF, - 0xFF,0xFF,0xFC,0x0F,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, -}; - -// Partly Cloudy Day - Sun behind cloud -static const uint8_t ICON_PARTLY_CLOUDY_DAY_48[] = { - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, - 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0xFF,0xFE,0x7F,0xFF,0xFF, 0xFF,0x9F,0xFE,0x7F,0xF9,0xFF, - 0xFF,0x3F,0xFE,0x7F,0xFC,0xFF, 0xFF,0x7F,0x80,0x01,0xFE,0xFF, 0xFF,0xFF,0x00,0x00,0xFF,0xFF, - 0xFF,0xFC,0x00,0x00,0x3F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF, 0xFF,0xF0,0x00,0x00,0x0F,0xFF, - 0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xE0,0xE0,0x00,0x00,0x07,0x07, 0xF0,0xE0,0x00,0x00,0x07,0x0F, - 0xF8,0xE0,0x00,0x00,0x07,0x1F, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xF0,0x00,0x00,0x0F,0xFF, - 0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF, - 0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF, 0xFF,0xF0,0x00,0x00,0x0F,0xFF, - 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF, - 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFE,0x00,0x00,0x00,0x00,0x7F, - 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, - 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, - 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFF,0x00,0x00,0x00,0x00,0xFF, - 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0xF0,0x00,0x00,0x0F,0xFF, - 0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, -}; - -// Partly Cloudy Night - Moon behind cloud (reuse partly cloudy day for simplicity) -static const uint8_t* ICON_PARTLY_CLOUDY_NIGHT_48 = ICON_PARTLY_CLOUDY_DAY_48; - -// Overcast - Full cloud -static const uint8_t ICON_OVERCAST_48[] = { - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFC,0x3F,0xFF,0xFF, - 0xFF,0xFF,0xF0,0x0F,0xFF,0xFF, 0xFF,0xFF,0xE0,0x07,0xFF,0xFF, 0xFF,0xFF,0xC0,0x03,0xFF,0xFF, - 0xFF,0xFF,0x80,0x01,0xFF,0xFF, 0xFF,0xFF,0x00,0x00,0xFF,0xFF, 0xFF,0xFF,0x00,0x00,0xFF,0xFF, - 0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF, - 0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, - 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFE,0x00,0x00,0x00,0x00,0x7F, - 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, - 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, - 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, - 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFF,0x00,0x00,0x00,0x00,0xFF, - 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0xF0,0x00,0x00,0x0F,0xFF, - 0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, -}; - -// Rain - Cloud with rain drops -static const uint8_t ICON_RAIN_48[] = { - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFC,0x3F,0xFF,0xFF, 0xFF,0xFF,0xF0,0x0F,0xFF,0xFF, - 0xFF,0xFF,0xE0,0x07,0xFF,0xFF, 0xFF,0xFF,0xC0,0x03,0xFF,0xFF, 0xFF,0xFF,0x80,0x01,0xFF,0xFF, - 0xFF,0xFF,0x00,0x00,0xFF,0xFF, 0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF, - 0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, - 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFE,0x00,0x00,0x00,0x00,0x7F, - 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, - 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFF,0x00,0x00,0x00,0x00,0xFF, - 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFB,0xDF,0xBF,0x7F,0xFF, - 0xFF,0xF3,0xCF,0x9F,0x3F,0xFF, 0xFF,0xF3,0xCF,0x9F,0x3F,0xFF, 0xFF,0xE7,0xE7,0xCF,0x9F,0xFF, - 0xFF,0xE7,0xE7,0xCF,0x9F,0xFF, 0xFF,0xCF,0xF3,0xE7,0xCF,0xFF, 0xFF,0xCF,0xF3,0xE7,0xCF,0xFF, - 0xFF,0xCF,0xF3,0xE7,0xCF,0xFF, 0xFF,0xDF,0xFB,0xF7,0xEF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, -}; - -// Snow - Cloud with snowflakes -static const uint8_t ICON_SNOW_48[] = { - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFC,0x3F,0xFF,0xFF, 0xFF,0xFF,0xF0,0x0F,0xFF,0xFF, - 0xFF,0xFF,0xE0,0x07,0xFF,0xFF, 0xFF,0xFF,0xC0,0x03,0xFF,0xFF, 0xFF,0xFF,0x80,0x01,0xFF,0xFF, - 0xFF,0xFF,0x00,0x00,0xFF,0xFF, 0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF, - 0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, - 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFE,0x00,0x00,0x00,0x00,0x7F, - 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, - 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFF,0x00,0x00,0x00,0x00,0xFF, - 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xF9,0xE7,0xCF,0x3F,0xFF, - 0xFF,0xFC,0xF3,0xE7,0x9F,0xFF, 0xFF,0xFE,0x79,0xF3,0xCF,0xFF, 0xFF,0xFF,0x3C,0xF9,0xE7,0xFF, - 0xFF,0xFE,0x79,0xF3,0xCF,0xFF, 0xFF,0xFC,0xF3,0xE7,0x9F,0xFF, 0xFF,0xF9,0xE7,0xCF,0x3F,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, -}; - -// Thunderstorm - Cloud with lightning bolt -static const uint8_t ICON_THUNDERSTORM_48[] = { - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFC,0x3F,0xFF,0xFF, 0xFF,0xFF,0xF0,0x0F,0xFF,0xFF, - 0xFF,0xFF,0xE0,0x07,0xFF,0xFF, 0xFF,0xFF,0xC0,0x03,0xFF,0xFF, 0xFF,0xFF,0x80,0x01,0xFF,0xFF, - 0xFF,0xFF,0x00,0x00,0xFF,0xFF, 0xFF,0xFE,0x00,0x00,0x7F,0xFF, 0xFF,0xF8,0x00,0x00,0x1F,0xFF, - 0xFF,0xF0,0x00,0x00,0x0F,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, - 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFE,0x00,0x00,0x00,0x00,0x7F, - 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFC,0x00,0x00,0x00,0x00,0x3F, - 0xFC,0x00,0x00,0x00,0x00,0x3F, 0xFE,0x00,0x00,0x00,0x00,0x7F, 0xFF,0x00,0x00,0x00,0x00,0xFF, - 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xE0,0x00,0x00,0x07,0xFF, 0xFF,0xFC,0x00,0x00,0x3F,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xF8,0xFF,0xFF,0xFF, 0xFF,0xFF,0xF1,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xE3,0xFF,0xFF,0xFF, 0xFF,0xFF,0xC7,0xFF,0xFF,0xFF, 0xFF,0xFF,0x80,0x3F,0xFF,0xFF, - 0xFF,0xFF,0x00,0x7F,0xFF,0xFF, 0xFF,0xFF,0xF0,0xFF,0xFF,0xFF, 0xFF,0xFF,0xE1,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xC3,0xFF,0xFF,0xFF, 0xFF,0xFF,0x87,0xFF,0xFF,0xFF, 0xFF,0xFF,0x0F,0xFF,0xFF,0xFF, - 0xFF,0xFF,0x1F,0xFF,0xFF,0xFF, 0xFF,0xFF,0x3F,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, -}; - -// Fog - Horizontal lines -static const uint8_t ICON_FOG_48[] = { - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, - 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF, - 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF, - 0xFF,0xC0,0x00,0x00,0x03,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0x80,0x00,0x00,0x01,0xFF, 0xFF,0x00,0x00,0x00,0x00,0xFF, 0xFF,0x80,0x00,0x00,0x01,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, -}; - -// clang-format on +// Large icons are provided by generated WI48_* arrays in WeatherIcons48.h. // ============================================================================ // 24x24 Small Weather Icons (1-bit, MSB-first) @@ -332,69 +159,6 @@ const uint8_t* getWeatherIconSmall(WeatherIconType type) { } #endif // WEATHER_ENABLE_SMALL_ICONS -const char* getWeatherDescription(int wmoCode) { - switch (wmoCode) { - case 0: - return "Clear sky"; - case 1: - return "Mainly clear"; - case 2: - return "Partly cloudy"; - case 3: - return "Overcast"; - case 45: - return "Fog"; - case 48: - return "Rime fog"; - case 51: - return "Light drizzle"; - case 53: - return "Drizzle"; - case 55: - return "Dense drizzle"; - case 56: - return "Freezing drizzle"; - case 57: - return "Dense freezing drizzle"; - case 61: - return "Slight rain"; - case 63: - return "Moderate rain"; - case 65: - return "Heavy rain"; - case 66: - return "Freezing rain"; - case 67: - return "Heavy freezing rain"; - case 71: - return "Slight snow"; - case 73: - return "Moderate snow"; - case 75: - return "Heavy snow"; - case 77: - return "Snow grains"; - case 80: - return "Slight showers"; - case 81: - return "Moderate showers"; - case 82: - return "Violent showers"; - case 85: - return "Snow showers"; - case 86: - return "Heavy snow showers"; - case 95: - return "Thunderstorm"; - case 96: - return "Thunderstorm, hail"; - case 99: - return "Thunderstorm, heavy hail"; - default: - return "Unknown"; - } -} - const char* getWindDirectionText(int degrees) { // Normalize to 0-360 degrees = ((degrees % 360) + 360) % 360; diff --git a/lib/Weather/WeatherIcons.h b/lib/Weather/WeatherIcons.h index 5e29f2e5..96cd3c7d 100644 --- a/lib/Weather/WeatherIcons.h +++ b/lib/Weather/WeatherIcons.h @@ -60,9 +60,10 @@ inline WeatherIconType getWeatherIconType(int wmoCode, bool isDay) { case 80: case 81: case 82: + return WeatherIconType::SHOWERS; case 85: case 86: - return WeatherIconType::SHOWERS; + return WeatherIconType::SNOW; case 95: case 96: case 99: @@ -72,9 +73,6 @@ inline WeatherIconType getWeatherIconType(int wmoCode, bool isDay) { } } -// Get human-readable description for WMO weather code -const char* getWeatherDescription(int wmoCode); - // Get the appropriate large icon bitmap (64x64, 1-bit, MSB first) const uint8_t* getWeatherIconLarge(WeatherIconType type); diff --git a/lib/Weather/WeatherSettingsStore.cpp b/lib/Weather/WeatherSettingsStore.cpp index 52267936..4a11c257 100644 --- a/lib/Weather/WeatherSettingsStore.cpp +++ b/lib/Weather/WeatherSettingsStore.cpp @@ -16,6 +16,7 @@ bool WeatherSettingsStore::saveToFile() const { JsonDocument doc; doc["latitude"] = latitude; doc["longitude"] = longitude; + doc["locationConfigured"] = locationConfigured; doc["locationName"] = locationName; doc["tempUnit"] = static_cast(tempUnit); doc["windUnit"] = static_cast(windUnit); @@ -47,6 +48,7 @@ bool WeatherSettingsStore::loadFromFile() { latitude = doc["latitude"] | 0.0f; longitude = doc["longitude"] | 0.0f; + locationConfigured = doc["locationConfigured"] | (latitude != 0.0f || longitude != 0.0f); locationName = doc["locationName"] | std::string(""); tempUnit = static_cast(doc["tempUnit"] | (uint8_t)0); windUnit = static_cast(doc["windUnit"] | (uint8_t)0); @@ -63,10 +65,18 @@ bool WeatherSettingsStore::loadFromFile() { void WeatherSettingsStore::setLocation(float lat, float lon, const std::string& name) { latitude = lat; longitude = lon; + locationConfigured = true; locationName = name; LOG_DBG("WEA", "Set location: %s (%.4f, %.4f)", name.c_str(), lat, lon); } +void WeatherSettingsStore::clearLocation() { + latitude = 0; + longitude = 0; + locationConfigured = false; + locationName.clear(); +} + void WeatherSettingsStore::setForecastDays(uint8_t days) { if (days < 1) days = 1; if (days > 5) days = 5; diff --git a/lib/Weather/WeatherSettingsStore.h b/lib/Weather/WeatherSettingsStore.h index 50eacfed..95938683 100644 --- a/lib/Weather/WeatherSettingsStore.h +++ b/lib/Weather/WeatherSettingsStore.h @@ -12,6 +12,7 @@ class WeatherSettingsStore { float latitude = 0; float longitude = 0; + bool locationConfigured = false; std::string locationName; WeatherTempUnit tempUnit = WeatherTempUnit::CELSIUS; WeatherWindUnit windUnit = WeatherWindUnit::KMH; @@ -31,10 +32,11 @@ class WeatherSettingsStore { // Location void setLocation(float lat, float lon, const std::string& name); + void clearLocation(); float getLatitude() const { return latitude; } float getLongitude() const { return longitude; } const std::string& getLocationName() const { return locationName; } - bool hasLocation() const { return latitude != 0 || longitude != 0; } + bool hasLocation() const { return locationConfigured; } // Units void setTempUnit(WeatherTempUnit unit) { tempUnit = unit; } diff --git a/scripts/convert_icon.py b/scripts/convert_icon.py index 81585636..980aa72f 100644 --- a/scripts/convert_icon.py +++ b/scripts/convert_icon.py @@ -3,63 +3,11 @@ import os from PIL import Image import cairosvg import io -import xml.etree.ElementTree as ET + +from svg_utils import fit_inside_canvas, parse_svg_intrinsic_size threshold = 128 - -def parse_svg_intrinsic_size(svg_data): - try: - root = ET.fromstring(svg_data) - except ET.ParseError: - return None, None - - viewbox = root.get("viewBox") or root.get("viewbox") - if viewbox: - parts = viewbox.replace(",", " ").split() - if len(parts) == 4: - try: - vb_w = float(parts[2]) - vb_h = float(parts[3]) - if vb_w > 0 and vb_h > 0: - return vb_w, vb_h - except ValueError: - pass - - def parse_len(value): - if not value: - return None - cleaned = "".join(ch for ch in value if ch.isdigit() or ch in ".-") - if not cleaned: - return None - try: - parsed = float(cleaned) - return parsed if parsed > 0 else None - except ValueError: - return None - - w = parse_len(root.get("width")) - h = parse_len(root.get("height")) - return w, h - - -def fit_inside_canvas(src_w, src_h, dst_w, dst_h): - if src_w <= 0 or src_h <= 0 or dst_w <= 0 or dst_h <= 0: - return dst_w, dst_h - - src_ratio = src_w / src_h - dst_ratio = dst_w / dst_h - - if src_ratio >= dst_ratio: - fit_w = dst_w - fit_h = max(1, int(round(fit_w / src_ratio))) - else: - fit_h = dst_h - fit_w = max(1, int(round(fit_h * src_ratio))) - - return fit_w, fit_h - - def svg_to_png_bytes(svg_path, width, height): with open(svg_path, "rb") as f: svg_data = f.read() @@ -124,7 +72,7 @@ def image_to_c_array(img, array_name): byte |= bit << (7 - b) packed.append(byte) # Format as C array - c = f"#pragma once\n#include \n\n" + c = "#pragma once\n#include \n\n" c += f"// size: {width}x{height}\n" c += f"static const uint8_t {array_name}[] = {{\n " for i, v in enumerate(packed): diff --git a/scripts/generate_weather_icons.py b/scripts/generate_weather_icons.py index 51a9b292..02bb532b 100644 --- a/scripts/generate_weather_icons.py +++ b/scripts/generate_weather_icons.py @@ -14,14 +14,15 @@ import shutil import subprocess import tempfile import urllib.request -import xml.etree.ElementTree as ET import zipfile from PIL import Image +from svg_utils import fit_inside_canvas, parse_svg_intrinsic_size + try: import cairosvg # type: ignore -except Exception: +except ImportError: cairosvg = None @@ -52,55 +53,6 @@ ICON_SOURCES = { "WI48_THUNDERSTORM": "wi-thunderstorm.svg", } - -def parse_svg_intrinsic_size(svg_data): - try: - root = ET.fromstring(svg_data) - except ET.ParseError: - return None, None - - viewbox = root.get("viewBox") or root.get("viewbox") - if viewbox: - parts = viewbox.replace(",", " ").split() - if len(parts) == 4: - try: - vb_w = float(parts[2]) - vb_h = float(parts[3]) - if vb_w > 0 and vb_h > 0: - return vb_w, vb_h - except ValueError: - pass - - def parse_len(value): - if not value: - return None - cleaned = "".join(ch for ch in value if ch.isdigit() or ch in ".-") - if not cleaned: - return None - try: - parsed = float(cleaned) - return parsed if parsed > 0 else None - except ValueError: - return None - - return parse_len(root.get("width")), parse_len(root.get("height")) - - -def fit_inside_canvas(src_w, src_h, dst_w, dst_h): - if src_w <= 0 or src_h <= 0: - return dst_w, dst_h - - src_ratio = src_w / src_h - dst_ratio = dst_w / dst_h - if src_ratio >= dst_ratio: - fit_w = dst_w - fit_h = max(1, int(round(fit_w / src_ratio))) - else: - fit_h = dst_h - fit_w = max(1, int(round(fit_h * src_ratio))) - return fit_w, fit_h - - def ensure_resvg_binary(): if shutil.which("resvg"): return Path(shutil.which("resvg")) diff --git a/scripts/svg_utils.py b/scripts/svg_utils.py new file mode 100644 index 00000000..309beb84 --- /dev/null +++ b/scripts/svg_utils.py @@ -0,0 +1,53 @@ +import xml.etree.ElementTree as ET + + +def parse_svg_intrinsic_size(svg_data): + try: + root = ET.fromstring(svg_data) + except ET.ParseError: + return None, None + + viewbox = root.get("viewBox") or root.get("viewbox") + if viewbox: + parts = viewbox.replace(",", " ").split() + if len(parts) == 4: + try: + vb_w = float(parts[2]) + vb_h = float(parts[3]) + if vb_w > 0 and vb_h > 0: + return vb_w, vb_h + except ValueError: + pass + + def parse_len(value): + if not value: + return None + cleaned = "".join(ch for ch in value if ch.isdigit() or ch in ".-") + if not cleaned: + return None + try: + parsed = float(cleaned) + return parsed if parsed > 0 else None + except ValueError: + return None + + w = parse_len(root.get("width")) + h = parse_len(root.get("height")) + return w, h + + +def fit_inside_canvas(src_w, src_h, dst_w, dst_h): + if src_w <= 0 or src_h <= 0 or dst_w <= 0 or dst_h <= 0: + return dst_w, dst_h + + src_ratio = src_w / src_h + dst_ratio = dst_w / dst_h + + if src_ratio >= dst_ratio: + fit_w = dst_w + fit_h = max(1, round(fit_w / src_ratio)) + else: + fit_h = dst_h + fit_w = max(1, round(fit_h * src_ratio)) + + return fit_w, fit_h diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index d314ecba..ec22cca4 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -230,7 +230,7 @@ void HomeActivity::render(RenderLock&&) { // Build menu items dynamically std::vector menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER), tr(STR_WEATHER), tr(STR_SETTINGS_TITLE)}; - std::vector menuIcons = {Folder, Recent, Library, Transfer, Settings}; + std::vector menuIcons = {Folder, Recent, Transfer, Library, Settings}; if (hasOpdsUrl) { // Insert OPDS Browser after Recents (before File Transfer) diff --git a/src/activities/weather/WeatherActivity.cpp b/src/activities/weather/WeatherActivity.cpp index 9b537ec7..b9ee5806 100644 --- a/src/activities/weather/WeatherActivity.cpp +++ b/src/activities/weather/WeatherActivity.cpp @@ -232,6 +232,27 @@ void WeatherActivity::fetchWeather() { requestUpdate(); } +void WeatherActivity::openSettingsActivity() { + renderer.setOrientation(GfxRenderer::Orientation::Portrait); + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult&) { + renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise); + forceRefresh = true; + loadAndDisplay(); + }); +} + +void WeatherActivity::triggerRefresh(const bool showPopup) { + showRefreshPopup = showPopup; + if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { + state = State::FETCHING; + requestUpdate(true); + fetchWeather(); + } else { + launchWifiSelection(); + } +} + void WeatherActivity::loop() { if (state == State::WIFI_SELECTION) { return; // Handled by WifiSelectionActivity @@ -239,24 +260,10 @@ void WeatherActivity::loop() { if (state == State::ERROR) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - // Open weather settings (useful when no location is configured) - renderer.setOrientation(GfxRenderer::Orientation::Portrait); - startActivityForResult(std::make_unique(renderer, mappedInput), - [this](const ActivityResult&) { - renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise); - forceRefresh = true; - loadAndDisplay(); - }); + openSettingsActivity(); } else if (mappedInput.wasReleased(MappedInputManager::Button::Left) || mappedInput.wasReleased(MappedInputManager::Button::Right)) { - // Retry fetch - if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { - state = State::FETCHING; - requestUpdate(true); - fetchWeather(); - } else { - launchWifiSelection(); - } + triggerRefresh(false); } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onGoHome(); } @@ -299,27 +306,10 @@ void WeatherActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onGoHome(); } else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - // Open weather settings - renderer.setOrientation(GfxRenderer::Orientation::Portrait); - startActivityForResult(std::make_unique(renderer, mappedInput), - [this](const ActivityResult&) { - // Re-apply landscape after returning from settings - renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise); - // Refresh after settings change - forceRefresh = true; - loadAndDisplay(); - }); + openSettingsActivity(); } else if (mappedInput.wasReleased(MappedInputManager::Button::Left) || mappedInput.wasReleased(MappedInputManager::Button::Right)) { - // Manual refresh with popup feedback. - showRefreshPopup = true; - if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { - state = State::FETCHING; - requestUpdate(true); - fetchWeather(); - } else { - launchWifiSelection(); - } + triggerRefresh(true); } } @@ -387,9 +377,9 @@ void WeatherActivity::render(RenderLock&&) { if (state == State::FETCHING && showRefreshPopup) { GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); + } else { + renderer.displayBuffer(); } - - renderer.displayBuffer(); } void WeatherActivity::renderCurrentConditions(int x, int y, int w, int h) { @@ -490,6 +480,11 @@ void WeatherActivity::renderDailyForecast(int x, int y, int w, int h) { const StrId dayNameIds[] = {StrId::STR_WEATHER_DAY_SUN, StrId::STR_WEATHER_DAY_MON, StrId::STR_WEATHER_DAY_TUE, StrId::STR_WEATHER_DAY_WED, StrId::STR_WEATHER_DAY_THU, StrId::STR_WEATHER_DAY_FRI, StrId::STR_WEATHER_DAY_SAT}; + const StrId monthNameIds[] = { + StrId::STR_WEATHER_MONTH_JAN, StrId::STR_WEATHER_MONTH_FEB, StrId::STR_WEATHER_MONTH_MAR, + StrId::STR_WEATHER_MONTH_APR, StrId::STR_WEATHER_MONTH_MAY, StrId::STR_WEATHER_MONTH_JUN, + StrId::STR_WEATHER_MONTH_JUL, StrId::STR_WEATHER_MONTH_AUG, StrId::STR_WEATHER_MONTH_SEP, + StrId::STR_WEATHER_MONTH_OCT, StrId::STR_WEATHER_MONTH_NOV, StrId::STR_WEATHER_MONTH_DEC}; const char* unitSuffix = WEATHER_SETTINGS.getTempUnit() == WeatherTempUnit::CELSIUS ? "C" : "F"; int numDays = static_cast(weatherData.daily.size()); @@ -527,8 +522,8 @@ void WeatherActivity::renderDailyForecast(int x, int y, int w, int h) { // Date (e.g. "Apr 3") - for all days char dateBuf[16]; - const char* monthNames[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; - snprintf(dateBuf, sizeof(dateBuf), "%s %d", monthNames[timeinfo.tm_mon], timeinfo.tm_mday); + const char* monthName = I18N.get(monthNameIds[timeinfo.tm_mon]); + snprintf(dateBuf, sizeof(dateBuf), "%s %d", monthName, timeinfo.tm_mday); int dateWidth = renderer.getTextWidth(SMALL_FONT_ID, dateBuf); renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - dateWidth) / 2, textY, dateBuf); textY += 18; diff --git a/src/activities/weather/WeatherActivity.h b/src/activities/weather/WeatherActivity.h index e28f51e1..142026b8 100644 --- a/src/activities/weather/WeatherActivity.h +++ b/src/activities/weather/WeatherActivity.h @@ -43,6 +43,8 @@ class WeatherActivity final : public Activity { void launchWifiSelection(); void onWifiSelectionComplete(bool connected); void fetchWeather(); + void openSettingsActivity(); + void triggerRefresh(bool showPopup); // Render sub-sections (landscape 800x480) void renderCurrentConditions(int x, int y, int w, int h); diff --git a/src/activities/weather/WeatherSettingsActivity.cpp b/src/activities/weather/WeatherSettingsActivity.cpp index 01491992..f2b7d19a 100644 --- a/src/activities/weather/WeatherSettingsActivity.cpp +++ b/src/activities/weather/WeatherSettingsActivity.cpp @@ -125,21 +125,13 @@ void WeatherSettingsActivity::launchCitySearch() { [this, query = kb.text](const ActivityResult& wifiResult) { if (wifiResult.isCancelled) return; searchResults = WeatherClient::searchCity(query); - if (searchResults.empty()) { - requestUpdate(); - return; - } - showingSearchResults = true; + showingSearchResults = !searchResults.empty(); selectedIndex = 0; requestUpdate(); }); } else { searchResults = WeatherClient::searchCity(kb.text); - if (searchResults.empty()) { - requestUpdate(); - return; - } - showingSearchResults = true; + showingSearchResults = !searchResults.empty(); selectedIndex = 0; requestUpdate(); } @@ -153,10 +145,14 @@ void WeatherSettingsActivity::launchLatitudeEntry() { [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); - float lat = strtof(kb.text.c_str(), nullptr); - if (lat >= -90.0f && lat <= 90.0f) { - WEATHER_SETTINGS.setLocation(lat, WEATHER_SETTINGS.getLongitude(), WEATHER_SETTINGS.getLocationName()); + char* end = nullptr; + const float lat = strtof(kb.text.c_str(), &end); + if (end != kb.text.c_str() && *end == '\0' && lat >= -90.0f && lat <= 90.0f) { + if (lat != WEATHER_SETTINGS.getLatitude()) { + WEATHER_SETTINGS.setLocation(lat, WEATHER_SETTINGS.getLongitude(), ""); + } WEATHER_SETTINGS.saveToFile(); + requestUpdate(); } } }); @@ -169,10 +165,14 @@ void WeatherSettingsActivity::launchLongitudeEntry() { [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); - float lon = strtof(kb.text.c_str(), nullptr); - if (lon >= -180.0f && lon <= 180.0f) { - WEATHER_SETTINGS.setLocation(WEATHER_SETTINGS.getLatitude(), lon, WEATHER_SETTINGS.getLocationName()); + char* end = nullptr; + const float lon = strtof(kb.text.c_str(), &end); + if (end != kb.text.c_str() && *end == '\0' && lon >= -180.0f && lon <= 180.0f) { + if (lon != WEATHER_SETTINGS.getLongitude()) { + WEATHER_SETTINGS.setLocation(WEATHER_SETTINGS.getLatitude(), lon, ""); + } WEATHER_SETTINGS.saveToFile(); + requestUpdate(); } } }); diff --git a/src/activities/weather/WeatherSettingsActivity.h b/src/activities/weather/WeatherSettingsActivity.h index e0367b73..135f7911 100644 --- a/src/activities/weather/WeatherSettingsActivity.h +++ b/src/activities/weather/WeatherSettingsActivity.h @@ -30,7 +30,7 @@ class WeatherSettingsActivity final : public Activity { std::vector searchResults; bool showingSearchResults = false; - static constexpr int MENU_ITEMS = 6; // Location, Lat, Lon, TempUnit, WindUnit, PrecipUnit + static constexpr size_t MENU_ITEMS = 6; // Location, Lat, Lon, TempUnit, WindUnit, PrecipUnit void handleSelection(); void launchCitySearch(); diff --git a/src/network/HttpDownloader.cpp b/src/network/HttpDownloader.cpp index c1a3ad71..799b8874 100644 --- a/src/network/HttpDownloader.cpp +++ b/src/network/HttpDownloader.cpp @@ -55,52 +55,37 @@ class FileWriteStream final : public Stream { bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) { // Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP std::unique_ptr client; - LOG_DBG("HTTP", "fetchUrl[1] start (https=%d)", UrlUtils::isHttpsUrl(url) ? 1 : 0); if (UrlUtils::isHttpsUrl(url)) { auto* secureClient = new NetworkClientSecure(); - LOG_DBG("HTTP", "fetchUrl[2] created NetworkClientSecure"); secureClient->setInsecure(); - LOG_DBG("HTTP", "fetchUrl[3] setInsecure done"); client.reset(secureClient); } else { client.reset(new NetworkClient()); - LOG_DBG("HTTP", "fetchUrl[2] created NetworkClient"); } HTTPClient http; LOG_DBG("HTTP", "Fetching: %s", url.c_str()); - LOG_DBG("HTTP", "fetchUrl[4] begin() before"); http.begin(*client, url.c_str()); - LOG_DBG("HTTP", "fetchUrl[5] begin() after"); http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - LOG_DBG("HTTP", "fetchUrl[6] redirects configured"); http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); - LOG_DBG("HTTP", "fetchUrl[7] user-agent header added"); // Add Basic HTTP auth if credentials are configured if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) { std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword; String encoded = base64::encode(credentials.c_str()); http.addHeader("Authorization", "Basic " + encoded); - LOG_DBG("HTTP", "fetchUrl[8] auth header added"); } - LOG_DBG("HTTP", "fetchUrl[9] GET() before"); const int httpCode = http.GET(); - LOG_DBG("HTTP", "fetchUrl[10] GET() after code=%d", httpCode); if (httpCode != HTTP_CODE_OK) { LOG_ERR("HTTP", "Fetch failed: %d", httpCode); - LOG_DBG("HTTP", "fetchUrl[11] end() after GET failure"); http.end(); return false; } - LOG_DBG("HTTP", "fetchUrl[12] writeToStream() before"); http.writeToStream(&outContent); - LOG_DBG("HTTP", "fetchUrl[13] writeToStream() after"); - LOG_DBG("HTTP", "fetchUrl[14] end() before success return"); http.end(); LOG_DBG("HTTP", "Fetch success"); @@ -120,41 +105,30 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& ProgressCallback progress) { // Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP std::unique_ptr client; - LOG_DBG("HTTP", "downloadToFile[1] start (https=%d)", UrlUtils::isHttpsUrl(url) ? 1 : 0); if (UrlUtils::isHttpsUrl(url)) { auto* secureClient = new NetworkClientSecure(); - LOG_DBG("HTTP", "downloadToFile[2] created NetworkClientSecure"); secureClient->setInsecure(); - LOG_DBG("HTTP", "downloadToFile[3] setInsecure done"); client.reset(secureClient); } else { client.reset(new NetworkClient()); - LOG_DBG("HTTP", "downloadToFile[2] created NetworkClient"); } HTTPClient http; LOG_DBG("HTTP", "Downloading: %s", url.c_str()); LOG_DBG("HTTP", "Destination: %s", destPath.c_str()); - LOG_DBG("HTTP", "downloadToFile[4] begin() before"); http.begin(*client, url.c_str()); - LOG_DBG("HTTP", "downloadToFile[5] begin() after"); http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - LOG_DBG("HTTP", "downloadToFile[6] redirects configured"); http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); - LOG_DBG("HTTP", "downloadToFile[7] user-agent header added"); // Add Basic HTTP auth if credentials are configured if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) { std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword; String encoded = base64::encode(credentials.c_str()); http.addHeader("Authorization", "Basic " + encoded); - LOG_DBG("HTTP", "downloadToFile[8] auth header added"); } - LOG_DBG("HTTP", "downloadToFile[9] GET() before"); const int httpCode = http.GET(); - LOG_DBG("HTTP", "downloadToFile[10] GET() after code=%d", httpCode); if (httpCode != HTTP_CODE_OK) { LOG_ERR("HTTP", "Download failed: %d", httpCode); http.end(); @@ -184,14 +158,10 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& // Let HTTPClient handle chunked decoding and stream body bytes into the file. FileWriteStream fileStream(file, contentLength, progress); - LOG_DBG("HTTP", "downloadToFile[11] writeToStream() before"); const int writeResult = http.writeToStream(&fileStream); - LOG_DBG("HTTP", "downloadToFile[12] writeToStream() after result=%d", writeResult); file.close(); - LOG_DBG("HTTP", "downloadToFile[13] file closed"); http.end(); - LOG_DBG("HTTP", "downloadToFile[14] http end done"); if (writeResult < 0) { LOG_ERR("HTTP", "writeToStream error: %d", writeResult);