2024-12-05 22:21:46 +00:00

492 lines
16 KiB
C++

#define USE_HSPI_FOR_EPD
// base class GxEPD2_GFX can be used to pass references or pointers to the display instance as parameter, uses ~1.2k more code
// enable or disable GxEPD2_GFX base class
#define ENABLE_GxEPD2_GFX 0
#include <GxEPD2_BW.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#define GxEPD2_DISPLAY_CLASS GxEPD2_BW
#define GxEPD2_DRIVER_CLASS GxEPD2_750_GDEY075T7 // GDEY075T7 800x480, UC8179 (GD7965), (FPC-C001 20.08.20)
// somehow there should be an easier way to do this
#define GxEPD2_BW_IS_GxEPD2_BW true
#define GxEPD2_1248_IS_GxEPD2_1248 true
#define IS_GxEPD(c, x) (c##x)
#define IS_GxEPD2_BW(x) IS_GxEPD(GxEPD2_BW_IS_, x)
#define IS_GxEPD2_1248(x) IS_GxEPD(GxEPD2_1248_IS_, x)
#if defined(ESP32)
#define MAX_DISPLAY_BUFFER_SIZE 65536ul // e.g.
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
#elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8))
#elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ 15, /*DC=*/ 27, /*RST=*/ 26, /*BUSY=*/ 25));
#endif
#if defined(ESP32) && defined(USE_HSPI_FOR_EPD)
SPIClass hspi(HSPI);
#endif
// <Local Includes>
#include "bitmap.h"
#include "config_loader.h"
#include "weather.h"
#include "helper_fn.h"
#include "nextcloud_cal.h"
// <Fonts>
#include <Fonts/FreeMono9pt7b.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeMonoBold12pt7b.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold24pt7b.h>
// <Connectivity>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <time.h>
#include <HTTPClient.h>
#include "tls_certificates.h"
#include <ArduinoJson.h>
// Globals
Config conf;
ActiveFeatures activeFeatures;
int loopIteration = 0;
void setup()
{
Serial.begin(115200);
delay(1000);
Serial.println();
Serial.println("setup");
// *** special handling for Waveshare ESP32 Driver board *** //
// ********************************************************* //
#if defined(ESP32) && defined(USE_HSPI_FOR_EPD)
hspi.begin(13, 12, 14, 15); // remap hspi for EPD (swap pins)
display.epd2.selectSPI(hspi, SPISettings(4000000, MSBFIRST, SPI_MODE0));
#endif
// *** end of special handling for Waveshare ESP32 Driver board *** //
// **************************************************************** //
display.init(115200);
// first update should be full refresh
delay(1000);
bootLogo();
// Do Selftest and Initiation here... Check for WiFi, check NTP Update, check connectivity.
int text_y = 155;
int line_height = 20;
bootMsg("v0.0.1-alpha", text_y);
text_y += line_height;
bootMsg("https://git.fjla.uk/fred.boniface/esphub", text_y);
text_y += line_height;
conf = loadConfig();
if (connect_WiFi(conf.ssid.c_str(), conf.wifiPass.c_str())) {
bootMsg("WiFi Connection: OK", text_y);
activeFeatures.network = true;
} else {
bootMsg("WiFi Connection: FAIL", text_y);
activeFeatures.network = false;
}
text_y += line_height;
if (update_time()) {
bootMsg("Network Time Update: OK", text_y);
} else {
bootMsg("Network Time Update: FAIL", text_y);
}
text_y += line_height;
if (checkUrlPing(conf.haUrl)) { // FAILS FOR SOME REASON -- DEBUG ME
bootMsg("Home Assistant URL Reachable: OK", text_y);
text_y += line_height;
if (haAnybodyHome() < 0) {
bootMsg("Home Assistant Home Entity Check: FAIL", text_y);
activeFeatures.homeassistant = false;
} else {
activeFeatures.homeassistant = true;
bootMsg("Home Assistant Home Entity Check: OK", text_y);
}
text_y += line_height;
} else {
bootMsg("Home Assistant URL Reachable: FAIL", text_y);
text_y += line_height;
activeFeatures.homeassistant = false;
}
if (getWeather(conf.latitude, conf.longitude).latitude) {
bootMsg("Fetch Weather Data: OK", text_y);
activeFeatures.weather = true;
} else {
bootMsg("Fetch Weather Data: FAIL", text_y);
activeFeatures.weather = false;
}
text_y += line_height;
// Check Nextcloud reachable here
// Nextcloud dev check:
StartEnd timestamp = calculateStartEnd();
String cal_test = downloadICSFile(conf.calUrls[0], String(timestamp.start), String(timestamp.end), conf.calUser, conf.calKey);
Serial.println(cal_test);
displayActiveFeatures(activeFeatures);
bootMsg("Weather Icons sourced from flaticon.com", text_y);
text_y += 2 * line_height;
bootMsg("Starting main loop...", text_y);
}
void loop() {
// Loop Vars
WeatherData weather;
// CalendarData calendar;
display.setFullWindow(); // Set Full Update Mode
if (!activeFeatures.network ||
(activeFeatures.homeassistant && haAnybodyHome() < 1)) {
displayFullScreenArt(); // Show artwork if no network or nobody is home
} else {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Unable to get time, some features will be buggy");
}
// Fetch weather data if it is active
if (activeFeatures.weather) {
weather = getWeather(conf.latitude, conf.longitude);
}
// Fetch calendar data if it is active
if (activeFeatures.calendar) {
// FETCH CALENDAR DATA HERE
}
Serial.printf("Loop iteration %d\n", loopIteration);
// Update the display
do {
display.fillScreen(GxEPD_WHITE);
display.fillRect(0, 0, 800, 80, GxEPD_BLACK); // Header background
displayHeaderText(timeinfo);
if (activeFeatures.weather && weather.latitude != 0 && weather.longitude != 0) {
displayWeather(weather);
}
if (!activeFeatures.calendar) { // Temporarily inverse the statenment during tests
displayCalendar(timeinfo);
}
} while (display.nextPage());
}
display.hibernate();
delay(3600000); // Sleep for 60 minutes
loopIteration++;
}
// Places the boot logo in position at the top of the screen
void bootLogo() {
constexpr int img_x = (800 - 648) / 2;
display.setFullWindow();
display.firstPage();
do
{
display.fillScreen(GxEPD_WHITE);
display.drawBitmap(img_x, 15, esphub_lrg, 648, 120, GxEPD_BLACK);
}
while (display.nextPage());
}
// Prints a one-line message to the screen, aligned with the boot logo at `y_pos`
void bootMsg(const char* msg, const uint16_t y_pos) {
constexpr uint16_t x_pos = 76; // Manually calculated based on values in bootLogo();
// Define the partial window area
display.setPartialWindow(0, 0, display.width(), display.height());
display.setFont(&FreeMono9pt7b); // Set font
display.setTextColor(GxEPD_BLACK); // Set color
display.setCursor(x_pos, y_pos); // Set the cursor for printing
display.print(msg); // Print the message
display.nextPage(); // Only needed for e-ink paginated updates
}
void displayActiveFeatures(const ActiveFeatures activeFeatures) {
constexpr uint16_t x_pos = 650; // X Position
uint16_t y_pos = 180; // Y Position
uint16_t line_height = 17;
display.setPartialWindow(0, 0, display.width(), display.height());
display.setFont(&FreeSans9pt7b); // Set font
display.setTextColor(GxEPD_WHITE); // Set color
//display.firstPage();
do {
// Convert booleans to "Y"/"N"
auto boolToYN = [](bool value) -> const char* { return value ? "Y" : "N"; };
display.fillRect(x_pos - line_height, y_pos - line_height, 170, 7 * line_height, GxEPD_BLACK);
display.setCursor(x_pos, y_pos);
display.print("Enabled Features\n");
display.setCursor(x_pos, y_pos + 2 * line_height);
display.print("Network: ");
display.print(boolToYN(activeFeatures.network));
display.setCursor(x_pos, y_pos + 3 * line_height);
display.print("HASS: ");
display.print(boolToYN(activeFeatures.homeassistant));
display.setCursor(x_pos, y_pos + 4 * line_height);
display.print("Calendar: ");
display.print(boolToYN(activeFeatures.calendar));
display.setCursor(x_pos, y_pos + 5 * line_height);
display.print("Weather: ");
display.print(boolToYN(activeFeatures.weather));
}
while (display.nextPage());
}
bool connect_WiFi(String ssid, String pass) {
Serial.print("Connecting to SSID ");
Serial.println(ssid);
WiFi.begin(ssid, pass);
int retries = 10;
while (WiFi.status() != WL_CONNECTED && retries > 0) {
delay(1000);
retries --;
}
if (WiFi.status() != WL_CONNECTED) {
return false;
}
return true;
}
bool update_time() {
configTime(0, 0, "time.fjla.net", "pool.ntp.org");
struct tm timeinfo;
if (!getLocalTime(&timeinfo, 20000)) {
Serial.println("Failed to obtain time");
return false;
}
return true;
}
bool checkUrlPing(const String& url) {
WiFiClientSecure *client = new WiFiClientSecure;
if (client) {
client -> setCACert(letsencrypt_root_ca);
{
HTTPClient https;
Serial.println("Starting HTTPS Request");
if (https.begin(*client, url)) {
int httpCode = https.GET();
Serial.printf("HTTPS GET code: %d\n", httpCode);
if (httpCode > 0) {
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
https.end();
delete client;
return true;
}
}
}
https.end();
delete client;
}
}
return false;
}
float haAnybodyHome() {
WiFiClientSecure *client = new WiFiClientSecure;
if (client) {
client -> setCACert(letsencrypt_root_ca);
String url = conf.haUrl + "/api/states/" + conf.homeEntity;
Serial.println("URL: " + url);
HTTPClient https;
Serial.println("Starting HTTPS Request to Home Assistant");
if (https.begin(*client, url)) {
https.addHeader("Authorization", "Bearer " + conf.haKey);
int httpCode = https.GET();
Serial.printf("Home Assistant GET Code: %d\n", httpCode);
if (httpCode == HTTP_CODE_OK) {
String payload = https.getString();
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, payload);
if (error) {
Serial.println("Failed to Parse JSON Response");
https.end();
delete client;
return -0.1;
}
float state = doc["state"].as<float>();
Serial.printf("Entity %s state: %f\n", conf.homeEntity.c_str(), state);
https.end();
delete client;
return state;
}
return -0.2;
}
}
}
String getGreeting(struct tm timeinfo) {
if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time from RTC");
return "Hello";
}
int currentHour = timeinfo.tm_hour; // Get the current hour
if (currentHour >= 5 && currentHour < 12) {
return "Good morning";
} else if (currentHour >= 12 && currentHour < 18) {
return "Good afternoon";
} else if (currentHour >= 18 && currentHour < 22) {
return "Good evening";
} else {
return "Good night";
}
}
String getDate(struct tm timeinfo) {
char dateStr[11];
strftime(dateStr, sizeof(dateStr), "%d/%m/%Y", &timeinfo);
return String(dateStr); // Convert the C string to a String object and return it
}
String getDayOfWeek(struct tm timeinfo) {
char dayStr[10];
strftime(dayStr, sizeof(dayStr), "%A", &timeinfo);
return String(dayStr);
}
void displayHeaderText(struct tm timeinfo) {
display.setTextColor(GxEPD_WHITE);
// Define vars for bound calculations
int16_t tbx, tby;
uint16_t tbw, tbh;
// Get Greeting and notable day
String greeting = getGreeting(timeinfo);
String notableDay = getNotableDay(timeinfo.tm_mon + 1, timeinfo.tm_mday);
String date = getDate(timeinfo);
String dayOfWeek = getDayOfWeek(timeinfo);
display.setFont(&FreeSansBold24pt7b); // Set Greeting Font
// Calculate Bounds for Greeting
display.getTextBounds(greeting, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t gx = (display.width() - tbw) / 2;
uint16_t gy = 42;
// Print Greeting
display.setCursor(gx, gy);
display.print(greeting);
display.setFont(&FreeMonoBold9pt7b); // Set Notable Day Font
// Calculate Bounds for Notable Day
display.getTextBounds(notableDay, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t nx = (display.width() - tbw) / 2;
uint16_t ny = 70;
// Print Notable Day
display.setCursor(nx, ny);
display.print(notableDay);
// Print date
display.setCursor(680, 25);
display.print(date);
display.setCursor(680, 60);
display.print(dayOfWeek);
}
void displayWeather(WeatherData weather) {
display.setFont(&FreeMonoBold12pt7b);
display.setTextColor(GxEPD_WHITE);
// Display Current Temperature
display.setCursor(14, 23);
display.print(String((int)round(weather.current.temperature_2m)) + "°C");
display.setFont(&FreeMonoBold9pt7b); // Set font for remaining prints
// Display Max Temp
display.drawBitmap(5, 27, hi, 22, 22, GxEPD_WHITE); // Draw 'max temp' bitmap
display.setCursor(23, 44);
display.print(String((int)round(weather.daily.temperature_2m_max)) + "°C");
// Display Min Temp
display.drawBitmap(5, 50, lo, 22, 22, GxEPD_WHITE); // Draw 'min temp' bitmap
display.setCursor(23, 64);
display.print(String((int)round(weather.daily.temperature_2m_min)) + "°C");
// Calculate the width of the longest temperature string & adjust x-position of weather bitmap and display
int longestTempWidth = 0;
int tempMaxWidth = String((int)round(weather.daily.temperature_2m_max)).length();
int tempMinWidth = String((int)round(weather.daily.temperature_2m_min)).length();
longestTempWidth = max(tempMaxWidth, tempMinWidth);
int weather_x = 58; // Default position
if (longestTempWidth > 2) {
weather_x += (longestTempWidth > 3 ? 9 : 5); // 67 is the correct X value if more than three. =2 will be halfway btwn Default and 67.
}
display.drawBitmap(weather_x, 8, getWeatherBitmap(weather.daily.weather_code), 64, 64, GxEPD_WHITE); // Draw Weather Bitmap
// Display wind & gust speed
display.drawBitmap(126, 8, wind, 22, 22, GxEPD_WHITE); // Draw 'wind speed' bitmap
display.setCursor(150, 23);
display.print("km/h");
display.setCursor(133, 45);
display.print(String(weather.daily.wind_speed_10m_max));
display.setCursor(133, 64);
display.print(String(weather.daily.wind_gusts_10m_max));
}
void displayFullScreenArt() {
display.setFullWindow();
int randomIdx = random(0, full_art_array_size);
display.firstPage();
do
{
display.fillScreen(GxEPD_WHITE);
display.drawBitmap(0, 0, full_art_array[randomIdx], 800, 480, GxEPD_BLACK);
}
while (display.nextPage());
}
void displayCalendar(struct tm timeinfo) {
const int cal_y = 80;
const int cal_head = 30;
const int cal_margin = 30;
const char* weekDays[] = {"S", "M", "T", "W", "T", "F", "S"};
const char* calNames[] = {"Shared", "Fred", "Jade", "Lucy", "Ava-Rose"};
// Draw Calendar Lines
display.drawLine(0, cal_y + cal_head, 480, cal_y + cal_head, GxEPD_BLACK); // Bottom of Header
for (int y = cal_y + cal_head; y <= 480; y += 74) { // Loop to separate into five rows
display.drawLine(0, y, 800, y, GxEPD_BLACK);
}
display.drawLine(cal_margin, cal_y, cal_margin, 480, GxEPD_BLACK); // Right of margin
for (int x = cal_margin; x <= 800; x += 154) { // Loop to separate into seven columns
display.drawLine(x, cal_y, x, 480, GxEPD_BLACK);
}
// Print Calendar Labels
display.setFont(&FreeMonoBold12pt7b);
display.setTextColor(GxEPD_BLACK);
for (int i = 0; i < 5; i++) {
display.setCursor(cal_margin + i * 154 + 10, cal_y + 23);
display.print(calNames[i]);
}
// Print Day/Date
for (int i = 0; i < 5; i++) {
int dayOfWk = (timeinfo.tm_wday + i) % 7;
display.setCursor(3, cal_head + i * 74 + 96);
display.print(weekDays[dayOfWk]);
}
}