Profe Tolocka
Published © GPL3+

The Next Holiday

Waiting for the next public holiday? This e-Paper panel uses Seeed Studio hardware and the Seeed_GFX library to show the days left.

IntermediateFull instructions provided4 hours99
The Next Holiday

Things used in this project

Hardware components

XIAO ePaper Display Board(ESP32-S3) - EE04
Seeed Studio XIAO ePaper Display Board(ESP32-S3) - EE04
×1
Seeed Studio Monochrome ePaper Display 5,83"
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Code

Next Holiday

C/C++
/*
 * Author: Ernesto Tolocka (Profe Tolocka)
 * Date: 2026-Apr-11
 * Description: Gadget that shows how many days remain until the next public holiday.
 * Notes: Uses the EE04 board and a 5.83-inch monochrome EPD
 * 
 * License: MIT
 */

#include <WiFi.h>
#include <time.h>
#include "TFT_eSPI.h"

#include <WiFiClientSecure.h>
#include <HTTPClient.h>

#include <ArduinoJson.h>    // Used to parse JSON

#include <esp_sleep.h>    // For deep sleep

#include "bitmap.h"   // Background image

EPaper epaper;

// Wi-Fi network name and password
const char* ssid = "yourSSID";
const char* password = "yourPassword";


// Location
// Modify these values according to your country
const int timeZone = -3;      // Local timezone
const char* country = "AR";   // Country code. See https://date.nager.at/Country

// Date.Nager endpoint
String endpoint = String("https://date.nager.at/api/v3/PublicHolidays/2026/") + country;

// Function to convert a text-format date to epoch
time_t stringDateToEpoch (const char* stringDate) {

  // Converts a string like 2026-02-02 to epoch

  int year,month, day;
  struct tm timeDate = {};   // Start empty

  sscanf(stringDate, "%d-%d-%d", &year, &month, &day);

  timeDate.tm_sec=0;
  timeDate.tm_min=0;
  timeDate.tm_hour=0;
  timeDate.tm_mday=day;
  timeDate.tm_mon=month-1;
  timeDate.tm_year=year-1900;
  
  Serial.println(&timeDate, "Time: %H:%M:%S");
  Serial.println(&timeDate, "Date: %Y-%m-%d");

  // Convert the structure to epoch
  return (mktime(&timeDate));

}

// Function to remove accents
String removeAccents (String s) {
  s.replace("á", "a");
  s.replace("é", "e");
  s.replace("í", "i");
  s.replace("ó", "o");
  s.replace("ú", "u");
  s.replace("ñ", "n");

  s.replace("Á", "A");
  s.replace("É", "E");
  s.replace("Í", "I");
  s.replace("Ó", "O");
  s.replace("Ú", "U");
  s.replace("Ñ", "N");

  return s;
}

// Function to put the ESP32 into deep sleep
void sleepSeconds (uint32_t s) {

  if (s < 60) s = 60;    // Just in case

  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
  
  esp_sleep_enable_timer_wakeup((uint64_t)s * 1000000ULL);
  esp_deep_sleep_start();
}

// Function to connect to WiFi with timeout
bool connectWiFi(uint32_t timeoutMs) {
  
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  uint32_t t0 = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - t0) < timeoutMs) {
    delay(250);
    Serial.print(".");
  }
  Serial.println();
  return WiFi.status() == WL_CONNECTED;
}


void setup() {
  Serial.begin(115200);

  if (!connectWiFi(30000)) {
    Serial.println("WiFi did not connect. Going to sleep for 5 min.");
    sleepSeconds(300);
    return;
  }
  Serial.println("WiFi connected!");


  // Configure and initialize display
  epaper.begin();
  epaper.fillScreen(TFT_WHITE);          // Clear screen
  epaper.setFreeFont(&Yellowtail_32);    // Select font
  epaper.setRotation(0);            
  epaper.setTextDatum(MC_DATUM);         // Set centered alignment

  epaper.setTextSize (1);
  epaper.drawString ("Days Until Next Holiday: ", epaper.width()/2,98);

  // Load background image
  epaper.drawBitmap(0, 0, holidayBack, 648, 480, TFT_BLACK);

  // Show everything
  epaper.update ();

  // Access NTP server and get current date and time (in RTC)
  configTime(timeZone * 3600, 0, "pool.ntp.org");

  // Read time into timeinfo
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo, 10000)) {
    Serial.println("Error reading RTC");
    sleepSeconds (300);
    return;    // Only for clarity
  }

  Serial.println(&timeinfo, "Time: %H:%M:%S");
  Serial.println(&timeinfo, "Date: %Y-%m-%d");

  // Convert to epoch
  time_t epochNow = mktime(&timeinfo);

  // Calculate how long until next midnight

  struct tm tomorrow = timeinfo;

  // Move to tomorrow's midnight
  tomorrow.tm_sec  = 0;
  tomorrow.tm_min  = 0;
  tomorrow.tm_hour = 0;
  
  tomorrow.tm_mday +=1; 
  
  time_t epochTomorrow = mktime(&tomorrow);

  int32_t seconds2Tomorrow = (int32_t)(epochTomorrow - epochNow);

  Serial.print ("Seconds until tomorrow:");
  Serial.println (seconds2Tomorrow);

  // Readjust current time in epoch from midnight
  timeinfo.tm_sec=0;
  timeinfo.tm_min=0;
  timeinfo.tm_hour=0;

  // Set a date for testing
  //timeinfo.tm_mday=30;
  //timeinfo.tm_mon=12-1;

  // Convert to epoch
  time_t epochToday = mktime(&timeinfo);
  
  // Access Nager server to get the list of holidays

  WiFiClientSecure client;
  client.setInsecure(); // Encrypted HTTPS, certificate not validated

  HTTPClient http;

  if (!http.begin(client, endpoint)) {
    Serial.println("http.begin() failed");
    sleepSeconds (300);
    return;    // Only for clarity
  }

  int httpCode = http.GET();
  Serial.printf("HTTP code: %d\n", httpCode);

  if (httpCode != HTTP_CODE_OK) {
    Serial.println("Server response (first 200 chars):");
    String errBody = http.getString();
    Serial.println(errBody.substring(0, 200));
    http.end();
    
    sleepSeconds (300);
    return;    // Only for clarity
  }

  // Download the entire JSON
  String payload = http.getString();
  http.end();

  Serial.printf("Payload length: %d bytes\n", payload.length());

  JsonDocument doc;

  DeserializationError error = deserializeJson(doc, payload);

  if (error) {
    Serial.print("Error parsing JSON: ");
    Serial.println(error.c_str());

    sleepSeconds (300);
    return;    // Only for clarity
  }

  // Iterate through the JSON calculating day difference
  // The first one greater than 0 is the next holiday

  bool found = false; 

  for (int i = 0; i < doc.size(); i++) {
    const char* holiday = doc[i]["date"];
    const char* name = doc[i]["name"];

    Serial.println(holiday);
    Serial.println(name);

    int epochDif = stringDateToEpoch (holiday) - epochToday;
    Serial.println (epochDif);

    if (epochDif >= 0) {   // If it is today, show 0 days

      int daysDif = epochDif / 86400;  // Seconds in a day
      Serial.println (daysDif);
      String holidayName = doc[i]["name"];  // Use "name" or "localName"
   
      // Select size and font for number of days
      epaper.setTextSize (3);
      epaper.setTextFont (7);
    
      epaper.drawNumber (daysDif, epaper.width()/2, 220);

      epaper.setTextSize (1);
      epaper.setFreeFont(&Yellowtail_32);    // Select font

      // Show only the first 38 characters
      epaper.drawString (removeAccents(holidayName.substring (0,38)), epaper.width()/2, 314);
      epaper.update ();

      found = true;
      break;
    }
  }

  if (!found) {
    // There are no more holidays :(
    
    // Select size and font for number of days
    epaper.setTextSize (3);
    epaper.setTextFont (7);
    epaper.drawString ("--", epaper.width()/2, 220);

    epaper.setTextSize (1);
    epaper.setFreeFont(&Yellowtail_32);    // Select font
    epaper.drawString ("No More Holidays!", epaper.width()/2, 314);


    epaper.update ();
  }

  // Everything is ready, go to sleep until tomorrow

  Serial.println ("Going to sleep...");

  // Delay to allow access from the IDE if necessary.
  delay (10000);

  sleepSeconds (seconds2Tomorrow);

}

void loop() {

  // Does nothing

}

Credits

Profe Tolocka
25 projects • 27 followers
Teacher. Maker. Tech Content Creatorhttps://linktr.ee/profetolocka

Comments