WatchyWatchFace/src/WatchFacePages/Weather.cpp

437 lines
No EOL
15 KiB
C++

#include "Weather.h"
#include "../SevenSegment.h"
#include "../Icons.h"
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/FreeSans12pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
#include <HTTPClient.h>
#include <math.h>
#include <sstream>
#include <iomanip>
RTC_DATA_ATTR uint64_t WatchFacePages::Weather::m_lastSyncTime = 0;
RTC_DATA_ATTR uint64_t WatchFacePages::Weather::m_lastSyncAttemptTime = 0;
RTC_DATA_ATTR int WatchFacePages::Weather::m_lastCalculatedDay = 0XFFFFFFFF;
RTC_DATA_ATTR double WatchFacePages::Weather::m_locationLat = DEFAULT_LATITUDE;
RTC_DATA_ATTR double WatchFacePages::Weather::m_locationLon = DEFAULT_LONGITUDE;
RTC_DATA_ATTR char WatchFacePages::Weather::m_locationCity[128];
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_sunriseHour;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_sunriseMinute;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_noonHour;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_noonMinute;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_sunsetHour;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_sunsetMinute;
RTC_DATA_ATTR double WatchFacePages::Weather::m_moonPhase;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_moonBitmap[8 * 64];
RTC_DATA_ATTR double WatchFacePages::Weather::m_currentTemperature;
RTC_DATA_ATTR float WatchFacePages::Weather::m_currentWindSpeed;
RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_currentHumidity;
RTC_DATA_ATTR char WatchFacePages::Weather::m_currentWeatherCondition[4];
WatchFacePages::Weather::Weather(WatchyDisplay & display, WatchFeatures::WatchFeatures & features)
: m_display(display), m_features(features)
{
}
void WatchFacePages::Weather::InitBoot()
{
memcpy(m_locationCity, DEFAULT_CITY_NAME, sizeof(DEFAULT_CITY_NAME));
m_locationCity[sizeof(DEFAULT_CITY_NAME)] = '\0';
}
void WatchFacePages::Weather::InitWake()
{
}
void WatchFacePages::Weather::DrawPage(bool partialRefresh)
{
Resync();
Recalc();
m_display.setFullWindow();
m_display.fillScreen(GxEPD_WHITE);
m_display.setTextColor(GxEPD_BLACK);
bool weatherOutdated = false;
// If it hasn't synced in 24 hours, assume weather is outdated
if (m_lastSyncTime == 0 || m_features.rtc.GetTimestamp() - m_lastSyncTime > 86400) {
weatherOutdated = true;
}
// City name and icon
m_display.setFont(&FreeSans9pt7b);
int16_t x, y;
uint16_t w, h;
m_display.drawBitmap(5, 5, Icons::city, 29, 23, GxEPD_BLACK);
m_display.setCursor(39, 22);
m_display.print(m_locationCity);
// Separator
m_display.fillRect(10, 35, DISPLAY_WIDTH - 20, 2, GxEPD_BLACK);
// Moon
m_display.drawBitmap(200 - 64 - 10, 45, m_moonBitmap, 64, 64, GxEPD_BLACK);
m_display.drawCircle(200 - 64 - 10 + 32, 45 + 32, 32, GxEPD_BLACK);
// Sunrise, solar noon and sunset
m_display.drawBitmap(10, 38, Icons::Weather::sunrise, 30, 30, GxEPD_BLACK);
std::ostringstream sunrise;
sunrise << std::setfill('0') << std::setw(2) << (int)m_sunriseHour << ":" << std::setw(2) << (int)m_sunriseMinute;
m_display.setFont(&FreeSans9pt7b);
m_display.setCursor(45, 56);
m_display.setTextColor(GxEPD_BLACK);
m_display.print(sunrise.str().c_str());
m_display.drawBitmap(10, 62, Icons::Weather::noon, 30, 30, GxEPD_BLACK);
std::ostringstream noon;
noon << std::setfill('0') << std::setw(2) << (int)m_noonHour << ":" << std::setw(2) << (int)m_noonMinute;
m_display.setCursor(45, 80);
m_display.print(noon.str().c_str());
m_display.drawBitmap(10, 87, Icons::Weather::sunset, 30, 30, GxEPD_BLACK);
std::ostringstream sunset;
sunset << std::setfill('0') << std::setw(2) << (int)m_sunsetHour << ":" << std::setw(2) << (int)m_sunsetMinute;
m_display.setFont(&FreeSans9pt7b);
m_display.setCursor(45, 106);
m_display.print(sunset.str().c_str());
// Temperature and weather icon
// Separator
m_display.fillRect(10, 116, DISPLAY_WIDTH - 20, 2, GxEPD_BLACK);
if (weatherOutdated) {
m_display.setFont(&FreeSans9pt7b);
m_display.getTextBounds("No Weather Data", 0, 0, &x, &y, &w, &h);
m_display.setCursor(DISPLAY_WIDTH / 2 - w / 2, 162);
m_display.print("No Weather Data");
} else {
// Unknown, Cloudy, Fog, Heavy Rain, Heavy Showers, Heavy Snow, Heavy Snow Showers, Light Rain, Light Showers, Light Sleet, Light Sleet Showers, Light Snow, Light Snow Showers, Partly Cloudy, Sunny, Thundery Heavy Rain, Thundery Showers, Thundery Snow Showers, Very Cloudy
std::vector<std::string> iconLookupCondition = {"?", "mm", "=", "///", "//", "**", "/*/", "/", ".", "x", "x/", "*", "*/", "m", "o", "/!/", "!/", "*!*", "mmm"};
std::string iconName = m_currentWeatherCondition;
int iconNum = 0;
for (int i = 0; i < iconLookupCondition.size(); i++) {
if (iconName == iconLookupCondition[i]) {
iconNum = i;
break;
}
}
// Get current time to see if it's day or night
tmElements_t currentTime;
m_features.rtc.Get(currentTime);
m_features.rtc.OffsetTime(currentTime, m_features.storage.GetTzOffset());
const unsigned char * iconBitmap = nullptr;
if (currentTime.Hour >= m_sunriseHour && currentTime.Hour < m_sunsetHour) {
const std::vector<const unsigned char * PROGMEM> dayIconLookupBitmap = {
Icons::Weather::alien,
Icons::Weather::day_cloudy,
Icons::Weather::day_fog,
Icons::Weather::day_rain_wind,
Icons::Weather::day_rain_mix,
Icons::Weather::day_snow,
Icons::Weather::day_snow_wind,
Icons::Weather::day_rain,
Icons::Weather::day_showers,
Icons::Weather::day_sleet,
Icons::Weather::day_sleet,
Icons::Weather::day_snow,
Icons::Weather::day_snow,
Icons::Weather::day_cloudy,
Icons::Weather::day_sunny,
Icons::Weather::day_thunderstorm,
Icons::Weather::day_thunderstorm,
Icons::Weather::day_snow_thunderstorm,
Icons::Weather::cloudy
};
iconBitmap = dayIconLookupBitmap[iconNum];
} else {
const std::vector<const unsigned char * PROGMEM> nightIconLookupBitmap = {
Icons::Weather::alien,
Icons::Weather::night_cloudy,
Icons::Weather::night_fog,
Icons::Weather::night_rain_wind,
Icons::Weather::night_rain_mix,
Icons::Weather::night_snow,
Icons::Weather::night_snow_wind,
Icons::Weather::night_rain,
Icons::Weather::night_showers,
Icons::Weather::night_sleet,
Icons::Weather::night_sleet,
Icons::Weather::night_snow,
Icons::Weather::night_snow,
Icons::Weather::night_cloudy,
Icons::Weather::night_clear,
Icons::Weather::night_thunderstorm,
Icons::Weather::night_thunderstorm,
Icons::Weather::night_snow_thunderstorm,
Icons::Weather::cloudy
};
iconBitmap = nightIconLookupBitmap[iconNum];
}
m_display.drawBitmap(5, 125, iconBitmap, 75, 75, GxEPD_BLACK);
m_display.setFont(&FreeSans12pt7b);
std::ostringstream temperature;
temperature << (int)m_currentTemperature << " C";
m_display.getTextBounds(temperature.str().c_str(), 0, 0, &x, &y, &w, &h);
m_display.setCursor(190 - w, 142);
m_display.print(temperature.str().c_str());
// Hacky degree symbol
m_display.fillCircle(190 - 19, 129, 3, GxEPD_BLACK);
m_display.fillCircle(190 - 19, 129, 1, GxEPD_WHITE);
// Wind speed
m_display.drawBitmap(165, 145, Icons::Weather::wind, 30, 30, GxEPD_BLACK);
std::ostringstream windSpeed;
windSpeed << std::fixed << std::setprecision(1) << m_currentWindSpeed << " m/s";
m_display.setFont(&FreeSans9pt7b);
m_display.getTextBounds(windSpeed.str().c_str(), 0, 0, &x, &y, &w, &h);
m_display.setCursor(160 - w, 165);
m_display.print(windSpeed.str().c_str());
// Humidity
m_display.drawBitmap(165, 170, Icons::Weather::humidity, 30, 30, GxEPD_BLACK);
std::ostringstream humidity;
humidity << (int)m_currentHumidity;
m_display.getTextBounds(humidity.str().c_str(), 0, 0, &x, &y, &w, &h);
m_display.setCursor(160 - w, 190);
m_display.print(humidity.str().c_str());
}
m_display.display(partialRefresh);
}
void WatchFacePages::Weather::Resync()
{
uint64_t currentTime = m_features.rtc.GetTimestamp();
if(m_lastSyncTime > 0 && currentTime - m_lastSyncTime < WEATHER_UPDATE_INTERVAL) {
return;
}
if (currentTime - m_lastSyncAttemptTime < WEATHER_UPDATE_BACKOFF) {
return;
}
if(!m_features.wifi.Connect()) {
m_lastSyncAttemptTime = currentTime;
return;
}
HTTPClient client;
client.setConnectTimeout(3000); // 3 second max timeout
// Get geolocation from IP
if (DO_GEOLOCATION) {
client.begin("http://ip-api.com/line/");
int httpCode = client.GET();
if (httpCode != 200) {
m_features.wifi.Disconnect();
m_lastSyncAttemptTime = currentTime;
return;
}
String payload = client.getString();
client.end();
std::string reponse(payload.c_str());
// Split into lines
std::vector<std::string> lines;
std::string::size_type pos = 0;
std::string::size_type prev = 0;
char delimiter = '\n';
while ((pos = reponse.find(delimiter, prev)) != std::string::npos) {
lines.push_back(reponse.substr(prev, pos - prev));
prev = pos + 1;
}
// Check if we got enough lines
if (lines.size() < 14) {
m_features.wifi.Disconnect();
m_lastSyncAttemptTime = currentTime;
return;
}
std::string location = lines[5];
location = location.substr(0, 127);
m_locationCity[location.length()] = '\0';
memcpy(m_locationCity, location.c_str(), location.length());
m_locationLat = std::stof(lines[7]);
m_locationLon = std::stof(lines[8]);
// Force recalculation next time
m_lastCalculatedDay = 0xFFFFFFFF;
}
std::ostringstream url;
url << "http://wttr.in/" << m_locationLat << "," << m_locationLon << "?0Q&format=%x:%t:%h:%w";
// Grr. wttr.in does a redirect to HTTPS if your agent isn't curl
client.setUserAgent("curl/8.0.1");
Serial.println(url.str().c_str());
client.begin(url.str().c_str());
int httpCode = client.GET();
m_features.wifi.Disconnect();
if (httpCode != 200) {
m_lastSyncAttemptTime = currentTime;
return;
}
String payload = client.getString();
client.end();
std::string reponse(payload.c_str());
// Split on :
std::vector<std::string> parts;
std::string::size_type pos = 0;
std::string::size_type prev = 0;
char delimiter = ':';
while ((pos = reponse.find(delimiter, prev)) != std::string::npos) {
parts.push_back(reponse.substr(prev, pos - prev));
prev = pos + 1;
}
parts.push_back(reponse.substr(prev));
if (parts.size() != 4) {
m_lastSyncAttemptTime = currentTime;
return;
}
memcpy(m_currentWeatherCondition, parts[0].c_str(), parts[0].length());
m_currentWeatherCondition[parts[0].length()] = '\0';
std::string temperature = parts[1].substr(1, parts[1].length() - 4);
Serial.println(temperature.c_str());
m_currentTemperature = std::stoi(temperature);
std::string humidity = parts[2].substr(0, parts[2].length() - 1);
m_currentHumidity = std::stoi(humidity);
std::string windSpeed = parts[3].substr(3, parts[3].length() - 7);
float windSpeedKmh = std::stof(windSpeed);
// Convert to m/s
m_currentWindSpeed = windSpeedKmh * 0.277778;
m_features.wifi.Disconnect();
m_lastSyncTime = currentTime;
}
void WatchFacePages::Weather::Recalc()
{
tmElements_t tm;
m_features.rtc.Get(tm);
if (m_lastCalculatedDay == tm.Day) {
return;
}
unsigned int dayOfYear = m_features.rtc.GetDayOfYear(tm);
double denominator = tm.Year / 4 ? 366.0 : 365.0;
double fractionalYear = (2.0 * M_PI / denominator) * (dayOfYear - 1 + ((0 - 12.0) / 24.0));
double eqtime = 229.18 * (0.000075 + 0.001868 * cos(fractionalYear) - 0.032077 * sin(fractionalYear) - 0.014615 * cos(2.0 * fractionalYear) - 0.040849 * sin(2.0 * fractionalYear));
double declination = 0.006918 - 0.399912 * cos(fractionalYear) + 0.070257 * sin(fractionalYear) - 0.006758 * cos(2.0 * fractionalYear) + 0.000907 * sin(2.0 * fractionalYear) - 0.002697 * cos(3.0 * fractionalYear) + 0.00148 * sin(3.0 * fractionalYear);
const double latitudeRadians = (double)m_locationLat * M_PI / 180.0;
const double longitudeRadians = (double)m_locationLon * M_PI / 180.0;
const double sunriseSunsetAngle = 1.5853349200000815;
double timeOffset = eqtime + 4.0 * (double)m_locationLon - 60.0;
double trueSolarTime = tm.Hour * 60.0 + tm.Minute + tm.Second / 60.0 + timeOffset;
double hourAngle = acos(cos(sunriseSunsetAngle) / (cos(latitudeRadians) * cos(declination)) - tan(latitudeRadians) * tan(declination));
hourAngle = hourAngle * 180.0 / M_PI;
double sunrise = 720 - 4.0 * ((double)m_locationLon + hourAngle) - eqtime;
double sunset = 720 - 4.0 * ((double)m_locationLon - hourAngle) - eqtime;
double noon = 720 - 4.0 * ((double)m_locationLon) - eqtime;
sunrise += m_features.storage.GetTzOffset() / 60;
sunset += m_features.storage.GetTzOffset() / 60;
noon += m_features.storage.GetTzOffset() / 60;
m_sunriseHour = sunrise / 60;
m_sunriseMinute = sunrise - m_sunriseHour * 60;
m_sunsetHour = sunset / 60;
m_sunsetMinute = sunset - m_sunsetHour * 60;
m_noonHour = noon / 60;
m_noonMinute = noon - m_noonHour * 60;
tmElements_t newMoon;
newMoon.Year = 2023 - 1970;
newMoon.Month = 5;
newMoon.Day = 19;
unsigned int daysDifference = m_features.rtc.DaysDifference(newMoon, tm);
m_moonPhase = fmod((double)daysDifference, 29.530588853) / 29.530588853;
// Draw a 1-bit bitmap with the moon phase
bool pixels[64 * 64];
for (int i = 0; i < 64 * 64; i++) {
pixels[i] = false;
}
for (int x = 0; x < 64; x++) {
for (int y = 0; y < 64; y++) {
float distance = sqrt(powf(fabsf(x - 32), 2) + powf(fabsf(y - 32), 2));
if (distance < 32) {
float inclination = asinf(((float)y - 32.0f) / 32.0f);
float azimuth = acosf((float)(x - 32) / (-32.0f * cosf(inclination)));
azimuth += M_PI;
azimuth += (m_moonPhase) * M_PI * 2.0f;
while (azimuth > M_PI * 2.0f) {
azimuth -= M_PI * 2.0f;
}
while (azimuth < 0) {
azimuth += M_PI * 2.0f;
}
if (azimuth > M_PI) {
pixels[x + y * 64] = true;
}
} else {
pixels[x + y * 64] = false;
}
}
}
// Needs to be split into two parts because pio does too aggressive optimisation
for (unsigned int y = 0; y < 32; y++) {
for (unsigned int x = 0; x < 8; x++) {
uint8_t value = 0;
for (unsigned int i = 0; i < 8; i++) {
value |= pixels[(x * 8 + i) + (y * 64)] << (7 - i);
}
m_moonBitmap[x + y * 8] = value;
}
}
for (unsigned int y = 32; y < 64; y++) {
for (unsigned int x = 0; x < 8; x++) {
uint8_t value = 0;
for (unsigned int i = 0; i < 8; i++) {
value |= pixels[(x * 8 + i) + (y * 64)] << (7 - i);
}
m_moonBitmap[x + y * 8] = value;
}
}
m_lastCalculatedDay = tm.Day;
}