Add sun and moon info
This commit is contained in:
@@ -378,6 +378,14 @@ STR_WEATHER_MONTH_SEP: "Sep"
|
|||||||
STR_WEATHER_MONTH_OCT: "Oct"
|
STR_WEATHER_MONTH_OCT: "Oct"
|
||||||
STR_WEATHER_MONTH_NOV: "Nov"
|
STR_WEATHER_MONTH_NOV: "Nov"
|
||||||
STR_WEATHER_MONTH_DEC: "Dec"
|
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_CLEAR_SKY: "Clear sky"
|
||||||
STR_WEATHER_DESC_MAINLY_CLEAR: "Mainly clear"
|
STR_WEATHER_DESC_MAINLY_CLEAR: "Mainly clear"
|
||||||
STR_WEATHER_DESC_PARTLY_CLOUDY: "Partly cloudy"
|
STR_WEATHER_DESC_PARTLY_CLOUDY: "Partly cloudy"
|
||||||
@@ -434,3 +442,5 @@ STR_UPTIME: "Uptime"
|
|||||||
STR_CHARGING: "Charging"
|
STR_CHARGING: "Charging"
|
||||||
STR_GATHERING_DATA: "Gathering data..."
|
STR_GATHERING_DATA: "Gathering data..."
|
||||||
STR_READING: "Reading..."
|
STR_READING: "Reading..."
|
||||||
|
STR_WEATHER_MOON_INFO: "Moon"
|
||||||
|
STR_WEATHER_SUN_INFO: "Sun"
|
||||||
@@ -363,6 +363,16 @@ STR_WEATHER_MONTH_OCT: "Okt"
|
|||||||
STR_WEATHER_MONTH_NOV: "Nov"
|
STR_WEATHER_MONTH_NOV: "Nov"
|
||||||
STR_WEATHER_MONTH_DEC: "Dez"
|
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_CLEAR_SKY: "Klarer Himmel"
|
||||||
STR_WEATHER_DESC_MAINLY_CLEAR: "Überwiegend klar"
|
STR_WEATHER_DESC_MAINLY_CLEAR: "Überwiegend klar"
|
||||||
STR_WEATHER_DESC_PARTLY_CLOUDY: "Teilweise bewölkt"
|
STR_WEATHER_DESC_PARTLY_CLOUDY: "Teilweise bewölkt"
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ bool WeatherClient::parseWeatherJson(const std::string& json, WeatherData& data)
|
|||||||
day.sunrise = sunrise[i] | (time_t)0;
|
day.sunrise = sunrise[i] | (time_t)0;
|
||||||
day.sunset = sunset[i] | (time_t)0;
|
day.sunset = sunset[i] | (time_t)0;
|
||||||
day.uvIndexMax = uvMax[i] | 0.0f;
|
day.uvIndexMax = uvMax[i] | 0.0f;
|
||||||
|
day.moonPhase = daily["moon_phase"] ? (daily["moon_phase"][i] | -1.0f) : -1.0f;
|
||||||
data.daily.push_back(day);
|
data.daily.push_back(day);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,6 +265,8 @@ bool WeatherClient::saveCache(const WeatherData& data) {
|
|||||||
d["uvIndexMax"] = day.uvIndexMax;
|
d["uvIndexMax"] = day.uvIndexMax;
|
||||||
d["sunrise"] = day.sunrise;
|
d["sunrise"] = day.sunrise;
|
||||||
d["sunset"] = day.sunset;
|
d["sunset"] = day.sunset;
|
||||||
|
d["moonPhase"] = day.moonPhase;
|
||||||
|
d["moonPhaseApiName"] = "moon_phase"; // for compatibility/tracing
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hourly
|
// Hourly
|
||||||
@@ -331,6 +334,7 @@ bool WeatherClient::loadCache(WeatherData& data) {
|
|||||||
day.uvIndexMax = d["uvIndexMax"] | 0.0f;
|
day.uvIndexMax = d["uvIndexMax"] | 0.0f;
|
||||||
day.sunrise = d["sunrise"] | (time_t)0;
|
day.sunrise = d["sunrise"] | (time_t)0;
|
||||||
day.sunset = d["sunset"] | (time_t)0;
|
day.sunset = d["sunset"] | (time_t)0;
|
||||||
|
day.moonPhase = d["moonPhase"] | -1.0f;
|
||||||
data.daily.push_back(day);
|
data.daily.push_back(day);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ struct DailyForecast {
|
|||||||
float uvIndexMax = 0;
|
float uvIndexMax = 0;
|
||||||
time_t sunrise = 0;
|
time_t sunrise = 0;
|
||||||
time_t sunset = 0;
|
time_t sunset = 0;
|
||||||
|
float moonPhase = -1.0f; // -1 means unknown, 0.0 new, 0.5 full
|
||||||
};
|
};
|
||||||
|
|
||||||
struct HourlyForecast {
|
struct HourlyForecast {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -42,6 +41,7 @@ void drawWeatherIconWithOrientation(const GfxRenderer& renderer, const uint8_t*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
StrId getWeatherDescriptionStrId(const int wmoCode) {
|
StrId getWeatherDescriptionStrId(const int wmoCode) {
|
||||||
switch (wmoCode) {
|
switch (wmoCode) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -104,6 +104,99 @@ StrId getWeatherDescriptionStrId(const int wmoCode) {
|
|||||||
return StrId::STR_WEATHER_DESC_UNKNOWN;
|
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<double>(timestamp) - knownNewMoon;
|
||||||
|
double phase = secondsSince / lunarCycle;
|
||||||
|
phase = phase - floor(phase);
|
||||||
|
if (phase < 0.0) phase += 1.0;
|
||||||
|
return static_cast<float>(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<double>(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<time_t>(untilFull * lunarCycle + 0.5);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void WeatherActivity::onEnter() {
|
void WeatherActivity::onEnter() {
|
||||||
@@ -330,7 +423,8 @@ void WeatherActivity::render(RenderLock&&) {
|
|||||||
if (state == State::ERROR) {
|
if (state == State::ERROR) {
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_ERROR_MSG));
|
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());
|
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);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
@@ -503,6 +597,8 @@ void WeatherActivity::renderDailyForecast(int x, int y, int w, int h) {
|
|||||||
const int extraPx = w % numDays;
|
const int extraPx = w % numDays;
|
||||||
|
|
||||||
int cardX = x;
|
int cardX = x;
|
||||||
|
int debugMoonX = x + w - 18;
|
||||||
|
int debugMoonYBase = y + 10;
|
||||||
|
|
||||||
for (int i = 0; i < numDays; i++) {
|
for (int i = 0; i < numDays; i++) {
|
||||||
const auto& day = weatherData.daily[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);
|
snprintf(uvBuf, sizeof(uvBuf), "UV: %.0f", day.uvIndexMax);
|
||||||
int uvWidth = renderer.getTextWidth(SMALL_FONT_ID, uvBuf);
|
int uvWidth = renderer.getTextWidth(SMALL_FONT_ID, uvBuf);
|
||||||
renderer.drawText(SMALL_FONT_ID, cardX + (cardWidth - uvWidth) / 2, textY, 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<int>(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
|
// Card separator
|
||||||
if (i < numDays - 1) {
|
if (i < numDays - 1) {
|
||||||
|
|||||||
Reference in New Issue
Block a user