From 52d88c820e45dab79c7c8c54e17ea22ddf8499b7 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Fri, 3 Apr 2026 18:14:19 +0200 Subject: [PATCH] Add sun and moon info --- lib/I18n/translations/english.yaml | 10 ++ lib/I18n/translations/german.yaml | 10 ++ lib/Weather/WeatherClient.cpp | 4 + lib/Weather/WeatherData.h | 1 + src/activities/weather/WeatherActivity.cpp | 138 ++++++++++++++++++++- 5 files changed, 161 insertions(+), 2 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 281e6a18..3e904089 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -378,6 +378,14 @@ STR_WEATHER_MONTH_SEP: "Sep" STR_WEATHER_MONTH_OCT: "Oct" STR_WEATHER_MONTH_NOV: "Nov" STR_WEATHER_MONTH_DEC: "Dec" +STR_WEATHER_MOON_NEW: "New Moon" +STR_WEATHER_MOON_WAXING_CRESCENT: "Waxing Crescent" +STR_WEATHER_MOON_FIRST_QUARTER: "First Quarter" +STR_WEATHER_MOON_WAXING_GIBBOUS: "Waxing Gibbous" +STR_WEATHER_MOON_FULL: "Full Moon" +STR_WEATHER_MOON_WANING_GIBBOUS: "Waning Gibbous" +STR_WEATHER_MOON_LAST_QUARTER: "Last Quarter" +STR_WEATHER_MOON_WANING_CRESCENT: "Waning Crescent" STR_WEATHER_DESC_CLEAR_SKY: "Clear sky" STR_WEATHER_DESC_MAINLY_CLEAR: "Mainly clear" STR_WEATHER_DESC_PARTLY_CLOUDY: "Partly cloudy" @@ -434,3 +442,5 @@ STR_UPTIME: "Uptime" STR_CHARGING: "Charging" STR_GATHERING_DATA: "Gathering data..." STR_READING: "Reading..." +STR_WEATHER_MOON_INFO: "Moon" +STR_WEATHER_SUN_INFO: "Sun" \ No newline at end of file diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 3ca77de0..e2bcbbc2 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -363,6 +363,16 @@ STR_WEATHER_MONTH_OCT: "Okt" STR_WEATHER_MONTH_NOV: "Nov" STR_WEATHER_MONTH_DEC: "Dez" +STR_WEATHER_SUN_INFO: "Sonne" +STR_WEATHER_MOON_NEW: "Neumond" +STR_WEATHER_MOON_WAXING_CRESCENT: "zunehm. Halbmond" +STR_WEATHER_MOON_FIRST_QUARTER: "1. Viertel" +STR_WEATHER_MOON_WAXING_GIBBOUS: "zunehm. Mond" +STR_WEATHER_MOON_FULL: "Vollmond" +STR_WEATHER_MOON_WANING_GIBBOUS: "abnehm. Mond" +STR_WEATHER_MOON_LAST_QUARTER: "3. Viertel" +STR_WEATHER_MOON_WANING_CRESCENT: "abneh. Halbmond" + STR_WEATHER_DESC_CLEAR_SKY: "Klarer Himmel" STR_WEATHER_DESC_MAINLY_CLEAR: "Überwiegend klar" STR_WEATHER_DESC_PARTLY_CLOUDY: "Teilweise bewölkt" diff --git a/lib/Weather/WeatherClient.cpp b/lib/Weather/WeatherClient.cpp index 8610142d..2b8956ba 100644 --- a/lib/Weather/WeatherClient.cpp +++ b/lib/Weather/WeatherClient.cpp @@ -200,6 +200,7 @@ bool WeatherClient::parseWeatherJson(const std::string& json, WeatherData& data) day.sunrise = sunrise[i] | (time_t)0; day.sunset = sunset[i] | (time_t)0; day.uvIndexMax = uvMax[i] | 0.0f; + day.moonPhase = daily["moon_phase"] ? (daily["moon_phase"][i] | -1.0f) : -1.0f; data.daily.push_back(day); } } @@ -264,6 +265,8 @@ bool WeatherClient::saveCache(const WeatherData& data) { d["uvIndexMax"] = day.uvIndexMax; d["sunrise"] = day.sunrise; d["sunset"] = day.sunset; + d["moonPhase"] = day.moonPhase; + d["moonPhaseApiName"] = "moon_phase"; // for compatibility/tracing } // Hourly @@ -331,6 +334,7 @@ bool WeatherClient::loadCache(WeatherData& data) { day.uvIndexMax = d["uvIndexMax"] | 0.0f; day.sunrise = d["sunrise"] | (time_t)0; day.sunset = d["sunset"] | (time_t)0; + day.moonPhase = d["moonPhase"] | -1.0f; data.daily.push_back(day); } diff --git a/lib/Weather/WeatherData.h b/lib/Weather/WeatherData.h index 1410f9d7..50db4b8b 100644 --- a/lib/Weather/WeatherData.h +++ b/lib/Weather/WeatherData.h @@ -25,6 +25,7 @@ struct DailyForecast { float uvIndexMax = 0; time_t sunrise = 0; time_t sunset = 0; + float moonPhase = -1.0f; // -1 means unknown, 0.0 new, 0.5 full }; struct HourlyForecast { diff --git a/src/activities/weather/WeatherActivity.cpp b/src/activities/weather/WeatherActivity.cpp index c3589118..f9009cd5 100644 --- a/src/activities/weather/WeatherActivity.cpp +++ b/src/activities/weather/WeatherActivity.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include #include @@ -42,6 +41,7 @@ void drawWeatherIconWithOrientation(const GfxRenderer& renderer, const uint8_t* } } + StrId getWeatherDescriptionStrId(const int wmoCode) { switch (wmoCode) { case 0: @@ -104,6 +104,99 @@ StrId getWeatherDescriptionStrId(const int wmoCode) { return StrId::STR_WEATHER_DESC_UNKNOWN; } } + +enum class MoonPhaseType { + NEW, + WAXING_CRESCENT, + FIRST_QUARTER, + WAXING_GIBBOUS, + FULL, + WANING_GIBBOUS, + LAST_QUARTER, + WANING_CRESCENT, +}; + +static MoonPhaseType getMoonPhaseType(float phase) { + if (phase < 0.03f || phase > 0.97f) return MoonPhaseType::NEW; + if (phase < 0.22f) return MoonPhaseType::WAXING_CRESCENT; + if (phase < 0.28f) return MoonPhaseType::FIRST_QUARTER; + if (phase < 0.47f) return MoonPhaseType::WAXING_GIBBOUS; + if (phase < 0.53f) return MoonPhaseType::FULL; + if (phase < 0.72f) return MoonPhaseType::WANING_GIBBOUS; + if (phase < 0.78f) return MoonPhaseType::LAST_QUARTER; + return MoonPhaseType::WANING_CRESCENT; +} + +static float getMoonPhaseCycleFromTime(time_t timestamp) { + constexpr double knownNewMoon = 947182440.0; // Jan 6, 2000 18:14 UTC + constexpr double lunarCycle = 2551442.8; // Seconds in synodic month + + double secondsSince = static_cast(timestamp) - knownNewMoon; + double phase = secondsSince / lunarCycle; + phase = phase - floor(phase); + if (phase < 0.0) phase += 1.0; + return static_cast(phase); +} + +static StrId getMoonPhaseStrId(MoonPhaseType type) { + switch (type) { + case MoonPhaseType::NEW: + return StrId::STR_WEATHER_MOON_NEW; + case MoonPhaseType::WAXING_CRESCENT: + return StrId::STR_WEATHER_MOON_WAXING_CRESCENT; + case MoonPhaseType::FIRST_QUARTER: + return StrId::STR_WEATHER_MOON_FIRST_QUARTER; + case MoonPhaseType::WAXING_GIBBOUS: + return StrId::STR_WEATHER_MOON_WAXING_GIBBOUS; + case MoonPhaseType::FULL: + return StrId::STR_WEATHER_MOON_FULL; + case MoonPhaseType::WANING_GIBBOUS: + return StrId::STR_WEATHER_MOON_WANING_GIBBOUS; + case MoonPhaseType::LAST_QUARTER: + return StrId::STR_WEATHER_MOON_LAST_QUARTER; + case MoonPhaseType::WANING_CRESCENT: + return StrId::STR_WEATHER_MOON_WANING_CRESCENT; + default: + return StrId::STR_WEATHER_DESC_UNKNOWN; + } +} + +static float getMoonIlluminationFromCycle(float cycle) { + // 0.0 = new, 0.5 = full; convert to illuminated fraction + const float pi = 3.14159265358979323846f; + float illum = 0.5f * (1.0f - cosf(2.0f * pi * cycle)); + return illum; +} + +static void formatSunriseSunsetTime(char* buf, size_t bufSize, time_t epoch, int utcOffsetSeconds, bool use24h) { + time_t localTime = epoch + utcOffsetSeconds; + struct tm timeinfo; + gmtime_r(&localTime, &timeinfo); + + if (use24h) { + snprintf(buf, bufSize, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min); + } else { + int hour = timeinfo.tm_hour % 12; + if (hour == 0) hour = 12; + const char* suffix = timeinfo.tm_hour < 12 ? "AM" : "PM"; + snprintf(buf, bufSize, "%d:%02d%s", hour, timeinfo.tm_min, suffix); + } +} + +static time_t getNextFullMoonTime(time_t now) { + constexpr double knownNewMoon = 947182440.0; + constexpr double lunarCycle = 2551442.8; + + double secondsSince = static_cast(now) - knownNewMoon; + double synodic = secondsSince / lunarCycle; + synodic -= floor(synodic); + if (synodic < 0.0) synodic += 1.0; + + double untilFull = 0.5 - synodic; + if (untilFull < 0.0) untilFull += 1.0; + + return now + static_cast(untilFull * lunarCycle + 0.5); +} } // namespace void WeatherActivity::onEnter() { @@ -330,7 +423,8 @@ void WeatherActivity::render(RenderLock&&) { if (state == State::ERROR) { renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_ERROR_MSG)); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, errorMessage.c_str()); - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_WEATHER_SETTINGS), tr(STR_WEATHER_REFRESH), ""); + const auto labels = + mappedInput.mapLabels(tr(STR_BACK), tr(STR_WEATHER_SETTINGS_SHORT), tr(STR_WEATHER_REFRESH), ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); return; @@ -503,6 +597,8 @@ void WeatherActivity::renderDailyForecast(int x, int y, int w, int h) { const int extraPx = w % numDays; int cardX = x; + int debugMoonX = x + w - 18; + int debugMoonYBase = y + 10; for (int i = 0; i < numDays; i++) { const auto& day = weatherData.daily[i]; @@ -567,6 +663,44 @@ void WeatherActivity::renderDailyForecast(int x, int y, int w, int h) { snprintf(uvBuf, sizeof(uvBuf), "UV: %.0f", day.uvIndexMax); int uvWidth = renderer.getTextWidth(SMALL_FONT_ID, uvBuf); renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - uvWidth) / 2, textY, uvBuf); + textY += 16; + + // Sunrise/Sunset + { + char sunriseStr[16]; + char sunsetStr[16]; + formatSunriseSunsetTime(sunriseStr, sizeof(sunriseStr), day.sunrise, weatherData.utcOffsetSeconds, + !SETTINGS.clockFormat12h); + formatSunriseSunsetTime(sunsetStr, sizeof(sunsetStr), day.sunset, weatherData.utcOffsetSeconds, + !SETTINGS.clockFormat12h); + + char sunBuf[48]; + snprintf(sunBuf, sizeof(sunBuf), "%s: %s/%s", tr(STR_WEATHER_SUN_INFO), sunriseStr, sunsetStr); + auto truncSun = renderer.truncatedText(SMALL_FONT_ID, sunBuf, cardWidth - 8); + int sunWidth = renderer.getTextWidth(SMALL_FONT_ID, truncSun.c_str()); + renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - sunWidth) / 2, textY, truncSun.c_str()); + textY += 14; + } + + // Moon phase (derived from local calculation when API data unavailable) + { + float phase = day.moonPhase; + if (phase < 0.0f || phase > 1.0f) { + phase = getMoonPhaseCycleFromTime(day.date + weatherData.utcOffsetSeconds + 12 * 3600); + } + MoonPhaseType phaseType = getMoonPhaseType(phase); + float illumination = getMoonIlluminationFromCycle(phase); + int phasePercent = static_cast(illumination * 100.0f + 0.5f); + + const char* phaseName = I18N.get(getMoonPhaseStrId(phaseType)); + char moonBuf[48]; + snprintf(moonBuf, sizeof(moonBuf), "%s %3d%%", phaseName, phasePercent); + + int moonWidth = renderer.getTextWidth(SMALL_FONT_ID, moonBuf); + renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - moonWidth) / 2, textY, moonBuf); + + textY += 14; + } // Card separator if (i < numDays - 1) {