Review changes
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
|
||||||
#include "../../src/network/HttpDownloader.h"
|
#include "../../src/network/HttpDownloader.h"
|
||||||
@@ -12,9 +13,7 @@ namespace {
|
|||||||
constexpr char WEATHER_CACHE_FILE[] = "/.crosspoint/weather_cache.json";
|
constexpr char WEATHER_CACHE_FILE[] = "/.crosspoint/weather_cache.json";
|
||||||
|
|
||||||
std::string buildForecastUrl(const WeatherSettingsStore& settings) {
|
std::string buildForecastUrl(const WeatherSettingsStore& settings) {
|
||||||
// Open-Meteo supports plain HTTP. Using HTTP here avoids TLS handshake
|
std::string url = "https://api.open-meteo.com/v1/forecast?";
|
||||||
// instability observed on some ESP32-C3 builds.
|
|
||||||
std::string url = "http://api.open-meteo.com/v1/forecast?";
|
|
||||||
url += "latitude=" + std::to_string(settings.getLatitude());
|
url += "latitude=" + std::to_string(settings.getLatitude());
|
||||||
url += "&longitude=" + std::to_string(settings.getLongitude());
|
url += "&longitude=" + std::to_string(settings.getLongitude());
|
||||||
url +=
|
url +=
|
||||||
@@ -33,8 +32,33 @@ std::string buildForecastUrl(const WeatherSettingsStore& settings) {
|
|||||||
url += "&forecast_hours=48";
|
url += "&forecast_hours=48";
|
||||||
return url;
|
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
|
} // namespace
|
||||||
|
|
||||||
|
std::string WeatherClient::buildRequestSignature(const WeatherSettingsStore& settings) {
|
||||||
|
return ::buildRequestSignature(settings);
|
||||||
|
}
|
||||||
|
|
||||||
WeatherData WeatherClient::getWeather(const WeatherSettingsStore& settings, bool forceRefresh) {
|
WeatherData WeatherClient::getWeather(const WeatherSettingsStore& settings, bool forceRefresh) {
|
||||||
if (!settings.hasLocation()) {
|
if (!settings.hasLocation()) {
|
||||||
WeatherData data;
|
WeatherData data;
|
||||||
@@ -42,19 +66,26 @@ WeatherData WeatherClient::getWeather(const WeatherSettingsStore& settings, bool
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::string requestSignature = buildRequestSignature(settings);
|
||||||
|
|
||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
WeatherData cached;
|
WeatherData cached;
|
||||||
if (loadCache(cached) && cached.valid) {
|
if (loadCache(cached) && cached.valid) {
|
||||||
time_t now;
|
if (cached.requestSignature != requestSignature) {
|
||||||
time(&now);
|
LOG_DBG("WEA", "Ignoring cache with mismatched request signature");
|
||||||
if (now - cached.fetchedAt < CACHE_TTL_SECONDS) {
|
Storage.remove(WEATHER_CACHE_FILE);
|
||||||
LOG_DBG("WEA", "Using cached weather data (age: %ld s)", (long)(now - cached.fetchedAt));
|
} 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;
|
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");
|
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 WeatherClient::fetchFromApi(const WeatherSettingsStore& settings) {
|
||||||
WeatherData data;
|
WeatherData data;
|
||||||
|
data.requestSignature = buildRequestSignature(settings);
|
||||||
std::string url = buildForecastUrl(settings);
|
std::string url = buildForecastUrl(settings);
|
||||||
LOG_DBG("WEA", "fetchFromApi[1] start");
|
LOG_DBG("WEA", "fetchFromApi[1] start");
|
||||||
LOG_DBG("WEA", "fetchFromApi[2] url length=%zu", url.size());
|
LOG_DBG("WEA", "fetchFromApi[2] url length=%zu", url.size());
|
||||||
@@ -188,6 +220,7 @@ bool WeatherClient::saveCache(const WeatherData& data) {
|
|||||||
Storage.mkdir("/.crosspoint");
|
Storage.mkdir("/.crosspoint");
|
||||||
|
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
|
doc["requestSignature"] = data.requestSignature;
|
||||||
doc["fetchedAt"] = data.fetchedAt;
|
doc["fetchedAt"] = data.fetchedAt;
|
||||||
doc["timezone"] = data.timezone;
|
doc["timezone"] = data.timezone;
|
||||||
doc["utcOffsetSeconds"] = data.utcOffsetSeconds;
|
doc["utcOffsetSeconds"] = data.utcOffsetSeconds;
|
||||||
@@ -253,6 +286,7 @@ bool WeatherClient::loadCache(WeatherData& data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data.fetchedAt = doc["fetchedAt"] | (time_t)0;
|
data.fetchedAt = doc["fetchedAt"] | (time_t)0;
|
||||||
|
data.requestSignature = doc["requestSignature"] | std::string("");
|
||||||
data.timezone = doc["timezone"] | std::string("");
|
data.timezone = doc["timezone"] | std::string("");
|
||||||
data.utcOffsetSeconds = doc["utcOffsetSeconds"] | 0;
|
data.utcOffsetSeconds = doc["utcOffsetSeconds"] | 0;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class WeatherClient {
|
|||||||
private:
|
private:
|
||||||
static WeatherData fetchFromApi(const WeatherSettingsStore& settings);
|
static WeatherData fetchFromApi(const WeatherSettingsStore& settings);
|
||||||
static bool parseWeatherJson(const std::string& json, WeatherData& data);
|
static bool parseWeatherJson(const std::string& json, WeatherData& data);
|
||||||
|
static std::string buildRequestSignature(const WeatherSettingsStore& settings);
|
||||||
static bool saveCache(const WeatherData& data);
|
static bool saveCache(const WeatherData& data);
|
||||||
static bool loadCache(WeatherData& data);
|
static bool loadCache(WeatherData& data);
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ struct WeatherData {
|
|||||||
CurrentWeather current;
|
CurrentWeather current;
|
||||||
std::vector<DailyForecast> daily;
|
std::vector<DailyForecast> daily;
|
||||||
std::vector<HourlyForecast> hourly;
|
std::vector<HourlyForecast> hourly;
|
||||||
|
std::string requestSignature;
|
||||||
std::string timezone;
|
std::string timezone;
|
||||||
int utcOffsetSeconds = 0;
|
int utcOffsetSeconds = 0;
|
||||||
time_t fetchedAt = 0;
|
time_t fetchedAt = 0;
|
||||||
|
|||||||
@@ -4,180 +4,7 @@
|
|||||||
|
|
||||||
// Weather icon glyph source attribution:
|
// Weather icon glyph source attribution:
|
||||||
// https://github.com/erikflowers/weather-icons
|
// https://github.com/erikflowers/weather-icons
|
||||||
// 64x64 monochrome icon bitmaps in this file are adapted from that icon set.
|
// Large icons are provided by generated WI48_* arrays in WeatherIcons48.h.
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 24x24 Small Weather Icons (1-bit, MSB-first)
|
// 24x24 Small Weather Icons (1-bit, MSB-first)
|
||||||
@@ -332,69 +159,6 @@ const uint8_t* getWeatherIconSmall(WeatherIconType type) {
|
|||||||
}
|
}
|
||||||
#endif // WEATHER_ENABLE_SMALL_ICONS
|
#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) {
|
const char* getWindDirectionText(int degrees) {
|
||||||
// Normalize to 0-360
|
// Normalize to 0-360
|
||||||
degrees = ((degrees % 360) + 360) % 360;
|
degrees = ((degrees % 360) + 360) % 360;
|
||||||
|
|||||||
@@ -60,9 +60,10 @@ inline WeatherIconType getWeatherIconType(int wmoCode, bool isDay) {
|
|||||||
case 80:
|
case 80:
|
||||||
case 81:
|
case 81:
|
||||||
case 82:
|
case 82:
|
||||||
|
return WeatherIconType::SHOWERS;
|
||||||
case 85:
|
case 85:
|
||||||
case 86:
|
case 86:
|
||||||
return WeatherIconType::SHOWERS;
|
return WeatherIconType::SNOW;
|
||||||
case 95:
|
case 95:
|
||||||
case 96:
|
case 96:
|
||||||
case 99:
|
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)
|
// Get the appropriate large icon bitmap (64x64, 1-bit, MSB first)
|
||||||
const uint8_t* getWeatherIconLarge(WeatherIconType type);
|
const uint8_t* getWeatherIconLarge(WeatherIconType type);
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ bool WeatherSettingsStore::saveToFile() const {
|
|||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc["latitude"] = latitude;
|
doc["latitude"] = latitude;
|
||||||
doc["longitude"] = longitude;
|
doc["longitude"] = longitude;
|
||||||
|
doc["locationConfigured"] = locationConfigured;
|
||||||
doc["locationName"] = locationName;
|
doc["locationName"] = locationName;
|
||||||
doc["tempUnit"] = static_cast<uint8_t>(tempUnit);
|
doc["tempUnit"] = static_cast<uint8_t>(tempUnit);
|
||||||
doc["windUnit"] = static_cast<uint8_t>(windUnit);
|
doc["windUnit"] = static_cast<uint8_t>(windUnit);
|
||||||
@@ -47,6 +48,7 @@ bool WeatherSettingsStore::loadFromFile() {
|
|||||||
|
|
||||||
latitude = doc["latitude"] | 0.0f;
|
latitude = doc["latitude"] | 0.0f;
|
||||||
longitude = doc["longitude"] | 0.0f;
|
longitude = doc["longitude"] | 0.0f;
|
||||||
|
locationConfigured = doc["locationConfigured"] | (latitude != 0.0f || longitude != 0.0f);
|
||||||
locationName = doc["locationName"] | std::string("");
|
locationName = doc["locationName"] | std::string("");
|
||||||
tempUnit = static_cast<WeatherTempUnit>(doc["tempUnit"] | (uint8_t)0);
|
tempUnit = static_cast<WeatherTempUnit>(doc["tempUnit"] | (uint8_t)0);
|
||||||
windUnit = static_cast<WeatherWindUnit>(doc["windUnit"] | (uint8_t)0);
|
windUnit = static_cast<WeatherWindUnit>(doc["windUnit"] | (uint8_t)0);
|
||||||
@@ -63,10 +65,18 @@ bool WeatherSettingsStore::loadFromFile() {
|
|||||||
void WeatherSettingsStore::setLocation(float lat, float lon, const std::string& name) {
|
void WeatherSettingsStore::setLocation(float lat, float lon, const std::string& name) {
|
||||||
latitude = lat;
|
latitude = lat;
|
||||||
longitude = lon;
|
longitude = lon;
|
||||||
|
locationConfigured = true;
|
||||||
locationName = name;
|
locationName = name;
|
||||||
LOG_DBG("WEA", "Set location: %s (%.4f, %.4f)", name.c_str(), lat, lon);
|
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) {
|
void WeatherSettingsStore::setForecastDays(uint8_t days) {
|
||||||
if (days < 1) days = 1;
|
if (days < 1) days = 1;
|
||||||
if (days > 5) days = 5;
|
if (days > 5) days = 5;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class WeatherSettingsStore {
|
|||||||
|
|
||||||
float latitude = 0;
|
float latitude = 0;
|
||||||
float longitude = 0;
|
float longitude = 0;
|
||||||
|
bool locationConfigured = false;
|
||||||
std::string locationName;
|
std::string locationName;
|
||||||
WeatherTempUnit tempUnit = WeatherTempUnit::CELSIUS;
|
WeatherTempUnit tempUnit = WeatherTempUnit::CELSIUS;
|
||||||
WeatherWindUnit windUnit = WeatherWindUnit::KMH;
|
WeatherWindUnit windUnit = WeatherWindUnit::KMH;
|
||||||
@@ -31,10 +32,11 @@ class WeatherSettingsStore {
|
|||||||
|
|
||||||
// Location
|
// Location
|
||||||
void setLocation(float lat, float lon, const std::string& name);
|
void setLocation(float lat, float lon, const std::string& name);
|
||||||
|
void clearLocation();
|
||||||
float getLatitude() const { return latitude; }
|
float getLatitude() const { return latitude; }
|
||||||
float getLongitude() const { return longitude; }
|
float getLongitude() const { return longitude; }
|
||||||
const std::string& getLocationName() const { return locationName; }
|
const std::string& getLocationName() const { return locationName; }
|
||||||
bool hasLocation() const { return latitude != 0 || longitude != 0; }
|
bool hasLocation() const { return locationConfigured; }
|
||||||
|
|
||||||
// Units
|
// Units
|
||||||
void setTempUnit(WeatherTempUnit unit) { tempUnit = unit; }
|
void setTempUnit(WeatherTempUnit unit) { tempUnit = unit; }
|
||||||
|
|||||||
+3
-55
@@ -3,63 +3,11 @@ import os
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
import cairosvg
|
import cairosvg
|
||||||
import io
|
import io
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
from svg_utils import fit_inside_canvas, parse_svg_intrinsic_size
|
||||||
|
|
||||||
threshold = 128
|
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):
|
def svg_to_png_bytes(svg_path, width, height):
|
||||||
with open(svg_path, "rb") as f:
|
with open(svg_path, "rb") as f:
|
||||||
svg_data = f.read()
|
svg_data = f.read()
|
||||||
@@ -124,7 +72,7 @@ def image_to_c_array(img, array_name):
|
|||||||
byte |= bit << (7 - b)
|
byte |= bit << (7 - b)
|
||||||
packed.append(byte)
|
packed.append(byte)
|
||||||
# Format as C array
|
# Format as C array
|
||||||
c = f"#pragma once\n#include <cstdint>\n\n"
|
c = "#pragma once\n#include <cstdint>\n\n"
|
||||||
c += f"// size: {width}x{height}\n"
|
c += f"// size: {width}x{height}\n"
|
||||||
c += f"static const uint8_t {array_name}[] = {{\n "
|
c += f"static const uint8_t {array_name}[] = {{\n "
|
||||||
for i, v in enumerate(packed):
|
for i, v in enumerate(packed):
|
||||||
|
|||||||
@@ -14,14 +14,15 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
from svg_utils import fit_inside_canvas, parse_svg_intrinsic_size
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cairosvg # type: ignore
|
import cairosvg # type: ignore
|
||||||
except Exception:
|
except ImportError:
|
||||||
cairosvg = None
|
cairosvg = None
|
||||||
|
|
||||||
|
|
||||||
@@ -52,55 +53,6 @@ ICON_SOURCES = {
|
|||||||
"WI48_THUNDERSTORM": "wi-thunderstorm.svg",
|
"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():
|
def ensure_resvg_binary():
|
||||||
if shutil.which("resvg"):
|
if shutil.which("resvg"):
|
||||||
return Path(shutil.which("resvg"))
|
return Path(shutil.which("resvg"))
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -230,7 +230,7 @@ void HomeActivity::render(RenderLock&&) {
|
|||||||
// Build menu items dynamically
|
// Build menu items dynamically
|
||||||
std::vector<const char*> menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER),
|
std::vector<const char*> menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER),
|
||||||
tr(STR_WEATHER), tr(STR_SETTINGS_TITLE)};
|
tr(STR_WEATHER), tr(STR_SETTINGS_TITLE)};
|
||||||
std::vector<UIIcon> menuIcons = {Folder, Recent, Library, Transfer, Settings};
|
std::vector<UIIcon> menuIcons = {Folder, Recent, Transfer, Library, Settings};
|
||||||
|
|
||||||
if (hasOpdsUrl) {
|
if (hasOpdsUrl) {
|
||||||
// Insert OPDS Browser after Recents (before File Transfer)
|
// Insert OPDS Browser after Recents (before File Transfer)
|
||||||
|
|||||||
@@ -232,6 +232,27 @@ void WeatherActivity::fetchWeather() {
|
|||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WeatherActivity::openSettingsActivity() {
|
||||||
|
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||||
|
startActivityForResult(std::make_unique<WeatherSettingsActivity>(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() {
|
void WeatherActivity::loop() {
|
||||||
if (state == State::WIFI_SELECTION) {
|
if (state == State::WIFI_SELECTION) {
|
||||||
return; // Handled by WifiSelectionActivity
|
return; // Handled by WifiSelectionActivity
|
||||||
@@ -239,24 +260,10 @@ void WeatherActivity::loop() {
|
|||||||
|
|
||||||
if (state == State::ERROR) {
|
if (state == State::ERROR) {
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
// Open weather settings (useful when no location is configured)
|
openSettingsActivity();
|
||||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
|
||||||
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput),
|
|
||||||
[this](const ActivityResult&) {
|
|
||||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
|
|
||||||
forceRefresh = true;
|
|
||||||
loadAndDisplay();
|
|
||||||
});
|
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||||
// Retry fetch
|
triggerRefresh(false);
|
||||||
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
|
|
||||||
state = State::FETCHING;
|
|
||||||
requestUpdate(true);
|
|
||||||
fetchWeather();
|
|
||||||
} else {
|
|
||||||
launchWifiSelection();
|
|
||||||
}
|
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
onGoHome();
|
onGoHome();
|
||||||
}
|
}
|
||||||
@@ -299,27 +306,10 @@ void WeatherActivity::loop() {
|
|||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
onGoHome();
|
onGoHome();
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
// Open weather settings
|
openSettingsActivity();
|
||||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
|
||||||
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput),
|
|
||||||
[this](const ActivityResult&) {
|
|
||||||
// Re-apply landscape after returning from settings
|
|
||||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
|
|
||||||
// Refresh after settings change
|
|
||||||
forceRefresh = true;
|
|
||||||
loadAndDisplay();
|
|
||||||
});
|
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left) ||
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||||
// Manual refresh with popup feedback.
|
triggerRefresh(true);
|
||||||
showRefreshPopup = true;
|
|
||||||
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
|
|
||||||
state = State::FETCHING;
|
|
||||||
requestUpdate(true);
|
|
||||||
fetchWeather();
|
|
||||||
} else {
|
|
||||||
launchWifiSelection();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,9 +377,9 @@ void WeatherActivity::render(RenderLock&&) {
|
|||||||
|
|
||||||
if (state == State::FETCHING && showRefreshPopup) {
|
if (state == State::FETCHING && showRefreshPopup) {
|
||||||
GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
|
GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
|
||||||
|
} else {
|
||||||
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.displayBuffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WeatherActivity::renderCurrentConditions(int x, int y, int w, int h) {
|
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,
|
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_WED, StrId::STR_WEATHER_DAY_THU, StrId::STR_WEATHER_DAY_FRI,
|
||||||
StrId::STR_WEATHER_DAY_SAT};
|
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";
|
const char* unitSuffix = WEATHER_SETTINGS.getTempUnit() == WeatherTempUnit::CELSIUS ? "C" : "F";
|
||||||
int numDays = static_cast<int>(weatherData.daily.size());
|
int numDays = static_cast<int>(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
|
// Date (e.g. "Apr 3") - for all days
|
||||||
char dateBuf[16];
|
char dateBuf[16];
|
||||||
const char* monthNames[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
|
const char* monthName = I18N.get(monthNameIds[timeinfo.tm_mon]);
|
||||||
snprintf(dateBuf, sizeof(dateBuf), "%s %d", monthNames[timeinfo.tm_mon], timeinfo.tm_mday);
|
snprintf(dateBuf, sizeof(dateBuf), "%s %d", monthName, timeinfo.tm_mday);
|
||||||
int dateWidth = renderer.getTextWidth(SMALL_FONT_ID, dateBuf);
|
int dateWidth = renderer.getTextWidth(SMALL_FONT_ID, dateBuf);
|
||||||
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - dateWidth) / 2, textY, dateBuf);
|
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - dateWidth) / 2, textY, dateBuf);
|
||||||
textY += 18;
|
textY += 18;
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ class WeatherActivity final : public Activity {
|
|||||||
void launchWifiSelection();
|
void launchWifiSelection();
|
||||||
void onWifiSelectionComplete(bool connected);
|
void onWifiSelectionComplete(bool connected);
|
||||||
void fetchWeather();
|
void fetchWeather();
|
||||||
|
void openSettingsActivity();
|
||||||
|
void triggerRefresh(bool showPopup);
|
||||||
|
|
||||||
// Render sub-sections (landscape 800x480)
|
// Render sub-sections (landscape 800x480)
|
||||||
void renderCurrentConditions(int x, int y, int w, int h);
|
void renderCurrentConditions(int x, int y, int w, int h);
|
||||||
|
|||||||
@@ -125,21 +125,13 @@ void WeatherSettingsActivity::launchCitySearch() {
|
|||||||
[this, query = kb.text](const ActivityResult& wifiResult) {
|
[this, query = kb.text](const ActivityResult& wifiResult) {
|
||||||
if (wifiResult.isCancelled) return;
|
if (wifiResult.isCancelled) return;
|
||||||
searchResults = WeatherClient::searchCity(query);
|
searchResults = WeatherClient::searchCity(query);
|
||||||
if (searchResults.empty()) {
|
showingSearchResults = !searchResults.empty();
|
||||||
requestUpdate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showingSearchResults = true;
|
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
searchResults = WeatherClient::searchCity(kb.text);
|
searchResults = WeatherClient::searchCity(kb.text);
|
||||||
if (searchResults.empty()) {
|
showingSearchResults = !searchResults.empty();
|
||||||
requestUpdate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showingSearchResults = true;
|
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
@@ -153,10 +145,14 @@ void WeatherSettingsActivity::launchLatitudeEntry() {
|
|||||||
[this](const ActivityResult& result) {
|
[this](const ActivityResult& result) {
|
||||||
if (!result.isCancelled) {
|
if (!result.isCancelled) {
|
||||||
const auto& kb = std::get<KeyboardResult>(result.data);
|
const auto& kb = std::get<KeyboardResult>(result.data);
|
||||||
float lat = strtof(kb.text.c_str(), nullptr);
|
char* end = nullptr;
|
||||||
if (lat >= -90.0f && lat <= 90.0f) {
|
const float lat = strtof(kb.text.c_str(), &end);
|
||||||
WEATHER_SETTINGS.setLocation(lat, WEATHER_SETTINGS.getLongitude(), WEATHER_SETTINGS.getLocationName());
|
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();
|
WEATHER_SETTINGS.saveToFile();
|
||||||
|
requestUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -169,10 +165,14 @@ void WeatherSettingsActivity::launchLongitudeEntry() {
|
|||||||
[this](const ActivityResult& result) {
|
[this](const ActivityResult& result) {
|
||||||
if (!result.isCancelled) {
|
if (!result.isCancelled) {
|
||||||
const auto& kb = std::get<KeyboardResult>(result.data);
|
const auto& kb = std::get<KeyboardResult>(result.data);
|
||||||
float lon = strtof(kb.text.c_str(), nullptr);
|
char* end = nullptr;
|
||||||
if (lon >= -180.0f && lon <= 180.0f) {
|
const float lon = strtof(kb.text.c_str(), &end);
|
||||||
WEATHER_SETTINGS.setLocation(WEATHER_SETTINGS.getLatitude(), lon, WEATHER_SETTINGS.getLocationName());
|
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();
|
WEATHER_SETTINGS.saveToFile();
|
||||||
|
requestUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class WeatherSettingsActivity final : public Activity {
|
|||||||
std::vector<GeocodingResult> searchResults;
|
std::vector<GeocodingResult> searchResults;
|
||||||
bool showingSearchResults = false;
|
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 handleSelection();
|
||||||
void launchCitySearch();
|
void launchCitySearch();
|
||||||
|
|||||||
@@ -55,52 +55,37 @@ class FileWriteStream final : public Stream {
|
|||||||
bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) {
|
bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) {
|
||||||
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
|
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
|
||||||
std::unique_ptr<NetworkClient> client;
|
std::unique_ptr<NetworkClient> client;
|
||||||
LOG_DBG("HTTP", "fetchUrl[1] start (https=%d)", UrlUtils::isHttpsUrl(url) ? 1 : 0);
|
|
||||||
if (UrlUtils::isHttpsUrl(url)) {
|
if (UrlUtils::isHttpsUrl(url)) {
|
||||||
auto* secureClient = new NetworkClientSecure();
|
auto* secureClient = new NetworkClientSecure();
|
||||||
LOG_DBG("HTTP", "fetchUrl[2] created NetworkClientSecure");
|
|
||||||
secureClient->setInsecure();
|
secureClient->setInsecure();
|
||||||
LOG_DBG("HTTP", "fetchUrl[3] setInsecure done");
|
|
||||||
client.reset(secureClient);
|
client.reset(secureClient);
|
||||||
} else {
|
} else {
|
||||||
client.reset(new NetworkClient());
|
client.reset(new NetworkClient());
|
||||||
LOG_DBG("HTTP", "fetchUrl[2] created NetworkClient");
|
|
||||||
}
|
}
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
|
|
||||||
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
|
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
|
||||||
|
|
||||||
LOG_DBG("HTTP", "fetchUrl[4] begin() before");
|
|
||||||
http.begin(*client, url.c_str());
|
http.begin(*client, url.c_str());
|
||||||
LOG_DBG("HTTP", "fetchUrl[5] begin() after");
|
|
||||||
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||||
LOG_DBG("HTTP", "fetchUrl[6] redirects configured");
|
|
||||||
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
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
|
// Add Basic HTTP auth if credentials are configured
|
||||||
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
|
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
|
||||||
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
|
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
|
||||||
String encoded = base64::encode(credentials.c_str());
|
String encoded = base64::encode(credentials.c_str());
|
||||||
http.addHeader("Authorization", "Basic " + encoded);
|
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();
|
const int httpCode = http.GET();
|
||||||
LOG_DBG("HTTP", "fetchUrl[10] GET() after code=%d", httpCode);
|
|
||||||
if (httpCode != HTTP_CODE_OK) {
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
LOG_ERR("HTTP", "Fetch failed: %d", httpCode);
|
LOG_ERR("HTTP", "Fetch failed: %d", httpCode);
|
||||||
LOG_DBG("HTTP", "fetchUrl[11] end() after GET failure");
|
|
||||||
http.end();
|
http.end();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DBG("HTTP", "fetchUrl[12] writeToStream() before");
|
|
||||||
http.writeToStream(&outContent);
|
http.writeToStream(&outContent);
|
||||||
LOG_DBG("HTTP", "fetchUrl[13] writeToStream() after");
|
|
||||||
|
|
||||||
LOG_DBG("HTTP", "fetchUrl[14] end() before success return");
|
|
||||||
http.end();
|
http.end();
|
||||||
|
|
||||||
LOG_DBG("HTTP", "Fetch success");
|
LOG_DBG("HTTP", "Fetch success");
|
||||||
@@ -120,41 +105,30 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
|
|||||||
ProgressCallback progress) {
|
ProgressCallback progress) {
|
||||||
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
|
// Use NetworkClientSecure for HTTPS, regular NetworkClient for HTTP
|
||||||
std::unique_ptr<NetworkClient> client;
|
std::unique_ptr<NetworkClient> client;
|
||||||
LOG_DBG("HTTP", "downloadToFile[1] start (https=%d)", UrlUtils::isHttpsUrl(url) ? 1 : 0);
|
|
||||||
if (UrlUtils::isHttpsUrl(url)) {
|
if (UrlUtils::isHttpsUrl(url)) {
|
||||||
auto* secureClient = new NetworkClientSecure();
|
auto* secureClient = new NetworkClientSecure();
|
||||||
LOG_DBG("HTTP", "downloadToFile[2] created NetworkClientSecure");
|
|
||||||
secureClient->setInsecure();
|
secureClient->setInsecure();
|
||||||
LOG_DBG("HTTP", "downloadToFile[3] setInsecure done");
|
|
||||||
client.reset(secureClient);
|
client.reset(secureClient);
|
||||||
} else {
|
} else {
|
||||||
client.reset(new NetworkClient());
|
client.reset(new NetworkClient());
|
||||||
LOG_DBG("HTTP", "downloadToFile[2] created NetworkClient");
|
|
||||||
}
|
}
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
|
|
||||||
LOG_DBG("HTTP", "Downloading: %s", url.c_str());
|
LOG_DBG("HTTP", "Downloading: %s", url.c_str());
|
||||||
LOG_DBG("HTTP", "Destination: %s", destPath.c_str());
|
LOG_DBG("HTTP", "Destination: %s", destPath.c_str());
|
||||||
|
|
||||||
LOG_DBG("HTTP", "downloadToFile[4] begin() before");
|
|
||||||
http.begin(*client, url.c_str());
|
http.begin(*client, url.c_str());
|
||||||
LOG_DBG("HTTP", "downloadToFile[5] begin() after");
|
|
||||||
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||||
LOG_DBG("HTTP", "downloadToFile[6] redirects configured");
|
|
||||||
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
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
|
// Add Basic HTTP auth if credentials are configured
|
||||||
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
|
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
|
||||||
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
|
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
|
||||||
String encoded = base64::encode(credentials.c_str());
|
String encoded = base64::encode(credentials.c_str());
|
||||||
http.addHeader("Authorization", "Basic " + encoded);
|
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();
|
const int httpCode = http.GET();
|
||||||
LOG_DBG("HTTP", "downloadToFile[10] GET() after code=%d", httpCode);
|
|
||||||
if (httpCode != HTTP_CODE_OK) {
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
LOG_ERR("HTTP", "Download failed: %d", httpCode);
|
LOG_ERR("HTTP", "Download failed: %d", httpCode);
|
||||||
http.end();
|
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.
|
// Let HTTPClient handle chunked decoding and stream body bytes into the file.
|
||||||
FileWriteStream fileStream(file, contentLength, progress);
|
FileWriteStream fileStream(file, contentLength, progress);
|
||||||
LOG_DBG("HTTP", "downloadToFile[11] writeToStream() before");
|
|
||||||
const int writeResult = http.writeToStream(&fileStream);
|
const int writeResult = http.writeToStream(&fileStream);
|
||||||
LOG_DBG("HTTP", "downloadToFile[12] writeToStream() after result=%d", writeResult);
|
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
LOG_DBG("HTTP", "downloadToFile[13] file closed");
|
|
||||||
http.end();
|
http.end();
|
||||||
LOG_DBG("HTTP", "downloadToFile[14] http end done");
|
|
||||||
|
|
||||||
if (writeResult < 0) {
|
if (writeResult < 0) {
|
||||||
LOG_ERR("HTTP", "writeToStream error: %d", writeResult);
|
LOG_ERR("HTTP", "writeToStream error: %d", writeResult);
|
||||||
|
|||||||
Reference in New Issue
Block a user